implemented user creation and deletion

This commit is contained in:
Patrick 2025-09-08 15:28:40 +02:00
parent 30e7d2139d
commit 170e8a8c4f
4 changed files with 154 additions and 44 deletions

View File

@ -15,17 +15,40 @@ const CHECK_QUERY: string =
const USER_DATABASE_SETUP: string[] = [ const USER_DATABASE_SETUP: string[] = [
"PRAGMA foreign_keys = ON;", "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 ( `CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT, name TEXT,
gender TEXT, gender TEXT,
address TEXT, address TEXT,
username TEXT, username TEXT UNIQUE,
password TEXT, password TEXT,
permissions INTEGER DEFAULT 0, permissions INTEGER DEFAULT 0,
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL 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 ( `CREATE TABLE refresh_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
@ -65,6 +88,9 @@ const USER_DATABASE_EMPTY: string =
const USER_DATABASE_UPDATE_PASSWORD: string = const USER_DATABASE_UPDATE_PASSWORD: string =
"UPDATE users SET password=$password WHERE id=$id;" "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 = /*const USER_DATABASE_ADD_ACCESS_TOKEN: string =
"INSERT INTO access_tokens (user_id, token, expiry_date) VALUES ($user_id, $token, $expiry_date);" "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;", "PRAGMA foreign_keys = ON;",
"CREATE TABLE meta (key TEXT PRIMARY KEY NOT NULL, value NUMBER);", "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 ( `CREATE TABLE records (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
@ -92,7 +118,7 @@ const ENTRY_DATABASE_SETUP: string[] = [
record_id INTEGER NOT NULL, record_id INTEGER NOT NULL,
date VARCHAR(10), date VARCHAR(10),
start VARCHAR(5), start VARCHAR(5),
end V<F52>ARCHAR(5), end VARCHAR(5),
comment TEXT, comment TEXT,
modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(record_id) REFERENCES records(id) 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 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) }}) 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 }[] { get_quarters(): { year: number, quarter: number }[] {
const query = this._database.query(ESTIMATES_DATABASE_GET_QUARTERS) const query = this._database.query(ESTIMATES_DATABASE_GET_QUARTERS)
const res = query.all(); const res = query.all() as { year: number, quarter: number }[];
return res; return res;
} }
@ -269,14 +295,14 @@ export class User {
} }
const query = this._database.query(ENTRY_DATABASE_GET_ENTRIES_IN_MONTH); 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; return res;
} }
get_entry(id: number): RecordEntry { get_entry(id: number): RecordEntry {
const query = this._database.query(ENTRY_DATABASE_GET_ENTRY_BY_ID); 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; return res;
} }
@ -318,23 +344,23 @@ export class User {
get_estimates(): Array<EstimatesEntry> { get_estimates(): Array<EstimatesEntry> {
const query = this._database.query(ESTIMATES_DATABASE_GET_ALL); const query = this._database.query(ESTIMATES_DATABASE_GET_ALL);
const res = query.all(); const res = query.all() as EstimatesEntry[];
return res; return res;
} }
get_estimate(year: number, quarter: number): EstimatesEntry { get_estimate(year: number, quarter: number): EstimatesEntry {
const query = this._database.query(ESTIMATES_DATABASE_GET_QUART); 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; return res;
} }
get_estimate_by_month(year: number, month: number): number { get_estimate_by_month(year: number, month: number): number {
const query = this._database.query(ESTIMATES_DATABASE_GET_QUART); 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) { 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 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 }); 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; return res.changes > 1;
} }
@ -365,8 +390,6 @@ function is_db_initialized(db: Database): boolean {
throw exception; throw exception;
} }
console.log(exception);
return false; return false;
} }
} }
@ -376,7 +399,9 @@ function get_user_db_name(user_id: number) {
} }
function setup_db(db: Database, setup_queries: string[]) { 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() { 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<boolean> { export async function create_user(user: { name: string, gender: string, address: string, username: string, password: string }): Promise<number | bigint> {
user.password = await Bun.password.hash(user.password, { algorithm: "bcrypt", cost: 11}); user.password = await Bun.password.hash(user.password, { algorithm: "bcrypt", cost: 11});
try { const statement = user_database.query(USER_DATABASE_ADD_USER);
const statement = user_database.query(USER_DATABASE_ADD_USER); const result = statement.run(user);
const result = statement.run(user);
return result.changes == 1; return result.lastInsertRowid;
} catch (e) {
console.log(e);
if (e instanceof SQLiteError) {
return false;
}
throw e;
}
} }
export function get_all_user(): { id: number, username: string, name: string }[] { 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 }); fs.mkdir(DATABASES_PATH, { recursive: true });
let userdb = new Database(get_user_db_name(user.id), { create: true, strict: true }); let userdb = new Database(get_user_db_name(user.id), { create: true, strict: true });
if (!is_db_initialized(userdb)) { if (!is_db_initialized(userdb)) {
setup_db(userdb, ENTRY_DATABASE_SETUP); setup_db(userdb, ENTRY_DATABASE_SETUP);
} }
@ -499,6 +516,13 @@ export function update_user_password(user_id: number, password: string) {
return result.changes > 0 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<EstimatesEntry, Exclude<keyof EstimatesEntry, "id" | "created">> type ExportEstimatesEntry = Pick<EstimatesEntry, Exclude<keyof EstimatesEntry, "id" | "created">>
type ExportRecordsEntry = Pick<RecordEntry, Exclude<keyof RecordEntry, "id" | "created">> type ExportRecordsEntry = Pick<RecordEntry, Exclude<keyof RecordEntry, "id" | "created">>

View File

@ -2,14 +2,15 @@ import type { PageServerLoad, Actions } from "./$types"
import type { UserEntry } from "$lib/db_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 Permissions from "$lib/permissions"
import { toInt } from "$lib/util" import { toInt } from "$lib/util"
import Logs from "$lib/server/log" import Logs from "$lib/server/log"
import SessionStore from "$lib/server/session_store" 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" import { change_password } from "$lib/server/auth"
export const load: PageServerLoad = ({ locals, url }) => { export const load: PageServerLoad = ({ locals, url }) => {
@ -20,7 +21,15 @@ export const load: PageServerLoad = ({ locals, url }) => {
let user: UserEntry|null = locals.user.toUserEntry() 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)) { if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.VIEW)) {
return fail(403, { message: "Insufficient Permissions" }) return fail(403, { message: "Insufficient Permissions" })
} }
@ -58,7 +67,7 @@ export const actions = {
const data = await request.formData(); 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 name = data.get("name") as string|null
const gender = data.get("gender") as string|null const gender = data.get("gender") as string|null
const address = data.get("address") as string|null const address = data.get("address") as string|null
@ -73,6 +82,39 @@ export const actions = {
return fail(400, { message: "invalid request" }) 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 if (locals.user.id != id
&& (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.EDIT) && (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.EDIT)
|| ((password1 != null || password2 != null) && !Permissions.has(locals.user.permissions, Permissions.USERADMIN.ADMIN)))) { || ((password1 != null || password2 != null) && !Permissions.has(locals.user.permissions, Permissions.USERADMIN.ADMIN)))) {
@ -83,6 +125,7 @@ export const actions = {
if (password1 != password2) { if (password1 != password2) {
return fail(400, { message: "Passwörter müssen übereinstimmen" }) return fail(400, { message: "Passwörter müssen übereinstimmen" })
} }
const result = change_password(id, password1) const result = change_password(id, password1)
if (!result) { if (!result) {
return fail(500, { message: "Database failure"}) return fail(500, { message: "Database failure"})
@ -154,5 +197,35 @@ export const actions = {
} }
return { message: "Import abgeschlossen"} 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 } satisfies Actions

View File

@ -17,12 +17,16 @@
</script> </script>
<form method="GET" id="form_export" action={`/user/export?${page.url.searchParams.toString()}`} ></form> <form method="GET" id="form_export" action={`/user/export?${page.url.searchParams.toString()}`} ></form>
<form method="POST" id="form_import" action="?/import" enctype="multipart/form-data"><input type="hidden" name="id" value={data.user.id} /></form> <form method="POST" id="form_import" action="?/import" enctype="multipart/form-data"><input type="hidden" name="id" value={data.user?.id ?? "new"} /></form>
<form method="POST" id="form_delete" action="?/delete" onsubmit={(event) => {
if (!confirm(`Sicher ${data.user?.name} zu löschen?`)) {
event.preventDefault()
}}}></form>
<form method="POST" id="form_edit" action={`?/edit&${page.url.searchParams.toString()}`} use:enhance={() => { <form method="POST" id="form_edit" action={`?/edit&${page.url.searchParams.toString()}`} use:enhance={() => {
return async ({update}) => { update({ reset: false }) } return async ({update}) => { update({ reset: false }) }
}}> }}>
<input type="hidden" name="id" value={data.user.id} /> <input type="hidden" name="id" value={data.user?.id ?? "new"} />
<div class="root"> <div class="root">
@ -41,7 +45,7 @@
<tbody> <tbody>
<tr> <tr>
<td>Name</td> <td>Name</td>
<td><input type="text" name="name" value={data.user.name} /></td> <td><input type="text" name="name" value={data.user?.name} /></td>
</tr> </tr>
<tr> <tr>
<td>Geschlecht</td> <td>Geschlecht</td>
@ -52,7 +56,7 @@
</tr> </tr>
<tr> <tr>
<td>Addresse</td> <td>Addresse</td>
<td><input type="text" name="address" value={data.user.address} /></td> <td><input type="text" name="address" value={data.user?.address} /></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -69,7 +73,7 @@
<tbody> <tbody>
<tr> <tr>
<td>Benutzername</td> <td>Benutzername</td>
<td colspan="2"><input type="text" name="username" value={data.user.username} /></td> <td colspan="2"><input type="text" name="username" value={data.user?.username} /></td>
</tr> </tr>
<tr> <tr>
<td>Passwort ändern</td> <td>Passwort ändern</td>
@ -103,7 +107,7 @@
type="checkbox" type="checkbox"
name="USERADMIN" name="USERADMIN"
value={permission.value} value={permission.value}
checked={Permissions.has(data.user.permissions, permission.value)} checked={Permissions.has(data.user?.permissions ?? 0, permission.value)}
disabled={disabled} disabled={disabled}
data-bits={Permissions.deconstruct(permission.value).join(" ")} data-bits={Permissions.deconstruct(permission.value).join(" ")}
onclick={(event) => { onclick={(event) => {
@ -140,8 +144,6 @@
} }
} }
}} /> }} />
{permission.name} {permission.name}
</label> </label>
@ -154,7 +156,7 @@
{/if} {/if}
<button class="save-button" type="submit">Speichern</button> <button class="save-button" type="submit">{data.user ? "Speichern" : "Anlegen"}</button>
<table> <table>
<colgroup> <colgroup>
@ -182,6 +184,9 @@
</tbody> </tbody>
</table> </table>
{#if data.user}
<button class="delete_button" form="form_delete" name="user" value={data.user?.id}>Löschen</button>
{/if}
</div> </div>
</form> </form>
@ -253,4 +258,8 @@ input[type="file"] {
text-align: center; text-align: center;
} }
.delete_button {
margin-top: 50px;
float: right;
}
</style> </style>

View File

@ -3,13 +3,14 @@
import type { PageProps } from "./$types" import type { PageProps } from "./$types"
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
const { data }: PageProps = $props(); import Permissions from "$lib/permissions"
console.log(data) const { data }: PageProps = $props();
</script> </script>
<form method="GET" id="form_manage_user" action="user"></form> <form method="GET" id="form_manage_user" action="user"></form>
<form method="GET" id="form_add_user" action="user"></form>
<div> <div>
<h1>Benutzerverwaltung</h1> <h1>Benutzerverwaltung</h1>
@ -20,6 +21,9 @@
{/each} {/each}
</select> </select>
<button type="submit" form="form_manage_user">Edit</button> <button type="submit" form="form_manage_user">Edit</button>
{#if Permissions.has(data.loggedInAs.permissions, Permissions.USERADMIN.CREATE)}
<button type="submit" form="form_add_user" name="user" value="new">Add</button>
{/if}
</div> </div>
<style> <style>