Files
ospab.host/ospabhost/backend/prisma/schema.prisma
2025-12-13 12:53:28 +03:00

525 lines
17 KiB
Plaintext

// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// This is your Prisma schema file,
// VPS/Server models removed - moving to S3 storage
model User {
id Int @id @default(autoincrement())
username String
email String @unique
password String
createdAt DateTime @default(now())
// plans Plan[] @relation("UserPlans")
operator Int @default(0)
isAdmin Boolean @default(false)
tickets Ticket[] @relation("UserTickets")
responses Response[] @relation("OperatorResponses")
checks Check[] @relation("UserChecks")
balance Float @default(0)
notifications Notification[]
pushSubscriptions PushSubscription[]
transactions Transaction[] // История всех транзакций
posts Post[] @relation("PostAuthor") // Статьи блога
comments Comment[] @relation("UserComments") // Комментарии
buckets StorageBucket[] // S3 хранилища пользователя
checkoutSessions StorageCheckoutSession[]
// Новые relations для расширенных настроек
sessions Session[]
loginHistory LoginHistory[]
apiKeys APIKey[]
notificationSettings NotificationSettings?
profile UserProfile?
qrLoginRequests QrLoginRequest[]
@@map("user")
}
model Check {
id Int @id @default(autoincrement())
userId Int
amount Float
status String @default("pending") // pending, approved, rejected
fileUrl String
createdAt DateTime @default(now())
user User @relation("UserChecks", fields: [userId], references: [id])
@@map("check")
}
model Service {
id Int @id @default(autoincrement())
name String @unique
price Float
// planId Int?
// plan Plan? @relation("PlanServices", fields: [planId], references: [id])
@@map("service")
}
model Ticket {
id Int @id @default(autoincrement())
title String
message String @db.Text
userId Int
status String @default("open") // open, in_progress, awaiting_reply, resolved, closed
priority String @default("normal") // low, normal, high, urgent
category String @default("general") // general, technical, billing, other
assignedTo Int? // ID оператора, которому назначен тикет
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
closedAt DateTime?
responses Response[] @relation("TicketResponses")
attachments TicketAttachment[]
user User? @relation("UserTickets", fields: [userId], references: [id])
@@map("ticket")
}
model Response {
id Int @id @default(autoincrement())
ticketId Int
operatorId Int
message String @db.Text
isInternal Boolean @default(false) // Внутренний комментарий (виден только операторам)
createdAt DateTime @default(now())
ticket Ticket @relation("TicketResponses", fields: [ticketId], references: [id], onDelete: Cascade)
operator User @relation("OperatorResponses", fields: [operatorId], references: [id])
attachments ResponseAttachment[]
@@map("response")
}
// Прикреплённые файлы к тикетам
model TicketAttachment {
id Int @id @default(autoincrement())
ticketId Int
ticket Ticket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
filename String
fileUrl String
fileSize Int // Размер в байтах
mimeType String
createdAt DateTime @default(now())
@@map("ticket_attachment")
}
// Прикреплённые файлы к ответам
model ResponseAttachment {
id Int @id @default(autoincrement())
responseId Int
response Response @relation(fields: [responseId], references: [id], onDelete: Cascade)
filename String
fileUrl String
fileSize Int
mimeType String
createdAt DateTime @default(now())
@@map("response_attachment")
}
// QR-код авторизация (как в Telegram Web)
model QrLoginRequest {
id Int @id @default(autoincrement())
code String @unique @db.VarChar(128) // Уникальный код QR
userId Int? // После подтверждения - ID пользователя
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
status String @default("pending") // pending, confirmed, expired, rejected
ipAddress String?
userAgent String? @db.Text
createdAt DateTime @default(now())
expiresAt DateTime // Через 60 секунд
confirmedAt DateTime?
@@index([code])
@@index([status, expiresAt])
@@map("qr_login_request")
}
// История всех транзакций (пополнения, списания, возвраты)
model Transaction {
id Int @id @default(autoincrement())
userId Int
amount Float
type String // deposit (пополнение), withdrawal (списание), refund (возврат)
description String
balanceBefore Float
balanceAfter Float
adminId Int? // ID админа, если операция выполнена админом
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@map("transaction")
}
// Блог
model Post {
id Int @id @default(autoincrement())
title String
content String @db.Text // Rich text content (HTML)
excerpt String? @db.Text // Краткое описание для ленты
coverImage String? // URL обложки
url String @unique // Пользовательский URL (blog_name)
status String @default("draft") // draft, published, archived
authorId Int
author User @relation("PostAuthor", fields: [authorId], references: [id])
views Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
publishedAt DateTime?
comments Comment[]
@@map("post")
}
// Комментарии к статьям блога
model Comment {
id Int @id @default(autoincrement())
postId Int
userId Int? // null если комментарий от гостя
authorName String? // Имя автора (для гостей)
content String @db.Text
status String @default("pending") // pending, approved, rejected
createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
user User? @relation("UserComments", fields: [userId], references: [id])
@@map("comment")
}
// Модель для уведомлений
model Notification {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type String // server_created, payment_charged, tariff_expiring, ticket_reply, payment_received, balance_low
title String
message String @db.Text
// Связанные сущности (опционально)
ticketId Int?
checkId Int?
// Метаданные
actionUrl String? // URL для перехода при клике
icon String? // Иконка (emoji или path)
color String? // Цвет (green, blue, orange, red, purple)
isRead Boolean @default(false)
createdAt DateTime @default(now())
@@index([userId, isRead])
@@index([userId, createdAt])
@@map("notification")
}
// Модель для Push-подписок (Web Push API)
model PushSubscription {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
endpoint String @db.VarChar(512)
p256dh String @db.Text // Публичный ключ для шифрования
auth String @db.Text // Токен аутентификации
userAgent String? @db.Text // Браузер/устройство
createdAt DateTime @default(now())
lastUsed DateTime @default(now())
@@unique([userId, endpoint])
@@index([userId])
@@map("push_subscription")
}
// Активные сеансы пользователя
model Session {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
token String @unique @db.VarChar(500) // JWT refresh token
ipAddress String?
userAgent String? @db.Text
device String? // Desktop, Mobile, Tablet
browser String? // Chrome, Firefox, Safari, etc.
location String? // Город/страна по IP
lastActivity DateTime @default(now())
createdAt DateTime @default(now())
expiresAt DateTime
@@index([userId])
@@map("session")
}
// История входов
model LoginHistory {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
ipAddress String
userAgent String? @db.Text
device String?
browser String?
location String?
success Boolean @default(true) // true = успешный вход, false = неудачная попытка
createdAt DateTime @default(now())
@@index([userId])
@@index([createdAt])
@@map("login_history")
}
// API ключи для разработчиков
model APIKey {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
name String // Название (например, "Production API")
key String @unique @db.VarChar(64) // Сам API ключ
prefix String @db.VarChar(16) // Префикс для отображения (ospab_xxxx)
permissions String? @db.Text // JSON массив разрешений ["servers:read", "servers:create", etc.]
lastUsed DateTime?
createdAt DateTime @default(now())
expiresAt DateTime?
@@index([userId])
@@index([key])
@@map("api_key")
}
// Настройки уведомлений пользователя
model NotificationSettings {
id Int @id @default(autoincrement())
userId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Email уведомления
emailBalanceLow Boolean @default(true)
emailPaymentCharged Boolean @default(true)
emailTicketReply Boolean @default(true)
emailNewsletter Boolean @default(false)
// Push уведомления
pushBalanceLow Boolean @default(true)
pushPaymentCharged Boolean @default(true)
pushTicketReply Boolean @default(true)
updatedAt DateTime @updatedAt
@@map("notification_settings")
}
// Настройки профиля
model UserProfile {
id Int @id @default(autoincrement())
userId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
avatarUrl String? // Путь к аватару
phoneNumber String?
timezone String? @default("Europe/Moscow")
language String? @default("ru")
// Настройки приватности
profilePublic Boolean @default(false)
showEmail Boolean @default(false)
// 2FA
twoFactorEnabled Boolean @default(false)
twoFactorSecret String? @db.Text
updatedAt DateTime @updatedAt
@@map("user_profile")
}
// S3 Bucket модель
model StorageBucket {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
name String // Уникальное имя бакета в рамках пользователя
plan String // Код тарифа из StoragePlan
quotaGb Int // Лимит включённого объёма в GB
usedBytes BigInt @default(0) // Текущий объём хранения в байтах
objectCount Int @default(0)
storageClass String @default("standard") // standard, infrequent, archive
region String @default("ru-central-1")
public Boolean @default(false)
versioning Boolean @default(false)
status String @default("active") // active, grace, suspended
monthlyPrice Float
nextBillingDate DateTime?
lastBilledAt DateTime?
autoRenew Boolean @default(true)
usageSyncedAt DateTime?
storagePlan StoragePlan? @relation(fields: [plan], references: [code])
regionConfig StorageRegion @relation("BucketRegion", fields: [region], references: [code])
storageClassConfig StorageClass @relation("BucketClass", fields: [storageClass], references: [code])
consoleCredential StorageConsoleCredential?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accessKeys StorageAccessKey[]
@@index([userId])
@@index([region])
@@index([storageClass])
@@unique([userId, name]) // Имя уникально в рамках пользователя
@@map("storage_bucket")
}
model StorageAccessKey {
id Int @id @default(autoincrement())
bucketId Int
bucket StorageBucket @relation(fields: [bucketId], references: [id], onDelete: Cascade)
accessKey String @unique
secretKey String // хранится в зашифрованном виде
label String?
createdAt DateTime @default(now())
lastUsedAt DateTime?
@@index([bucketId])
@@map("storage_access_key")
}
model StorageConsoleCredential {
id Int @id @default(autoincrement())
bucketId Int @unique
bucket StorageBucket @relation(fields: [bucketId], references: [id], onDelete: Cascade)
login String
passwordHash String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastGeneratedAt DateTime? // Для rate limiting (1 раз в неделю)
@@map("storage_console_credential")
}
model StoragePlan {
id Int @id @default(autoincrement())
code String @unique
name String
price Float
pricePerGb Decimal? @db.Decimal(10, 4) // Цена за 1 GB для кастомного тарифа
bandwidthPerGb Decimal? @db.Decimal(10, 4) // GB трафика на 1 GB хранения
requestsPerGb Int? // Количество операций на 1 GB хранения
quotaGb Int // Базовая квота для обычных тарифов (0 для custom)
bandwidthGb Int // Базовый трафик для обычных тарифов (0 для custom)
requestLimit String // Текстовое описание лимита операций
order Int @default(0)
isActive Boolean @default(true)
description String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
buckets StorageBucket[]
checkoutSessions StorageCheckoutSession[]
@@map("storage_plan")
}
model StorageCheckoutSession {
id String @id @default(uuid())
userId Int?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
planId Int
plan StoragePlan @relation(fields: [planId], references: [id])
planCode String
planName String
planDescription String?
price Float
promoCodeId Int?
promoCode PromoCode? @relation(fields: [promoCodeId], references: [id])
promoDiscount Float? @default(0)
quotaGb Int
bandwidthGb Int
requestLimit String
createdAt DateTime @default(now())
expiresAt DateTime
consumedAt DateTime?
@@index([userId])
@@index([planId])
@@map("storage_checkout_session")
}
model StorageRegion {
id Int @id @default(autoincrement())
code String @unique
name String
description String?
endpoint String?
isDefault Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
buckets StorageBucket[] @relation("BucketRegion")
@@map("storage_region")
}
model StorageClass {
id Int @id @default(autoincrement())
code String @unique
name String
description String?
redundancy String?
performance String?
retrievalFee String?
isDefault Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
buckets StorageBucket[] @relation("BucketClass")
@@map("storage_class")
}
model PromoCode {
id Int @id @default(autoincrement())
code String @unique
amount Float // discount amount in RUB
used Boolean @default(false)
usedBy Int?
usedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [usedBy], references: [id])
@@map("promo_code")
}