Files
myeasycms-v2/apps/web/app/api/club/membership-apply/route.ts
T. Zehetbauer 5c5aaabae5
Some checks failed
Workflow / ʦ TypeScript (pull_request) Failing after 5m57s
Workflow / ⚫️ Test (pull_request) Has been skipped
refactor: remove obsolete member management API module
2026-04-03 14:08:31 +02:00

142 lines
3.6 KiB
TypeScript

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);
}
}