436 lines
12 KiB
Plaintext
436 lines
12 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tenancy
|
|
// ============================================================================
|
|
|
|
model Tenant {
|
|
id String @id @default(cuid())
|
|
slug String @unique
|
|
name String
|
|
isActive Boolean @default(true)
|
|
isForwardingPaused Boolean @default(false)
|
|
settings Json @default("{}")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
admins Admin[]
|
|
tenantBots TenantBot[]
|
|
groups Group[]
|
|
messages Message[]
|
|
approvals Approval[]
|
|
syncRoutes SyncRoute[]
|
|
consentRecords ConsentRecord[]
|
|
memberOptOuts MemberOptOut[]
|
|
towerUsers TowerUser[]
|
|
auditEvents AuditEvent[]
|
|
rules TenantRule[]
|
|
groupAccesses GroupAccess[]
|
|
}
|
|
|
|
enum AdminRole {
|
|
OWNER
|
|
ADMIN
|
|
VIEWER
|
|
}
|
|
|
|
model Admin {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
email String
|
|
passwordHash String
|
|
role AdminRole @default(ADMIN)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
claimedGroups Group[] @relation("claimer")
|
|
|
|
@@unique([tenantId, email])
|
|
@@index([tenantId])
|
|
}
|
|
|
|
model SuperAdmin {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
passwordHash String
|
|
name String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
enum ActorType {
|
|
ADMIN
|
|
SYSTEM
|
|
ADAPTER
|
|
MEMBER
|
|
}
|
|
|
|
model AuditEvent {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
actorType ActorType
|
|
actorId String?
|
|
action String
|
|
resourceType String
|
|
resourceId String
|
|
payload Json @default("{}")
|
|
traceId String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, createdAt])
|
|
@@index([resourceType, resourceId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// WhatsApp accounts (Phase 2B: bots only, tenant-less, accessed via TenantBot)
|
|
// ============================================================================
|
|
|
|
enum AccountStatus {
|
|
ACTIVE
|
|
DISCONNECTED
|
|
BANNED
|
|
PAIRING
|
|
}
|
|
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
// tenantId REMOVED in Phase 2B — bots are global, access scoped via TenantBot
|
|
platform String
|
|
jid String
|
|
sessionPath String
|
|
displayName String?
|
|
status AccountStatus @default(ACTIVE)
|
|
qrCode String?
|
|
isBot Boolean @default(true)
|
|
pairingToken String? @unique
|
|
pairingExpiresAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenants TenantBot[]
|
|
groups Group[]
|
|
|
|
@@unique([platform, jid])
|
|
@@index([isBot])
|
|
@@index([status])
|
|
}
|
|
|
|
// Many-to-many: which tenants may claim groups from which bot.
|
|
// Phase 2B ships with implicit "all tenants" (UI auto-grants on first claim),
|
|
// but the table is wired so multi-bot + restricted sharing works in later phases.
|
|
model TenantBot {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
accountId String
|
|
account Account @relation(fields: [accountId], references: [id])
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([tenantId, accountId])
|
|
@@index([accountId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Groups + claim lifecycle
|
|
// ============================================================================
|
|
|
|
enum GroupClaimStatus {
|
|
PENDING_CLAIM
|
|
CLAIMED
|
|
RELEASED
|
|
EXPIRED
|
|
}
|
|
|
|
model Group {
|
|
id String @id @default(cuid())
|
|
// tenantId nullable: null while PENDING_CLAIM/EXPIRED, set once CLAIMED
|
|
tenantId String?
|
|
tenant Tenant? @relation(fields: [tenantId], references: [id])
|
|
platform String
|
|
platformId String
|
|
name String
|
|
description String?
|
|
isActive Boolean @default(true)
|
|
accountId String?
|
|
account Account? @relation(fields: [accountId], references: [id])
|
|
claimStatus GroupClaimStatus @default(PENDING_CLAIM)
|
|
claimedAt DateTime?
|
|
claimedByAdminId String?
|
|
claimedByAdmin Admin? @relation("claimer", fields: [claimedByAdminId], references: [id])
|
|
claimExpiresAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
messages Message[]
|
|
syncRoutesFrom SyncRoute[] @relation("sourceGroup")
|
|
syncRoutesTo SyncRoute[] @relation("targetGroup")
|
|
consents ConsentRecord[]
|
|
claimTokens GroupClaimToken[]
|
|
groupAccesses GroupAccess[]
|
|
|
|
@@unique([platform, platformId])
|
|
@@index([accountId])
|
|
@@index([tenantId])
|
|
@@index([claimStatus])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Message ingest + approval
|
|
// ============================================================================
|
|
|
|
model Message {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
platform String
|
|
platformMsgId String
|
|
sourceGroupId String
|
|
sourceGroup Group @relation(fields: [sourceGroupId], references: [id])
|
|
senderJid String
|
|
senderName String?
|
|
senderTowerUserId String?
|
|
senderTowerUser TowerUser? @relation("senderTowerUser", fields: [senderTowerUserId], references: [id])
|
|
content String
|
|
mediaUrl String?
|
|
tags String[]
|
|
status MessageStatus @default(PENDING)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
approval Approval?
|
|
|
|
@@unique([platform, platformMsgId])
|
|
@@index([tenantId])
|
|
@@index([senderTowerUserId])
|
|
}
|
|
|
|
enum MessageStatus {
|
|
PENDING
|
|
APPROVED
|
|
REJECTED
|
|
DISTRIBUTED
|
|
ARCHIVED
|
|
}
|
|
|
|
model Approval {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
messageId String @unique
|
|
message Message @relation(fields: [messageId], references: [id])
|
|
adminId String
|
|
decision ApprovalDecision
|
|
notes String?
|
|
decidedAt DateTime @default(now())
|
|
|
|
@@index([tenantId])
|
|
}
|
|
|
|
enum ApprovalDecision {
|
|
APPROVED
|
|
REJECTED
|
|
}
|
|
|
|
model SyncRoute {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
sourceGroupId String
|
|
sourceGroup Group @relation("sourceGroup", fields: [sourceGroupId], references: [id])
|
|
targetGroupId String
|
|
targetGroup Group @relation("targetGroup", fields: [targetGroupId], references: [id])
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([sourceGroupId, targetGroupId])
|
|
@@index([tenantId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Group claiming + sharing
|
|
// ============================================================================
|
|
|
|
model GroupClaimToken {
|
|
id String @id @default(cuid())
|
|
groupId String
|
|
group Group @relation(fields: [groupId], references: [id])
|
|
token String @unique
|
|
creatorJid String
|
|
expiresAt DateTime
|
|
consumedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
model GroupAccess {
|
|
id String @id @default(cuid())
|
|
groupId String
|
|
group Group @relation(fields: [groupId], references: [id])
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
grantedBy String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([groupId, tenantId])
|
|
@@index([tenantId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Member onboarding (Phase 2B)
|
|
// ============================================================================
|
|
|
|
enum ConsentScope {
|
|
INGEST
|
|
ARCHIVE
|
|
REPLICATE
|
|
DISPLAY
|
|
}
|
|
|
|
enum ConsentStatus {
|
|
GRANTED
|
|
REVOKED
|
|
}
|
|
|
|
enum MemberOptOutReason {
|
|
STOP_KEYWORD
|
|
SELF_PORTAL
|
|
ADMIN_ACTION
|
|
}
|
|
|
|
// Hashed identity: SHA-256 of E.164 phone number (pepper via JWT_SECRET).
|
|
model TowerUser {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
phoneHash String
|
|
jid String
|
|
displayName String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
consents ConsentRecord[]
|
|
optOuts MemberOptOut[]
|
|
sessions TowerSession[]
|
|
messages Message[] @relation("senderTowerUser")
|
|
|
|
@@unique([tenantId, phoneHash])
|
|
@@index([phoneHash])
|
|
@@index([tenantId])
|
|
@@index([jid])
|
|
}
|
|
|
|
model TowerSession {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
tokenHash String @unique
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
model ConsentRecord {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
groupId String
|
|
group Group @relation(fields: [groupId], references: [id])
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
scopes ConsentScope[]
|
|
retentionDays Int @default(90)
|
|
policyVersion String
|
|
status ConsentStatus @default(GRANTED)
|
|
proofEventId String
|
|
effectiveAt DateTime @default(now())
|
|
revokedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, groupId, userId])
|
|
@@index([status])
|
|
@@index([userId])
|
|
}
|
|
|
|
model MemberOptOut {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
groupId String?
|
|
reason MemberOptOutReason
|
|
notes String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, userId])
|
|
@@index([groupId])
|
|
@@index([userId])
|
|
}
|
|
|
|
model OtpChallenge {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
jid String
|
|
phoneHash String
|
|
code String
|
|
scopes ConsentScope[]
|
|
retentionDays Int @default(90)
|
|
policyVersion String
|
|
groupId String
|
|
expiresAt DateTime
|
|
consumedAt DateTime?
|
|
sentAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, jid])
|
|
@@index([expiresAt])
|
|
@@index([sentAt])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tenant Rules Engine
|
|
// ============================================================================
|
|
|
|
enum RuleMatchType {
|
|
HASHTAG
|
|
PREFIX
|
|
REACTION_EMOJI
|
|
}
|
|
|
|
enum RuleAction {
|
|
FLAG
|
|
AUTO_APPROVE
|
|
SKIP
|
|
REJECT
|
|
}
|
|
|
|
model TenantRule {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
matchType RuleMatchType
|
|
matchValue String
|
|
action RuleAction
|
|
priority Int @default(0)
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([tenantId, matchType, matchValue])
|
|
@@index([tenantId, isActive])
|
|
@@index([tenantId, matchType])
|
|
}
|