From 170e8a8c4fa5f8f8415afdc4c72fbb75217f187a Mon Sep 17 00:00:00 2001 From: Patrick Date: Mon, 8 Sep 2025 15:28:40 +0200 Subject: [PATCH] implemented user creation and deletion --- src/lib/server/database.ts | 82 ++++++++++++++++++++----------- src/routes/user/+page.server.ts | 81 ++++++++++++++++++++++++++++-- src/routes/user/+page.svelte | 27 ++++++---- src/routes/useradmin/+page.svelte | 8 ++- 4 files changed, 154 insertions(+), 44 deletions(-) diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 3fbf669..d2c6f6e 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -15,17 +15,40 @@ const CHECK_QUERY: string = const USER_DATABASE_SETUP: string[] = [ "PRAGMA foreign_keys = ON;", + "CREATE TABLE meta (key TEXT PRIMARY KEY NOT NULL, value NUMBER);", + "INSERT INTO meta(key, value) VALUES ('triggerActive', 1);", + `CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT, gender TEXT, address TEXT, - username TEXT, + username TEXT UNIQUE, password TEXT, permissions INTEGER DEFAULT 0, created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL );`, + `CREATE TABLE IF NOT EXISTS users_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + user_id INTEGER, + name TEXT, + gender TEXT, + address TEXT, + username TEXT, + permissions INTEGER DEFAULT 0, + created DATETIME NOT NULL, + deleted DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) + );`, + + `CREATE TRIGGER user_delete_history + BEFORE DELETE ON users + WHEN (SELECT value FROM meta WHERE key = 'triggerActive') = 1 + BEGIN + INSERT INTO users_history(user_id, name, gender, address, username, permissions, created) VALUES (OLD.id, OLD.name, OLD.gender, OLD.address, OLD.username, OLD.permissions, OLD.created); + END;`, + `CREATE TABLE refresh_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER NOT NULL, @@ -65,6 +88,9 @@ const USER_DATABASE_EMPTY: string = const USER_DATABASE_UPDATE_PASSWORD: string = "UPDATE users SET password=$password WHERE id=$id;" +const USER_DATABASE_REMOVE_USER: string = + "DELETE FROM users WHERE id = $id;" + /*const USER_DATABASE_ADD_ACCESS_TOKEN: string = "INSERT INTO access_tokens (user_id, token, expiry_date) VALUES ($user_id, $token, $expiry_date);" @@ -75,7 +101,7 @@ const ENTRY_DATABASE_SETUP: string[] = [ "PRAGMA foreign_keys = ON;", "CREATE TABLE meta (key TEXT PRIMARY KEY NOT NULL, value NUMBER);", - "INSERT INTO meta(key, value) VALUES ('triggerActive', 1)", + "INSERT INTO meta(key, value) VALUES ('triggerActive', 1);", `CREATE TABLE records ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, @@ -92,7 +118,7 @@ const ENTRY_DATABASE_SETUP: string[] = [ record_id INTEGER NOT NULL, date VARCHAR(10), start VARCHAR(5), - end VARCHAR(5), + end VARCHAR(5), comment TEXT, modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, FOREIGN KEY(record_id) REFERENCES records(id) @@ -232,9 +258,9 @@ export class User { } } - get_months(): { year: string, month: string }[] { + get_months(): { year: number, month: number }[] { const query = this._database.query(ENTRY_DATABASE_GET_MONTHS); - const res = query.all(); + const res = query.all() as { year: string, month: string }[]; const ret = res.map((v) => { return { year: toInt(v.year), month: toInt(v.month) }}) @@ -243,7 +269,7 @@ export class User { get_quarters(): { year: number, quarter: number }[] { const query = this._database.query(ESTIMATES_DATABASE_GET_QUARTERS) - const res = query.all(); + const res = query.all() as { year: number, quarter: number }[]; return res; } @@ -269,14 +295,14 @@ export class User { } const query = this._database.query(ENTRY_DATABASE_GET_ENTRIES_IN_MONTH); - const res = query.all({ year: year.toString(), month: month.toString().padStart(2, '0') }); + const res = query.all({ year: year.toString(), month: month.toString().padStart(2, '0') }) as RecordEntry[]; return res; } get_entry(id: number): RecordEntry { const query = this._database.query(ENTRY_DATABASE_GET_ENTRY_BY_ID); - const res = query.get({ id: id }); + const res = query.get({ id: id }) as RecordEntry; return res; } @@ -318,23 +344,23 @@ export class User { get_estimates(): Array { const query = this._database.query(ESTIMATES_DATABASE_GET_ALL); - const res = query.all(); + const res = query.all() as EstimatesEntry[]; return res; } get_estimate(year: number, quarter: number): EstimatesEntry { const query = this._database.query(ESTIMATES_DATABASE_GET_QUART); - const res = query.get({ year: year, quarter: quarter }); + const res = query.get({ year: year, quarter: quarter }) as EstimatesEntry; return res; } get_estimate_by_month(year: number, month: number): number { const query = this._database.query(ESTIMATES_DATABASE_GET_QUART); - const res = query.all({ year: year, quarter: Math.floor(month / 4 + 1) }); + const res = query.get({ year: year, quarter: Math.floor(month / 4 + 1) }) as EstimatesEntry; - return res[0]?.[`estimate_${month % 3}`] ?? NaN; + return res?.[`estimate_${month % 3}`] ?? NaN; } insert_estimate(year: number, quarter: number, estimate_0: number, estimate_1: number, estimate_2: number) { @@ -344,7 +370,6 @@ export class User { const query = this._database.query(ESTIMATES_DATABASE_INSERT); const res = query.run({ year: year, quarter: quarter, estimate_0: estimate_0, estimate_1: estimate_1, estimate_2: estimate_2 }); - console.log(res) return res.changes > 1; } @@ -365,8 +390,6 @@ function is_db_initialized(db: Database): boolean { throw exception; } - console.log(exception); - return false; } } @@ -376,7 +399,9 @@ function get_user_db_name(user_id: number) { } function setup_db(db: Database, setup_queries: string[]) { - setup_queries.forEach((q) => { /*console.log(q);*/ db.query(q).run(); }); + db.transaction(() => { + setup_queries.forEach((q) => { db.query(q).run(); }); + })() } export async function init_db() { @@ -397,23 +422,15 @@ export function close_db() { } } -export async function create_user(user: { name: string, gender: string, address: string, username: string, password: string }): Promise { +export async function create_user(user: { name: string, gender: string, address: string, username: string, password: string }): Promise { user.password = await Bun.password.hash(user.password, { algorithm: "bcrypt", cost: 11}); - try { - const statement = user_database.query(USER_DATABASE_ADD_USER); - const result = statement.run(user); + const statement = user_database.query(USER_DATABASE_ADD_USER); + const result = statement.run(user); - return result.changes == 1; - } catch (e) { - console.log(e); - if (e instanceof SQLiteError) { - return false; - } + return result.lastInsertRowid; - throw e; - } } export function get_all_user(): { id: number, username: string, name: string }[] { @@ -454,7 +471,7 @@ export function get_user_by_name(username: string): User | null { fs.mkdir(DATABASES_PATH, { recursive: true }); let userdb = new Database(get_user_db_name(user.id), { create: true, strict: true }); - + if (!is_db_initialized(userdb)) { setup_db(userdb, ENTRY_DATABASE_SETUP); } @@ -499,6 +516,13 @@ export function update_user_password(user_id: number, password: string) { return result.changes > 0 } +export function remove_user(user_id: number) { + const query = user_database.prepare(USER_DATABASE_REMOVE_USER) + const result = query.run({ id: user_id }) + + return result.changes > 0 +} + type ExportEstimatesEntry = Pick> type ExportRecordsEntry = Pick> diff --git a/src/routes/user/+page.server.ts b/src/routes/user/+page.server.ts index 8533a33..0e9aca8 100644 --- a/src/routes/user/+page.server.ts +++ b/src/routes/user/+page.server.ts @@ -2,14 +2,15 @@ import type { PageServerLoad, Actions } from "./$types" import type { UserEntry } from "$lib/db_types" -import { fail } from "@sveltejs/kit" +import { SQLiteError } from "bun:sqlite" +import { fail, redirect } from "@sveltejs/kit" import Permissions from "$lib/permissions" import { toInt } from "$lib/util" import Logs from "$lib/server/log" import SessionStore from "$lib/server/session_store" -import { get_user_entry_by_id, updateUser, import_user_data } from "$lib/server/database" +import { get_user_entry_by_id, updateUser, import_user_data, create_user, remove_user } from "$lib/server/database" import { change_password } from "$lib/server/auth" export const load: PageServerLoad = ({ locals, url }) => { @@ -20,7 +21,15 @@ export const load: PageServerLoad = ({ locals, url }) => { let user: UserEntry|null = locals.user.toUserEntry() - if (locals.user.id != (toInt(url.searchParams.get("user") ?? locals.user.id.toFixed(0)))) { + const target_user = url.searchParams.get("user") as string|null + + if (target_user == "new") { + return { + user: null + } + } + + if (locals.user.id != (toInt(target_user ?? locals.user.id.toFixed(0)))) { if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.VIEW)) { return fail(403, { message: "Insufficient Permissions" }) } @@ -58,7 +67,7 @@ export const actions = { const data = await request.formData(); - const id = toInt((data.get("id") as string|null) ?? "NaN") + const id = data.get("id") == "new" ? -1 : toInt((data.get("id") as string|null) ?? "NaN") const name = data.get("name") as string|null const gender = data.get("gender") as string|null const address = data.get("address") as string|null @@ -73,6 +82,39 @@ export const actions = { return fail(400, { message: "invalid request" }) } + if (id == -1) { + if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.CREATE)) { + return fail(403, { message: "Keine Berechtigung" }) + } + + if (username.length == 0) { + return fail(400, { message: "Benutzername muss angegeben werden" }) + } + + if (password1 == null || password2 == null || password1.length == 0 || password2.length == 0) { + return fail(400, { message: "Passwort muss für einen neuen Benutzer angegeben werden" }) + } + + if (password1 != password2) { + return fail(400, { message: "Passwörter müssen übereinstimmen" }) + } + + let new_user: number | bigint = -1 + try { + new_user = await create_user({ name, gender, address, username, password: password1 }) + } catch (e) { + if (e instanceof SQLiteError && e.code == "SQLITE_CONSTRAINT_UNIQUE") { + return fail(400, { message: "Benutzername ist bereits vergeben" }) + } + throw e + } + + if (new_user < 0) { + return fail(500, { message: "Interner Fehler" }) + } + return redirect(303, `/user?user=${new_user}`) + } + if (locals.user.id != id && (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.EDIT) || ((password1 != null || password2 != null) && !Permissions.has(locals.user.permissions, Permissions.USERADMIN.ADMIN)))) { @@ -83,6 +125,7 @@ export const actions = { if (password1 != password2) { return fail(400, { message: "Passwörter müssen übereinstimmen" }) } + const result = change_password(id, password1) if (!result) { return fail(500, { message: "Database failure"}) @@ -154,5 +197,35 @@ export const actions = { } return { message: "Import abgeschlossen"} + }, + delete: async ({ locals, request }) => { + + if (!locals.user) { + return fail(401, { message: "Unauthorized User" }) + } + + const data = await request.formData() + + const user_id = toInt((data.get("user") as string|null) ?? "NaN") + + if (isNaN(user_id)) { + return fail(400, { message: "Bad request" }) + } + + if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.DELETE)) { + Logs.user.warn(`User ${locals.user.id} tried to delete user ${id} without sufficient permissions`) + return fail(403, { message: "Unzureichend Berechtigung" }) + } + + const result = remove_user(user_id) + if (!result) { + return fail(500, { message: "Fehler beim Löschen" }) + } + + if (Permissions.has(locals.user.permissions, Permissions.USERADMIN.VIEW)) { + return redirect(303, "/useradmin") + } + + return { message: "Erfolgreich gelöscht" } } } satisfies Actions diff --git a/src/routes/user/+page.svelte b/src/routes/user/+page.svelte index 1cbab69..5cc62a3 100644 --- a/src/routes/user/+page.svelte +++ b/src/routes/user/+page.svelte @@ -17,12 +17,16 @@
-
+
+
{ + if (!confirm(`Sicher ${data.user?.name} zu löschen?`)) { + event.preventDefault() + }}}>
{ return async ({update}) => { update({ reset: false }) } }}> - +
@@ -41,7 +45,7 @@ Name - + Geschlecht @@ -52,7 +56,7 @@ Addresse - + @@ -69,7 +73,7 @@ Benutzername - + Passwort ändern @@ -103,7 +107,7 @@ type="checkbox" name="USERADMIN" value={permission.value} - checked={Permissions.has(data.user.permissions, permission.value)} + checked={Permissions.has(data.user?.permissions ?? 0, permission.value)} disabled={disabled} data-bits={Permissions.deconstruct(permission.value).join(" ")} onclick={(event) => { @@ -140,8 +144,6 @@ } } - - }} /> {permission.name} @@ -154,7 +156,7 @@ {/if} - + @@ -182,6 +184,9 @@
+ {#if data.user} + + {/if}
@@ -253,4 +258,8 @@ input[type="file"] { text-align: center; } +.delete_button { + margin-top: 50px; + float: right; +} diff --git a/src/routes/useradmin/+page.svelte b/src/routes/useradmin/+page.svelte index 374fd9a..62a3dd5 100644 --- a/src/routes/useradmin/+page.svelte +++ b/src/routes/useradmin/+page.svelte @@ -3,13 +3,14 @@ import type { PageProps } from "./$types" import { enhance } from "$app/forms"; - const { data }: PageProps = $props(); + import Permissions from "$lib/permissions" - console.log(data) + const { data }: PageProps = $props();
+

Benutzerverwaltung

@@ -20,6 +21,9 @@ {/each} + {#if Permissions.has(data.loggedInAs.permissions, Permissions.USERADMIN.CREATE)} + + {/if}