Neue Tabellen:
- User Konto mit Rolle, optionalem Passwort-Hash, Sperrzaehler.
Das Passwort ist optional, weil ein Konto auch ausschliesslich
per Passkey nutzbar sein darf.
- Session serverseitige Sitzung. Gespeichert wird nur der SHA-256-Hash
des Cookie-Tokens, damit ein Datenbankleck keine uebernehmbaren
Sitzungen ergibt.
- Credential ein Passkey je Zeile, mit oeffentlichem Schluessel, Transporten
und Signaturzaehler.
- WebAuthnChallenge kurzlebig und einmalig verwendbar, deshalb in der
Datenbank statt im Cookie.
- AppSettings Betriebsart und Registrierung, genau eine Zeile.
Project und GitConnection bekommen einen Besitzer. Bewusst nullable: so
ueberstehen Bestandsdaten die Migration und werden beim Anlegen des ersten
Kontos uebernommen. Slug und Verbindungsname sind dadurch nur noch je
Besitzer eindeutig - zwei Personen duerfen beide ein Projekt "dashboard"
haben.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
286 lines
8.8 KiB
Plaintext
286 lines
8.8 KiB
Plaintext
// ─────────────────────────────────────────────────────────────
|
||
// Nebula – Datenmodell
|
||
// Datenbank: PostgreSQL (lokal oder via docker compose)
|
||
// ─────────────────────────────────────────────────────────────
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
// linux-musl-openssl-3.0.x wird fuer das Alpine-Docker-Image benoetigt.
|
||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
/// Lebenszyklus eines Projekts. IDEA ist der Startzustand fuer
|
||
/// frisch notierte Einfaelle, ARCHIVED blendet Projekte aus dem
|
||
/// Standard-Board aus, ohne sie zu loeschen.
|
||
enum ProjectStatus {
|
||
IDEA
|
||
OPEN
|
||
PLANNING
|
||
IN_DEVELOPMENT
|
||
DONE
|
||
ARCHIVED
|
||
}
|
||
|
||
/// Unterstuetzte Git-Systeme.
|
||
enum GitProvider {
|
||
GITHUB
|
||
GITLAB
|
||
GITEA
|
||
}
|
||
|
||
/// ADMIN darf Benutzer und Einstellungen verwalten. Auf fremde Projekte hat
|
||
/// auch ein Admin keinen Zugriff – die Trennung ist bewusst hart.
|
||
enum UserRole {
|
||
ADMIN
|
||
USER
|
||
}
|
||
|
||
/// SINGLE_USER: genau ein Konto, keine Registrierung, keine Benutzerliste.
|
||
/// MULTI_USER: mehrere Konten, jedes mit streng getrennten Daten.
|
||
enum AppMode {
|
||
SINGLE_USER
|
||
MULTI_USER
|
||
}
|
||
|
||
/// Ein Benutzerkonto. Das Passwort ist optional, weil ein Konto auch
|
||
/// ausschliesslich per Passkey nutzbar sein darf.
|
||
model User {
|
||
id String @id @default(cuid())
|
||
/// Immer kleingeschrieben gespeichert, damit die Anmeldung
|
||
/// gross-/kleinschreibungsunabhaengig funktioniert.
|
||
username String @unique
|
||
displayName String?
|
||
email String? @unique
|
||
/// scrypt-Hash im Format scrypt$N$r$p$<salt>$<hash>; null = nur Passkey.
|
||
passwordHash String?
|
||
role UserRole @default(USER)
|
||
isActive Boolean @default(true)
|
||
|
||
lastLoginAt DateTime?
|
||
/// Zaehler und Sperre gegen Passwortraten.
|
||
failedLoginCount Int @default(0)
|
||
lockedUntil DateTime?
|
||
|
||
sessions Session[]
|
||
credentials Credential[]
|
||
projects Project[]
|
||
connections GitConnection[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([role])
|
||
}
|
||
|
||
/// Serverseitige Sitzung. In der Datenbank liegt nur der SHA-256-Hash des
|
||
/// Cookie-Tokens, damit ein Datenbankleck keine uebernehmbaren Sitzungen ergibt.
|
||
model Session {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
tokenHash String @unique
|
||
expiresAt DateTime
|
||
lastSeenAt DateTime @default(now())
|
||
userAgent String?
|
||
ip String?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([userId])
|
||
@@index([expiresAt])
|
||
}
|
||
|
||
/// Ein registrierter Passkey (WebAuthn-Credential).
|
||
model Credential {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
/// base64url-kodierte Credential-ID des Authenticators.
|
||
credentialId String @unique
|
||
publicKey Bytes
|
||
counter Int @default(0)
|
||
transports String[] @default([])
|
||
deviceType String?
|
||
backedUp Boolean @default(false)
|
||
/// Frei waehlbarer Name, z. B. "MacBook Touch ID".
|
||
name String?
|
||
lastUsedAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([userId])
|
||
}
|
||
|
||
/// Kurzlebige WebAuthn-Challenge. Wird nach Gebrauch geloescht, damit sie
|
||
/// kein zweites Mal verwendet werden kann.
|
||
model WebAuthnChallenge {
|
||
id String @id @default(cuid())
|
||
challenge String @unique
|
||
/// null bei der Anmeldung ueber auffindbare Passkeys.
|
||
userId String?
|
||
/// "registration" oder "authentication"
|
||
purpose String
|
||
expiresAt DateTime
|
||
createdAt DateTime @default(now())
|
||
|
||
@@index([expiresAt])
|
||
}
|
||
|
||
/// Einstellungen der Instanz. Genau eine Zeile mit der id "singleton".
|
||
model AppSettings {
|
||
id String @id @default("singleton")
|
||
mode AppMode @default(SINGLE_USER)
|
||
/// Nur im Mehrbenutzerbetrieb wirksam.
|
||
allowRegistration Boolean @default(false)
|
||
/// Anzeigename gegenueber dem Authenticator beim Anlegen eines Passkeys.
|
||
rpName String @default("Nebula")
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
/// Eine Verbindung zu einem Git-System inkl. verschluesseltem Token.
|
||
model GitConnection {
|
||
id String @id @default(cuid())
|
||
name String
|
||
provider GitProvider
|
||
/// Basis-URL der Instanz, z. B. https://github.com, https://gitlab.com,
|
||
/// https://git.meine-domain.de
|
||
baseUrl String
|
||
/// Optionaler Benutzername – rein informativ fuer die Anzeige.
|
||
username String?
|
||
|
||
/// Personal Access Token, AES-256-GCM-verschluesselt (siehe src/lib/crypto.ts).
|
||
tokenCipher String?
|
||
/// Gekuerzte Darstellung des Tokens fuer die UI, z. B. "ghp_…4f2a".
|
||
tokenHint String?
|
||
|
||
lastCheckedAt DateTime?
|
||
lastCheckOk Boolean?
|
||
lastCheckMessage String?
|
||
|
||
/// Besitzer. Nullable, damit Bestandsdaten aus der Zeit vor der
|
||
/// Benutzerverwaltung erhalten bleiben; beim Einrichten des ersten
|
||
/// Kontos werden sie diesem zugeordnet.
|
||
ownerId String?
|
||
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||
|
||
projects Project[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([ownerId, name])
|
||
@@index([provider])
|
||
@@index([ownerId])
|
||
}
|
||
|
||
/// Ein Vibe-Coding-Projekt – von der reinen Idee bis zum fertigen Repo.
|
||
model Project {
|
||
id String @id @default(cuid())
|
||
name String
|
||
/// Pro Besitzer eindeutig – siehe @@unique unten.
|
||
slug String
|
||
/// Einzeiler fuer die Karte im Dashboard.
|
||
summary String?
|
||
/// Ausfuehrliche Beschreibung / Ideenskizze (Markdown-freundlich).
|
||
description String?
|
||
status ProjectStatus @default(IDEA)
|
||
/// 1 = niedrig, 2 = normal, 3 = hoch, 4 = kritisch
|
||
priority Int @default(2)
|
||
/// Fortschritt in Prozent (0–100)
|
||
progress Int @default(0)
|
||
/// Akzentfarbe der Karte: violet | cyan | emerald | amber | rose | blue
|
||
accent String @default("violet")
|
||
tags String[] @default([])
|
||
favorite Boolean @default(false)
|
||
|
||
// ── Git-Anbindung ──────────────────────────────────────────
|
||
repoUrl String?
|
||
/// Owner bzw. Namespace/Gruppe (bei GitLab auch verschachtelt: "gruppe/subgruppe")
|
||
repoOwner String?
|
||
repoName String?
|
||
defaultBranch String?
|
||
connectionId String?
|
||
connection GitConnection? @relation(fields: [connectionId], references: [id], onDelete: SetNull)
|
||
|
||
lastSyncedAt DateTime?
|
||
syncError String?
|
||
|
||
/// Besitzer. Nullable wie bei GitConnection, damit Bestandsdaten beim
|
||
/// Einrichten des ersten Kontos uebernommen werden koennen.
|
||
ownerId String?
|
||
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||
|
||
notes Note[]
|
||
snapshot RepoSnapshot?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([ownerId, slug])
|
||
@@index([status])
|
||
@@index([connectionId])
|
||
@@index([updatedAt])
|
||
@@index([ownerId])
|
||
}
|
||
|
||
/// Freie Notizen zu einem Projekt.
|
||
model Note {
|
||
id String @id @default(cuid())
|
||
projectId String
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
title String?
|
||
content String
|
||
pinned Boolean @default(false)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([projectId])
|
||
@@index([pinned])
|
||
}
|
||
|
||
/// Zwischengespeicherter Stand des verknuepften Repositories. Wird beim
|
||
/// Sync ueberschrieben, damit das Dashboard auch ohne Netz Daten zeigt.
|
||
model RepoSnapshot {
|
||
id String @id @default(cuid())
|
||
projectId String @unique
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
|
||
fullName String?
|
||
description String?
|
||
defaultBranch String?
|
||
htmlUrl String?
|
||
visibility String?
|
||
|
||
stars Int @default(0)
|
||
forks Int @default(0)
|
||
openIssues Int @default(0)
|
||
openPullRequests Int @default(0)
|
||
sizeKb Int @default(0)
|
||
|
||
lastCommitSha String?
|
||
lastCommitMessage String?
|
||
lastCommitAuthor String?
|
||
lastCommitAt DateTime?
|
||
|
||
latestTag String?
|
||
latestRelease String?
|
||
latestReleaseAt DateTime?
|
||
|
||
branchCount Int @default(0)
|
||
tagCount Int @default(0)
|
||
|
||
/// Rohdaten der Listen – bewusst als JSON, damit providerspezifische
|
||
/// Zusatzfelder nicht verloren gehen.
|
||
branches Json @default("[]")
|
||
tags Json @default("[]")
|
||
commits Json @default("[]")
|
||
releases Json @default("[]")
|
||
languages Json @default("{}")
|
||
|
||
pushedAt DateTime?
|
||
fetchedAt DateTime @default(now())
|
||
}
|