syncova-policies/app/api/privacy-policies/route.ts
Weapie c951a4123a
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 2m3s
Sortierung nach Titel und haengendes Overlay beheben
Sortierung: Erklaerungen erscheinen im Admin wie im oeffentlichen
Bereich alphabetisch nach Titel. Bewusst in JavaScript statt ueber die
Datenbank - SQLite vergleicht binaer, dort landeten "Aenderungs-
mitteilung" und "Uebernahme" hinter "Zaehlerstandsablesung". Intl mit
deutscher Kollation sortiert Ae bei A. Gemeinsame Regel in
lib/policy-sort.ts, benutzt von der Server-Komponente, der API und der
Admin-Tabelle; die Spaltenkoepfe bleiben umschaltbar.

Overlay: @radix-ui/react-select verlangte react-dismissable-layer
1.1.11, alle anderen Radix-Pakete 1.1.19. Dieses Paket setzt und
entfernt pointer-events auf dem body und fuehrt dazu eine Liste offener
Layer im Modulzustand - bei zwei Kopien zwei getrennte Listen. Ein
Select im Dialog liess deshalb beim Schliessen das Overlay des Dialogs
verschwinden und pointer-events: none stehen. Die Seite war danach bis
zum Neuladen tot.

Belegt im Browser mit einer Wegwerfseite (Select im Dialog):
  zwei Versionen -> Dialog nach Select-Schliessen nicht mehr bedienbar,
                    body pointer-events bleibt "none"
  eine Version   -> body wieder leer, Seite klickbar

react-select, -label, -slot und -tabs auf aktuelle Versionen gehoben;
damit liegt jedes Radix-Kernpaket nur noch einmal im Baum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:37:58 +02:00

137 lines
5.0 KiB
TypeScript
Raw 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 { PrismaClient } from '@prisma/client'
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
const prisma = new PrismaClient()
/**
* 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: any, 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: any) {
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 = await request.json()
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 }
)
}
}