Switch admin auth to mobile, restyle shell/login with Vuesax icons, and add product detail fields plus public product pages. Co-authored-by: Cursor <cursoragent@cursor.com>
189 lines
5.7 KiB
Plaintext
189 lines
5.7 KiB
Plaintext
generator client {
|
|
provider = "prisma-client"
|
|
output = "../src/generated/prisma"
|
|
moduleFormat = "esm"
|
|
importFileExtension = "js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
enum Role {
|
|
ADMIN
|
|
EDITOR
|
|
}
|
|
|
|
enum MessageStatus {
|
|
NEW
|
|
READ
|
|
ARCHIVED
|
|
}
|
|
|
|
model AdminUser {
|
|
id Int @id @default(autoincrement())
|
|
phone String @unique
|
|
name String
|
|
passwordHash String
|
|
role Role @default(EDITOR)
|
|
isActive Boolean @default(true)
|
|
lastLoginAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
refreshTokens RefreshToken[]
|
|
auditLogs AuditLog[]
|
|
}
|
|
|
|
model RefreshToken {
|
|
id Int @id @default(autoincrement())
|
|
userId Int
|
|
user AdminUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
/// sha256 of the opaque token stored in the httpOnly cookie
|
|
tokenHash String @unique
|
|
expiresAt DateTime
|
|
revokedAt DateTime?
|
|
userAgent String @default("")
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
}
|
|
|
|
model Media {
|
|
id Int @id @default(autoincrement())
|
|
/// object key in the S3 bucket; the public URL is MEDIA_PUBLIC_URL + "/" + key
|
|
key String @unique
|
|
mime String
|
|
size Int
|
|
width Int?
|
|
height Int?
|
|
alt String @default("")
|
|
originalName String @default("")
|
|
createdAt DateTime @default(now())
|
|
categoryCovers Category[] @relation("CategoryCover")
|
|
productImages Product[] @relation("ProductImage")
|
|
productGallery ProductImage[]
|
|
}
|
|
|
|
model Category {
|
|
id Int @id @default(autoincrement())
|
|
slug String @unique
|
|
code String
|
|
name String
|
|
nameEn String @default("")
|
|
shortDesc String @default("")
|
|
fullDesc String @default("")
|
|
/// Lucide icon name
|
|
icon String @default("")
|
|
coverImageId Int?
|
|
coverImage Media? @relation("CategoryCover", fields: [coverImageId], references: [id], onDelete: SetNull)
|
|
sortOrder Int @default(0)
|
|
isPublished Boolean @default(true)
|
|
seoTitle String @default("")
|
|
seoDescription String @default("")
|
|
groups ProductGroup[]
|
|
products Product[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([sortOrder])
|
|
}
|
|
|
|
model ProductGroup {
|
|
id Int @id @default(autoincrement())
|
|
categoryId Int
|
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
|
slug String
|
|
name String
|
|
desc String @default("")
|
|
sortOrder Int @default(0)
|
|
products Product[]
|
|
|
|
@@unique([categoryId, slug])
|
|
}
|
|
|
|
model Product {
|
|
id Int @id @default(autoincrement())
|
|
categoryId Int
|
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict)
|
|
groupId Int?
|
|
group ProductGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)
|
|
code String @unique
|
|
slug String @unique
|
|
name String
|
|
nameEn String @default("")
|
|
desc String @default("")
|
|
specs String[]
|
|
variants String[]
|
|
/// long description for the product page; a blank line starts a new paragraph
|
|
details String @default("")
|
|
brand String @default("")
|
|
/// country of manufacture
|
|
origin String @default("")
|
|
sizes String[] @default([])
|
|
standards String[] @default([])
|
|
features String[] @default([])
|
|
applications String[] @default([])
|
|
/// technical specification table: [{ "name": "...", "value": "..." }]
|
|
attributes Json @default("[]")
|
|
packaging String @default("")
|
|
minOrder String @default("")
|
|
seoTitle String @default("")
|
|
seoDescription String @default("")
|
|
imageId Int?
|
|
image Media? @relation("ProductImage", fields: [imageId], references: [id], onDelete: SetNull)
|
|
gallery ProductImage[]
|
|
sortOrder Int @default(0)
|
|
isPublished Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([categoryId, sortOrder])
|
|
}
|
|
|
|
model ProductImage {
|
|
productId Int
|
|
mediaId Int
|
|
sortOrder Int @default(0)
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([productId, mediaId])
|
|
}
|
|
|
|
/// One row per editable website section; `data` is validated by the zod schema for `key` (@imen/shared)
|
|
model SiteSection {
|
|
key String @id
|
|
data Json
|
|
updatedById Int?
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model ContactMessage {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
org String
|
|
phone String
|
|
message String @default("")
|
|
status MessageStatus @default(NEW)
|
|
ip String @default("")
|
|
userAgent String @default("")
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([status, createdAt])
|
|
}
|
|
|
|
model AuditLog {
|
|
id Int @id @default(autoincrement())
|
|
userId Int?
|
|
user AdminUser? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
/// create | update | delete | login ...
|
|
action String
|
|
/// product | category | section | media | user | message
|
|
entity String
|
|
entityId String @default("")
|
|
summary String @default("")
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([createdAt])
|
|
}
|