import * as z from 'zod'; import { apiError, apiSuccess, emailSchema, requiredString, } from '@kit/next/route-helpers'; import { checkRateLimit, getClientIp } from '@kit/next/routes/rate-limit'; import { getLogger } from '@kit/shared/logger'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; const MembershipApplySchema = z.object({ accountId: requiredString('Konto-ID'), firstName: requiredString('Vorname'), lastName: requiredString('Nachname'), email: emailSchema, phone: z.string().optional(), street: z.string().optional(), postalCode: z.string().optional(), city: z.string().optional(), dateOfBirth: z.string().optional(), message: z.string().optional(), captchaToken: z.string().optional(), }); // Rate limits const MAX_PER_IP = 5; const MAX_PER_ACCOUNT = 20; const WINDOW_MS = 60 * 60 * 1000; // 1 hour export async function POST(request: Request) { const logger = await getLogger(); try { // Rate limit by IP const ip = getClientIp(request); const ipLimit = checkRateLimit( `membership-apply:ip:${ip}`, MAX_PER_IP, WINDOW_MS, ); if (!ipLimit.allowed) { return apiError( 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.', 429, ); } const body = await request.json(); const parsed = MembershipApplySchema.safeParse(body); if (!parsed.success) { return apiError(parsed.error.issues[0]?.message ?? 'Ungültige Eingabe'); } const { accountId, firstName, lastName, email, phone, street, postalCode, city, dateOfBirth, message, captchaToken, } = parsed.data; // Rate limit by account const accountLimit = checkRateLimit( `membership-apply:account:${accountId}`, MAX_PER_ACCOUNT, WINDOW_MS, ); if (!accountLimit.allowed) { return apiError('Zu viele Bewerbungen für diese Organisation.', 429); } // Verify CAPTCHA when configured — token is required, not optional if (process.env.CAPTCHA_SECRET_TOKEN) { if (!captchaToken) { return apiError('CAPTCHA-Überprüfung erforderlich.', 400); } const { verifyCaptchaToken } = await import('@kit/auth/captcha/server'); try { await verifyCaptchaToken(captchaToken); } catch { return apiError('CAPTCHA-Überprüfung fehlgeschlagen.', 400); } } const supabase = getSupabaseServerAdminClient(); // Validate that the account exists before inserting const { data: account } = await supabase .from('accounts') .select('id') .eq('id', accountId) .single(); if (!account) { return apiError('Ungültige Organisation.', 400); } const { error } = await supabase.from('membership_applications').insert({ account_id: accountId, first_name: firstName, last_name: lastName, email, phone: phone || null, street: street || null, postal_code: postalCode || null, city: city || null, date_of_birth: dateOfBirth || null, message: message || null, status: 'submitted', }); if (error) { logger.error( { error, context: 'membership-apply-insert' }, '[membership-apply] Insert error', ); return apiError('Bewerbung fehlgeschlagen', 500); } return apiSuccess({ message: 'Bewerbung erfolgreich eingereicht' }); } catch (err) { logger.error( { error: err, context: 'membership-apply' }, '[membership-apply] Error', ); return apiError('Serverfehler', 500); } }