// 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") }