Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 191 additions & 12 deletions src/controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { deletePasskey, getAllUsers, getPasskey, getUserByWebAuthnID, getUserPasskeys, storePasskey, updatePasskeyCounter } from "./db.js";
import { deletePasskey, getAllUsers, getPasskey, getUserByWebAuthnID, getUserPasskeys, storePasskey, updatePasskeyCounter,
createInviteLink, getInviteLink, removeInviteLink, setInviteLinkUsed,
getAllInviteLinks,
deleteUser} from "./db.js";
import fs from "fs";
import path from "path";
import {
Expand All @@ -16,24 +19,27 @@ 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"});
}
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)
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"});
}
Expand All @@ -45,13 +51,144 @@ 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' });
}

// 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) {
if (!isAdmin(req)) {
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) {
if (!isAdmin(req)) {
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
res.render('inviteRegistration', { title: 'Registrierung über Einladung'});
}

export function inviteRegistrationPost(req, res) {
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(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: { inviteId }, title: "Registrierung über Einladung" });
}
// Validierung
if (!username || !password || !passwordRepeat || !displayname) {
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: { inviteId }, title: "Registrierung über Einladung" });
}
if (!isUsernameValid(username)) {
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: { inviteId }, title: "Registrierung über Einladung" });
}
if (!isDisplaynameValid(displayname)) {
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: { inviteId }, title: "Registrierung über Einladung" });
}
// Registrierung durchführen
const uuid = crypto.randomUUID();
try {
await storeUser(uuid, username, password, displayname);
await setInviteLinkUsed(inviteId, uuid);
return res.redirect('/login');
} catch (err) {
console.log(err);
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: { inviteId }, title: "Registrierung über Einladung" });
});
}

// Admin: Passwort setzen (Formular-Submit)
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"});
}
Expand Down Expand Up @@ -306,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}`);
Expand All @@ -318,10 +495,12 @@ 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 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});
res.render('dashboard', {username: uname, displayname: dname, error: error, title: "Dashboard", state: state, redirect_uri: redirect_uri,is_admin});

}

Expand Down
65 changes: 65 additions & 0 deletions src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,69 @@ 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;
}

// 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;
}
23 changes: 22 additions & 1 deletion src/webapp.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
displaynamechange,
registerUser,
token,
auth_request,
passkeyAdd,
passkeyManage,
passkeyRemove,
Expand All @@ -29,7 +30,9 @@ 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 { adminDeleteUserView, adminDeleteUserPost } from "./controller.js";
import * as bodyParser from "express";

const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -62,12 +65,30 @@ 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)
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)
webapp.get('/admin/invite/delete/:id', inviteDeleteView)
webapp.post('/admin/invite/delete/:id', inviteDeletePost)
webapp.get('/register/invite', inviteRegistrationView)
webapp.post('/register/invite', inviteRegistrationPost)

webapp.get('/info', getInformation)

webapp.get('/register', registerUser)
Expand Down
Loading
Loading