syncova-policies/app/api/privacy-policies/route.ts
Weapie 265ca8ac38
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 5m15s
Improve accessibility and access controls; verify production and database recovery
2026-09-14 11:44:41 +02:00

138 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from 'next/server'
import { parsePolicyInput, type PolicyInput } from '@/lib/policy-input'
import { isAdminSession, requireAdmin } from '@/lib/require-admin'
import {
DEFAULTED_FIELDS,
} from '@/lib/policy-defaults'
import { readPolicyDefaults, toApiPolicy } from '@/lib/policy-defaults.server'
import { describeMissing, missingRequiredFields } from '@/lib/policy-completeness'
import { resolveSlug } from '@/lib/policy-slug.server'
import { sortByTitle } from '@/lib/policy-sort'
import { PolicyDefaults } from '@/types/privacy-policy'
// Prisma client instance
import { prisma } from "@/lib/prisma"
/**
* Prüft die Angaben einer Erklärung.
*
* Zwei Stufen: Ohne Titel, Verarbeitungstätigkeit und Empfänger lässt sich
* nichts speichern. Die übrigen Pflichtangaben aus Art. 13/14 DSGVO werden
* erst beim Veröffentlichen verlangt – ein Entwurf darf unvollständig sein,
* eine öffentlich sichtbare Erklärung nicht.
*/
function validatePrivacyPolicyData(data: PolicyInput, defaults: PolicyDefaults) {
const errors: string[] = []
// Ohne diese Angaben ist der Datensatz nicht einmal als Entwurf brauchbar
if (!data.title?.trim()) errors.push('Titel ist erforderlich')
if (!data.processingDescription?.trim()) errors.push('Verarbeitungstätigkeit ist erforderlich')
if (!data.recipients?.trim()) errors.push('Empfänger sind erforderlich')
// Optional boolean validation
if (data.isPublic !== undefined && typeof data.isPublic !== 'boolean') {
errors.push('isPublic muss ein Boolean-Wert sein')
}
// Vollständigkeit nur verlangen, wenn die Erklärung öffentlich sein soll
if (data.isPublic !== false && errors.length === 0) {
for (const entry of missingRequiredFields(data, defaults)) {
errors.push(`${describeMissing(entry)} – zum Veröffentlichen erforderlich`)
}
}
return errors
}
/**
* Übernimmt die Baustein-Felder aus der Anfrage: leer wird zu `null` und
* bedeutet damit „Standardtext verwenden“.
*/
function defaultedFieldsFrom(data: PolicyInput) {
return Object.fromEntries(
DEFAULTED_FIELDS.map((field) => [field, data[field]?.trim() || null])
) as Record<(typeof DEFAULTED_FIELDS)[number], string | null>
}
// GET /api/privacy-policies - returns privacy policies based on admin parameter
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
// `?admin=true` schließt die nicht veröffentlichten Erklärungen ein und
// setzt deshalb eine angemeldete Admin-Sitzung voraus. Ohne sie wird ganz
// normal die öffentliche Liste ausgeliefert, statt einen Fehler zu werfen.
const isAdmin =
searchParams.get('admin') === 'true' && (await isAdminSession())
// Get policies based on admin flag
const [policies, defaults] = await Promise.all([
prisma.privacyPolicy.findMany({
where: isAdmin ? {} : {
isPublic: true
},
}),
readPolicyDefaults(prisma),
])
// Standardtexte einsetzen, damit Konsumenten immer den geltenden Text sehen.
// Sortiert nach Titel – nicht über die Datenbank, siehe lib/policy-sort.
return NextResponse.json(
sortByTitle(policies.map((policy) => toApiPolicy(policy, defaults)))
)
} catch (error) {
console.error('Error fetching privacy policies:', error)
return NextResponse.json(
{ error: 'Fehler beim Abrufen der Datenschutzerklärungen' },
{ status: 500 }
)
}
}
// POST /api/privacy-policies - creates a new privacy policy (Admin)
export async function POST(request: NextRequest) {
try {
const denied = await requireAdmin()
if (denied) return denied
const data = parsePolicyInput(await request.json().catch(() => null))
if (!data) return NextResponse.json({ error: 'Ungültige Feldtypen oder zu lange Texte.' }, { status: 400 })
const defaults = await readPolicyDefaults(prisma)
// Validate required fields
const validationErrors = validatePrivacyPolicyData(data, defaults)
if (validationErrors.length > 0) {
return NextResponse.json(
{ error: 'Validierungsfehler', details: validationErrors },
{ status: 400 }
)
}
// Create new privacy policy
const newPolicy = await prisma.privacyPolicy.create({
data: {
title: data.title.trim(),
slug: await resolveSlug(prisma, data.title),
...defaultedFieldsFrom(data),
processingDescription: data.processingDescription.trim(),
processingPurposes: data.processingPurposes?.trim() || null,
legalBasis: data.legalBasis?.trim() || null,
legitimateInterests: data.legitimateInterests?.trim() || null,
recipients: data.recipients.trim(),
storageDuration: data.storageDuration?.trim() || null,
dataSource: data.dataSource?.trim() || null,
dataCategories: data.dataCategories?.trim() || null,
isPublic: data.isPublic !== undefined ? data.isPublic : true
}
})
return NextResponse.json(toApiPolicy(newPolicy, defaults), { status: 201 })
} catch (error) {
console.error('Error creating privacy policy:', error)
return NextResponse.json(
{ error: 'Fehler beim Erstellen der Datenschutzerklärung' },
{ status: 500 }
)
}
}