Add files via upload
This commit is contained in:
parent
a30ab9973c
commit
ca61491a31
1 changed files with 114 additions and 115 deletions
|
|
@ -329,9 +329,7 @@ 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;
|
||||
|
|
@ -507,11 +505,7 @@ export const user = new Elysia()
|
|||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Role
|
||||
<select
|
||||
name="newUserRole"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
required
|
||||
>
|
||||
<select name="newUserRole" class="rounded-sm bg-neutral-800 p-3" required>
|
||||
<option value="user">Normal user</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
|
|
@ -552,75 +546,74 @@ export const user = new Elysia()
|
|||
<tr>
|
||||
<td>{u.email}</td>
|
||||
<td class="capitalize">{u.role}</td>
|
||||
|
||||
<td>
|
||||
<div class="flex items-center gap-6">
|
||||
{/* Edit / details icon */}
|
||||
<form method="get" action={`${WEBROOT}/account/edit-user`}>
|
||||
<input type="hidden" name="userId" value={String(u.id)} />
|
||||
<button
|
||||
type="submit"
|
||||
class={`
|
||||
inline-flex items-center justify-center text-accent-400
|
||||
hover:text-accent-500
|
||||
`}
|
||||
title="Edit user"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
</form>
|
||||
<td>
|
||||
<div class="flex items-center gap-6">
|
||||
{/* Edit / details icon */}
|
||||
<form method="get" action={`${WEBROOT}/account/edit-user`}>
|
||||
<input type="hidden" name="userId" value={String(u.id)} />
|
||||
<button
|
||||
type="submit"
|
||||
class={`
|
||||
inline-flex items-center justify-center text-accent-400
|
||||
hover:text-accent-500
|
||||
`}
|
||||
title="Edit user"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
</form>
|
||||
|
||||
{/* Delete icon */}
|
||||
<form
|
||||
method="post"
|
||||
action={`${WEBROOT}/account/delete-user`}
|
||||
onsubmit="return confirm('Are you sure you want to delete this user?');"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="deleteUserId"
|
||||
value={String(u.id)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class={`
|
||||
inline-flex items-center justify-center text-accent-400
|
||||
hover:text-accent-500
|
||||
`}
|
||||
title="Delete user"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
{/* Delete icon */}
|
||||
<form
|
||||
method="post"
|
||||
action={`${WEBROOT}/account/delete-user`}
|
||||
onsubmit="return confirm('Are you sure you want to delete this user?');"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="deleteUserId"
|
||||
value={String(u.id)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class={`
|
||||
inline-flex items-center justify-center text-accent-400
|
||||
hover:text-accent-500
|
||||
`}
|
||||
title="Delete user"
|
||||
>
|
||||
<svg
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
@ -650,9 +643,7 @@ export const user = new Elysia()
|
|||
if (!tokenUser) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(
|
||||
tokenUser.id,
|
||||
);
|
||||
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(tokenUser.id);
|
||||
|
||||
if (!existingUser) {
|
||||
if (auth?.value) {
|
||||
|
|
@ -843,11 +834,7 @@ export const user = new Elysia()
|
|||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
Role
|
||||
<select
|
||||
name="role"
|
||||
class="rounded-sm bg-neutral-800 p-3 capitalize"
|
||||
required
|
||||
>
|
||||
<select name="role" class="rounded-sm bg-neutral-800 p-3 capitalize" required>
|
||||
<option value="user" selected={targetUser.role === "user"}>
|
||||
Normal user
|
||||
</option>
|
||||
|
|
@ -868,10 +855,7 @@ export const user = new Elysia()
|
|||
</label>
|
||||
</fieldset>
|
||||
<div class="flex flex-row gap-4">
|
||||
<a
|
||||
href={`${WEBROOT}/account`}
|
||||
class="w-full btn-secondary text-center"
|
||||
>
|
||||
<a href={`${WEBROOT}/account`} class="w-full btn-secondary text-center">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="w-full btn-primary">
|
||||
|
|
@ -926,35 +910,51 @@ export const user = new Elysia()
|
|||
return redirect(`${WEBROOT}/account`, 302);
|
||||
}
|
||||
|
||||
// Prevent demoting the last admin
|
||||
if (targetUser.role === "admin" && role !== "admin") {
|
||||
const adminCountRow = db
|
||||
.query("SELECT COUNT(*) AS cnt FROM users WHERE role = 'admin'")
|
||||
.get() as { cnt: number };
|
||||
if (adminCountRow.cnt <= 1) {
|
||||
set.status = 400;
|
||||
return { message: "You cannot demote the last remaining admin." };
|
||||
}
|
||||
}
|
||||
|
||||
const fields: string[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const values: any[] = [];
|
||||
|
||||
if (role === "admin" || role === "user") {
|
||||
fields.push("role");
|
||||
values.push(role);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
fields.push("password");
|
||||
values.push(await Bun.password.hash(newPassword));
|
||||
hashedPassword = await Bun.password.hash(newPassword);
|
||||
}
|
||||
|
||||
if (fields.length > 0) {
|
||||
db.query(
|
||||
`UPDATE users SET ${fields.map((f) => `${f}=?`).join(", ")} WHERE id=?`,
|
||||
).run(...values, targetId);
|
||||
// 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;
|
||||
return { message: "You cannot demote the last remaining admin." };
|
||||
}
|
||||
}
|
||||
|
||||
// Password change (optional)
|
||||
if (hashedPassword) {
|
||||
db.query("UPDATE users SET password = ? WHERE id = ?").run(hashedPassword, targetId);
|
||||
}
|
||||
|
||||
db.exec("COMMIT");
|
||||
} catch (e) {
|
||||
try {
|
||||
db.exec("ROLLBACK");
|
||||
} catch (rollbackErr) {
|
||||
console.warn("[user/edit-user] ROLLBACK failed:", rollbackErr);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return redirect(`${WEBROOT}/account`, 302);
|
||||
|
|
@ -1046,4 +1046,3 @@ export const user = new Elysia()
|
|||
cookie: "session",
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue