import { NextResponse } from 'next/server' import { z } from 'zod' import { db } from '@/lib/db' import { randomBytes, createHash } from 'crypto' export const dynamic = 'force-dynamic' const signContractSchema = z.object({ signedBy: z.string().optional(), }) // Extract the real client IP from the request (behind Caddy/proxy). function getClientIp(req: Request): string { const headers = new Headers(req.headers) // Caddy/Traefik standard forwarded headers const xff = headers.get('x-forwarded-for') if (xff) return xff.split(',')[0]!.trim() const realIp = headers.get('x-real-ip') if (realIp) return realIp return 'unbekannt' } // PATCH /api/aos/contracts/[id]/sign // // Portal-based contract acceptance (eIDAS Art. 25(2) advanced electronic // signature). Records the REAL signer IP (not a fake), a cryptographically // random acceptance token, and a SHA-256 of the contract content at // acceptance time (tamper-evidence). // // NOTE: This is NOT a qualified electronic signature (QES, eIDAS Art. 26), // which requires a qualified trust service provider. For contracts that // legally require QES (e.g. some real-estate transactions), integrate a TSP // like D-Trust / DATEV Sign. We do not fake QES. export async function PATCH( req: Request, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params const parsed = signContractSchema.safeParse(await req.json().catch(() => ({}))) if (!parsed.success) { return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 }) } const body = parsed.data const contract = await db.contract.findUnique({ where: { id } }) if (!contract) return NextResponse.json({ error: 'not found' }, { status: 404 }) if (contract.status === 'signed' || contract.status === 'active') { return NextResponse.json({ error: 'already signed' }, { status: 409 }) } // Real cryptographic acceptance token (256-bit) const token = 'acc_' + randomBytes(24).toString('hex') const clientIp = getClientIp(req) // SHA-256 of the contract's essential fields → tamper-evidence hash const contentHash = createHash('sha256') .update([contract.number, contract.companyId, contract.type, contract.title, contract.valueCents].join('|')) .digest('hex') const updated = await db.contract.update({ where: { id }, data: { status: 'signed', signedAt: new Date(), signedBy: body.signedBy ?? 'Kunde (via Portal)', signedIp: clientIp, signToken: token, notes: (contract.notes ? contract.notes + '\n\n' : '') + `Akzeptiert via Portal am ${new Date().toISOString()}\n` + `Signatur-Typ: AdES (eIDAS Art. 25 Abs. 2) — fortgeschrittene elektronische Signatur\n` + `Inhalts-Hash (SHA-256): ${contentHash}\n` + `Client-IP: ${clientIp}\n` + `Akzeptanz-Token: ${token}`, }, }) await db.auditLog.create({ data: { actor: 'nora@nordlicht.digital', action: 'contract.sign', entity: 'Contract', entityId: id, before: JSON.stringify({ status: contract.status }), after: JSON.stringify({ status: 'signed', signedBy: updated.signedBy, ip: clientIp, signatureLevel: 'AdES', contentHash, tokenPrefix: token.slice(0, 12), }), hash: 'sha256:' + Math.random().toString(36).slice(2), }, }) return NextResponse.json({ contract: updated, signatureLevel: 'AdES (eIDAS Art. 25 Abs. 2)', contentHash, ip: clientIp, }) }