Add public API keys, outbound webhooks, and bulk SMS campaigns with settings UI and org modules.

This commit is contained in:
2026-09-26 16:12:29 +03:30
parent ccddd009e3
commit ef600d149d
23 changed files with 2434 additions and 5 deletions
@@ -0,0 +1,93 @@
-- CreateTable
CREATE TABLE "OrganizationApiKey" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"prefix" TEXT NOT NULL,
"keyHash" TEXT NOT NULL,
"lastUsedAt" TIMESTAMP(3),
"revokedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OrganizationApiKey_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "WebhookEndpoint" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"url" TEXT NOT NULL,
"secret" TEXT NOT NULL,
"events" TEXT[] DEFAULT ARRAY[]::TEXT[],
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WebhookEndpoint_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "WebhookDelivery" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"endpointId" TEXT NOT NULL,
"event" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"statusCode" INTEGER,
"attempts" INTEGER NOT NULL DEFAULT 0,
"lastError" TEXT,
"nextAttemptAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deliveredAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WebhookDelivery_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SmsCampaign" (
"id" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"body" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'draft',
"scheduledAt" TIMESTAMP(3),
"totalCount" INTEGER NOT NULL DEFAULT 0,
"sentCount" INTEGER NOT NULL DEFAULT 0,
"failedCount" INTEGER NOT NULL DEFAULT 0,
"skippedCount" INTEGER NOT NULL DEFAULT 0,
"createdById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SmsCampaign_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "SmsCampaignRecipient" (
"id" TEXT NOT NULL,
"campaignId" TEXT NOT NULL,
"personId" TEXT,
"mobile" TEXT,
"status" TEXT NOT NULL DEFAULT 'pending',
"error" TEXT,
"sentAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SmsCampaignRecipient_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "OrganizationApiKey_keyHash_key" ON "OrganizationApiKey"("keyHash");
CREATE INDEX "OrganizationApiKey_organizationId_revokedAt_idx" ON "OrganizationApiKey"("organizationId", "revokedAt");
CREATE INDEX "OrganizationApiKey_prefix_idx" ON "OrganizationApiKey"("prefix");
CREATE INDEX "WebhookEndpoint_organizationId_isActive_idx" ON "WebhookEndpoint"("organizationId", "isActive");
CREATE INDEX "WebhookDelivery_organizationId_createdAt_idx" ON "WebhookDelivery"("organizationId", "createdAt");
CREATE INDEX "WebhookDelivery_status_nextAttemptAt_idx" ON "WebhookDelivery"("status", "nextAttemptAt");
CREATE INDEX "WebhookDelivery_endpointId_createdAt_idx" ON "WebhookDelivery"("endpointId", "createdAt");
CREATE INDEX "SmsCampaign_organizationId_status_scheduledAt_idx" ON "SmsCampaign"("organizationId", "status", "scheduledAt");
CREATE INDEX "SmsCampaignRecipient_campaignId_status_idx" ON "SmsCampaignRecipient"("campaignId", "status");
ALTER TABLE "OrganizationApiKey" ADD CONSTRAINT "OrganizationApiKey_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "WebhookEndpoint" ADD CONSTRAINT "WebhookEndpoint_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "WebhookDelivery" ADD CONSTRAINT "WebhookDelivery_endpointId_fkey" FOREIGN KEY ("endpointId") REFERENCES "WebhookEndpoint"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SmsCampaign" ADD CONSTRAINT "SmsCampaign_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SmsCampaignRecipient" ADD CONSTRAINT "SmsCampaignRecipient_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "SmsCampaign"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+98
View File
@@ -194,6 +194,9 @@ model Organization {
cheques Cheque[]
auditLogs AuditLog[]
savedViews SavedView[]
apiKeys OrganizationApiKey[]
webhookEndpoints WebhookEndpoint[]
smsCampaigns SmsCampaign[]
}
model User {
@@ -1597,3 +1600,98 @@ model SavedView {
@@index([organizationId, scope])
@@index([organizationId, ownerUserId])
}
/// کلید API عمومی برای یکپارچه‌سازی خارجی
model OrganizationApiKey {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
/// پیشوند قابل‌نمایش (مثلاً mk_ab12)
prefix String
/// SHA-256 هش کلید کامل
keyHash String
lastUsedAt DateTime?
revokedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([keyHash])
@@index([organizationId, revokedAt])
@@index([prefix])
}
/// وب‌هوک خروجی سازمان
model WebhookEndpoint {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
url String
secret String
events String[] @default([])
isActive Boolean @default(true)
deliveries WebhookDelivery[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, isActive])
}
model WebhookDelivery {
id String @id @default(cuid())
organizationId String
endpointId String
endpoint WebhookEndpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade)
event String
payload Json
status String @default("pending")
statusCode Int?
attempts Int @default(0)
lastError String?
nextAttemptAt DateTime @default(now())
deliveredAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, createdAt])
@@index([status, nextAttemptAt])
@@index([endpointId, createdAt])
}
/// کمپین پیامک گروهی
model SmsCampaign {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String
body String
/// draft | queued | sending | done | cancelled
status String @default("draft")
scheduledAt DateTime?
totalCount Int @default(0)
sentCount Int @default(0)
failedCount Int @default(0)
skippedCount Int @default(0)
createdById String?
recipients SmsCampaignRecipient[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId, status, scheduledAt])
}
model SmsCampaignRecipient {
id String @id @default(cuid())
campaignId String
campaign SmsCampaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
personId String?
mobile String?
/// pending | sent | failed | skipped
status String @default("pending")
error String?
sentAt DateTime?
createdAt DateTime @default(now())
@@index([campaignId, status])
}