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

@@ -0,0 +1,270 @@
-- CreateEnum
CREATE TYPE "AuthMethod" AS ENUM ('COOKIE', 'PASSKEY');
-- CreateEnum
CREATE TYPE "PostType" AS ENUM ('FEATURE_REQUEST', 'BUG_REPORT');
-- CreateEnum
CREATE TYPE "PostStatus" AS ENUM ('OPEN', 'UNDER_REVIEW', 'PLANNED', 'IN_PROGRESS', 'DONE', 'DECLINED');
-- CreateTable
CREATE TABLE "Board" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"externalUrl" TEXT,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"voteBudget" INTEGER NOT NULL DEFAULT 10,
"voteBudgetReset" TEXT NOT NULL DEFAULT 'monthly',
"lastBudgetReset" TIMESTAMP(3),
"allowMultiVote" BOOLEAN NOT NULL DEFAULT false,
"rssEnabled" BOOLEAN NOT NULL DEFAULT true,
"rssFeedCount" INTEGER NOT NULL DEFAULT 50,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Board_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"authMethod" "AuthMethod" NOT NULL DEFAULT 'COOKIE',
"tokenHash" TEXT,
"username" TEXT,
"usernameIdx" TEXT,
"displayName" TEXT,
"darkMode" TEXT NOT NULL DEFAULT 'system',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Passkey" (
"id" TEXT NOT NULL,
"credentialId" TEXT NOT NULL,
"credentialIdIdx" TEXT NOT NULL,
"credentialPublicKey" BYTEA NOT NULL,
"counter" BIGINT NOT NULL,
"credentialDeviceType" TEXT NOT NULL,
"credentialBackedUp" BOOLEAN NOT NULL,
"transports" TEXT,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Passkey_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Post" (
"id" TEXT NOT NULL,
"type" "PostType" NOT NULL,
"title" TEXT NOT NULL,
"description" JSONB NOT NULL,
"status" "PostStatus" NOT NULL DEFAULT 'OPEN',
"category" TEXT,
"voteCount" INTEGER NOT NULL DEFAULT 0,
"isPinned" BOOLEAN NOT NULL DEFAULT false,
"boardId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "StatusChange" (
"id" TEXT NOT NULL,
"postId" TEXT NOT NULL,
"fromStatus" "PostStatus" NOT NULL,
"toStatus" "PostStatus" NOT NULL,
"changedBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "StatusChange_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Comment" (
"id" TEXT NOT NULL,
"body" TEXT NOT NULL,
"postId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Reaction" (
"id" TEXT NOT NULL,
"emoji" TEXT NOT NULL,
"commentId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Reaction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Vote" (
"id" TEXT NOT NULL,
"weight" INTEGER NOT NULL DEFAULT 1,
"postId" TEXT NOT NULL,
"voterId" TEXT NOT NULL,
"budgetPeriod" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Vote_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AdminUser" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AdminUser_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AdminResponse" (
"id" TEXT NOT NULL,
"body" TEXT NOT NULL,
"postId" TEXT NOT NULL,
"adminId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AdminResponse_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ActivityEvent" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"boardId" TEXT NOT NULL,
"postId" TEXT,
"metadata" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ActivityEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PushSubscription" (
"id" TEXT NOT NULL,
"endpoint" TEXT NOT NULL,
"endpointIdx" TEXT NOT NULL,
"keysP256dh" TEXT NOT NULL,
"keysAuth" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"boardId" TEXT,
"postId" TEXT,
"failureCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PushSubscription_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Category" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Board_slug_key" ON "Board"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "User_tokenHash_key" ON "User"("tokenHash");
-- CreateIndex
CREATE UNIQUE INDEX "User_usernameIdx_key" ON "User"("usernameIdx");
-- CreateIndex
CREATE UNIQUE INDEX "Passkey_credentialIdIdx_key" ON "Passkey"("credentialIdIdx");
-- CreateIndex
CREATE UNIQUE INDEX "Reaction_commentId_userId_emoji_key" ON "Reaction"("commentId", "userId", "emoji");
-- CreateIndex
CREATE UNIQUE INDEX "Vote_postId_voterId_key" ON "Vote"("postId", "voterId");
-- CreateIndex
CREATE UNIQUE INDEX "AdminUser_email_key" ON "AdminUser"("email");
-- CreateIndex
CREATE INDEX "ActivityEvent_boardId_createdAt_idx" ON "ActivityEvent"("boardId", "createdAt");
-- CreateIndex
CREATE INDEX "ActivityEvent_createdAt_idx" ON "ActivityEvent"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "PushSubscription_endpointIdx_key" ON "PushSubscription"("endpointIdx");
-- CreateIndex
CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Category_slug_key" ON "Category"("slug");
-- AddForeignKey
ALTER TABLE "Passkey" ADD CONSTRAINT "Passkey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "StatusChange" ADD CONSTRAINT "StatusChange_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Vote" ADD CONSTRAINT "Vote_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Vote" ADD CONSTRAINT "Vote_voterId_fkey" FOREIGN KEY ("voterId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AdminResponse" ADD CONSTRAINT "AdminResponse_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AdminResponse" ADD CONSTRAINT "AdminResponse_adminId_fkey" FOREIGN KEY ("adminId") REFERENCES "AdminUser"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ActivityEvent" ADD CONSTRAINT "ActivityEvent_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ActivityEvent" ADD CONSTRAINT "ActivityEvent_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PushSubscription" ADD CONSTRAINT "PushSubscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PushSubscription" ADD CONSTRAINT "PushSubscription_boardId_fkey" FOREIGN KEY ("boardId") REFERENCES "Board"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PushSubscription" ADD CONSTRAINT "PushSubscription_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,28 @@
-- CreateTable
CREATE TABLE "EditHistory" (
"id" TEXT NOT NULL,
"postId" TEXT,
"commentId" TEXT,
"editedBy" TEXT NOT NULL,
"previousTitle" TEXT,
"previousDescription" JSONB,
"previousBody" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "EditHistory_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "EditHistory_postId_createdAt_idx" ON "EditHistory"("postId", "createdAt");
-- CreateIndex
CREATE INDEX "EditHistory_commentId_createdAt_idx" ON "EditHistory"("commentId", "createdAt");
-- AddForeignKey
ALTER TABLE "EditHistory" ADD CONSTRAINT "EditHistory_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EditHistory" ADD CONSTRAINT "EditHistory_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "EditHistory" ADD CONSTRAINT "EditHistory_editedBy_fkey" FOREIGN KEY ("editedBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Post" ADD COLUMN "isEditLocked" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "EditHistory" ALTER COLUMN "editedBy" DROP NOT NULL;
-- DropForeignKey
ALTER TABLE "EditHistory" DROP CONSTRAINT "EditHistory_editedBy_fkey";
-- AddForeignKey
ALTER TABLE "EditHistory" ADD CONSTRAINT "EditHistory_editedBy_fkey" FOREIGN KEY ("editedBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,16 @@
CREATE TABLE "SiteSettings" (
"id" TEXT NOT NULL DEFAULT 'default',
"appName" TEXT NOT NULL DEFAULT 'Echoboard',
"logoUrl" TEXT,
"faviconUrl" TEXT,
"accentColor" TEXT NOT NULL DEFAULT '#F59E0B',
"headerFont" TEXT,
"bodyFont" TEXT,
"poweredByVisible" BOOLEAN NOT NULL DEFAULT true,
"customCss" TEXT,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SiteSettings_pkey" PRIMARY KEY ("id")
);
INSERT INTO "SiteSettings" ("id", "updatedAt") VALUES ('default', NOW());

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

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

1298
packages/api/prisma/seed.ts Normal file

File diff suppressed because it is too large Load Diff