Add files via upload

This commit is contained in:
Kosztyk 2026-01-14 15:52:54 +02:00 committed by GitHub
parent a30ab9973c
commit ca61491a31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -329,9 +329,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;
@ -507,11 +505,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>
@ -552,7 +546,6 @@ export const user = new Elysia()
<tr> <tr>
<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 items-center gap-6">
{/* Edit / details icon */} {/* Edit / details icon */}
@ -650,9 +643,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) {
@ -843,11 +834,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>
@ -868,10 +855,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">
@ -926,35 +910,51 @@ export const user = new Elysia()
return redirect(`${WEBROOT}/account`, 302); return redirect(`${WEBROOT}/account`, 302);
} }
// Prevent demoting the last admin // IMPORTANT: do not hold a SQLite write transaction open across an `await`.
if (targetUser.role === "admin" && role !== "admin") { // Hash any password outside the transaction to avoid lock contention.
const adminCountRow = db let hashedPassword: string | null = null;
.query("SELECT COUNT(*) AS cnt FROM users WHERE role = 'admin'") if (newPassword && newPassword.trim().length > 0) {
.get() as { cnt: number }; hashedPassword = await Bun.password.hash(newPassword);
if (adminCountRow.cnt <= 1) { }
// Atomic last-admin protection: concurrent demotions must not be able to leave zero admins.
// Serialize writers and make demotion conditional in a single statement.
db.exec("BEGIN IMMEDIATE");
try {
// Role change
if (role === "admin") {
db.query("UPDATE users SET role = 'admin' WHERE id = ?").run(targetId);
} else if (role === "user") {
const demoteRes = db
.query(
`UPDATE users
SET role = 'user'
WHERE id = ?
AND role = 'admin'
AND (SELECT COUNT(*) FROM users WHERE role = 'admin') > 1`,
)
.run(targetId);
if (targetUser.role === "admin" && demoteRes.changes === 0) {
db.exec("ROLLBACK");
set.status = 400; set.status = 400;
return { message: "You cannot demote the last remaining admin." }; return { message: "You cannot demote the last remaining admin." };
} }
} }
const fields: string[] = []; // Password change (optional)
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (hashedPassword) {
const values: any[] = []; db.query("UPDATE users SET password = ? WHERE id = ?").run(hashedPassword, targetId);
if (role === "admin" || role === "user") {
fields.push("role");
values.push(role);
} }
if (newPassword && newPassword.trim().length > 0) { db.exec("COMMIT");
fields.push("password"); } catch (e) {
values.push(await Bun.password.hash(newPassword)); try {
db.exec("ROLLBACK");
} catch (rollbackErr) {
console.warn("[user/edit-user] ROLLBACK failed:", rollbackErr);
} }
throw e;
if (fields.length > 0) {
db.query(
`UPDATE users SET ${fields.map((f) => `${f}=?`).join(", ")} WHERE id=?`,
).run(...values, targetId);
} }
return redirect(`${WEBROOT}/account`, 302); return redirect(`${WEBROOT}/account`, 302);
@ -1046,4 +1046,3 @@ export const user = new Elysia()
cookie: "session", cookie: "session",
}, },
); );