homeio
PublikFork dari doctor-io/homeio
route.ts
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 },
);
}
}