Implemented a basic json import and export of user data

This commit is contained in:
Patrick 2025-09-08 00:34:58 +02:00
parent 8620e9b0cd
commit 503c86a84b
4 changed files with 262 additions and 8 deletions

View File

@ -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<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,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

View File

@ -7,9 +7,18 @@
import Permissions from "$lib/permissions"
const { data, form }: PageProps = $props()
let file_upload: HTMLButtonElement
$effect(() => {
file_upload.disabled = true
})
</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} /></form>
<form method="POST" id="form_edit" action={`?/edit&${page.url.searchParams.toString()}`} use:enhance={() => {
return async ({update}) => { update({ reset: false }) }
}}>
@ -145,7 +154,33 @@
{/if}
<button type="submit">Speichern</button>
<button class="save-button" type="submit">Speichern</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>
</div>
@ -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;
}
</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`
}})
}