Introduce granular role-based permissions with SuperAdmin env override, admin roles UI, and permission-gated API routes. Fix admin user Nostr metadata by batching relay profile fetches, normalizing npub pubkeys to hex, and adding reusable NostrAvatar/useNostrProfile components. Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
5.4 KiB
Plaintext
208 lines
5.4 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "sqlite"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
pubkey String @unique
|
|
role String? // admin, moderator, writer. Null means Guest (no elevated role). SuperAdmin is env-sourced and never stored.
|
|
displayName String?
|
|
username String? @unique
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
// Maps an assignable role to a granted permission key. Editable at runtime.
|
|
// One row per (role, permission). SuperAdmin is never stored here; it bypasses
|
|
// all checks via the env list.
|
|
model RolePermission {
|
|
role String
|
|
permission String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([role, permission])
|
|
}
|
|
|
|
model Organizer {
|
|
id String @id @default(uuid())
|
|
name String
|
|
slug String @unique
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
meetups Meetup[]
|
|
}
|
|
|
|
model Meetup {
|
|
id String @id @default(uuid())
|
|
title String
|
|
description String
|
|
date String
|
|
time String
|
|
location String
|
|
link String?
|
|
imageId String?
|
|
status String @default("DRAFT") // DRAFT, PUBLISHED, CANCELLED (Upcoming/Past derived from date)
|
|
featured Boolean @default(false)
|
|
visibility String @default("PUBLIC") // PUBLIC, HIDDEN
|
|
organizerId String
|
|
organizer Organizer @relation(fields: [organizerId], references: [id], onDelete: Restrict)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Media {
|
|
id String @id // ULID, not auto-generated
|
|
slug String
|
|
type String // "image" | "video"
|
|
mimeType String
|
|
size Int
|
|
originalFilename String
|
|
uploadedBy String
|
|
title String?
|
|
description String?
|
|
altText String?
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model Post {
|
|
id String @id @default(uuid())
|
|
nostrEventId String @unique
|
|
naddr String?
|
|
title String
|
|
slug String @unique
|
|
content String @default("")
|
|
excerpt String?
|
|
authorPubkey String
|
|
authorName String?
|
|
featured Boolean @default(false)
|
|
visible Boolean @default(true)
|
|
publishedAt DateTime
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
categories PostCategory[]
|
|
}
|
|
|
|
model Category {
|
|
id String @id @default(uuid())
|
|
name String
|
|
slug String @unique
|
|
sortOrder Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
posts PostCategory[]
|
|
}
|
|
|
|
model PostCategory {
|
|
postId String
|
|
categoryId String
|
|
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([postId, categoryId])
|
|
}
|
|
|
|
model HiddenContent {
|
|
id String @id @default(uuid())
|
|
nostrEventId String
|
|
reason String?
|
|
hiddenBy String
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model BlockedPubkey {
|
|
id String @id @default(uuid())
|
|
pubkey String
|
|
reason String?
|
|
blockedBy String
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model Relay {
|
|
id String @id @default(uuid())
|
|
url String @unique
|
|
priority Int @default(0)
|
|
active Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model UserRelay {
|
|
id String @id @default(uuid())
|
|
pubkey String
|
|
url String
|
|
read Boolean @default(true)
|
|
write Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([pubkey, url])
|
|
}
|
|
|
|
model Setting {
|
|
id String @id @default(uuid())
|
|
key String @unique
|
|
value String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model NostrEventCache {
|
|
id String @id @default(uuid())
|
|
eventId String @unique
|
|
kind Int
|
|
pubkey String
|
|
content String
|
|
tags String // JSON string
|
|
createdAt Int // event timestamp
|
|
cachedAt DateTime @default(now())
|
|
}
|
|
|
|
model Submission {
|
|
id String @id @default(uuid())
|
|
eventId String?
|
|
naddr String?
|
|
title String
|
|
authorPubkey String
|
|
status String @default("PENDING") // PENDING, APPROVED, REJECTED
|
|
reviewedBy String?
|
|
reviewNote String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Faq {
|
|
id String @id @default(uuid())
|
|
question String
|
|
answer String
|
|
order Int @default(0)
|
|
showOnHomepage Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
/// Pending Lightning invoice metadata (message is created only after payment via webhook).
|
|
model BoardInvoicePending {
|
|
paymentHash String @id
|
|
content String
|
|
guestName String?
|
|
pubkey String?
|
|
profilePic String?
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
/// Paid board message (LNbits payment confirmed).
|
|
model BoardMessage {
|
|
id String @id @default(uuid())
|
|
paymentHash String @unique
|
|
content String
|
|
authorName String
|
|
pubkey String?
|
|
profilePic String?
|
|
satsPaid Int
|
|
status String @default("active") // active, hidden, deleted
|
|
likeCount Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
}
|