Add files via upload

This commit is contained in:
Kosztyk 2026-01-14 13:04:49 +02:00 committed by GitHub
parent 48fefdd5be
commit 03e964bad7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -13,9 +13,19 @@ import {
WEBROOT, WEBROOT,
} from "../helpers/env"; } from "../helpers/env";
export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false; 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 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",
@ -67,7 +77,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 }) => {
if (!FIRST_RUN) { if (!computeFirstRun()) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
@ -182,27 +192,28 @@ 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 } }) => {
// first user allowed even if ACCOUNT_REGISTRATION=false // DB-driven "first user" detection + race-safe creation.
const isFirstUser = FIRST_RUN; // 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();
// first user allowed even if ACCOUNT_REGISTRATION=false
if (!ACCOUNT_REGISTRATION && !isFirstUser) { if (!ACCOUNT_REGISTRATION && !isFirstUser) {
db.exec("ROLLBACK");
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
if (FIRST_RUN) { const existingUser = db.query("SELECT 1 FROM users WHERE email = ?").get(email);
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";
@ -213,24 +224,25 @@ 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");
// Refresh after successful creation
FIRST_RUN = computeFirstRun();
const accessToken = await jwt.sign({ const accessToken = await jwt.sign({
id: String(userRow.id), id: String(userRow.id),
role: userRow.role, role: userRow.role ?? "user",
}); });
if (!auth) { if (!auth) {
set.status = 500; set.status = 500;
return { return { message: "No auth cookie, perhaps your browser is blocking cookies." };
message: "No auth cookie, perhaps your browser is blocking cookies.",
};
} }
// set cookie // set cookie
@ -243,9 +255,18 @@ export const user = new Elysia()
}); });
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
}, } catch (e) {
{ body: "signIn" }, try {
) db.exec("ROLLBACK");
} catch (rollbackErr) {
console.warn("[user/register] ROLLBACK failed:", rollbackErr);
}
throw e;
}
},
{ body: "signIn" },
)
.get( .get(
"/login", "/login",
async ({ jwt, redirect, cookie: { auth } }) => { async ({ jwt, redirect, cookie: { auth } }) => {
@ -547,35 +568,18 @@ export const user = new Elysia()
<td>{u.email}</td> <td>{u.email}</td>
<td class="capitalize">{u.role}</td> <td class="capitalize">{u.role}</td>
<td> <td>
<div class="flex items-center gap-6"> <div class="flex flex-wrap items-center gap-3">
{/* Edit / details icon */}
<form method="get" action={`${WEBROOT}/account/edit-user`}> <form method="get" action={`${WEBROOT}/account/edit-user`}>
<input type="hidden" name="userId" value={String(u.id)} /> <input type="hidden" name="userId" value={String(u.id)} />
<button <button
type="submit" type="submit"
class={` class="btn-secondary px-3 py-2"
inline-flex items-center justify-center text-accent-400
hover:text-accent-500
`}
title="Edit user" title="Edit user"
> >
<svg Edit
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-6 w-6"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.458 12C3.732 7.943 7.523 5 12 5s8.268 2.943 9.542 7c-1.274 4.057-5.065 7-9.542 7s-8.268-2.943-9.542-7z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button> </button>
</form> </form>
{/* Delete icon */}
<form <form
method="post" method="post"
action={`${WEBROOT}/account/delete-user`} action={`${WEBROOT}/account/delete-user`}
@ -588,28 +592,10 @@ export const user = new Elysia()
/> />
<button <button
type="submit" type="submit"
class={` class="btn-secondary px-3 py-2"
inline-flex items-center justify-center text-accent-400
hover:text-accent-500
`}
title="Delete user" title="Delete user"
> >
<svg Delete
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-6 w-6"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 7h16" />
<path d="M10 11v6" />
<path d="M14 11v6" />
<path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12" />
<path d="M9 4h6a1 1 0 0 1 1 1v2H8V5a1 1 0 0 1 1-1z" />
</svg>
</button> </button>
</form> </form>
</div> </div>
@ -817,9 +803,7 @@ export const user = new Elysia()
<form <form
method="post" method="post"
action={`${WEBROOT}/account/edit-user`} action={`${WEBROOT}/account/edit-user`}
class={` class="flex flex-col gap-4"
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">