Added user administration

This commit is contained in:
Patrick 2025-09-08 16:04:18 +02:00
commit 8c2039c8a4
6 changed files with 412 additions and 48 deletions

View File

@ -6,7 +6,7 @@
"position": 0, "position": 0,
"name": "Anzeigen" "name": "Anzeigen"
}, },
"ADD": { "CREATE": {
"position": 1, "position": 1,
"name": "Anlegen" "name": "Anlegen"
}, },
@ -25,7 +25,7 @@
}, },
"meta_permissions": { "meta_permissions": {
"MANAGE": { "MANAGE": {
"permissions": ["VIEW", "ADD", "DELETE", "EDIT"], "permissions": ["VIEW", "CREATE", "DELETE", "EDIT"],
"name": "Verwalten" "name": "Verwalten"
} }
} }

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,
@ -83,7 +109,8 @@ const ENTRY_DATABASE_SETUP: string[] = [
start VARCHAR(5), start VARCHAR(5),
end VARCHAR(5), end VARCHAR(5),
comment TEXT, comment TEXT,
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
UNIQUE (date, start, end)
);`, );`,
`CREATE TABLE records_history ( `CREATE TABLE records_history (
@ -170,6 +197,12 @@ const ENTRY_DATABASE_EDIT_ENTRY: string =
const ENTRY_DATABASE_REMOVE_ENTRY: string = const ENTRY_DATABASE_REMOVE_ENTRY: string =
"DELETE FROM records WHERE id = $id;"; "DELETE FROM records WHERE id = $id;";
const ENTRY_DATABASE_EXPORT: string =
"SELECT date, start, end, comment FROM records;"
const ENTRY_DATABASE_IMPORT: string =
"INSERT OR IGNORE INTO records(date, start, end, comment) VALUES ($date, $start, $end, $comment);"
const ESTIMATES_DATABASE_GET_ALL: string = const ESTIMATES_DATABASE_GET_ALL: string =
"SELECT * FROM estimates ORDER BY year DESC, quarter DESC;" "SELECT * FROM estimates ORDER BY year DESC, quarter DESC;"
@ -182,6 +215,12 @@ const ESTIMATES_DATABASE_GET_QUART: string =
const ESTIMATES_DATABASE_INSERT: string = const ESTIMATES_DATABASE_INSERT: string =
"INSERT INTO estimates(year, quarter, estimate_0, estimate_1, estimate_2) VALUES ($year, $quarter, $estimate_0, $estimate_1, $estimate_2);" "INSERT INTO estimates(year, quarter, estimate_0, estimate_1, estimate_2) VALUES ($year, $quarter, $estimate_0, $estimate_1, $estimate_2);"
const ESTIMATES_DATABASE_EXPORT: string =
"SELECT year, quarter, estimate_0, estimate_1, estimate_2 FROM estimates;"
const ESTIMATES_DATABASE_IMPORT: string =
"INSERT OR IGNORE INTO estimates(year, quarter, estimate_0, estimate_1, estimate_2) VALUES ($year, $quarter, $estimate_0, $estimate_1, $estimate_2);"
export class User { export class User {
id: number; id: number;
gender: string; gender: string;
@ -219,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) }})
@ -230,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;
} }
@ -256,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;
} }
@ -305,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) {
@ -331,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;
} }
@ -352,18 +390,18 @@ function is_db_initialized(db: Database): boolean {
throw exception; throw exception;
} }
console.log(exception);
return false; return false;
} }
} }
function get_user_db_name(user: UserEntry) { function get_user_db_name(user_id: number) {
return DATABASES_PATH + "user-" + user.id + ".sqlite" return DATABASES_PATH + "user-" + user_id + ".sqlite"
} }
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() {
@ -384,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 }[] {
@ -440,8 +470,8 @@ 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), { 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);
} }
@ -485,3 +515,109 @@ 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 ExportRecordsEntry = Pick<RecordEntry, Exclude<keyof RecordEntry, "id" | "created">>
export async function export_user_data(user_id: number): Promise<string | null> {
const db_name = get_user_db_name(user_id)
if (!(await fs.exists(db_name))) {
return null
}
const userdb = new Database(db_name, { create: false, strict: true });
let user_data: { id: number } & Partial<{ estimates: Array<ExportEstimatesEntry>, records: Array<ExportRecordsEntry> }> = { id: user_id }
try {
user_data.estimates = userdb.query(ESTIMATES_DATABASE_EXPORT).all() as Array<ExportEstimatesEntry>
} catch (e) {
if (!(e instanceof SQLiteError)) {
throw e
}
}
try {
user_data.records = userdb.query(ENTRY_DATABASE_EXPORT).all() as Array<ExportRecordsEntry>
} catch (e) {
if (!(e instanceof SQLiteError)) {
throw e
}
}
return JSON.stringify(user_data)
}
export async function import_user_data(data: Partial<{ id: number, estimates: Array<unknown>, records: Array<unknown> }>) {
// null or unknown ar of type object
if (!data.id || !(typeof data.id === "number")) {
throw new Error("Invalid JSON: id")
}
if (data.estimates != null) {
if (!(Array.isArray(data.estimates))) {
throw new Error("Invalid JSON: estimates")
}
if (!data.estimates.every((v: unknown): v is ExportEstimatesEntry =>
typeof v === "object"
&& v != null
&& "year" in v
&& "quarter" in v
&& "estimate_0" in v
&& "estimate_1" in v
&& "estimate_2" in v
&& typeof v.year === "number"
&& typeof v.quarter === "number"
&& (typeof v.estimate_0 === "number" || v.estimate_0 == null)
&& (typeof v.estimate_1 === "number" || v.estimate_1 == null)
&& (typeof v.estimate_2 === "number" || v.estimate_2 == null))) {
throw new Error("Invalid JSON: estimates entry")
}
}
if (data.records != null) {
if (!(Array.isArray(data.records))) {
throw new Error("Invalid JSON: records")
}
if (!data.records.every((v: unknown): v is ExportRecordsEntry =>
typeof v === "object"
&& v != null
&& "date" in v
&& "start" in v
&& "end" in v
&& "comment" in v
&& typeof v.date === "string"
&& typeof v.start === "string"
&& typeof v.end === "string"
&& typeof v.comment === "string")) {
throw new Error("Invalid JSON: records entry")
}
}
const user_database_name = get_user_db_name(data.id)
const user_database = new Database(user_database_name, { strict: true })
user_database.transaction((records: Array<ExportRecordsEntry> | null, estimates: Array<ExportEstimatesEntry> | null) => {
if (records != null) {
const query = user_database.prepare(ENTRY_DATABASE_IMPORT)
for (const record of records) { query.run(record) }
}
if (estimates != null) {
const query = user_database.prepare(ESTIMATES_DATABASE_IMPORT)
for (const estimate of estimates) { query.run(estimate) }
}
})(data.records, data.estimates)
}

