Compare commits

...

21 Commits

Author SHA1 Message Date
Patrick 8c2039c8a4 Added user administration 2025-09-08 16:04:18 +02:00
Patrick 170e8a8c4f implemented user creation and deletion 2025-09-08 15:28:40 +02:00
Patrick 30e7d2139d renamed add to create 2025-09-08 15:28:10 +02:00
Patrick 503c86a84b Implemented a basic json import and export of user data 2025-09-08 00:34:58 +02:00
Patrick 8620e9b0cd renamed EDIT_PASSWORD to ADMIN 2025-08-25 17:23:47 +02:00
Patrick b1787cda4e fixed the inability to fully remove permissions 2025-08-25 17:21:58 +02:00
Patrick 0cd32a0276 reintroduced permission check 2025-08-25 16:53:57 +02:00
Patrick 1049b04968 checking for permission to display permissions 2025-08-25 16:53:11 +02:00
Patrick a46c302be8 now includes full user information in loggedInAs 2025-08-25 16:52:12 +02:00
Patrick 23752001e7 added changing permisisons to database 2025-08-25 16:50:26 +02:00
Patrick 98570a1e5f added permission selector to account page 2025-08-25 15:29:34 +02:00
Patrick 1f72e586f2 moved ALL property to top level as function, added iterate, is_meta and deconstruct and refactored display_name 2025-08-25 15:27:25 +02:00
Patrick 9093ddaeb5 added display name to config 2025-08-22 13:54:27 +02:00
Patrick b2e6f059a6 extended account page 2025-08-21 08:44:54 +02:00
Patrick 803dcd19cb logout, account and useradmin now only shows when logged in 2025-08-20 21:18:38 +02:00
Patrick 67255f3773 added any permissions check and added ALL Group permission 2025-08-20 21:17:17 +02:00
Patrick 243612ebbc Added user rudimentary admin page and ability to view other users 2025-08-05 17:53:44 +02:00
Patrick 9d00b1b76c added account link 2025-08-05 15:16:11 +02:00
Patrick f18f430c0e added basic user editing 2025-08-05 15:16:11 +02:00
Patrick f0b2e9a818 first steps of implementing permissions 2025-08-05 15:16:11 +02:00
Patrick 1adbcf9a44 prepared implementation of permissions 2025-08-05 15:16:11 +02:00
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,7 +470,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), { 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

@ -8,12 +8,25 @@
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>