good forst commit

This commit is contained in:
2026-06-09 02:02:40 +05:30
parent 801c1d7121
commit 249d759e6a
215 changed files with 15425 additions and 1240 deletions
@@ -0,0 +1,101 @@
-- CreateEnum
CREATE TYPE "AdminRole" AS ENUM ('OWNER', 'ADMIN', 'VIEWER');
-- CreateEnum
CREATE TYPE "ActorType" AS ENUM ('ADMIN', 'SYSTEM', 'ADAPTER');
-- CreateTable: Tenant (must come first — referenced by everything else)
CREATE TABLE "Tenant" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"settings" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Tenant_slug_key" ON "Tenant"("slug");
-- Insert default tenant so existing rows can be backfilled
INSERT INTO "Tenant" ("id", "slug", "name", "settings", "updatedAt")
VALUES ('default', 'default', 'Default Tenant', '{}', CURRENT_TIMESTAMP);
-- Add tenantId columns as nullable first
ALTER TABLE "Account" ADD COLUMN "tenantId" TEXT;
ALTER TABLE "Approval" ADD COLUMN "tenantId" TEXT;
ALTER TABLE "ConsentRecord" ADD COLUMN "tenantId" TEXT;
ALTER TABLE "Group" ADD COLUMN "tenantId" TEXT;
ALTER TABLE "Message" ADD COLUMN "tenantId" TEXT;
ALTER TABLE "SyncRoute" ADD COLUMN "tenantId" TEXT;
-- Backfill all existing rows to the default tenant
UPDATE "Account" SET "tenantId" = 'default';
UPDATE "Approval" SET "tenantId" = 'default';
UPDATE "ConsentRecord" SET "tenantId" = 'default';
UPDATE "Group" SET "tenantId" = 'default';
UPDATE "Message" SET "tenantId" = 'default';
UPDATE "SyncRoute" SET "tenantId" = 'default';
-- Now enforce NOT NULL
ALTER TABLE "Account" ALTER COLUMN "tenantId" SET NOT NULL;
ALTER TABLE "Approval" ALTER COLUMN "tenantId" SET NOT NULL;
ALTER TABLE "ConsentRecord" ALTER COLUMN "tenantId" SET NOT NULL;
ALTER TABLE "Group" ALTER COLUMN "tenantId" SET NOT NULL;
ALTER TABLE "Message" ALTER COLUMN "tenantId" SET NOT NULL;
ALTER TABLE "SyncRoute" ALTER COLUMN "tenantId" SET NOT NULL;
-- CreateTable: Admin (new — NOT NULL tenantId is fine, we'll seed default admin later)
CREATE TABLE "Admin" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"role" "AdminRole" NOT NULL DEFAULT 'ADMIN',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Admin_pkey" PRIMARY KEY ("id")
);
-- CreateTable: AuditEvent (new — NOT NULL tenantId is fine)
CREATE TABLE "AuditEvent" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"actorType" "ActorType" NOT NULL,
"actorId" TEXT,
"action" TEXT NOT NULL,
"resourceType" TEXT NOT NULL,
"resourceId" TEXT NOT NULL,
"payload" JSONB NOT NULL DEFAULT '{}',
"traceId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditEvent_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Admin_tenantId_idx" ON "Admin"("tenantId");
CREATE UNIQUE INDEX "Admin_tenantId_email_key" ON "Admin"("tenantId", "email");
CREATE INDEX "AuditEvent_tenantId_createdAt_idx" ON "AuditEvent"("tenantId", "createdAt");
CREATE INDEX "AuditEvent_resourceType_resourceId_idx" ON "AuditEvent"("resourceType", "resourceId");
CREATE INDEX "Account_tenantId_idx" ON "Account"("tenantId");
CREATE INDEX "Approval_tenantId_idx" ON "Approval"("tenantId");
CREATE INDEX "ConsentRecord_tenantId_idx" ON "ConsentRecord"("tenantId");
CREATE INDEX "Group_tenantId_idx" ON "Group"("tenantId");
CREATE INDEX "Message_tenantId_idx" ON "Message"("tenantId");
CREATE INDEX "SyncRoute_tenantId_idx" ON "SyncRoute"("tenantId");
-- AddForeignKey
ALTER TABLE "Admin" ADD CONSTRAINT "Admin_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "AuditEvent" ADD CONSTRAINT "AuditEvent_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "Group" ADD CONSTRAINT "Group_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "Message" ADD CONSTRAINT "Message_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "Approval" ADD CONSTRAINT "Approval_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "SyncRoute" ADD CONSTRAINT "SyncRoute_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "ConsentRecord" ADD CONSTRAINT "ConsentRecord_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "Account" ADD CONSTRAINT "Account_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,201 @@
-- CreateEnum
CREATE TYPE "GroupClaimStatus" AS ENUM ('PENDING_CLAIM', 'CLAIMED', 'RELEASED', 'EXPIRED');
-- CreateEnum
CREATE TYPE "ConsentScope" AS ENUM ('INGEST', 'ARCHIVE', 'REPLICATE', 'DISPLAY');
-- CreateEnum
CREATE TYPE "ConsentStatus" AS ENUM ('GRANTED', 'REVOKED');
-- CreateEnum
CREATE TYPE "MemberOptOutReason" AS ENUM ('STOP_KEYWORD', 'SELF_PORTAL', 'ADMIN_ACTION');
-- AlterEnum
ALTER TYPE "AccountStatus" ADD VALUE 'PAIRING';
-- AlterEnum
ALTER TYPE "ActorType" ADD VALUE 'MEMBER';
-- DropForeignKey
ALTER TABLE "Account" DROP CONSTRAINT "Account_tenantId_fkey";
-- DropForeignKey
ALTER TABLE "Group" DROP CONSTRAINT "Group_tenantId_fkey";
-- DropIndex
DROP INDEX "Account_tenantId_idx";
-- DropIndex
DROP INDEX "ConsentRecord_groupId_memberJid_consentType_key";
-- DropIndex
DROP INDEX "ConsentRecord_tenantId_idx";
-- AlterTable
ALTER TABLE "Account" DROP COLUMN "tenantId",
ADD COLUMN "isBot" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "pairingExpiresAt" TIMESTAMP(3),
ADD COLUMN "pairingToken" TEXT;
-- AlterTable
ALTER TABLE "ConsentRecord" DROP COLUMN "consentType",
DROP COLUMN "grantedAt",
DROP COLUMN "memberJid",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "effectiveAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "policyVersion" TEXT NOT NULL,
ADD COLUMN "proofEventId" TEXT NOT NULL,
ADD COLUMN "retentionDays" INTEGER NOT NULL DEFAULT 90,
ADD COLUMN "scopes" "ConsentScope"[],
ADD COLUMN "status" "ConsentStatus" NOT NULL DEFAULT 'GRANTED',
ADD COLUMN "userId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Group" ADD COLUMN "claimExpiresAt" TIMESTAMP(3),
ADD COLUMN "claimStatus" "GroupClaimStatus" NOT NULL DEFAULT 'PENDING_CLAIM',
ADD COLUMN "claimedAt" TIMESTAMP(3),
ADD COLUMN "claimedByAdminId" TEXT,
ALTER COLUMN "tenantId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Message" ADD COLUMN "senderTowerUserId" TEXT;
-- CreateTable
CREATE TABLE "TenantBot" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "TenantBot_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TowerUser" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"phoneHash" TEXT NOT NULL,
"jid" TEXT NOT NULL,
"displayName" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TowerUser_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TowerSession" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "TowerSession_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MemberOptOut" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"groupId" TEXT,
"reason" "MemberOptOutReason" NOT NULL,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "MemberOptOut_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "TenantBot_accountId_idx" ON "TenantBot"("accountId");
-- CreateIndex
CREATE UNIQUE INDEX "TenantBot_tenantId_accountId_key" ON "TenantBot"("tenantId", "accountId");
-- CreateIndex
CREATE INDEX "TowerUser_phoneHash_idx" ON "TowerUser"("phoneHash");
-- CreateIndex
CREATE INDEX "TowerUser_tenantId_idx" ON "TowerUser"("tenantId");
-- CreateIndex
CREATE INDEX "TowerUser_jid_idx" ON "TowerUser"("jid");
-- CreateIndex
CREATE UNIQUE INDEX "TowerUser_tenantId_phoneHash_key" ON "TowerUser"("tenantId", "phoneHash");
-- CreateIndex
CREATE UNIQUE INDEX "TowerSession_tokenHash_key" ON "TowerSession"("tokenHash");
-- CreateIndex
CREATE INDEX "TowerSession_userId_idx" ON "TowerSession"("userId");
-- CreateIndex
CREATE INDEX "TowerSession_expiresAt_idx" ON "TowerSession"("expiresAt");
-- CreateIndex
CREATE INDEX "MemberOptOut_tenantId_userId_idx" ON "MemberOptOut"("tenantId", "userId");
-- CreateIndex
CREATE INDEX "MemberOptOut_groupId_idx" ON "MemberOptOut"("groupId");
-- CreateIndex
CREATE INDEX "MemberOptOut_userId_idx" ON "MemberOptOut"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "Account_pairingToken_key" ON "Account"("pairingToken");
-- CreateIndex
CREATE INDEX "Account_isBot_idx" ON "Account"("isBot");
-- CreateIndex
CREATE INDEX "Account_status_idx" ON "Account"("status");
-- CreateIndex
CREATE INDEX "ConsentRecord_tenantId_groupId_userId_idx" ON "ConsentRecord"("tenantId", "groupId", "userId");
-- CreateIndex
CREATE INDEX "ConsentRecord_status_idx" ON "ConsentRecord"("status");
-- CreateIndex
CREATE INDEX "ConsentRecord_userId_idx" ON "ConsentRecord"("userId");
-- CreateIndex
CREATE INDEX "Group_claimStatus_idx" ON "Group"("claimStatus");
-- CreateIndex
CREATE INDEX "Message_senderTowerUserId_idx" ON "Message"("senderTowerUserId");
-- AddForeignKey
ALTER TABLE "TenantBot" ADD CONSTRAINT "TenantBot_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TenantBot" ADD CONSTRAINT "TenantBot_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Group" ADD CONSTRAINT "Group_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Group" ADD CONSTRAINT "Group_claimedByAdminId_fkey" FOREIGN KEY ("claimedByAdminId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Message" ADD CONSTRAINT "Message_senderTowerUserId_fkey" FOREIGN KEY ("senderTowerUserId") REFERENCES "TowerUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TowerUser" ADD CONSTRAINT "TowerUser_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TowerSession" ADD CONSTRAINT "TowerSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "TowerUser"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ConsentRecord" ADD CONSTRAINT "ConsentRecord_userId_fkey" FOREIGN KEY ("userId") REFERENCES "TowerUser"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MemberOptOut" ADD CONSTRAINT "MemberOptOut_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "MemberOptOut" ADD CONSTRAINT "MemberOptOut_userId_fkey" FOREIGN KEY ("userId") REFERENCES "TowerUser"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,28 @@
-- CreateTable
CREATE TABLE "OtpChallenge" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"jid" TEXT NOT NULL,
"phoneHash" TEXT NOT NULL,
"code" TEXT NOT NULL,
"scopes" "ConsentScope"[],
"retentionDays" INTEGER NOT NULL DEFAULT 90,
"policyVersion" TEXT NOT NULL,
"groupId" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"consumedAt" TIMESTAMP(3),
"sentAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OtpChallenge_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "OtpChallenge_tenantId_jid_idx" ON "OtpChallenge"("tenantId", "jid");
-- CreateIndex
CREATE INDEX "OtpChallenge_expiresAt_idx" ON "OtpChallenge"("expiresAt");
-- CreateIndex
CREATE INDEX "OtpChallenge_sentAt_idx" ON "OtpChallenge"("sentAt");
@@ -0,0 +1,18 @@
-- AlterTable
ALTER TABLE "Tenant" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "isForwardingPaused" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "SuperAdmin" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"name" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SuperAdmin_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "SuperAdmin_email_key" ON "SuperAdmin"("email");
@@ -0,0 +1,32 @@
-- CreateEnum
CREATE TYPE "RuleMatchType" AS ENUM ('HASHTAG', 'PREFIX', 'REACTION_EMOJI');
-- CreateEnum
CREATE TYPE "RuleAction" AS ENUM ('FLAG', 'AUTO_APPROVE', 'SKIP', 'REJECT');
-- CreateTable
CREATE TABLE "TenantRule" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"matchType" "RuleMatchType" NOT NULL,
"matchValue" TEXT NOT NULL,
"action" "RuleAction" NOT NULL,
"priority" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TenantRule_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "TenantRule_tenantId_isActive_idx" ON "TenantRule"("tenantId", "isActive");
-- CreateIndex
CREATE INDEX "TenantRule_tenantId_matchType_idx" ON "TenantRule"("tenantId", "matchType");
-- CreateIndex
CREATE UNIQUE INDEX "TenantRule_tenantId_matchType_matchValue_key" ON "TenantRule"("tenantId", "matchType", "matchValue");
-- AddForeignKey
ALTER TABLE "TenantRule" ADD CONSTRAINT "TenantRule_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,38 @@
-- CreateTable: GroupClaimToken
CREATE TABLE "GroupClaimToken" (
"id" TEXT NOT NULL,
"groupId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"creatorJid" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"consumedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "GroupClaimToken_pkey" PRIMARY KEY ("id")
);
-- CreateTable: GroupAccess
CREATE TABLE "GroupAccess" (
"id" TEXT NOT NULL,
"groupId" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"grantedBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "GroupAccess_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "GroupClaimToken_token_key" ON "GroupClaimToken"("token");
CREATE INDEX "GroupClaimToken_expiresAt_idx" ON "GroupClaimToken"("expiresAt");
-- CreateIndex
CREATE UNIQUE INDEX "GroupAccess_groupId_tenantId_key" ON "GroupAccess"("groupId", "tenantId");
CREATE INDEX "GroupAccess_tenantId_idx" ON "GroupAccess"("tenantId");
-- AddForeignKey
ALTER TABLE "GroupClaimToken" ADD CONSTRAINT "GroupClaimToken_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GroupAccess" ADD CONSTRAINT "GroupAccess_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "GroupAccess" ADD CONSTRAINT "GroupAccess_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+369 -51
View File
@@ -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])
}
+59
View File
@@ -0,0 +1,59 @@
/* eslint-disable no-console */
import { PrismaClient, AdminRole } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
const DEFAULT_TENANT_SLUG = 'default';
const SEED_ADMIN_EMAIL = process.env['SEED_ADMIN_EMAIL'] ?? 'admin@tower.local';
const SEED_ADMIN_PASSWORD = process.env['SEED_ADMIN_PASSWORD'] ?? 'tower_dev_password';
const SUPER_ADMIN_EMAIL = process.env['SUPER_ADMIN_EMAIL'] ?? 'super@tower.local';
const SUPER_ADMIN_PASSWORD = process.env['SUPER_ADMIN_PASSWORD'] ?? 'super_dev_password';
const BCRYPT_ROUNDS = Number(process.env['BCRYPT_ROUNDS'] ?? '10');
async function main(): Promise<void> {
const tenant = await prisma.tenant.upsert({
where: { slug: DEFAULT_TENANT_SLUG },
update: {},
create: { slug: DEFAULT_TENANT_SLUG, name: 'Default Tenant' },
});
const passwordHash = await bcrypt.hash(SEED_ADMIN_PASSWORD, BCRYPT_ROUNDS);
const admin = await prisma.admin.upsert({
where: { tenantId_email: { tenantId: tenant.id, email: SEED_ADMIN_EMAIL } },
update: { passwordHash, role: AdminRole.OWNER },
create: {
tenantId: tenant.id,
email: SEED_ADMIN_EMAIL,
passwordHash,
role: AdminRole.OWNER,
},
});
const superPasswordHash = await bcrypt.hash(SUPER_ADMIN_PASSWORD, BCRYPT_ROUNDS);
const superAdmin = await prisma.superAdmin.upsert({
where: { email: SUPER_ADMIN_EMAIL },
update: { passwordHash: superPasswordHash },
create: {
email: SUPER_ADMIN_EMAIL,
passwordHash: superPasswordHash,
name: 'Super Admin',
},
});
console.log('Seed complete:');
console.log(` Tenant: ${tenant.slug} (${tenant.id})`);
console.log(` Admin: ${admin.email} (${admin.id}) role=${admin.role}`);
console.log(` SuperAdmin: ${superAdmin.email} (${superAdmin.id})`);
console.log(` Password: ${SEED_ADMIN_PASSWORD} (dev only — change for production)`);
console.log(` Super pwd: ${SUPER_ADMIN_PASSWORD} (dev only — change for production)`);
}
main()
.catch((err) => {
console.error('Seed failed:', err);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});