Files
filezzy-staging/backend/prisma/schema.prisma
2026-02-04 14:16:04 +01:00

819 lines
33 KiB
Plaintext

// ═══════════════════════════════════════════════════════════════════════════
// PRISMA SCHEMA - Tools Platform
// ═══════════════════════════════════════════════════════════════════════════
// Feature: Database & Authentication Foundation
// Branch: 001-database-auth-foundation
//
// This schema defines the complete database structure for the tools platform,
// including user management, subscriptions, payments, tools, jobs, and analytics.
// ═══════════════════════════════════════════════════════════════════════════
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ═══════════════════════════════════════════════════════════════════════════
// USER MODEL
// Synced with Keycloak - keycloakId is the link
// ═══════════════════════════════════════════════════════════════════════════
model User {
id String @id @default(uuid())
keycloakId String @unique // Links to Keycloak user ID
email String @unique
name String?
// Tier (derived from subscription, but cached for performance)
tier UserTier @default(FREE)
// Auth Wrapper - Account Status
emailVerified Boolean @default(false)
accountStatus AccountStatus @default(ACTIVE)
// i18n - User's preferred locale (Feature 009)
preferredLocale String? @default("en") // User's language preference (en, fr, ar)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
// Monetization (014): Day Pass expiry; if > NOW() user has DAY_PASS tier
dayPassExpiresAt DateTime?
// Relations
subscription Subscription?
payments Payment[]
jobs Job[]
usageLogs UsageLog[]
batches Batch[]
sessions Session[] // Auth Wrapper - Active sessions
authEvents AuthEvent[] // Auth Wrapper - Event log
emailTokens EmailToken[] // Feature 008 - Email tokens
emailLogs EmailLog[] // Feature 008 - Email delivery logs
adminNotes UserAdminNote[] // Step 01 - Admin notes on this user
@@index([keycloakId])
@@index([email])
@@index([accountStatus]) // Auth Wrapper - Filter by status
@@index([preferredLocale]) // Feature 009 - i18n analytics
}
enum UserTier {
FREE
PREMIUM
}
enum AccountStatus {
ACTIVE
LOCKED
DISABLED
}
// ═══════════════════════════════════════════════════════════════════════════
// SUBSCRIPTION MODEL
// Tracks active subscriptions (Stripe/PayPal)
// ═══════════════════════════════════════════════════════════════════════════
model Subscription {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Plan
plan SubscriptionPlan
// Status
status SubscriptionStatus
// Payment Provider
provider PaymentProvider
providerSubscriptionId String? // Stripe/PayPal subscription ID
providerCustomerId String? // Stripe customer ID / PayPal payer ID
// Billing Period
currentPeriodStart DateTime?
currentPeriodEnd DateTime?
// Cancellation
cancelledAt DateTime?
cancelAtPeriodEnd Boolean @default(false)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([providerSubscriptionId])
@@index([status])
@@index([status, currentPeriodEnd]) // Expiring subscriptions query
}
enum SubscriptionPlan {
PREMIUM_MONTHLY
PREMIUM_YEARLY
}
enum SubscriptionStatus {
ACTIVE
CANCELLED
PAST_DUE
EXPIRED
TRIALING
}
enum PaymentProvider {
STRIPE
PAYPAL
PADDLE
}
// ═══════════════════════════════════════════════════════════════════════════
// PAYMENT MODEL
// Tracks payment history
// ═══════════════════════════════════════════════════════════════════════════
model Payment {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Amount
amount Decimal @db.Decimal(10, 2)
currency String @default("USD")
// Payment Details
provider PaymentProvider
providerPaymentId String? // Stripe PaymentIntent / PayPal Order ID
// Status
status PaymentStatus
// Type
type PaymentType
// Timestamps
createdAt DateTime @default(now())
@@index([userId])
@@index([providerPaymentId])
@@index([createdAt])
}
enum PaymentStatus {
PENDING
COMPLETED
FAILED
REFUNDED
}
enum PaymentType {
SUBSCRIPTION_INITIAL
SUBSCRIPTION_RENEWAL
SUBSCRIPTION_UPGRADE
DAY_PASS_PURCHASE
}
// ═══════════════════════════════════════════════════════════════════════════
// TOOL MODEL
// Defines available tools and their configuration
// ═══════════════════════════════════════════════════════════════════════════
model Tool {
id String @id @default(uuid())
slug String @unique // 'pdf-merge', 'image-remove-bg'
category String // 'pdf', 'image', 'utilities'
name String // 'Merge PDF'
description String?
// Monetization (014): minimum tier to access; GUEST=anyone, FREE=registered+, PREMIUM=Day Pass/Pro
accessLevel AccessLevel @default(FREE)
// When false, using this tool does not count toward ops limit (e.g. QR code, frontend-only)
countsAsOperation Boolean @default(true)
// Processing
dockerService String? // 'stirling-pdf', 'rembg', etc.
processingType ProcessingType @default(API) // API or CLI
// Status
isActive Boolean @default(true)
// SEO
metaTitle String?
metaDescription String?
// Localized content (optional JSON: locale -> string) - Feature 001-localise-tools-errors
nameLocalized Json? // e.g. {"fr": "Fusionner PDF", "ar": "دمج PDF"}
descriptionLocalized Json?
metaTitleLocalized Json?
metaDescriptionLocalized Json?
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
jobs Job[]
usageLogs UsageLog[]
@@index([slug])
@@index([category])
@@index([accessLevel])
@@index([countsAsOperation])
}
enum AccessLevel {
GUEST // Anyone can use (no account required)
FREE // Registered free users and above
PREMIUM // Day Pass and Pro users only
}
enum ProcessingType {
API
CLI
}
// ═══════════════════════════════════════════════════════════════════════════
// JOB MODEL
// Tracks processing jobs
// ═══════════════════════════════════════════════════════════════════════════
model Job {
id String @id @default(uuid())
// User (optional for anonymous)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
// Tool
toolId String
tool Tool @relation(fields: [toolId], references: [id])
// Batch (optional - for batch processing)
batchId String?
batch Batch? @relation("BatchJobs", fields: [batchId], references: [id], onDelete: SetNull)
// Status
status JobStatus @default(QUEUED)
progress Int @default(0) // 0-100
// Files
inputFileIds String[] // MinIO file IDs
outputFileId String? // MinIO file ID
// Processing
processingTimeMs Int?
errorMessage String?
// Metadata
metadata Json? // Tool-specific options
// Anonymous tracking
ipHash String? // Hashed IP for anonymous users
// Email notification tracking (Feature 008)
emailNotificationSentAt DateTime? // When job failure email was sent
emailNotificationCount Int @default(0) // Number of notifications sent
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completedAt DateTime?
// Auto-delete after 24 hours
expiresAt DateTime @default(dbgenerated("NOW() + INTERVAL '24 hours'"))
@@index([userId])
@@index([batchId])
@@index([status])
@@index([createdAt])
@@index([expiresAt])
@@index([userId, createdAt(sort: Desc)]) // User's recent jobs
@@index([expiresAt, status]) // Cleanup queries
@@index([status, createdAt]) // Monitoring queries
}
enum JobStatus {
QUEUED
PROCESSING
COMPLETED
FAILED
CANCELLED
}
// ═══════════════════════════════════════════════════════════════════════════
// BATCH MODEL
// Tracks batch processing operations for PREMIUM users
// ═══════════════════════════════════════════════════════════════════════════
model Batch {
id String @id @default(uuid())
// User
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Status
status BatchStatus @default(PENDING)
// Progress tracking
totalJobs Int
completedJobs Int @default(0)
failedJobs Int @default(0)
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Auto-cleanup after 24 hours
expiresAt DateTime?
// Relations
jobs Job[] @relation("BatchJobs")
@@index([userId])
@@index([status])
@@index([expiresAt])
@@index([userId, createdAt(sort: Desc)])
}
enum BatchStatus {
PENDING
PROCESSING
COMPLETED
FAILED
PARTIAL
}
// ═══════════════════════════════════════════════════════════════════════════
// USAGE LOG MODEL
// Analytics and usage tracking
// ═══════════════════════════════════════════════════════════════════════════
model UsageLog {
id String @id @default(uuid())
// User (optional for anonymous)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
// Tool
toolId String
tool Tool @relation(fields: [toolId], references: [id])
// Request Info
fileSizeMb Decimal? @db.Decimal(10, 2)
processingTimeMs Int?
status String // 'success', 'failed'
// Anonymous tracking
ipHash String?
userAgent String?
country String?
// Timestamps
createdAt DateTime @default(now())
@@index([userId])
@@index([toolId])
@@index([createdAt])
@@index([toolId, createdAt]) // Tool analytics by date
@@index([userId, createdAt(sort: Desc)]) // User usage history
}
// ═══════════════════════════════════════════════════════════════════════════
// SESSION MODEL - Auth Wrapper
// Tracks active user sessions for listing and revocation
// ═══════════════════════════════════════════════════════════════════════════
model Session {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Keycloak Integration
keycloakSessionId String @unique // Links to Keycloak session
// Device Information
deviceInfo Json // { type, os, browser, location }
ipAddress String
userAgent String
// Session Lifecycle
createdAt DateTime @default(now())
lastActivityAt DateTime @updatedAt
expiresAt DateTime
@@index([userId, createdAt(sort: Desc)]) // User's sessions
@@index([expiresAt]) // Cleanup query
}
// ═══════════════════════════════════════════════════════════════════════════
// AUTH EVENT MODEL - Auth Wrapper
// Audit log for authentication events
// ═══════════════════════════════════════════════════════════════════════════
model AuthEvent {
id String @id @default(uuid())
// User (nullable for failed login attempts)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
// Event Details
eventType AuthEventType
outcome AuthEventOutcome
// Request Information
ipAddress String
userAgent String
deviceInfo Json // Parsed device info
// Error Information
failureReason String? // Error code for failures
metadata Json? // Additional context
// Timestamp
timestamp DateTime @default(now())
@@index([userId, timestamp(sort: Desc)]) // User's event history
@@index([eventType]) // Filter by type
@@index([outcome, timestamp(sort: Desc)]) // Failure analysis
@@index([timestamp]) // Cleanup query
}
enum AuthEventType {
LOGIN
LOGIN_FAILED
LOGOUT
REGISTRATION
TOKEN_REFRESH
TOKEN_REFRESH_FAILED
PASSWORD_CHANGE
PASSWORD_RESET_REQUEST
PASSWORD_RESET_COMPLETE
PROFILE_UPDATE
SESSION_REVOKED
ACCOUNT_LOCKED
ACCOUNT_UNLOCKED
SOCIAL_LOGIN
SOCIAL_LOGIN_FAILED
IDENTITY_LINKED
IDENTITY_UNLINKED
}
enum AuthEventOutcome {
SUCCESS
FAILURE
}
// ═══════════════════════════════════════════════════════════════════════════
// FEATURE FLAG MODEL (Optional - for complex flags)
// Simple flags use ENV, complex flags use this table
// ═══════════════════════════════════════════════════════════════════════════
model FeatureFlag {
id String @id @default(uuid())
name String @unique // 'beta_feature_x'
description String?
enabled Boolean @default(false)
// Targeting (optional)
userIds String[] // Specific users
userTiers UserTier[] // Specific tiers
rolloutPercent Int @default(0) // 0-100
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ═══════════════════════════════════════════════════════════════════════════
// PENDING REGISTRATION - User created in our DB only after email verification
// Stores Keycloak user + token until user clicks verification link
// ═══════════════════════════════════════════════════════════════════════════
model PendingRegistration {
id String @id @default(uuid())
keycloakId String // Keycloak user ID (user exists in Keycloak, not yet in our DB)
email String
name String?
tokenHash String @unique
expiresAt DateTime
usedAt DateTime?
createdAt DateTime @default(now())
@@index([tokenHash])
@@index([email])
@@index([expiresAt])
}
// EMAIL TOKEN MODEL - Feature 008
// Stores secure tokens for email verification (password reset, job retry still use this)
// ═══════════════════════════════════════════════════════════════════════════
model EmailToken {
id String @id @default(uuid())
// User Association
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Token Data
tokenHash String @unique // SHA-256 hash of the token
tokenType EmailTokenType
// Lifecycle
createdAt DateTime @default(now())
expiresAt DateTime // Automatically expires
usedAt DateTime? // Single-use enforcement
// Optional Context
metadata Json? // Additional data (e.g., email address, job ID)
@@index([userId, tokenType]) // Find user's tokens by type
@@index([tokenHash]) // Fast token lookup
@@index([expiresAt]) // Cleanup expired tokens
@@index([tokenType, createdAt(sort: Desc)]) // Recent tokens by type
}
enum EmailTokenType {
VERIFICATION // Email address verification (24h expiry)
PASSWORD_RESET // Password reset flow (1h expiry)
JOB_RETRY // Job retry link (7d expiry)
}
// ═══════════════════════════════════════════════════════════════════════════
// EMAIL LOG MODEL - Feature 008
// Tracks all email sending attempts for monitoring and debugging
// ═══════════════════════════════════════════════════════════════════════════
model EmailLog {
id String @id @default(uuid())
// User Association (nullable for anonymous recipients)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
// Email Details
recipientEmail String
recipientName String?
emailType EmailType
subject String
// Delivery Status
status EmailStatus @default(PENDING)
resendMessageId String? // Resend's message ID for tracking
// Error Tracking
errorMessage String?
errorCode String?
retryCount Int @default(0)
// Timestamps
sentAt DateTime @default(now())
deliveredAt DateTime? // Updated via webhook (future)
bouncedAt DateTime? // Updated via webhook (future)
// Metadata
metadata Json? // Additional context (e.g., template variables)
@@index([userId, sentAt(sort: Desc)]) // User's email history
@@index([emailType, sentAt(sort: Desc)]) // Emails by type
@@index([status, sentAt(sort: Desc)]) // Failed emails
@@index([recipientEmail]) // Find emails to specific address
@@index([sentAt]) // Time-based queries
@@index([resendMessageId]) // Lookup by Resend ID
}
enum EmailType {
VERIFICATION // Email address verification
PASSWORD_RESET // Password reset request
PASSWORD_CHANGED // Password changed confirmation
WELCOME // Welcome email after verification
CONTACT_AUTO_REPLY // Contact form auto-reply
MISSED_JOB // Job failure notification (legacy)
JOB_COMPLETED // Job completed with download link
JOB_FAILED // Job failure notification
SUBSCRIPTION_CONFIRMED // Pro subscription created
SUBSCRIPTION_CANCELLED // Subscription cancelled
DAY_PASS_PURCHASED // Day pass purchase confirmation
DAY_PASS_EXPIRING_SOON // Day pass expiring in 2-4h
DAY_PASS_EXPIRED // Day pass expired
SUBSCRIPTION_EXPIRING_SOON // Subscription renewal in 7d/1d
PAYMENT_FAILED // Subscription payment failed
USAGE_LIMIT_WARNING // Free tier usage threshold
PROMO_UPGRADE // Campaign: promo upgrade
FEATURE_ANNOUNCEMENT // Campaign: feature announcement
ADMIN_CUSTOM // Admin composer: custom subject/body (Step 06)
}
enum EmailStatus {
PENDING // Queued but not yet sent
SENT // Successfully sent to Resend
DELIVERED // Delivered to recipient inbox (webhook)
FAILED // Sending failed
BOUNCED // Bounced by recipient server (webhook)
COMPLAINED // Marked as spam by recipient (webhook)
}
// ═══════════════════════════════════════════════════════════════════════════
// DELETED EMAIL - Account deletion abuse prevention
// One row per deletion; 3+ deletions in 30 days for same email blocks registration
// ═══════════════════════════════════════════════════════════════════════════
model DeletedEmail {
id String @id @default(uuid())
email String
deletedAt DateTime @default(now())
@@index([email])
@@index([email, deletedAt])
}
// ═══════════════════════════════════════════════════════════════════════════
// ADMIN AUDIT LOG (002-admin-dashboard-polish)
// One row per admin action (tool update, user update)
// ═══════════════════════════════════════════════════════════════════════════
model AdminAuditLog {
id String @id @default(uuid())
adminUserId String // Keycloak sub or User.id
adminUserEmail String?
action String // e.g. tool.update, user.update, admin.login, config.change
entityType String // tool, user, config, payment, etc.
entityId String
changes Json? // optional summary of what changed (details)
ipAddress String? // Admin IP for audit (Step 01)
createdAt DateTime @default(now())
@@index([entityType, entityId])
@@index([createdAt(sort: Desc)])
@@index([adminUserId])
}
// ═══════════════════════════════════════════════════════════════════════════
// ADMIN TASKS - Step 01 (Admin Panel)
// Tasks & reminders for admins
// ═══════════════════════════════════════════════════════════════════════════
model AdminTask {
id String @id @default(uuid())
title String
description String?
category String // daily, weekly, monthly, quarterly
dueDate DateTime? @map("due_date")
recurring String? // daily, weekly, monthly or null
status AdminTaskStatus @default(PENDING)
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([category])
@@index([status])
@@index([dueDate])
@@index([createdAt(sort: Desc)])
}
enum AdminTaskStatus {
PENDING
IN_PROGRESS
COMPLETED
CANCELLED
}
// ═══════════════════════════════════════════════════════════════════════════
// EMAIL CAMPAIGN - Step 01 (Admin Panel)
// Batch email campaign records (optional persistence)
// ═══════════════════════════════════════════════════════════════════════════
model EmailCampaign {
id String @id @default(uuid())
name String
subject String
content String? @db.Text
recipientsFilter Json? // segment, limit, etc.
status EmailCampaignStatus @default(DRAFT)
sentCount Int @default(0) @map("sent_count")
failedCount Int @default(0) @map("failed_count")
createdAt DateTime @default(now()) @map("created_at")
sentAt DateTime? @map("sent_at")
@@index([status])
@@index([createdAt(sort: Desc)])
}
enum EmailCampaignStatus {
DRAFT
SCHEDULED
SENDING
COMPLETED
CANCELLED
}
// ═══════════════════════════════════════════════════════════════════════════
// COUPON - Step 01 (Admin Panel)
// Coupon codes for promotions
// ═══════════════════════════════════════════════════════════════════════════
model Coupon {
id String @id @default(uuid())
code String @unique
discountType CouponDiscountType @map("discount_type")
discountValue Decimal @db.Decimal(10, 2) @map("discount_value")
validFrom DateTime @map("valid_from")
validUntil DateTime @map("valid_until")
usageLimit Int? @map("usage_limit") // total uses, null = unlimited
usedCount Int @default(0) @map("used_count")
tierRestrict String[] @default([]) @map("tier_restrict") // empty = all tiers
countryRestrict String[] @default([]) @map("country_restrict") // empty = all countries
perUserLimit Int? @map("per_user_limit") // max uses per user, null = unlimited
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([code])
@@index([validFrom, validUntil])
@@index([isActive])
}
enum CouponDiscountType {
PERCENT
FIXED
}
// ═══════════════════════════════════════════════════════════════════════════
// SEO SUBMISSION - Step 01 (Admin Panel)
// Sitemap/URL submission history to search engines
// ═══════════════════════════════════════════════════════════════════════════
model SeoSubmission {
id String @id @default(uuid())
url String
platform String // google, bing, etc.
status String // submitted, success, error
submittedAt DateTime @default(now()) @map("submitted_at")
response Json? // API response or error
@@index([platform])
@@index([submittedAt(sort: Desc)])
}
// ═══════════════════════════════════════════════════════════════════════════
// USER ADMIN NOTE - Step 01 (Admin Panel)
// Admin notes on users (support, internal notes)
// ═══════════════════════════════════════════════════════════════════════════
model UserAdminNote {
id String @id @default(uuid())
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
adminId String @map("admin_id") // User.id of admin who wrote the note
note String @db.Text
createdAt DateTime @default(now()) @map("created_at")
@@index([userId])
@@index([createdAt(sort: Desc)])
}
// ═══════════════════════════════════════════════════════════════════════════
// APP CONFIG - Runtime configuration (022-runtime-config, Tier 2)
// Editable from Admin; cached in Redis.
// ═══════════════════════════════════════════════════════════════════════════
model AppConfig {
id String @id @default(uuid())
key String @unique
value Json
valueType String @map("value_type") // string, number, boolean, json
category String // features, limits, pricing, ui, seo, admin
description String?
isSensitive Boolean @default(false) @map("is_sensitive")
isPublic Boolean @default(false) @map("is_public")
updatedBy String? @map("updated_by")
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
@@index([key])
@@index([category])
@@index([isPublic])
}
model AppConfigAudit {
id String @id @default(uuid())
configKey String @map("config_key")
oldValue Json? @map("old_value")
newValue Json? @map("new_value")
changedBy String? @map("changed_by")
changeReason String? @map("change_reason")
ipAddress String? @map("ip_address")
createdAt DateTime @default(now()) @map("created_at")
@@index([configKey])
@@index([createdAt(sort: Desc)])
}