Implemented a basic json import and export of user data
This commit is contained in:
parent
8620e9b0cd
commit
503c86a84b
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
}})
|
||||
}
|
||||
Loading…
Reference in New Issue