Handy-Insel Portal: Startseite, Kopfleiste und Authentik-Anbindung

- Portal mit Kacheln aus Authentik (pro Benutzer gefiltert) und "Aktuelles"
- Web-Component <hi-topbar> als zentraler App-Umschalter für alle Tools
- OIDC-Login über Authentik, zentrales Abmelden
- Setup-Skript für Authentik (Gruppen-Bindungen, Portal-App, Service-Account)
- Deployment auf den Portal-CT per systemd

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kerimatilganandClaude Opus 5.5 committed 2026-09-27 12:18:32 +02:00
commit 6606420b4a
43 files changed
+5577

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
import { Router } from 'express';
import { config, isAllowedUrl } from '../config.js';
import { buildLoginUrl, buildLogoutUrl, handleCallback } from '../oidc.js';
export const authRouter = Router();
/** Rücksprung nur auf das Portal selbst oder erlaubte Tool-Domains. */
function safeReturnTo(raw: unknown): string {
if (typeof raw !== 'string' || !raw) return '/';
if (raw.startsWith('/') && !raw.startsWith('//')) return raw;
return isAllowedUrl(raw) ? raw : '/';
}
authRouter.get('/login', async (req, res, next) => {
try {
const returnTo = safeReturnTo(req.query.returnTo);
if (config.devUser) {
req.session.user = {
sub: `dev-${config.devUser}`,
username: config.devUser,
name: config.devUser.charAt(0).toUpperCase() + config.devUser.slice(1),
email: `${config.devUser}@example.invalid`,
groups: ['HI-Mitarbeiter', 'HI-Admin'],
};
return res.redirect(returnTo);
}
const { url, state, codeVerifier } = await buildLoginUrl();
req.session.login = { state, codeVerifier, returnTo };
req.session.save((err) => (err ? next(err) : res.redirect(url)));
} catch (err) {
next(err);
}
});
authRouter.get('/callback', async (req, res) => {
const pending = req.session.login;
if (!pending) return res.redirect('/auth/login');
try {
const query = req.originalUrl.slice(req.originalUrl.indexOf('?'));
const { user, idToken } = await handleCallback(query, pending);
const returnTo = pending.returnTo;
// Neue Session-ID nach dem Login (Schutz vor Session Fixation).
req.session.regenerate((err) => {
if (err) return res.status(500).send('Anmeldung fehlgeschlagen.');
req.session.user = user;
req.session.idToken = idToken;
req.session.save(() => res.redirect(returnTo));
});
} catch (err) {
console.error('[auth] Callback fehlgeschlagen:', err);
delete req.session.login;
res.status(401).send('Anmeldung fehlgeschlagen. <a href="/auth/login">Erneut versuchen</a>');
}
});
authRouter.get('/logout', async (req, res, next) => {
try {
const url = await buildLogoutUrl(req.session.idToken);
req.session.destroy(() => {
res.clearCookie('hi.portal.sid');
res.redirect(url);
});
} catch (err) {
next(err);
}
});