Files
HealthLog/src/app/api/auth/register/route.ts
T
Marc-André BombeckandClaude Opus 4.7 02b99553b9 chore(style): repo-wide prettier sweep before v1.4.6 tag
Long-overdue prettier sweep; prettier had drifted vs. checked-in code
across ~130 files (mostly long-argument line-break wrapping in TS and
table-padding in markdown). Pure formatting — no semantic changes —
and required to clear the v1.4.6 release gate's `pnpm format:check`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:27:17 +02:00

116 lines
3.3 KiB
TypeScript

import { prisma } from "@/lib/db";
import { registerSchema } from "@/lib/validations/auth";
import { hashPassword, checkPasswordStrength } from "@/lib/auth/password";
import { createSession } from "@/lib/auth/session";
import { auditLog } from "@/lib/auth/audit";
import {
apiSuccess,
apiError,
getClientIp,
safeJson,
} from "@/lib/api-response";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { ensureDbCompatibility } from "@/lib/db-compat";
import { NextRequest, NextResponse } from "next/server";
import { apiHandler } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { resolveServerLocale } from "@/lib/i18n/server-locale";
export const POST = apiHandler(async (request: NextRequest) => {
const ip = getClientIp(request) ?? "unknown";
const rl = await checkRateLimit(`auth:register:${ip}`, 5, 15 * 60 * 1000);
if (!rl.allowed) {
return NextResponse.json(
{
data: null,
error: "Too many registration attempts. Please try again later.",
},
{ status: 429, headers: rateLimitHeaders(rl) },
);
}
await ensureDbCompatibility();
const userCount = await prisma.user.count();
// Check if registration is enabled.
// Safety valve: when there are zero users, always allow bootstrap registration.
let registrationEnabled = true;
try {
const settings = await prisma.appSettings.findUnique({
where: { id: "singleton" },
});
if (settings && !settings.registrationEnabled && userCount > 0) {
registrationEnabled = false;
}
} catch {
// Table may not exist yet; allow registration
}
if (!registrationEnabled) {
return apiError("Registration is disabled", 403);
}
const { data: body, error: jsonError } = await safeJson(request);
if (jsonError) return jsonError;
const parsed = registerSchema.safeParse(body);
if (!parsed.success) {
return apiError(parsed.error.issues[0].message, 422);
}
const { email, username, password } = parsed.data;
// Check if email or username already taken (unified message to prevent enumeration)
const [existingEmail, existingUsername] = await Promise.all([
prisma.user.findUnique({ where: { email } }),
prisma.user.findUnique({ where: { username } }),
]);
if (existingEmail || existingUsername) {
return apiError("Username or email already taken", 409);
}
// Validate password strength (locale-aware feedback)
const locale = await resolveServerLocale({ request });
const strength = checkPasswordStrength(password, [username, email], locale);
if (!strength.isAcceptable) {
return apiError(
strength.feedback[0] || "Password too weak (score < 3)",
422,
);
}
const passwordHash = await hashPassword(password);
// First user becomes admin
const role = userCount === 0 ? "ADMIN" : "USER";
// Create user
const user = await prisma.user.create({
data: {
email,
username,
passwordHash,
role,
},
});
// Create session immediately
const ua = request.headers.get("user-agent");
await createSession(user.id, ip, ua);
await auditLog("auth.register", {
userId: user.id,
ipAddress: ip,
details: { method: "password" },
});
annotate({ action: { name: "auth.register" } });
return apiSuccess(
{
user: { id: user.id, username: user.username, email: user.email },
},
201,
);
});