Added administration and password change

This commit is contained in:
2025-10-07 23:07:49 +02:00
parent 38eee8b38c
commit 25037f4798
8 changed files with 508 additions and 10 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "jeopardy",
"version": "1.0.4",
"version": "1.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jeopardy",
"version": "1.0.4",
"version": "1.0.5",
"dependencies": {
"axios": "^1.12.2",
"cookie": "^1.0.2"

View File

@@ -1,7 +1,7 @@
{
"name": "jeopardy",
"private": true,
"version": "1.0.4",
"version": "1.0.5",
"type": "module",
"scripts": {
"dev": "vite dev",

View File

@@ -17,6 +17,17 @@
background-color: rgba(0, 0, 0, 0.1);
}
.btn:disabled {
color: grey !important;
border-color: gray !important;
cursor: unset !important;
}
.btn:disabled:hover {
background-color: unset !important;
cursor: unset !important;
}
.inputField {
padding: 8px;
}

View File

@@ -6,6 +6,7 @@
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
/>
<script src="https://kit.fontawesome.com/4115dc7344.js" crossorigin="anonymous"></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="size-full">

85
src/lib/Modal.svelte Normal file
View File

@@ -0,0 +1,85 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
showModal: boolean;
header: Snippet;
children: Snippet;
actionButtons?: Snippet;
cancelFn?: () => void;
okFn: () => Promise<boolean>;
oncloseFn?: () => void;
[key: string]: unknown;
}
let {
showModal = $bindable(),
header,
children,
cancelFn,
okFn,
oncloseFn,
actionButtons
}: Props = $props();
let dialog: HTMLDialogElement | undefined = $state(); // HTMLDialogElement
$effect(() => {
if (showModal) dialog?.showModal();
});
</script>
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
<dialog
bind:this={dialog}
onclose={() => {
showModal = false;
if (oncloseFn) oncloseFn();
}}
onclick={(e) => {
if (e.target === dialog) dialog.close();
}}
class="rounded-md"
>
<div class="flex flex-col gap-4 p-4">
{@render header?.()}
{@render children?.()}
<!-- svelte-ignore a11y_autofocus -->
<div class="flex justify-end gap-4">
{@render actionButtons?.()}
<button
autofocus
onclick={() => {
dialog?.close();
if (cancelFn) cancelFn();
}}
class="btn">Abbrechen</button
>
<button
autofocus
onclick={async () => {
if (await okFn()) {
dialog?.close();
}
}}
class="btn min-w-[64px]">Ok</button
>
</div>
</div>
</dialog>
<style>
dialog {
position: fixed;
top: 50%;
left: 50%;
/* Move it back 50% relative to self */
-webkit-transform: translateX(-50%) translateY(-50%);
-moz-transform: translateX(-50%) translateY(-50%);
-ms-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
</style>

View File

@@ -1,7 +1,9 @@
<script lang="ts">
import { afterNavigate, goto } from "$app/navigation";
import { isAuthenticated } from "$lib/Auth.svelte";
import { goto } from "$app/navigation";
import { env } from "$env/dynamic/public";
import UserSvelte from "$lib/User.svelte";
import websocket, { SocketConnectionType } from "$lib/websocket.svelte";
import axios from "axios";
$effect(() => {
if (websocket.connectionType === SocketConnectionType.HOST) {
@@ -13,15 +15,53 @@
goto("/connected/display");
}
});
function logout() {
cookieStore
.delete("jeopardytoken")
.then(() => {
goto("/login");
})
.catch((e) => {
alert("Logout fehlgeschlagen!");
goto("/login");
});
}
async function logoutFromAllDevices() {
axios
.post(
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/user/logout`,
{},
{
withCredentials: true
}
)
.then(() => {
logout();
})
.catch(() => {
alert("Logout fehlgeschlagen!");
});
}
</script>
<div class="flex h-full flex-col">
<div class="flex items-center">
<h1 class="m-4 mb-8 text-7xl font-bold">Jeopardy</h1>
<div class="ms-4 me-4 flex items-center gap-4">
<h1 class="text-7xl font-bold">Jeopardy</h1>
<div class="grow"></div>
<button type="button" class="btn me-4" onclick={() => goto("/settings")}
>Einstellungen</button
{#if UserSvelte.role === "admin"}
<button type="button" class="btn" onclick={() => goto("/admin")}>Administration</button>
{/if}
<button type="button" class="btn" onclick={() => goto("/settings")}>Einstellungen</button>
<button type="button" class="btn" onclick={logout}>Logout</button>
<button type="button" class="btn" onclick={logoutFromAllDevices}
>Logout von allen Geräten</button
>
<div class="btn profile ps-2 pe-2">
<i class="fa-regular fa-user"></i>
{UserSvelte.username}
</div>
</div>
<div class="flex h-full grow items-center justify-around p-4">
@@ -33,3 +73,14 @@
>
</div>
</div>
<style>
.profile {
border-color: gray;
}
.profile:hover {
background-color: unset !important;
cursor: unset !important;
}
</style>

View File

@@ -0,0 +1,350 @@
<script lang="ts">
import { afterNavigate, goto } from "$app/navigation";
import type { UserObj } from "$lib/User.svelte";
import { env } from "$env/dynamic/public";
import axios from "axios";
import { onMount } from "svelte";
import Modal from "$lib/Modal.svelte";
import UserSvelte from "$lib/User.svelte";
let users: UserObj[] = $state([]);
let roles: string[] = $state([]);
let showAddUser = $state(false);
let addUserName = $state("");
let showDeleteUser = $state(false);
let showResetPassword = $state(false);
let showChangeRole = $state(false);
let selectedUser: UserObj | undefined = $state(undefined);
let roleToChange: string = $state("");
let error = $state("");
async function loadUsers() {
console.log("loading users");
return axios
.get(
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user/list`,
{
withCredentials: true
}
)
.then((response) => {
if (response.status === 200) {
users = response.data;
}
})
.catch((e) => {
console.log(e);
});
}
async function loadRoles() {
console.log("loading roles");
return axios
.get(
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/roles`,
{
withCredentials: true
}
)
.then((response) => {
if (response.status === 200) {
roles = response.data;
}
})
.catch((e) => {
console.log(e);
});
}
afterNavigate(() => {
if (UserSvelte.role !== "admin") goto("/");
});
onMount(() => {
if (UserSvelte.role !== "admin") goto("/");
loadRoles().then(loadUsers);
});
async function addUserOk(): Promise<boolean> {
error = "";
if (addUserName.length <= 0) {
error = "Gib einen Nutzernamen ein";
return false;
}
return axios
.put(
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user`,
{
username: addUserName
},
{
withCredentials: true
}
)
.then((response) => {
if (response.status === 200) {
alert(`Passwort: ${response.data.password}`);
return true;
} else {
throw false;
}
})
.catch(() => {
error = "Etwas ist schief gelaufen";
return false;
});
}
async function deleteUserOk(): Promise<boolean> {
error = "";
if (selectedUser === undefined) {
error = "Etwas ist schief gelaufen. Kein Nutzer zum Löschen definiert.";
return false;
}
return axios
.delete(
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user`,
{
withCredentials: true,
data: {
userid: selectedUser._id
}
}
)
.then(() => {
return true;
})
.catch(() => {
error = "Nutzer konnte nicht gelöscht werden";
return false;
});
}
async function resetPasswordOk(): Promise<boolean> {
error = "";
if (selectedUser === undefined) {
error = "Etwas ist schief gelaufen. Kein Nutzer zum Löschen definiert.";
return false;
}
return axios
.post(
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user/resetpw`,
{
userid: selectedUser._id
},
{
withCredentials: true
}
)
.then((response) => {
if (response.status === 200) {
alert(`Passwort: ${response.data.password}`);
return true;
} else {
throw false;
}
})
.catch(() => {
error = "Etwas ist schief gelaufen";
return false;
});
}
async function changeRoleOk(): Promise<boolean> {
error = "";
if (selectedUser === undefined) {
error = "Etwas ist schief gelaufen. Kein Nutzer zum Löschen definiert.";
return false;
}
if (roleToChange === selectedUser.role) {
error = `${selectedUser.username} hat bereits die Rolle ${roleToChange}. Wähle eine andere Rolle aus.`;
return false;
}
return axios
.post(
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user/changerole`,
{
userid: selectedUser._id,
role: roleToChange
},
{
withCredentials: true
}
)
.then((response) => {
if (response.status === 200) {
return true;
} else {
throw false;
}
})
.catch(() => {
error = "Etwas ist schief gelaufen";
return false;
});
}
function addUserCancel() {
error = "";
addUserName = "";
}
function selectedUserCancel() {
error = "";
selectedUser = undefined;
roleToChange = "";
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center">
<h2 class="text-4xl font-bold">Administration</h2>
<div class="grow"></div>
<button type="button" class="btn" onclick={() => goto("/")}>Zurück</button>
</div>
<div class="flex flex-col gap-2">
{#each users as user (user._id)}
<div class="flex justify-between gap-4 rounded-md border-1 p-2">
<div class="shrink-0 grow-0 text-center align-middle">{user._id}</div>
<div>{user.username}</div>
<div>{user.role}</div>
<div class="flex gap-2">
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
type="button"
class="btn"
disabled={user._id === UserSvelte.id}
onclick={() => {
selectedUser = user;
roleToChange = selectedUser.role;
showChangeRole = true;
}}><i class="fa-solid fa-user"></i> Rolle ändern</button
>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
type="button"
class="btn"
disabled={user._id === UserSvelte.id}
onclick={() => {
selectedUser = user;
showResetPassword = true;
}}
><i class="fa-solid fa-arrow-rotate-right"></i> Passwort zurücksetzen</button
>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
disabled={user._id === UserSvelte.id}
type="button"
class="btn border-red-600 text-red-600"
onclick={() => {
selectedUser = user;
showDeleteUser = true;
}}><i class="fa-solid fa-trash"></i></button
>
</div>
</div>
{/each}
</div>
<button type="button" class="btn" onclick={() => (showAddUser = true)}>Nutzer hinzufügen</button
>
</div>
<Modal bind:showModal={showAddUser} okFn={addUserOk} cancelFn={addUserCancel} oncloseFn={loadUsers}>
{#snippet header()}
<h2 class="text-3xl">Nutzer hinzufügen</h2>
{/snippet}
<div>
<label for="username" class="">Name</label>
<div>
<input
type="text"
name="username"
id="username"
class="borders mt-2 mb-2 w-full"
bind:value={addUserName}
/>
</div>
{#if error.length > 0}
<div class="text-red-700">{error}</div>
{/if}
</div>
</Modal>
<Modal
bind:showModal={showDeleteUser}
okFn={deleteUserOk}
cancelFn={selectedUserCancel}
oncloseFn={loadUsers}
>
{#snippet header()}
<h2 class="text-3xl">Nutzer löschen</h2>
{/snippet}
<div>Soll Nutzer {selectedUser?.username} wirklich gelöscht werden?</div>
{#if error.length > 0}
<div class="text-red-700">{error}</div>
{/if}
</Modal>
<Modal
bind:showModal={showResetPassword}
okFn={resetPasswordOk}
cancelFn={selectedUserCancel}
oncloseFn={loadUsers}
>
{#snippet header()}
<h2 class="text-3xl">Passwort zurücksetzen</h2>
{/snippet}
<div>
Soll das Passwort von Nutzer {selectedUser?.username} wirklich zurückgesetzt werden?
</div>
{#if error.length > 0}
<div class="text-red-700">{error}</div>
{/if}
</Modal>
<Modal
bind:showModal={showChangeRole}
okFn={changeRoleOk}
cancelFn={selectedUserCancel}
oncloseFn={loadUsers}
>
{#snippet header()}
<h2 class="text-3xl">Rolle von {selectedUser?.username} ändern</h2>
{/snippet}
<div>
{#if selectedUser}
<select name="roles" id="role-select" class="btn w-full" bind:value={roleToChange}>
{#each roles as role, index (index)}
<option value={role}>{role}</option>
{/each}
</select>
{/if}
</div>
{#if error.length > 0}
<div class="text-red-700">{error}</div>
{/if}
</Modal>
<style>
.borders {
border: 1px solid black;
border-radius: 5px;
display: flex;
padding: 2px;
}
</style>

View File

@@ -38,7 +38,7 @@
}
</script>
<div class="m-4 flex flex-col gap-4">
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center">
<h2 class="text-4xl font-bold">Einstellungen</h2>
<div class="grow"></div>