From e00123d4bc792c670c528ca19cd380af26f65521 Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 21:21:11 +0200 Subject: [PATCH 01/14] =?UTF-8?q?F=C3=BCge=20Einladungslink-System=20hinzu?= =?UTF-8?q?:=20Implementiere=20Funktionen=20zum=20Erstellen,=20L=C3=B6sche?= =?UTF-8?q?n=20und=20Registrieren=20=C3=BCber=20Einladungslinks;=20aktuali?= =?UTF-8?q?siere=20die=20Admin-Oberfl=C3=A4che=20und=20erstelle=20entsprec?= =?UTF-8?q?hende=20Pug-Templates.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller.js | 93 ++++++++++++++++++++++++++++++++++++ src/db.js | 49 +++++++++++++++++++ src/webapp.js | 10 +++- views/admin.pug | 55 ++++++++++++++------- views/inviteCreate.pug | 9 ++++ views/inviteDelete.pug | 8 ++++ views/inviteRegistration.pug | 20 ++++++++ 7 files changed, 226 insertions(+), 18 deletions(-) create mode 100644 views/inviteCreate.pug create mode 100644 views/inviteDelete.pug create mode 100644 views/inviteRegistration.pug diff --git a/src/controller.js b/src/controller.js index eedb777..22cc9c2 100644 --- a/src/controller.js +++ b/src/controller.js @@ -45,6 +45,99 @@ export function adminPasswordResetView(req, res) { res.render('adminPasswordReset', {username: username, title: `Passwort zurücksetzen: ${username}`}); } +// Einladungslink-System +export function inviteCreateView(req, res) { + // Admin-Formular zum Erstellen eines Einladungslinks anzeigen + res.render('inviteCreate', { title: 'Einladung erstellen' }); +} + +export function inviteCreatePost(req, res) { + const adminUuid = process.env.ADMIN_UUID; + if (!req.session.uuid || req.session.uuid !== adminUuid) { + return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); + } + const { inviteId } = req.body; + if (!inviteId || inviteId.trim() === "") { + return res.redirect('/admin?error=inviteIdMissing'); + } + createInviteLink(inviteId.trim()).then(() => { + return res.redirect('/admin'); + }).catch((err) => { + return res.redirect('/admin?error=inviteCreateFailed'); + }); +} + +export function inviteDeleteView(req, res) { + // Bestätigungsseite zum Löschen eines Einladungslinks anzeigen + const { id } = req.params; + res.render('inviteDelete', { title: 'Einladung löschen', invite: { id } }); +} + +export function inviteDeletePost(req, res) { + const adminUuid = process.env.ADMIN_UUID; + if (!req.session.uuid || req.session.uuid !== adminUuid) { + return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); + } + const { id } = req.params; + if (!id || id.trim() === "") { + return res.redirect('/admin?error=inviteIdMissing'); + } + removeInviteLink(id.trim()).then(() => { + return res.redirect('/admin'); + }).catch((err) => { + return res.redirect('/admin?error=inviteDeleteFailed'); + }); +} + +export function inviteRegistrationView(req, res) { + // Registrierung über Einladung anzeigen + const { id } = req.params; + res.render('inviteRegistration', { title: 'Registrierung über Einladung', invite: { id } }); +} + +export function inviteRegistrationPost(req, res) { + const { id } = req.params; + const { username, password, passwordRepeat, displayname } = req.body; + // Lade Invite-Daten + getInviteData(id).then(async (invite) => { + if (!invite || (invite.usedAt && invite.usedAt !== "")) { + return res.render('inviteRegistration', { error: "Ungültiger oder bereits genutzter Einladungslink.", invite: { id }, title: "Registrierung über Einladung" }); + } + // Validierung + if (!username || !password || !passwordRepeat || !displayname) { + return res.render('inviteRegistration', { error: "Bitte alle Felder ausfüllen.", invite: { id }, title: "Registrierung über Einladung" }); + } + if (password !== passwordRepeat) { + return res.render('inviteRegistration', { error: "Passwörter stimmen nicht überein.", invite: { id }, title: "Registrierung über Einladung" }); + } + if (!isUsernameValid(username)) { + return res.render('inviteRegistration', { error: "Ungültiger Benutzername.", invite: { id }, title: "Registrierung über Einladung" }); + } + if (!isPasswordValid(password)) { + return res.render('inviteRegistration', { error: "Ungültiges Passwort.", invite: { id }, title: "Registrierung über Einladung" }); + } + if (!isDisplaynameValid(displayname)) { + return res.render('inviteRegistration', { error: "Ungültiger Anzeigename.", invite: { id }, title: "Registrierung über Einladung" }); + } + // Prüfe, ob Username verfügbar + const exists = await isUsernameAvailable(username); + if (exists) { + return res.render('inviteRegistration', { error: "Benutzername bereits vergeben.", invite: { id }, title: "Registrierung über Einladung" }); + } + // Registrierung durchführen + const uuid = crypto.randomUUID(); + try { + await storeUser(uuid, username, password, displayname); + await setInviteLinkUsed(id); + return res.redirect('/login'); + } catch (err) { + return res.render('inviteRegistration', { error: "Fehler bei der Registrierung.", invite: { id }, title: "Registrierung über Einladung" }); + } + }).catch(() => { + return res.render('inviteRegistration', { error: "Ungültiger Einladungslink.", invite: { id }, title: "Registrierung über Einladung" }); + }); +} + // Admin: Passwort setzen (Formular-Submit) export function doAdminPasswordReset(req, res) { const { username } = req.params; diff --git a/src/db.js b/src/db.js index 6716595..f75be0a 100644 --- a/src/db.js +++ b/src/db.js @@ -204,4 +204,53 @@ export async function getPasskeysByWebAuthnUserID(webauthnUserID) { } return getUserPasskeys(uuid); +} + +export async function createInviteLink(id) { + const exists = await rc.exists("guard:invite:" + escape(id)); + if (exists) { + throw new Error("Invite link already exists"); + } + const now = Date.now(); + await rc.hSet("guard:invite:" + escape(id), { + id, + createdAt: now, + usedAt: "", + usedBy: "" + }); +} + +export async function removeInviteLink(id) { + await rc.del("guard:invite:" + escape(id)); +} + +export async function setInviteLinkUsed(id,uuid){ + const now = Date.now(); + await rc.hSet("guard:invite:" + escape(id), "usedAt", now); + await rc.hSet("guard:invite:" + escape(id), "usedBy", uuid); +} + +export async function getInviteLink(id) { + const data = await rc.hGetAll("guard:invite:" + escape(id)); + if (!data || Object.keys(data).length === 0) { + return null; + } + return { + id: data.id, + createdAt: new Date(Number(data.createdAt)).toLocaleString(), + usedAt: data.usedAt ? new Date(Number(data.usedAt)).toLocaleString() : "", + usedBy: data.usedBy || "" + }; +} + +export async function getAllInviteLinks() { + const keys = await rc.keys("guard:invite:*"); + const invites = []; + for (const key of keys) { + const invite = await getInviteLink(key.split(":").pop()); + if (invite) { + invites.push(invite); + } + } + return invites; } \ No newline at end of file diff --git a/src/webapp.js b/src/webapp.js index 4eb266b..a9df87f 100644 --- a/src/webapp.js +++ b/src/webapp.js @@ -29,7 +29,7 @@ import { endpointVerifyAuthenticationResponse, endpointVerifyRegistrationResponse } from "./controller.js"; -import { adminPasswordResetView, doAdminPasswordReset } from "./controller.js"; +import { adminPasswordResetView, doAdminPasswordReset, inviteCreateView, inviteCreatePost, inviteDeleteView, inviteDeletePost, inviteRegistrationView, inviteRegistrationPost } from "./controller.js"; import * as bodyParser from "express"; const __filename = fileURLToPath(import.meta.url); @@ -68,6 +68,14 @@ webapp.get('/admin', admin) webapp.get('/admin/passwordreset/:username', adminPasswordResetView) webapp.post('/admin/passwordreset/:username', doAdminPasswordReset) +// Einladungslink-System +webapp.get('/admin/invite/create', inviteCreateView) +webapp.post('/admin/invite/create', inviteCreatePost) +webapp.get('/admin/invite/delete/:id', inviteDeleteView) +webapp.post('/admin/invite/delete/:id', inviteDeletePost) +webapp.get('/register/invite/:id', inviteRegistrationView) +webapp.post('/register/invite/:id', inviteRegistrationPost) + webapp.get('/info', getInformation) webapp.get('/register', registerUser) diff --git a/views/admin.pug b/views/admin.pug index 1be142e..615ebdb 100644 --- a/views/admin.pug +++ b/views/admin.pug @@ -4,21 +4,42 @@ block inner-content h1.fs-1.mt-2.mb-3 Admin: Nutzerübersicht table.table.table-striped.w-full.text-left.border-collapse thead.bg-gray-100 - tr - th.px-4.py-2.fs-5.font-semibold.text-gray-700 UUID - th.px-4.py-2.fs-5.font-semibold.text-gray-700 Displayname - th.px-4.py-2.fs-5.font-semibold.text-gray-700 Benutzername - th.px-4.py-2.fs-5.font-semibold.text-gray-700 Erstellungsdatum - th.px-4.py-2.fs-5.font-semibold.text-gray-700 Letzter Login - th.px-4.py-2.fs-5.font-semibold.text-gray-700 Aktionen - tbody - each user in users tr - td.px-4.py-2.fs-5.border-b.border-gray-200= user.uuid - td.px-4.py-2.fs-5.border-b.border-gray-200= user.displayname - td.px-4.py-2.fs-5.border-b.border-gray-200= user.username - td.px-4.py-2.fs-5.border-b.border-gray-200= user.creation - td.px-4.py-2.fs-5.border-b.border-gray-200= user.lastLogin - td.px-4.py-2.fs-5.border-b.border-gray-200 - a(href=`/admin/passwordreset/${encodeURIComponent(user.username)}`) - button.btn.btn-primary.fs-5.m-1 Passwort zurücksetzen + th.px-4.py-2.fs-5.font-semibold.text-gray-700 UUID + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Displayname + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Benutzername + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Erstellungsdatum + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Letzter Login + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Aktionen + tbody + each user in users + tr + td.px-4.py-2.fs-5.border-b.border-gray-200= user.uuid + td.px-4.py-2.fs-5.border-b.border-gray-200= user.displayname + td.px-4.py-2.fs-5.border-b.border-gray-200= user.username + td.px-4.py-2.fs-5.border-b.border-gray-200= user.creation + td.px-4.py-2.fs-5.border-b.border-gray-200= user.lastLogin + td.px-4.py-2.fs-5.border-b.border-gray-200 + a(href=`/admin/passwordreset/${encodeURIComponent(user.username)}`) + button.btn.btn-primary.fs-5.m-1 Passwort zurücksetzen + + h2.fs-3.mt-5.mb-3 Einladungslinks + a.btn.btn-success.mb-3(href="/admin/invite/create") Neue Einladung erstellen + if invites && invites.length > 0 + table.table.table-striped.w-full.text-left.border-collapse + thead.bg-gray-100 + tr + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Einladungscode + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Erstellt am + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Genutzt am + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Aktionen + tbody + each invite in invites + tr + td.px-4.py-2.fs-5.border-b.border-gray-200= invite.id + td.px-4.py-2.fs-5.border-b.border-gray-200= invite.createdAt ? new Date(Number(invite.createdAt)).toLocaleString() : "" + td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedAt && invite.usedAt !== "" ? new Date(Number(invite.usedAt)).toLocaleString() : "-" + td.px-4.py-2.fs-5.border-b.border-gray-200 + a.btn.btn-danger.fs-5.m-1(href=`/admin/invite/delete/${invite.id}`) Löschen + else + p.text-gray-600 Keine Einladungslinks vorhanden. diff --git a/views/inviteCreate.pug b/views/inviteCreate.pug new file mode 100644 index 0000000..4acd4b4 --- /dev/null +++ b/views/inviteCreate.pug @@ -0,0 +1,9 @@ +extends neumorphLayout + +block inner-content + h1.fs-1.mt-2.mb-3 Einladung erstellen + form(method="post" action="/admin/invite/create") + .mb-3 + label(for="inviteId") Einladungscode + input#inviteId.form-control(type="text" name="inviteId" required placeholder="Einladungscode eingeben") + button.btn.btn-primary(type="submit") Einladung erstellen diff --git a/views/inviteDelete.pug b/views/inviteDelete.pug new file mode 100644 index 0000000..4574afb --- /dev/null +++ b/views/inviteDelete.pug @@ -0,0 +1,8 @@ +extends neumorphLayout + +block inner-content + h1.fs-1.mt-2.mb-3 Einladung löschen + p Möchtest du die Einladung wirklich löschen? + form(method="post" action=`/admin/invite/delete/${invite.id}`) + button.btn.btn-danger(type="submit") Einladung löschen + a.btn.btn-secondary(href="/admin") Abbrechen diff --git a/views/inviteRegistration.pug b/views/inviteRegistration.pug new file mode 100644 index 0000000..215e8fc --- /dev/null +++ b/views/inviteRegistration.pug @@ -0,0 +1,20 @@ +extends neumorphLayout + +block inner-content + h1.fs-1.mt-2.mb-3 Registrierung über Einladung + if error + .alert.alert-danger= error + form(method="post" action=`/register/invite/${invite.id}`) + .mb-3 + label(for="username") Benutzername + input#username.form-control(type="text" name="username" required) + .mb-3 + label(for="displayname") Anzeigename + input#displayname.form-control(type="text" name="displayname" required) + .mb-3 + label(for="password") Passwort + input#password.form-control(type="password" name="password" required) + .mb-3 + label(for="passwordRepeat") Passwort wiederholen + input#passwordRepeat.form-control(type="password" name="passwordRepeat" required) + button.btn.btn-primary(type="submit") Registrieren From 2e79bf95dd257639fc76ab704a5dd1a0edff8b52 Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 21:23:14 +0200 Subject: [PATCH 02/14] Fixed missing imports --- src/controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controller.js b/src/controller.js index 22cc9c2..e573772 100644 --- a/src/controller.js +++ b/src/controller.js @@ -1,4 +1,5 @@ -import { deletePasskey, getAllUsers, getPasskey, getUserByWebAuthnID, getUserPasskeys, storePasskey, updatePasskeyCounter } from "./db.js"; +import { deletePasskey, getAllUsers, getPasskey, getUserByWebAuthnID, getUserPasskeys, storePasskey, updatePasskeyCounter, + createInviteLink, getInviteData, removeInviteLink, setInviteLinkUsed } from "./db.js"; import fs from "fs"; import path from "path"; import { From a7f32f047e96321cc1387eec27cce1678ed4162a Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 21:28:18 +0200 Subject: [PATCH 03/14] Fixing incorrect imports --- src/controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controller.js b/src/controller.js index e573772..1704d54 100644 --- a/src/controller.js +++ b/src/controller.js @@ -1,5 +1,5 @@ import { deletePasskey, getAllUsers, getPasskey, getUserByWebAuthnID, getUserPasskeys, storePasskey, updatePasskeyCounter, - createInviteLink, getInviteData, removeInviteLink, setInviteLinkUsed } from "./db.js"; + createInviteLink, getInviteLink, removeInviteLink, setInviteLinkUsed } from "./db.js"; import fs from "fs"; import path from "path"; import { @@ -100,7 +100,7 @@ export function inviteRegistrationPost(req, res) { const { id } = req.params; const { username, password, passwordRepeat, displayname } = req.body; // Lade Invite-Daten - getInviteData(id).then(async (invite) => { + getInviteLink(id).then(async (invite) => { if (!invite || (invite.usedAt && invite.usedAt !== "")) { return res.render('inviteRegistration', { error: "Ungültiger oder bereits genutzter Einladungslink.", invite: { id }, title: "Registrierung über Einladung" }); } From a4776e8104a718ac02423b661f93e37f2129a3ce Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 21:35:20 +0200 Subject: [PATCH 04/14] =?UTF-8?q?F=C3=BCge=20Einladungen=20zur=20Admin-Pan?= =?UTF-8?q?el-Ansicht=20hinzu:=20Lade=20und=20zeige=20alle=20Einladungslin?= =?UTF-8?q?ks=20an.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/controller.js b/src/controller.js index 1704d54..36a53f8 100644 --- a/src/controller.js +++ b/src/controller.js @@ -1,5 +1,6 @@ import { deletePasskey, getAllUsers, getPasskey, getUserByWebAuthnID, getUserPasskeys, storePasskey, updatePasskeyCounter, - createInviteLink, getInviteLink, removeInviteLink, setInviteLinkUsed } from "./db.js"; + createInviteLink, getInviteLink, removeInviteLink, setInviteLinkUsed, + getAllInviteLinks} from "./db.js"; import fs from "fs"; import path from "path"; import { @@ -25,8 +26,9 @@ export async function admin(req, res) { return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); } const users = await getAllUsers(); - log("Admin-Panel Zugriff gewährt. Nutzer geladen: " + users.length); - res.render('admin', {users, title: "Admin"}); + const invites = await getAllInviteLinks(); + log("Admin-Panel Zugriff gewährt. Nutzer geladen: " + users.length + ", Einladungen geladen: " + (Array.isArray(invites) ? invites.length : 0)); + res.render('admin', {users, invites, title: "Admin"}); } // Admin: Passwort zurücksetzen (Formular) From 5c75bb9c8786f84eec8d6239ab3b610d3d9c2e3d Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 21:38:40 +0200 Subject: [PATCH 05/14] =?UTF-8?q?F=C3=BCge=20Admin-Bereichslink=20zur=20Da?= =?UTF-8?q?shboard-Ansicht=20hinzu=20und=20aktualisiere=20die=20Admin-?= =?UTF-8?q?=C3=9Cbersicht=20mit=20neuen=20Layout-Anpassungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller.js | 7 ++++--- views/admin.pug | 12 +++++++----- views/dashboard.pug | 2 ++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/controller.js b/src/controller.js index 36a53f8..2b4e35e 100644 --- a/src/controller.js +++ b/src/controller.js @@ -414,10 +414,11 @@ export async function dashboard(req, res) { log("Dashboard-View: Nutzer nicht eingeloggt. Weiterleitung zu: " + suffix); return res.redirect(suffix); } - let uname = await getUsername(req.session.uuid); - let dname = await getDisplayname(req.session.uuid); + const uname = await getUsername(req.session.uuid); + const dname = await getDisplayname(req.session.uuid); + const is_admin = req.session.uuid === adminUuid; log(`Dashboard-View für Nutzer: ${uname}, Displayname: ${dname}`); - res.render('dashboard', {username: uname, displayname: dname, error: error, title: "Dashboard", state: state, redirect_uri: redirect_uri}); + res.render('dashboard', {username: uname, displayname: dname, error: error, title: "Dashboard", state: state, redirect_uri: redirect_uri,is_admin}); } diff --git a/views/admin.pug b/views/admin.pug index 615ebdb..40873c4 100644 --- a/views/admin.pug +++ b/views/admin.pug @@ -1,8 +1,9 @@ extends neumorphLayout block inner-content - h1.fs-1.mt-2.mb-3 Admin: Nutzerübersicht - table.table.table-striped.w-full.text-left.border-collapse + h1.fs-1.mb-3 Admin: Nutzerübersicht + h2.fs-3.mt-3.mb-3 Nutzer + table.table.table-striped.w-full.text-left.border-collapse.mt-2 thead.bg-gray-100 tr th.px-4.py-2.fs-5.font-semibold.text-gray-700 UUID @@ -23,10 +24,9 @@ block inner-content a(href=`/admin/passwordreset/${encodeURIComponent(user.username)}`) button.btn.btn-primary.fs-5.m-1 Passwort zurücksetzen - h2.fs-3.mt-5.mb-3 Einladungslinks - a.btn.btn-success.mb-3(href="/admin/invite/create") Neue Einladung erstellen + h2.fs-3.mt-3.mb-3 Einladungslinks if invites && invites.length > 0 - table.table.table-striped.w-full.text-left.border-collapse + table.table.table-striped.w-full.text-left.border-collapse.mt-2 thead.bg-gray-100 tr th.px-4.py-2.fs-5.font-semibold.text-gray-700 Einladungscode @@ -43,3 +43,5 @@ block inner-content a.btn.btn-danger.fs-5.m-1(href=`/admin/invite/delete/${invite.id}`) Löschen else p.text-gray-600 Keine Einladungslinks vorhanden. + + a.btn.btn-success.mt-2.mb-3(href="/admin/invite/create") Neue Einladung erstellen diff --git a/views/dashboard.pug b/views/dashboard.pug index b227429..eca9b4c 100644 --- a/views/dashboard.pug +++ b/views/dashboard.pug @@ -6,6 +6,8 @@ block inner-content a.btn.btn-primary.fs-5.m-1(href="usernamechange") Nutzername ändern a.btn.btn-primary.fs-5.m-1(href="passwordchange") Passwort ändern a.btn.btn-primary.fs-5.m-1(href="passkeymanage") Passkeys verwalten + if is_admin + a.btn.btn-warning.fs-5.m-1(href="admin") Admin-Bereich br .mb-3 a.btn.btn-outline-secondary.fs-5.m-1#logoutButton(href="logout") Abmelden \ No newline at end of file From d5e87f487845b535161718dc4d95853e4e67d11e Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 21:41:07 +0200 Subject: [PATCH 06/14] fixes --- src/controller.js | 1 + views/error.pug | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controller.js b/src/controller.js index 2b4e35e..952d261 100644 --- a/src/controller.js +++ b/src/controller.js @@ -416,6 +416,7 @@ export async function dashboard(req, res) { } const uname = await getUsername(req.session.uuid); const dname = await getDisplayname(req.session.uuid); + const adminUuid = process.env.ADMIN_UUID; const is_admin = req.session.uuid === adminUuid; log(`Dashboard-View für Nutzer: ${uname}, Displayname: ${dname}`); res.render('dashboard', {username: uname, displayname: dname, error: error, title: "Dashboard", state: state, redirect_uri: redirect_uri,is_admin}); diff --git a/views/error.pug b/views/error.pug index c540809..a178107 100644 --- a/views/error.pug +++ b/views/error.pug @@ -4,5 +4,5 @@ block inner-content .spinner-border.fs-5 .alert.alert-danger(hidden)#alert #alert-text - span.fs-2.text-danger #{error} + span.fs-2.text-danger.ml-1 #{error} p.fs-5 #{message} \ No newline at end of file From afaa46a9d5fbedde0de45d49ab005588e2f5fb65 Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 21:45:49 +0200 Subject: [PATCH 07/14] =?UTF-8?q?F=C3=BCge=20zus=C3=A4tzliche=20Spalten=20?= =?UTF-8?q?f=C3=BCr=20Einladungscode,=20Erstellungsdatum=20und=20Benutzer?= =?UTF-8?q?=20in=20der=20Einladungslinks-Tabelle=20hinzu=20und=20verbesser?= =?UTF-8?q?e=20das=20Layout=20im=20L=C3=B6schformular.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- views/admin.pug | 4 ++++ views/inviteDelete.pug | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/views/admin.pug b/views/admin.pug index 40873c4..e746c93 100644 --- a/views/admin.pug +++ b/views/admin.pug @@ -32,13 +32,17 @@ block inner-content th.px-4.py-2.fs-5.font-semibold.text-gray-700 Einladungscode th.px-4.py-2.fs-5.font-semibold.text-gray-700 Erstellt am th.px-4.py-2.fs-5.font-semibold.text-gray-700 Genutzt am + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Genutzt am + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Benutzer th.px-4.py-2.fs-5.font-semibold.text-gray-700 Aktionen tbody each invite in invites tr td.px-4.py-2.fs-5.border-b.border-gray-200= invite.id td.px-4.py-2.fs-5.border-b.border-gray-200= invite.createdAt ? new Date(Number(invite.createdAt)).toLocaleString() : "" + td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedAt td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedAt && invite.usedAt !== "" ? new Date(Number(invite.usedAt)).toLocaleString() : "-" + td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedBy && invite.usedBy !== "" ? invite.usedBy : "-" td.px-4.py-2.fs-5.border-b.border-gray-200 a.btn.btn-danger.fs-5.m-1(href=`/admin/invite/delete/${invite.id}`) Löschen else diff --git a/views/inviteDelete.pug b/views/inviteDelete.pug index 4574afb..fe654b9 100644 --- a/views/inviteDelete.pug +++ b/views/inviteDelete.pug @@ -4,5 +4,5 @@ block inner-content h1.fs-1.mt-2.mb-3 Einladung löschen p Möchtest du die Einladung wirklich löschen? form(method="post" action=`/admin/invite/delete/${invite.id}`) - button.btn.btn-danger(type="submit") Einladung löschen + button.btn.btn-danger.mr-2(type="submit") Einladung löschen a.btn.btn-secondary(href="/admin") Abbrechen From 039e71d0e97eb75b34f252da2eb44985169692e5 Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 21:51:44 +0200 Subject: [PATCH 08/14] =?UTF-8?q?Aktualisiere=20die=20Einladungssystem-Log?= =?UTF-8?q?ik=20zur=20Verwendung=20von=20UUIDs=20und=20verbessere=20die=20?= =?UTF-8?q?Fehleranzeige=20in=20der=20Registrierung.=20Korrigiere=20die=20?= =?UTF-8?q?Spalten=C3=BCberschrift=20in=20der=20Admin-Ansicht=20f=C3=BCr?= =?UTF-8?q?=20Einladungslinks.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller.js | 3 ++- views/admin.pug | 4 ++-- views/error.pug | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/controller.js b/src/controller.js index 952d261..e8bcf6e 100644 --- a/src/controller.js +++ b/src/controller.js @@ -131,9 +131,10 @@ export function inviteRegistrationPost(req, res) { const uuid = crypto.randomUUID(); try { await storeUser(uuid, username, password, displayname); - await setInviteLinkUsed(id); + await setInviteLinkUsed(id, uuid); return res.redirect('/login'); } catch (err) { + console.log(err); return res.render('inviteRegistration', { error: "Fehler bei der Registrierung.", invite: { id }, title: "Registrierung über Einladung" }); } }).catch(() => { diff --git a/views/admin.pug b/views/admin.pug index e746c93..4096dc0 100644 --- a/views/admin.pug +++ b/views/admin.pug @@ -31,7 +31,7 @@ block inner-content tr th.px-4.py-2.fs-5.font-semibold.text-gray-700 Einladungscode th.px-4.py-2.fs-5.font-semibold.text-gray-700 Erstellt am - th.px-4.py-2.fs-5.font-semibold.text-gray-700 Genutzt am + th.px-4.py-2.fs-5.font-semibold.text-gray-700 Erstellt am th.px-4.py-2.fs-5.font-semibold.text-gray-700 Genutzt am th.px-4.py-2.fs-5.font-semibold.text-gray-700 Benutzer th.px-4.py-2.fs-5.font-semibold.text-gray-700 Aktionen @@ -40,7 +40,7 @@ block inner-content tr td.px-4.py-2.fs-5.border-b.border-gray-200= invite.id td.px-4.py-2.fs-5.border-b.border-gray-200= invite.createdAt ? new Date(Number(invite.createdAt)).toLocaleString() : "" - td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedAt + td.px-4.py-2.fs-5.border-b.border-gray-200= invite.createdAt td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedAt && invite.usedAt !== "" ? new Date(Number(invite.usedAt)).toLocaleString() : "-" td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedBy && invite.usedBy !== "" ? invite.usedBy : "-" td.px-4.py-2.fs-5.border-b.border-gray-200 diff --git a/views/error.pug b/views/error.pug index a178107..0d49910 100644 --- a/views/error.pug +++ b/views/error.pug @@ -4,5 +4,7 @@ block inner-content .spinner-border.fs-5 .alert.alert-danger(hidden)#alert #alert-text - span.fs-2.text-danger.ml-1 #{error} + span + | + span.fs-2.text-danger #{error} p.fs-5 #{message} \ No newline at end of file From cf9460155e8907c26e836186f2ba7d1cd3a0fd87 Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 22:00:17 +0200 Subject: [PATCH 09/14] =?UTF-8?q?F=C3=BCge=20Einladungscode-Feld=20zur=20R?= =?UTF-8?q?egistrierung=20=C3=BCber=20Einladung=20hinzu=20und=20verbessere?= =?UTF-8?q?=20die=20Fehlerbehandlung=20f=C3=BCr=20fehlende=20Einladungscod?= =?UTF-8?q?es.=20Aktualisiere=20die=20Routen=20f=C3=BCr=20die=20Registrier?= =?UTF-8?q?ung.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller.js | 12 +++++++----- src/webapp.js | 4 ++-- views/admin.pug | 3 +-- views/inviteRegistration.pug | 3 +++ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/controller.js b/src/controller.js index e8bcf6e..64685dd 100644 --- a/src/controller.js +++ b/src/controller.js @@ -94,16 +94,18 @@ export function inviteDeletePost(req, res) { export function inviteRegistrationView(req, res) { // Registrierung über Einladung anzeigen - const { id } = req.params; - res.render('inviteRegistration', { title: 'Registrierung über Einladung', invite: { id } }); + res.render('inviteRegistration', { title: 'Registrierung über Einladung'}); } export function inviteRegistrationPost(req, res) { - const { id } = req.params; - const { username, password, passwordRepeat, displayname } = req.body; + const { username, password, passwordRepeat, displayname, inviteId } = req.body; + if (!inviteId || inviteId.trim() === "") { + return res.render('inviteRegistration', { error: "Einladungscode fehlt.", title: "Registrierung über Einladung" }); + } // Lade Invite-Daten - getInviteLink(id).then(async (invite) => { + getInviteLink(inviteId).then(async (invite) => { if (!invite || (invite.usedAt && invite.usedAt !== "")) { + console.log('tried to used invalid or already used invite link',invite); return res.render('inviteRegistration', { error: "Ungültiger oder bereits genutzter Einladungslink.", invite: { id }, title: "Registrierung über Einladung" }); } // Validierung diff --git a/src/webapp.js b/src/webapp.js index a9df87f..fa8afb4 100644 --- a/src/webapp.js +++ b/src/webapp.js @@ -73,8 +73,8 @@ webapp.get('/admin/invite/create', inviteCreateView) webapp.post('/admin/invite/create', inviteCreatePost) webapp.get('/admin/invite/delete/:id', inviteDeleteView) webapp.post('/admin/invite/delete/:id', inviteDeletePost) -webapp.get('/register/invite/:id', inviteRegistrationView) -webapp.post('/register/invite/:id', inviteRegistrationPost) +webapp.get('/register/invite', inviteRegistrationView) +webapp.post('/register/invite', inviteRegistrationPost) webapp.get('/info', getInformation) diff --git a/views/admin.pug b/views/admin.pug index 4096dc0..9fed534 100644 --- a/views/admin.pug +++ b/views/admin.pug @@ -39,9 +39,8 @@ block inner-content each invite in invites tr td.px-4.py-2.fs-5.border-b.border-gray-200= invite.id - td.px-4.py-2.fs-5.border-b.border-gray-200= invite.createdAt ? new Date(Number(invite.createdAt)).toLocaleString() : "" td.px-4.py-2.fs-5.border-b.border-gray-200= invite.createdAt - td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedAt && invite.usedAt !== "" ? new Date(Number(invite.usedAt)).toLocaleString() : "-" + td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedAt && invite.usedAt !== "" ? invite.usedAt : "-" td.px-4.py-2.fs-5.border-b.border-gray-200= invite.usedBy && invite.usedBy !== "" ? invite.usedBy : "-" td.px-4.py-2.fs-5.border-b.border-gray-200 a.btn.btn-danger.fs-5.m-1(href=`/admin/invite/delete/${invite.id}`) Löschen diff --git a/views/inviteRegistration.pug b/views/inviteRegistration.pug index 215e8fc..49293b6 100644 --- a/views/inviteRegistration.pug +++ b/views/inviteRegistration.pug @@ -17,4 +17,7 @@ block inner-content .mb-3 label(for="passwordRepeat") Passwort wiederholen input#passwordRepeat.form-control(type="password" name="passwordRepeat" required) + .mb-3 + label(for="inviteId") Einladungscode + input#inviteId.form-control(type="text" name="inviteId" required) button.btn.btn-primary(type="submit") Registrieren From 4d53783d558caa0af4b457634c5dc947989c9492 Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 22:00:58 +0200 Subject: [PATCH 10/14] fixed post link for invite registration --- views/inviteRegistration.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/inviteRegistration.pug b/views/inviteRegistration.pug index 49293b6..e663e45 100644 --- a/views/inviteRegistration.pug +++ b/views/inviteRegistration.pug @@ -4,7 +4,7 @@ block inner-content h1.fs-1.mt-2.mb-3 Registrierung über Einladung if error .alert.alert-danger= error - form(method="post" action=`/register/invite/${invite.id}`) + form(method="post" action=`/register/invite`) .mb-3 label(for="username") Benutzername input#username.form-control(type="text" name="username" required) From 35d2d15be773c581be63b2321364484cc81cafbb Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 22:03:43 +0200 Subject: [PATCH 11/14] Verbessere die Fehlerbehandlung in der Einladung zur Registrierung, indem die Verwendung von inviteId anstelle von id in den Fehlermeldungen aktualisiert wird. --- src/controller.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/controller.js b/src/controller.js index 64685dd..71e4d65 100644 --- a/src/controller.js +++ b/src/controller.js @@ -106,41 +106,41 @@ export function inviteRegistrationPost(req, res) { getInviteLink(inviteId).then(async (invite) => { if (!invite || (invite.usedAt && invite.usedAt !== "")) { console.log('tried to used invalid or already used invite link',invite); - return res.render('inviteRegistration', { error: "Ungültiger oder bereits genutzter Einladungslink.", invite: { id }, title: "Registrierung über Einladung" }); + return res.render('inviteRegistration', { error: "Ungültiger oder bereits genutzter Einladungslink.", invite: { inviteId }, title: "Registrierung über Einladung" }); } // Validierung if (!username || !password || !passwordRepeat || !displayname) { - return res.render('inviteRegistration', { error: "Bitte alle Felder ausfüllen.", invite: { id }, title: "Registrierung über Einladung" }); + return res.render('inviteRegistration', { error: "Bitte alle Felder ausfüllen.", invite: { inviteId }, title: "Registrierung über Einladung" }); } if (password !== passwordRepeat) { - return res.render('inviteRegistration', { error: "Passwörter stimmen nicht überein.", invite: { id }, title: "Registrierung über Einladung" }); + return res.render('inviteRegistration', { error: "Passwörter stimmen nicht überein.", invite: { inviteId }, title: "Registrierung über Einladung" }); } if (!isUsernameValid(username)) { - return res.render('inviteRegistration', { error: "Ungültiger Benutzername.", invite: { id }, title: "Registrierung über Einladung" }); + return res.render('inviteRegistration', { error: "Ungültiger Benutzername.", invite: { inviteId }, title: "Registrierung über Einladung" }); } if (!isPasswordValid(password)) { - return res.render('inviteRegistration', { error: "Ungültiges Passwort.", invite: { id }, title: "Registrierung über Einladung" }); + return res.render('inviteRegistration', { error: "Ungültiges Passwort.", invite: { inviteId }, title: "Registrierung über Einladung" }); } if (!isDisplaynameValid(displayname)) { - return res.render('inviteRegistration', { error: "Ungültiger Anzeigename.", invite: { id }, title: "Registrierung über Einladung" }); + return res.render('inviteRegistration', { error: "Ungültiger Anzeigename.", invite: { inviteId }, title: "Registrierung über Einladung" }); } // Prüfe, ob Username verfügbar const exists = await isUsernameAvailable(username); if (exists) { - return res.render('inviteRegistration', { error: "Benutzername bereits vergeben.", invite: { id }, title: "Registrierung über Einladung" }); + return res.render('inviteRegistration', { error: "Benutzername bereits vergeben.", invite: { inviteId }, title: "Registrierung über Einladung" }); } // Registrierung durchführen const uuid = crypto.randomUUID(); try { await storeUser(uuid, username, password, displayname); - await setInviteLinkUsed(id, uuid); + await setInviteLinkUsed(inviteId, uuid); return res.redirect('/login'); } catch (err) { console.log(err); - return res.render('inviteRegistration', { error: "Fehler bei der Registrierung.", invite: { id }, title: "Registrierung über Einladung" }); + return res.render('inviteRegistration', { error: "Fehler bei der Registrierung.", invite: { inviteId }, title: "Registrierung über Einladung" }); } }).catch(() => { - return res.render('inviteRegistration', { error: "Ungültiger Einladungslink.", invite: { id }, title: "Registrierung über Einladung" }); + return res.render('inviteRegistration', { error: "Ungültiger Einladungslink.", invite: { inviteId }, title: "Registrierung über Einladung" }); }); } From 69dbb39278a79c660eeacf79746e025d4958892e Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 22:09:44 +0200 Subject: [PATCH 12/14] =?UTF-8?q?F=C3=BCge=20Funktionalit=C3=A4t=20zum=20L?= =?UTF-8?q?=C3=B6schen=20von=20Nutzern=20im=20Admin-Bereich=20hinzu=20und?= =?UTF-8?q?=20implementiere=20die=20zugeh=C3=B6rigen=20Ansichten=20und=20R?= =?UTF-8?q?outen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller.js | 57 ++++++++++++++++++++++++++++++++------- src/db.js | 16 +++++++++++ src/webapp.js | 6 +++++ views/admin.pug | 3 ++- views/adminDeleteUser.pug | 12 +++++++++ 5 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 views/adminDeleteUser.pug diff --git a/src/controller.js b/src/controller.js index 71e4d65..cc1ce15 100644 --- a/src/controller.js +++ b/src/controller.js @@ -18,10 +18,13 @@ function log(message) { fs.appendFileSync(logPath, `[${timestamp}] ${message}\n`); console.log(`[${timestamp}] ${message}`); } + +function isAdmin(req) { + return req.session.uuid && req.session.uuid === process.env.ADMIN_UUID; +} export async function admin(req, res) { log("Admin-Panel Zugriff versucht von UUID: " + req.session.uuid); - const adminUuid = process.env.ADMIN_UUID; - if (!req.session.uuid || req.session.uuid !== adminUuid) { + if (!isAdmin(req)) { log("Admin-Panel Zugriff verweigert für UUID: " + req.session.uuid); return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); } @@ -35,8 +38,7 @@ export async function admin(req, res) { export function adminPasswordResetView(req, res) { const { username } = req.params; log(`AdminPasswordReset-View aufgerufen von UUID: ${req.session.uuid} für username: ${username}`); - const adminUuid = process.env.ADMIN_UUID; - if (!req.session.uuid || req.session.uuid !== adminUuid) { + if (!isAdmin(req)) { log(`AdminPasswordReset Zugriff verweigert für UUID: ${req.session.uuid}`); return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); } @@ -54,9 +56,46 @@ export function inviteCreateView(req, res) { res.render('inviteCreate', { title: 'Einladung erstellen' }); } +// Admin: Nutzer löschen (Bestätigungsseite) +export async function adminDeleteUserView(req, res) { + const { username } = req.params; + if (!isAdmin(req)) { + return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); + } + if (!username) { + return res.redirect('/admin?error=missingUsername'); + } + const uuid = await getUUIDByUsername(username); + if (!uuid) { + return res.redirect('/admin?error=userNotFound'); + } + const displayname = await getDisplayname(uuid); + res.render('adminDeleteUser', {user: {uuid, username, displayname}, title: `Nutzer löschen: ${username}`}); +} + +// Admin: Nutzer löschen (POST) +export async function adminDeleteUserPost(req, res) { + const { username } = req.params; + if (!isAdmin(req)) { + return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); + } + if (!username) { + return res.redirect('/admin?error=missingUsername'); + } + const uuid = await getUUIDByUsername(username); + if (!uuid) { + return res.redirect('/admin?error=userNotFound'); + } + try { + await deleteUser(uuid); + return res.redirect('/admin'); + } catch (err) { + return res.redirect('/admin?error=deleteFailed'); + } +} + export function inviteCreatePost(req, res) { - const adminUuid = process.env.ADMIN_UUID; - if (!req.session.uuid || req.session.uuid !== adminUuid) { + if (!isAdmin(req)) { return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); } const { inviteId } = req.body; @@ -77,8 +116,7 @@ export function inviteDeleteView(req, res) { } export function inviteDeletePost(req, res) { - const adminUuid = process.env.ADMIN_UUID; - if (!req.session.uuid || req.session.uuid !== adminUuid) { + if (!isAdmin(req)) { return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); } const { id } = req.params; @@ -149,8 +187,7 @@ export function doAdminPasswordReset(req, res) { const { username } = req.params; const { password, passwordRepeat } = req.body; log(`AdminPasswordReset Aktion von UUID: ${req.session.uuid} für username: ${username}`); - const adminUuid = process.env.ADMIN_UUID; - if (!req.session.uuid || req.session.uuid !== adminUuid) { + if (!isAdmin(req)) { log(`AdminPasswordReset Zugriff verweigert für UUID: ${req.session.uuid}`); return res.status(403).render('error', {error: 403, message: "Zugriff verweigert"}); } diff --git a/src/db.js b/src/db.js index f75be0a..746f15a 100644 --- a/src/db.js +++ b/src/db.js @@ -253,4 +253,20 @@ export async function getAllInviteLinks() { } } return invites; +} + +// Admin: Nutzer löschen +export async function deleteUser(uuid) { + // Hole alle Passkeys des Nutzers und lösche sie + const passkeyIds = await rc.hKeys("guard:user:" + escape(uuid) + ":passkeys"); + for (const id of passkeyIds) { + await deletePasskey(id); + } + // Lösche Nutzer-Daten + const username = await getUsername(uuid); + await rc.hDel("guard:usernames", username); + await rc.del("guard:user:" + escape(uuid)); + await rc.del("guard:user:" + escape(uuid) + ":passkeys"); + // Optional: weitere zugehörige Daten entfernen + return true; } \ No newline at end of file diff --git a/src/webapp.js b/src/webapp.js index fa8afb4..1afb3b3 100644 --- a/src/webapp.js +++ b/src/webapp.js @@ -30,6 +30,8 @@ import { endpointVerifyRegistrationResponse } from "./controller.js"; import { adminPasswordResetView, doAdminPasswordReset, inviteCreateView, inviteCreatePost, inviteDeleteView, inviteDeletePost, inviteRegistrationView, inviteRegistrationPost } from "./controller.js"; + +import { adminDeleteUserView, adminDeleteUserPost } from "./controller.js"; import * as bodyParser from "express"; const __filename = fileURLToPath(import.meta.url); @@ -68,6 +70,10 @@ webapp.get('/admin', admin) webapp.get('/admin/passwordreset/:username', adminPasswordResetView) webapp.post('/admin/passwordreset/:username', doAdminPasswordReset) +// Admin: Nutzer löschen +webapp.get('/admin/deleteuser/:username', adminDeleteUserView) +webapp.post('/admin/deleteuser/:username', adminDeleteUserPost) + // Einladungslink-System webapp.get('/admin/invite/create', inviteCreateView) webapp.post('/admin/invite/create', inviteCreatePost) diff --git a/views/admin.pug b/views/admin.pug index 9fed534..a983dc0 100644 --- a/views/admin.pug +++ b/views/admin.pug @@ -23,6 +23,8 @@ block inner-content td.px-4.py-2.fs-5.border-b.border-gray-200 a(href=`/admin/passwordreset/${encodeURIComponent(user.username)}`) button.btn.btn-primary.fs-5.m-1 Passwort zurücksetzen + a(href=`/admin/deleteuser/${encodeURIComponent(user.username)}`) + button.btn.btn-danger.fs-5.m-1 Löschen h2.fs-3.mt-3.mb-3 Einladungslinks if invites && invites.length > 0 @@ -31,7 +33,6 @@ block inner-content tr th.px-4.py-2.fs-5.font-semibold.text-gray-700 Einladungscode th.px-4.py-2.fs-5.font-semibold.text-gray-700 Erstellt am - th.px-4.py-2.fs-5.font-semibold.text-gray-700 Erstellt am th.px-4.py-2.fs-5.font-semibold.text-gray-700 Genutzt am th.px-4.py-2.fs-5.font-semibold.text-gray-700 Benutzer th.px-4.py-2.fs-5.font-semibold.text-gray-700 Aktionen diff --git a/views/adminDeleteUser.pug b/views/adminDeleteUser.pug new file mode 100644 index 0000000..17ac7b2 --- /dev/null +++ b/views/adminDeleteUser.pug @@ -0,0 +1,12 @@ +extends neumorphLayout + +block inner-content + h1.fs-1.mb-3 Nutzer löschen + p.fs-4.mb-3 Möchten Sie den Nutzer wirklich löschen? + ul + li UUID: #{user.uuid} + li Benutzername: #{user.username} + li Displayname: #{user.displayname} + form(method="post", action=`/admin/deleteuser/${encodeURIComponent(user.username)}`) + button.btn.btn-danger.fs-5(type="submit") Löschen + a.btn.btn-secondary.fs-5.ms-2(href="/admin") Abbrechen From 468e448d76c0f0a267b4669c585aec9e70434c2f Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 31 Aug 2025 22:13:16 +0200 Subject: [PATCH 13/14] =?UTF-8?q?Verbessere=20das=20Layout=20der=20Nutzerl?= =?UTF-8?q?=C3=B6schansicht=20im=20Admin-Bereich=20mit=20einer=20anspreche?= =?UTF-8?q?nderen=20Kartenstruktur=20und=20verbesserten=20Fehlermeldungen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller.js | 3 ++- views/adminDeleteUser.pug | 30 +++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/controller.js b/src/controller.js index cc1ce15..47e2e38 100644 --- a/src/controller.js +++ b/src/controller.js @@ -1,6 +1,7 @@ import { deletePasskey, getAllUsers, getPasskey, getUserByWebAuthnID, getUserPasskeys, storePasskey, updatePasskeyCounter, createInviteLink, getInviteLink, removeInviteLink, setInviteLinkUsed, - getAllInviteLinks} from "./db.js"; + getAllInviteLinks, + deleteUser} from "./db.js"; import fs from "fs"; import path from "path"; import { diff --git a/views/adminDeleteUser.pug b/views/adminDeleteUser.pug index 17ac7b2..a566dea 100644 --- a/views/adminDeleteUser.pug +++ b/views/adminDeleteUser.pug @@ -1,12 +1,24 @@ extends neumorphLayout block inner-content - h1.fs-1.mb-3 Nutzer löschen - p.fs-4.mb-3 Möchten Sie den Nutzer wirklich löschen? - ul - li UUID: #{user.uuid} - li Benutzername: #{user.username} - li Displayname: #{user.displayname} - form(method="post", action=`/admin/deleteuser/${encodeURIComponent(user.username)}`) - button.btn.btn-danger.fs-5(type="submit") Löschen - a.btn.btn-secondary.fs-5.ms-2(href="/admin") Abbrechen + .card.shadow.p-4.mx-auto(style="max-width: 500px;") + h1.fs-2.mb-4.text-danger.text-center Nutzer löschen + p.fs-5.mb-4.text-center Möchten Sie den Nutzer wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. + .list-group.mb-4 + .list-group-item + strong UUID: + span.ms-2.text-monospace #{user.uuid} + .list-group-item + strong Benutzername: + span.ms-2 #{user.username} + .list-group-item + strong Displayname: + span.ms-2 #{user.displayname} + form(method="post", action=`/admin/deleteuser/${encodeURIComponent(user.username)}`) + .d-flex.justify-content-center.gap-3 + button.btn.btn-danger.fs-5.px-4(type="submit") + i.bi.bi-trash.me-2 + | Löschen + a.btn.btn-secondary.fs-5.px-4(href="/admin") + i.bi.bi-arrow-left.me-2 + | Abbrechen From 02df03e2a15776d011712339999102b206688b6a Mon Sep 17 00:00:00 2001 From: Tim Morgner Date: Sun, 7 Sep 2025 19:29:09 +0200 Subject: [PATCH 14/14] =?UTF-8?q?F=C3=BCge=20Authentifizierungsanfrage-Han?= =?UTF-8?q?dler=20hinzu=20und=20implementiere=20die=20zugeh=C3=B6rige=20Ro?= =?UTF-8?q?ute=20f=C3=BCr=20die=20Gesundheitspr=C3=BCfung.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller.js | 40 ++++++++++++++++++++++++++++++++++++++++ src/webapp.js | 7 +++++++ 2 files changed, 47 insertions(+) diff --git a/src/controller.js b/src/controller.js index 47e2e38..78566b1 100644 --- a/src/controller.js +++ b/src/controller.js @@ -443,6 +443,46 @@ function isLoggedIn(req) { return req.session.uuid != null; } +export async function auth_request(req, res) { + // Helper to build the original URL the user tried to access + const originalUrl = (() => { + // Prefer proxy-provided headers if behind NGINX + const scheme = req.headers['x-forwarded-proto'] || 'https'; + const host = req.headers['x-forwarded-host'] || req.headers.host || ''; + const uri = req.headers['x-original-uri'] || req.originalUrl || '/'; + return `${scheme}://${host}${uri}`; + })(); + + if (!isLoggedIn(req)) { + log("Auth-Request: Nutzer nicht eingeloggt."); + + // Optional hint headers for gateways or API clients + // NGINX won't automatically use these, but you can read them in config + // with auth_request_set $login_url $upstream_http_x_login_url; etc. + const loginUrl = `/login?redirect_uri=${encodeURIComponent(originalUrl)}`; + res.setHeader('X-Login-URL', loginUrl); + res.setHeader('X-Return-To', originalUrl); + res.setHeader('Cache-Control', 'no-store'); + + return res.status(401).end(); + } + + log("Auth-Request: Nutzer ist eingeloggt."); + + const uuid = req.session.uuid; + const username = await getUsername(uuid); + const displayname = await getDisplayname(uuid); + + res.setHeader('X-User-UUID', uuid); + res.setHeader('X-User-Username', username); + res.setHeader('X-User-Displayname', displayname); + + res.setHeader('X-Auth-Method', 'session'); + res.setHeader('Cache-Control', 'no-store'); + + // 204 is common for auth_request success; 200 also works + return res.status(204).end(); +} export async function dashboard(req, res) { const {redirect_uri, error, state} = req.query; log(`Dashboard-View aufgerufen. Session UUID: ${req.session.uuid}, redirect_uri: ${redirect_uri}, error: ${error}, state: ${state}`); diff --git a/src/webapp.js b/src/webapp.js index 1afb3b3..e605e65 100644 --- a/src/webapp.js +++ b/src/webapp.js @@ -16,6 +16,7 @@ import { displaynamechange, registerUser, token, + auth_request, passkeyAdd, passkeyManage, passkeyRemove, @@ -64,6 +65,12 @@ webapp.use(bodyParser.urlencoded({extended: true})); // WEP PAGES // +webapp.get('/health', (req, res) => { + res.status(200).send('OK'); +}); + +webapp.get('/auth_request', auth_request) + webapp.get('/', dashboard) webapp.get('/admin', admin)