From 104cafbcec6b437366bfeb90fa5c8c68286aecef Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 5 Jun 2025 14:32:12 +0200 Subject: [PATCH] Added Login --- src/hooks.server.ts | 33 ++++- src/lib/server/auth.ts | 21 ++++ src/lib/server/database.ts | 93 ++++++++++++-- src/lib/server/session_store.ts | 127 +++++++++++++++++++ src/routes/+layout.svelte | 19 +++ src/routes/+page.server.ts | 144 ++++++++++++---------- src/routes/dokumente/+page.server.ts | 28 +++-- src/routes/dokumente/[...file]/+server.ts | 9 +- src/routes/login/+page.server.ts | 73 +++++++++++ src/routes/login/+page.svelte | 48 ++++++++ src/routes/schaetzung/+page.server.ts | 42 ++++--- svelte.config.js | 4 +- 12 files changed, 527 insertions(+), 114 deletions(-) create mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/session_store.ts create mode 100644 src/routes/login/+page.server.ts create mode 100644 src/routes/login/+page.svelte diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 5b72685..4215a6c 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,6 +1,7 @@ import type { Handle } from "@sveltejs/kit"; -import { error } from "@sveltejs/kit"; +import { error, redirect } from "@sveltejs/kit"; +import SessionStore from "$lib/server/session_store" import { init_db, close_db, get_user } from "$lib/server/database"; async function init() { @@ -39,11 +40,33 @@ process.on('SIGINT', (_) => { }) export let handle: Handle = async function ({ event, resolve }) { - - let user = get_user(); - if (!user) { - return error(404, "No user"); // redirect login + console.log("incoming ", event.request.method, " request to: ", event.url.href, " (route id: ", event.route.id, ")"); + + if (event.route.id == null) { + return error(404, "This page does not exist."); + } + + const token = event.cookies.get("session_id") + const user = SessionStore.get_user_by_access_token(token ?? "") + + console.log("tried access with token: ", token) + + if (!token || !user) { + if (event.request.method == "POST" && event.route.id != "/login") { + return error(401, "Invalid Session"); + } + + if (token) { + event.cookies.delete("session_id", { path: "/" }); + } + + if (event.route.id == "/login") { + return await resolve(event); + } else { + event.url.searchParams.set("redirect", event.route.id); + return redirect(302, `/login?${event.url.searchParams}`); + } } event.locals.user = user; diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..618ec35 --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,21 @@ +import Bun from 'bun'; + +import { User, get_user_by_name } from "$lib/server/database"; + +export async function authorize_password(username: string, password: string): Promise { + + const user = get_user_by_name(username); + const password_hash = user?.password ?? ""; + + console.log("Username: ", username); + console.log("Password: ", password); + + console.log(user); + + const res = await Bun.password.verify(password, password_hash, "bcrypt"); + console.log("hash res:", res); + + return res ? user : null; +} + + diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 8201e69..7988c46 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import { Database, SQLiteError } from "bun:sqlite"; -import { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types"; +import type { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types"; import { calculateDuration, parseDate, toInt, isTimeValidHHMM } from "$lib/util"; const DATABASES_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/databases/"; @@ -11,22 +11,54 @@ const CHECK_QUERY: string = "SELECT * FROM sqlite_master;"; const USER_DATABASE_SETUP: string[] = [ + "PRAGMA foreign_keys = ON;", + `CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - gender TEXT, name TEXT, + gender TEXT, address TEXT, - initials TEXT, + username TEXT, + password TEXT, created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL );`, + + `CREATE TABLE refresh_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + user_id INTEGER NOT NULL, + token TEXT UNIQUE, + expiry_date DATETIME, + created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) + );`, + + /*`CREATE TABLE session_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + user_id INTEGER NOT NULL, + create_token_id INTEGER NOT NULL, + token TEXT UNIQUE, + expiry_date DATETIME, + created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id), + FOREIGN KEY(create_token_id) REFERENCES access_tokens(id) + );`,*/ ]; const USER_DATABASE_ADD_USER: string = - "INSERT INTO users (name, initials) VALUES ($name, $initials);"; + "INSERT INTO users (name, gender, address, username, password) VALUES ($name, $gender, $address, $username, $password);"; const USER_DATABASE_GET_USER: string = "SELECT * FROM users;"; +const USER_DATABASE_GET_USER_BY_NAME: string = + "SELECT * FROM users WHERE username = $username;" + +/*const USER_DATABASE_ADD_ACCESS_TOKEN: string = + "INSERT INTO access_tokens (user_id, token, expiry_date) VALUES ($user_id, $token, $expiry_date);" + +const USER_DATABASE_REMOVE_ACCESS_TOKEN: string = + "UPDATE access_tokens SET expiry_date = NULL WHERE token = $token;"*/ + const ENTRY_DATABASE_SETUP: string[] = [ "PRAGMA foreign_keys = ON;", @@ -143,17 +175,19 @@ export class User { gender: string; name: string; address: string; - initials: string; + username: string; + password: string created: string; - private database: Database; + private _database: Database; constructor(user: UserEntry, db: Database) { this.id = user.id; this.gender = user.gender; this.name = user.name; this.address = user.address; - this.initials = user.initials; + this.username = user.username; + this.password = user.password; this.created = user.created; this._database = db; } @@ -308,11 +342,14 @@ function setup_db(db: Database, setup_queries: string[]) { export async function init_db() { const stdout = await fs.mkdir(DATABASES_PATH, { recursive: true }); - console.log(stdout) + user_database = new Database(USER_DATABASE_PATH, { strict: true, create: true }); if (!is_db_initialized(user_database)) { setup_db(user_database, USER_DATABASE_SETUP); + + console.log("create user"); + create_user({name: "PM", gender: "x", address: "home", username: "P", password: "M" }); } } @@ -323,13 +360,15 @@ export function close_db() { } } -export function create_user(name: string, initials: string): boolean { +export async function create_user(user: { name: string, gender: string, address: string, username: string, password: string }): Promise { + + user.password = await Bun.password.hash(user.password, { algorithm: "bcrypt", cost: 11}); try { const statement = user_database.query(USER_DATABASE_ADD_USER); - const result = statement.run({ name: name, initials: initials }); + const result = statement.run(user); - return true; + return result.changes == 1; } catch (e) { console.log(e); if (e instanceof SQLiteError) { @@ -344,7 +383,7 @@ function _get_user(): UserEntry | null { try { const statement = user_database.prepare(USER_DATABASE_GET_USER); - const result: UserEntry = statement.get(); + const result = statement.get() as UserEntry; return result; @@ -358,7 +397,7 @@ function _get_user(): UserEntry | null { } -export function get_user(): User | null { +export function get_user(id: number): User | null { const user = _get_user(); if (user == null) { @@ -385,3 +424,31 @@ export function get_user(): User | null { } } + +export function get_user_by_name(username: string): User | null { + + try { + const query = user_database.prepare(USER_DATABASE_GET_USER_BY_NAME); + const user = query.get({ username: username }) as UserEntry | null ; + + if (!user) { + return null; + } + + fs.mkdir(DATABASES_PATH, { recursive: true }); + + let userdb = new Database(get_user_db_name(user), { create: true, strict: true }); + + if (!is_db_initialized(userdb)) { + setup_db(userdb, ENTRY_DATABASE_SETUP); + } + + return new User(user, userdb); + } catch (exception) { + if (!(exception instanceof SQLiteError)) { + throw exception; + } + } + + return null; +} diff --git a/src/lib/server/session_store.ts b/src/lib/server/session_store.ts new file mode 100644 index 0000000..ed6504b --- /dev/null +++ b/src/lib/server/session_store.ts @@ -0,0 +1,127 @@ +import type { User } from "$lib/server/database"; + +import Crypto from "node:crypto"; + +interface UserSession { + user: User; + last_active: Date; +} + +export interface TokenInfo { + user_id: number; + creation_time: Date; + expiry_time: Date; +} + +class LoginError extends Error { + constructor(message: string) { + super(message); + } +} + +class UserLoginError extends LoginError { + user_id: number; + + constructor(user_id: number, message: string) { + super(message); + this.user_id = user_id; + } +} + + +const TOKEN_LENGTH = 256; + +const active_users = new Map(); +const active_session_tokens = new Map(); + + +function issue_access_token_for_user(user: User, expiry_date: Date): string { + + let user_session = active_users.get(user.id); + if (user_session === undefined) { + user_session = { + user: user, + last_active: new Date() + } as UserSession; + + active_users.set(user.id, user_session); + } + + /* | Token (256) | Expiry date as getTime | */ + const token_info: TokenInfo = { + user_id: user.id, + creation_time: new Date(), + expiry_time: expiry_date + } + const session_token = Buffer.concat( + [ Crypto.randomBytes(TOKEN_LENGTH / 4 * 3), + Buffer.from(token_info.creation_time.getTime().toFixed(0)) ] + ).toString("base64"); + + if (active_session_tokens.has(session_token)) { + throw new UserLoginError(user.id, "Tried issuing a duplicate session token."); + } + + active_session_tokens.set(session_token, token_info); + + return session_token; +} + +function get_user_by_access_token(token: string): User | null { + + const token_info = active_session_tokens.get(token); + + if (token_info === undefined || token_info.expiry_time.getTime() < Date.now()) { + return null; + } + + const user = active_users.get(token_info.user_id); + + if (user === undefined) { + return null; + } + + user.last_active = new Date(); + + return user.user; +} + +function logout_user_session(token: string): boolean { + const token_info = active_session_tokens.get(token); + + if (!token_info) { + console.log("this shouldn't happen."); + return false; + } + + token_info.expiry_time = new Date(0); + + return true; +} + +async function __clean_session_store() { + console.log("cleaning sessions"); + + const active_users_session = new Set(); + active_session_tokens.forEach((token_info, token, token_map) => { + if (token_info.expiry_time.getTime() < Date.now()) { + token_map.delete(token); + } else { + active_users_session.add(token_info.user_id); + } + }); + + active_users.forEach((user_info, user_id, user_map) => { + if (user_info.last_active.getTime() + 15*60*1000 < Date.now() && !active_users.has(user_id)) { + user_map.delete(user_id) + } + }); +} + +export default class SessionStore { + static issue_access_token_for_user = issue_access_token_for_user; + static get_user_by_access_token = get_user_by_access_token; + static logout_user_session = logout_user_session; +} + +setInterval(__clean_session_store, 15*60*1000); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 43d4083..6dee6d5 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -9,6 +9,14 @@
  • Stundenliste
  • Stundenschätzung
  • Dokumente
  • + +
  • +
  • Benutzerverwaltung
  • +
  • +
    + +
    +
  • {/snippet} @@ -50,12 +58,23 @@ list-style-type: none; padding: 0px; margin: 0px; + height: 100%; } .nav ul li { padding-bottom: 5px; } +.nav button { + padding: 0; + border: none; + background: none; + font-family: arial, sans-serif; + text-decoration-line: underline; + color: blue; + cursor: pointer; +} + .nav h1 { width: 100%; diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 8e9f330..c9e6a9c 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,52 +1,55 @@ import type { PageServerLoad, Actions } from "./$types"; -import type { SQLiteError } from "bun:sqlite"; +import { SQLiteError } from "bun:sqlite"; +import type { FileProperties } from "$lib/server/docstore" import type { RecordEntry, EstimatesEntry } from "$lib/db_types"; import { fail, redirect } from '@sveltejs/kit'; -import { MONTHS, toInt, parseDate, isTimeValidHHMM, month_of } from "$lib/util" -import { get_user, User } from "$lib/server/database"; +import { toInt, parseDate, isTimeValidHHMM } from "$lib/util" import { getRecordFiles } from "$lib/server/docstore"; export const load: PageServerLoad = async ({ locals }) => { - let records: { entry: RecordEntry, parsedDate: Date } = locals.user.get_entries().map((v) => ({ entry: v, parsedDate: parseDate(v.date) })); - let estimates: EstimatesEntry = locals.user.get_estimates() + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }); + } + + let records = locals.user.get_entries().map((v) => ({ entry: v, parsedDate: parseDate(v.date) ?? new Date(0) })); + let estimates = locals.user.get_estimates() let documents = (await getRecordFiles(locals.user)).map((v) => { return { year: toInt(v.identifier.substring(0, 4)), month: toInt(v.identifier.substring(5, 7)), file: v }; }); - let records_grouped = Map.groupBy(records, (v) => { return v.parsedDate.getFullYear() } ); - - records_grouped.forEach((value, key, map) => { - let m = Map.groupBy(value, (v) => v.parsedDate.getMonth()); - // remove parsed date - m.forEach((value, key, map) => { - map.set(key, value.map((v) => v.entry)); + let records_grouped: Map> = new Map() + Map.groupBy(records, (v) => { return v.parsedDate.getFullYear() } ).forEach((value, key, _) => { + let month_map: Map = new Map() + Map.groupBy(value, (v) => v.parsedDate.getMonth()).forEach((value, key, _) => { + // remove parsed date + month_map.set(key, value.map((v) => v.entry)); }); - map.set(key, m); + records_grouped.set(key, month_map); }); - let estimates_grouped = Map.groupBy(estimates, (v) => { return v.year }); - estimates_grouped.forEach((value, key, map) => { + let estimates_grouped: Map>= new Map(); + Map.groupBy(estimates, (v) => { return v.year }).forEach((value, key, _) => { let arr = value.map((v) => [ { key: ((v.quarter - 1) * 3 + 2), value: v["estimate_2"] }, { key: ((v.quarter - 1) * 3 + 1), value: v["estimate_1"] }, { key: ((v.quarter - 1) * 3 + 0), value: v["estimate_0"] }, ]).flat(); - let m = Map.groupBy(arr, (e) => e.key); - m.forEach((value, key, map) => { - map.set(key, value[0].value); + let m: Map = new Map(); + Map.groupBy(arr, (e) => e.key).forEach((value, key, _) => { + m.set(key, value[0].value); }) - map.set(key, m); + estimates_grouped.set(key, m); }) - let documents_grouped = Map.groupBy(documents, (v) => v.year ); - documents_grouped.forEach((value, key, map) => { - let m = Map.groupBy(value, (v) => v.month); - m.forEach((value, key, map) => { - map.set(key, value.map((v) => v.file)[0]); + let documents_grouped: Map> = new Map(); + Map.groupBy(documents, (v) => v.year ).forEach((value, key, _) => { + let m: Map = new Map() + Map.groupBy(value, (v) => v.month).forEach((value, key, _) => { + m.set(key, value.map((v) => v.file)[0]); }) - map.set(key, m); + documents_grouped.set(key, m); }) return { @@ -58,6 +61,9 @@ export const load: PageServerLoad = async ({ locals }) => { export const actions = { new_entry: async ({locals, request}) => { + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }) + } const ok = "ok"; const missing = "missing"; @@ -65,12 +71,12 @@ export const actions = { const data = await request.formData(); - let date = data.get("date"); - let start = data.get("start"); - let end = data.get("end"); - let comment = data.get("comment"); + let date = data.get("date") as string; + let start = data.get("start") as string; + let end = data.get("end") as string; + let comment = data.get("comment") as string; - let return_obj = { + let return_obj: Map = new Map(Object.entries({ date : { status: ok, value: date @@ -87,27 +93,27 @@ export const actions = { status: ok, value: comment }, - } + })) if (date == null) { - return_obj.date.status = missing; + return_obj.get("date")!.status = missing; } else if (parseDate(date) == null) { - return_obj.date.status = invalid; + return_obj.get("date")!.status = invalid; } if (start == null) { - return_obj.start.status = missing; + return_obj.get("start")!.status = missing; } else if (!isTimeValidHHMM(start)) { - return_obj.start.status = invalid; + return_obj.get("start")!.status = invalid; } if (end == null) { - return_obj.end.status = missing; + return_obj.get("end")!.status = missing; } else if (!isTimeValidHHMM(end)) { - return_obj.end.status = invalid; + return_obj.get("end")!.status = invalid; } - if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) { + if ([...return_obj.values()].some((v) => { return v.status != ok; })) { return fail(400, { new_entry: return_obj }); } @@ -117,9 +123,12 @@ export const actions = { return fail(500, { }) } - return { new_entry: { success: true } }; + return { success: true, new_entry: return_obj }; }, edit_entry: async ({locals, request}) => { + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }) + } const ok = "ok"; const missing = "missing"; @@ -127,13 +136,13 @@ export const actions = { const data = await request.formData(); - let id = data.get("id"); - let date = data.get("date"); - let start = data.get("start"); - let end = data.get("end"); - let comment = data.get("comment"); + let id = data.get("id") as string; + let date = data.get("date") as string; + let start = data.get("start") as string; + let end = data.get("end") as string; + let comment = data.get("comment") as string; - let return_obj = { + let return_obj : Map = new Map(Object.entries({ id: { status: ok, value: id, @@ -154,53 +163,52 @@ export const actions = { status: ok, value: comment }, - - } + })) if (id == null) { - return_obj.id.status = missing; + return_obj.get("id")!.status = missing; } else if (isNaN(toInt(id))) { - return_obj.id.status = invalid; + return_obj.get("id")!.status = invalid; } if (date == null) { - return_obj.date.status = missing; + return_obj.get("date")!.status = missing; } else if (parseDate(date) == null) { - return_obj.date.status = invalid; + return_obj.get("date")!.status = invalid; } if (start == null) { - return_obj.start.status = missing; + return_obj.get("start")!.status = missing; } else if (!isTimeValidHHMM(start)) { - return_obj.start.status = invalid; + return_obj.get("start")!.status = invalid; } if (end == null) { - return_obj.end.status = missing; + return_obj.get("end")!.status = missing; } else if (!isTimeValidHHMM(end)) { - return_obj.end.status = invalid; + return_obj.get("end")!.status = invalid; } - if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) { + if ([...return_obj.values()].some((v) => { return v.status != ok; })) { return fail(400, { edit_entry: return_obj }); } - id = toInt(id); + let user_id = toInt(id); - let current = locals.user.get_entry(id); + let current = locals.user.get_entry(user_id); if (!current) { - return fail(404, { new_entry: return_obj }); + return fail(404, { edit_entry: return_obj }); } - if (current.id == id && current.date == date && current.start == start && current.end == end && current.comment == comment) { - return { success: false, edit_entry: { return_obj } } + if (current.id == user_id && current.date == date && current.start == start && current.end == end && current.comment == comment) { + return { success: false, edit_entry: return_obj } } let res = false; try { - res = locals.user.update_entry(id, date, start, end, comment); + res = locals.user.update_entry(user_id, date, start, end, comment); } catch (e) { if (!(e instanceof SQLiteError)) { throw e; @@ -215,23 +223,27 @@ export const actions = { return { success: true }; }, remove_entry: async ({ locals, request })=> { + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }) + } + const data = await request.formData(); - let id = data.get("id"); + let id = data.get("id") as string; if (id == null) { return fail(400, { id: id }); } - id = toInt(id); - if (isNaN(id)) { + let user_id = toInt(id); + if (isNaN(user_id)) { return fail(400, { id: id }); } let res = false; try { - res = locals.user.remove_entry(id); + res = locals.user.remove_entry(user_id); } catch (e) { if (!(e instanceof SQLiteError)) { throw e; diff --git a/src/routes/dokumente/+page.server.ts b/src/routes/dokumente/+page.server.ts index 3041c22..08dc250 100644 --- a/src/routes/dokumente/+page.server.ts +++ b/src/routes/dokumente/+page.server.ts @@ -6,6 +6,11 @@ import { toInt } from "$lib/util" import { getAllFiles, getRecordFiles, getEstimateFiles, generateEstimatePDF, generateRecordPDF } from "$lib/server/docstore"; export const load: PageServerLoad = async ({ locals }) => { + + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }) + } + return { documents: await getAllFiles(locals.user), records: await getRecordFiles(locals.user), @@ -14,11 +19,15 @@ export const load: PageServerLoad = async ({ locals }) => { } export const actions = { - create_estimate: async ({ locals, request }) => { + create_estimate: async ({ locals, request }) => { + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }) + } + const data = await request.formData(); - const quarter = toInt(data.get("quarter") ?? ""); - const year = toInt(data.get("year") ?? ""); + const quarter = toInt(data.get("quarter") as string ?? ""); + const year = toInt(data.get("year") as string ?? ""); if (isNaN(year) || isNaN(quarter) || quarter < 1 || quarter > 4) { return fail(400, { success: false, message: "Invalid parameter", year: year, quarter: quarter }); @@ -26,7 +35,7 @@ export const actions = { try { await generateEstimatePDF(locals.user, year, quarter); - } catch (e) { + } catch (e: any) { console.log(e); return fail(403, { success: false, message: e.toString(), year: year, quarter: quarter }); } @@ -34,10 +43,14 @@ export const actions = { return { success: true }; }, create_record: async ({ locals, request }) => { + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }) + } + const data = await request.formData(); - const month = toInt(data.get("month") ?? ""); - const year = toInt(data.get("year") ?? ""); + const month = toInt(data.get("month") as string|null ?? ""); + const year = toInt(data.get("year") as string|null ?? ""); if (isNaN(year) || isNaN(month) || month < 1 || month > 12) { return fail(400, { success: false, message: "Invalid parameter", year: year, month: month }); @@ -45,14 +58,13 @@ export const actions = { try { await generateRecordPDF(locals.user, year, month); - } catch (e) { + } catch (e: any) { console.log(e); return fail(403, { success: false, message: e.toString(), year: year, month: month }); } redirect(303, "dokumente") - return { success: true }; } } satisfies Actions; diff --git a/src/routes/dokumente/[...file]/+server.ts b/src/routes/dokumente/[...file]/+server.ts index 29806ff..a16cc8c 100644 --- a/src/routes/dokumente/[...file]/+server.ts +++ b/src/routes/dokumente/[...file]/+server.ts @@ -1,13 +1,16 @@ import type { RequestHandler } from "./$types"; -import { redirect } from "@sveltejs/kit" +import { fail } from "@sveltejs/kit" import { getFile } from "$lib/server/docstore" -export const GET: RequestHandler = async ({ locals, url, params }) => { +export const GET: RequestHandler = async ({ locals, params }) => { + + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }) + } const file = await getFile(locals.user, params.file); - //redirect(307, "/dokumente") return new Response(file); } diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts new file mode 100644 index 0000000..00cbea3 --- /dev/null +++ b/src/routes/login/+page.server.ts @@ -0,0 +1,73 @@ +import type { Actions } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; +import { fail, redirect } from "@sveltejs/kit"; + +import { authorize_password } from "$lib/server/auth"; +import SessionStore from "$lib/server/session_store"; + +export const load: PageServerLoad = ({ locals, url }) => { + let redirect_url = url.searchParams.get("redirect") ?? "/"; + + if (locals.user != null) { + redirect(302, redirect_url); + } +} + + +export const actions = { + login: async ({ locals, request, cookies, url }) => { + + let redirect_url = url.searchParams.get("redirect") ?? "/"; + + if (locals.user != null) { + redirect(302, redirect_url); + } + + console.log("logging in"); + + const params = await request.formData(); + const username = params.get("username") as string | null; + const password = params.get("password") as string | null; + + if (username == null || password == null) { + return fail(400, { message: "Invalid request" }); + } + + const user = await authorize_password(username, password); + if (user == null) { + return fail(403, { message: "Benutzername oder Passwort falsch.", username: username }) + } + + const expiry_date = new Date(Date.now() + 15*60*1000) + + const token = SessionStore.issue_access_token_for_user(user, expiry_date) + + cookies.set("session_id", token, { + expires: expiry_date, + httpOnly: true, + secure: true, + sameSite: 'strict', + path: '/' + }) + + redirect(302, redirect_url); + }, + logout: async ({ locals, cookies }) => { + + if (locals.user == null) { + return fail(403, { message: "Not logged in." }); + } + + const token = cookies.get("session_id"); + if (!token) { + console.log("how is this user logged in right now?"); + return fail(500); + } + + SessionStore.logout_user_session(token); + + cookies.delete("session_id", { path: "/" }); + + return redirect(302, "/login"); + } +} satisfies Actions; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte new file mode 100644 index 0000000..ab3e7e7 --- /dev/null +++ b/src/routes/login/+page.svelte @@ -0,0 +1,48 @@ + + +
    +

    Login

    +
    + + + + + + + + + + + + + + +
    +
    +
    + + diff --git a/src/routes/schaetzung/+page.server.ts b/src/routes/schaetzung/+page.server.ts index 6341c27..7134d44 100644 --- a/src/routes/schaetzung/+page.server.ts +++ b/src/routes/schaetzung/+page.server.ts @@ -1,4 +1,5 @@ import type { PageServerLoad, Actions } from "./$types"; +import type { FileProperties } from "$lib/server/docstore" import { fail } from '@sveltejs/kit'; @@ -7,30 +8,34 @@ import { MONTHS, toInt, toFloat } from "$lib/util"; import { getEstimateFiles } from "$lib/server/docstore"; export const load: PageServerLoad = async ({ locals }) => { + + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }) + } let estimates = locals.user.get_estimates(); let documents = await getEstimateFiles(locals.user); - let estimates_grouped = Map.groupBy(estimates, (v) => v.year); - estimates_grouped.forEach((value, key, map) => { + let estimates_grouped: Map> = new Map(); + Map.groupBy(estimates, (v) => v.year).forEach((value, key, _) => { - let quarters = Map.groupBy(value, (v) => v.quarter) - quarters.forEach((_value, _key, _map) => { + let quarters = new Map() + Map.groupBy(value, (v) => v.quarter).forEach((_value, _key, _) => { let months = _value.map((v) => [ { month: MONTHS[(v.quarter - 1) * 3 + 2], estimate: v.estimate_2 }, { month: MONTHS[(v.quarter - 1) * 3 + 1], estimate: v.estimate_1 }, { month: MONTHS[(v.quarter - 1) * 3 + 0], estimate: v.estimate_0 }, ] ).flat(); - _map.set(_key, months); + quarters.set(_key, months); }) - map.set(key, quarters); + estimates_grouped.set(key, quarters); }) - let documents_grouped = Map.groupBy(documents, (v) => toInt(v.identifier.slice(0, 4))) - documents_grouped.forEach((value, key, map) => { + let documents_grouped: Map> = new Map() + Map.groupBy(documents, (v) => toInt(v.identifier.slice(0, 4))).forEach((value, key, _) => { let quarters = Map.groupBy(value, (v) => toInt(v.identifier.slice(5, 7))); - map.set(key, quarters); + documents_grouped.set(key, quarters); }) return { @@ -40,17 +45,18 @@ export const load: PageServerLoad = async ({ locals }) => { } export const actions = { - add_quarter: async ({ locals, request }) => { - + add_quarter: async ({ locals, request }) => { + if (!locals.user) { + return fail(403, { message: "Unauthorized user" }) + } + const data = await request.formData(); - const year = data.get("year"); - const quart = data.get("quarter"); - const estimate_0 = data.get("estimate_0"); - const estimate_1 = data.get("estimate_1"); - const estimate_2 = data.get("estimate_2"); - - console.log(data); + const year = data.get("year") as string; + const quart = data.get("quarter") as string; + const estimate_0 = data.get("estimate_0") as string; + const estimate_1 = data.get("estimate_1") as string; + const estimate_2 = data.get("estimate_2") as string; if (year == null || quart == null || estimate_0 == null || estimate_1 == null || estimate_2 == null) { return fail(400, { year: year, quarter: quart, estimate_0: estimate_0, estimate_1: estimate_1, estimate_2: estimate_2 }); diff --git a/svelte.config.js b/svelte.config.js index 5e55923..c3da896 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -12,7 +12,9 @@ const config = { // If your environment is not supported, or you settled on a specific environment, switch out the adapter. // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter() - } + }, + + warningFilter: (warning) => !warning.code.startsWith('a11y') }; export default config