18 Commits

Author SHA1 Message Date
cbea96f892 implement deletion of admin user, frontend and backend 2025-08-06 11:52:39 +02:00
7e87a01c59 rename userList variable to CamelCase 2025-08-06 11:44:48 +02:00
4f0526c71f rename loop item variable for userlist-user 2025-08-06 09:59:18 +02:00
dd06c93b1c fix currentUser fetching from loaded data 2025-08-06 09:58:00 +02:00
26b94938a9 user list and possibility of adding new users implemented on FE and BE 2025-08-06 08:37:24 +02:00
b5ff4a72fb Merge branch 'development' into f052_admin_area 2025-08-05 08:34:16 +02:00
eea1621339 Merge branch 'f078_delete_unused_views' into development
All checks were successful
InnoHub Processor/tatort/pipeline/head This commit looks good
2025-08-05 08:31:35 +02:00
c53400d7b7 Merge pull request 'replace copy-to-clipboard with share-via-link-mail-to' (#24) from f054_mail-to-link into development
All checks were successful
InnoHub Processor/tatort/pipeline/head This commit looks good
Reviewed-on: #24
2025-08-05 08:22:47 +02:00
ef1ddb58a0 encode body in mail-to-link button 2025-08-05 08:21:28 +02:00
8c4f95d52e Merge pull request 'f076_api_endpoints_for_renaming_crimes' (#25) from f076_api_endpoints_for_renaming_crimes into development
All checks were successful
InnoHub Processor/tatort/pipeline/head This commit looks good
Reviewed-on: #25
2025-08-05 08:13:26 +02:00
5b408b096e remove console.log 2025-08-05 08:12:34 +02:00
aae8237aae Merge pull request 'fix SonarQube issue, mainly type annotations' (#26) from f079_sonarqube_issues_fixen into development
All checks were successful
InnoHub Processor/tatort/pipeline/head This commit looks good
Reviewed-on: #26
Reviewed-by: jared <jared.busch@polizei.niedersachsen.de>
2025-08-04 14:10:49 +02:00
54e3a64dac remove unused link button to deleted view 2025-08-01 12:40:36 +02:00
5927f1dacd fix SonarQube issue, mainly type annotations 2025-08-01 10:29:34 +02:00
f39debd0b8 fix renaming PUT endpoint to work around parameters 2025-08-01 09:44:45 +02:00
7d265341cc rename index looping variable 2025-08-01 08:45:17 +02:00
d6f2956bcb replace copy-to-clipboard with share-via-link-mail-to 2025-07-31 13:15:00 +02:00
e1cf6dc16f remove old unused ´view´ and ´tatorte´ views 2025-07-31 10:47:31 +02:00
13 changed files with 325 additions and 234 deletions

View File

@@ -0,0 +1,54 @@
import { db } from '$lib/server/dbService';
export const getUsers = (): { userId: string; userName: string }[] => {
const getUsersSQLStmt = `SELECT id, name
FROM users;`;
const statement = db.prepare(getUsersSQLStmt);
const result = statement.all() as { id: string; name: string }[];
const userList: { userId: string; userName: string }[] = [];
for (const resultItem of result) {
const user = { userId: resultItem.id, userName: resultItem.name };
userList.push(user);
}
return userList;
};
export const addUser = (userName: string, userPassword: string): number => {
const addUserSQLStmt = `INSERT into users(name, pw)
values (?, ?)`;
const statement = db.prepare(addUserSQLStmt);
let rowCount;
try {
const info = statement.run(userName, userPassword);
rowCount = info.changes;
} catch (error) {
console.log(error);
rowCount = 0;
}
return rowCount;
};
export const deleteUser = (userId: string) => {
// make sure to not delete the last entry
const deleteUserSQLStmt = `DELETE
FROM users
WHERE id = ?
AND (SELECT COUNT(*) FROM users) > 1;`;
const statement = db.prepare(deleteUserSQLStmt);
let rowCount;
try {
const info = statement.run(userId);
rowCount = info.changes;
} catch (error) {
console.log(error);
rowCount = 0;
}
return rowCount;
};

View File

@@ -48,11 +48,11 @@
>
<FileRect class=" group-hover:text-indigo-600" {outline} />
</div>
<a href="/view" class="mt-6 block font-semibold text-gray-900">
Ansicht
<a href="/user-management" class="mt-6 block font-semibold text-gray-900">
Benutzerverwaltung
<span class="absolute inset-0"></span>
</a>
<p class="mt-1 text-gray-600">Schau Dir einen Tatort in der 3D Ansicht an.</p>
<p class="mt-1 text-gray-600">Füge neue Benutzer hinzu oder entferne welche.</p>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import { getVorgaenge } from '$lib/server/vorgangService';
import type { PageServerLoad } from '../../(token-based)/view/$types';
export const load: PageServerLoad = async () => {
const vorgangList = await getVorgaenge();
const vorgangList = getVorgaenge();
return {
vorgangList

View File

@@ -1,37 +0,0 @@
import { client } from '$lib/minio';
import { fail } from '@sveltejs/kit';
import vorgangNumberOccupied from '$lib/helper/vorgangNumberOccupied.js';
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ request }: {request: Request}) => {
const data = await request.formData();
const vorgangNumber = data.get('vorgangNumber');
const description = data.get('description');
if (!vorgangNumber) {
return fail(400, {
vorgangNumber,
description,
error: { vorgangNumber: 'Es muss eine Vorgangsnummer vorhanden sein.' }
});
}
if (await vorgangNumberOccupied(`${vorgangNumber}`)) {
return fail(400, {
vorgangNumber,
description,
error: { vorgangNumber: 'Die Vorgangsnummer wurde im System bereits angelegt.' }
});
}
const config = `${JSON.stringify({ vorgangNumber, description, version: 1 })}\n`;
await client.putObject('tatort', `${vorgangNumber}/config.json`, config, undefined, {
'Content-Type': 'application/json'
});
return { success: true };
}
};

View File

@@ -1,115 +0,0 @@
<script lang="ts">
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;
let open = false;
$: open = form?.success ?? false;
</script>
<div class="mx-auto max-w-2xl">
<div class="flex flex-col items-center justify-center w-full">
<h1 class="text-xl">Neuer Vorgang</h1>
</div>
<form method="POST">
<div class="space-y-12">
<div class="border-b border-gray-900/10 pb-12">
<p class="mt-8 text-sm leading-6 text-gray-600">
This information will be displayed publicly so be careful what you share.
</p>
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-8">
<div>
<label for="vorgangNumber" class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex"
>{#if form?.error?.vorgangNumber}
<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?.vorgangNumber ?? ''}
type="text"
name="vorgangNumber"
id="vorgangNumber"
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"
/>
</div>
</div>
{#if form?.error?.vorgangNumber}
<p class="block text-sm leading-6 text-red-900 mt-2">{form.error.vorgangNumber}</p>
{/if}
</div>
<div>
<label for="description" class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex"
>{#if form?.description}
<span class="inline-block mr-1"><Exclamation /></span>
{/if} Beschreibung</span
></label
>
<div class="mt-2">
<textarea
value={form?.description?.toString() ?? ''}
id="description"
name="description"
rows="3"
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"
></textarea>
</div>
{#if form?.error}
<p class="block text-sm leading-6 text-red-900 mt-2">{form.description}</p>
{/if}
</div>
<label for="vorgang-token">
<span >Zugangstoken (optional) </span>
</label>
<div class="mt-2">
<div
>
<input
type="text"
id="vorgang-token"
/>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-x-6">
<button type="button" class="text-sm font-semibold leading-6 text-gray-900">Cancel</button>
<Button
type="submit"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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"
>Save</Button
>
</div>
</div>
</form>
<Modal {open}
><ModalTitle>vorgang anlegen</ModalTitle><ModalContent>
{#if form?.success}
<Alert class="w-full">Vorgang erfolgreich angelegt</Alert>
{:else}
<Alert class="w-full" type="error">Fehler beim Upload</Alert>
{/if}
</ModalContent>
<ModalFooter><Button on:click={() => (open = false)}>Ok</Button></ModalFooter>
</Modal>
</div>

View File

@@ -17,11 +17,11 @@ const isRequiredFieldValid = (value: unknown) => {
export const actions = {
url: async ({ request }: { request: Request }) => {
const data = await request.formData();
const vorgangName = data.get('vorgang');
const crimeName = data.get('name');
const type = data.get('type');
const vorgangPIN = data.get('vorgangPIN');
const fileName = data.get('fileName');
const vorgangName: string | null = data.get('vorgang') as string;
const crimeName: string | null = data.get('name') as string;
const type: string | null = data.get('type') as string;
const vorgangPIN: string | null = data.get('vorgangPIN') as string;
const fileName: string | null = data.get('fileName') as string;
const vorgangExists = vorgangNameExists(vorgangName);
let vorgangToken;

View File

@@ -0,0 +1,193 @@
<script lang="ts">
import {onMount} from 'svelte';
import Button from "$lib/components/Button.svelte";
import jsSHA from 'jssha'
const {data} = $props();
let userName = $state('')
let userPassword = $state('')
let userList = $state([])
let addUserError = $state(false);
let addUserSuccess = $state(false);
const currentUser: string = data.user.id;
onMount(async () => {
userList = await getUsers();
})
async function getUsers() {
const URL = "/api/users"
const response = await fetch(URL);
return await response.json();
}
async function addUser() {
if (userName == "") {
alert("Der Benutzername darf nicht leer sein.")
return;
}
if (userPassword == "") {
alert("Das Passwort darf nicht leer sein.")
return;
}
const URL = "/api/users";
const hashedUserPassword = new jsSHA('SHA-512', 'TEXT').update(userPassword).getHash('HEX');
const userData = {userName: userName, userPassword: hashedUserPassword}
const response = await fetch(URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
})
if (response.ok) {
userList = await getUsers();
addUserSuccess = true;
resetInput();
} else {
addUserError = true;
}
}
function resetInput() {
userName = "";
userPassword = "";
addUserError = false;
setInterval(() => {
addUserSuccess = false;
}, 5000);
}
async function deleteUser(userId: number) {
const URL = `/api/users/${userId}`;
const response = await fetch(URL, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
if (response.status == 204) {
userList = await getUsers();
} else {
alert("Nutzer konnte nicht gelöscht werden")
}
}
</script>
<h1 class="flex justify-center text-3xl md:text-4xl font-bold text-gray-800 dark:text-white tracking-tight mb-4">
Benutzerverwaltung
</h1>
<h1 class="flex justify-center text-lg md:text-xl font-medium text-gray-800 dark:text-white tracking-tight mb-2">
Benutzerliste
</h1>
<div class="w-1/4 mx-auto">
<table class="min-w-full border border-gray-300 rounded overflow-hidden">
<thead class="bg-gray-100 dark:bg-gray-700">
<tr>
<th class="text-left px-4 py-2">Benutzername</th>
<th class="text-center px-4 py-2">Entfernen</th>
</tr>
</thead>
<tbody>
{#each userList as userItem}
<tr class="border-t border-gray-200 dark:border-gray-600">
<td class="px-4 py-2 text-gray-800 dark:text-white">{userItem.userName}</td>
<td class="px-4 py-2 text-center">
<button
class="text-red-600 hover:text-red-800 focus:outline-none focus:ring-2 focus:ring-red-500 rounded-full p-1 transition"
on:click={() => deleteUser(userItem.userId)}
aria-label="Delete user"
>
{#if (userItem.userName != currentUser)}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"/>
</svg>
{/if}
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<h1 style="margin-top: 50px;"
class="flex justify-center text-lg md:text-xl font-medium text-gray-800 dark:text-white tracking-tight mb-2">
Neuer Nutzer
</h1>
<div class="mx-auto flex justify-center">
<form>
<div class="form-group">
<label for="username">Benutzername</label>
<input bind:value={userName} type="text" id="username" placeholder="Namen eingeben"/>
</div>
<div class="form-group">
<label for="password">Passwort</label>
<input bind:value={userPassword} type="password" id="password" placeholder="Passwort vergeben"/>
</div>
</form>
</div>
<div class="mx-auto flex flex-col items-center space-y-4" style="margin-top: 20px;">
{#if addUserError}
<div class="flex items-center bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 rounded shadow-sm"
role="alert">
<svg class="h-5 w-5 mr-3 text-yellow-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01M12 5a7 7 0 100 14 7 7 0 000-14z"/>
</svg>
<span class="text-sm font-medium">Der Benutzer konnte nicht hinzugefügt werden.</span>
</div>
{/if}
{#if addUserSuccess}
<div class="flex items-center bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 rounded shadow-sm"
role="alert">
<svg class="h-5 w-5 mr-3 text-yellow-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01M12 5a7 7 0 100 14 7 7 0 000-14z"/>
</svg>
<span class="text-sm font-medium">Der Benutzer wurde hinzugefügt.</span>
</div>
{/if}
<Button on:click={addUser}>Benutzer hinzufügen</Button>
</div>
<style>
.form-group {
display: flex;
flex-direction: column;
margin-bottom: 15px;
}
label {
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
input[type="password"] {
padding: 8px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
</style>

View File

@@ -59,8 +59,8 @@
return;
}
async function handle_input(ev: KeyboardEvent, i: number) {
let item = crimesList[i];
async function handleEditFieldInput(ev: KeyboardEvent, listItemIndex: number) {
let item = crimesList[listItemIndex];
if (ev.key == 'Escape') {
let text_field_id = `label__${item.name}`;
@@ -93,7 +93,7 @@
ev.preventDefault();
// construct PUT URL
const url = $page.url;
const url = $page.url.pathname.split('?')[0];
let data_obj: { new_name: string; old_name: string } = { new_name: '', old_name: '' };
data_obj['new_name'] = new_name;
@@ -129,13 +129,22 @@
}
}
async function setClipboard(text) {
const type = "text/plain";
const clipboardItemData = {
[type]: text,
};
const clipboardItem = new ClipboardItem(clipboardItemData);
await navigator.clipboard.write([clipboardItem]);
function constructMailToLink() {
const subject = "Link zum Tatvorgang";
const link = $page.url.toString().split('?')[0];
const body = `Hallo,
hier ist der Link zum Tatvorgang:
${link}
Der Zugangs-PIN wird zur Sicherheit über einen zweiten Kommunikationskanal übermittelt.
Mit freundlichen Grüßen,
`;
const mailtoLink = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
return mailtoLink;
}
</script>
@@ -144,12 +153,12 @@
<h1 class="text-xl">Vorgang {vorgang.name}</h1>
{#if data?.user?.admin}
Zugangs-PIN: {vorgang.pin}
<Button on:click={() => setClipboard($page.url.toString().split('?')[0])}>Copy Link</Button>
<a href="{constructMailToLink()}"><Button>Share Link</Button></a>
{/if}
</div>
<div class="mx-auto flex justify-center max-w-7xl h-full">
<ul class="divide-y divide-gray-100">
{#each crimesList as item, i}
{#each crimesList as item, crimeListItemIndex}
<li>
<a
href="/view/{$page.params.vorgang}/{item.name}?pin={vorgangPIN}"
@@ -168,12 +177,12 @@
tabindex="0"
aria-label="Dateiname bearbeiten"
on:focusout={() => {
defocus_element(i);
defocus_element(crimeListItemIndex);
}}
on:keydown|stopPropagation={// event needed to identify ID
// TO-DO: check if event is needed or if index is sufficient
async (ev) => {
handle_input(ev, i);
handleEditFieldInput(ev, crimeListItemIndex);
}}>{item.name}</span
>

View File

@@ -2,21 +2,20 @@ import { client } from '$lib/minio';
import { json } from '@sveltejs/kit';
// rename operation
// rename operation for crimes
export async function PUT({ request }: {request: Request}) {
const data = await request.json();
// Vorgang
const vorgang = request.url.split('/').at(-1);
const vorgangToken = request.url.split('/').at(-1);
// prepare copy, incl. check if new name exists already
const old_name = data["old_name"];
const src_full_path = `/tatort/${vorgang}/${old_name}`;
const new_name = `${vorgang}/${data["new_name"]}`;
const crimeOldName = data["old_name"];
const crimeS3FullBucketPathOld = `/tatort/${vorgangToken}/${crimeOldName}`;
const crimeNewName = `${vorgangToken}/${data["new_name"]}`;
try {
await client.statObject('tatort', new_name);
await client.statObject('tatort', crimeNewName);
return json({ msg: 'Die Datei existiert bereits.' }, { status: 400 });
} catch (error) {
// continue operation
@@ -24,10 +23,10 @@ export async function PUT({ request }: {request: Request}) {
}
// actual copy operation
await client.copyObject('tatort', new_name, src_full_path)
await client.copyObject('tatort', crimeNewName, crimeS3FullBucketPathOld)
// delete
await client.removeObject('tatort', `${vorgang}/${old_name}`)
await client.removeObject('tatort', `${vorgangToken}/${crimeOldName}`)
// return success or failure

View File

@@ -1,11 +0,0 @@
import { redirect } from '@sveltejs/kit';
export const actions = {
default: async ({request}: {request: Request}) => {
const data = await request.formData();
const vorgangId = data.get('vorgang-id');
const vorgangToken = data.get('vorgang-token');
if( vorgangId && vorgangToken) throw redirect(303, `/list/${vorgangId}?token=${vorgangToken}`);
}
}

View File

@@ -1,39 +0,0 @@
<script lang="ts">
import BaseInputField from '$lib/components/BaseInputField.svelte';
import Button from '$lib/components/Button.svelte';
import ArrowRight from '$lib/icons/Arrow-right.svelte';
export let form;
</script>
<div class="mx-auto max-w-2xl">
<div class="flex flex-col items-center justify-center w-full">
<h1 class="text-xl">Vorgang ansehen</h1>
</div>
<p class="mt-8 mb-8 text-sm leading-6 text-gray-600">
Anhand der Vorgangsnummer werden Sie zu den Dateien des Vorgangs weitergeleitet und können sich
den Vorgang dann ansehen.
</p>
<form method="POST">
<BaseInputField
id="vorgang-id"
name="vorgang-id"
label="Vorgangskennung"
type="text"
value={form?.vorgangId}
/>
<div class="mt-5">
<BaseInputField
id="vorgang-token"
name="vorgang-token"
label="Zugangstoken"
type="text"
value={form?.vorgangToken}
error={form?.error?.message}
/>
</div>
<div class="flex justify-end pt-4">
<Button type="submit"><ArrowRight /></Button>
</div>
</form>
</div>

View File

@@ -0,0 +1,25 @@
import { json } from '@sveltejs/kit';
import { addUser, getUsers } from '$lib/server/userService';
export function GET({ locals }) {
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const userList = getUsers();
return new Response(JSON.stringify(userList));
}
export async function POST({ request, locals }) {
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const data = await request.json();
const userName = data.userName;
const userPassword = data.userPassword;
const rowCount = addUser(userName, userPassword);
return new Response(null, { status: rowCount == 1 ? 200 : 400 });
}

View File

@@ -0,0 +1,13 @@
import { json } from '@sveltejs/kit';
import { deleteUser } from '$lib/server/userService';
export async function DELETE({ params, locals }) {
if (!locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const userId = params.user;
const rowCount = deleteUser(userId);
return new Response(null, { status: rowCount == 1 ? 204 : 400 });
}