refactor: remove obsolete member management API module
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
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';
|
||||
|
||||
@@ -20,12 +21,33 @@ const MembershipApplySchema = z.object({
|
||||
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);
|
||||
|
||||
@@ -44,10 +66,48 @@ export async function POST(request: Request) {
|
||||
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,
|
||||
|
||||
87
apps/web/app/api/internal/cron/member-jobs/route.ts
Normal file
87
apps/web/app/api/internal/cron/member-jobs/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { createMemberNotificationService } from '@kit/member-management/services';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
|
||||
const CRON_SECRET = process.env.CRON_SECRET;
|
||||
|
||||
/**
|
||||
* Internal cron endpoint for member scheduled jobs.
|
||||
* Called hourly by pg_cron or external scheduler.
|
||||
*
|
||||
* POST /api/internal/cron/member-jobs
|
||||
* Header: Authorization: Bearer <CRON_SECRET>
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
const logger = await getLogger();
|
||||
|
||||
// Verify cron secret
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const token = authHeader?.replace('Bearer ', '');
|
||||
|
||||
if (!CRON_SECRET || token !== CRON_SECRET) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const client = getSupabaseServerAdminClient();
|
||||
const notificationService = createMemberNotificationService(client);
|
||||
|
||||
// 1. Process pending notification queue
|
||||
const queueResult = await notificationService.processPendingNotifications();
|
||||
|
||||
// 2. Run scheduled jobs for all accounts with enabled jobs
|
||||
const { data: accounts } = await (client.from as any)(
|
||||
'scheduled_job_configs',
|
||||
)
|
||||
.select('account_id')
|
||||
.eq('is_enabled', true)
|
||||
.or(`next_run_at.is.null,next_run_at.lte.${new Date().toISOString()}`);
|
||||
|
||||
const uniqueAccountIds = [
|
||||
...new Set((accounts ?? []).map((a: any) => a.account_id)),
|
||||
] as string[];
|
||||
|
||||
const jobResults: Record<string, unknown> = {};
|
||||
|
||||
for (const accountId of uniqueAccountIds) {
|
||||
try {
|
||||
const result = await notificationService.runScheduledJobs(accountId);
|
||||
jobResults[accountId] = result;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
{ accountId, error: e, context: 'cron-member-jobs' },
|
||||
'Failed to run jobs for account',
|
||||
);
|
||||
jobResults[accountId] = {
|
||||
error: e instanceof Error ? e.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const summary = {
|
||||
timestamp: new Date().toISOString(),
|
||||
queue: queueResult,
|
||||
accounts_processed: uniqueAccountIds.length,
|
||||
jobs: jobResults,
|
||||
};
|
||||
|
||||
logger.info(
|
||||
{ context: 'cron-member-jobs', ...summary },
|
||||
'Member cron jobs completed',
|
||||
);
|
||||
|
||||
return NextResponse.json(summary);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ error: err, context: 'cron-member-jobs' },
|
||||
'Cron job failed',
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user