From 4fd9862dbde6be070351401f30565315148953bd Mon Sep 17 00:00:00 2001 From: Kosztyk <36381705+Kosztyk@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:58:51 +0200 Subject: [PATCH] Add files via upload --- src/pages/root.tsx | 42 ++-- src/pages/user.tsx | 543 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 558 insertions(+), 27 deletions(-) diff --git a/src/pages/root.tsx b/src/pages/root.tsx index f2a83de..490dae9 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -16,6 +16,8 @@ import { } from "../helpers/env"; import { FIRST_RUN, userService } from "./user"; +type JwtUser = { id: string; role: string } & JWTPayloadSpec; + export const root = new Elysia().use(userService).get( "/", async ({ jwt, redirect, cookie: { auth, jobId } }) => { @@ -30,18 +32,23 @@ export const root = new Elysia().use(userService).get( } // validate jwt - let user: ({ id: string } & JWTPayloadSpec) | false = false; + let user: JwtUser | null = null; + if (ALLOW_UNAUTHENTICATED) { + // unauthenticated / guest mode const newUserId = String( UNAUTHENTICATED_USER_SHARING ? 0 : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), ); + const accessToken = await jwt.sign({ id: newUserId, + role: "user", }); - user = { id: newUserId }; + user = { id: newUserId, role: "user" } as JwtUser; + if (!auth) { return { message: "No auth cookie, perhaps your browser is blocking cookies.", @@ -57,10 +64,14 @@ export const root = new Elysia().use(userService).get( sameSite: "strict", }); } else if (auth?.value) { - user = await jwt.verify(auth.value); + const decoded = await jwt.verify(auth.value); + + if (decoded && typeof decoded === "object" && "id" in decoded && "role" in decoded) { + user = decoded as JwtUser; + } if ( - user !== false && + user && user.id && (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) ) { @@ -82,27 +93,31 @@ export const root = new Elysia().use(userService).get( // create a new job db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run( - user.id, + Number.parseInt(user.id), new Date().toISOString(), ); - const { id } = db - .query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC") - .get(user.id) as { id: number }; + const newJob = db.query("SELECT last_insert_rowid() AS id").get() as { id: number }; if (!jobId) { return { message: "Cookies should be enabled to use this app." }; } jobId.set({ - value: id, + value: newJob.id.toString(), httpOnly: true, secure: !HTTP_ALLOWED, maxAge: 24 * 60 * 60, sameSite: "strict", }); - console.log("jobId set to:", id); + console.log("jobId set to:", newJob.id); + + const converters = await getAllTargets(); + + const storedJobs = db.query( + "SELECT jobs.*, users.email FROM jobs INNER JOIN users ON jobs.user_id = users.id ORDER BY jobs.date_created DESC", + ).all() as (User & { date_created: string; num_files: number; status: string })[]; return ( @@ -174,7 +189,7 @@ export const root = new Elysia().use(userService).get( sm:h-[30vh] `} > - {Object.entries(getAllTargets()).map(([converter, targets]) => ( + {Object.entries(converters).map(([converter, targets]) => (
{targets.map((target) => (
+ + {userData.role === "admin" && ( + <> +
+
+

Add new user

+

+ Create additional users for this ConvertX instance. Admins can create other + admins or normal users. +

+
+
+
+ + + +
+
+ +
+
+
+ + {otherUsers.length > 0 && ( +
+
+

Manage users

+

+ Edit or delete users from this instance. You cannot delete yourself or the + last remaining admin. +

+
+
+ + + + + + + + + + {otherUsers.map((u) => ( + + + + + + ))} + +
EmailRoleActions
{u.email}{u.role} +
+ {/* Edit / details icon */} +
+ + +
+ + {/* Delete icon */} +
+ + +
+
+
+
+
+ )} + + )}
@@ -461,11 +646,13 @@ export const user = new Elysia() return redirect(`${WEBROOT}/login`, 302); } - const user = await jwt.verify(auth.value); - if (!user) { + const tokenUser = await jwt.verify(auth.value); + if (!tokenUser) { return redirect(`${WEBROOT}/login`, 302); } - const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id); + const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get( + tokenUser.id, + ); if (!existingUser) { if (auth?.value) { @@ -483,15 +670,15 @@ export const user = new Elysia() }; } - const fields = []; - const values = []; + const fields: string[] = []; + const values: any[] = []; if (body.email) { - const existingUser = await db + const existingUserWithEmail = await db .query("SELECT id FROM users WHERE email = ?") .as(User) .get(body.email); - if (existingUser && existingUser.id.toString() !== user.id) { + if (existingUserWithEmail && existingUserWithEmail.id.toString() !== tokenUser.id) { set.status = 409; return { message: "Email already in use." }; } @@ -506,7 +693,7 @@ export const user = new Elysia() if (fields.length > 0) { db.query( `UPDATE users SET ${fields.map((field) => `${field}=?`).join(", ")} WHERE id=?`, - ).run(...values, user.id); + ).run(...values, tokenUser.id); } return redirect(`${WEBROOT}/`, 302); @@ -519,4 +706,332 @@ export const user = new Elysia() }), cookie: "session", }, + ) + .post( + "/account/add-user", + async ({ body, set, redirect, jwt, cookie: { auth } }) => { + if (!auth?.value) { + return redirect(`${WEBROOT}/login`, 302); + } + + const tokenUser = await jwt.verify(auth.value); + if (!tokenUser) { + return redirect(`${WEBROOT}/login`, 302); + } + + const actingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(tokenUser.id); + + if (!actingUser) { + if (auth?.value) { + auth.remove(); + } + return redirect(`${WEBROOT}/login`, 302); + } + + if (actingUser.role !== "admin") { + set.status = 403; + return { + message: "Only admins can create new users.", + }; + } + + const { newUserEmail, newUserPassword, newUserRole } = body as { + newUserEmail: string; + newUserPassword: string; + newUserRole: string; + }; + + if (!newUserEmail || !newUserPassword) { + set.status = 400; + return { + message: "Missing email or password.", + }; + } + + const existingNewUser = db.query("SELECT id FROM users WHERE email = ?").get(newUserEmail); + if (existingNewUser) { + set.status = 400; + return { + message: "A user with this email already exists.", + }; + } + + const hashedPassword = await Bun.password.hash(newUserPassword); + const role = newUserRole === "admin" ? "admin" : "user"; + + db.query("INSERT INTO users (email, password, role) VALUES (?, ?, ?)").run( + newUserEmail, + hashedPassword, + role, + ); + + return redirect(`${WEBROOT}/account`, 302); + }, + { + body: t.Object({ + newUserEmail: t.String(), + newUserPassword: t.String(), + newUserRole: t.String(), + }), + cookie: "session", + }, + ) + .get( + "/account/edit-user", + async ({ query, user, redirect }) => { + if (!user) { + return redirect(`${WEBROOT}/login`, 302); + } + + const actingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id); + if (!actingUser || actingUser.role !== "admin") { + return redirect(`${WEBROOT}/account`, 302); + } + + const targetId = Number.parseInt(query.userId, 10); + if (!Number.isFinite(targetId) || targetId === actingUser.id) { + return redirect(`${WEBROOT}/account`, 302); + } + + const targetUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(targetId); + if (!targetUser) { + return redirect(`${WEBROOT}/account`, 302); + } + + return ( + + <> +
+
+
+
+

Edit user

+

+ Change this user's role or set a new password. Leave password blank to keep + it unchanged. +

+
+
+ +
+ + + +
+
+ + Cancel + + +
+
+
+
+ + + ); + }, + { + auth: true, + query: t.Object({ + userId: t.String(), + }), + }, + ) + .post( + "/account/edit-user", + async ({ body, set, redirect, jwt, cookie: { auth } }) => { + if (!auth?.value) { + return redirect(`${WEBROOT}/login`, 302); + } + + const tokenUser = await jwt.verify(auth.value); + if (!tokenUser) { + return redirect(`${WEBROOT}/login`, 302); + } + + const actingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(tokenUser.id); + if (!actingUser || actingUser.role !== "admin") { + set.status = 403; + return { message: "Only admins can edit users." }; + } + + const { userId, role, newPassword } = body as { + userId: string; + role: string; + newPassword?: string; + }; + + const targetId = Number.parseInt(userId, 10); + if (!Number.isFinite(targetId) || targetId === actingUser.id) { + return redirect(`${WEBROOT}/account`, 302); + } + + const targetUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(targetId); + if (!targetUser) { + 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[] = []; + const values: any[] = []; + + if (role === "admin" || role === "user") { + fields.push("role"); + values.push(role); + } + + if (newPassword && newPassword.trim().length > 0) { + fields.push("password"); + values.push(await Bun.password.hash(newPassword)); + } + + if (fields.length > 0) { + db.query( + `UPDATE users SET ${fields.map((f) => `${f}=?`).join(", ")} WHERE id=?`, + ).run(...values, targetId); + } + + return redirect(`${WEBROOT}/account`, 302); + }, + { + body: t.Object({ + userId: t.String(), + role: t.String(), + newPassword: t.MaybeEmpty(t.String()), + }), + cookie: "session", + }, + ) + .post( + "/account/delete-user", + async ({ body, set, redirect, jwt, cookie: { auth } }) => { + if (!auth?.value) { + return redirect(`${WEBROOT}/login`, 302); + } + + const tokenUser = await jwt.verify(auth.value); + if (!tokenUser) { + return redirect(`${WEBROOT}/login`, 302); + } + + const actingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(tokenUser.id); + + if (!actingUser) { + if (auth?.value) { + auth.remove(); + } + return redirect(`${WEBROOT}/login`, 302); + } + + if (actingUser.role !== "admin") { + set.status = 403; + return { message: "Only admins can delete users." }; + } + + const { deleteUserId } = body as { deleteUserId: string }; + const targetId = Number.parseInt(deleteUserId, 10); + + if (!Number.isFinite(targetId)) { + set.status = 400; + return { message: "Invalid user id." }; + } + + if (targetId === actingUser.id) { + set.status = 400; + return { message: "You cannot delete your own account from here." }; + } + + const targetUser = db + .query("SELECT * FROM users WHERE id = ?") + .as(User) + .get(targetId as unknown as number); + + if (!targetUser) { + return redirect(`${WEBROOT}/account`, 302); + } + + if (targetUser.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 delete the last remaining admin." }; + } + } + + // delete this user's jobs and files (to avoid FK issues) + db.query( + "DELETE FROM file_names WHERE job_id IN (SELECT id FROM jobs WHERE user_id = ?)", + ).run(targetId); + db.query("DELETE FROM jobs WHERE user_id = ?").run(targetId); + db.query("DELETE FROM users WHERE id = ?").run(targetId); + + return redirect(`${WEBROOT}/account`, 302); + }, + { + body: t.Object({ + deleteUserId: t.String(), + }), + cookie: "session", + }, ); +