good forst commit
This commit is contained in:
+369
-51
@@ -7,45 +7,211 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Group {
|
||||
id String @id @default(cuid())
|
||||
platform String
|
||||
platformId String
|
||||
name String
|
||||
description String?
|
||||
isActive Boolean @default(true)
|
||||
accountId String?
|
||||
account Account? @relation(fields: [accountId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
// ============================================================================
|
||||
// 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[]
|
||||
syncRoutesFrom SyncRoute[] @relation("sourceGroup")
|
||||
syncRoutesTo SyncRoute[] @relation("targetGroup")
|
||||
approvals Approval[]
|
||||
syncRoutes SyncRoute[]
|
||||
consentRecords ConsentRecord[]
|
||||
memberOptOuts MemberOptOut[]
|
||||
towerUsers TowerUser[]
|
||||
auditEvents AuditEvent[]
|
||||
rules TenantRule[]
|
||||
groupAccesses GroupAccess[]
|
||||
}
|
||||
|
||||
@@unique([platform, platformId])
|
||||
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())
|
||||
platform String
|
||||
platformMsgId String
|
||||
sourceGroupId String
|
||||
sourceGroup Group @relation(fields: [sourceGroupId], references: [id])
|
||||
senderJid String
|
||||
senderName String?
|
||||
content String
|
||||
mediaUrl String?
|
||||
tags String[]
|
||||
status MessageStatus @default(PENDING)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
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 {
|
||||
@@ -58,12 +224,16 @@ enum MessageStatus {
|
||||
|
||||
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 {
|
||||
@@ -73,6 +243,8 @@ enum ApprovalDecision {
|
||||
|
||||
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
|
||||
@@ -81,37 +253,183 @@ model SyncRoute {
|
||||
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())
|
||||
groupId String
|
||||
group Group @relation(fields: [groupId], references: [id])
|
||||
memberJid String
|
||||
consentType String
|
||||
grantedAt DateTime @default(now())
|
||||
revokedAt DateTime?
|
||||
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())
|
||||
|
||||
@@unique([groupId, memberJid, consentType])
|
||||
@@index([tenantId, groupId, userId])
|
||||
@@index([status])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
enum AccountStatus {
|
||||
ACTIVE
|
||||
DISCONNECTED
|
||||
BANNED
|
||||
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 Account {
|
||||
id String @id @default(cuid())
|
||||
platform String
|
||||
jid String
|
||||
sessionPath String
|
||||
displayName String?
|
||||
status AccountStatus @default(ACTIVE)
|
||||
qrCode String?
|
||||
groups Group[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
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())
|
||||
|
||||
@@unique([platform, jid])
|
||||
@@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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user