4 Commits

12 changed files with 103 additions and 100 deletions

View File

@@ -1,5 +1,7 @@
# --- Build stage --- # --- Build stage ---
FROM node:22 AS build FROM node:22 AS build
ENV NODE_ENV=production
ENV ORIGIN=https://tatort.innovation-hub-niedersachsen.de
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm ci RUN npm ci
@@ -9,13 +11,7 @@ RUN npm run build
# --- Production stage --- # --- Production stage ---
FROM node:22-alpine3.20 FROM node:22-alpine3.20
WORKDIR /app COPY --from=build /app .
ENV NODE_ENV=production
ENV ORIGIN=https://tatort.innovation-hub-niedersachsen.de
COPY --from=build /app/build ./build
COPY --from=build /app/package*.json ./
COPY --from=build /app/config.json ./config.json
RUN npm ci --omit=dev
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
EXPOSE 3000 EXPOSE 3000
CMD ["node", "build/index.js"] CMD ["sh", "-c", "ORIGIN=https://tatort.innovation-hub-niedersachsen.de node build/index.js"]

View File

@@ -1,10 +1,10 @@
{ {
"minio": { "minio": {
"endPoint": "api-s3.innovation-hub-niedersachsen.de", "endPoint": "sws3.innovation-hub-niedersachsen.de",
"port": 443, "port": 443,
"useSSL": true, "useSSL": true,
"accessKey": "AbCdEfGhIjKlMnOpQrSt", "accessKey": "wjpKrmaqXra99rX3D61H",
"secretKey": "UvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWx" "secretKey": "fTPi0u0FR6Lv9Y9IKydWv6WM0EA5XrsK008HCt9u"
}, },
"jwt": { "jwt": {
"secret": "@S2!q@@wXz$dCQ8JoVsHLpzaJ6JCfB", "secret": "@S2!q@@wXz$dCQ8JoVsHLpzaJ6JCfB",
@@ -14,4 +14,4 @@
"admin": { "password": "A-InnoHUB_2025!", "admin": true }, "admin": { "password": "A-InnoHUB_2025!", "admin": true },
"user": { "password": "U-InnoHUB_2025!", "admin": false } "user": { "password": "U-InnoHUB_2025!", "admin": false }
} }
} }

View File

@@ -1,10 +1,10 @@
{ {
"minio": { "minio": {
"endPoint": "api-s3.innovation-hub-niedersachsen.de", "endPoint": "sws3.innovation-hub-niedersachsen.de",
"port": 443, "port": 443,
"useSSL": true, "useSSL": true,
"accessKey": "AbCdEfGhIjKlMnOpQrSt", "accessKey": "wjpKrmaqXra99rX3D61H",
"secretKey": "UvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWx" "secretKey": "fTPi0u0FR6Lv9Y9IKydWv6WM0EA5XrsK008HCt9u"
}, },
"jwt": { "jwt": {
"secret": "@S2!q@@wXz$dCQ8JoVsHLpzaJ6JCfB", "secret": "@S2!q@@wXz$dCQ8JoVsHLpzaJ6JCfB",
@@ -14,4 +14,4 @@
"admin": { "password": "A-InnoHUB_2025!", "admin": true }, "admin": { "password": "A-InnoHUB_2025!", "admin": true },
"user": { "password": "U-InnoHUB_2025!", "admin": false } "user": { "password": "U-InnoHUB_2025!", "admin": false }
} }
} }

View File

@@ -1,10 +1,10 @@
{ {
"minio": { "minio": {
"endPoint": "api-s3.innovation-hub-niedersachsen.de", "endPoint": "sws3.innovation-hub-niedersachsen.de",
"port": 443, "port": 443,
"useSSL": true, "useSSL": true,
"accessKey": "GxKhfnfkNvlDU7qzsz0D", "accessKey": "wjpKrmaqXra99rX3D61H",
"secretKey": "cqSM5rIRr4MPtqzu2sNKgmB9k2OghPbyxwAWogeM" "secretKey": "fTPi0u0FR6Lv9Y9IKydWv6WM0EA5XrsK008HCt9u"
}, },
"jwt": { "jwt": {
"secret": "@S2!q@@wXz$dCQ8JoVsHLpzaJ6JCfB", "secret": "@S2!q@@wXz$dCQ8JoVsHLpzaJ6JCfB",
@@ -14,4 +14,4 @@
"admin": { "password": "A-InnoHUB_2025!", "admin": true }, "admin": { "password": "A-InnoHUB_2025!", "admin": true },
"user": { "password": "U-InnoHUB_2025!", "admin": false } "user": { "password": "U-InnoHUB_2025!", "admin": false }
} }
} }

14
package-lock.json generated
View File

