diff --git a/src/pages/user.tsx b/src/pages/user.tsx index c133097..0650afc 100644 --- a/src/pages/user.tsx +++ b/src/pages/user.tsx @@ -13,19 +13,9 @@ import { WEBROOT, } from "../helpers/env"; -function computeFirstRun(): boolean { - // DB-driven, so it stays correct across reloads and multi-process setups. - return db.query("SELECT 1 FROM users LIMIT 1").get() === null; -} - -// Exported for backwards compatibility; refreshed per-request via userService.derive(). -export let FIRST_RUN = computeFirstRun(); +export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false; export const userService = new Elysia({ name: "user/service" }) - .derive(() => { - FIRST_RUN = computeFirstRun(); - return {}; - }) .use( jwt({ name: "jwt", @@ -77,7 +67,7 @@ export const userService = new Elysia({ name: "user/service" }) export const user = new Elysia() .use(userService) .get("/setup", ({ redirect }) => { - if (!computeFirstRun()) { + if (!FIRST_RUN) { return redirect(`${WEBROOT}/login`, 302); } @@ -192,29 +182,28 @@ export const user = new Elysia() ); }) .post( - "/register", - async ({ body: { email, password }, set, redirect, jwt, cookie: { auth } }) => { - // DB-driven "first user" detection + race-safe creation. - // Hash outside the write-lock to keep the lock window short. - const savedPassword = await Bun.password.hash(password); - - db.exec("BEGIN IMMEDIATE"); - try { - const isFirstUser = computeFirstRun(); - + "/register", + async ({ body: { email, password }, set, redirect, jwt, cookie: { auth } }) => { // first user allowed even if ACCOUNT_REGISTRATION=false + const isFirstUser = FIRST_RUN; + if (!ACCOUNT_REGISTRATION && !isFirstUser) { - db.exec("ROLLBACK"); return redirect(`${WEBROOT}/login`, 302); } - const existingUser = db.query("SELECT 1 FROM users WHERE email = ?").get(email); - if (existingUser) { - db.exec("ROLLBACK"); - set.status = 400; - return { message: "Email already in use." }; + if (FIRST_RUN) { + FIRST_RUN = false; } + const existingUser = await db.query("SELECT * FROM users WHERE email = ?").get(email); + if (existingUser) { + set.status = 400; + return { + message: "Email already in use.", + }; + } + const savedPassword = await Bun.password.hash(password); + const role = isFirstUser ? "admin" : "user"; db.query("INSERT INTO users (email, password, role) VALUES (?, ?, ?)").run( @@ -224,25 +213,24 @@ export const user = new Elysia() ); const userRow = db.query("SELECT * FROM users WHERE email = ?").as(User).get(email); + if (!userRow) { - db.exec("ROLLBACK"); set.status = 500; - return { message: "Failed to create user." }; + return { + message: "Failed to create user.", + }; } - db.exec("COMMIT"); - - // Refresh after successful creation - FIRST_RUN = computeFirstRun(); - const accessToken = await jwt.sign({ id: String(userRow.id), - role: userRow.role ?? "user", + role: userRow.role, }); if (!auth) { set.status = 500; - return { message: "No auth cookie, perhaps your browser is blocking cookies." }; + return { + message: "No auth cookie, perhaps your browser is blocking cookies.", + }; } // set cookie @@ -255,18 +243,9 @@ export const user = new Elysia() }); return redirect(`${WEBROOT}/`, 302); - } catch (e) { - try { - db.exec("ROLLBACK"); - } catch (rollbackErr) { - console.warn("[user/register] ROLLBACK failed:", rollbackErr); - } - throw e; - } - }, - { body: "signIn" }, -) - + }, + { body: "signIn" }, + ) .get( "/login", async ({ jwt, redirect, cookie: { auth } }) => { @@ -350,7 +329,9 @@ export const user = new Elysia() .post( "/login", async function handler({ body, set, redirect, jwt, cookie: { auth } }) { - const existingUser = db.query("SELECT * FROM users WHERE email = ?").as(User).get(body.email); + const existingUser = db.query("SELECT * FROM users WHERE email = ?").as(User).get( + body.email, + ); if (!existingUser) { set.status = 403; @@ -526,7 +507,11 @@ export const user = new Elysia()