View File

@ -2,6 +2,7 @@ import type { PageServerLoad, Actions } from "./$types"
import type { UserEntry } from "$lib/db_types" import type { UserEntry } from "$lib/db_types"
import { SQLiteError } from "bun:sqlite"
import { fail, redirect } from "@sveltejs/kit" import { fail, redirect } from "@sveltejs/kit"
import Permissions from "$lib/permissions" import Permissions from "$lib/permissions"
@ -9,7 +10,7 @@ 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 } 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"})
@ -103,5 +146,86 @@ export const actions = {
return { message: "Erfolgreich gespeichert" } return { message: "Erfolgreich gespeichert" }
},
import: async ({ locals, request }) => {
if (!locals.user) {
return fail(401, { message: "Unauthorized User" })
}
const data = await request.formData()
const id = toInt(data.get("id") as string|null ?? "")
const file = data.get("file")
if (isNaN(id)) {
return fail(400, { message: "Invalid id" })
}
if (!file || typeof file === "string") {
return fail(400, { message: "Invalid file" })
}
let import_data
try {
import_data = await file.json()
} catch (e) {
if (e instanceof SyntaxError) {
console.log(e)
return fail(400, { message: "Uploaded file is erronous" })
}
throw e
}
if (!import_data.id || import_data.id != id) {
return fail(400, { message: "Datei für falschen Benutzer hochgeladen" })
}
if (locals.user.id != id) {
if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.ADMIN)) {
Logs.user.warn(`User ${locals.user.id} tried to import data for user ${id} without sufficient permissions`)
return fail(403, { message: "Unzureichend Berechtigung" })
}
}
try {
import_user_data(import_data)
} catch (e) {
Logs.user.error(`Failed to import data for user ${id} by user ${locals.user.id}: ${(e as Error).message}`)
return fail(500, { message: "Import fehlgeschlagen" })
}
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

