Phase 2, erster Teil: Akten anlegen, lesen, Vermerke schreiben
Fachlogik in lib/cases/service.ts, Zugriffsprüfung in lib/access/. Jede Funktion prüft selbst und schreibt ihren Protokolleintrag in derselben Transaktion wie die Änderung — die Server Actions daneben sind dünn. Schreibgrenzen nach docs/03: niemand legt eine Akte über der eigenen Stufe an, niemand stuft über die eigene Stufe hoch, archiviert und versiegelt sind schreibgeschützt auch für Admins. Eine Herabstufung verlangt eine Begründung und landet als sichtbarer Systemeintrag im Verlauf, nicht nur im Protokoll. Aktenzeichen über CaseCounter mit INSERT ... ON CONFLICT DO UPDATE, geprüft mit acht gleichzeitigen Anlagen ohne Doppelung. Wer eine Akte anlegt, wird ihr Ermittler, sonst verliert er sie aus dem Blick, sobald jemand hochstuft. Fallliste cursorbasiert auf (updatedAt, id), Sichtbarkeit und Trefferzahl in derselben Abfrage. Aktenansicht mit Kopfdaten sofort und Verlauf in eigener Suspense-Grenze. Vermerke optimistisch mit clientseitiger UUIDv7, die zugleich Idempotenzschlüssel ist — zweimal gesendet bleibt ein Eintrag. Lesezugriffe ab Stufe 3 werden protokolliert, gepuffert und alle zwei Sekunden gebündelt geschrieben, damit das Protokoll die Antwort nicht ausbremst. Die vier Startvorlagen stehen jetzt im Seed statt in Phase 3: ohne Vorlage lässt sich keine Akte anlegen.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
|
||||
|
||||
/**
|
||||
* Schreibgrenzen der Freigabestufe, siehe docs/03-auth-und-rechte.md.
|
||||
*
|
||||
* Die Leseprüfung steckt in `visibleCaseWhere`. Diese Regeln kommen obendrauf
|
||||
* und gelten für jede Server Action, unabhängig davon, was die Oberfläche
|
||||
* vorher versteckt hat.
|
||||
*/
|
||||
|
||||
/** Status, in denen eine Akte nicht mehr verändert wird. */
|
||||
export const READ_ONLY_STATUS = ['ARCHIVED', 'SEALED'] as const
|
||||
|
||||
export type WriteDenial =
|
||||
| { ok: false; reason: 'no-permission'; permission: Permission }
|
||||
| { ok: false; reason: 'clearance-too-low'; required: number }
|
||||
| { ok: false; reason: 'read-only'; status: string }
|
||||
| { ok: false; reason: 'closed' }
|
||||
|
||||
export type WriteCheck = { ok: true } | WriteDenial
|
||||
|
||||
function requirePermission(p: EffectivePermissions, permission: Permission): WriteCheck {
|
||||
if (p.isAdmin || p.permissions.has(permission)) return { ok: true }
|
||||
return { ok: false, reason: 'no-permission', permission }
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf dieser Nutzer eine Akte mit dieser Klassifizierung anlegen?
|
||||
*
|
||||
* Niemand legt eine Akte über der eigenen Stufe an — auch kein Admin, denn er
|
||||
* hätte sonst eine Akte erzeugt, deren Sichtbarkeit er nicht überblickt.
|
||||
* Der Admin hat ohnehin Stufe 5.
|
||||
*/
|
||||
export function canCreateCase(p: EffectivePermissions, classification: number): WriteCheck {
|
||||
const permitted = requirePermission(p, 'case.create')
|
||||
if (!permitted.ok) return permitted
|
||||
if (classification > p.clearanceLevel) {
|
||||
return { ok: false, reason: 'clearance-too-low', required: classification }
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf dieser Nutzer diese Akte verändern?
|
||||
*
|
||||
* Setzt voraus, dass die Akte bereits durch `visibleCaseWhere` gelaufen ist:
|
||||
* was man nicht sehen darf, kann man nicht anfassen.
|
||||
*/
|
||||
export function canEditCase(
|
||||
p: EffectivePermissions,
|
||||
target: { status: string; classification: number },
|
||||
permission: Permission = 'case.edit',
|
||||
): WriteCheck {
|
||||
if ((READ_ONLY_STATUS as readonly string[]).includes(target.status)) {
|
||||
// Archiviert und versiegelt sind schreibgeschützt, auch für Admins.
|
||||
return { ok: false, reason: 'read-only', status: target.status }
|
||||
}
|
||||
|
||||
const permitted = requirePermission(p, permission)
|
||||
if (!permitted.ok) return permitted
|
||||
|
||||
if (target.classification > p.clearanceLevel && !p.isAdmin) {
|
||||
return { ok: false, reason: 'clearance-too-low', required: target.classification }
|
||||
}
|
||||
|
||||
if (target.status === 'CLOSED' && permission !== 'case.reopen') {
|
||||
// Änderungen an abgeschlossenen Fällen brauchen ein eigenes Recht und
|
||||
// erzeugen zusätzlich einen SYSTEM-Eintrag.
|
||||
const closedAllowed = requirePermission(p, 'case.edit_closed')
|
||||
if (!closedAllowed.ok) return { ok: false, reason: 'closed' }
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf dieser Nutzer die Klassifizierung auf diesen Wert setzen?
|
||||
*
|
||||
* Die neue Stufe darf die eigene nicht übersteigen. Eine Herabstufung ist der
|
||||
* heikelste Vorgang im System: sie macht Inhalte für Leute sichtbar, die sie
|
||||
* vorher nicht sehen durften. Erlaubt ist sie trotzdem — sie wird nur sichtbar
|
||||
* protokolliert, im Verlauf und im Zugriffsprotokoll.
|
||||
*/
|
||||
export function canReclassify(
|
||||
p: EffectivePermissions,
|
||||
target: { status: string; classification: number },
|
||||
next: number,
|
||||
): WriteCheck {
|
||||
if (next < 0 || next > 5) return { ok: false, reason: 'clearance-too-low', required: next }
|
||||
if (next > p.clearanceLevel) {
|
||||
return { ok: false, reason: 'clearance-too-low', required: next }
|
||||
}
|
||||
return canEditCase(p, target)
|
||||
}
|
||||
|
||||
/** Für Server Actions: wirft mit einer Meldung, die in die Oberfläche darf. */
|
||||
export function assertWritable(check: WriteCheck, messages: Record<string, string>): void {
|
||||
if (check.ok) return
|
||||
throw new Error(messages[check.reason] ?? messages.default ?? 'Nicht erlaubt')
|
||||
}
|
||||
Reference in New Issue
Block a user