Compare commits

..

No commits in common. "e2f011597d4efc3a4c4c1ce4084009bdcc1905bd" and "888e35f8390e70ab615e7b77db2d1a2f3dc1fc3a" have entirely different histories.

17 changed files with 398 additions and 680 deletions

View File

@ -1,7 +1,6 @@
import type { Handle } from "@sveltejs/kit";
import { error, redirect } from "@sveltejs/kit";
import { error } from "@sveltejs/kit";
import SessionStore from "$lib/server/session_store"
import { init_db, close_db, get_user } from "$lib/server/database";
async function init() {
@ -40,33 +39,11 @@ process.on('SIGINT', (_) => {
})
export let handle: Handle = async function ({ event, resolve }) {
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.");
}
let user = get_user();
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}`);
}
if (!user) {
return error(404, "No user"); // redirect login
}
event.locals.user = user;

View File

@ -1,21 +0,0 @@
import Bun from 'bun';
import { User, get_user_by_name } from "$lib/server/database";
export async function authorize_password(username: string, password: string): Promise<User | null> {
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;
}

View File

@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import { Database, SQLiteError } from "bun:sqlite";
import type { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types";
import { 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,54 +11,22 @@ 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,
name TEXT,
gender TEXT,
name TEXT,
address TEXT,
username TEXT,
password TEXT,
initials 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, gender, address, username, password) VALUES ($name, $gender, $address, $username, $password);";
"INSERT INTO users (name, initials) VALUES ($name, $initials);";
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;",
@ -175,19 +143,17 @@ export class User {
gender: string;
name: string;
address: string;
username: string;
password: string
initials: 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.username = user.username;
this.password = user.password;
this.initials = user.initials;
this.created = user.created;
this._database = db;
}
@ -342,14 +308,11 @@ 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" });
}
}
@ -360,15 +323,13 @@ export function close_db() {
}
}
export async function create_user(user: { name: string, gender: string, address: string, username: string, password: string }): Promise<boolean> {
user.password = await Bun.password.hash(user.password, { algorithm: "bcrypt", cost: 11});
export function create_user(name: string, initials: string): boolean {
try {
const statement = user_database.query(USER_DATABASE_ADD_USER);
const result = statement.run(user);
const result = statement.run({ name: name, initials: initials });
return result.changes == 1;
return true;
} catch (e) {
console.log(e);
if (e instanceof SQLiteError) {
@ -383,7 +344,7 @@ function _get_user(): UserEntry | null {
try {
const statement = user_database.prepare(USER_DATABASE_GET_USER);
const result = statement.get() as UserEntry;
const result: UserEntry = statement.get();
return result;
@ -397,7 +358,7 @@ function _get_user(): UserEntry | null {
}
export function get_user(id: number): User | null {
export function get_user(): User | null {
const user = _get_user();
if (user == null) {
@ -424,31 +385,3 @@ export function get_user(id: number): 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;
}

View File

@ -1,127 +0,0 @@
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<number, UserSession>();
const active_session_tokens = new Map<string, TokenInfo>();
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<number>();
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);

View File

@ -9,14 +9,6 @@
<li><a href="/">Stundenliste</a></li>
<li><a href="/schaetzung">Stundenschätzung</a></li>
<li><a href="/dokumente">Dokumente</a></li>
<li style="height: 20px"></li>
<li>Benutzerverwaltung</li>
<li>
<form method="POST" action="/login?/logout">
<button type="submit">Logout</button>
</form>
</li>
</ul>
</div>
{/snippet}
@ -58,23 +50,12 @@
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%;

View File

@ -1,55 +1,52 @@
import type { PageServerLoad, Actions } from "./$types";
import { SQLiteError } from "bun:sqlite";
import type { 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 { toInt, parseDate, isTimeValidHHMM } from "$lib/util"
import { MONTHS, toInt, parseDate, isTimeValidHHMM, month_of } from "$lib/util"
import { get_user, User } from "$lib/server/database";
import { getRecordFiles } from "$lib/server/docstore";
export const load: PageServerLoad = async ({ locals }) => {
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 records: { entry: RecordEntry, parsedDate: Date } = locals.user.get_entries().map((v) => ({ entry: v, parsedDate: parseDate(v.date) }));
let estimates: EstimatesEntry = 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<number, Map<number, RecordEntry[]>> = new Map()
Map.groupBy(records, (v) => { return v.parsedDate.getFullYear() } ).forEach((value, key, _) => {
let month_map: Map<number, RecordEntry[]> = new Map()
Map.groupBy(value, (v) => v.parsedDate.getMonth()).forEach((value, key, _) => {
// remove parsed date
month_map.set(key, value.map((v) => v.entry));
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));
});
records_grouped.set(key, month_map);
map.set(key, m);
});
let estimates_grouped: Map<number, Map<number, number>>= new Map();
Map.groupBy(estimates, (v) => { return v.year }).forEach((value, key, _) => {
let estimates_grouped = Map.groupBy(estimates, (v) => { return v.year });
estimates_grouped.forEach((value, key, map) => {
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<number, number> = new Map();
Map.groupBy(arr, (e) => e.key).forEach((value, key, _) => {
m.set(key, value[0].value);
let m = Map.groupBy(arr, (e) => e.key);
m.forEach((value, key, map) => {
map.set(key, value[0].value);
})
estimates_grouped.set(key, m);
map.set(key, m);
})
let documents_grouped: Map<number, Map<number, FileProperties>> = new Map();
Map.groupBy(documents, (v) => v.year ).forEach((value, key, _) => {
let m: Map<number, FileProperties> = new Map()
Map.groupBy(value, (v) => v.month).forEach((value, key, _) => {
m.set(key, value.map((v) => v.file)[0]);
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]);
})
documents_grouped.set(key, m);
map.set(key, m);
})
return {
@ -61,9 +58,6 @@ 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";
@ -71,12 +65,12 @@ export const actions = {
const data = await request.formData();
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 date = data.get("date");
let start = data.get("start");
let end = data.get("end");
let comment = data.get("comment");
let return_obj: Map<string, { status: string, value: string }> = new Map(Object.entries({
let return_obj = {
date : {
status: ok,
value: date
@ -93,27 +87,27 @@ export const actions = {
status: ok,
value: comment
},
}))
}
if (date == null) {
return_obj.get("date")!.status = missing;
return_obj.date.status = missing;
} else if (parseDate(date) == null) {
return_obj.get("date")!.status = invalid;
return_obj.date.status = invalid;
}
if (start == null) {
return_obj.get("start")!.status = missing;
return_obj.start.status = missing;
} else if (!isTimeValidHHMM(start)) {
return_obj.get("start")!.status = invalid;
return_obj.start.status = invalid;
}
if (end == null) {
return_obj.get("end")!.status = missing;
return_obj.end.status = missing;
} else if (!isTimeValidHHMM(end)) {
return_obj.get("end")!.status = invalid;
return_obj.end.status = invalid;
}
if ([...return_obj.values()].some((v) => { return v.status != ok; })) {
if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) {
return fail(400, { new_entry: return_obj });
}
@ -123,12 +117,9 @@ export const actions = {
return fail(500, { })
}
return { success: true, new_entry: return_obj };
return { new_entry: { success: true } };
},
edit_entry: async ({locals, request}) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
const ok = "ok";
const missing = "missing";
@ -136,13 +127,13 @@ export const actions = {
const data = await request.formData();
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 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 return_obj : Map<string, { status: string, value: string }> = new Map(Object.entries({
let return_obj = {
id: {
status: ok,
value: id,
@ -163,52 +154,53 @@ export const actions = {
status: ok,
value: comment
},
}))
}
if (id == null) {
return_obj.get("id")!.status = missing;
return_obj.id.status = missing;
} else if (isNaN(toInt(id))) {
return_obj.get("id")!.status = invalid;
return_obj.id.status = invalid;
}
if (date == null) {
return_obj.get("date")!.status = missing;
return_obj.date.status = missing;
} else if (parseDate(date) == null) {
return_obj.get("date")!.status = invalid;
return_obj.date.status = invalid;
}
if (start == null) {
return_obj.get("start")!.status = missing;
return_obj.start.status = missing;
} else if (!isTimeValidHHMM(start)) {
return_obj.get("start")!.status = invalid;
return_obj.start.status = invalid;
}
if (end == null) {
return_obj.get("end")!.status = missing;
return_obj.end.status = missing;
} else if (!isTimeValidHHMM(end)) {
return_obj.get("end")!.status = invalid;
return_obj.end.status = invalid;
}
if ([...return_obj.values()].some((v) => { return v.status != ok; })) {
if (Object.keys(return_obj).some((v) => { return return_obj[v].status != ok; })) {
return fail(400, { edit_entry: return_obj });
}
let user_id = toInt(id);
id = toInt(id);
let current = locals.user.get_entry(user_id);
let current = locals.user.get_entry(id);
if (!current) {
return fail(404, { edit_entry: return_obj });
return fail(404, { new_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 }
if (current.id == 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(user_id, date, start, end, comment);
res = locals.user.update_entry(id, date, start, end, comment);
} catch (e) {
if (!(e instanceof SQLiteError)) {
throw e;
@ -223,27 +215,23 @@ 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") as string;
let id = data.get("id");
if (id == null) {
return fail(400, { id: id });
}
let user_id = toInt(id);
if (isNaN(user_id)) {
id = toInt(id);
if (isNaN(id)) {
return fail(400, { id: id });
}
let res = false;
try {
res = locals.user.remove_entry(user_id);
res = locals.user.remove_entry(id);
} catch (e) {
if (!(e instanceof SQLiteError)) {
throw e;

View File

@ -14,6 +14,9 @@
let { data, form } : PageProps = $props();
//$inspect(data);
const HEADERS: string[] = [ "Datum", "Wochentag", "Beginn", "Ende", "Dauer", "Anmerkung" ];
const status_ok = "ok";
const status_missing = "missing";
@ -39,43 +42,22 @@
})
function setNewState() {
if (form?.success != null) {
new_state = {
date: {
valid: true,
value: ""
},
start: {
valid: true,
value: ""
},
end: {
valid: true,
value: ""
},
comment: {
value: ""
}
}
return
} else {
new_state = {
date: {
valid: form?.new_entry?.date?.value !== "" || form?.new_entry?.date?.valid,
value: form?.new_entry?.date?.value ?? "",
},
start: {
valid: form?.new_entry?.start?.value !== "" || form?.new_entry?.start?.valid,
value: form?.new_entry?.start?.value ?? "",
},
end: {
valid: form?.new_entry?.end?.value !== "" || form?.new_entry?.end?.valid,
value: form?.new_entry?.end?.value ?? "",
},
comment: {
value: form?.new_entry?.date?.comment ?? "",
},
}
new_state = {
date: {
valid: form?.new_entry?.date?.value !== "" || form?.new_entry?.date?.valid,
value: form?.new_entry?.date?.value ?? "",
},
start: {
valid: form?.new_entry?.start?.value !== "" || form?.new_entry?.start?.valid,
value: form?.new_entry?.start?.value ?? "",
},
end: {
valid: form?.new_entry?.end?.value !== "" || form?.new_entry?.end?.valid,
value: form?.new_entry?.end?.value ?? "",
},
comment: {
value: form?.new_entry?.date?.comment ?? "",
},
}
}

View File

@ -6,11 +6,6 @@ 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),
@ -19,15 +14,11 @@ export const load: PageServerLoad = async ({ locals }) => {
}
export const actions = {
create_estimate: async ({ locals, request }) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
create_estimate: async ({ locals, request }) => {
const data = await request.formData();
const quarter = toInt(data.get("quarter") as string ?? "");
const year = toInt(data.get("year") as string ?? "");
const quarter = toInt(data.get("quarter") ?? "");
const year = toInt(data.get("year") ?? "");
if (isNaN(year) || isNaN(quarter) || quarter < 1 || quarter > 4) {
return fail(400, { success: false, message: "Invalid parameter", year: year, quarter: quarter });
@ -35,7 +26,7 @@ export const actions = {
try {
await generateEstimatePDF(locals.user, year, quarter);
} catch (e: any) {
} catch (e) {
console.log(e);
return fail(403, { success: false, message: e.toString(), year: year, quarter: quarter });
}
@ -43,14 +34,10 @@ 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") as string|null ?? "");
const year = toInt(data.get("year") as string|null ?? "");
const month = toInt(data.get("month") ?? "");
const year = toInt(data.get("year") ?? "");
if (isNaN(year) || isNaN(month) || month < 1 || month > 12) {
return fail(400, { success: false, message: "Invalid parameter", year: year, month: month });
@ -58,13 +45,14 @@ export const actions = {
try {
await generateRecordPDF(locals.user, year, month);
} catch (e: any) {
} catch (e) {
console.log(e);
return fail(403, { success: false, message: e.toString(), year: year, month: month });
}
redirect(303, "dokumente")
return { success: true };
}
} satisfies Actions;

View File

@ -1,21 +1,24 @@
<script lang="ts">
import type { PageProps } from "./$types";
import type { FileProperties } from "$lib/server/docstore";
import { enhance } from "$app/forms";
import { isoToLocalDate, padInt } from "$lib/util";
import Expander from "./expander.svelte";
let { data } : PageProps = $props();
let { data, form } : PageProps = $props();
$inspect(data);
</script>
<div>
<h1>Dokumente</h1>
<form id="form_download" method="GET" ></form>
<form id="form_download" method="GET" >
{#snippet table(first_cell_name: string, rows: FileProperties[])}
{#snippet table(first_cell_name: string, rows)}
<table>
<thead>
<tr>
@ -29,7 +32,7 @@
{#each rows as file}
<tr>
<td>{file.identifier}</td>
<td>{file.name}</td>
<td>{file.filename}</td>
<td>{isoToLocalDate(file.cdate.toISOString())}<br/>um {padInt(file.cdate.getHours(), 2)}:{padInt(file.cdate.getMinutes(), 2)}:{padInt(file.cdate.getSeconds(), 2)}</td>
<td>
<form method="GET" action={`dokumente/${file.path}`} target="_blank">
@ -83,6 +86,78 @@
</Expander>
</div>
<div style:height="100px"></div>
<!--<form method="POST" id="create_est" action="?/create_estimate" use:enhance></form>
<form method="POST" id="create_rec" action="?/create_record" use:enhance></form>
<table>
<caption>Dokument erstellen</caption>
<tbody>
<tr>
<td>Stundenliste</td>
<td>
<input form="create_rec" style:width="2ch" name="month" /> -
<input form="create_rec" style:width="4ch" name="year" placeholder="Jahr" value={new Date().getFullYear()} />
</td>
<td>
<button form="create_rec" type="submit">Erstellen</button>
</td>
</tr>
<tr>
<td>Stundenschätzung</td>
<td>
<input form="create_est" style:width="1ch" name="quarter" />. Quartal
<input form="create_est" style:width="4ch" name="year" placeholder="Jahr" value={new Date().getFullYear()} />
</td>
<td>
<button form="create_est" type="submit">Erstellen</button>
</td>
</tr>
</tbody>
</table>
<div style:width="100%" style="text-align: center;">{!(form?.success) ? form?.message : "Dokument erstellt."}</div>
<div style:height="20px"></div>
<table>
<caption>Erstellte Dokumente</caption>
<thead>
<tr>
<th style:width="30ch">Dateiname</th>
<th style:width="15ch">Erstellt</th>
<th style:width="12ch">Aktion</th>
</tr>
</thead>
<tbody>
{#each data.documents as doc}
<tr>
<td>{doc.name}</td>
<td>{`${isoToLocalDate(doc.timestamp.toISOString())} um ${padInt(doc.timestamp.getHours(), 2)}:${padInt(doc.timestamp.getMinutes(), 2)}:${padInt(doc.timestamp.getSeconds(), 2)}`}</td>
<td>
<form method="GET" action={`dokumente/${doc.path}`}>
<button type="submit">Download</button>
</form>
</td>
</tr>
{/each}
<tr></tr>
</tbody>
{#if data.documents.length === 0}
<tfoot>
<tr>
<td class="td-no-elements" colspan="999">Noch keine Dokumente</td>
</tr>
</tfoot>
{/if}
</table> -->
</div>
<style>
@ -112,6 +187,10 @@ tbody tr {
border-bottom: 1px solid black;
}
tbody tr th {
text-align: center;
}
tbody tr td {
vertical-align: middle;
text-align: center;

View File

@ -1,16 +1,13 @@
import type { RequestHandler } from "./$types";
import { fail } from "@sveltejs/kit"
import { redirect } from "@sveltejs/kit"
import { getFile } from "$lib/server/docstore"
export const GET: RequestHandler = async ({ locals, params }) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
export const GET: RequestHandler = async ({ locals, url, params }) => {
const file = await getFile(locals.user, params.file);
//redirect(307, "/dokumente")
return new Response(file);
}

View File

@ -2,7 +2,7 @@
import type { Snippet } from "svelte";
let { id, label, children } : { id: string, label: string, children: Snippet } = $props();
let { id, label, children } : { label: string, children: Snippet } = $props();
let _id = id + "_check";
</script>

View File

@ -1,73 +0,0 @@
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;

View File

@ -1,48 +0,0 @@
<script lang="ts">
import type { PageProps } from "./$types";
let { form }: PageProps = $props();
</script>
<div>
<h1>Login</h1>
<form id="login" method="POST" action="?/login">
<table>
<tbody>
<tr>
<td><label for="username">Benutzername:</label></td>
<td><input type="text" id="username" name="username" value={form?.username ?? ""} required /></td>
</tr>
<tr>
<td><label for="password">Passwort:</label></td>
<td><input type="password" id="password" name="password" required /></td>
</tr>
<tr>
<td colspan="2"><button type="submit">Login</button></td>
</tr>
</tbody>
</table>
</form>
</div>
<style>
div {
position: absolute;
top: 33%;
left: 50%;
transform: translate(-50%, -50%);
}
table {
margin: auto;
}
tr td:first-of-type {
padding-right: 10px;
}
button {
margin: 0 auto;
padding: 0 auto;
}
</style>

View File

@ -497,127 +497,135 @@
</script>
<td>
<!-- svelte-ignore a11y_autofocus -->
<input
bind:this={dateInput}
bind:value={states.date.value}
class:form-invalid={!states.date.valid}
name="date"
type="text"
form={targetForm}
onfocusin={
(_) => {
dateInput.select();
states.date.valid = true;
<!--<tr>
{#each { length: prepend_td }, n}
<td></td>
{/each}-->
<td>
<input
bind:this={dateInput}
bind:value={states.date.value}
class:form-invalid={!states.date.valid}
name="date"
type="text"
form={targetForm}
onfocusin={
(_) => {
dateInput.select();
states.date.valid = true;
}
}
}
onfocusout={
(_) => {
states.date.valid = validateDate(dateInput);
states.date.value = dateInput.value;
}
}
onkeydown={
(event) => {
if (event.key == "Enter") {
onfocusout={
(_) => {
states.date.valid = validateDate(dateInput);
states.date.value = dateInput.value;
}
}
}
disabled={!enabled}
{autofocus}
required>
</td>
<td>
{inWeekDay}
</td>
<td>
<input
bind:this={startInput}
bind:value={states.start.value}
class:form-invalid={!states.start.valid}
name="start"
type="text"
form={targetForm}
onfocusin={
(_) => {
startInput.select();
states.start.valid = true;
onkeydown={
(event) => {
if (event.key == "Enter") {
states.date.valid = validateDate(dateInput);
states.date.value = dateInput.value;
}
}
}
}
onfocusout={
(_) => {
states.start.valid = validateTime(startInput);
states.start.value = startInput.value;
disabled={!enabled}
{autofocus}
required>
</td>
<td>
{inWeekDay}
</td>
<td>
<input
bind:this={startInput}
bind:value={states.start.value}
class:form-invalid={!states.start.valid}
name="start"
type="text"
form={targetForm}
onfocusin={
(_) => {
startInput.select();
states.start.valid = true;
}
}
}
onkeydown={
(event) => {
if (event.key == "Enter") {
onfocusout={
(_) => {
states.start.valid = validateTime(startInput);
states.start.value = startInput.value;
}
}
}
disabled={!enabled}
required>
</td>
onkeydown={
(event) => {
if (event.key == "Enter") {
states.start.valid = validateTime(startInput);
states.start.value = startInput.value;
}
}
}
disabled={!enabled}
required>
</td>
<td>
<input
bind:this={endInput}
bind:value={states.end.value}
class:form-invalid={!states.end.valid}
name="end"
type="text"
form={targetForm}
onfocusin={
(_) => {
endInput.select();
states.end.valid = true;
<td>
<input
bind:this={endInput}
bind:value={states.end.value}
class:form-invalid={!states.end.valid}
name="end"
type="text"
form={targetForm}
onfocusin={
(_) => {
endInput.select();
states.end.valid = true;
}
}
}
onfocusout={
(_) => {
states.end.valid = validateTime(endInput);
states.end.value = endInput.value;
}
}
onkeydown={
(event) => {
if (event.key == "Enter") {
onfocusout={
(_) => {
states.end.valid = validateTime(endInput);
states.end.value = endInput.value;
}
}
}
disabled={!enabled}
required>
</td>
onkeydown={
(event) => {
if (event.key == "Enter") {
states.end.valid = validateTime(endInput);
states.end.value = endInput.value;
}
}
}
disabled={!enabled}
required>
</td>
<td>
{inDuration}
</td>
<td>
{inDuration}
</td>
<td>
<input
name="comment"
type="text"
form={targetForm}
value={states.comment.value}
disabled={!enabled}>
</td>
<td>
<input
name="comment"
type="text"
form={targetForm}
value={states.comment.value}
disabled={!enabled}>
</td>
<td class="action">
{@render children?.()}
</td>
<td class="action">
{@render children?.()}
</td>
<!--</tr>-->
<style>
/* * {
border: 1px solid;
}*/
td input {
box-sizing: border-box;

View File

@ -1,5 +1,4 @@
import type { PageServerLoad, Actions } from "./$types";
import type { FileProperties } from "$lib/server/docstore"
import { fail } from '@sveltejs/kit';
@ -8,34 +7,30 @@ 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<number, Map<number, {month: string, estimate: number}[]>> = new Map();
Map.groupBy(estimates, (v) => v.year).forEach((value, key, _) => {
let estimates_grouped = Map.groupBy(estimates, (v) => v.year);
estimates_grouped.forEach((value, key, map) => {
let quarters = new Map()
Map.groupBy(value, (v) => v.quarter).forEach((_value, _key, _) => {
let quarters = Map.groupBy(value, (v) => v.quarter)
quarters.forEach((_value, _key, _map) => {
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();
quarters.set(_key, months);
_map.set(_key, months);
})
estimates_grouped.set(key, quarters);
map.set(key, quarters);
})
let documents_grouped: Map<number, Map<number, FileProperties[]>> = new Map()
Map.groupBy(documents, (v) => toInt(v.identifier.slice(0, 4))).forEach((value, key, _) => {
let documents_grouped = Map.groupBy(documents, (v) => toInt(v.identifier.slice(0, 4)))
documents_grouped.forEach((value, key, map) => {
let quarters = Map.groupBy(value, (v) => toInt(v.identifier.slice(5, 7)));
documents_grouped.set(key, quarters);
map.set(key, quarters);
})
return {
@ -45,18 +40,17 @@ export const load: PageServerLoad = async ({ locals }) => {
}
export const actions = {
add_quarter: async ({ locals, request }) => {
if (!locals.user) {
return fail(403, { message: "Unauthorized user" })
}
add_quarter: async ({ locals, request }) => {
const data = await request.formData();
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;
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);
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 });

View File

@ -4,7 +4,9 @@
import { enhance } from "$app/forms";
import { MONTHS, toInt, toFloat, padInt } from "$lib/util";
let { data }: PageProps = $props();
let { form, data }: PageProps = $props();
$inspect(data);
let next = $state((() => {
if (data.estimates.size == 0) {
@ -23,6 +25,7 @@
estimate_2: { value: "" },
editing: { value: "" }
})
$inspect(estimate_store);
function validate_year(event: InputEvent) {
const input = event.target as HTMLInputElement;
@ -52,6 +55,20 @@
}
}
/*const TODAY = new Date();
const NEXT_QUART = (data?.estimates?.length > 0)
? new Date(data.estimates[0].year + (data.estimates[0].quarter === 4 ? 1 : 0), 4 * (data.estimates[0].quarter - (data.estimates[0].quarter === 4 ? -4 : 0)))
: TODAY;
const add_quart = quart_from_date(NEXT_QUART);
function quart_from_date(date: Date) {
return Math.floor((date.getMonth()+1) / 4) + 1;
}
function month_from_quart(quart: number, num: number) {
return new Date(TODAY.getFullYear(), (quart - 1) * 4 + num);
}*/
</script>
<form id="form_add_quart" method="POST" action="?/add_quarter" use:enhance></form>
@ -70,20 +87,20 @@
<tbody>
<tr>
<td rowspan="3">Neue Schätzung: <input form="form_add_quart" style:width="7ch" type="number" name="year" oninput={validate_year as any} value={next.year} tabindex="8"/> - <input form="form_add_quart" style:width="4ch" type="number" name="quarter" oninput={validate_quarter as any} value={new_quart} tabindex="9"/>. Quartal</td>
<td rowspan="3">Neue Schätzung: <input form="form_add_quart" style:width="7ch" type="number" name="year" oninput={validate_year} value={next.year} tabindex="8"/> - <input form="form_add_quart" style:width="4ch" type="number" name="quarter" oninput={validate_quarter} value={new_quart} tabindex="9"/>. Quartal</td>
<td>{MONTHS[(new_quart - 1) * 3 + 0]}</td>
<td><input form="form_add_quart" type="text" name="estimate_0" oninput={(e: any) => validate_estimate(e, estimate_store.estimate_0)} tabindex="10" /></td>
<td><input form="form_add_quart" type="text" name="estimate_0" oninput={(e) => validate_estimate(e, estimate_store.estimate_0)} tabindex="10" /></td>
<td rowspan="3"><button form="form_add_quart" type="submit" tabindex="13">Erstellen</button></td>
</tr>
<tr>
<td>{MONTHS[(new_quart - 1) * 3 + 1]}</td>
<td><input form="form_add_quart" type="text" name="estimate_1" oninput={(e: any) => validate_estimate(e, estimate_store.estimate_1)} tabindex="11" /></td>
<td><input form="form_add_quart" type="text" name="estimate_1" oninput={(e) => validate_estimate(e, estimate_store.estimate_1)} tabindex="11" /></td>
</tr>
<tr>
<td>{MONTHS[(new_quart - 1) * 3 + 2]}</td>
<td><input form="form_add_quart" type="text" name="estimate_2" oninput={(e: any) => validate_estimate(e, estimate_store.estimate_2)} tabindex="12" /></td>
<td><input form="form_add_quart" type="text" name="estimate_2" oninput={(e) => validate_estimate(e, estimate_store.estimate_2)} tabindex="12" /></td>
</tr>
</tbody>
@ -125,7 +142,7 @@
<td class="action" rowspan="3">
{#if data.documents?.get(year)?.get(quarter)?.[0] != null}
{@const document = data.documents!.get(year)!.get(quarter)![0]}
{@const document = data.documents.get(year).get(quarter)[0]}
<form method="GET" action={`/dokumente/${document.path}`}>
<button type="submit">Download PDF</button>
</form>
@ -150,15 +167,60 @@
{/each}
</tbody>
{#if data.estimates.size === 0}
<tfoot>
<tr>
<td class="td-no-elements" colspan="999">No records</td>
</tr>
</tfoot>
{/if}
{#if data.estimates === undefined || data.estimates.size === 0}
<tfoot>
<tr>
<td class="td-no-elements" colspan="999">No records</td>
</tr>
</tfoot>
{/if}
</table>
{"" /*<tr>
<td rowspan="3">{add_quart}. Quartal {NEXT_QUART.getFullYear()}</td>
<td>{month_of(month_from_quart(add_quart, 0))}</td>
<!-- svelte-ignore a11y_positive_tabindex -->
<td><input form="form_add_quart" type="text" tabindex="1" name="estimate_0" value={form?.data?.estimate_0}/></td>
<td rowspan="3">
<input form="form_add_quart" type="hidden" name="year" value={NEXT_QUART.getFullYear()}/>
<input form="form_add_quart" type="hidden" name="quarter" value={add_quart}/>
<button form="form_add_quart" type="submit">+</button>
</td>
</tr>
<tr>
<td>{month_of(month_from_quart(add_quart, 1))}</td>
<!-- svelte-ignore a11y_positive_tabindex -->
<td><input form="form_add_quart" type="text" tabindex="2" name="estimate_1" value={form?.data?.estimate_1}/></td>
</tr>
<tr>
<td>{month_of(month_from_quart(add_quart, 2))}</td>
<!-- svelte-ignore a11y_positive_tabindex -->
<td><input form="form_add_quart" type="text" tabindex="3" name="estimate_2" value={form?.data?.estimate_2}/></td>
</tr>*/}
{"" /*
{#if months.length > 0}
<tr>
<td rowspan={rowspan}>
{quarter.quarter}. Quartal {quarter.year}
</td>
<td>{month_of(month_from_quart(quarter.quarter, months[0].i))}</td>
<td>{months[0].estimate.toFixed(2)}</td>
<td rowspan={rowspan}>Edit</td>
</tr>
{#each months.slice(1) as month}
<tr>
<td>{month_of(month_from_quart(quarter.quarter, month.i))}</td>
<td>{month.estimate.toFixed(2)}</td>
</tr>
{/each}
{/if}*/}
<style>
form {

View File

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