- Use @prisma/adapter-pg for PrismaClient (Prisma 7 removed datasourceUrl/datasources) - prisma.config.ts: migrate.url for CLI commands - schema.prisma: no url in datasource (Prisma 7 requirement)
256 lines
7.5 KiB
Plaintext
256 lines
7.5 KiB
Plaintext
// ============================================================
|
||
// LiveServer-M1 — Database Schema
|
||
// Educational video conferencing platform on LiveKit
|
||
// ============================================================
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
}
|
||
|
||
// ======================== ENUMS =============================
|
||
|
||
enum UserRole {
|
||
ADMIN
|
||
HOST
|
||
GUEST
|
||
}
|
||
|
||
enum RoomStatus {
|
||
WAITING // Создана, но лекция не началась
|
||
ACTIVE // Лекция идёт
|
||
ENDED // Лекция завершена
|
||
}
|
||
|
||
enum LobbyStatus {
|
||
PENDING
|
||
APPROVED
|
||
REJECTED
|
||
}
|
||
|
||
// ======================== AUTH ==============================
|
||
// Таблицы совместимы с better-auth (Prisma adapter)
|
||
// Docs: https://www.better-auth.com/docs/adapters/prisma
|
||
|
||
model User {
|
||
id String @id @default(cuid())
|
||
email String @unique
|
||
name String
|
||
emailVerified Boolean @default(false)
|
||
image String?
|
||
role UserRole @default(HOST)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
// better-auth relations
|
||
sessions Session[]
|
||
accounts Account[]
|
||
|
||
// App relations
|
||
hostedRooms Room[] @relation("RoomHost")
|
||
participantHistories ParticipantHistory[]
|
||
bannedByEntries BannedEntry[] @relation("BannedBy")
|
||
|
||
@@map("users")
|
||
}
|
||
|
||
model Session {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
token String @unique
|
||
expiresAt DateTime
|
||
ipAddress String?
|
||
userAgent String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([userId])
|
||
@@map("sessions")
|
||
}
|
||
|
||
model Account {
|
||
id String @id @default(cuid())
|
||
userId String
|
||
accountId String
|
||
providerId String
|
||
accessToken String?
|
||
refreshToken String?
|
||
accessTokenExpiresAt DateTime?
|
||
refreshTokenExpiresAt DateTime?
|
||
scope String?
|
||
idToken String?
|
||
password String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([userId])
|
||
@@map("accounts")
|
||
}
|
||
|
||
model Verification {
|
||
id String @id @default(cuid())
|
||
identifier String
|
||
value String
|
||
expiresAt DateTime
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@map("verifications")
|
||
}
|
||
|
||
// ======================== ROOMS =============================
|
||
|
||
model Room {
|
||
id String @id @default(cuid())
|
||
name String
|
||
code String @unique @default(cuid()) // Короткий код для инвайт-ссылки
|
||
hostId String
|
||
status RoomStatus @default(WAITING)
|
||
|
||
// Security settings
|
||
lobbyEnabled Boolean @default(true)
|
||
pinHash String? // bcrypt hash, null = без PIN
|
||
webinarMode Boolean @default(false)
|
||
isLocked Boolean @default(false) // Хост может заблокировать вход полностью
|
||
|
||
// LiveKit
|
||
livekitRoomName String? @unique // Имя комнаты в LiveKit (создаётся при старте)
|
||
|
||
// Timestamps
|
||
createdAt DateTime @default(now())
|
||
startedAt DateTime? // Когда хост начал лекцию
|
||
endedAt DateTime? // Когда завершилась
|
||
|
||
// Relations
|
||
host User @relation("RoomHost", fields: [hostId], references: [id])
|
||
lobbyEntries LobbyEntry[]
|
||
participantHistories ParticipantHistory[]
|
||
chatMessages ChatMessage[]
|
||
sharedFiles SharedFile[]
|
||
bannedEntries BannedEntry[]
|
||
lectureArtifact LectureArtifact?
|
||
|
||
@@index([hostId])
|
||
@@index([status])
|
||
@@index([code])
|
||
@@map("rooms")
|
||
}
|
||
|
||
// ======================== LOBBY =============================
|
||
|
||
model LobbyEntry {
|
||
id String @id @default(cuid())
|
||
roomId String
|
||
displayName String
|
||
sessionId String // UUID, генерируется на клиенте
|
||
sessionFingerprint String? // Browser fingerprint hash
|
||
ipAddress String?
|
||
status LobbyStatus @default(PENDING)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([roomId, status])
|
||
@@index([sessionId])
|
||
@@map("lobby_entries")
|
||
}
|
||
|
||
// ======================== PARTICIPANTS ======================
|
||
|
||
model ParticipantHistory {
|
||
id String @id @default(cuid())
|
||
roomId String
|
||
userId String? // null для гостей без аккаунта
|
||
sessionId String // UUID, совпадает с LobbyEntry.sessionId
|
||
displayName String
|
||
role UserRole
|
||
joinedAt DateTime @default(now())
|
||
leftAt DateTime?
|
||
|
||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||
|
||
@@index([roomId])
|
||
@@index([userId])
|
||
@@index([sessionId])
|
||
@@map("participant_histories")
|
||
}
|
||
|
||
// ======================== CHAT ==============================
|
||
|
||
model ChatMessage {
|
||
id String @id @default(cuid())
|
||
roomId String
|
||
sessionId String // Кто отправил (связь через sessionId, не userId — гости тоже пишут)
|
||
senderName String
|
||
content String
|
||
createdAt DateTime @default(now())
|
||
|
||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([roomId, createdAt])
|
||
@@map("chat_messages")
|
||
}
|
||
|
||
// ======================== FILES =============================
|
||
|
||
model SharedFile {
|
||
id String @id @default(cuid())
|
||
roomId String
|
||
sessionId String // Кто загрузил
|
||
fileName String
|
||
fileKey String // S3 object key
|
||
fileSize Int
|
||
mimeType String
|
||
createdAt DateTime @default(now())
|
||
|
||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([roomId])
|
||
@@map("shared_files")
|
||
}
|
||
|
||
// ======================== BANS ==============================
|
||
|
||
model BannedEntry {
|
||
id String @id @default(cuid())
|
||
roomId String
|
||
sessionFingerprint String // Основной идентификатор для бана
|
||
ipAddress String? // Вторичный сигнал
|
||
displayName String? // Для UI — кого забанили
|
||
reason String?
|
||
bannedById String // Хост, который забанил
|
||
createdAt DateTime @default(now())
|
||
|
||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||
bannedBy User @relation("BannedBy", fields: [bannedById], references: [id])
|
||
|
||
@@unique([roomId, sessionFingerprint])
|
||
@@index([roomId])
|
||
@@map("banned_entries")
|
||
}
|
||
|
||
// ======================== LECTURE ARTIFACTS ==================
|
||
|
||
model LectureArtifact {
|
||
id String @id @default(cuid())
|
||
roomId String @unique // 1:1 с Room
|
||
transcriptText String? @db.Text // Полный текст транскрипта (для поиска)
|
||
transcriptUrl String? // S3 key для raw-файла транскрипта
|
||
summary String? @db.Text // AI-сгенерированная суммаризация (Markdown/JSON)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||
|
||
@@map("lecture_artifacts")
|
||
}
|