diff --git a/proto/src/app/api/contact/route.ts b/proto/src/app/api/contact/route.ts index a5e7baa..01719ef 100755 --- a/proto/src/app/api/contact/route.ts +++ b/proto/src/app/api/contact/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { getCollection } from '@/lib/mongodb' +import { getPayload } from 'payload' +import config from '@payload-config' interface ContactFormData { name: string @@ -9,16 +10,6 @@ interface ContactFormData { gdprConsent: boolean } -interface ContactSubmission { - name: string - email: string - subject: string - message: string - gdprConsent: true - status: 'new' - createdAt: Date -} - // Simple spam protection - rate limiting by IP const rateLimitMap = new Map() const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute @@ -127,20 +118,21 @@ export async function POST(request: NextRequest) { ) } - const submissions = await getCollection('contact_submissions') - const submission: ContactSubmission = { - ...sanitizedData, - gdprConsent: true, - status: 'new', - createdAt: new Date(), - } - const result = await submissions.insertOne(submission) - + const payload = await getPayload({ config }) + const submission = await payload.create({ + collection: 'contact-submissions', + data: { + ...sanitizedData, + gdprConsent: true, + status: 'new', + }, + }) + return NextResponse.json( - { + { message: 'Üzenet sikeresen elküldve!', - timestamp: submission.createdAt.toISOString(), - submissionId: result.insertedId.toHexString() + timestamp: submission.createdAt, + submissionId: String(submission.id) }, { status: 200 } ) diff --git a/proto/src/app/api/contact/route.unit.test.ts b/proto/src/app/api/contact/route.unit.test.ts index e66212c..756c4f2 100755 --- a/proto/src/app/api/contact/route.unit.test.ts +++ b/proto/src/app/api/contact/route.unit.test.ts @@ -3,8 +3,9 @@ * These tests focus on testing the business logic without complex mocking */ import { POST } from './route' +import { getPayload } from 'payload' -const mockInsertOne = jest.fn() +const mockCreate = jest.fn() // Mock the logger to avoid complex setup jest.mock('@/lib/logger', () => ({ @@ -15,16 +16,28 @@ jest.mock('@/lib/logger', () => ({ }) })) -jest.mock('@/lib/mongodb', () => ({ - getCollection: jest.fn(async () => ({ insertOne: mockInsertOne })), +// WHY mock the resolved relative path instead of the '@payload-config' alias: +// next/jest's SWC transform rewrites the tsconfig path alias to a real +// relative specifier at transform time (Jest itself doesn't understand +// tsconfig `paths`), so a `jest.mock('@payload-config', ...)` never actually +// intercepts what route.ts ends up requiring — it silently falls through to +// the real proto/src/payload.config.ts (mongooseAdapter, Users/LegalPages/etc +// imports), which needs a live MongoDB and PAYLOAD_SECRET, exactly what these +// tests avoid. The route only ever passes this value through to the (also +// mocked) getPayload(), so its actual shape doesn't matter here. +jest.mock('../../../payload.config', () => ({ __esModule: true, default: {} })) + +jest.mock('payload', () => ({ + getPayload: jest.fn(async () => ({ create: mockCreate })), })) describe('/api/contact Unit Tests', () => { beforeEach(() => { - mockInsertOne.mockReset() + mockCreate.mockReset() + ;(getPayload as jest.Mock).mockClear() }) - describe('MongoDB persistence', () => { + describe('Payload persistence', () => { const validData = { name: 'Test User', email: 'test@example.com', @@ -34,7 +47,7 @@ describe('/api/contact Unit Tests', () => { } it('persists a valid submission before reporting success', async () => { - mockInsertOne.mockResolvedValue({ insertedId: { toHexString: () => 'submission-123' } }) + mockCreate.mockResolvedValue({ id: 'submission-123', createdAt: '2026-09-12T00:00:00.000Z' }) const request = { headers: new Headers({ 'x-forwarded-for': 'persistence-success' }), json: async () => validData, @@ -45,15 +58,17 @@ describe('/api/contact Unit Tests', () => { expect(response.status).toBe(200) expect(body.submissionId).toBe('submission-123') - expect(mockInsertOne).toHaveBeenCalledWith(expect.objectContaining({ - ...validData, - status: 'new', - createdAt: expect.any(Date), - })) + expect(mockCreate).toHaveBeenCalledWith({ + collection: 'contact-submissions', + data: expect.objectContaining({ + ...validData, + status: 'new', + }), + }) }) it('returns a server error when persistence fails', async () => { - mockInsertOne.mockRejectedValue(new Error('MongoDB unavailable')) + mockCreate.mockRejectedValue(new Error('MongoDB unavailable')) const request = { headers: new Headers({ 'x-forwarded-for': 'persistence-failure' }), json: async () => validData, @@ -62,7 +77,7 @@ describe('/api/contact Unit Tests', () => { const response = await POST(request) expect(response.status).toBe(500) - expect(mockInsertOne).toHaveBeenCalledTimes(1) + expect(mockCreate).toHaveBeenCalledTimes(1) }) it('rejects an over-long message before persisting', async () => { @@ -75,7 +90,7 @@ describe('/api/contact Unit Tests', () => { const response = await POST(request) expect(response.status).toBe(400) - expect(mockInsertOne).not.toHaveBeenCalled() + expect(mockCreate).not.toHaveBeenCalled() }) }) diff --git a/proto/src/collections/ContactSubmissions.ts b/proto/src/collections/ContactSubmissions.ts new file mode 100644 index 0000000..7785379 --- /dev/null +++ b/proto/src/collections/ContactSubmissions.ts @@ -0,0 +1,59 @@ +import type { CollectionConfig } from 'payload' + +/** + * MITHOME-94 — a /api/contact beérkező üzeneteinek tárolója. + * + * WHY nincs egyedi `access` blokk: a Payload alapértelmezése (csak + * bejelentkezett usernek engedélyezett minden művelet) itt pont a kívánt + * viselkedés — az ügyfél az admin felületen látja a beérkezéseket, kívülről + * senki nem olvashatja/írhatja a REST API-n keresztül. A src/app/api/contact + * route.ts a Local API-t használja (`payload.create`), ami alapból + * `overrideAccess: true`-val fut — ez a szerver-oldali írás nem ütközik az + * access control-lal, csak a publikus REST hozzáférés van letiltva. + * + * WHY nincs mezőnkénti hosszkorlát/validáció itt duplikálva: a rate limiting + * és a bemenet-validáció (hossz, email formátum, spam-szűrés) szándékosan + * az API route-on marad (lásd a ticket leírását) — ez a collection csak a + * már ellenőrzött adat tárolója. + * + * WHY nincs versions/drafts: ez tényadat (egy beérkezett üzenet), nem + * szerkesztendő tartalom — a `status` mező követi a feldolgozás állapotát. + */ +export const ContactSubmissions: CollectionConfig = { + slug: 'contact-submissions', + admin: { + useAsTitle: 'subject', + defaultColumns: ['subject', 'name', 'email', 'status', 'createdAt'], + group: 'Kapcsolatfelvételek', + }, + defaultSort: '-createdAt', + fields: [ + { name: 'name', type: 'text', required: true }, + { name: 'email', type: 'text', required: true }, + { name: 'subject', type: 'text', required: true }, + { name: 'message', type: 'textarea', required: true }, + { + name: 'gdprConsent', + type: 'checkbox', + required: true, + admin: { + description: 'Az űrlapon elfogadott adatkezelési hozzájárulás — a route csak true értékkel enged menteni.', + }, + }, + { + name: 'status', + type: 'select', + required: true, + defaultValue: 'new', + options: [ + { label: 'Új', value: 'new' }, + { label: 'Elolvasva', value: 'read' }, + { label: 'Megválaszolva', value: 'replied' }, + { label: 'Archiválva', value: 'archived' }, + ], + admin: { + description: 'Kézzel karbantartott feldolgozási állapot — a route mindig "new"-ként hozza létre.', + }, + }, + ], +} diff --git a/proto/src/lib/mongodb.test.ts b/proto/src/lib/mongodb.test.ts index ce945c3..62d64b8 100755 --- a/proto/src/lib/mongodb.test.ts +++ b/proto/src/lib/mongodb.test.ts @@ -93,16 +93,6 @@ describe('MongoDB Connection (Unit Tests)', () => { expect(typeof result.collection).toBe('function') }) - it('should return collection from database', async () => { - const { getCollection } = await import('./mongodb') - - const collection = await getCollection('test_collection') - - expect(collection).toBeDefined() - expect(mockDb.collection).toHaveBeenCalledWith('test_collection') - expect(typeof collection.findOne).toBe('function') - }) - it('should check MongoDB connection successfully', async () => { const { checkMongoConnection } = await import('./mongodb') diff --git a/proto/src/lib/mongodb.ts b/proto/src/lib/mongodb.ts index beba78b..24926c3 100755 --- a/proto/src/lib/mongodb.ts +++ b/proto/src/lib/mongodb.ts @@ -1,4 +1,10 @@ -import { MongoClient, Db, Collection, Document } from 'mongodb' +import { MongoClient, Db } from 'mongodb' + +// WHY this file only exposes getDb/checkMongoConnection now: it used to also +// export getCollection(), used by src/app/api/contact/route.ts for a raw, +// Payload-external `contact_submissions` collection. MITHOME-94 moved that +// write to a proper Payload collection (ContactSubmissions) via the Local +// API — getDb() itself stays alive only for /api/health's connectivity check. const options = { maxPoolSize: 10, @@ -50,11 +56,6 @@ export async function getDb(): Promise { return client.db(process.env.MONGODB_DB || 'mozdit') } -export async function getCollection(collectionName: string): Promise> { - const db = await getDb() - return db.collection(collectionName) -} - // Health check for MongoDB connection export async function checkMongoConnection(): Promise { try { diff --git a/proto/src/payload.config.ts b/proto/src/payload.config.ts index bdbc74f..459b143 100644 --- a/proto/src/payload.config.ts +++ b/proto/src/payload.config.ts @@ -17,6 +17,10 @@ * MITHOME-120: admin gyorskeresés (QuickSearch) az admin.components.header * slotba regisztrálva — teljes szöveges keresés minden Global/Collection * mezőjében, mindkét locale-ban. + * MITHOME-94: ContactSubmissions collection — a /api/contact korábban egy + * nyers, Payload-on kívüli Mongo collection-be (`contact_submissions`, + * proto/src/lib/mongodb.ts) írt, amit az ügyfél nem látott sehol az admin + * felületen. Mostantól Payload collection, a route.ts a Local API-n ír bele. */ import path from 'path' import { fileURLToPath } from 'url' @@ -29,6 +33,7 @@ import { Users } from './collections/Users' import { LegalPages } from './collections/LegalPages' import { Media } from './collections/Media' import { Partners } from './collections/Partners' +import { ContactSubmissions } from './collections/ContactSubmissions' import { Home } from './globals/Home' import { About } from './globals/About' import { Services } from './globals/Services' @@ -55,7 +60,7 @@ export default buildConfig({ editor: lexicalEditor(), - collections: [Users, LegalPages, Media, Partners], + collections: [Users, LegalPages, Media, Partners, ContactSubmissions], globals: [Home, About, Services, Contact, Common],