From 503c86a84b4c26bb4dbd9743db8818ecda5d9c43 Mon Sep 17 00:00:00 2001 From: Patrick Date: Mon, 8 Sep 2025 00:34:58 +0200 Subject: [PATCH] Implemented a basic json import and export of user data --- src/lib/server/database.ts | 120 +++++++++++++++++++++++++++++- src/routes/user/+page.server.ts | 55 +++++++++++++- src/routes/user/+page.svelte | 54 +++++++++++++- src/routes/user/export/+server.ts | 41 ++++++++++ 4 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 src/routes/user/export/+server.ts diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 0d50315..3fbf669 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -83,7 +83,8 @@ const ENTRY_DATABASE_SETUP: string[] = [ start VARCHAR(5), end VARCHAR(5), comment TEXT, - created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL + created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + UNIQUE (date, start, end) );`, `CREATE TABLE records_history ( @@ -170,6 +171,12 @@ const ENTRY_DATABASE_EDIT_ENTRY: string = const ENTRY_DATABASE_REMOVE_ENTRY: string = "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 = "SELECT * FROM estimates ORDER BY year DESC, quarter DESC;" @@ -182,6 +189,12 @@ const ESTIMATES_DATABASE_GET_QUART: 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);" +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 { id: number; gender: string; @@ -358,8 +371,8 @@ function is_db_initialized(db: Database): boolean { } } -function get_user_db_name(user: UserEntry) { - return DATABASES_PATH + "user-" + user.id + ".sqlite" +function get_user_db_name(user_id: number) { + return DATABASES_PATH + "user-" + user_id + ".sqlite" } function setup_db(db: Database, setup_queries: string[]) { @@ -440,7 +453,7 @@ export function get_user_by_name(username: string): User | null { fs.mkdir(DATABASES_PATH, { recursive: true }); - let userdb = new Database(get_user_db_name(user), { create: true, strict: true }); + let userdb = new Database(get_user_db_name(user.id), { create: true, strict: true }); if (!is_db_initialized(userdb)) { setup_db(userdb, ENTRY_DATABASE_SETUP); @@ -485,3 +498,102 @@ export function update_user_password(user_id: number, password: string) { return result.changes > 0 } + + +type ExportEstimatesEntry = Pick> +type ExportRecordsEntry = Pick> + +export async function export_user_data(user_id: number): Promise { + 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, records: Array }> = { id: user_id } + + try { + user_data.estimates = userdb.query(ESTIMATES_DATABASE_EXPORT).all() as Array + } catch (e) { + if (!(e instanceof SQLiteError)) { + throw e + } + } + + try { + user_data.records = userdb.query(ENTRY_DATABASE_EXPORT).all() as Array + } 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, records: Array }>) { + + // 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 | null, estimates: Array | 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) +} diff --git a/src/routes/user/+page.server.ts b/src/routes/user/+page.server.ts index 6cbcc73..8533a33 100644 --- a/src/routes/user/+page.server.ts +++ b/src/routes/user/+page.server.ts @@ -2,14 +2,14 @@ import type { PageServerLoad, Actions } from "./$types" import type { UserEntry } from "$lib/db_types" -import { fail, redirect } from "@sveltejs/kit" +import { fail } from "@sveltejs/kit" import Permissions from "$lib/permissions" import { toInt } from "$lib/util" import Logs from "$lib/server/log" import SessionStore from "$lib/server/session_store" -import { get_user_entry_by_id, updateUser } from "$lib/server/database" +import { get_user_entry_by_id, updateUser, import_user_data } from "$lib/server/database" import { change_password } from "$lib/server/auth" export const load: PageServerLoad = ({ locals, url }) => { @@ -103,5 +103,56 @@ export const actions = { 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"} } } satisfies Actions diff --git a/src/routes/user/+page.svelte b/src/routes/user/+page.svelte index 62e5aa2..1cbab69 100644 --- a/src/routes/user/+page.svelte +++ b/src/routes/user/+page.svelte @@ -7,9 +7,18 @@ import Permissions from "$lib/permissions" const { data, form }: PageProps = $props() + + let file_upload: HTMLButtonElement + $effect(() => { + file_upload.disabled = true + }) + +
+
+
{ return async ({update}) => { update({ reset: false }) } }}> @@ -145,7 +154,33 @@ {/if} - + + + + + + + + + + + + + + + + + + + + + + + + + +
Daten
Exportieren:
Importieren: {file_upload.disabled = ((event.target as HTMLInputElement).files?.length == 0)}} required/>
@@ -198,9 +233,24 @@ label:has(input[type="checkbox"][disabled]) { color: gray; } -button { +.save-button { width: 15%; 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; +} + diff --git a/src/routes/user/export/+server.ts b/src/routes/user/export/+server.ts new file mode 100644 index 0000000..48ee595 --- /dev/null +++ b/src/routes/user/export/+server.ts @@ -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` + }}) +}