Add files via upload

This commit is contained in:
Kosztyk 2026-01-12 22:21:04 +02:00 committed by GitHub
parent 59b2fd19c9
commit 8baa7c2c55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 77 additions and 99 deletions

View file

@ -76,7 +76,7 @@ export const root = new Elysia().use(userService).get(
// fallback: treat as normal user if role missing // fallback: treat as normal user if role missing
user = { ...verifiedUser, id: verifiedUser.id, role: "user" }; user = { ...verifiedUser, id: verifiedUser.id, role: "user" };
} else { } else {
user = verifiedUser as ({ id: string; role: string } & JWTPayloadSpec); user = verifiedUser as { id: string; role: string } & JWTPayloadSpec;
} }
} }

View file

@ -193,88 +193,88 @@ export const user = new Elysia()
</BaseHtml> </BaseHtml>
); );
}) })
.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. // 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. // We hash outside the write-lock to keep the lock window short.
const savedPassword = await Bun.password.hash(password); const savedPassword = await Bun.password.hash(password);
// Acquire a write lock so only one instance can perform "count==0 then insert" at a time. // Acquire a write lock so only one instance can perform "count==0 then insert" at a time.
db.exec("BEGIN IMMEDIATE"); db.exec("BEGIN IMMEDIATE");
try { try {
const isFirstUser = computeFirstRun(); const isFirstUser = computeFirstRun();
// first user allowed even if ACCOUNT_REGISTRATION=false // first user allowed even if ACCOUNT_REGISTRATION=false
if (!ACCOUNT_REGISTRATION && !isFirstUser) { if (!ACCOUNT_REGISTRATION && !isFirstUser) {
db.exec("ROLLBACK"); 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); const existingUser = db.query("SELECT 1 FROM users WHERE email = ?").get(email);
if (existingUser) { if (existingUser) {
db.exec("ROLLBACK"); db.exec("ROLLBACK");
set.status = 400; set.status = 400;
return { return {
message: "Email already in use.", message: "Email already in use.",
}; };
} }
const role = isFirstUser ? "admin" : "user"; const role = isFirstUser ? "admin" : "user";
db.query("INSERT INTO users (email, password, role) VALUES (?, ?, ?)").run( db.query("INSERT INTO users (email, password, role) VALUES (?, ?, ?)").run(
email, email,
savedPassword, savedPassword,
role, role,
); );
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"); db.exec("ROLLBACK");
set.status = 500; set.status = 500;
return { return {
message: "Failed to create user.", message: "Failed to create user.",
}; };
} }
db.exec("COMMIT"); db.exec("COMMIT");
FIRST_RUN = false; 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 ?? "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
auth.set({ auth.set({
value: accessToken, value: accessToken,
httpOnly: true, httpOnly: true,
secure: !HTTP_ALLOWED, secure: !HTTP_ALLOWED,
maxAge: 60 * 60 * 24 * 7, maxAge: 60 * 60 * 24 * 7,
sameSite: "strict", sameSite: "strict",
}); });
return redirect(`${WEBROOT}/`, 302); return redirect(`${WEBROOT}/`, 302);
} catch (e) { } catch (e) {
try { try {
db.exec("ROLLBACK"); db.exec("ROLLBACK");
} catch (rollbackErr) { } catch {
console.warn("[user/register] ROLLBACK failed:", rollbackErr); // ignore rollback errors
} }
throw e; throw e;
} }
}, },
{ body: "signIn" }, { body: "signIn" },
) )
.get( .get(
"/login", "/login",
async ({ jwt, redirect, cookie: { auth } }) => { async ({ jwt, redirect, cookie: { auth } }) => {
if (computeFirstRun()) { if (computeFirstRun()) {
@ -357,9 +357,7 @@ export const user = new Elysia()
.post( .post(
"/login", "/login",
async function handler({ body, set, redirect, jwt, cookie: { auth } }) { async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
const existingUser = db.query("SELECT * FROM users WHERE email = ?").as(User).get( const existingUser = db.query("SELECT * FROM users WHERE email = ?").as(User).get(body.email);
body.email,
);
if (!existingUser) { if (!existingUser) {
set.status = 403; set.status = 403;
@ -535,11 +533,7 @@ export const user = new Elysia()
</label> </label>
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1">
Role Role
<select <select name="newUserRole" class="rounded-sm bg-neutral-800 p-3" required>
name="newUserRole"
class="rounded-sm bg-neutral-800 p-3"
required
>
<option value="user">Normal user</option> <option value="user">Normal user</option>
<option value="admin">Admin</option> <option value="admin">Admin</option>
</select> </select>
@ -583,15 +577,8 @@ export const user = new Elysia()
<td> <td>
<div class="flex items-center gap-6"> <div class="flex items-center gap-6">
{/* Edit / details icon */} {/* Edit / details icon */}
<form <form method="get" action={`${WEBROOT}/account/edit-user`}>
method="get" <input type="hidden" name="userId" value={String(u.id)} />
action={`${WEBROOT}/account/edit-user`}
>
<input
type="hidden"
name="userId"
value={String(u.id)}
/>
<button <button
type="submit" type="submit"
class={` class={`
@ -684,9 +671,7 @@ export const user = new Elysia()
if (!tokenUser) { if (!tokenUser) {
return redirect(`${WEBROOT}/login`, 302); return redirect(`${WEBROOT}/login`, 302);
} }
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get( const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(tokenUser.id);
tokenUser.id,
);
if (!existingUser) { if (!existingUser) {
if (auth?.value) { if (auth?.value) {
@ -875,11 +860,7 @@ export const user = new Elysia()
</label> </label>
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1">
Role Role
<select <select name="role" class="rounded-sm bg-neutral-800 p-3 capitalize" required>
name="role"
class="rounded-sm bg-neutral-800 p-3 capitalize"
required
>
<option value="user" selected={targetUser.role === "user"}> <option value="user" selected={targetUser.role === "user"}>
Normal user Normal user
</option> </option>
@ -900,10 +881,7 @@ export const user = new Elysia()
</label> </label>
</fieldset> </fieldset>
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<a <a href={`${WEBROOT}/account`} class="w-full btn-secondary text-center">
href={`${WEBROOT}/account`}
class="w-full btn-secondary text-center"
>
Cancel Cancel
</a> </a>
<button type="submit" class="w-full btn-primary"> <button type="submit" class="w-full btn-primary">
@ -984,9 +962,10 @@ export const user = new Elysia()
} }
if (fields.length > 0) { if (fields.length > 0) {
db.query( db.query(`UPDATE users SET ${fields.map((f) => `${f}=?`).join(", ")} WHERE id=?`).run(
`UPDATE users SET ${fields.map((f) => `${f}=?`).join(", ")} WHERE id=?`, ...values,
).run(...values, targetId); targetId,
);
} }
return redirect(`${WEBROOT}/account`, 302); return redirect(`${WEBROOT}/account`, 302);
@ -1078,4 +1057,3 @@ export const user = new Elysia()
cookie: "session", cookie: "session",
}, },
); );