All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 5m15s
138 lines
5.2 KiB
TypeScript
138 lines
5.2 KiB
TypeScript
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 }
|
||
)
|
||
}
|
||
}
|