refactor login page, change routes to token-based, add service classes

This commit is contained in:
2025-06-17 16:19:22 +02:00
parent 6d3e6ad047
commit 34f8fd5490
36 changed files with 405 additions and 305 deletions

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import Exclamation from '$lib/icons/Exclamation.svelte';
let { label = '', error = null, value, name, id, type = 'text'} = $props();
</script>
<div>
<label for={name} class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex"
>{#if error}
<span class="inline-block mr-1"><Exclamation /></span>
{/if}
{label}</span
></label
>
<div class="mt-2">
<div
class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600"
>
<input
class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 text-sm leading-6"
bind:value
{type}
{name}
{id}
/>
</div>
</div>
{#if error}
<p class="block text-sm leading-6 text-red-900 mt-2">{error}</p>
{/if}
</div>

View File

@@ -14,8 +14,8 @@
<script lang="ts">
import { page } from '$app/stores';
import Trash from '$lib/icons/Trash.svelte';
import Panel from '$lib/components/ui/Panel.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Panel from '$lib/components/Panel.svelte';
import Button from '$lib/components/Button.svelte';
import { clickOutside } from '$lib/helpers/clickOutside.js';
const { adminMode, prediction, predictionRemove } = $page.data;

View File

@@ -0,0 +1,37 @@
<script>
import Profile from "$lib/icons/Profile.svelte";
export let data;
</script>
<div class="flex-none">
<footer class="justify-end">
<div class="bg-gray-100">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="flex justify-between divide-x divide-gray-900/5 border-x border-gray-900/5">
<a
href="/list"
class="px-4 py-1 -ml-4 flex items-center justify-center gap-x-2.5 text-sm font-semibold leading-6 text-gray-500 hover:bg-gray-200 hover:text-gray-700"
>
&copy; 2023 Innovation Hub Niedersachen
</a>
<a
href="/"
class="px-4 py-1 flex items-center justify-center gap-x-2.5 text-sm font-semibold leading-6 text-gray-500 hover:bg-gray-200 hover:text-gray-700"
>
back
</a>
<a
href="/"
class="px-4 py-1 -mr-4 flex items-center justify-center gap-x-2.5 text-sm font-semibold leading-6 text-gray-500 hover:bg-gray-200 hover:text-gray-700"
>
<!--icon-->
<Profile />
{data?.user?.id}
</a>
</div>
</div>
</div>
</footer>
</div>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import Chevron from '$lib/icons/Chevron-right.svelte';
export let data;
</script>
<div class="flex flex-col">
<header class="flex-none relative isolate z-10 bg-white px-8">
<nav
class="mx-auto flex max-w-7xl items-center justify-between p-6 lg:px-8"
aria-label="Global"
>
<div class="flex w-48">
<a href="/" class="-m-1.5 p-1.5 w-10">
<span class="sr-only">Tatort Niedersachen</span>
<img class="h-8 w-auto" src="/Landeswappen_NI.svg" alt="Landeswappen Niedersachsen" />
</a>
</div>
<h1 class="text-3xl text-slate-400 font-bold">Tatort</h1>
<div class="lg:flex lg:justify-end w-48">
{#if data.user}
<form method="POST" action="/anmeldung?/logout">
<input type="hidden" />
<button type="submit" class="text-sm font-semibold leading-6 text-gray-900"
><span
><span class="align-middle inline-block">Abmelden</span><span
class="align-middle inline-block"><Chevron /></span
></span
></button
>
</form>
{/if}
</div>
</nav>
</header>
</div>

View File

@@ -1,22 +0,0 @@
import { client } from '$lib/minio';
/**
* Check if caseNumber is used
* @param {string} caseNumber
* @returns {Promise<boolean>}
*/
export default async function caseNumberOccupied(caseNumber) {
const prefix = `${caseNumber}`;
const promise = new Promise((resolve) => {
let stream = client.listObjectsV2('tatort', prefix, false, '');
stream.on('data', () => {
stream.destroy();
resolve(true);
});
stream.on('end', () => {
resolve(false);
});
});
return promise;
}

View File

@@ -1,7 +1,7 @@
import { client } from '$lib/minio';
export default async function caseNumberOccupied (caseNumber: string): Promise<boolean> {
const prefix = `${caseNumber}/config.json`;
const prefix = `${caseNumber}`;
const promise: Promise<boolean> = new Promise((resolve) => {
const stream = client.listObjectsV2('tatort', prefix, false, '');
stream.on('data', () => {

View File

@@ -0,0 +1,29 @@
import { dev } from '$app/environment';
import { fail, redirect, type Cookies, type RequestEvent } from '@sveltejs/kit';
import { authenticate } from '$lib/auth';
const COOKIE_NAME = 'session';
export const loginUser = async ({ request, cookies }: { request: Request; cookies: Cookies }) => {
const data = await request.formData();
const user = data.get('user');
const password = data.get('password');
const token = authenticate(user, password);
if (!token) return fail(400, { user, incorrect: true });
cookies.set(COOKIE_NAME, token, {
path: '/',
httpOnly: true,
sameSite: 'strict',
secure: !dev
});
return redirect(303, '/');
};
export const logoutUser = async (event: RequestEvent) => {
event.cookies.delete(COOKIE_NAME, { path: '/' });
event.locals.user = null;
return { success: true };
};

View File

@@ -0,0 +1,58 @@
import { client } from '$lib/minio';
const BUCKET = 'tatort';
export const getVorgang = ({ params }) => {
const prefix = params.vorgang ? `${params.vorgang}/` : '';
const stream = client.listObjectsV2('tatort', prefix, false, '');
const result = new ReadableStream({
start(controller) {
stream.on('data', (data) => {
if (prefix === '') {
if (data.prefix)
controller.enqueue(`${JSON.stringify({ ...data, name: data.prefix.slice(0, -1) })}\n`);
return;
}
const name = data.name.slice(prefix.length);
if (name === 'config.json') return;
// zugangscode datei
if (name === '__perm__') return;
controller.enqueue(`${JSON.stringify({ ...data, name, prefix })}\n`);
});
stream.on('end', () => {
controller.close();
});
},
cancel() {
stream.destroy();
}
});
return new Response(result, {
headers: {
'content-type': 'text/event-stream'
}
});
};
export const checkIfExactDirectoryExists = (dir: string): Promise<boolean> => {
return new Promise<boolean>((resolve, reject) => {
const prefix = dir.endsWith('/') ? dir : `${dir}/`;
const stream = client.listObjectsV2(BUCKET, prefix, false, '');
stream.on('data', (obj) => {
if (obj.prefix === undefined && obj.name.startsWith(prefix)) {
stream.destroy();
resolve(true);
}
});
stream.on('error', (err) => reject(err));
stream.on('end', () => resolve(false));
});
}

View File

@@ -0,0 +1,68 @@
import { fail, redirect } from '@sveltejs/kit';
import { client } from '$lib/minio';
import { checkIfExactDirectoryExists } from './s3ClientService';
/**
*
* @param request
* @returns
*/
export const getVorgangByCaseNumber = async ( request: Request) => {
const data = await request.formData();
const caseNumber = data.get('caseNumber');
const user_token = data.get('token');
if (!caseNumber) {
return fail(400, {
success: false,
caseNumber,
error: { message: 'Die Vorgangsnummer darf nicht leer sein.' }
});
}
if (typeof caseNumber === 'string' && !(await checkIfExactDirectoryExists(caseNumber))) {
return fail(400, {
success: false,
caseNumber,
error: { message: 'Die Vorgangsnummer existiert in dieser Anwendung nicht.' }
});
}
const token = await getTokenOrNull(caseNumber);
if (token && token != user_token) {
return fail(400, {
success: false,
caseNumber,
error: { message: 'Der Token ist falsch.' }
});
}
redirect(303, `/list/${caseNumber}`);
}
const getTokenOrNull = async (vorgang) => {
const code_name = '__perm__';
const obj_path = `${vorgang}/${code_name}`;
let resp = null;
let code_saved = '';
try {
resp = await client.getObject('tatort', obj_path);
code_saved = await new Response(resp).text();
} catch (error) {
if (error.name == 'S3Error') {
resp = null;
}
}
if (resp != null) {
return code_saved;
} else {
return null;
}
}

View File

@@ -1,5 +1,5 @@
import { redirect, type ServerLoadEvent } from '@sveltejs/kit';
import type { PageServerLoad } from './view/[vorgang]/[tatort]/$types';
import type { PageServerLoad } from '../anmeldung/$types';
export const load: PageServerLoad = (event: ServerLoadEvent) => {
if (!event.locals.user && event.url.pathname !== '/anmeldung') throw redirect(303, '/anmeldung');

View File

@@ -1,73 +1,18 @@
<script lang="ts">
import Chevron from '$lib/icons/Chevron-right.svelte';
import Profile from '$lib/icons/Profile.svelte';
import Footer from '$lib/components/Footer.svelte';
import Header from '$lib/components/Header.svelte';
export let data;
</script>
<div class="h-screen v-screen flex flex-col">
<div class="flex flex-col h-full">
<header class="flex-none relative isolate z-10 bg-white px-8">
<nav
class="mx-auto flex max-w-7xl items-center justify-between p-6 lg:px-8"
aria-label="Global"
>
<div class="flex w-48">
<a href="/" class="-m-1.5 p-1.5 w-10">
<span class="sr-only">Tatort Niedersachen</span>
<img class="h-8 w-auto" src="/Landeswappen_NI.svg" alt="Landeswappen Niedersachsen" />
</a>
</div>
<h1 class="text-3xl text-slate-400 font-bold">Tatort</h1>
<div class="lg:flex lg:justify-end w-48">
{#if data.user}
<form method="POST" action="/anmeldung?/logout">
<input type="hidden" />
<button type="submit" class="text-sm font-semibold leading-6 text-gray-900"
><span
><span class="align-middle inline-block">Abmelden</span><span
class="align-middle inline-block"><Chevron /></span
></span
></button
>
</form>
{/if}
</div>
</nav>
</header>
<Header {data}/>
<div class="h-full grow overflow-scroll">
<slot />
</div>
<div class="flex-none">
<footer class="justify-end">
<div class="bg-gray-100">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="flex justify-between divide-x divide-gray-900/5 border-x border-gray-900/5">
<a
href="/list"
class="px-4 py-1 -ml-4 flex items-center justify-center gap-x-2.5 text-sm font-semibold leading-6 text-gray-500 hover:bg-gray-200 hover:text-gray-700"
>
&copy; 2023 Innovation Hub Niedersachen
</a>
<a
href="/"
class="px-4 py-1 flex items-center justify-center gap-x-2.5 text-sm font-semibold leading-6 text-gray-500 hover:bg-gray-200 hover:text-gray-700"
>
back
</a>
<a
href="/"
class="px-4 py-1 -mr-4 flex items-center justify-center gap-x-2.5 text-sm font-semibold leading-6 text-gray-500 hover:bg-gray-200 hover:text-gray-700"
>
<!--icon-->
<Profile />
<Footer {data}/>
{data.user.id}
</a>
</div>
</div>
</div>
</footer>
</div>
</div>
</div>

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import Alert from '$lib/components/ui/Alert.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Modal from '$lib/components/ui/Modal/Modal.svelte';
import ModalTitle from '$lib/components/ui/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/ui/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/ui/Modal/ModalFooter.svelte';
import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Modal/Modal.svelte';
import ModalTitle from '$lib/components/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import Exclamation from '$lib/icons/Exclamation.svelte';
export let form;

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { deserialize } from '$app/forms';
import Alert from '$lib/components/ui/Alert.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Modal from '$lib/components/ui/Modal/Modal.svelte';
import ModalTitle from '$lib/components/ui/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/ui/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/ui/Modal/ModalFooter.svelte';
import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Modal/Modal.svelte';
import ModalTitle from '$lib/components/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import shortenFileSize from '$lib/helper/shortenFileSize.js';
import Exclamation from '$lib/icons/Exclamation.svelte';
import FileRect from '$lib/icons/File-rect.svelte';

View File

@@ -1,75 +0,0 @@
import caseNumberOccupied from '$lib/helper/caseNumberOccupied';
import { fail, redirect } from '@sveltejs/kit';
import { client } from '$lib/minio';
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ request }: {request: Request}) => {
const data = await request.formData();
const caseNumber = data.get('caseNumber');
const user_token = data.get('token');
if (!caseNumber) {
return fail(400, {
success: false,
caseNumber,
error: { caseNumber: 'Die Vorgangsnummer darf nicht leer sein.' }
});
}
if (typeof caseNumber === 'string' && !(await caseNumberOccupied(caseNumber))) {
return fail(400, {
success: false,
caseNumber,
error: { caseNumber: 'Die Vorgangsnummer existiert in dieser Anwendung nicht.' }
});
}
//
// Ab hier ist Vorgang vorhanden
//
// Jetzt prüfen, ob Code vorhanden ist und
// dem eingegebenen Code entspricht
const token = await get_code_or_null(caseNumber);
console.log(`xxx ${token}, ${user_token}`);
// token vorhanden, check ob gleich sind
if (token && token != user_token) {
console.log(`ooo token check`);
return fail(400, {
success: false,
caseNumber,
error: { token: 'Der Token ist falsch.' }
});
}
redirect(303, `/list/${caseNumber}`);
}
};
// returns `code` oder `null`
async function get_code_or_null(vorg) {
const code_name = '__perm__';
const obj_path = `${vorg}/${code_name}`;
let resp = null;
let code_saved = '';
try {
resp = await client.getObject('tatort', obj_path);
code_saved = await new Response(resp).text();
} catch (error) {
if (error.name == 'S3Error') {
resp = null;
}
}
if (resp != null) {
return code_saved;
} else {
return null;
}
}

View File

@@ -0,0 +1,10 @@
import { type ServerLoadEvent } from '@sveltejs/kit';
import type { PageServerLoad } from '../anmeldung/$types';
export const load: PageServerLoad = (event: ServerLoadEvent) => {
if (event.locals.user) {
return {
user: event.locals.user
};
}
};

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import Footer from '$lib/components/Footer.svelte';
import Header from '$lib/components/Header.svelte';
export let data;
</script>
<div class="h-screen v-screen flex flex-col">
<Header {data} />
<div class="mt-10 flex-grow items-center justify-center">
<slot />
</div>
<Footer {data} />
</div>

View File

@@ -5,12 +5,12 @@
import timeElapsed from '$lib/helper/timeElapsed';
import Alert from '$lib/components/ui/Alert.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Modal from '$lib/components/ui/Modal/Modal.svelte';
import ModalTitle from '$lib/components/ui/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/ui/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/ui/Modal/ModalFooter.svelte';
import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Modal/Modal.svelte';
import ModalTitle from '$lib/components/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import Cube from '$lib/icons/Cube.svelte';
import Edit from '$lib/icons/Edit.svelte';
import Trash from '$lib/icons/Trash.svelte';
@@ -92,7 +92,6 @@
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'false');
text_field.setAttribute('contenteditable', 'false');
text_field.textContent = item.name;
}
@@ -173,9 +172,9 @@
>
<div class=" flex gap-x-4">
<Cube />
<button on:click="{() => console.log('test')}">test</button>
<div class="min-w-0 flex-auto">
{#if data.user.admin}
{#if data?.user?.admin}
<span
id="label__{item.name}"
class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"

View File

@@ -0,0 +1,6 @@
import { getVorgangByCaseNumber } from '$lib/server/vorgangService';
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({request}: {request: Request}) => getVorgangByCaseNumber(request)
}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Button from '$lib/components/ui/Button.svelte';
import Button from '$lib/components/Button.svelte';
import Exclamation from '$lib/icons/Exclamation.svelte';
export let form;

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import Panel from '$lib/components/ui/Panel.svelte';
import Panel from '$lib/components/Panel.svelte';
import { onMount } from 'svelte';
import Button from '$lib/components/ui/Button.svelte';
import Button from '$lib/components/Button.svelte';
export let data;

View File

@@ -1,32 +1,9 @@
import { dev } from '$app/environment';
import { fail, redirect, type Cookies } from '@sveltejs/kit';
import { authenticate } from '$lib/auth';
import type { RequestEvent } from '../(angemeldet)/$types';
import { loginUser, logoutUser } from '$lib/server/authService';
import { getVorgangByCaseNumber } from '$lib/server/vorgangService.js';
const COOKIE_NAME = 'session';
/** @type {import('./$types').Actions} */
export const actions = {
login: async ({ request, cookies }: {request: Request, cookies: Cookies}) => {
const data = await request.formData();
const user = data.get('user');
const password = data.get('password');
const token = authenticate(user, password);
if (!token) return fail(400, { user, incorrect: true });
cookies.set(COOKIE_NAME, token, {
path: '/',
httpOnly: true,
sameSite: 'strict',
secure: !dev
});
throw redirect(303, '/');
},
logout: async (event: RequestEvent) => {
event.cookies.delete(COOKIE_NAME, {path: '/'});
event.locals.user = null;
return { success: true };
}
};
login: ({ request, cookies }) => loginUser({request, cookies}),
logout: (event) => logoutUser(event),
getVorgang: ({request}) => getVorgangByCaseNumber(request)
} as const;

View File

@@ -1,19 +1,16 @@
<script lang="ts">
import Button from '$lib/components/ui/Button.svelte';
import Modal from '$lib/components/ui/Modal/Modal.svelte';
import ModalContent from '$lib/components/ui/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/ui/Modal/ModalFooter.svelte';
import ModalTitle from '$lib/components/ui/Modal/ModalTitle.svelte';
import Exclamation from '$lib/icons/Exclamation.svelte';
import BaseInputField from '$lib/components/BaseInputField.svelte';
import Button from '$lib/components/Button.svelte';
import Modal from '$lib/components/Modal/Modal.svelte';
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import ModalTitle from '$lib/components/Modal/ModalTitle.svelte';
import ArrowRight from '$lib/icons/Arrow-right.svelte';
import Login from '$lib/icons/Login.svelte';
import type { ActionData } from './$types';
export let form: ActionData;
export let form;
let user = form?.user ?? '';
let open = false;
$: open = form?.success ?? false;
export let open = false;
</script>
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
@@ -21,48 +18,37 @@
<img class="mx-auto h-10 w-auto" src="/Landeswappen_NI.svg" alt="Landeswappen Niedersachsen" />
<h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
Anmeldung zum 3D Tatort
Willkommen beim 3D Tatort
</h2>
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form class="space-y-6" action="?/login" method="POST">
<div>
<label for="caseNumber" class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex"
>{#if form?.error?.caseNumber}
<span class="inline-block mr-1"><Exclamation /></span>
{/if} Vorgangs-Nr.</span
></label
>
<div class="mt-2">
<div
class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600"
>
<input
value={form?.caseNumber ?? ''}
type="text"
name="caseNumber"
<div class="w-full max-w-sm mx-auto">
<div class="relative mt-5 bg-gray-50 rounded-xl shadow-xl p-3 pt-1">
<div class="mt-10">
<form action="?/getVorgang" method="POST">
<BaseInputField
id="caseNumber"
class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 text-sm leading-6"
name="caseNumber"
label="Vorgangs-Nr."
type="text"
value={form?.caseNumber}
error={form?.error?.message}
/>
</div>
</div>
{#if form?.error?.caseNumber}
<p class="block text-sm leading-6 text-red-900 mt-2">{form.error.caseNumber}</p>
{/if}
<div class="flex justify-end pt-4">
<Button type="submit"><ArrowRight /></Button>
</div>
</form>
<div class="flex justify-end pt-4">
</div>
</div>
<div class="flex justify-end mt-10 px-3">
<Button on:click={() => (open = true)} ><Login /></Button>
</div>
</div>
</div>
<Modal {open}>
<ModalTitle>Anmelden</ModalTitle>
<ModalContent>
<div class="content-center">
<form action="">
<ModalContent class="flex justify-center">
<form action="?/login" method="POST">
<div>
<label for="user" class="text-sm font-medium leading-6 text-gray-900">Kennung</label>
<div class="mt-2">
@@ -72,7 +58,7 @@
type="text"
autocomplete="email"
required
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
class="rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
@@ -93,16 +79,14 @@
</div>
</div>
<div>
<button
<div class="flex justify-end">
<Button
type="submit"
class="mt-10 flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>Anmelden</button
class="mt-5"
>Anmelden</Button
>
</div>
</form>
</div>
</ModalContent>
<ModalFooter><Button on:click={() => (open = false)}>Ok</Button></ModalFooter>
</Modal>
</div>