Files
web-app-launcher/prisma/schema.prisma
T
alexei.dolgolyov b9f3a2ca0b feat(auth): Session model + remember-me
Replace the single `user.refreshToken` column with a proper Session
table so users can have multiple concurrent sessions (phone, laptop,
etc.), each with their own refresh token, expiry, label, and
remember-me flag.

- Add Session model (id, userId, tokenHash, label, userAgent,
  ipAddress, rememberMe, lastUsedAt, expiresAt).
- Drop `User.refreshToken` and `User.refreshTokenExpiresAt`.
- authService: new createSession/validateSession/rotateSession/
  revokeSession/listUserSessions helpers; remove refresh-token-on-user
  functions.
- sessionCookies helper now issues a session_id cookie alongside
  access_token and refresh_token; rotateSessionCookies keeps the same
  session id on refresh.
- Login form adds a "Keep me signed in for 30 days" checkbox;
  TTL is 7d by default, 30d with remember-me.
- User-Agent parsed into a friendly label ("Chrome on Windows") for
  the upcoming sessions page.
- hooks.server.ts, refresh endpoint, logout, register, oauth callback,
  and onboarding all switched to the new session API.
2026-04-16 03:41:52 +03:00

372 lines
10 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
password String?
displayName String
avatarUrl String?
authProvider String @default("local") // local | oauth
role String @default("user") // admin | user
onboardingComplete Boolean @default(false)
trackRecentApps Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
themeMode String?
primaryHue Int?
primarySaturation Int?
backgroundType String?
locale String?
groups UserGroup[]
sessions Session[]
createdApps App[]
boards Board[]
favorites UserFavorite[]
clicks AppClick[]
notificationChannels NotificationChannel[]
notifications Notification[]
apiTokens ApiToken[]
auditLogs AuditLog[]
boardTemplates BoardTemplate[]
@@index([email])
}
model Session {
id String @id @default(cuid())
userId String
tokenHash String // bcrypt hash of the refresh token
label String? // user-friendly, e.g. "Chrome on Windows"
userAgent String?
ipAddress String?
rememberMe Boolean @default(false)
lastUsedAt DateTime @default(now())
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([expiresAt])
}
model Group {
id String @id @default(cuid())
name String @unique
description String?
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users UserGroup[]
}
model UserGroup {
id String @id @default(cuid())
userId String
groupId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
@@unique([userId, groupId])
@@index([userId])
@@index([groupId])
}
model App {
id String @id @default(cuid())
name String
url String
icon String?
iconType String @default("lucide") // lucide | simple | url | emoji
description String?
category String?
tags String @default("") // comma-separated
healthcheckEnabled Boolean @default(false)
healthcheckInterval Int @default(300) // seconds
healthcheckMethod String @default("GET")
healthcheckExpectedStatus Int @default(200)
healthcheckTimeout Int @default(5000) // milliseconds
integrationType String?
integrationConfig String?
integrationEnabled Boolean @default(false)
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
statuses AppStatus[]
widgets Widget[]
appTags AppTag[]
links AppLink[]
clicks AppClick[]
notifications Notification[]
favorites UserFavorite[]
@@index([name])
@@index([category])
@@index([createdById])
}
model AppStatus {
id String @id @default(cuid())
appId String
status String @default("unknown") // online | offline | degraded | unknown
responseTime Int? // milliseconds
checkedAt DateTime @default(now())
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@index([appId])
@@index([checkedAt])
}
model Board {
id String @id @default(cuid())
name String
icon String?
description String?
isDefault Boolean @default(false)
isGuestAccessible Boolean @default(false)
backgroundConfig String? // JSON stored as string for SQLite
themeHue Int?
themeSaturation Int?
backgroundType String?
cardSize String?
wallpaperUrl String?
wallpaperBlur Int?
wallpaperOverlay Float?
customCss String?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
sections Section[]
@@index([createdById])
}
model Section {
id String @id @default(cuid())
boardId String
title String
icon String?
order Int @default(0)
isExpandedByDefault Boolean @default(true)
cardSize String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
widgets Widget[]
@@index([boardId])
}
model Widget {
id String @id @default(cuid())
sectionId String
type String // app | bookmark | note | embed | status
order Int @default(0)
config String @default("{}") // JSON stored as string for SQLite
appId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
section Section @relation(fields: [sectionId], references: [id], onDelete: Cascade)
app App? @relation(fields: [appId], references: [id], onDelete: SetNull)
@@index([sectionId])
@@index([appId])
}
model Permission {
id String @id @default(cuid())
entityType String // board | app
entityId String
targetType String // user | group
targetId String
level String // view | edit | admin
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([entityType, entityId, targetType, targetId])
@@index([entityType, entityId])
@@index([targetType, targetId])
}
model SystemSettings {
id String @id @default("singleton")
authMode String @default("local") // local | oauth | both
registrationEnabled Boolean @default(true)
oauthClientId String?
oauthClientSecret String?
oauthDiscoveryUrl String?
defaultTheme String @default("dark")
defaultPrimaryColor String @default("#6366f1")
healthcheckDefaults String @default("{}") // JSON stored as string for SQLite
customCss String?
onboardingComplete Boolean @default(false)
backupEnabled Boolean @default(false)
backupCronExpression String @default("0 3 * * *") // default: daily at 3 AM
backupMaxCount Int @default(10)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// --- New models for Phases 4-7 ---
model Tag {
id String @id @default(cuid())
name String @unique
color String?
createdAt DateTime @default(now())
appTags AppTag[]
@@index([name])
}
model AppTag {
id String @id @default(cuid())
appId String
tagId String
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@unique([appId, tagId])
@@index([appId])
@@index([tagId])
}
model AppLink {
id String @id @default(cuid())
appId String
label String
url String
icon String?
order Int @default(0)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@index([appId])
}
model UserFavorite {
id String @id @default(cuid())
userId String
appId String
order Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@unique([userId, appId])
@@index([userId])
@@index([appId])
}
model AppClick {
id String @id @default(cuid())
userId String
appId String
clickedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
app App @relation(fields: [appId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([appId])
@@index([clickedAt])
}
model NotificationChannel {
id String @id @default(cuid())
userId String
type String // discord | slack | telegram | http
config String @default("{}") // JSON stored as string for SQLite
enabled Boolean @default(true)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
model Notification {
id String @id @default(cuid())
userId String
appId String?
event String // app_online | app_offline | app_degraded
message String
sentAt DateTime @default(now())
readAt DateTime?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
app App? @relation(fields: [appId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([appId])
@@index([sentAt])
}
model ApiToken {
id String @id @default(cuid())
userId String
name String
tokenHash String @unique
scope String // read | write | admin
lastUsedAt DateTime?
expiresAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([tokenHash])
}
model AuditLog {
id String @id @default(cuid())
userId String?
action String // user_created | user_deleted | etc.
entityType String
entityId String
details String @default("{}") // JSON stored as string for SQLite
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([action])
@@index([entityType, entityId])
@@index([createdAt])
}
model BoardTemplate {
id String @id @default(cuid())
name String
description String?
icon String?
config String @default("{}") // JSON stored as string for SQLite
isBuiltin Boolean @default(false)
createdById String?
createdAt DateTime @default(now())
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull)
@@index([createdById])
}