@ -7,13 +7,26 @@
import Permissions from "$lib/permissions" import Permissions from "$lib/permissions"
const { data, form }: PageProps = $props() const { data, form }: PageProps = $props()
let file_upload: HTMLButtonElement
$effect(() => {
file_upload.disabled = true
})
</script> </script>
<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 ?? "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">
@ -32,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>
@ -43,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>
@ -60,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>
@ -94,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) => {
@ -131,8 +144,6 @@
} }
} }
}} /> }} />
{permission.name} {permission.name}
</label> </label>
@ -145,8 +156,37 @@
{/if} {/if}
<button type="submit">Speichern</button> <button class="save-button" type="submit">{data.user ? "Speichern" : "Anlegen"}</button>
<table>
<colgroup>
<col class="leader2" />
<col class="form2 center-button" />
</colgroup>
<thead>
<tr><th colspan="2">Daten</th></tr>
</thead>
<tbody>
<tr>
<td>Exportieren:</td>
<td><button type="submit" form="form_export">Download</button></td>
</tr>
<tr>
<td rowspan="2">Importieren:</td>
<td><input type="file" form="form_import" name="file" accept="application/json" onchange={
(event) => {file_upload.disabled = ((event.target as HTMLInputElement).files?.length == 0)}} required/></td>
</tr>
<tr>
<td><button type="submit" form="form_import" bind:this={file_upload} >Hochladen</button></td>
</tr>
</tbody>
</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>
@ -198,9 +238,28 @@ label:has(input[type="checkbox"][disabled]) {
color: gray; color: gray;
} }
button { .save-button {
width: 15%; width: 15%;
margin-left: 85%; margin-left: 85%;
} }
table td:has(button) {
text-align: center;
}
table button {
width: 75%;
}
input[type="file"] {
width: 100%;
padding: 5px;
padding-left: 10px;
border: 1px dotted lightblue;
text-align: center;
}
.delete_button {
margin-top: 50px;
float: right;
}
</style> </style>

View File

@ -0,0 +1,41 @@
import type { RequestHandler } from "./$types.d"
import { json, error } from "@sveltejs/kit"
import Logs from "$lib/server/log"
import { get_user_entry_by_id, export_user_data } from "$lib/server/database"
import Permissions from "$lib/permissions"
import { toInt } from "$lib/util"
export const GET: RequestHandler = async ({ locals, url }) => {
if (locals.user == null) {
return error(401, { message: "Unauthorized user" })
}
let user_id = locals.user.id
const user_id_param = toInt(url.searchParams.get("user") ?? "")
if (!isNaN(user_id_param) && user_id != user_id_param) {
if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.ADMIN)) {
Logs.user.warn(`User ${locals.user.id} tried to export records for user ${url.searchParams.get("user")} without sufficient permissions`)
return error(403, { message: "Insufficient permissions" })
}
const new_user = get_user_entry_by_id(user_id_param)
if (new_user == null) {
return error(400, { message: "invalid user" })
}
user_id = new_user.id
}
Logs.user.info(`User ${locals.user.id} exported userdata for user ${user_id}`)
const data = await export_user_data(user_id)
return new Response(data, {
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename=Export-${locals.user.username}-${(new Date()).toJSON()}.json`
}})
}

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>