@@ -1461,9 +1461,9 @@
} }
}, },
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.49.2", "version": "2.49.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.2.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.0.tgz",
"integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", "integrity": "sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@standard-schema/spec": "^1.0.0", "@standard-schema/spec": "^1.0.0",
@@ -4534,12 +4534,12 @@
} }
}, },
"node_modules/jws": { "node_modules/jws": {
"version": "3.2.3", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jwa": "^1.4.2", "jwa": "^1.4.1",
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },

View File

@@ -1,3 +1,3 @@
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
export default JSON.parse(readFileSync('./config.json').toString()); export default JSON.parse(readFileSync('./config_prod.json').toString());

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition';
import shortenFileSize from '$lib/helper/shortenFileSize'; import shortenFileSize from '$lib/helper/shortenFileSize';
import timeElapsed from '$lib/helper/timeElapsed'; import timeElapsed from '$lib/helper/timeElapsed';
import { deserialize } from '$app/forms'; import { deserialize } from '$app/forms';
@@ -42,14 +41,10 @@
let formErrors: Record<string, any> | null = $state(null); let formErrors: Record<string, any> | null = $state(null);
let etag: string | null = $state(null); let etag: string | null = $state(null);
let files: FileList | null = $state(null); let files: FileList | null = $state(null);
let fileInput = $state(null);
// Model Variablen für Upload // Model Variablen für Upload
let openUL = $state(false); let openUL = $state(false);
let inProgressUL = $state(form === null); let inProgressUL = $state(form === null);
// Variablen für Copy-Funktion
let copied = $state(false);
async function buttonClick(event: MouseEvent) { async function buttonClick(event: MouseEvent) {
if (!(await validateForm())) { if (!(await validateForm())) {
@@ -103,7 +98,6 @@
openUL = false; openUL = false;
name = ''; name = '';
files = null; files = null;
fileInput.value = "";
await invalidateAll(); await invalidateAll();
crimesList = data.crimesList; crimesList = data.crimesList;
} }
@@ -134,17 +128,17 @@
// big endian! // big endian!
let file = files[0]; let file = files[0];
const fileHeader = file.slice(0, 4); let file_header = file.slice(0, 4);
const buffer = await fileHeader.arrayBuffer(); console.log(file_header);
console.log(fileHeader); let header_bytes = await file_header.bytes();
let headerBytes = new Uint8Array(buffer); let file_header_hex = '0x' + header_bytes.toHex().toString();
let fileHeaderHex = '0x' + headerBytes.toHex().toString();
if (GLD_MAGIC == swap_endian(fileHeaderHex)) { if (GLD_MAGIC == swap_endian(file_header_hex)) {
return true; return true;
} else { } else {
return false; return false;
} }
return true;
} }
async function getUrl() { async function getUrl() {
@@ -254,8 +248,9 @@
} }
} }
async function copyAndOpenMail() { function constructMailToLink() {
const subject = 'Link zum Tatvorgang'; const subject = 'Link zum Tatvorgang';
const link = data.url.origin + data.url.pathname; const link = data.url.origin + data.url.pathname;
const body = `Hallo, const body = `Hallo,
@@ -267,56 +262,17 @@ Der Zugangs-PIN wird zur Sicherheit über einen zweiten Kommunikationskanal übe
Mit freundlichen Grüßen, Mit freundlichen Grüßen,
`; `;
try { const mailtoLink = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
await navigator.clipboard.writeText(body);
copied = true;
// Kurz warten, dann Mail öffnen return mailtoLink;
setTimeout(() => { }
const mailtoLink = `mailto:?subject=${encodeURIComponent(subject)}`;
window.location.href = mailtoLink;
}, 1000);
setTimeout(() => copied = false, 2000);
} catch (err) {
console.error('Clipboard-Fehler:', err);
error = 'Konnte Text nicht kopieren. Bitte manuell markieren und kopieren.';
}
}
function closeModal() { function closeModal() {
open = false; open = false;
isError = false; isError = false;
} }
// drag and drop functionality
let isDragging = $state(false);
async function handleDrop(event) {
event.preventDefault();
isDragging = false;
if (event.dataTransfer?.files?.length) {
files = event.dataTransfer.files;
}
if (!(await check_valid_glb_file())) {
formErrors = { file: 'Keine gültige .GLD-Datei' }
// reset form fields etc.
files = null;
fileInput.value = '';
} else {
formErrors = { ...formErrors, file: ''}
};
}
</script> </script>
<svelte:window
on:dragover|preventDefault
on:drop|preventDefault
/>
{#if data.vorgang && crimesList} {#if data.vorgang && crimesList}
<div class="-z-10 bg-white"> <div class="-z-10 bg-white">
<div class="flex flex-col items-center justify-center w-full"> <div class="flex flex-col items-center justify-center w-full">
@@ -332,10 +288,9 @@ Mit freundlichen Grüßen,
onDelete={null} onDelete={null}
/> />
</div> </div>
<Button variant="secondary" on:click={copyAndOpenMail} disabled={isEmptyList}>Link kopieren und Mail verfassen</Button> <a class="pt-2 pb-6" href={constructMailToLink()}
{#if copied} ><Button disabled={isEmptyList}>Share Link</Button></a
<p transition:fade>✔ Kopiert! Per Ctrl+V einfügen.</p> >
{/if}
{/if} {/if}
</div> </div>
<div class="mx-auto flex justify-center max-w-7xl h-full"> <div class="mx-auto flex justify-center max-w-7xl h-full">
@@ -443,13 +398,7 @@ Mit freundlichen Grüßen,
</span> </span>
</label> </label>
<div <div
class="mt-2 flex justify-center rounded-lg border border-dashed px-6 py-10 class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-10"
{isDragging
? 'border-blue-500 bg-blue-50'
: 'border-gray-900/25'}"
on:dragover|preventDefault={() => (isDragging = true)}
on:dragleave={() => (isDragging = false)}
on:drop={handleDrop}
> >
<div class="text-center"> <div class="text-center">
<FileRect /> <FileRect />
@@ -459,7 +408,7 @@ Mit freundlichen Grüßen,
class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500" class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
> >
<span>Wähle eine Datei aus</span> <span>Wähle eine Datei aus</span>
<input id="file" bind:this={fileInput} bind:files name="file" type="file" class="sr-only" /> <input id="file" bind:files name="file" type="file" class="sr-only" />
</label> </label>
<p class="pl-1">oder ziehe sie ins Feld</p> <p class="pl-1">oder ziehe sie ins Feld</p>
</div> </div>

View File

@@ -18,7 +18,13 @@ describe('NameItemEditor - Funktionalität', () => {
onDelete onDelete
}; };
test.todo('FocusIn nach Klick auf edit'); test('Focus Input nach Klick auf edit', async () => {
render(NameItemEditor, { props: baseProps });
await fireEvent.click(screen.getByTestId('edit-button'));
const input = screen.getByTestId('test-input');
expect(document.activeElement).toBe(input);
});
it('zeigt initial Edit/Delete Buttons und aktuellen Namen', () => { it('zeigt initial Edit/Delete Buttons und aktuellen Namen', () => {
render(NameItemEditor, { props: baseProps }); render(NameItemEditor, { props: baseProps });

View File

@@ -41,7 +41,7 @@ export const baseData = {
vorgang: testVorgangsList[0], vorgang: testVorgangsList[0],
vorgangList: testVorgangsList, vorgangList: testVorgangsList,
crimesList: testCrimesList, crimesList: testCrimesList,
url: `https://example.com/list/${testVorgangsList[0].vorgangToken}`, url: new URL(`https://example.com/list/${testVorgangsList[0].vorgangToken}`),
crimeNames: ['modell-A', 'Fall-A'] crimeNames: ['modell-A', 'Fall-A']
}; };

View File

@@ -77,7 +77,39 @@ describe('Vorgang Anzeige via Token', () => {
// Cookie wird nicht gesetzt // Cookie wird nicht gesetzt
expect(cookiesSet).not.toHaveBeenCalled(); expect(cookiesSet).not.toHaveBeenCalled();
}); });
it.todo('Überprüfe was passiert, wenn Eingabe falsch, bzw. nicht im System passend gefunden');
it('Falsche PIN', 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)
};
// PIN-Validierung nicht erfolgreich
vi.mocked(vorgangPINValidation).mockReturnValueOnce(false);
const cookiesSet = vi.fn();
const event = {
request: mockRequest,
cookies: {
set: cookiesSet
}
};
const result = await actions.default(event);
expect(result.status).toBe(400);
expect(result.data.message).toMatch(/Falsch/i);
});
// Nicht vorhandener Vorgang-Token nicht notwendig, da PIN-Check
// entsprechend fehlerhaft
it.skip('Nicht vorhandener Vorgang-Token', () => {});
}); });
describe('Teste Guard', () => { describe('Teste Guard', () => {

View File

@@ -26,9 +26,29 @@ async function clickPlusButton() {
} }
describe('Seite: Vorgangsansicht', () => { describe('Seite: Vorgangsansicht', () => {
test.todo('Share Link disabled wenn Liste leer'); test('Share Link disabled wenn Liste leer', () => {
const testData = { ...baseData, crimesList: [] };
render(TatortListPage, { props: { data: testData } });
const button = screen.getByRole('button', { name: /share link/i });
expect(button).toBeInTheDocument()
expect(button).toBeDisabled();
});
describe('Szenario: Admin + Liste gefüllt - Funktionalität', () => { describe('Szenario: Admin + Liste gefüllt - Funktionalität', () => {
test.todo('Share Link Link generierung richtig'); test('Share Link Link generierung richtig', () => {
const testData = { ...baseData};
render(TatortListPage, { props: { data: testData } });
const link = screen.getByRole('link', { name: /share link/i });
expect(link).toBeInTheDocument()
// const vorgangTokenFirstUUIDGroup = testData.vorgangList[0].vorgangToken.split('-')[0]
const vorgangURL = testData.url.toString()
const vorgangURLEncoded = encodeURIComponent(vorgangURL)
expect(link).toHaveAttribute('href', expect.stringContaining(vorgangURLEncoded));
});
it('führt PUT-Request aus und aktualisiert UI nach onSave', async () => { it('führt PUT-Request aus und aktualisiert UI nach onSave', async () => {
const data = structuredClone(baseData); const data = structuredClone(baseData);