mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-09 15:50:58 +00:00
- Nouvelle table tree_immich_configs (adresse + clé d'API chiffrée AES-256-GCM, clé dérivée de JWT_SECRET) et routes GET/PUT/DELETE /trees/:id/immich/config réservées aux administrateurs de l'arbre ; journal d'audit sans secret (adresse et « clé changée » uniquement). - Fin des variables d'env IMMICH_ENABLED / IMMICH_API_URL / IMMICH_API_KEY : l'adresse se choisit parmi IMMICH_ALLOWED_API_URLS, la liste établie par l'opérateur du serveur. Elle filtre l'enregistrement ET chaque requête : retirer une adresse coupe aussitôt les connexions qui l'utilisent. - Changer d'instance exige une nouvelle clé : l'ancienne est écrasée et ne voyage jamais vers le nouveau serveur. - Le désassignement reste possible sans connexion : purement local, il nettoie association et portrait stocké même après suppression de la connexion ; le panneau affiche « Retirer l'association » dans tous les états. - Onglet Immich dans Paramètres de l'arbre : menu des instances autorisées, test de connexion, suppression avec confirmation ; i18n anglais des nouveaux messages. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
398 lines
12 KiB
Plaintext
398 lines
12 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
enum MembershipRole {
|
|
owner
|
|
admin
|
|
member
|
|
}
|
|
|
|
enum ContributionPolicy {
|
|
direct
|
|
pending
|
|
}
|
|
|
|
enum VisitorPolicy {
|
|
read_only
|
|
}
|
|
|
|
enum ContributionSessionStatus {
|
|
pending
|
|
approved
|
|
rejected
|
|
}
|
|
|
|
enum ContributionActorMode {
|
|
user
|
|
tree_access
|
|
}
|
|
|
|
enum ContributionEntityType {
|
|
person
|
|
union
|
|
parent_child_link
|
|
annotation
|
|
media
|
|
}
|
|
|
|
// Souvenir envoyé par la famille : en attente jusqu'à la relecture du propriétaire
|
|
enum MediaStatus {
|
|
approved
|
|
pending
|
|
rejected
|
|
}
|
|
|
|
enum AnnotationType {
|
|
drawing
|
|
sticker
|
|
text
|
|
photo
|
|
}
|
|
|
|
enum ContributionAction {
|
|
create
|
|
update
|
|
delete
|
|
reorder
|
|
}
|
|
|
|
enum ConflictState {
|
|
none
|
|
needs_review
|
|
}
|
|
|
|
enum ContributionDecision {
|
|
approved
|
|
rejected
|
|
}
|
|
|
|
enum SharedAccessRole {
|
|
visitor
|
|
contributor
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
passwordHash String
|
|
lastOpenedTreeId String?
|
|
lastOpenedAccessMode String?
|
|
lastOpenedRole String?
|
|
lastOpenedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
ownedTrees Tree[] @relation("TreeOwner")
|
|
memberships TreeMembership[]
|
|
sharedTreeAccesses UserTreeAccess[]
|
|
submittedContributionSessions ContributionSession[] @relation("ContributionSubmittedBy")
|
|
reviewedContributionSessions ContributionSession[] @relation("ContributionReviewedBy")
|
|
treeVisits TreeVisit[]
|
|
lastOpenedTree Tree? @relation("UserLastOpenedTree", fields: [lastOpenedTreeId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([lastOpenedAt])
|
|
@@map("users")
|
|
}
|
|
|
|
model Tree {
|
|
id String @id @default(cuid())
|
|
ownerUserId String
|
|
slug String @unique
|
|
name String
|
|
description String?
|
|
rootPersonId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
deletedAt DateTime?
|
|
owner User @relation("TreeOwner", fields: [ownerUserId], references: [id], onDelete: Restrict)
|
|
accessPasswords TreeAccessPasswords?
|
|
sharePassword TreeSharePassword?
|
|
memberships TreeMembership[]
|
|
sharedUserAccesses UserTreeAccess[]
|
|
settings TreeSettings?
|
|
immichConfig TreeImmichConfig?
|
|
persons Person[]
|
|
unions Union[]
|
|
parentChildLinks ParentChildLink[]
|
|
mediaItems MediaItem[]
|
|
contributionSessions ContributionSession[]
|
|
auditLogs AuditLog[]
|
|
annotations Annotation[]
|
|
visits TreeVisit[]
|
|
lastOpenedByUsers User[] @relation("UserLastOpenedTree")
|
|
|
|
@@index([ownerUserId])
|
|
@@map("trees")
|
|
}
|
|
|
|
model TreeAccessPasswords {
|
|
treeId String @id
|
|
visitorHash String
|
|
contributorHash String
|
|
updatedAt DateTime @updatedAt
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("tree_access_passwords")
|
|
}
|
|
|
|
// Mot de passe de partage en clair chiffré, pour que le propriétaire voie toujours celui en vigueur.
|
|
// Table à part : écrire dans tree_access_passwords changerait updatedAt, qui sert de version aux accès ouverts.
|
|
model TreeSharePassword {
|
|
treeId String @id
|
|
passwordEnc String
|
|
setAt DateTime @default(now())
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("tree_share_passwords")
|
|
}
|
|
|
|
model TreeMembership {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
userId String
|
|
role MembershipRole
|
|
contributionModeOverride ContributionPolicy?
|
|
createdAt DateTime @default(now())
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([treeId, userId])
|
|
@@index([userId])
|
|
@@map("tree_memberships")
|
|
}
|
|
|
|
model UserTreeAccess {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
userId String
|
|
role SharedAccessRole
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @default(now()) @updatedAt
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([treeId, userId])
|
|
@@index([userId, updatedAt])
|
|
@@map("user_tree_accesses")
|
|
}
|
|
|
|
model TreeSettings {
|
|
treeId String @id
|
|
memberContributionPolicy ContributionPolicy @default(pending)
|
|
visitorPolicy VisitorPolicy @default(read_only)
|
|
contributorPolicy ContributionPolicy @default(pending)
|
|
updatedAt DateTime @updatedAt
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("tree_settings")
|
|
}
|
|
|
|
// Connexion Immich propre à chaque arbre (reconnaissance des visages) : l'adresse et la clé de
|
|
// l'instance sont choisies par ses administrateurs, jamais héritées du serveur. La clé est
|
|
// chiffrée au repos ; une ligne absente signifie « aucun accès Immich » pour cet arbre.
|
|
model TreeImmichConfig {
|
|
treeId String @id
|
|
apiUrl String
|
|
apiKeyEnc String
|
|
updatedAt DateTime @updatedAt
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("tree_immich_configs")
|
|
}
|
|
|
|
model Person {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
firstName String
|
|
lastName String
|
|
birthName String?
|
|
birthDate DateTime?
|
|
deathDate DateTime?
|
|
birthPlace String?
|
|
profession String?
|
|
region String?
|
|
nationality String?
|
|
avatarPath String?
|
|
avatarMimeType String?
|
|
sex String?
|
|
notes String?
|
|
createdBy String?
|
|
updatedBy String?
|
|
// Personne Immich associée (reconnaissance des visages) : identifiant côté Immich, ou null.
|
|
immichPersonId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
deletedAt DateTime?
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([treeId])
|
|
@@map("persons")
|
|
}
|
|
|
|
model Union {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
partner1PersonId String
|
|
partner2PersonId String?
|
|
unionType String?
|
|
startDate DateTime?
|
|
endDate DateTime?
|
|
displayOrder Int @default(1)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
deletedAt DateTime?
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([treeId])
|
|
@@map("unions")
|
|
}
|
|
|
|
model ParentChildLink {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
parentPersonId String
|
|
childPersonId String
|
|
viaUnionId String?
|
|
parentageType String?
|
|
displayOrder Int @default(1)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
deletedAt DateTime?
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([treeId])
|
|
@@map("parent_child_links")
|
|
}
|
|
|
|
model MediaItem {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
personId String
|
|
type String
|
|
mimeType String
|
|
filePath String
|
|
thumbPath String?
|
|
caption String?
|
|
source String?
|
|
sizeBytes Int
|
|
displayOrder Int @default(1)
|
|
isFeatured Boolean @default(false)
|
|
uploadedBy String?
|
|
status MediaStatus @default(approved)
|
|
contributionSessionId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
deletedAt DateTime?
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([treeId, personId, displayOrder])
|
|
@@map("media_items")
|
|
}
|
|
|
|
model ContributionSession {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
submittedByUserId String?
|
|
submittedByLabel String
|
|
comment String?
|
|
mode ContributionActorMode
|
|
status ContributionSessionStatus @default(pending)
|
|
title String
|
|
createdAt DateTime @default(now())
|
|
reviewedAt DateTime?
|
|
reviewedBy String?
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
submittedByUser User? @relation("ContributionSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull)
|
|
reviewer User? @relation("ContributionReviewedBy", fields: [reviewedBy], references: [id], onDelete: SetNull)
|
|
changes ContributionChange[]
|
|
|
|
@@index([treeId, status, createdAt])
|
|
@@index([submittedByUserId])
|
|
@@map("contribution_sessions")
|
|
}
|
|
|
|
model ContributionChange {
|
|
id String @id @default(cuid())
|
|
sessionId String
|
|
entityType ContributionEntityType
|
|
entityId String?
|
|
action ContributionAction
|
|
beforeJson Json?
|
|
afterJson Json?
|
|
conflictState ConflictState @default(none)
|
|
decision ContributionDecision?
|
|
createdAt DateTime @default(now())
|
|
session ContributionSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([sessionId])
|
|
@@map("contribution_changes")
|
|
}
|
|
|
|
model Annotation {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
type AnnotationType
|
|
x Float
|
|
y Float
|
|
content String
|
|
style Json @default("{}")
|
|
zIndex Int @default(0)
|
|
createdBy String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
deletedAt DateTime?
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([treeId])
|
|
@@map("annotations")
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
actorType String
|
|
actorId String?
|
|
action String
|
|
entityType String
|
|
entityId String
|
|
payloadJson Json?
|
|
createdAt DateTime @default(now())
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([treeId, createdAt])
|
|
@@map("audit_log")
|
|
}
|
|
|
|
// Journal des visites d'un arbre, lu par ses administrateurs (onglet Visites).
|
|
// Pas d'adresse IP : visitorKey est une empreinte (arbre + IP + navigateur) qui sert à compter les visiteurs distincts.
|
|
model TreeVisit {
|
|
id String @id @default(cuid())
|
|
treeId String
|
|
userId String?
|
|
accessKind String
|
|
visitorName String?
|
|
visitorKey String
|
|
device String
|
|
createdAt DateTime @default(now())
|
|
tree Tree @relation(fields: [treeId], references: [id], onDelete: Cascade)
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([treeId, createdAt])
|
|
@@index([treeId, visitorKey, createdAt])
|
|
@@map("tree_visits")
|
|
}
|
|
|
|
model WaitlistEntry {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
prenom String?
|
|
source String @default("direct")
|
|
createdAt DateTime @default(now())
|
|
|
|
@@map("waitlist_entries")
|
|
}
|