fix: API Routes — Ergebnisprüfung, Suppression-Check vor Enqueue, Bulk-Enqueue, Status-Codes, Draft-Guard
- getTenantId in shared Utility src/lib/tenant-header.ts ausgelagert (alle 4 Route-Dateien) - send/route.ts: updateCampaignStatus-Ergebnis geprüft (C1), addBulk statt sequentiellem await (C2), Suppression-Check vor Enqueue (C3) - [id]/route.ts: GET unterscheidet 404/500, PATCH unterscheidet 404/400 - schedule/route.ts: immediate-Typ abgelehnt (I4), Draft-Guard hinzugefügt (I5) - .eslintrc.json ergänzt (next/core-web-vitals) — fehlte im Projekt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getCampaign, updateCampaignStatus } from '../../../../../server/db/campaigns'
|
||||
import { enqueueEmailSend } from '../../../../../queues/email-send.queue'
|
||||
import { emailSendQueue } from '../../../../../queues/email-send.queue'
|
||||
import { withTenant } from '../../../../../server/db/tenant'
|
||||
import { hashEmail } from '../../../../../lib/crypto'
|
||||
|
||||
function getTenantId(req: NextRequest): string {
|
||||
return req.headers.get('x-tenant-id') ?? 'default'
|
||||
}
|
||||
import { checkSuppression } from '../../../../../server/suppression/check'
|
||||
import { getTenantId } from '../../../../../lib/tenant-header'
|
||||
|
||||
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
const tenantId = getTenantId(req)
|
||||
@@ -19,7 +17,11 @@ export async function POST(req: NextRequest, { params }: { params: { id: string
|
||||
return NextResponse.json({ error: 'Nur Draft-Kampagnen können versendet werden' }, { status: 400 })
|
||||
}
|
||||
|
||||
await updateCampaignStatus(tenantId, params.id, 'sending')
|
||||
// Status auf 'sending' setzen — Ergebnis prüfen (Fix C1)
|
||||
const statusResult = await updateCampaignStatus(tenantId, params.id, 'sending')
|
||||
if (!statusResult.ok) {
|
||||
return NextResponse.json({ error: statusResult.error.message }, { status: 500 })
|
||||
}
|
||||
|
||||
const recipients = await withTenant(tenantId, (client) =>
|
||||
client.query<{ email: string }>(
|
||||
@@ -31,14 +33,32 @@ export async function POST(req: NextRequest, { params }: { params: { id: string
|
||||
)
|
||||
)
|
||||
|
||||
for (const recipient of recipients) {
|
||||
await enqueueEmailSend({
|
||||
tenantId,
|
||||
campaignId: params.id,
|
||||
recipientEmail: recipient.email,
|
||||
recipientHash: hashEmail(recipient.email),
|
||||
// Suppression-Check PFLICHT — kein Opt-out-Empfänger darf in die Queue (Fix C3)
|
||||
const unsuppressedRecipients = await Promise.all(
|
||||
recipients.map(async (r) => {
|
||||
const suppressed = await checkSuppression(tenantId, r.email)
|
||||
return suppressed ? null : r
|
||||
})
|
||||
}
|
||||
).then((results) => results.filter((r): r is { email: string } => r !== null))
|
||||
|
||||
return NextResponse.json({ queued: recipients.length })
|
||||
// Bulk-Enqueue — schnell, kein sequenzielles await (Fix C2)
|
||||
const addedJobs = await emailSendQueue.addBulk(
|
||||
unsuppressedRecipients.map((recipient) => ({
|
||||
name: 'send',
|
||||
data: {
|
||||
tenantId,
|
||||
campaignId: params.id,
|
||||
recipientEmail: recipient.email,
|
||||
recipientHash: hashEmail(recipient.email),
|
||||
},
|
||||
opts: {
|
||||
attempts: 3,
|
||||
backoff: { type: 'exponential' as const, delay: 2000 },
|
||||
removeOnComplete: 100,
|
||||
removeOnFail: { count: 500 },
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
return NextResponse.json({ queued: addedJobs.length })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user