- Add package.json for backend dependencies and scripts. - Create Prisma schema for multi-tenant event management. - Implement main application module and configure global settings. - Develop authentication module with JWT and Authentik integration. - Create DTOs for guest account creation and KC management. - Implement role-based access control with custom guards and decorators. - Add services and controllers for managing KCs and guest accounts. - Set up global validation and CORS in the main application entry point. - Establish Prisma module for database access throughout the application. - Document project plan and architecture for multi-tenant platform.
192 lines
5.2 KiB
Plaintext
192 lines
5.2 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
/// A Konfi-Castle event; the top-level tenant. One instance manages many KCs.
|
|
model Kc {
|
|
id String @id @default(cuid())
|
|
name String
|
|
inviteCode String @unique
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
gemeinden Gemeinde[]
|
|
memberships Membership[]
|
|
wahlen Wahl[]
|
|
files File[]
|
|
channels ChatChannel[]
|
|
guests GuestAccount[]
|
|
}
|
|
|
|
/// A local congregation/community participating in one Kc.
|
|
model Gemeinde {
|
|
id String @id @default(cuid())
|
|
name String
|
|
kcId String
|
|
createdAt DateTime @default(now())
|
|
|
|
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
|
memberships Membership[]
|
|
guests GuestAccount[]
|
|
|
|
@@unique([kcId, name])
|
|
}
|
|
|
|
enum Role {
|
|
LEITUNGSTEAM
|
|
GEMEINDE_VERANTWORTLICHER
|
|
GEMEINDE_TEAMER
|
|
}
|
|
|
|
/// Authentik-backed user (team member with elevated rights).
|
|
model User {
|
|
id String @id @default(cuid())
|
|
authentikSub String @unique
|
|
email String @unique
|
|
firstName String
|
|
lastName String
|
|
createdAt DateTime @default(now())
|
|
|
|
memberships Membership[]
|
|
messages ChatMessage[]
|
|
}
|
|
|
|
/// Scopes a User's role to a specific Kc (and Gemeinde, if applicable).
|
|
/// LEITUNGSTEAM memberships apply to all Kcs implicitly and omit gemeindeId.
|
|
model Membership {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
kcId String
|
|
gemeindeId String?
|
|
role Role
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
|
gemeinde Gemeinde? @relation(fields: [gemeindeId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, kcId, gemeindeId])
|
|
}
|
|
|
|
/// Local, non-Authentik account for Konfis/guests, scoped to one Kc/event.
|
|
model GuestAccount {
|
|
id String @id @default(cuid())
|
|
kcId String
|
|
gemeindeId String?
|
|
firstName String
|
|
lastName String
|
|
createdAt DateTime @default(now())
|
|
|
|
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
|
gemeinde Gemeinde? @relation(fields: [gemeindeId], references: [id], onDelete: Cascade)
|
|
messages ChatMessage[]
|
|
teilnehmer Teilnehmer[]
|
|
}
|
|
|
|
/// A workshop election, scoped to a Kc; name carries a date key + "Teil".
|
|
model Wahl {
|
|
id String @id @default(cuid())
|
|
kcId String
|
|
name String
|
|
datumsSchluessel String
|
|
teil String
|
|
isOpen Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
|
workshops Workshop[]
|
|
teilnehmer Teilnehmer[]
|
|
}
|
|
|
|
model Workshop {
|
|
id String @id @default(cuid())
|
|
wahlId String
|
|
name String
|
|
kapazitaet Int
|
|
|
|
wahl Wahl @relation(fields: [wahlId], references: [id], onDelete: Cascade)
|
|
zuteilungen Zuteilung[]
|
|
}
|
|
|
|
/// A participant's submitted choices for a Wahl.
|
|
model Teilnehmer {
|
|
id String @id @default(cuid())
|
|
wahlId String
|
|
guestAccountId String
|
|
prioritaeten Json
|
|
createdAt DateTime @default(now())
|
|
|
|
wahl Wahl @relation(fields: [wahlId], references: [id], onDelete: Cascade)
|
|
guestAccount GuestAccount @relation(fields: [guestAccountId], references: [id], onDelete: Cascade)
|
|
zuteilung Zuteilung?
|
|
|
|
@@unique([wahlId, guestAccountId])
|
|
}
|
|
|
|
/// Result of the assignment algorithm (or a manual force-assignment) for one Teilnehmer.
|
|
model Zuteilung {
|
|
id String @id @default(cuid())
|
|
teilnehmerId String @unique
|
|
workshopId String
|
|
isForced Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
teilnehmer Teilnehmer @relation(fields: [teilnehmerId], references: [id], onDelete: Cascade)
|
|
workshop Workshop @relation(fields: [workshopId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
enum FileVisibility {
|
|
ALLE
|
|
ALLE_AUSSER_KONFIS
|
|
NUR_LT
|
|
}
|
|
|
|
model File {
|
|
id String @id @default(cuid())
|
|
kcId String
|
|
storageKey String
|
|
filename String
|
|
visibility FileVisibility
|
|
uploadedById String
|
|
createdAt DateTime @default(now())
|
|
|
|
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
enum ChatChannelType {
|
|
GEMEINDE_GRUPPE
|
|
DIREKT
|
|
LT_UEBERGREIFEND
|
|
BROADCAST
|
|
}
|
|
|
|
model ChatChannel {
|
|
id String @id @default(cuid())
|
|
kcId String
|
|
type ChatChannelType
|
|
gemeindeId String?
|
|
createdAt DateTime @default(now())
|
|
|
|
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
|
messages ChatMessage[]
|
|
}
|
|
|
|
model ChatMessage {
|
|
id String @id @default(cuid())
|
|
channelId String
|
|
senderUserId String?
|
|
senderGuestId String?
|
|
body String
|
|
createdAt DateTime @default(now())
|
|
|
|
channel ChatChannel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
|
senderUser User? @relation(fields: [senderUserId], references: [id])
|
|
senderGuest GuestAccount? @relation(fields: [senderGuestId], references: [id])
|
|
}
|