security hardening, team invites, granular locking, view counts, board subscriptions, scheduled changelog, mentions, recovery codes, accessibility and hover states

This commit is contained in:
2026-03-21 17:37:01 +02:00
parent f07eddf29e
commit 5ba25fb956
142 changed files with 30397 additions and 2287 deletions

View File

@@ -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])
}