good forst commit
This commit is contained in:
@@ -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");
|
||||
|
||||
+18
@@ -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
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user