fix: DSGVO-Findings 5+7+6 — Tenant-Guard, APP_URL-Pflichtcheck, Constraint-Migration
- getTenantId wirft bei fehlendem Header statt silent fallback auf 'default' - Alle API-Routes fangen den Fehler ab und antworten mit 401 - Worker gibt err() zurück wenn APP_URL nicht konfiguriert ist - Migration: recipient_has_one Constraint schließt NULL/NULL-Zeilen aus Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,11 @@ import { getCampaign, updateCampaign } from '../../../../server/db/campaigns'
|
||||
import { getTenantId } from '../../../../lib/tenant-header'
|
||||
|
||||
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
const result = await getCampaign(getTenantId(req), params.id)
|
||||
let tenantId: string
|
||||
try { tenantId = getTenantId(req) } catch {
|
||||
return NextResponse.json({ error: 'Tenant nicht identifizierbar' }, { status: 401 })
|
||||
}
|
||||
const result = await getCampaign(tenantId, params.id)
|
||||
if (!result.ok) {
|
||||
const status = result.error.message.includes('nicht gefunden') ? 404 : 500
|
||||
return NextResponse.json({ error: result.error.message }, { status })
|
||||
@@ -13,7 +17,10 @@ export async function GET(req: NextRequest, { params }: { params: { id: string }
|
||||
}
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
const tenantId = getTenantId(req)
|
||||
let tenantId: string
|
||||
try { tenantId = getTenantId(req) } catch {
|
||||
return NextResponse.json({ error: 'Tenant nicht identifizierbar' }, { status: 401 })
|
||||
}
|
||||
const body = await req.json()
|
||||
const parsed = UpdateCampaignSchema.safeParse(body)
|
||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 })
|
||||
|
||||
@@ -4,7 +4,10 @@ import { getCampaign, updateCampaignStatus } from '../../../../../server/db/camp
|
||||
import { getTenantId } from '../../../../../lib/tenant-header'
|
||||
|
||||
export async function POST(req: NextRequest, { params }: { params: { id: string } }) {
|
||||
const tenantId = getTenantId(req)
|
||||
let tenantId: string
|
||||
try { tenantId = getTenantId(req) } catch {
|
||||
return NextResponse.json({ error: 'Tenant nicht identifizierbar' }, { status: 401 })
|
||||
}
|
||||
const body = await req.json()
|
||||
const parsed = ScheduleCampaignSchema.safeParse(body)
|
||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 })
|
||||
|
||||
@@ -7,7 +7,10 @@ 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)
|
||||
let tenantId: string
|
||||
try { tenantId = getTenantId(req) } catch {
|
||||
return NextResponse.json({ error: 'Tenant nicht identifizierbar' }, { status: 401 })
|
||||
}
|
||||
|
||||
const campaignResult = await getCampaign(tenantId, params.id)
|
||||
if (!campaignResult.ok) {
|
||||
|
||||
@@ -4,14 +4,20 @@ import { createCampaign, listCampaigns } from '../../../server/db/campaigns'
|
||||
import { getTenantId } from '../../../lib/tenant-header'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const tenantId = getTenantId(req)
|
||||
let tenantId: string
|
||||
try { tenantId = getTenantId(req) } catch {
|
||||
return NextResponse.json({ error: 'Tenant nicht identifizierbar' }, { status: 401 })
|
||||
}
|
||||
const result = await listCampaigns(tenantId)
|
||||
if (!result.ok) return NextResponse.json({ error: result.error.message }, { status: 500 })
|
||||
return NextResponse.json(result.data)
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const tenantId = getTenantId(req)
|
||||
let tenantId: string
|
||||
try { tenantId = getTenantId(req) } catch {
|
||||
return NextResponse.json({ error: 'Tenant nicht identifizierbar' }, { status: 401 })
|
||||
}
|
||||
const body = await req.json()
|
||||
const parsed = CreateCampaignSchema.safeParse(body)
|
||||
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 })
|
||||
|
||||
17
src/lib/tenant-header.test.ts
Normal file
17
src/lib/tenant-header.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { getTenantId } from './tenant-header'
|
||||
|
||||
describe('getTenantId', () => {
|
||||
it('gibt tenantId zurück wenn Header gesetzt', () => {
|
||||
const req = new NextRequest('http://localhost/api', {
|
||||
headers: { 'x-tenant-id': 'abc123' },
|
||||
})
|
||||
expect(getTenantId(req)).toBe('abc123')
|
||||
})
|
||||
|
||||
it('wirft Fehler wenn x-tenant-id fehlt', () => {
|
||||
const req = new NextRequest('http://localhost/api')
|
||||
expect(() => getTenantId(req)).toThrow('x-tenant-id Header fehlt')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
export function getTenantId(req: NextRequest): string {
|
||||
return req.headers.get('x-tenant-id') ?? 'default'
|
||||
const tenantId = req.headers.get('x-tenant-id')
|
||||
if (!tenantId) throw new Error('x-tenant-id Header fehlt')
|
||||
return tenantId
|
||||
}
|
||||
|
||||
@@ -40,7 +40,10 @@ const jobData = {
|
||||
}
|
||||
|
||||
describe('processEmailSendJob', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
process.env.APP_URL = 'http://localhost:3000'
|
||||
})
|
||||
|
||||
it('sendet E-Mail wenn nicht suppressed', async () => {
|
||||
vi.mocked(getCampaign).mockResolvedValue({ ok: true, data: mockCampaign })
|
||||
@@ -133,4 +136,17 @@ describe('processEmailSendJob', () => {
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
})
|
||||
|
||||
it('gibt err zurück wenn APP_URL nicht gesetzt', async () => {
|
||||
const originalUrl = process.env.APP_URL
|
||||
delete process.env.APP_URL
|
||||
vi.mocked(getCampaign).mockResolvedValue({ ok: true, data: mockCampaign })
|
||||
vi.mocked(checkSuppression).mockResolvedValue(false)
|
||||
|
||||
const result = await processEmailSendJob(jobData)
|
||||
|
||||
expect(result.ok).toBe(false)
|
||||
if (!result.ok) expect(result.error.message).toContain('APP_URL')
|
||||
process.env.APP_URL = originalUrl
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,7 +18,8 @@ export async function processEmailSendJob(data: EmailSendJobData): Promise<Resul
|
||||
return ok(undefined)
|
||||
}
|
||||
|
||||
const appUrl = process.env.APP_URL ?? 'http://localhost:3000'
|
||||
const appUrl = process.env.APP_URL
|
||||
if (!appUrl) return err(new Error('APP_URL nicht konfiguriert — Unsubscribe-Link kann nicht erstellt werden'))
|
||||
const unsubUrl = `${appUrl}/unsub?tid=${data.tenantId}&cid=${data.campaignId}&r=${data.recipientHash}`
|
||||
|
||||
const sendResult = await sendEmail({
|
||||
|
||||
Reference in New Issue
Block a user