From 1adbcf9a44d5e57cb5d5783698c09d9968c29c55 Mon Sep 17 00:00:00 2001 From: Patrick Date: Mon, 21 Jul 2025 04:24:21 +0200 Subject: [PATCH 01/17] prepared implementation of permissions --- src/lib/db_types.ts | 1 + src/lib/server/database.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/db_types.ts b/src/lib/db_types.ts index 3b1f9bf..0909611 100644 --- a/src/lib/db_types.ts +++ b/src/lib/db_types.ts @@ -5,6 +5,7 @@ export interface UserEntry { address: string; username: string; password: string; + permissions: number; created: string; } diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index a6b0fb2..63625b6 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -20,6 +20,7 @@ const USER_DATABASE_SETUP: string[] = [ address TEXT, username TEXT, password TEXT, + permissions INTEGER DEFAULT 0, created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL );`, @@ -179,7 +180,8 @@ export class User { name: string; address: string; username: string; - password: string + password: string; + permissions: number; created: string; private _database: Database; @@ -191,6 +193,7 @@ export class User { this.address = user.address; this.username = user.username; this.password = user.password; + this.permissions = user.permissions; this.created = user.created; this._database = db; } -- 2.40.1 From f0b2e9a8185573325ccff20b36f207418ea613bb Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 30 Jul 2025 04:34:54 +0200 Subject: [PATCH 02/17] first steps of implementing permissions --- src/lib/permissions.config.json | 15 ++++++ src/lib/permissions.ts | 94 +++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/lib/permissions.config.json create mode 100644 src/lib/permissions.ts diff --git a/src/lib/permissions.config.json b/src/lib/permissions.config.json new file mode 100644 index 0000000..59d34a7 --- /dev/null +++ b/src/lib/permissions.config.json @@ -0,0 +1,15 @@ +{ + "USERADMIN": { + "size": 8, + "permissions": { + "VIEW": 0, + "ADD": 1, + "DELETE": 2, + "EDIT": 3, + "EDIT_PASSWORD": 4 + }, + "meta_permissions": { + "MANAGE": ["VIEW", "ADD", "DELETE", "EDIT"] + } + }, +} diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts new file mode 100644 index 0000000..a687425 --- /dev/null +++ b/src/lib/permissions.ts @@ -0,0 +1,94 @@ + +type GroupPermissionsDef = Record + +interface GroupDef { + size: number + permissions: GroupPermissionsDef + meta_permissions?: Record> +} + +type PermissionDef = Record + + + + +import raw from "./permissions.config.json" +const config = raw as const + +const __validate_config = (config: unknown): config is PermissionDef => { + const error = (message: string) => new Error("Failed to parse permissions.config.json: " + message) + + if (typeof config !== "object" || config === null) throw error("configuration is not an object or null.") + + for (const [group, definition] of Object.entries(config)) { + if (typeof definition !== "object") + throw error(`definition for ${group} is not an object (is ${typeof definition})`) + if (definition === null) + throw error(`definition for ${group} is null`) + + if (typeof definition.size !== "number") + throw error(`type of size in group ${group} is not number (is ${typeof definition.size})`) + + if (typeof definition.permissions !== "object") + throw error(`definition of permissions for group ${group} is not an object`) + for (const [name, position] of Object.entries(definition.permissions)) { + if (typeof position !== "number") + throw error(`position of ${name} in group ${group} is not a number (is ${typeof position})`) + if (position >= definition.size) + throw error(`position ${position} of permission ${name} in group ${group} is out of bounds (size is ${definition.size})`) + } + + if (definition.meta_permissions !== undefined) { + const permissions = Object.keys(definition.permissions) + + if (typeof definition.meta_permissions !== "object") + throw error(`meta_permissions of group ${group} is not an object (is ${typeof definition.meta_permissions})`) + for (const [name, parts] of Object.entries(definition.meta_permissions)) { + if (Object.keys(definition.permissions).includes(name)) + throw error(`meta permission ${name} uses the same name as the permission`) + if (typeof parts !== "object" && !(parts instanceof Array) || parts === null) + throw error(`definition of meta permission ${name} in group ${group} is not an array`) + for (const partial of (parts as Array)) { + if (!permissions.includes(partial)) + throw error(`permission ${partial} of definition of meta permission ${name} in group ${group} is not a permission of this group`) + } + } + } + } + + return true +} + +type PermissionGroups = keyof typeof config + +function toPermissionObj(config: PermissionDef): Record> { + let curr_pos = 0 + const obj: Record> = {} + + for (const [name, definition] of Object.entries(config)) { + obj[name] = {} + for (const [permission, value] of Object.entries(definition.permissions)) { + obj[name][permission] = 1 << (curr_pos + value) + } + for (const [meta, permissions] of Object.entries(definition?.meta_permissions ?? {})) { + let mask = 0; + for (const permission in permissions) { + mask |= 1 << definition.permissions[permission] + } + + obj[name][meta] = mask << curr_pos + } + + curr_pos += definition.size + } + return obj +} + +__validate_config(config) + + +export default { + ...toPermissionObj(config), + + has: (user_permissions: number, permission: number): boolean => (user_permissions & permission) > 0 +} -- 2.40.1 From f18f430c0ebc9016b361f2f956a6c7dcc164409f Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 31 Jul 2025 07:02:21 +0200 Subject: [PATCH 03/17] added basic user editing --- src/lib/permissions.config.json | 2 +- src/lib/server/auth.ts | 6 +- src/lib/server/database.ts | 89 +++++++++++++------------- src/lib/server/session_store.ts | 8 +++ src/routes/user/+page.server.ts | 69 ++++++++++++++++++++ src/routes/user/+page.svelte | 109 ++++++++++++++++++++++++++++++++ 6 files changed, 238 insertions(+), 45 deletions(-) create mode 100644 src/routes/user/+page.server.ts create mode 100644 src/routes/user/+page.svelte diff --git a/src/lib/permissions.config.json b/src/lib/permissions.config.json index 59d34a7..1deec24 100644 --- a/src/lib/permissions.config.json +++ b/src/lib/permissions.config.json @@ -11,5 +11,5 @@ "meta_permissions": { "MANAGE": ["VIEW", "ADD", "DELETE", "EDIT"] } - }, + } } diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index c66da1a..ae8c08d 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -1,6 +1,6 @@ import Bun from 'bun'; -import { User, get_user_by_name } from "$lib/server/database"; +import { User, get_user_by_name, update_user_password } from "$lib/server/database"; export async function authorize_password(username: string, password: string): Promise { @@ -12,4 +12,8 @@ export async function authorize_password(username: string, password: string): Pr return res ? user : null; } +export async function change_password(user_id: number, new_password: string): Promise { + const hash = await Bun.password.hash(new_password, { algorithm: "bcrypt", cost: 11}); + return update_user_password(user_id, hash) +} diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 63625b6..66c3024 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -48,8 +48,8 @@ const USER_DATABASE_SETUP: string[] = [ const USER_DATABASE_ADD_USER: string = "INSERT INTO users (name, gender, address, username, password) VALUES ($name, $gender, $address, $username, $password);"; -const USER_DATABASE_GET_USER: string = - "SELECT * FROM users;"; +const USER_DATABASE_GET_ALL_USER: string = + "SELECT id, username, name FROM users;"; const USER_DATABASE_GET_USER_BY_NAME: string = "SELECT * FROM users WHERE username = $username;" @@ -57,6 +57,9 @@ const USER_DATABASE_GET_USER_BY_NAME: string = const USER_DATABASE_EMPTY: string = "SELECT EXISTS (SELECT 1 FROM users);" +const USER_DATABASE_UPDATE_PASSWORD: string = + "UPDATE users SET password=$password 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);" @@ -83,7 +86,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) @@ -198,6 +201,19 @@ export class User { this._database = db; } + toUserEntry(): UserEntry { + return { + id: this.id, + gender: this.gender, + name: this.name, + address: this.address, + username: this.username, + password: this.password, + permissions: this.permissions, + created: this.created, + } + } + get_months(): { year: string, month: string }[] { const query = this._database.query(ENTRY_DATABASE_GET_MONTHS); const res = query.all(); @@ -381,51 +397,16 @@ export async function create_user(user: { name: string, gender: string, address: throw e; } } - -function _get_user(): UserEntry | null { +export function get_all_user(): { id: number, username: string, name: string }[] { try { - const statement = user_database.prepare(USER_DATABASE_GET_USER); - const result = statement.get() as UserEntry; - - return result; - - } catch (e) { - if (e instanceof SQLiteError) { - return null; - } - - throw e; - } - -} - -export function get_user(id: number): User | null { - - const user = _get_user(); - if (user == null) { - return null; - } + const query = user_database.prepare(USER_DATABASE_GET_ALL_USER) + const user = query.all() as { id: number, username: string, name: string }[] - const db_name = get_user_db_name(user); - - try { - - fs.mkdir(DATABASES_PATH, { recursive: true }); - - let userdb = new Database(db_name, { create: true, strict: true }); - - if (!is_db_initialized(userdb)) { - setup_db(userdb, ENTRY_DATABASE_SETUP); - } - - return new User(user, userdb); + return user } catch (e) { - console.log(e); - - return null; + throw e } - } export function get_user_by_name(username: string): User | null { @@ -462,3 +443,25 @@ export function do_users_exist(): any { // sqlite trims the first "SELECT " and ";" from the query string return (answer as any)?.[USER_DATABASE_EMPTY.slice(7, -1)]; } + +export function updateUser(data: {id: number, gender?: string, name?: string, address?: string, username?: string }) { + let changed: Array = [] + if (data.gender) changed.push("gender=$gender") + if (data.name) changed.push("name=$name") + if (data.address) changed.push("address=$address") + if (data.username) changed.push("username=$username") + + const update_query = "UPDATE users SET " + changed.join(", ") + " WHERE id=$id;" + + const query = user_database.prepare(update_query) + const result = query.run(data) + + return get_user_by_name(data?.name ?? "") // GET USER BY lastRowId from result +} + +export function update_user_password(user_id: number, password: string) { + const query = user_database.prepare(USER_DATABASE_UPDATE_PASSWORD) + const result = query.run({ password: password, id: user_id }) + + return result.changes > 0 +} diff --git a/src/lib/server/session_store.ts b/src/lib/server/session_store.ts index d27b6a8..cdd5fac 100644 --- a/src/lib/server/session_store.ts +++ b/src/lib/server/session_store.ts @@ -104,6 +104,13 @@ function logout_user_session(token: string): boolean { return true; } +function reload_user_data(user: User) { + const session_info = active_users.get(user.id) + if (!session_info) return + + session_info.user = user +} + async function __clean_session_store() { let cleaned_user_sessions = 0 @@ -135,6 +142,7 @@ export default class SessionStore { static issue_access_token_for_user = issue_access_token_for_user; static get_user_by_access_token = get_user_by_access_token; static logout_user_session = logout_user_session; + static reload_user_data = reload_user_data; } setInterval(__clean_session_store, 15*60*1000); diff --git a/src/routes/user/+page.server.ts b/src/routes/user/+page.server.ts new file mode 100644 index 0000000..b977c47 --- /dev/null +++ b/src/routes/user/+page.server.ts @@ -0,0 +1,69 @@ +import type { PageServerLoad, Actions } from "./$types" + +import { fail } from "@sveltejs/kit" + +import Permissions from "$lib/permissions" +import { toInt } from "$lib/util" + +import SessionStore from "$lib/server/session_store" +import { updateUser } from "$lib/server/database" +import { change_password } from "$lib/server/auth" + +export const load: PageServerLoad = ({ locals, url }) => { + + if (locals.user == null) { + return fail(403, { message: "Unauthorized user" }) + } + + return { + user: locals.user.toUserEntry() + } +} + + +export const actions = { + edit: async ({locals, request}) => { + + if (locals.user == null) { + Logs.route.warn("An unauthorized user tried to edit an user") + return fail(403, { message: "Unauthorized user" }) + } + + const data = await request.formData(); + + const id = 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 + + const username = data.get("username") as string|null + const password1 = data.get("password1") as string|null + const password2 = data.get("password2") as string|null + + if (isNaN(id) || name == null || gender == null || address == null || username == null) { + return fail(400, { message: "invalid request" }) + } + + if (locals.user.id != id + && (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.EDIT) + || ((password1 != null || password2 != null) && !Permissions.has(locals.user.permissions, Permissions.USERADMIN.EDIT_PASSWORD)))) { + return fail(403, { message: "Unauthorized action" }) + } + + if ((password1 != null || password2 != null)) { + 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"}) + } + } + + const updated_user = updateUser({id, name, gender, address, username}) + SessionStore.reload_user_data(updated_user ?? locals.user) + + return {} + + } +} satisfies Actions diff --git a/src/routes/user/+page.svelte b/src/routes/user/+page.svelte new file mode 100644 index 0000000..e714ab0 --- /dev/null +++ b/src/routes/user/+page.svelte @@ -0,0 +1,109 @@ + + +
+ + +
+ +

Benutzer

+ +

{form?.message}

+ + + + + + + + + + + + + + + + + + + + + + + +
Persönliche Daten
Name
Geschlecht +
Addresse
+ + + + + + + + + + + + + + + + + + + + + +
Benutzer
Benutzername
Passwort ändern
+ + + +
+ +
+ + -- 2.40.1 From 9d00b1b76cb2f22301c824dbb2c7e754d0911a90 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 5 Aug 2025 15:15:45 +0200 Subject: [PATCH 04/17] added account link --- src/routes/+layout.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d237206..e4be645 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,6 +11,7 @@
  • Dokumente
  • +
  • Account
  • Benutzerverwaltung
  • -- 2.40.1 From 243612ebbcd47475cc17bc83bc8047631117efd0 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 5 Aug 2025 17:53:44 +0200 Subject: [PATCH 05/17] Added user rudimentary admin page and ability to view other users --- src/lib/server/database.ts | 19 ++++++++++++++ src/routes/+layout.svelte | 2 +- src/routes/user/+page.server.ts | 32 ++++++++++++++++++++---- src/routes/useradmin/+page.server.ts | 26 +++++++++++++++++++ src/routes/useradmin/+page.svelte | 37 ++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 src/routes/useradmin/+page.server.ts create mode 100644 src/routes/useradmin/+page.svelte diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 66c3024..716859c 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -4,6 +4,8 @@ import { Database, SQLiteError } from "bun:sqlite"; import type { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types"; import { calculateDuration, parseDate, toInt, isTimeValidHHMM } from "$lib/util"; +import Logs from "$lib/server/log" + const DATABASES_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/databases/"; const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite"; @@ -51,6 +53,9 @@ const USER_DATABASE_ADD_USER: string = const USER_DATABASE_GET_ALL_USER: string = "SELECT id, username, name FROM users;"; +const USER_DATABASE_GET_USER_BY_ID: string = + "SELECT * FROM users WHERE id = $id;" + const USER_DATABASE_GET_USER_BY_NAME: string = "SELECT * FROM users WHERE username = $username;" @@ -409,6 +414,20 @@ export function get_all_user(): { id: number, username: string, name: string }[] } } +export function get_user_entry_by_id(id: number): UserEntry | null { + + try { + const query = user_database.query(USER_DATABASE_GET_USER_BY_ID) + const user = query.get({ id: id }) as UserEntry | null; + + return user + } catch (e) { + Logs.db.error(`Encountered exception when retrievieng user ${id} from database: ${e.message}`) + } + + return null +} + export function get_user_by_name(username: string): User | null { try { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index e4be645..345e3fc 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -12,7 +12,7 @@
  • Account
  • -
  • Benutzerverwaltung
  • +
  • Benutzerverwaltung
  • diff --git a/src/routes/user/+page.server.ts b/src/routes/user/+page.server.ts index b977c47..f664ab5 100644 --- a/src/routes/user/+page.server.ts +++ b/src/routes/user/+page.server.ts @@ -1,22 +1,44 @@ import type { PageServerLoad, Actions } from "./$types" -import { fail } from "@sveltejs/kit" +import type { UserEntry } from "$lib/db_types" + +import { fail, redirect } from "@sveltejs/kit" import Permissions from "$lib/permissions" import { toInt } from "$lib/util" import SessionStore from "$lib/server/session_store" -import { updateUser } from "$lib/server/database" +import { get_user_entry_by_id, updateUser } from "$lib/server/database" import { change_password } from "$lib/server/auth" export const load: PageServerLoad = ({ locals, url }) => { if (locals.user == null) { - return fail(403, { message: "Unauthorized user" }) + return fail(401, { message: "Unauthorized user" }) } + let user: UserEntry|null = locals.user.toUserEntry() + + if (url.searchParams.has("user")) { + if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.VIEW)) { + //return fail(403, { message: "Insufficient Permissions" }) + } + + let user_id = toInt(url.searchParams.get("user") ?? "") + + if (isNaN(user_id)) { + return fail(400, { message: `Invalid user id: ${url.searchParams.get("user")}`}) + } + + user = get_user_entry_by_id(user_id) + + if (user == null) { + return fail(404, { message: `User ${user_id} not found` }) + } + } + return { - user: locals.user.toUserEntry() + user: user } } @@ -26,7 +48,7 @@ export const actions = { if (locals.user == null) { Logs.route.warn("An unauthorized user tried to edit an user") - return fail(403, { message: "Unauthorized user" }) + return fail(401, { message: "Unauthorized user" }) } const data = await request.formData(); diff --git a/src/routes/useradmin/+page.server.ts b/src/routes/useradmin/+page.server.ts new file mode 100644 index 0000000..2bd8857 --- /dev/null +++ b/src/routes/useradmin/+page.server.ts @@ -0,0 +1,26 @@ +import type { PageServerLoad, Actions } from "./$types" + +import { fail } from "@sveltejs/kit" + +import Permissions from "$lib/permissions"; + +import { get_all_user } from "$lib/server/database" + +export const load: PageServerLoad = ({ locals }) => { + if (locals.user == null) { + return fail(403, { message: "Unauthorized user" }) + } + + if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.VIEW)) { + //return fail(403, { message: "No permission" }) + } + + const user = get_all_user() + + return { + user: user + } +} + +export const actions = { +} satisfies Actions diff --git a/src/routes/useradmin/+page.svelte b/src/routes/useradmin/+page.svelte new file mode 100644 index 0000000..374fd9a --- /dev/null +++ b/src/routes/useradmin/+page.svelte @@ -0,0 +1,37 @@ + + +
  • + +
    +

    Benutzerverwaltung

    + + + +
    + + -- 2.40.1 From 67255f3773eac4c19b2a26100395ebeff01be119 Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 20 Aug 2025 21:17:17 +0200 Subject: [PATCH 06/17] added any permissions check and added ALL Group permission --- src/lib/permissions.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts index a687425..5198a18 100644 --- a/src/lib/permissions.ts +++ b/src/lib/permissions.ts @@ -67,8 +67,13 @@ function toPermissionObj(config: PermissionDef): Record (user_permissions & permission) > 0 + has: (user_permissions: number, permissions: number): boolean => (user_permissions & permissions) == permissions, + any: (user_permissions: number, permissions: number): boolean => (user_permissions & permissions) > 0, } -- 2.40.1 From 803dcd19cb12310d4af26e6d7b831f9ad5367db9 Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 20 Aug 2025 21:18:38 +0200 Subject: [PATCH 07/17] logout, account and useradmin now only shows when logged in --- src/routes/+layout.server.ts | 10 ++++++++ src/routes/+layout.svelte | 45 +++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 src/routes/+layout.server.ts diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..da5d484 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,10 @@ +import type { LayoutServerLoad } from "./$types" + +import Permissions from "$lib/permissions" + +export const load: LayoutServerLoad = ({ locals }) => { + return { + loggedInAs: locals.user?.username, + isAdmin: Permissions.any(locals.user?.permissions ?? 0, Permissions.USERADMIN.ALL) + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 345e3fc..785715b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,25 +1,34 @@ {#snippet nav(classlist: string)} -
    -

    Navigation

    - -
    +
    +

    Navigation

    + +
    {/snippet}