generated from nhcarrigan/template
7579f1ec97
## Summary This PR implements several improvements to the library application: - Added start and finish date tracking for media items - Added "Retired" category for abandoned media - Implemented avatar-based user menu with dropdown navigation - Added automatic background token refresh to prevent session expiry - Created centralised logging system with frontend-to-API log forwarding - Added toast notifications for error handling ## Changes ### Media Tracking (#41) - Added `dateStarted` and `dateFinished` fields to Books, Games, Manga, Music, and Shows - Updated TypeScript types, Prisma schema, and API services - Added manual date input fields to frontend forms - Properly converts HTML date strings to Date objects before API submission ### Retired Category (#43) - Added `RETIRED` status to all media type enums - Updated Prisma schema, frontend dropdowns, and filter buttons - Added status label handling for retired items ### User Menu (#46) - Replaced username text with avatar image in header - Created dropdown menu with navigation items (Users, Audit, Suggestions) - Added logout button to menu - Implemented keyboard accessibility (tabindex, role, keyup handlers) ### Token Refresh (#44) - Implemented automatic token refresh every 13 minutes in background - Added proactive refresh to prevent token expiry during form filling - Prevents users from losing form data due to expired sessions ### Centralised Logging (#1) - Created `/log` endpoint on API to receive frontend logs - Replaced API console.log calls with @nhcarrigan/logger - Created ConsoleLoggerService to intercept all console methods on frontend - Added global error handlers (window.error, unhandledrejection) on frontend - Added process error handlers (uncaughtException, unhandledRejection, SIGTERM, SIGINT) on API - All frontend console activity now forwarded to centralised logging ### Error Handling - Created ToastService and ToastComponent for displaying errors - Integrated with GlobalErrorHandler and HTTP interceptor - Added accessibility features (keyboard navigation, ARIA attributes) - Set toast opacity to 40% for optimal readability ### Testing & Build - Fixed pre-existing test failure for GET / route (now returns version info) - Added ESM module mocking (jsdom, marked, dompurify, @nhcarrigan/logger) - Configured Jest with isolatedModules to handle TypeScript errors - Excluded test-setup.ts from production build - All tests passing (123 total) - Build passing with no errors ## Test Plan - [x] All tests pass (123 tests) - [x] Build passes without errors - [x] Lint passes (only pre-existing warnings) - [x] Date fields work correctly on all media types - [x] Retired status displays and filters properly - [x] Avatar menu opens/closes correctly with keyboard and mouse - [x] Token refresh prevents session expiry - [x] Toast notifications appear for errors - [x] Frontend logs forward to API successfully - [x] Root route returns version information Closes #41 Closes #43 Closes #44 Closes #46 Closes #1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Hikari <hikari@nhcarrigan.com> Reviewed-on: #50 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
319 lines
7.6 KiB
Plaintext
319 lines
7.6 KiB
Plaintext
// This is your Prisma schema file,
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
|
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "mongodb"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
type Link {
|
|
title String
|
|
url String
|
|
}
|
|
|
|
model Game {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
title String
|
|
platform String?
|
|
status GameStatus
|
|
dateAdded DateTime @default(now())
|
|
dateStarted DateTime?
|
|
dateCompleted DateTime?
|
|
dateFinished DateTime?
|
|
rating Int? @db.Int @default(0)
|
|
notes String?
|
|
coverImage String?
|
|
tags String[]
|
|
links Link[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
comments Comment[]
|
|
}
|
|
|
|
enum GameStatus {
|
|
PLAYING
|
|
COMPLETED
|
|
BACKLOG
|
|
RETIRED
|
|
}
|
|
|
|
model Book {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
title String
|
|
author String
|
|
isbn String?
|
|
status BookStatus
|
|
dateAdded DateTime @default(now())
|
|
dateStarted DateTime?
|
|
dateFinished DateTime?
|
|
rating Int? @db.Int @default(0)
|
|
notes String?
|
|
coverImage String?
|
|
tags String[]
|
|
links Link[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
comments Comment[]
|
|
}
|
|
|
|
enum BookStatus {
|
|
READING
|
|
FINISHED
|
|
TO_READ
|
|
RETIRED
|
|
}
|
|
|
|
model Music {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
title String
|
|
artist String
|
|
type MusicType
|
|
status MusicStatus
|
|
dateAdded DateTime @default(now())
|
|
dateStarted DateTime?
|
|
dateCompleted DateTime?
|
|
dateFinished DateTime?
|
|
rating Int? @db.Int @default(0)
|
|
notes String?
|
|
coverArt String?
|
|
tags String[]
|
|
links Link[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
comments Comment[]
|
|
}
|
|
|
|
enum MusicType {
|
|
ALBUM
|
|
SINGLE
|
|
EP
|
|
}
|
|
|
|
enum MusicStatus {
|
|
LISTENING
|
|
COMPLETED
|
|
WANT_TO_LISTEN
|
|
RETIRED
|
|
}
|
|
|
|
model Art {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
title String
|
|
artist String
|
|
description String?
|
|
imageUrl String
|
|
tags String[]
|
|
links Link[]
|
|
dateAdded DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
comments Comment[]
|
|
}
|
|
|
|
model Show {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
title String
|
|
type ShowType
|
|
status ShowStatus
|
|
dateAdded DateTime @default(now())
|
|
dateStarted DateTime?
|
|
dateCompleted DateTime?
|
|
dateFinished DateTime?
|
|
rating Int? @db.Int @default(0)
|
|
notes String?
|
|
coverImage String?
|
|
tags String[]
|
|
links Link[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
comments Comment[]
|
|
}
|
|
|
|
enum ShowType {
|
|
TV_SERIES
|
|
ANIME
|
|
FILM
|
|
DOCUMENTARY
|
|
}
|
|
|
|
enum ShowStatus {
|
|
WATCHING
|
|
COMPLETED
|
|
WANT_TO_WATCH
|
|
RETIRED
|
|
}
|
|
|
|
model Manga {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
title String
|
|
author String
|
|
status MangaStatus
|
|
dateAdded DateTime @default(now())
|
|
dateStarted DateTime?
|
|
dateCompleted DateTime?
|
|
dateFinished DateTime?
|
|
rating Int? @db.Int @default(0)
|
|
notes String?
|
|
coverImage String?
|
|
tags String[]
|
|
links Link[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
comments Comment[]
|
|
}
|
|
|
|
enum MangaStatus {
|
|
READING
|
|
COMPLETED
|
|
WANT_TO_READ
|
|
RETIRED
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
discordId String @unique
|
|
username String
|
|
email String @unique
|
|
avatar String?
|
|
isAdmin Boolean @default(false)
|
|
isBanned Boolean @default(false)
|
|
inDiscord Boolean @default(false)
|
|
isVip Boolean @default(false)
|
|
isMod Boolean @default(false)
|
|
isStaff Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
comments Comment[]
|
|
suggestions Suggestion[]
|
|
likes Like[]
|
|
refreshTokens RefreshToken[]
|
|
}
|
|
|
|
model Comment {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
content String
|
|
rawContent String?
|
|
userId String @db.ObjectId
|
|
user User @relation(fields: [userId], references: [id])
|
|
gameId String? @db.ObjectId
|
|
game Game? @relation(fields: [gameId], references: [id])
|
|
bookId String? @db.ObjectId
|
|
book Book? @relation(fields: [bookId], references: [id])
|
|
musicId String? @db.ObjectId
|
|
music Music? @relation(fields: [musicId], references: [id])
|
|
artId String? @db.ObjectId
|
|
art Art? @relation(fields: [artId], references: [id])
|
|
showId String? @db.ObjectId
|
|
show Show? @relation(fields: [showId], references: [id])
|
|
mangaId String? @db.ObjectId
|
|
manga Manga? @relation(fields: [mangaId], references: [id])
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model AuditLog {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
action AuditAction
|
|
category AuditCategory
|
|
userId String? @db.ObjectId
|
|
targetUserId String? @db.ObjectId
|
|
resourceType String?
|
|
resourceId String?
|
|
details String?
|
|
userAgent String?
|
|
success Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
enum AuditAction {
|
|
LOGIN
|
|
LOGOUT
|
|
LOGIN_FAILED
|
|
COMMENT_CREATE
|
|
COMMENT_UPDATE
|
|
COMMENT_DELETE
|
|
ENTRY_CREATE
|
|
ENTRY_UPDATE
|
|
ENTRY_DELETE
|
|
LIKE
|
|
UNLIKE
|
|
USER_BAN
|
|
USER_UNBAN
|
|
RATE_LIMIT_EXCEEDED
|
|
CSRF_VALIDATION_FAILED
|
|
UNAUTHORIZED_ACCESS
|
|
}
|
|
|
|
enum AuditCategory {
|
|
AUTH
|
|
CONTENT
|
|
ADMIN
|
|
SECURITY
|
|
}
|
|
|
|
model Suggestion {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
userId String @db.ObjectId
|
|
user User @relation(fields: [userId], references: [id])
|
|
entityType SuggestionEntity
|
|
status SuggestionStatus @default(UNREVIEWED)
|
|
declineReason String?
|
|
|
|
// Data for the suggested item (stored as JSON)
|
|
title String
|
|
gameData Json?
|
|
bookData Json?
|
|
musicData Json?
|
|
artData Json?
|
|
showData Json?
|
|
mangaData Json?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
enum SuggestionEntity {
|
|
GAME
|
|
BOOK
|
|
MUSIC
|
|
ART
|
|
SHOW
|
|
MANGA
|
|
}
|
|
|
|
enum SuggestionStatus {
|
|
UNREVIEWED
|
|
ACCEPTED
|
|
DECLINED
|
|
}
|
|
|
|
model Like {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
userId String @db.ObjectId
|
|
user User @relation(fields: [userId], references: [id])
|
|
entityType String // 'book', 'game', 'show', 'manga', 'music', 'art'
|
|
entityId String @db.ObjectId
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([userId, entityType, entityId])
|
|
}
|
|
|
|
model RefreshToken {
|
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
|
token String @unique
|
|
userId String @db.ObjectId
|
|
user User @relation(fields: [userId], references: [id])
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([expiresAt])
|
|
}
|