- Создан модуль FreeKassa с обработкой платежей, webhook, IP whitelist, MD5 подписью - Переписан frontend billing.tsx для формы оплаты FreeKassa - Удалены файлы и зависимости DePay (depay.routes.ts, @depay/widgets) - Полностью удалена система проверки чеков операторами: * Удален backend модуль /modules/check/ * Удалена frontend страница checkverification.tsx * Очищены импорты, маршруты, WebSocket события * Удалено поле checkId из Notification схемы * Удалены переводы для чеков - Добавлена поддержка спецсимволов в секретных словах FreeKassa - Добавлена документация PAYMENT_MIGRATION.md
535 lines
18 KiB
Plaintext
535 lines
18 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")
|
|
cryptoPayments CryptoPayment[] @relation("UserCryptoPayments")
|
|
balance Float @default(0)
|
|
notifications Notification[]
|
|
pushSubscriptions PushSubscription[]
|
|
transactions Transaction[] // История всех транзакций
|
|
posts Post[] @relation("PostAuthor") // Статьи блога
|
|
comments Comment[] @relation("UserComments") // Комментарии
|
|
buckets StorageBucket[] // S3 хранилища пользователя
|
|
checkoutSessions StorageCheckoutSession[]
|
|
// Список промокодов, использованных пользователем
|
|
usedPromoCodes PromoCode[]
|
|
|
|
// Новые relations для расширенных настроек
|
|
sessions Session[]
|
|
loginHistory LoginHistory[]
|
|
apiKeys APIKey[]
|
|
notificationSettings NotificationSettings?
|
|
profile UserProfile?
|
|
qrLoginRequests QrLoginRequest[]
|
|
|
|
@@map("user")
|
|
}
|
|
|
|
model CryptoPayment {
|
|
id Int @id @default(autoincrement())
|
|
userId Int
|
|
amount Float // Amount in RUB credited to balance
|
|
cryptoAmount Float? // Amount in USDT paid
|
|
exchangeRate Float? // USDT to RUB exchange rate at payment time
|
|
status String @default("pending") // pending, completed, failed
|
|
transactionHash String? // Blockchain transaction hash
|
|
blockchain String @default("polygon") // polygon, ethereum, bsc, etc.
|
|
token String @default("USDT") // USDT, USDC, ETH, etc.
|
|
paymentProvider String @default("depay") // depay
|
|
fileUrl String? // Not used for crypto payments (legacy from Check)
|
|
createdAt DateTime @default(now())
|
|
user User @relation("UserCryptoPayments", fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("crypto_payment")
|
|
}
|
|
|
|
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?
|
|
|
|
// Метаданные
|
|
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[]
|
|
|
|
@@unique([userId, name]) // Имя уникально в рамках пользователя
|
|
@@index([userId])
|
|
@@index([region])
|
|
@@index([storageClass])
|
|
@@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], onDelete: SetNull)
|
|
|
|
// Обратная связь для корзин, в которые применяли этот промокод
|
|
checkoutSessions StorageCheckoutSession[]
|
|
|
|
@@map("promo_code")
|
|
}
|