mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 06:12:09 +01:00
- Add Zod validation schemas with strong password requirements (12+ chars, complexity) - Implement rate limiting for authentication endpoints (registration, password reset) - Remove duplicate MetricCard component, consolidate to ui/metric-card.tsx - Update README.md to use pnpm commands consistently - Enhance authentication security with 12-round bcrypt hashing - Add comprehensive input validation for all API endpoints - Fix security vulnerabilities in user registration and password reset flows 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
291 lines
8.1 KiB
Plaintext
291 lines
8.1 KiB
Plaintext
generator client {
|
||
provider = "prisma-client-js"
|
||
previewFeatures = ["driverAdapters"]
|
||
}
|
||
|
||
datasource db {
|
||
provider = "postgresql"
|
||
url = env("DATABASE_URL")
|
||
directUrl = env("DATABASE_URL_DIRECT")
|
||
}
|
||
|
||
/// *
|
||
/// * COMPANY (multi-tenant root)
|
||
model Company {
|
||
id String @id @default(uuid())
|
||
name String
|
||
csvUrl String
|
||
csvUsername String?
|
||
csvPassword String?
|
||
sentimentAlert Float?
|
||
dashboardOpts Json?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
companyAiModels CompanyAIModel[]
|
||
sessions Session[]
|
||
imports SessionImport[]
|
||
users User[] @relation("CompanyUsers")
|
||
}
|
||
|
||
/// *
|
||
/// * USER (auth accounts)
|
||
model User {
|
||
id String @id @default(uuid())
|
||
email String @unique
|
||
password String
|
||
role UserRole @default(USER)
|
||
companyId String
|
||
resetToken String?
|
||
resetTokenExpiry DateTime?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
company Company @relation("CompanyUsers", fields: [companyId], references: [id], onDelete: Cascade)
|
||
}
|
||
|
||
/// *
|
||
/// * 1. Normalised session ---------------------------
|
||
model Session {
|
||
id String @id @default(uuid())
|
||
companyId String
|
||
importId String? @unique
|
||
/// *
|
||
/// * session-level data (processed from SessionImport)
|
||
startTime DateTime
|
||
endTime DateTime
|
||
ipAddress String?
|
||
country String?
|
||
fullTranscriptUrl String?
|
||
avgResponseTime Float?
|
||
initialMsg String?
|
||
language String?
|
||
messagesSent Int?
|
||
sentiment SentimentCategory?
|
||
escalated Boolean?
|
||
forwardedHr Boolean?
|
||
category SessionCategory?
|
||
summary String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
aiProcessingRequests AIProcessingRequest[]
|
||
messages Message[]
|
||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||
import SessionImport? @relation("ImportToSession", fields: [importId], references: [id])
|
||
processingStatus SessionProcessingStatus[]
|
||
sessionQuestions SessionQuestion[]
|
||
|
||
@@index([companyId, startTime])
|
||
}
|
||
|
||
/// *
|
||
/// * 2. Raw CSV row (pure data storage) ----------
|
||
model SessionImport {
|
||
id String @id @default(uuid())
|
||
companyId String
|
||
externalSessionId String @unique
|
||
startTimeRaw String
|
||
endTimeRaw String
|
||
ipAddress String?
|
||
countryCode String?
|
||
language String?
|
||
messagesSent Int?
|
||
sentimentRaw String?
|
||
escalatedRaw String?
|
||
forwardedHrRaw String?
|
||
fullTranscriptUrl String?
|
||
avgResponseTimeSeconds Float?
|
||
tokens Int?
|
||
tokensEur Float?
|
||
category String?
|
||
initialMessage String?
|
||
rawTranscriptContent String?
|
||
createdAt DateTime @default(now())
|
||
session Session? @relation("ImportToSession")
|
||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([companyId, externalSessionId])
|
||
}
|
||
|
||
/// *
|
||
/// * MESSAGE (individual lines)
|
||
model Message {
|
||
id String @id @default(uuid())
|
||
sessionId String
|
||
timestamp DateTime?
|
||
role String
|
||
content String
|
||
order Int
|
||
createdAt DateTime @default(now())
|
||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([sessionId, order])
|
||
@@index([sessionId, order])
|
||
}
|
||
|
||
/// *
|
||
/// * UNIFIED PROCESSING STATUS TRACKING
|
||
model SessionProcessingStatus {
|
||
id String @id @default(uuid())
|
||
sessionId String
|
||
stage ProcessingStage
|
||
status ProcessingStatus @default(PENDING)
|
||
startedAt DateTime?
|
||
completedAt DateTime?
|
||
errorMessage String?
|
||
retryCount Int @default(0)
|
||
metadata Json?
|
||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([sessionId, stage])
|
||
@@index([stage, status])
|
||
@@index([sessionId])
|
||
}
|
||
|
||
/// *
|
||
/// * QUESTION MANAGEMENT (separate from Session for better analytics)
|
||
model Question {
|
||
id String @id @default(uuid())
|
||
content String @unique
|
||
createdAt DateTime @default(now())
|
||
sessionQuestions SessionQuestion[]
|
||
}
|
||
|
||
model SessionQuestion {
|
||
id String @id @default(uuid())
|
||
sessionId String
|
||
questionId String
|
||
order Int
|
||
createdAt DateTime @default(now())
|
||
question Question @relation(fields: [questionId], references: [id])
|
||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([sessionId, questionId])
|
||
@@unique([sessionId, order])
|
||
@@index([sessionId])
|
||
}
|
||
|
||
/// *
|
||
/// * AI PROCESSING COST TRACKING
|
||
model AIProcessingRequest {
|
||
id String @id @default(uuid())
|
||
sessionId String
|
||
openaiRequestId String?
|
||
model String
|
||
serviceTier String?
|
||
systemFingerprint String?
|
||
promptTokens Int
|
||
completionTokens Int
|
||
totalTokens Int
|
||
cachedTokens Int?
|
||
audioTokensPrompt Int?
|
||
reasoningTokens Int?
|
||
audioTokensCompletion Int?
|
||
acceptedPredictionTokens Int?
|
||
rejectedPredictionTokens Int?
|
||
promptTokenCost Float
|
||
completionTokenCost Float
|
||
totalCostEur Float
|
||
processingType String
|
||
success Boolean
|
||
errorMessage String?
|
||
requestedAt DateTime @default(now())
|
||
completedAt DateTime?
|
||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([sessionId])
|
||
@@index([requestedAt])
|
||
@@index([model])
|
||
}
|
||
|
||
/// *
|
||
/// * AI Model definitions (without pricing)
|
||
model AIModel {
|
||
id String @id @default(uuid())
|
||
name String @unique
|
||
provider String
|
||
maxTokens Int?
|
||
isActive Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
pricing AIModelPricing[]
|
||
companyModels CompanyAIModel[]
|
||
|
||
@@index([provider, isActive])
|
||
}
|
||
|
||
/// *
|
||
/// * Time-based pricing for AI models
|
||
model AIModelPricing {
|
||
id String @id @default(uuid())
|
||
aiModelId String
|
||
promptTokenCost Float
|
||
completionTokenCost Float
|
||
effectiveFrom DateTime
|
||
effectiveUntil DateTime?
|
||
createdAt DateTime @default(now())
|
||
aiModel AIModel @relation(fields: [aiModelId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([aiModelId, effectiveFrom])
|
||
@@index([effectiveFrom, effectiveUntil])
|
||
}
|
||
|
||
/// *
|
||
/// * Company-specific AI model assignments
|
||
model CompanyAIModel {
|
||
id String @id @default(uuid())
|
||
companyId String
|
||
aiModelId String
|
||
isDefault Boolean @default(false)
|
||
createdAt DateTime @default(now())
|
||
aiModel AIModel @relation(fields: [aiModelId], references: [id], onDelete: Cascade)
|
||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([companyId, aiModelId])
|
||
@@index([companyId, isDefault])
|
||
}
|
||
|
||
/// *
|
||
/// * ENUMS – fewer magic strings
|
||
enum UserRole {
|
||
ADMIN
|
||
USER
|
||
AUDITOR
|
||
}
|
||
|
||
enum SentimentCategory {
|
||
POSITIVE
|
||
NEUTRAL
|
||
NEGATIVE
|
||
}
|
||
|
||
enum SessionCategory {
|
||
SCHEDULE_HOURS
|
||
LEAVE_VACATION
|
||
SICK_LEAVE_RECOVERY
|
||
SALARY_COMPENSATION
|
||
CONTRACT_HOURS
|
||
ONBOARDING
|
||
OFFBOARDING
|
||
WORKWEAR_STAFF_PASS
|
||
TEAM_CONTACTS
|
||
PERSONAL_QUESTIONS
|
||
ACCESS_LOGIN
|
||
SOCIAL_QUESTIONS
|
||
UNRECOGNIZED_OTHER
|
||
}
|
||
|
||
enum ProcessingStage {
|
||
CSV_IMPORT
|
||
TRANSCRIPT_FETCH
|
||
SESSION_CREATION
|
||
AI_ANALYSIS
|
||
QUESTION_EXTRACTION
|
||
}
|
||
|
||
enum ProcessingStatus {
|
||
PENDING
|
||
IN_PROGRESS
|
||
COMPLETED
|
||
FAILED
|
||
SKIPPED
|
||
}
|