security hardening, team invites, granular locking, view counts, board subscriptions, scheduled changelog, mentions, recovery codes, accessibility and hover states
This commit is contained in:
@@ -17,32 +17,46 @@ enum PostType {
|
||||
BUG_REPORT
|
||||
}
|
||||
|
||||
enum PostStatus {
|
||||
OPEN
|
||||
UNDER_REVIEW
|
||||
PLANNED
|
||||
IN_PROGRESS
|
||||
DONE
|
||||
DECLINED
|
||||
}
|
||||
|
||||
model Board {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
name String
|
||||
description String?
|
||||
externalUrl String?
|
||||
iconName String?
|
||||
iconColor String?
|
||||
isArchived Boolean @default(false)
|
||||
voteBudget Int @default(10)
|
||||
voteBudgetReset String @default("monthly")
|
||||
lastBudgetReset DateTime?
|
||||
allowMultiVote Boolean @default(false)
|
||||
rssEnabled Boolean @default(true)
|
||||
rssFeedCount Int @default(50)
|
||||
staleDays Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
posts Post[]
|
||||
activityEvents ActivityEvent[]
|
||||
pushSubscriptions PushSubscription[]
|
||||
statusConfig BoardStatus[]
|
||||
templates BoardTemplate[]
|
||||
changelogEntries ChangelogEntry[]
|
||||
}
|
||||
|
||||
model BoardStatus {
|
||||
id String @id @default(cuid())
|
||||
boardId String
|
||||
status String
|
||||
label String
|
||||
color String
|
||||
position Int @default(0)
|
||||
enabled Boolean @default(true)
|
||||
|
||||
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([boardId, status])
|
||||
@@index([boardId, position])
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -52,16 +66,35 @@ model User {
|
||||
username String?
|
||||
usernameIdx String? @unique
|
||||
displayName String?
|
||||
avatarPath String?
|
||||
darkMode String @default("system")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
passkeys Passkey[]
|
||||
posts Post[]
|
||||
comments Comment[]
|
||||
reactions Reaction[]
|
||||
votes Vote[]
|
||||
passkeys Passkey[]
|
||||
posts Post[]
|
||||
comments Comment[]
|
||||
reactions Reaction[]
|
||||
votes Vote[]
|
||||
notifications Notification[]
|
||||
pushSubscriptions PushSubscription[]
|
||||
attachments Attachment[]
|
||||
adminLink AdminUser?
|
||||
edits EditHistory[]
|
||||
recoveryCode RecoveryCode?
|
||||
}
|
||||
|
||||
model RecoveryCode {
|
||||
id String @id @default(cuid())
|
||||
codeHash String
|
||||
phraseIdx String @unique
|
||||
userId String @unique
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
model Passkey {
|
||||
@@ -80,18 +113,26 @@ model Passkey {
|
||||
}
|
||||
|
||||
model Post {
|
||||
id String @id @default(cuid())
|
||||
type PostType
|
||||
title String
|
||||
description Json
|
||||
status PostStatus @default(OPEN)
|
||||
category String?
|
||||
voteCount Int @default(0)
|
||||
isPinned Boolean @default(false)
|
||||
boardId String
|
||||
authorId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
type PostType
|
||||
title String
|
||||
description Json
|
||||
status String @default("OPEN")
|
||||
statusReason String?
|
||||
category String?
|
||||
templateId String?
|
||||
voteCount Int @default(0)
|
||||
viewCount Int @default(0)
|
||||
isPinned Boolean @default(false)
|
||||
isEditLocked Boolean @default(false)
|
||||
isThreadLocked Boolean @default(false)
|
||||
isVotingLocked Boolean @default(false)
|
||||
onBehalfOf String?
|
||||
boardId String
|
||||
authorId String
|
||||
lastActivityAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
||||
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||
@@ -99,15 +140,23 @@ model Post {
|
||||
comments Comment[]
|
||||
votes Vote[]
|
||||
adminResponses AdminResponse[]
|
||||
adminNotes AdminNote[]
|
||||
notifications Notification[]
|
||||
activityEvents ActivityEvent[]
|
||||
pushSubscriptions PushSubscription[]
|
||||
tags PostTag[]
|
||||
attachments Attachment[]
|
||||
editHistory EditHistory[]
|
||||
|
||||
@@index([boardId, status])
|
||||
}
|
||||
|
||||
model StatusChange {
|
||||
id String @id @default(cuid())
|
||||
postId String
|
||||
fromStatus PostStatus
|
||||
toStatus PostStatus
|
||||
fromStatus String
|
||||
toStatus String
|
||||
reason String?
|
||||
changedBy String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@ -115,16 +164,28 @@ model StatusChange {
|
||||
}
|
||||
|
||||
model Comment {
|
||||
id String @id @default(cuid())
|
||||
body String
|
||||
postId String
|
||||
authorId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
body String
|
||||
postId String
|
||||
authorId String
|
||||
replyToId String?
|
||||
isAdmin Boolean @default(false)
|
||||
isEditLocked Boolean @default(false)
|
||||
adminUserId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||
reactions Reaction[]
|
||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||
replyTo Comment? @relation("CommentReplies", fields: [replyToId], references: [id], onDelete: SetNull)
|
||||
replies Comment[] @relation("CommentReplies")
|
||||
adminUser AdminUser? @relation(fields: [adminUserId], references: [id], onDelete: SetNull)
|
||||
reactions Reaction[]
|
||||
attachments Attachment[]
|
||||
editHistory EditHistory[]
|
||||
|
||||
@@index([replyToId])
|
||||
@@index([postId, createdAt])
|
||||
}
|
||||
|
||||
model Reaction {
|
||||
@@ -143,6 +204,7 @@ model Reaction {
|
||||
model Vote {
|
||||
id String @id @default(cuid())
|
||||
weight Int @default(1)
|
||||
importance String?
|
||||
postId String
|
||||
voterId String
|
||||
budgetPeriod String
|
||||
@@ -154,13 +216,50 @@ model Vote {
|
||||
@@unique([postId, voterId])
|
||||
}
|
||||
|
||||
model AdminUser {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
passwordHash String
|
||||
createdAt DateTime @default(now())
|
||||
enum TeamRole {
|
||||
SUPER_ADMIN
|
||||
ADMIN
|
||||
MODERATOR
|
||||
}
|
||||
|
||||
responses AdminResponse[]
|
||||
model AdminUser {
|
||||
id String @id @default(cuid())
|
||||
email String? @unique
|
||||
passwordHash String?
|
||||
role TeamRole @default(ADMIN)
|
||||
displayName String?
|
||||
teamTitle String?
|
||||
linkedUserId String? @unique
|
||||
invitedById String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
linkedUser User? @relation(fields: [linkedUserId], references: [id], onDelete: SetNull)
|
||||
invitedBy AdminUser? @relation("TeamInviter", fields: [invitedById], references: [id], onDelete: SetNull)
|
||||
invitees AdminUser[] @relation("TeamInviter")
|
||||
responses AdminResponse[]
|
||||
notes AdminNote[]
|
||||
comments Comment[]
|
||||
createdInvites TeamInvite[] @relation("InviteCreator")
|
||||
claimedInvite TeamInvite? @relation("InviteClaimer")
|
||||
}
|
||||
|
||||
model TeamInvite {
|
||||
id String @id @default(cuid())
|
||||
tokenHash String @unique
|
||||
role TeamRole
|
||||
label String?
|
||||
expiresAt DateTime
|
||||
createdById String
|
||||
claimedById String? @unique
|
||||
claimedAt DateTime?
|
||||
recoveryHash String?
|
||||
recoveryIdx String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
createdBy AdminUser @relation("InviteCreator", fields: [createdById], references: [id], onDelete: Cascade)
|
||||
claimedBy AdminUser? @relation("InviteClaimer", fields: [claimedById], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
model AdminResponse {
|
||||
@@ -190,15 +289,16 @@ model ActivityEvent {
|
||||
}
|
||||
|
||||
model PushSubscription {
|
||||
id String @id @default(cuid())
|
||||
endpoint String
|
||||
endpointIdx String @unique
|
||||
keysP256dh String
|
||||
keysAuth String
|
||||
userId String
|
||||
boardId String?
|
||||
postId String?
|
||||
createdAt DateTime @default(now())
|
||||
id String @id @default(cuid())
|
||||
endpoint String
|
||||
endpointIdx String @unique
|
||||
keysP256dh String
|
||||
keysAuth String
|
||||
userId String
|
||||
boardId String?
|
||||
postId String?
|
||||
failureCount Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
board Board? @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
||||
@@ -211,3 +311,151 @@ model Category {
|
||||
slug String @unique
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id String @id @default(cuid())
|
||||
type String
|
||||
title String
|
||||
body String
|
||||
postId String?
|
||||
userId String
|
||||
read Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
post Post? @relation(fields: [postId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([userId, read, createdAt])
|
||||
}
|
||||
|
||||
model Webhook {
|
||||
id String @id @default(cuid())
|
||||
url String
|
||||
secret String
|
||||
events String[] @default(["status_changed", "post_created", "comment_added"])
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model PostMerge {
|
||||
id String @id @default(cuid())
|
||||
sourcePostId String
|
||||
targetPostId String
|
||||
mergedBy String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([sourcePostId])
|
||||
}
|
||||
|
||||
model ChangelogEntry {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
body String
|
||||
boardId String?
|
||||
publishedAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
board Board? @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([boardId, publishedAt])
|
||||
}
|
||||
|
||||
model AdminNote {
|
||||
id String @id @default(cuid())
|
||||
body String
|
||||
postId String
|
||||
adminId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
admin AdminUser @relation(fields: [adminId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
color String @default("#6366F1")
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
posts PostTag[]
|
||||
}
|
||||
|
||||
model PostTag {
|
||||
postId String
|
||||
tagId String
|
||||
|
||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([postId, tagId])
|
||||
}
|
||||
|
||||
model Attachment {
|
||||
id String @id @default(cuid())
|
||||
filename String
|
||||
path String
|
||||
size Int
|
||||
mimeType String
|
||||
postId String?
|
||||
commentId String?
|
||||
uploaderId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
post Post? @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
||||
uploader User @relation(fields: [uploaderId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model BoardTemplate {
|
||||
id String @id @default(cuid())
|
||||
boardId String
|
||||
name String
|
||||
fields Json
|
||||
isDefault Boolean @default(false)
|
||||
position Int @default(0)
|
||||
|
||||
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([boardId, position])
|
||||
}
|
||||
|
||||
model SiteSettings {
|
||||
id String @id @default("default")
|
||||
appName String @default("Echoboard")
|
||||
logoUrl String?
|
||||
faviconUrl String?
|
||||
accentColor String @default("#F59E0B")
|
||||
headerFont String?
|
||||
bodyFont String?
|
||||
poweredByVisible Boolean @default(true)
|
||||
customCss String?
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model BlockedToken {
|
||||
id String @id @default(cuid())
|
||||
tokenHash String @unique
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
model EditHistory {
|
||||
id String @id @default(cuid())
|
||||
postId String?
|
||||
commentId String?
|
||||
editedBy String?
|
||||
previousTitle String?
|
||||
previousDescription Json?
|
||||
previousBody String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
post Post? @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
||||
editor User? @relation(fields: [editedBy], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([postId, createdAt])
|
||||
@@index([commentId, createdAt])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user