mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
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>
116 lines
3.3 KiB
TypeScript
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,
|
|
);
|
|
});
|