Catalyst / admin/Synapse-Sonar 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Sonar

public

Attack Surface Simulation

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Synapse-Sonar / synapse-sonar / app / api / setup / route.ts 2303 B · main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
import { z } from "zod";
import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma";

export const runtime = "nodejs";

/**
 * First-run bootstrap. Creates the very first Tenant + Tenant Admin.
 *
 * Username + password only — no organization required. A default tenant is
 * created silently (the admin can rename it later). One-shot: refuses once any
 * user exists.
 */

/** GET /api/setup → whether first-run setup is still required. */
export async function GET() {
  const userCount = await prisma.user.count();
  return NextResponse.json({ needsSetup: userCount === 0 });
}

const SetupSchema = z.object({
  username: z
    .string()
    .min(3)
    .max(40)
    .regex(/^[a-zA-Z0-9._-]+$/, "Letters, numbers, dot, underscore and hyphen only"),
  name: z.string().max(80).optional(),
  password: z.string().min(10, "Use at least 10 characters"),
});

export async function POST(req: NextRequest) {
  // Bootstrap guard: refuse if the system is already initialized.
  const existing = await prisma.user.count();
  if (existing > 0) {
    return NextResponse.json(
      { error: "Setup has already been completed" },
      { status: 409 }
    );
  }

  let body;
  try {
    body = SetupSchema.parse(await req.json());
  } catch (err) {
    if (err instanceof z.ZodError) {
      return NextResponse.json(
        { error: "Invalid input", details: err.issues },
        { status: 422 }
      );
    }
    return NextResponse.json({ error: "Invalid request" }, { status: 400 });
  }

  // Create the default tenant + admin atomically.
  const result = await prisma.$transaction(async (tx) => {
    const tenant = await tx.tenant.create({
      data: {
        name: "My Organization",
        slug: `org-${crypto.randomBytes(4).toString("hex")}`,
      },
    });
    const admin = await tx.user.create({
      data: {
        tenantId: tenant.id,
        username: body.username,
        name: body.name,
        role: "TENANT_ADMIN",
        status: "ACTIVE",
        passwordHash: await bcrypt.hash(body.password, 12),
      },
      select: { id: true, username: true },
    });
    return { tenant, admin };
  });

  return NextResponse.json({ ok: true, username: result.admin.username });
}