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>
487 lines
12 KiB
Plaintext
487 lines
12 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[]
|
|
|
|
// Audit
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([email])
|
|
@@index([role])
|
|
}
|
|
|
|
enum UserRole {
|
|
user
|
|
family_admin
|
|
family_member
|
|
support
|
|
}
|
|
|
|
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])
|
|
}
|
|
|
|
// ============================================
|
|
// 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])
|
|
}
|
|
|
|
// ============================================
|
|
// 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])
|
|
}
|