Bernt Christian Egeland 2 luni în urmă
părinte
comite
a2a5770e6d

+ 72 - 0
prisma/seed_dummy_data.ts

@@ -152,8 +152,78 @@ async function provisionDemoAccount() {
   console.log(`  Owner: ${DEMO_EMAIL} (org: ${DEMO_ORG_NAME})\n`);
 }
 
+/**
+ * Remove files a demo visitor uploaded, while preserving the seed's own cached
+ * vehicle images. `prisma/seed-assets/vehicles/` ships empty, so those images
+ * were fetched from Unsplash once and cached in the data volume — deleting them
+ * would trigger a full re-download on every 3-hourly reset.
+ */
+function cleanupUploads() {
+  const orgRoot = path.join(DATA_ROOT, "uploads", ORG_ID);
+  if (!fs.existsSync(orgRoot)) return;
+
+  const keep = new Set(Object.keys(vehicleImages));
+  let removed = 0;
+
+  const walk = (dir: string) => {
+    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+      const full = path.join(dir, entry.name);
+      if (entry.isDirectory()) {
+        walk(full);
+        // Drop directories that are now empty (e.g. documents/, diagnostics/).
+        if (fs.readdirSync(full).length === 0) fs.rmdirSync(full);
+        continue;
+      }
+      // Preserve the cached seed images living in uploads/<org>/vehicles/.
+      if (dir === UPLOAD_DIR && keep.has(entry.name)) continue;
+      fs.unlinkSync(full);
+      removed++;
+    }
+  };
+
+  try {
+    walk(orgRoot);
+    console.log(`  Removed ${removed} visitor-uploaded file(s).`);
+  } catch (err) {
+    console.warn("  Upload cleanup failed:", err);
+  }
+}
+
 async function cleanup() {
   console.log("Cleaning existing data...");
+
+  // Config/state a visitor can change that would otherwise survive every reset:
+  // branding, invoice templates, webhooks pointing at external URLs, schedules,
+  // custom presets. Deleting a Webhook cascades its WebhookDelivery rows.
+  await prisma.webhook.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.reportSchedule.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.teamInvitation.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.laborPreset.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.inspectionTemplate.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.customFieldDefinition.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.subscription.deleteMany({ where: { organizationId: ORG_ID } });
+  // 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 } });
+
+  // Transient/accumulating records.
+  await prisma.aiChat.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.notification.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.telegramMessage.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.customerMagicLink.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.customerSmsCode.deleteMany({ where: { organizationId: ORG_ID } });
+  await prisma.auditLog.deleteMany({ where: { organizationId: ORG_ID } });
+
+  // Any team member a visitor added — but never the demo owner, which
+  // provisionDemoAccount() upserts just before this runs.
+  await prisma.organizationMember.deleteMany({
+    where: { organizationId: ORG_ID, userId: { not: USER_ID } },
+  });
+  // Custom roles; OrganizationMember.roleId is onDelete: SetNull, so safe.
+  await prisma.role.deleteMany({ where: { organizationId: ORG_ID } });
+
+  // Core CRUD data. Inspection, ServiceRequest, CustomerSession, StatusReport
+  // and InspectionQuoteRequest cascade from Vehicle/Customer/ServiceRecord.
   await prisma.technician.deleteMany({ where: { organizationId: ORG_ID } });
   await prisma.smsMessage.deleteMany({ where: { organizationId: ORG_ID } });
   await prisma.quote.deleteMany({ where: { organizationId: ORG_ID } });
@@ -164,6 +234,8 @@ async function cleanup() {
   await prisma.vehicle.deleteMany({ where: { organizationId: ORG_ID } });
   await prisma.customer.deleteMany({ where: { organizationId: ORG_ID } });
   await prisma.inventoryPart.deleteMany({ where: { organizationId: ORG_ID } });
+
+  cleanupUploads();
   console.log("  Done.\n");
 }
 

+ 7 - 0
src/features/webhooks/Actions/webhookActions.ts

@@ -12,6 +12,7 @@ import {
 import { generateWebhookSecret, signPayload } from "../Lib/sign";
 import { deliverOnce } from "../Lib/deliver";
 import { checkWebhookUrl } from "../Lib/ssrf";
+import { demoGuard } from "@/lib/demo";
 
 const SSRF_REASONS: Record<string, string> = {
   invalid_url: "URL is not valid",
@@ -66,6 +67,9 @@ export async function getWebhooks() {
 export async function createWebhook(input: unknown) {
   return withAuth(
     async ({ organizationId, userId }) => {
+      // Otherwise the demo becomes an open outbound HTTP relay: SSRF blocks
+      // private ranges, but not arbitrary public URLs.
+      demoGuard();
       const data = createWebhookSchema.parse(input);
       const safety = await checkWebhookUrl(data.url);
       if (!safety.ok) throw new Error(ssrfMessage(safety.reason));
@@ -110,6 +114,7 @@ export async function createWebhook(input: unknown) {
 export async function updateWebhook(input: unknown) {
   return withAuth(
     async ({ organizationId }) => {
+      demoGuard();
       const data = updateWebhookSchema.parse(input);
       const existing = await db.webhook.findFirst({
         where: { id: data.id, organizationId },
@@ -232,6 +237,7 @@ export async function rotateWebhookSecret(id: string) {
 export async function sendTestWebhook(id: string) {
   return withAuth(
     async ({ organizationId, userId }) => {
+      demoGuard();
       const webhook = await db.webhook.findFirst({
         where: { id, organizationId },
       });
@@ -308,6 +314,7 @@ export async function getWebhookDeliveries(webhookId: string, limit: number = 25
 export async function retryWebhookDelivery(deliveryId: string) {
   return withAuth(
     async ({ organizationId }) => {
+      demoGuard();
       const delivery = await db.webhookDelivery.findFirst({
         where: { id: deliveryId, webhook: { organizationId } },
         select: { id: true, status: true },