import { z } from "zod"; import { NextResponse } from "next/server"; import { createRequestId, logServerAction, withServerTiming, } from "@/lib/server/logging/logger"; import { hasAnyUsers } from "@/lib/server/modules/auth/repository"; import { AuthError, registerUser } from "@/lib/server/modules/auth/service"; import { bootstrapDefaultCasaosCatalog } from "@/lib/server/modules/store/catalog"; import { ensureDataRootDirectories, resolveDataRootDirectory, } from "@/lib/server/storage/data-root"; export const runtime = "nodejs"; const registerSchema = z.object({ username: z.string().min(1), password: z.string().min(1), confirmPassword: z.string().min(1), }); export async function POST(request: Request) { const requestId = createRequestId(); try { // Homeio is single-user, and the database already knows whether that user // exists — so nothing else gets a say. This used to be gated on an // AUTH_ALLOW_REGISTRATION env var as well, which install.sh wrote as // `true` and never turned off: a second source of truth for a fact the // first one already held, and when they disagreed the flag won. Any // script-installed server reachable from the internet would hand a full // account to whoever asked for one, with no roles to limit it. const usersExist = await hasAnyUsers(); if (usersExist) { return NextResponse.json( { error: "Registration is closed: this server already has an account", }, { status: 403 }, ); } const body = await request.json(); const parsed = registerSchema.safeParse(body); if (!parsed.success) { return NextResponse.json( { error: "Invalid request payload", }, { status: 400 }, ); } await withServerTiming( { layer: "system", action: "storage.data.bootstrap", requestId, meta: { dataRoot: resolveDataRootDirectory(), }, }, async () => ensureDataRootDirectories(), ); // Catalog bootstrap is best-effort during registration. A single broken // upstream YAML used to throw here and turn the whole register call into // a 500. The user can refresh the catalog later from Settings, and the // per-entry parser now skips broken stacks, so this should rarely fail — // but if it does, registration still succeeds. try { await withServerTiming( { layer: "system", action: "store.catalog.bootstrap", requestId, }, async () => bootstrapDefaultCasaosCatalog(), ); } catch (error) { logServerAction({ level: "warn", layer: "api", action: "auth.register", status: "error", requestId, error, meta: { stage: "catalog_bootstrap_skipped" }, }); } const user = await withServerTiming( { layer: "api", action: "auth.register", requestId, }, async () => registerUser(parsed.data), ); return NextResponse.json( { data: { id: user.id, username: user.username, }, }, { status: 201 }, ); } catch (error) { const statusCode = error instanceof AuthError ? error.statusCode : 500; logServerAction({ level: statusCode >= 500 ? "error" : "warn", layer: "api", action: "auth.register", status: "error", requestId, error, }); return NextResponse.json( { error: error instanceof AuthError ? error.message : "Failed to register user", }, { status: statusCode }, ); } }