From 45bcce0fb252ec87ed7c75beb5f3abd6518eac43 Mon Sep 17 00:00:00 2001 From: Chi Cong Tran Date: Wed, 1 Oct 2025 09:54:21 +0200 Subject: [PATCH 1/4] hide PIN during Anmeldung and within route guards --- src/routes/(token-based)/+layout.server.ts | 13 ++++++------- src/routes/anmeldung/+page.server.ts | 13 +++++++++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/routes/(token-based)/+layout.server.ts b/src/routes/(token-based)/+layout.server.ts index 543cbb2..42713ba 100644 --- a/src/routes/(token-based)/+layout.server.ts +++ b/src/routes/(token-based)/+layout.server.ts @@ -1,12 +1,9 @@ -import { - vorgangPINValidation, - vorgangExists -} from '$lib/server/vorgangService'; +import { vorgangPINValidation, vorgangExists } from '$lib/server/vorgangService'; import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './list/[vorgang]/$types'; import { ROUTE_NAMES } from '..'; -export const load: PageServerLoad = async ({ params, url, locals }) => { +export const load: PageServerLoad = async ({ params, cookies, locals }) => { if (locals.user) { return { user: locals.user @@ -14,10 +11,12 @@ export const load: PageServerLoad = async ({ params, url, locals }) => { } const vorgangToken = params.vorgang; - const vorgangPIN = url.searchParams.get('pin'); + const COOKIE_NAME = `token-${vorgangToken}`; + const vorgangPIN = cookies.get(COOKIE_NAME); const isVorgangValid = vorgangExists(vorgangToken); const isVorgangPINValid = vorgangPINValidation(vorgangToken, vorgangPIN); - if (!isVorgangValid || !isVorgangPINValid) throw redirect(303, ROUTE_NAMES.ANMELDUNG_VORGANG_PARAM(vorgangToken)); + if (!isVorgangValid || !isVorgangPINValid) + throw redirect(303, ROUTE_NAMES.ANMELDUNG_VORGANG_PARAM(vorgangToken)); }; diff --git a/src/routes/anmeldung/+page.server.ts b/src/routes/anmeldung/+page.server.ts index bfe8d21..a4d6007 100644 --- a/src/routes/anmeldung/+page.server.ts +++ b/src/routes/anmeldung/+page.server.ts @@ -1,3 +1,4 @@ +import { dev } from '$app/environment'; import { loginUser, logoutUser } from '$lib/server/authService'; import { redirect } from '@sveltejs/kit'; import { ROUTE_NAMES } from '../index.js'; @@ -5,13 +6,21 @@ import { ROUTE_NAMES } from '../index.js'; export const actions = { login: ({ request, cookies }) => loginUser({ request, cookies }), logout: (event) => logoutUser(event), - getVorgangByToken: async ({ request }) => { + getVorgangByToken: async ({ request, cookies }) => { const data = await request.formData(); const vorgangToken = data.get('vorgang-token'); const vorgangPIN = data.get('vorgang-pin'); if (!vorgangToken || !vorgangPIN) return; - throw redirect(303, ROUTE_NAMES.VORGANG(vorgangToken, vorgangPIN)); + const COOKIE_NAME = `token-${vorgangToken}` + cookies.set(COOKIE_NAME, vorgangPIN, { + path: '/', + httpOnly: true, + sameSite: 'strict', + secure: !dev + }); + + throw redirect(303, ROUTE_NAMES.VORGANG(vorgangToken)); } } as const; -- 2.43.0 From 4c4be8ba42132e95b5e0e44a83d538b42073b30d Mon Sep 17 00:00:00 2001 From: Chi Cong Tran Date: Wed, 1 Oct 2025 11:57:40 +0200 Subject: [PATCH 2/4] remove attachment of to URLs on list-view and routes --- src/routes/(angemeldet)/list/+page.svelte | 2 +- src/routes/index.ts | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/routes/(angemeldet)/list/+page.svelte b/src/routes/(angemeldet)/list/+page.svelte index d2be366..1a7f888 100644 --- a/src/routes/(angemeldet)/list/+page.svelte +++ b/src/routes/(angemeldet)/list/+page.svelte @@ -50,7 +50,7 @@ {#each vorgangList as vorgangItem}
  • diff --git a/src/routes/index.ts b/src/routes/index.ts index 176a558..27df54c 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -11,14 +11,8 @@ export const ROUTE_NAMES = { USERMGMT: '/user-management', // (token-based) - // `pin` param is optional - VORGANG: (vorgangToken: string, vorgangPIN: string) => - vorgangPIN ? `/list/${vorgangToken}?pin=${vorgangPIN}` : `/list/${vorgangToken}`, - - CRIME: (vorgangToken: string, tatort: string, vorgangPIN: string) => - vorgangPIN - ? `/view/${vorgangToken}/${tatort}?pin=${vorgangPIN}` - : `/view/${vorgangToken}/${tatort}`, + VORGANG: (vorgangToken: string) => `/list/${vorgangToken}`, + CRIME: (vorgangToken: string, tatort: string) => `/view/${vorgangToken}/${tatort}`, // Anmeldung: actions ANMELDUNG: '/anmeldung', -- 2.43.0 From 45b5a36d04d207eb721018da0de428af0f55662d Mon Sep 17 00:00:00 2001 From: Chi Cong Tran Date: Mon, 6 Oct 2025 09:40:30 +0200 Subject: [PATCH 3/4] add tests for PIN handling via cookies --- tests/views/Anmeldung.test.ts | 143 +++++++++++++++++++++++++++ tests/views/VorgangList.view.test.ts | 9 ++ 2 files changed, 152 insertions(+) create mode 100644 tests/views/Anmeldung.test.ts diff --git a/tests/views/Anmeldung.test.ts b/tests/views/Anmeldung.test.ts new file mode 100644 index 0000000..f357c10 --- /dev/null +++ b/tests/views/Anmeldung.test.ts @@ -0,0 +1,143 @@ +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 { baseData } from '../fixtures'; +import { ROUTE_NAMES } from '../../src/routes'; +import { dev } from '$app/environment'; +import { vorgangExists, vorgangPINValidation } from '$lib/server/vorgangService'; + +vi.mock('$lib/server/vorgangService', () => ({ + vorgangExists: vi.fn(), + vorgangPINValidation: vi.fn(), +})); + +describe('Vorgang Anzeige via Token', () => { + it('Setze Cookie nach erfolgreicher Eingabe', async () => { + // Mock formData + const vorgObj = baseData.vorgang; + + const formData = new FormData(); + formData.set('vorgang-token', vorgObj.vorgangToken); + formData.set('vorgang-pin', vorgObj.vorgangPIN); + + const mockRequest = { + formData: vi.fn().mockResolvedValue(formData) + }; + + const cookiesSet = vi.fn(); + + const event = { + request: mockRequest, + cookies: { + set: cookiesSet + } + }; + + let thrownRedirect; + try { + await actions.getVorgangByToken(event); + } catch (e) { + thrownRedirect = e; + } + + // Redirect bei erfolgreicher Eingabe + expect(thrownRedirect?.status).toBe(303); + expect(thrownRedirect?.location).toBe(ROUTE_NAMES.VORGANG(vorgObj.vorgangToken)); + + // Cookie wurde gesetzt + const COOKIE_NAME = `token-${vorgObj.vorgangToken}` + expect(cookiesSet).toHaveBeenCalledWith(COOKIE_NAME, vorgObj.vorgangPIN, { + path: '/', + httpOnly: true, + sameSite: 'strict', + secure: !dev + }); + }); + + 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(); + + // Cookie wird nicht gesetzt + expect(cookiesSet).not.toHaveBeenCalled(); + }); +}); + +describe('Teste Guard', () => { + it('Lese Cookie aus', async () => { + const vorgObj = baseData.vorgang; + + 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: { + get: cookiesGet + }, + locals: {}, + params: {vorgang: vorgObj.vorgangToken} + }; + vi.mocked(vorgangExists).mockReturnValueOnce(true); + vi.mocked(vorgangPINValidation).mockReturnValueOnce(true); + + await load(event); + + expect(cookiesGet).toHaveBeenCalledWith(COOKIE_NAME); + }); + + it('Kein Cookie gesetzt', async () => { + const vorgObj = baseData.vorgang; + + 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: { + get: cookiesGet + }, + locals: {}, + params: {vorgang: vorgObj.vorgangToken} + }; + vi.mocked(vorgangExists).mockReturnValueOnce(true); + vi.mocked(vorgangPINValidation).mockReturnValueOnce(false); + + let thrownRedirect; + try { + await load(event); + 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(cookiesGet).toHaveBeenCalledWith(COOKIE_NAME); + }); +}); diff --git a/tests/views/VorgangList.view.test.ts b/tests/views/VorgangList.view.test.ts index c03bdf3..8349c2f 100644 --- a/tests/views/VorgangList.view.test.ts +++ b/tests/views/VorgangList.view.test.ts @@ -33,4 +33,13 @@ describe('Teste Links auf Korrektheit', () => { expect(linkElement).toBeInTheDocument(); expect(linkElement).toHaveAttribute('href', expectedURL); }); + + it('Links enthalten keinen VorgangsPIN', () => { + const vorgListOneItem = baseData.vorgangList.slice(0, 1) + + render(VorgangListPage, { props: { data: { ...baseData, vorgangList: vorgListOneItem } } }); + const listItem = screen.getByTestId("test-list-item"); + const linkElement = within(listItem).getByRole('link'); + expect(linkElement.getAttribute('href')?.toLowerCase()).not.toContain('pin'); + }); }); -- 2.43.0 From 468427622fcebeac186437d7773273b3ff51e63e Mon Sep 17 00:00:00 2001 From: Chi Cong Tran Date: Thu, 9 Oct 2025 13:05:21 +0200 Subject: [PATCH 4/4] fix PR remarks --- src/routes/(token-based)/+layout.server.ts | 8 ++++---- tests/views/Anmeldung.test.ts | 5 +++-- vite.config.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/routes/(token-based)/+layout.server.ts b/src/routes/(token-based)/+layout.server.ts index 42713ba..ea7d7f9 100644 --- a/src/routes/(token-based)/+layout.server.ts +++ b/src/routes/(token-based)/+layout.server.ts @@ -1,18 +1,18 @@ import { vorgangPINValidation, vorgangExists } from '$lib/server/vorgangService'; import { redirect } from '@sveltejs/kit'; -import type { PageServerLoad } from './list/[vorgang]/$types'; +import type { LayoutServerLoad } from './$types'; import { ROUTE_NAMES } from '..'; -export const load: PageServerLoad = async ({ params, cookies, locals }) => { +export const load: LayoutServerLoad = async ({ params, cookies, locals }) => { if (locals.user) { return { user: locals.user }; } - const vorgangToken = params.vorgang; + const vorgangToken = params.vorgang || ''; const COOKIE_NAME = `token-${vorgangToken}`; - const vorgangPIN = cookies.get(COOKIE_NAME); + const vorgangPIN = cookies.get(COOKIE_NAME) || ''; const isVorgangValid = vorgangExists(vorgangToken); const isVorgangPINValid = vorgangPINValidation(vorgangToken, vorgangPIN); diff --git a/tests/views/Anmeldung.test.ts b/tests/views/Anmeldung.test.ts index f357c10..96cf9b3 100644 --- a/tests/views/Anmeldung.test.ts +++ b/tests/views/Anmeldung.test.ts @@ -6,6 +6,7 @@ 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'; vi.mock('$lib/server/vorgangService', () => ({ vorgangExists: vi.fn(), @@ -34,11 +35,11 @@ describe('Vorgang Anzeige via Token', () => { } }; - let thrownRedirect; + let thrownRedirect: Redirect | undefined; try { await actions.getVorgangByToken(event); } catch (e) { - thrownRedirect = e; + thrownRedirect = e as Redirect; } // Redirect bei erfolgreicher Eingabe diff --git a/vite.config.ts b/vite.config.ts index c506829..ac7ee3b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ resolve: { alias: { $lib: path.resolve('./src/lib'), - $root: path.resolve(__dirname, 'src') + $root: path.resolve(__dirname, './src') } }, test: { -- 2.43.0