feat(chat): free-form GRUPPE channels with mutable participants
- ChatChannelType.GRUPPE: created by Leitungsteam (any KC) or a Gemeinde Verantwortliche/r (own KC), mixing team users and guests/Konfis as explicit ChatParticipant rows (unlike GEMEINDE_GRUPPE, membership is not derived from Gemeinde) - POST /chat/:kcId/gruppen to create, GET participant-candidates, and POST/DELETE /chat/gruppen/:channelId/participants to manage membership (creator, LT, or Verantwortliche/r of that KC) - ChatGateway broadcasts chat:participants-changed on membership change - PushService updated for nullable ChatParticipant.userId + new guestAccountId column - SyncService now replicates ChatParticipant - Prisma migration + 14 new unit tests (75/75 passing), tsc clean - CI: add .gitea/workflows/cybedefend-scan.yml + .cybedefend project config
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ChatChannelType" ADD VALUE 'GRUPPE';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ChatChannel" ADD COLUMN "createdByUserId" TEXT,
|
||||
ADD COLUMN "name" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ChatParticipant" ADD COLUMN "guestAccountId" TEXT,
|
||||
ALTER COLUMN "userId" DROP NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ChatParticipant_channelId_guestAccountId_key" ON "ChatParticipant"("channelId", "guestAccountId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ChatParticipant" ADD CONSTRAINT "ChatParticipant_guestAccountId_fkey" FOREIGN KEY ("guestAccountId") REFERENCES "GuestAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
+49
-33
@@ -16,14 +16,14 @@ model Kc {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
gemeinden Gemeinde[]
|
||||
memberships Membership[]
|
||||
wahlen Wahl[]
|
||||
files File[]
|
||||
channels ChatChannel[]
|
||||
guests GuestAccount[]
|
||||
localUsers User[]
|
||||
teamerInvites TeamerInvite[]
|
||||
gemeinden Gemeinde[]
|
||||
memberships Membership[]
|
||||
wahlen Wahl[]
|
||||
files File[]
|
||||
channels ChatChannel[]
|
||||
guests GuestAccount[]
|
||||
localUsers User[]
|
||||
teamerInvites TeamerInvite[]
|
||||
verantwortlicheInvites VerantwortlicheInvite[]
|
||||
}
|
||||
|
||||
@@ -34,10 +34,10 @@ model Gemeinde {
|
||||
kcId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
memberships Membership[]
|
||||
guests GuestAccount[]
|
||||
teamerInvites TeamerInvite[]
|
||||
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
memberships Membership[]
|
||||
guests GuestAccount[]
|
||||
teamerInvites TeamerInvite[]
|
||||
verantwortlicheInvites VerantwortlicheInvite[]
|
||||
|
||||
@@unique([kcId, name])
|
||||
@@ -110,11 +110,12 @@ model GuestAccount {
|
||||
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[]
|
||||
deviceTokens DeviceToken[]
|
||||
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
gemeinde Gemeinde? @relation(fields: [gemeindeId], references: [id], onDelete: Cascade)
|
||||
messages ChatMessage[]
|
||||
teilnehmer Teilnehmer[]
|
||||
deviceTokens DeviceToken[]
|
||||
chatParticipations ChatParticipant[]
|
||||
}
|
||||
|
||||
/// A push-notification target (FCM registration token) bound to whoever
|
||||
@@ -202,13 +203,13 @@ model Wahl {
|
||||
/// A workshop offered in one phase of a Wahl. `phase` is 1-based and must be
|
||||
/// <= the owning Wahl's `phasenAnzahl`.
|
||||
model Workshop {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
wahlId String
|
||||
phase Int @default(1)
|
||||
phase Int @default(1)
|
||||
name String
|
||||
beschreibung String?
|
||||
kapazitaet Int
|
||||
minTeilnehmer Int @default(0)
|
||||
minTeilnehmer Int @default(0)
|
||||
|
||||
wahl Wahl @relation(fields: [wahlId], references: [id], onDelete: Cascade)
|
||||
zuteilungen Zuteilung[]
|
||||
@@ -282,32 +283,47 @@ enum ChatChannelType {
|
||||
DIREKT
|
||||
LT_UEBERGREIFEND
|
||||
BROADCAST
|
||||
/// Freely composed group chat: created by a Leitungsteam member or a
|
||||
/// Gemeinde Verantwortliche/r (for their own KC), with an explicit,
|
||||
/// mutable participant list (team users and/or guests) via ChatParticipant
|
||||
/// - unlike GEMEINDE_GRUPPE, membership is not derived from Gemeinde.
|
||||
GRUPPE
|
||||
}
|
||||
|
||||
model ChatChannel {
|
||||
id String @id @default(cuid())
|
||||
kcId String
|
||||
type ChatChannelType
|
||||
gemeindeId String?
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(cuid())
|
||||
kcId String
|
||||
type ChatChannelType
|
||||
gemeindeId String?
|
||||
/// Display name; used by GRUPPE channels (optional for other types).
|
||||
name String?
|
||||
/// Who created the channel; only set for GRUPPE so far. Used to let the
|
||||
/// creator manage participants alongside Leitungsteam/Verantwortliche.
|
||||
createdByUserId String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
messages ChatMessage[]
|
||||
participants ChatParticipant[]
|
||||
}
|
||||
|
||||
/// Explicit membership for DIREKT (1:1) channels; other channel types derive
|
||||
/// access from Membership/Gemeinde instead of this table.
|
||||
/// Explicit membership for DIREKT (1:1) and GRUPPE channels; other channel
|
||||
/// types derive access from Membership/Gemeinde instead of this table.
|
||||
/// Exactly one of userId/guestAccountId is set per row.
|
||||
model ChatParticipant {
|
||||
id String @id @default(cuid())
|
||||
channelId String
|
||||
userId String
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(cuid())
|
||||
channelId String
|
||||
userId String?
|
||||
guestAccountId String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
channel ChatChannel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
channel ChatChannel @relation(fields: [channelId], references: [id], onDelete: Cascade)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
guestAccount GuestAccount? @relation(fields: [guestAccountId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([channelId, userId])
|
||||
@@unique([channelId, guestAccountId])
|
||||
}
|
||||
|
||||
model ChatMessage {
|
||||
|
||||
Reference in New Issue
Block a user