Umschaltbar ueber AUTH_ENABLED in der .env. Steht sie auf false, verhaelt
sich alles wie bisher - geprueft mit den 200 bestehenden Pruefungen.
Kern ist eine Actor- und Policy-Schicht: getActor() liefert bei
ausgeschalteter Anmeldung einen system-Actor mit Vollzugriff, sonst den
angemeldeten Benutzer. Dadurch braucht keine einzige Route ein
'if (anmeldungAktiv)' - sie fragen alle nur can() bzw. requirePermission().
- Datenmodell: User, Session, Group, Membership; Codes bekommen Gruppe und
Ersteller. Die Migration legt eine Standardgruppe an und ordnet ihr
bestehende Codes zu, bevor die Spalte verpflichtend wird - sonst
scheiterte sie auf jeder bereits benutzten Installation.
- Passwoerter mit scrypt aus der Node-Standardbibliothek. Bewusst kein
Argon2: das braeuchte ein natives Modul passend zur Container-Plattform.
- Sitzungen in der Datenbank, im Cookie nur ein Zufallstoken; gespeichert
wird dessen SHA-256. Deaktivieren oder ein neues Passwort beendet alle
offenen Sitzungen sofort.
- Aussperrschutz: der letzte aktive Administrator kann sich nicht selbst
entmachten, deaktivieren oder loeschen. Der Schalter liegt in der .env,
nicht in der Oberflaeche - so bleibt der Weg zurueck immer offen.
- Ersteinrichtung legt den ersten Administrator an und schliesst sich danach.
- Die Weiterleitung /r/{slug} bleibt oeffentlich, sonst liesse sich kein
gedruckter Code mehr scannen.
Zwei Fehler dabei gefunden und behoben:
- 'return handleLogin(...)' innerhalb eines try-Blocks gibt das Promise
zurueck, bevor es sich aufloest. Die Ablehnung kam nie im catch an und
wurde zu einem nackten 500er statt einer 401. Jetzt mit await.
- Die Fehlerklassen wurden ueber instanceof erkannt. type-module.ts landet
aber sowohl im Server- als auch im Client-Buendel, es gibt also zwei
Klassenidentitaeten - saubere 400er wurden dadurch still zu 500ern. Die
Erkennung laeuft jetzt ueber eine Eigenschaft.
Geprueft: 51 neue Pruefungen fuer Anmeldung und die vollstaendige
Rollenmatrix, dazu die 200 bestehenden bei ausgeschalteter Anmeldung.
143 lines
4.7 KiB
Plaintext
143 lines
4.7 KiB
Plaintext
// QR-Code-Generator — Datenmodell
|
|
// Doku: https://pris.ly/d/prisma-schema
|
|
|
|
generator client {
|
|
provider = "prisma-client"
|
|
output = "../src/generated/prisma"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
/// Rolle eines Benutzers innerhalb einer Gruppe.
|
|
///
|
|
/// Darüber liegt noch eine Ebene: `User.isInstanceAdmin`. Sie darf Gruppen
|
|
/// und Benutzer überhaupt erst anlegen — ohne sie gäbe es niemanden, der die
|
|
/// erste Gruppe erstellt.
|
|
enum Role {
|
|
/// Darf zusätzlich Mitglieder und Rollen der Gruppe verwalten.
|
|
ADMIN
|
|
/// Darf Codes anlegen, ändern und löschen.
|
|
EDITOR
|
|
/// Darf nur lesen und herunterladen.
|
|
VIEWER
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
name String?
|
|
/// scrypt-Hash, siehe src/lib/auth/password.ts — nie das Klartextpasswort.
|
|
passwordHash String
|
|
/// Darf Gruppen, Benutzer und Domains verwalten.
|
|
isInstanceAdmin Boolean @default(false)
|
|
/// Deaktivierte Benutzer können sich nicht anmelden; laufende Sitzungen
|
|
/// werden beim nächsten Zugriff verworfen.
|
|
isActive Boolean @default(true)
|
|
failedLogins Int @default(0)
|
|
lockedUntil DateTime?
|
|
lastLoginAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
sessions Session[]
|
|
memberships Membership[]
|
|
createdCodes QrCode[] @relation("QrCodeCreator")
|
|
|
|
@@index([email])
|
|
}
|
|
|
|
/// Angemeldete Sitzung. In der Datenbank statt in einem signierten Cookie,
|
|
/// damit sie sich serverseitig beenden lässt — etwa wenn ein Benutzer
|
|
/// deaktiviert wird.
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
/// SHA-256 des Cookie-Werts. Der Wert selbst steht nirgends in der Datenbank:
|
|
/// wer sie liest, kann sich damit trotzdem nicht anmelden.
|
|
tokenHash String @unique
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
expiresAt DateTime
|
|
lastSeenAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
/// Eine Gruppe bündelt Benutzer und die Codes, die ihnen gehören.
|
|
model Group {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
description String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
memberships Membership[]
|
|
codes QrCode[]
|
|
}
|
|
|
|
/// Mitgliedschaft eines Benutzers in einer Gruppe, mit Rolle. Ein Benutzer
|
|
/// kann in mehreren Gruppen sein — mit unterschiedlicher Rolle je Gruppe.
|
|
model Membership {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
groupId String
|
|
role Role
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, groupId])
|
|
@@index([groupId])
|
|
}
|
|
|
|
/// Freigabe eines QR-Typs durch die Verwaltung.
|
|
///
|
|
/// Es gibt nur Zeilen für Typen, die bewusst umgeschaltet wurden — fehlt ein
|
|
/// Eintrag, gilt der Typ als freigegeben. So bleibt eine frische Installation
|
|
/// ohne Vorbelegung nutzbar, und neue Typen aus späteren Versionen sind
|
|
/// automatisch sichtbar.
|
|
model QrTypeSetting {
|
|
/// Kennung aus der Typ-Registry, z. B. "website".
|
|
type String @id
|
|
enabled Boolean
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
/// Ein dynamischer QR-Code. Der QR-Code kodiert `{BASE_URL}/r/{slug}` und
|
|
/// leitet beim Aufruf auf das Ziel weiter, das aus `data` aufgelöst wird.
|
|
///
|
|
/// `type` + `data` sind bewusst generisch gehalten: die Typ-Module legen ihr
|
|
/// eigenes Payload-Format in `data` ab, ohne Schemaänderung.
|
|
model QrCode {
|
|
id String @id @default(cuid())
|
|
slug String @unique
|
|
type String
|
|
label String?
|
|
data Json
|
|
fgColor String @default("#000000")
|
|
bgColor String @default("#FFFFFF")
|
|
size Int @default(512)
|
|
ecLevel String @default("M")
|
|
margin Int @default(4)
|
|
scans Int @default(0)
|
|
lastScanAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
/// Jeder Code gehört einer Gruppe — auch bei ausgeschalteter Anmeldung.
|
|
/// Dann ist es die beim ersten Start angelegte Standardgruppe.
|
|
groupId String
|
|
group Group @relation(fields: [groupId], references: [id])
|
|
/// Wer den Code angelegt hat. Leer für Codes aus der Zeit vor der
|
|
/// Anmeldung und für Codes, deren Ersteller gelöscht wurde.
|
|
createdById String?
|
|
createdBy User? @relation("QrCodeCreator", fields: [createdById], references: [id], onDelete: SetNull)
|
|
|
|
@@index([createdAt])
|
|
@@index([groupId])
|
|
}
|