Bernt Christian Egeland 3 недель назад
Родитель
Сommit
4ba2792d2a
1 измененных файлов с 114 добавлено и 1 удалено
  1. 114 1
      prisma/seed_dummy_data.ts

+ 114 - 1
prisma/seed_dummy_data.ts

@@ -1,6 +1,6 @@
 import { PrismaClient } from "../src/generated/prisma/client";
 import { PrismaPg } from "@prisma/adapter-pg";
-import { randomBytes, scryptSync } from "node:crypto";
+import { createCipheriv, createHash, hkdfSync, randomBytes, scryptSync } from "node:crypto";
 import * as fs from "fs";
 import * as path from "path";
 import * as https from "https";
@@ -16,6 +16,27 @@ function hashPassword(password: string): string {
   return `${salt}:${key.toString("hex")}`;
 }
 
+// Copy of src/features/integrations/Lib/vault.ts sealCredentials(). The image
+// does not ship that file, and the app must be able to open what is sealed
+// here, so the format (v1.iv.tag.data, AES-256-GCM) and the key derivation
+// (INTEGRATIONS_ENCRYPTION_KEY, else HKDF of BETTER_AUTH_SECRET) match exactly.
+function sealCredentials(value: Record<string, unknown>): string {
+  const explicit = process.env.INTEGRATIONS_ENCRYPTION_KEY?.trim();
+  let key: Buffer;
+  if (explicit) {
+    if (!/^[0-9a-f]{64}$/i.test(explicit)) throw new Error("INTEGRATIONS_ENCRYPTION_KEY must be 64 hex characters");
+    key = Buffer.from(explicit, "hex");
+  } else {
+    const authSecret = process.env.BETTER_AUTH_SECRET;
+    if (!authSecret) throw new Error("Set BETTER_AUTH_SECRET or INTEGRATIONS_ENCRYPTION_KEY to seed integration connections");
+    key = Buffer.from(hkdfSync("sha256", authSecret, "torqvoice", "integrations-vault", 32));
+  }
+  const iv = randomBytes(12);
+  const cipher = createCipheriv("aes-256-gcm", key, iv);
+  const encrypted = Buffer.concat([cipher.update(Buffer.from(JSON.stringify(value), "utf8")), cipher.final()]);
+  return ["v1", iv.toString("base64url"), cipher.getAuthTag().toString("base64url"), encrypted.toString("base64url")].join(".");
+}
+
 const prisma = new PrismaClient({
   adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }),
 });
@@ -586,6 +607,9 @@ async function cleanup() {
   // Wipes branding, logo, invoice layout, localization, tax config. The
   // maintenance.* keys are re-upserted later in the seed.
   await prisma.appSetting.deleteMany({ where: { organizationId: ORG_ID } });
+  // Links, jobs and logs cascade. Recreated below from the placeholder
+  // provider config, so the catalog shows the same three vendors connected.
+  await prisma.integrationConnection.deleteMany({ where: { organizationId: ORG_ID } });
 
   // Transient/accumulating records.
   await prisma.aiChat.deleteMany({ where: { organizationId: ORG_ID } });
@@ -742,6 +766,13 @@ async function seed() {
     // Proof the webhook plumbing works, which is otherwise invisible from
     // inside the app and leaves the settings page looking half finished.
     "whatsapp.webhookSeenAt": hoursAgo(2).toISOString(),
+    // The three channels above are also created as catalog connections
+    // below, which is what adopting them on first use would leave behind.
+    // These markers say that has happened, so a disconnect in the demo
+    // sticks until the next reset instead of being undone by the old rows.
+    "integrations.sms.adoptedAt": NOW.toISOString(),
+    "integrations.telegram.adoptedAt": NOW.toISOString(),
+    "integrations.whatsapp.adoptedAt": NOW.toISOString(),
     // Tire hotel is opt-in per workshop, so the sidebar entry and the
     // routes stay hidden until this is set.
     "tireHotel.enabled": "true",
@@ -770,6 +801,88 @@ async function seed() {
   );
   console.log(`  Applied ${Object.keys(settings).length} settings`);
 
+  // -- Integration connections --
+  // The messaging vendors configured above, as the catalog sees them: sealed
+  // credentials, the settings adoption would split out of the old rows, and
+  // the sending identity as the connected account. Cosmetic only: the
+  // credentials are not real, and DEMO_MODE refuses every outbound call and
+  // every connector request before it reaches the network.
+  console.log("\nCreating integration connections...");
+  const webhookSecretHash = (secret: string) => createHash("sha256").update(secret).digest("hex");
+  const smsWebhookSecret = randomBytes(24).toString("hex");
+  const telegramWebhookSecret = randomBytes(24).toString("hex");
+  const connections: Array<{
+    connectorId: string;
+    credentials: Record<string, string>;
+    settings: Record<string, unknown>;
+    externalAccountName: string;
+  }> = [
+    {
+      connectorId: "twilio-sms",
+      credentials: {
+        accountSid: settings["sms.twilio.accountSid"],
+        authToken: settings["sms.twilio.authToken"],
+        webhookSecret: smsWebhookSecret,
+      },
+      settings: {
+        phoneNumber: settings["sms.phoneNumber"],
+        webhookSecretHash: webhookSecretHash(smsWebhookSecret),
+      },
+      externalAccountName: settings["sms.phoneNumber"],
+    },
+    {
+      connectorId: "telegram",
+      credentials: {
+        botToken: settings["telegram.botToken"],
+        webhookSecret: telegramWebhookSecret,
+      },
+      settings: {
+        enabled: true,
+        botUsername: settings["telegram.botUsername"],
+        webhookSecretHash: webhookSecretHash(telegramWebhookSecret),
+      },
+      externalAccountName: `@${settings["telegram.botUsername"]}`,
+    },
+    {
+      connectorId: "whatsapp-meta",
+      credentials: {
+        phoneNumberId: settings["whatsapp.cred.meta.phoneNumberId"],
+        accessToken: settings["whatsapp.cred.meta.accessToken"],
+        verifyToken: settings["whatsapp.cred.meta.verifyToken"],
+      },
+      settings: {
+        enabled: true,
+        phoneNumber: settings["whatsapp.from"],
+        templateName: settings["whatsapp.tpl.meta.text.name"],
+        templateLanguage: settings["whatsapp.tpl.meta.text.language"],
+        templateVariables: settings["whatsapp.tpl.meta.text.variables"],
+        mediaTemplateName: settings["whatsapp.tpl.meta.media.name"],
+        mediaTemplateLanguage: settings["whatsapp.tpl.meta.media.language"],
+        mediaTemplateVariables: settings["whatsapp.tpl.meta.media.variables"],
+      },
+      externalAccountName: settings["whatsapp.from"],
+    },
+  ];
+  await Promise.all(
+    connections.map((c) =>
+      prisma.integrationConnection.create({
+        data: {
+          organizationId: ORG_ID,
+          connectorId: c.connectorId,
+          status: "active",
+          label: "Adopted from settings",
+          credentials: sealCredentials(c.credentials),
+          settings: c.settings,
+          externalAccountName: c.externalAccountName,
+          createdById: USER_ID,
+          lastHealthAt: hoursAgo(1),
+          createdAt: hoursAgo(24 * 90),
+        },
+      }),
+    ),
+  );
+  console.log(`  Created ${connections.length} integration connections`);
+
   // -- Customers (20) --
   console.log("\nCreating customers...");
   const customers = await Promise.all([