13 Commits

Author SHA1 Message Date
a9e3d8264c Merge pull request 'f092_ViewAuth-von-User-vereinfachen' (#37) from f092_ViewAuth-von-User-vereinfachen into development
All checks were successful
InnoHub Processor/tatort/pipeline/head This commit looks good
Reviewed-on: #37
2025-10-30 13:04:08 +01:00
332a3e5c15 change description of test case: load() now returns undefined if not logged-in 2025-10-30 12:16:21 +01:00
4fc6da850b change invalid user login 2025-10-30 12:04:58 +01:00
36273fd426 fix tests for refactoring of viewer Vorgang-PIN-validation 2025-10-30 11:15:07 +01:00
793ddb17d6 magic strings for login and logout 2025-10-30 10:56:23 +01:00
349d2cea6a named actions for logging in and out 2025-10-30 10:38:11 +01:00
23f2feeefb remove ununsed import 2025-10-30 10:36:50 +01:00
48fe999b5b protect admin pages after refactoring 2025-10-30 10:35:45 +01:00
c857041e21 refactor viewer-login page with error messages and validation 2025-10-30 08:57:58 +01:00
e26b36121a refactor homepage for admin-user and login mask if not logged in 2025-10-29 12:34:38 +01:00
416118197b test Login angepasst, return fail wenn formaDaten leer 2025-10-17 12:12:07 +02:00
01afbea9a3 Merge branch 'development' into f092_ViewAuth-von-User-vereinfachen 2025-10-17 10:37:39 +02:00
69422d1f92 refactoring UUID Anzeige, noch keine Tests angepasst 2025-10-13 13:01:12 +02:00
14 changed files with 263 additions and 215 deletions

View File

@@ -21,7 +21,7 @@
<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="{ROUTE_NAMES.ANMELDUNG_LOGOUT}">
<form method="POST" action="{ROUTE_NAMES.LOGOUT}">
<input type="hidden" />
<button type="submit" class="text-sm font-semibold leading-6 text-gray-900"
><span

View File

@@ -12,7 +12,8 @@ export const loginUser = async ({ request, cookies }: { request: Request; cookie
const token = authenticate(user, password);
if (!token) return fail(400, { user, incorrect: true });
if (!token) return fail(400, { user, incorrect: true,
message: "Ungültige Zugangsdaten" });
cookies.set(COOKIE_NAME, token, {
path: ROUTE_NAMES.ROOT,
@@ -26,5 +27,5 @@ export const loginUser = async ({ request, cookies }: { request: Request; cookie
export const logoutUser = async (event: RequestEvent) => {
event.cookies.delete(COOKIE_NAME, { path: ROUTE_NAMES.ROOT });
event.locals.user = null;
return { success: true };
return redirect(303, ROUTE_NAMES.ROOT);
};

View File

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

View File

@@ -5,6 +5,8 @@
export let data;
</script>
{#if data.user?.admin}
<div class="h-screen v-screen flex flex-col">
<div class="flex flex-col h-full">
<Header {data}/>
@@ -16,3 +18,10 @@
</div>
</div>
{:else}
<div class="h-screen bg-white"><slot /></div>
{/if}

View File

@@ -0,0 +1,6 @@
import { loginUser, logoutUser } from '$lib/server/authService';
export const actions = {
login: ({ request, cookies }) => loginUser({ request, cookies }),
logout: (event) => logoutUser(event),
} as const;

View File

@@ -2,18 +2,21 @@
import AddProcess from '$lib/icons/Add-Process.svelte';
import FileRect from '$lib/icons/File-rect.svelte';
import ListIcon from '$lib/icons/List-icon.svelte';
import Button from '$lib/components/Button.svelte';
import ArrowRight from '$lib/icons/Arrow-right.svelte';
import { ROUTE_NAMES } from '../index.js';
export let data;
export let form;
export let outline = true;
</script>
{#if data.user?.admin}
<div
class=" inset-x-0 top-0 -z-10 h-full flex items-center justify-center bg-white shadow-lg ring-1 ring-gray-900/5"
>
<div class="mx-auto flex justify-center max-w-7xl py-10 px-8 w-full">
{#if data.user.admin}
<div class="group relative rounded-lg p-6 text-sm leading-6 hover:bg-gray-50 w-1/4">
<div
class="flex h-11 w-11 items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white"
@@ -28,8 +31,6 @@
Verschaffe Dir einen Überblick über alle gespeicherten Tatorte.
</p>
</div>
{/if}
{#if data.user.admin}
<div class="group relative rounded-lg p-6 text-sm leading-6 hover:bg-gray-50 w-1/4">
<div
class="flex h-11 w-11 items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white"
@@ -42,7 +43,6 @@
</a>
<p class="mt-1 text-gray-600">Fügen Sie einem Tatort Bilder hinzu.</p>
</div>
{/if}
<div class="group relative rounded-lg p-6 text-sm leading-6 hover:bg-gray-50 w-1/4">
<div
class="flex h-11 w-11 items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white"
@@ -58,5 +58,64 @@
</div>
</div>
{:else}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<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">
Willkommen beim 3D Tatort
</h2>
</div>
<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="{ROUTE_NAMES.LOGIN}" method="POST">
<div>
<label for="user" class="text-sm font-medium leading-6 text-gray-900">Name</label>
<div class="mt-2">
<input
id="user"
name="user"
type="text"
autocomplete="email"
required
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>
<div>
<label for="password" class="block text-sm font-medium leading-6 text-gray-900"
>Passwort</label
>
<div class="mt-2">
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
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"
/>
</div>
</div>
{#if form?.incorrect}
<p class="block text-sm leading-6 text-red-900 mt-2">{form.message}</p>
{/if}
<div class="flex justify-end">
<Button type="submit" class="mt-5">Anmelden</Button>
</div>
</form>
</div>
</div>
</div>
</div>
{/if}
<style>
</style>

View File

@@ -1,7 +1,12 @@
import { getVorgaenge } from '$lib/server/vorgangService';
import type { PageServerLoad } from '../../(token-based)/view/$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async (event) => {
if (!event.locals.user) {
error(404, 'Not Found')
}
export const load: PageServerLoad = async () => {
const vorgangList = getVorgaenge();
return {

View File

@@ -1,6 +1,6 @@
import { Readable } from 'stream';
import { BUCKET, client } from '$lib/minio';
import { fail } from '@sveltejs/kit';
import { fail, error } from '@sveltejs/kit';
import { v4 as uuidv4 } from 'uuid';
import { db } from '$lib/server/dbService';
@@ -123,3 +123,10 @@ export const actions = {
return { etag, error };
}
};
export const load: PageServerLoad = async (event) => {
if (!event.locals.user) {
error(404, 'Not found')
}
};

View File

@@ -0,0 +1,8 @@
import type { PageServerLoad } from '../../(token-based)/view/$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async (event) => {
if (!event.locals.user) {
error(404, 'Not Found')
}
};

View File

@@ -1,19 +1,23 @@
import { dev } from '$app/environment';
import { loginUser, logoutUser } from '$lib/server/authService';
import { redirect } from '@sveltejs/kit';
import { error, fail, redirect } from '@sveltejs/kit';
import { ROUTE_NAMES } from '../index.js';
import { vorgangPINValidation } from '$lib/server/vorgangService.js';
export const actions = {
login: ({ request, cookies }) => loginUser({ request, cookies }),
logout: (event) => logoutUser(event),
getVorgangByToken: async ({ request, cookies }) => {
default: async ({ request, cookies }) => {
const data = await request.formData();
const vorgangToken = data.get('vorgang-token');
const vorgangPIN = data.get('vorgang-pin');
const vorgangPIN = data.get('vorgang-pin') as string;
if (!vorgangToken || !vorgangPIN) return;
if (!vorgangPIN) {
return fail(400, { message: 'Bitte einen PIN eingeben.'});
}
const COOKIE_NAME = `token-${vorgangToken}`
if (!vorgangPINValidation(vorgangToken, vorgangPIN)) {
return fail(400, { message: 'Falsche Zugangsdaten.'});
}
const COOKIE_NAME = `token-${vorgangToken}`;
cookies.set(COOKIE_NAME, vorgangPIN, {
path: '/',
httpOnly: true,
@@ -24,3 +28,8 @@ export const actions = {
throw redirect(303, ROUTE_NAMES.VORGANG(vorgangToken));
}
} as const;
export const load: PageServerLoad = async ({ url }) => {
const vorgang = url.searchParams.get('vorgang');
if (!vorgang) error(404, "Not Found");
};

View File

@@ -1,22 +1,15 @@
<script lang="ts">
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';
export let form;
export let open = false;
import { page } from '$app/state';
import { ROUTE_NAMES } from '../index.js';
const vorgangToken = page.url.searchParams.get('vorgang');
</script>
{#if vorgangToken}
<div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<img class="mx-auto h-10 w-auto" src="/Landeswappen_NI.svg" alt="Landeswappen Niedersachsen" />
@@ -28,14 +21,9 @@
<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="{ROUTE_NAMES.ANMELDUNG_GET_VORGANG_BY_TOKEN}" method="POST">
<BaseInputField
id="vorgang-token"
name="vorgang-token"
label="Vorgangskennung"
type="text"
value={vorgangToken}
/>
<form method="POST">
<input type="hidden" name="vorgang-token" value={vorgangToken} />
<div class="mt-5">
<BaseInputField
id="vorgang-pin"
@@ -46,55 +34,17 @@
error={form?.error?.message}
/>
</div>
{#if form?.message}
<p class="block text-sm leading-6 text-red-900 mt-2">{form.message}</p>
{/if}
<div class="flex justify-end pt-4">
<Button type="submit"><ArrowRight /></Button>
</div>
</form>
</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 class="flex justify-center">
<form action="{ROUTE_NAMES.ANMELDUNG_LOGIN}" method="POST">
<div>
<label for="user" class="text-sm font-medium leading-6 text-gray-900">Kennung</label>
<div class="mt-2">
<input
id="user"
name="user"
type="text"
autocomplete="email"
required
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>
<div>
<label for="password" class="block text-sm font-medium leading-6 text-gray-900"
>Passwort</label
>
<div class="mt-2">
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
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"
/>
</div>
</div>
<div class="flex justify-end">
<Button type="submit" class="mt-5">Anmelden</Button>
</div>
</form>
</ModalContent>
<ModalFooter><Button on:click={() => (open = false)}>Ok</Button></ModalFooter>
</Modal>
</div>
{/if}

View File

@@ -16,8 +16,8 @@ export const ROUTE_NAMES = {
// Anmeldung: actions
ANMELDUNG: '/anmeldung',
ANMELDUNG_LOGIN: '/anmeldung?/login',
ANMELDUNG_LOGOUT: '/anmeldung?/logout',
LOGIN: '/?/login',
LOGOUT: '/?/logout',
ANMELDUNG_GET_VORGANG_BY_TOKEN: '/anmeldung?/getVorgangByToken',
ANMELDUNG_VORGANG_PARAM: (vorgangToken: string) => `/anmeldung?vorgang=${vorgangToken}`
};

View File

@@ -1,16 +1,18 @@
import { describe, it, expect, vi } from 'vitest';
import { actions } from '$root/routes/anmeldung/+page.server';
import { load } from '$root/routes/(token-based)/+layout.server'
// import { actions } from '$root/routes/anmeldung/+page.server';
// import { load } from '$root/routes/(token-based)/+layout.server'
import { actions } from '../../src/routes/anmeldung/+page.server';
import { load } from '../../src/routes/(token-based)/+layout.server';
import { baseData } from '../fixtures';
import { ROUTE_NAMES } from '../../src/routes';
import { dev } from '$app/environment';
import { vorgangExists, vorgangPINValidation } from '$lib/server/vorgangService';
import { Redirect } from '@sveltejs/kit';
import type { Redirect } from '@sveltejs/kit';
vi.mock('$lib/server/vorgangService', () => ({
vorgangExists: vi.fn(),
vorgangPINValidation: vi.fn(),
vorgangPINValidation: vi.fn()
}));
describe('Vorgang Anzeige via Token', () => {
@@ -25,6 +27,7 @@ describe('Vorgang Anzeige via Token', () => {
const mockRequest = {
formData: vi.fn().mockResolvedValue(formData)
};
vi.mocked(vorgangPINValidation).mockReturnValueOnce(true);
const cookiesSet = vi.fn();
@@ -37,7 +40,7 @@ describe('Vorgang Anzeige via Token', () => {
let thrownRedirect: Redirect | undefined;
try {
await actions.getVorgangByToken(event);
await actions.default(event);
} catch (e) {
thrownRedirect = e as Redirect;
}
@@ -47,7 +50,7 @@ describe('Vorgang Anzeige via Token', () => {
expect(thrownRedirect?.location).toBe(ROUTE_NAMES.VORGANG(vorgObj.vorgangToken));
// Cookie wurde gesetzt
const COOKIE_NAME = `token-${vorgObj.vorgangToken}`
const COOKIE_NAME = `token-${vorgObj.vorgangToken}`;
expect(cookiesSet).toHaveBeenCalledWith(COOKIE_NAME, vorgObj.vorgangPIN, {
path: '/',
httpOnly: true,
@@ -58,40 +61,35 @@ describe('Vorgang Anzeige via Token', () => {
it('Schlägt fehl wenn keine Daten übergeben werden', async () => {
const formData = new FormData(); // no data
const mockRequest = {
formData: vi.fn().mockResolvedValue(formData)
};
const cookiesSet = vi.fn();
const event = {
request: mockRequest,
cookies: {
set: cookiesSet
}
};
const result = await actions.getVorgangByToken(event);
expect(result).toBeUndefined();
const result = await actions.default(event);
expect(result.status).toBe(400);
expect(result.data.message).toMatch(/PIN eingeben/i);
// Cookie wird nicht gesetzt
expect(cookiesSet).not.toHaveBeenCalled();
});
it.todo('Überprüfe was passiert, wenn Eingabe falsch, bzw. nicht im System passend gefunden');
});
describe('Teste Guard', () => {
it('Lese Cookie aus', async () => {
const vorgObj = baseData.vorgang;
const COOKIE_NAME = `token-${vorgObj.vorgangToken}`
const COOKIE_NAME = `token-${vorgObj.vorgangToken}`;
const cookiesGet = vi.fn().mockImplementation((key: string) => {
if (key === COOKIE_NAME) return vorgObj.vorgangPIN;
return undefined;
});
// mocked objects
const event = {
cookies: {
@@ -111,13 +109,12 @@ describe('Teste Guard', () => {
it('Kein Cookie gesetzt', async () => {
const vorgObj = baseData.vorgang;
const COOKIE_NAME = `token-${vorgObj.vorgangToken}`
const COOKIE_NAME = `token-${vorgObj.vorgangToken}`;
const cookiesGet = vi.fn().mockImplementation((key: string) => {
if (key === COOKIE_NAME) return vorgObj.vorgangPIN;
return undefined;
});
// mocked objects
const event = {
cookies: {
@@ -132,12 +129,14 @@ describe('Teste Guard', () => {
let thrownRedirect;
try {
await load(event);
throw new Error('Function did not throw')
throw new Error('Function did not throw');
} catch (e) {
thrownRedirect = e;
}
expect(thrownRedirect?.status).toBe(303);
expect(thrownRedirect?.location).toBe(ROUTE_NAMES.ANMELDUNG_VORGANG_PARAM(vorgObj.vorgangToken));
expect(thrownRedirect?.location).toBe(
ROUTE_NAMES.ANMELDUNG_VORGANG_PARAM(vorgObj.vorgangToken)
);
expect(cookiesGet).toHaveBeenCalledWith(COOKIE_NAME);
});

View File

@@ -4,20 +4,15 @@ import { ROUTE_NAMES } from '../../src/routes';
import { baseData, mockEvent } from '../fixtures';
describe('+layout.server load(): Teste korrekte URL', () => {
test('Werfe redirect zu /anmeldung wenn User nicht eingeloggt', async () => {
test('Werfe keinen Redirect und gebe nichts zurück', async () => {
const mockEvent = {
locals: {
user: null
},
url: new URL(`https://example.com/not-anmeldung`)
};
try {
load(mockEvent);
throw new Error('Expected load() to throw');
} catch (err) {
expect(err.status).toBe(303);
expect(err.location).toBe(ROUTE_NAMES.ANMELDUNG);
}
const res = load(mockEvent);
expect(res).toBe(undefined);
});
});