Build waitlist landing page with Solid.js (hero, features, tier comparison, waitlist signup form, blog preview, footer). Create waitlist signup and blog API endpoints in Fastify. Add WaitlistEntry and BlogPost models to Prisma schema. Create analytics hooks for GA4 and Mixpanel tracking. Fix pre-existing Prisma schema issue (AnalysisJob relation missing User field). - Landing page: responsive Solid.js app with hero, 6 feature cards, 3-tier pricing comparison table, blog preview, and full waitlist signup form with interest tier selection - API: POST /api/waitlist/signup, GET /api/waitlist/count, GET /api/blog, GET /api/blog/:slug, CRUD /api/admin/blog - DB models: WaitlistEntry (with UTM params, conversion tracking, source), BlogPost (with tags, view count, publish scheduling) - Analytics: useAnalytics hook with initAnalytics(), trackEvent(), trackWaitlistSignup(), trackPageView() — GA4 and Mixpanel dual-tracking - Blog: listing, detail, and admin CRUD routes; seed.ts with 3 starter articles - Fix: AnalysisJob.analysisJobId missing @unique constraint, missing analysisJobs[] on User model Delegated to CMO: FRE-5280 (GA4 config), FRE-5281 (Mixpanel config), FRE-5282 (email marketing platform) Co-Authored-By: Paperclip <noreply@paperclip.ing>
679 lines
16 KiB
Plaintext
679 lines
16 KiB
Plaintext
// Prisma schema for ShieldAI
|
|
// All models for the multi-service SaaS platform
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ============================================
|
|
// User & Authentication Models
|
|
// ============================================
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
email String @unique
|
|
emailVerified DateTime?
|
|
name String?
|
|
image String?
|
|
role UserRole @default(user)
|
|
|
|
// Relationships
|
|
accounts Account[]
|
|
sessions Session[]
|
|
familyGroups FamilyGroupMember[]
|
|
familyGroupOwned FamilyGroup[] @relation("FamilyGroupOwner")
|
|
subscriptions Subscription[]
|
|
alerts Alert[]
|
|
voiceEnrollments VoiceEnrollment[]
|
|
voiceAnalyses VoiceAnalysis[]
|
|
spamFeedback SpamFeedback[]
|
|
spamRules SpamRule[]
|
|
normalizedAlerts NormalizedAlert[]
|
|
correlationGroups CorrelationGroup[]
|
|
securityReports SecurityReport[]
|
|
analysisJobs AnalysisJob[]
|
|
|
|
// Audit
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([email])
|
|
@@index([role])
|
|
}
|
|
|
|
enum UserRole {
|
|
user
|
|
family_admin
|
|
family_member
|
|
support
|
|
}
|
|
|
|
enum DetectionVerdict {
|
|
NATURAL
|
|
SYNTHETIC
|
|
UNCERTAIN
|
|
}
|
|
|
|
enum AnalysisType {
|
|
SYNTHETIC_DETECTION
|
|
VOICE_MATCH
|
|
BATCH
|
|
}
|
|
|
|
enum AnalysisJobStatus {
|
|
PENDING
|
|
RUNNING
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
model Account {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
provider String
|
|
providerAccountId String
|
|
access_token String?
|
|
refresh_token String?
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([userId, provider, providerAccountId])
|
|
@@index([userId])
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
sessionToken String @unique
|
|
expires DateTime
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([sessionToken])
|
|
@@index([userId])
|
|
}
|
|
|
|
// ============================================
|
|
// Family & Subscription Models
|
|
// ============================================
|
|
|
|
model FamilyGroup {
|
|
id String @id @default(uuid())
|
|
name String
|
|
ownerId String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
owner User @relation("FamilyGroupOwner", fields: [ownerId], references: [id])
|
|
members FamilyGroupMember[]
|
|
subscriptions Subscription[]
|
|
|
|
@@index([ownerId])
|
|
@@index([name])
|
|
}
|
|
|
|
model FamilyGroupMember {
|
|
id String @id @default(uuid())
|
|
groupId String
|
|
userId String
|
|
role FamilyMemberRole @default(member)
|
|
joinedAt DateTime @default(now())
|
|
|
|
group FamilyGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([groupId, userId])
|
|
@@index([groupId])
|
|
@@index([userId])
|
|
}
|
|
|
|
enum FamilyMemberRole {
|
|
owner
|
|
admin
|
|
member
|
|
}
|
|
|
|
model Subscription {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
familyGroupId String?
|
|
stripeId String? @unique
|
|
tier SubscriptionTier @default(basic)
|
|
status SubscriptionStatus @default(active)
|
|
currentPeriodStart DateTime
|
|
currentPeriodEnd DateTime
|
|
cancelAtPeriodEnd Boolean @default(false)
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
familyGroup FamilyGroup? @relation(fields: [familyGroupId], references: [id])
|
|
|
|
watchlistItems WatchlistItem[]
|
|
exposures Exposure[]
|
|
alerts Alert[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([familyGroupId])
|
|
@@index([stripeId])
|
|
@@index([tier])
|
|
}
|
|
|
|
enum SubscriptionTier {
|
|
basic
|
|
plus
|
|
premium
|
|
}
|
|
|
|
enum SubscriptionStatus {
|
|
active
|
|
past_due
|
|
canceled
|
|
unpaid
|
|
trialing
|
|
}
|
|
|
|
// ============================================
|
|
// DarkWatch Models (Dark Web Monitoring)
|
|
// ============================================
|
|
|
|
model WatchlistItem {
|
|
id String @id @default(uuid())
|
|
subscriptionId String
|
|
type WatchlistType
|
|
value String
|
|
hash String // SHA-256 hash for deduplication
|
|
isActive Boolean @default(true)
|
|
|
|
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
|
exposures Exposure[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([subscriptionId, type, hash])
|
|
@@index([subscriptionId])
|
|
@@index([type])
|
|
@@index([hash])
|
|
}
|
|
|
|
enum WatchlistType {
|
|
email
|
|
phoneNumber
|
|
ssn
|
|
address
|
|
domain
|
|
}
|
|
|
|
model Exposure {
|
|
id String @id @default(uuid())
|
|
subscriptionId String
|
|
watchlistItemId String?
|
|
source ExposureSource
|
|
dataType WatchlistType
|
|
identifier String
|
|
identifierHash String
|
|
severity ExposureSeverity @default(info)
|
|
metadata Json? // Additional source-specific data
|
|
isFirstTime Boolean @default(false)
|
|
|
|
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
|
watchlistItem WatchlistItem? @relation(fields: [watchlistItemId], references: [id])
|
|
alerts Alert[]
|
|
|
|
detectedAt DateTime
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([subscriptionId])
|
|
@@index([watchlistItemId])
|
|
@@index([source])
|
|
@@index([severity])
|
|
@@index([detectedAt])
|
|
}
|
|
|
|
enum ExposureSource {
|
|
hibp // Have I Been Pwned
|
|
securityTrails
|
|
censys
|
|
darkWebForum
|
|
shodan
|
|
honeypot
|
|
}
|
|
|
|
enum ExposureSeverity {
|
|
info
|
|
warning
|
|
critical
|
|
}
|
|
|
|
// ============================================
|
|
// Notification & Alert Models
|
|
// ============================================
|
|
|
|
model Alert {
|
|
id String @id @default(uuid())
|
|
subscriptionId String
|
|
userId String
|
|
exposureId String?
|
|
type AlertType
|
|
title String
|
|
message String
|
|
severity AlertSeverity @default(info)
|
|
isRead Boolean @default(false)
|
|
readAt DateTime?
|
|
channel AlertChannel[] // Array of notification channels
|
|
|
|
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
exposure Exposure? @relation(fields: [exposureId], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([subscriptionId])
|
|
@@index([userId])
|
|
@@index([isRead])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
enum AlertType {
|
|
exposure_detected
|
|
exposure_resolved
|
|
scan_complete
|
|
subscription_changed
|
|
system_warning
|
|
}
|
|
|
|
enum AlertSeverity {
|
|
info
|
|
warning
|
|
critical
|
|
}
|
|
|
|
enum AlertChannel {
|
|
email
|
|
push
|
|
sms
|
|
}
|
|
|
|
// ============================================
|
|
// VoicePrint Models (Voice Cloning Detection)
|
|
// ============================================
|
|
|
|
model VoiceEnrollment {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
name String
|
|
voiceHash String // FAISS embedding hash
|
|
audioMetadata Json? // Sample rate, duration, etc.
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
analyses VoiceAnalysis[]
|
|
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([voiceHash])
|
|
}
|
|
|
|
model VoiceAnalysis {
|
|
id String @id @default(uuid())
|
|
enrollmentId String?
|
|
userId String
|
|
audioHash String // Content hash of audio file
|
|
isSynthetic Boolean
|
|
confidence Float // 0.0 to 1.0
|
|
analysisResult Json // Full ML analysis results
|
|
audioUrl String // S3 storage URL
|
|
|
|
enrollment VoiceEnrollment? @relation(fields: [enrollmentId], references: [id])
|
|
user User @relation(fields: [userId], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([enrollmentId])
|
|
@@index([audioHash])
|
|
}
|
|
|
|
model AnalysisJob {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
analysisType AnalysisType
|
|
audioFilePath String
|
|
status AnalysisJobStatus
|
|
errorMessage String?
|
|
completedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id])
|
|
result AnalysisResult?
|
|
|
|
@@index([userId])
|
|
@@index([status])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model AnalysisResult {
|
|
id String @id @default(uuid())
|
|
analysisJobId String @unique
|
|
syntheticScore Float
|
|
verdict DetectionVerdict
|
|
confidence Float
|
|
processingTimeMs Int
|
|
matchedEnrollmentId String?
|
|
matchedSimilarity Float?
|
|
modelVersion String?
|
|
|
|
analysisJob AnalysisJob @relation(fields: [analysisJobId], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([analysisJobId])
|
|
@@index([syntheticScore])
|
|
@@index([verdict])
|
|
}
|
|
|
|
// ============================================
|
|
// SpamShield Models (Spam Detection)
|
|
// ============================================
|
|
|
|
model SpamFeedback {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
phoneNumber String
|
|
phoneNumberHash String // SHA-256 hash
|
|
isSpam Boolean
|
|
confidence Float? // ML model confidence
|
|
feedbackType FeedbackType
|
|
metadata Json? // Call duration, time, etc.
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([phoneNumberHash])
|
|
@@index([isSpam])
|
|
}
|
|
|
|
enum FeedbackType {
|
|
initial_detection
|
|
user_confirmation
|
|
user_rejection
|
|
auto_learned
|
|
}
|
|
|
|
model SpamRule {
|
|
id String @id @default(uuid())
|
|
userId String?
|
|
isGlobal Boolean @default(false)
|
|
ruleType RuleType
|
|
pattern String
|
|
action RuleAction
|
|
priority Int @default(0)
|
|
isActive Boolean @default(true)
|
|
|
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([isGlobal])
|
|
@@index([ruleType])
|
|
}
|
|
|
|
enum RuleType {
|
|
phoneNumber
|
|
areaCode
|
|
prefix
|
|
pattern
|
|
reputation
|
|
}
|
|
|
|
enum RuleAction {
|
|
block
|
|
flag
|
|
allow
|
|
challenge
|
|
}
|
|
|
|
// ============================================
|
|
// Audit & Analytics Models
|
|
// ============================================
|
|
|
|
model AuditLog {
|
|
id String @id @default(uuid())
|
|
userId String?
|
|
action String
|
|
resource String
|
|
resourceId String?
|
|
changes Json? // Before/after values
|
|
metadata Json?
|
|
ipAddress String?
|
|
userAgent String?
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([action])
|
|
@@index([resource])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model KPISnapshot {
|
|
id String @id @default(uuid())
|
|
date DateTime @unique
|
|
metricName String
|
|
metricValue Float
|
|
metadata Json?
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([metricName])
|
|
@@index([date])
|
|
}
|
|
|
|
// ============================================
|
|
// Cross-Service Alert Correlation Models
|
|
// ============================================
|
|
|
|
enum AlertSource {
|
|
DARKWATCH
|
|
SPAMSHIELD
|
|
VOICEPRINT
|
|
CALL_ANALYSIS
|
|
}
|
|
|
|
enum AlertCategory {
|
|
BREACH_EXPOSURE
|
|
SPAM_CALL
|
|
SPAM_SMS
|
|
SYNTHETIC_VOICE
|
|
VOICE_MISMATCH
|
|
CALL_ANOMALY
|
|
CALL_QUALITY
|
|
CALL_EVENT
|
|
}
|
|
|
|
enum NormalizedAlertSeverity {
|
|
LOW
|
|
INFO
|
|
MEDIUM
|
|
WARNING
|
|
HIGH
|
|
CRITICAL
|
|
}
|
|
|
|
enum CorrelationStatus {
|
|
ACTIVE
|
|
RESOLVED
|
|
}
|
|
|
|
model NormalizedAlert {
|
|
id String @id @default(uuid())
|
|
source AlertSource
|
|
category AlertCategory
|
|
severity NormalizedAlertSeverity
|
|
userId String
|
|
title String
|
|
description String
|
|
entities Json
|
|
sourceAlertId String
|
|
groupId String?
|
|
payload Json?
|
|
createdAt DateTime
|
|
updatedAt DateTime @default(now()) @updatedAt
|
|
|
|
correlationGroup CorrelationGroup? @relation(fields: [groupId], references: [id])
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([sourceAlertId])
|
|
@@index([userId])
|
|
@@index([groupId])
|
|
@@index([source])
|
|
@@index([severity])
|
|
@@index([createdAt])
|
|
@@index([userId, createdAt])
|
|
}
|
|
|
|
model CorrelationGroup {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
entities Json
|
|
highestSeverity NormalizedAlertSeverity
|
|
status CorrelationStatus @default(ACTIVE)
|
|
alertCount Int @default(0)
|
|
summary String?
|
|
resolvedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @default(now()) @updatedAt
|
|
|
|
alerts NormalizedAlert[]
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([status])
|
|
@@index([userId, status])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
// ============================================
|
|
// Report Generation Models
|
|
// ============================================
|
|
|
|
enum ReportType {
|
|
MONTHLY_PLUS
|
|
ANNUAL_PREMIUM
|
|
}
|
|
|
|
enum ReportStatus {
|
|
PENDING
|
|
GENERATING
|
|
COMPLETED
|
|
FAILED
|
|
DELIVERED
|
|
}
|
|
|
|
model SecurityReport {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
subscriptionId String
|
|
reportType ReportType
|
|
status ReportStatus @default(PENDING)
|
|
periodStart DateTime
|
|
periodEnd DateTime
|
|
title String
|
|
summary String?
|
|
htmlContent String?
|
|
pdfUrl String?
|
|
dataPayload Json?
|
|
error String?
|
|
scheduledFor DateTime?
|
|
deliveredAt DateTime?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @default(now()) @updatedAt
|
|
|
|
@@index([userId])
|
|
@@index([subscriptionId])
|
|
@@index([reportType])
|
|
@@index([status])
|
|
@@index([periodStart, periodEnd])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
// ============================================
|
|
// Waitlist & Marketing Models
|
|
// ============================================
|
|
|
|
model WaitlistEntry {
|
|
id String @id @default(uuid())
|
|
email String
|
|
name String?
|
|
source String? // landing_page, blog, referral, social, paid_search
|
|
tier SubscriptionTier? // interest level
|
|
utmSource String?
|
|
utmMedium String?
|
|
utmCampaign String?
|
|
metadata Json? // Browser, device, location, etc.
|
|
|
|
// Conversion tracking
|
|
convertedAt DateTime?
|
|
convertedToUserId String?
|
|
convertedToSubscriptionId String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([email])
|
|
@@index([source])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model BlogPost {
|
|
id String @id @default(uuid())
|
|
slug String @unique
|
|
title String
|
|
excerpt String?
|
|
content String
|
|
authorName String?
|
|
coverImageUrl String?
|
|
tags String[] // Array of tag strings
|
|
published Boolean @default(false)
|
|
publishedAt DateTime?
|
|
viewCount Int @default(0)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([slug])
|
|
@@index([published, publishedAt])
|
|
@@index([tags])
|
|
}
|