Add files via upload

This commit is contained in:
Kosztyk 2026-01-12 23:38:50 +02:00 committed by GitHub
parent 6722907b74
commit ac18fcc92a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -13,20 +13,9 @@ import {
WEBROOT, WEBROOT,
} from "../helpers/env"; } from "../helpers/env";
function computeFirstRun(): boolean { export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
// DB-driven, so it stays correct across reloads and per-request updates.
return db.query("SELECT 1 FROM users LIMIT 1").get() === null;
}
// Kept as an exported boolean for backwards-compatibility (e.g. root.tsx imports FIRST_RUN),
// but it is refreshed per-request via userService. Do not rely on it being constant.
export let FIRST_RUN = computeFirstRun();
export const userService = new Elysia({ name: "user/service" }) export const userService = new Elysia({ name: "user/service" })
.derive(() => {
FIRST_RUN = computeFirstRun();
return {};
})
.use( .use(
jwt({ jwt({
name: "jwt", name: "jwt",
@ -78,8 +67,7 @@ export const userService = new Elysia({ name: "user/service" })
export const user = new Elysia() export const user = new Elysia()
.use(userService) .use(userService)
.get("/setup", ({ redirect }) => { .get("/setup", ({ redirect }) => {
const isFirstRun = computeFirstRun(); if (!FIRST_RUN) {
if (!isFirstRun) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
@ -196,29 +184,25 @@ export const user = new Elysia()
.post( .post(
"/register", "/register",
async ({ body: { email, password }, set, redirect, jwt, cookie: { auth } }) => { async ({ body: { email, password }, set, redirect, jwt, cookie: { auth } }) => {
// DB-driven "first user" detection (no stale in-memory flag) + race-safe creation.
// We hash outside the write-lock to keep the lock window short.
const savedPassword = await Bun.password.hash(password);
// Acquire a write lock so only one instance can perform "count==0 then insert" at a time.
db.exec("BEGIN IMMEDIATE");
try {
const isFirstUser = computeFirstRun();
// first user allowed even if ACCOUNT_REGISTRATION=false // first user allowed even if ACCOUNT_REGISTRATION=false
const isFirstUser = FIRST_RUN;
if (!ACCOUNT_REGISTRATION && !isFirstUser) { if (!ACCOUNT_REGISTRATION && !isFirstUser) {
db.exec("ROLLBACK");
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
const existingUser = db.query("SELECT 1 FROM users WHERE email = ?").get(email); if (FIRST_RUN) {
FIRST_RUN = false;
}
const existingUser = await db.query("SELECT * FROM users WHERE email = ?").get(email);
if (existingUser) { if (existingUser) {
db.exec("ROLLBACK");
set.status = 400; set.status = 400;
return { return {
message: "Email already in use.", message: "Email already in use.",
}; };
} }
const savedPassword = await Bun.password.hash(password);
const role = isFirstUser ? "admin" : "user"; const role = isFirstUser ? "admin" : "user";
@ -231,19 +215,15 @@ export const user = new Elysia()
const userRow = db.query("SELECT * FROM users WHERE email = ?").as(User).get(email); const userRow = db.query("SELECT * FROM users WHERE email = ?").as(User).get(email);
if (!userRow) { if (!userRow) {
db.exec("ROLLBACK");
set.status = 500; set.status = 500;
return { return {
message: "Failed to create user.", message: "Failed to create user.",
}; };
} }
db.exec("COMMIT");
FIRST_RUN = false;
const accessToken = await jwt.sign({ const accessToken = await jwt.sign({
id: String(userRow.id), id: String(userRow.id),
role: userRow.role ?? "user", role: userRow.role,
}); });
if (!auth) { if (!auth) {
@ -263,21 +243,13 @@ export const user = new Elysia()
}); });
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} catch (e) {
try {
db.exec("ROLLBACK");
} catch {
// ignore rollback errors
}
throw e;
}
}, },
{ body: "signIn" }, { body: "signIn" },
) )
.get( .get(
"/login", "/login",
async ({ jwt, redirect, cookie: { auth } }) => { async ({ jwt, redirect, cookie: { auth } }) => {
if (computeFirstRun()) { if (FIRST_RUN) {
return redirect(`${WEBROOT}/setup`, 302); return redirect(`${WEBROOT}/setup`, 302);
} }
@ -845,7 +817,9 @@ export const user = new Elysia()
<form <form
method="post" method="post"
action={`${WEBROOT}/account/edit-user`} action={`${WEBROOT}/account/edit-user`}
class="flex flex-col gap-4" class={`
flex flex-col gap-4
`}
> >
<input type="hidden" name="userId" value={String(targetUser.id)} /> <input type="hidden" name="userId" value={String(targetUser.id)} />
<fieldset class="mb-4 flex flex-col gap-4"> <fieldset class="mb-4 flex flex-col gap-4">
@ -938,6 +912,14 @@ export const user = new Elysia()
// Atomic last-admin protection: concurrent demotions must not be able to leave zero admins. // Atomic last-admin protection: concurrent demotions must not be able to leave zero admins.
// Use a write transaction to serialize changes and a conditional UPDATE for demotion. // Use a write transaction to serialize changes and a conditional UPDATE for demotion.
//
// IMPORTANT: do not hold a SQLite write transaction open across an `await`.
// Hash any password outside the transaction to avoid lock contention.
let hashedPassword: string | null = null;
if (newPassword && newPassword.trim().length > 0) {
hashedPassword = await Bun.password.hash(newPassword);
}
db.exec("BEGIN IMMEDIATE"); db.exec("BEGIN IMMEDIATE");
try { try {
// Role change // Role change
@ -963,9 +945,8 @@ export const user = new Elysia()
} }
// Password change (optional) // Password change (optional)
if (newPassword && newPassword.trim().length > 0) { if (hashedPassword) {
const hashed = await Bun.password.hash(newPassword); db.query("UPDATE users SET password = ? WHERE id = ?").run(hashedPassword, targetId);
db.query("UPDATE users SET password = ? WHERE id = ?").run(hashed, targetId);
} }
db.exec("COMMIT"); db.exec("COMMIT");