diff --git a/assets/js/main.js b/assets/js/main.js
index 90040ce..23999ae 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -542,8 +542,15 @@
}
// Login Handling
+ $.get('/api/auth/sso/config').done(function(config) {
+ if (config.enabled) {
+ var $ssoBtn = $('
Login mit Keycloak');
+ $('#login-form .actions').append($ssoBtn);
+ }
+ });
+
$.get('/api/auth/me').done(function(u) {
- $('#login-username, #login-password, #login-submit').hide();
+ $('#login-username, #login-password, #login-submit, #sso-login-li').hide();
$('#logout-btn').show().css('display', 'inline-block');
$('#pw-change-section').show();
loadAvailableFolders();
diff --git a/backend/server.js b/backend/server.js
index eb07a22..2907ee2 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -6,6 +6,8 @@ const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const db = require('./database');
+const crypto = require('crypto');
+const { Issuer } = require('openid-client');
const app = express();
const JWT_SECRET = process.env.JWT_SECRET;
@@ -183,6 +185,116 @@ app.get('/api/auth/me', (req, res) => {
res.json(req.user);
});
+// ==================== KEYCLOAK SSO API ====================
+
+let oidcClient = null;
+
+async function getOidcClient() {
+ if (oidcClient) return oidcClient;
+
+ const ssoAuthority = process.env.SSO_AUTHORITY || 'https://auth.mischlabs.de/realms/mischlabs';
+ const ssoClientId = process.env.SSO_CLIENT_ID || 'pocketbase';
+ const ssoClientSecret = process.env.SSO_CLIENT_SECRET;
+ const ssoRedirectUri = process.env.SSO_REDIRECT_URI || 'https://tom.mischlabs.de/api/auth/sso/callback';
+
+ if (!ssoClientSecret) {
+ throw new Error('SSO_CLIENT_SECRET is not configured');
+ }
+
+ const issuer = await Issuer.discover(ssoAuthority);
+ oidcClient = new issuer.Client({
+ client_id: ssoClientId,
+ client_secret: ssoClientSecret,
+ redirect_uris: [ssoRedirectUri],
+ response_types: ['code'],
+ });
+
+ return oidcClient;
+}
+
+app.get('/api/auth/sso/config', (req, res) => {
+ res.json({
+ enabled: !!(process.env.SSO_CLIENT_SECRET && process.env.SSO_CLIENT_ID && process.env.SSO_AUTHORITY),
+ });
+});
+
+app.get('/api/auth/sso/login', async (req, res) => {
+ try {
+ const client = await getOidcClient();
+ const authorizationUrl = client.authorizationUrl({
+ scope: 'openid email profile',
+ state: 'mischlabs-state',
+ });
+ res.redirect(authorizationUrl);
+ } catch (err) {
+ console.error('SSO Login Error:', err);
+ res.status(500).send('SSO Login Initialisierung fehlgeschlagen: ' + err.message);
+ }
+});
+
+app.get('/api/auth/sso/callback', async (req, res) => {
+ try {
+ const client = await getOidcClient();
+ const params = client.callbackParams(req);
+ const redirectUri = process.env.SSO_REDIRECT_URI || 'https://tom.mischlabs.de/api/auth/sso/callback';
+ const tokenSet = await client.callback(redirectUri, params, { state: 'mischlabs-state' });
+ const userinfo = await client.userinfo(tokenSet.access_token);
+
+ const username = userinfo.preferred_username || userinfo.name || userinfo.sub;
+ const email = userinfo.email;
+
+ if (!username) {
+ return res.status(400).send('Kein Benutzername im OIDC Token gefunden.');
+ }
+
+ db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
+ if (err) return res.status(500).send('Datenbankfehler');
+
+ const handleUserLogin = (dbUser) => {
+ const token = jwt.sign(
+ { id: dbUser.id, username: dbUser.username, role: dbUser.role, created_at: dbUser.created_at },
+ JWT_SECRET, { expiresIn: '7d' }
+ );
+ res.cookie('token', token, { httpOnly: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 });
+ res.redirect('/admin.html');
+ };
+
+ if (user) {
+ handleUserLogin(user);
+ } else {
+ const randomPassword = crypto.randomBytes(32).toString('hex');
+ const hash = bcrypt.hashSync(randomPassword, 10);
+
+ db.get('SELECT COUNT(*) as count FROM users', [], (countErr, row) => {
+ let role = 'client';
+ if ((row && row.count === 0) || username.toLowerCase() === 'mrdiderot') {
+ role = 'admin';
+ }
+
+ db.run(
+ 'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)',
+ [username, hash, role],
+ function (insertErr) {
+ if (insertErr) {
+ console.error('Failed JIT provisioning:', insertErr.message);
+ return res.status(500).send('Fehler bei der Benutzererstellung');
+ }
+ const newUserId = this.lastID;
+ db.get('SELECT * FROM users WHERE id = ?', [newUserId], (getErr, newUser) => {
+ if (getErr || !newUser) return res.status(500).send('Fehler beim Laden des neuen Benutzers');
+ handleUserLogin(newUser);
+ });
+ }
+ );
+ });
+ }
+ });
+ } catch (err) {
+ console.error('SSO Callback Error:', err);
+ res.status(500).send('SSO Authentifizierung fehlgeschlagen: ' + err.message);
+ }
+});
+
app.post('/api/auth/change-password', requireAuth, (req, res) => {
const { old_password, new_password } = req.body;
if (!new_password || new_password.length < 4) {
diff --git a/brain.old b/brain.old
new file mode 100644
index 0000000..bb427a4
--- /dev/null
+++ b/brain.old
@@ -0,0 +1,210 @@
+# Brain.md - Projekt-Wissensdatenbank
+
+## Projektziel
+Eine eigene Fotografie-Portfolio-Website basierend auf dem Template von rampatra/photography.
+Keine 1:1 Kopie, sondern eine individuell angepasste Version mit eigenen Anspruechen.
+
+## Besitzer & Kontakt
+- **Name**: Tom Misch
+- **Marke**: Mischkomposition
+- **Domain**: tom.mischlabs.de
+- **E-Mail**: tom@mischlabs.de
+- **Instagram**: @Mischkomposition
+- **Gitea Repo**: https://git.mischlabs.de/MrDiderot/Photography.git
+- **Lokaler Pfad**: d:\Vibecoding\Website\Photography
+
+---
+
+## Technologie-Stack
+
+| Komponente | Technologie |
+|---|---|
+| Static Site Generator | **Jekyll** (Ruby-basiert) |
+| Backend | **Node.js / Express** (Auth, API, Static Serving) |
+| Datenbank | **SQLite** (Nutzerverwaltung, Bildzuweisung) |
+| Templating | Liquid Templates |
+| Styling | SCSS -> kompiliertes CSS |
+| JS Libraries | jQuery, Poptrox (Lightbox), EXIF.js |
+| Build-Tool | Gulp (SCSS kompilieren, JS minifizieren, Bilder resizen) |
+| Hosting | **UGREEN NAS (Docker)** via Node.js Express |
+| Containerisierung | Docker Multi-Stage (Jekyll Build + Node.js Backend) |
+| Auto-Update | **Watchtower** (prueft alle 5 Min auf neue Images) |
+| Fonts | FontAwesome, Google Fonts (Source Sans Pro) |
+| Paketmanager | npm (Node), Bundler (Ruby) |
+
+## Projektstruktur
+
+```
+Photography/
+├── _config.yml # Zentrale Konfiguration (Titel, Social Links, EXIF etc.)
+├── _layouts/
+│ └── default.html # Basis-Layout (Head + Body + Scripts)
+├── _includes/
+│ ├── header.html # Bereich: CSS, Fonts
+│ └── footer.html # Script-Einbindungen (jQuery, Poptrox, EXIF, Main)
+├── index.html # Hauptseite: Header, Galerie-Grid, Footer/About, Login-Panel
+├── admin.html # Admin-Dashboard (Nutzerverwaltung, Bildzuweisung)
+├── backend/
+│ ├── server.js # Express Backend (Auth, API, Static Files, Port 8090)
+│ └── database.js # SQLite Datenbank-Setup (Users, User-Images)
+├── assets/
+│ ├── css/ # Kompilierte CSS-Dateien (.min.css)
+│ ├── sass/ # SCSS Quelldateien
+│ │ ├── base/ # Reset, Typography, Page
+│ │ ├── components/ # Buttons, Forms, Icons, Panels, Poptrox
+│ │ ├── layout/ # Header, Footer, Main, Wrapper
+│ │ └── libs/ # Breakpoints, Mixins, Vars, Functions
+│ ├── js/ # JavaScript (jQuery, Poptrox, EXIF, Main, Admin)
+│ ├── fonts/ # FontAwesome Fonts
+│ └── webfonts/ # FA5 Webfonts
+├── images/
+│ ├── fulls/ # Vollaufloesung Bilder
+│ └── thumbs/ # Thumbnails (512px)
+├── gulpfile.mjs # Build: SCSS->CSS, JS minify, Image resize
+├── package.json # Node Dependencies (Express, SQLite, JWT, bcrypt)
+├── Gemfile # Ruby Dependencies (jekyll)
+├── Dockerfile # Multi-Stage: Jekyll Build + Node.js Runtime
+├── docker-compose.yml # Container + Watchtower
+├── entrypoint.sh # Thumbnail-Generierung + Node.js Start
+└── nginx.conf # (Legacy, nicht mehr aktiv - Express dient statische Dateien)
+```
+
+## Wie die Galerie funktioniert (Dynamisch)
+
+1. **Bilder hochladen**: Fotos in Nextcloud-Ordner `Websitebilder` ablegen
+2. **Volume-Mount**: Docker mountet den Nextcloud-Ordner als `/app/public/images/fulls/` (read-only)
+3. **Thumbnails**: `entrypoint.sh` generiert automatisch 512px Thumbnails via ImageMagick
+4. **Thumbnail-Check**: Alle 60 Sekunden prueft der Container auf neue Bilder
+5. **Express API**: `/images/fulls/` liefert JSON-Liste der Bilddateien
+6. **JavaScript (main.js)**: Fetcht die JSON-Liste und baut dynamisch `` Elemente
+7. **Poptrox-Lightbox**: Klick oeffnet Vollbild + EXIF-Daten (clientseitig via exif.js)
+8. **Download-Button**: In der Lightbox kann jedes Bild heruntergeladen werden
+
+### Zugriffslogik
+- **Nicht eingeloggt**: Nur Root-Bilder (direkt in Websitebilder/) sichtbar
+- **Admin**: Alle Bilder (Root + alle Unterordner) sichtbar
+- **User (permanent)**: Root-Bilder + zugewiesene Unterordner
+- **Client (30 Tage)**: Root-Bilder + zugewiesene Unterordner, Account laeuft nach 30 Tagen ab
+
+### Nextcloud-Integration
+- **Nextcloud-Pfad**: `/volume1/nextcloud/data/Tom/files/Websitebilder`
+- **Mount im Container**: `/app/public/images/fulls:ro`
+- **Thumbnails-Volume**: Docker-Volume `thumbs` (persistent)
+- **Datenbank-Volume**: Docker-Volume `database` (persistent, SQLite)
+- **Workflow**: Bild in Nextcloud hochladen -> max. 60s warten -> Seite neu laden -> fertig
+
+## Login-System & Nutzerverwaltung
+
+### Architektur
+- **Backend**: Node.js/Express auf Port 8090
+- **Auth**: JWT-Token als httpOnly Cookie (7 Tage gueltig)
+- **Datenbank**: SQLite unter `/app/data/database.sqlite`
+- **Passwort-Hashing**: bcryptjs (10 Rounds)
+
+### Rollen
+| Rolle | Beschreibung | Ablauf |
+|---|---|---|
+| `admin` | Voller Zugriff, Nutzerverwaltung, alle Bilder | Nie |
+| `user` | Root-Bilder + zugewiesene Ordner | Nie |
+| `client` | Root-Bilder + zugewiesene Ordner | 30 Tage nach Erstellung |
+
+### Ordner-basierte Bildzuweisung
+- Bilder direkt in `Websitebilder/` = oeffentlich (fuer alle sichtbar)
+- Bilder in Unterordnern (z.B. `Websitebilder/Hochzeit/`) = nur fuer zugewiesene Nutzer
+- Zuweisung erfolgt auf Ordner-Ebene (nicht einzelne Bilder)
+- Tabelle `user_folders` speichert Zuweisungen (user_id, folder_name)
+
+### Initialer Admin
+- Es wird kein festes Standardkonto mehr angelegt.
+- Bestehende Installationen behalten Nutzer und Rollen im persistenten SQLite-Volume.
+- Fuer frische Installationen kann einmalig `INITIAL_ADMIN_USERNAME=MrDiderot` zusammen mit `INITIAL_ADMIN_PASSWORD_HASH` gesetzt werden.
+
+### API-Endpunkte
+| Endpunkt | Methode | Beschreibung |
+|---|---|---|
+| `/api/auth/login` | POST | Login (username, password) |
+| `/api/auth/logout` | POST | Logout (Cookie loeschen) |
+| `/api/auth/me` | GET | Aktueller User |
+| `/api/auth/change-password` | POST | Eigenes Passwort aendern |
+| `/api/admin/users` | GET/POST | Nutzer auflisten / erstellen (mit Rolle) |
+| `/api/admin/users/:id` | DELETE | Nutzer loeschen (nicht sich selbst) |
+| `/api/admin/users/:id/password` | PUT | Admin aendert User-Passwort |
+| `/api/admin/folders` | GET | Verfuegbare Unterordner auflisten |
+| `/api/admin/assign` | GET/POST | Ordner-Zuweisungen anzeigen / setzen |
+| `/images/fulls/` | GET | Bilderliste (gefiltert nach User-Rolle + Ordnern) |
+
+## Wichtige Konfiguration (_config.yml)
+
+- `baseurl`: URL der Website
+- `title`, `subtitle`, `author`: Seitentitel und Autor
+- `header.title/subtitle`: Header-Anzeige
+- `footer.name/bio`: Footer-Infos
+- `social_urls`: Links zu Social Media
+- `exif`: Welche EXIF-Tags angezeigt werden (Model, FNumber, ExposureTime, ISO)
+
+## Build-Befehle
+
+| Befehl | Aktion |
+|---|---|
+| `bundle install` | Ruby Dependencies installieren |
+| `bundle exec jekyll serve` | Lokaler Dev-Server (nur Frontend) |
+| `npm install` | Node Dependencies installieren |
+| `gulp build` | SCSS kompilieren + JS minifizieren |
+| `gulp resize` | Bilder zu fulls (1024px) + thumbs (512px) |
+| `gulp` | Alles (build + resize) |
+
+## Docker / NAS Hosting
+
+- **NAS**: UGREEN mit UGOS PRO
+- **Port**: 8090 (intern und extern)
+- **Domain**: tom.mischlabs.de (via Cloudflare Tunnel)
+
+### Docker-Dateien
+- `Dockerfile` - Multi-Stage: Ruby/Jekyll baut Site -> Node.js Express serviert alles
+- `docker-compose.yml` - Photography Container + Watchtower, Volumes fuer Bilder/Thumbs/DB
+- `entrypoint.sh` - Thumbnail-Generierung + Node.js Server-Start
+- `.dockerignore` - Haelt Build-Context klein (kein .git, node_modules etc.)
+
+### Docker-Befehle (auf NAS via SSH)
+| Befehl | Aktion |
+|---|---|
+| `sudo docker compose up -d` | Container starten |
+| `sudo docker compose down` | Container stoppen |
+| `sudo docker compose logs -f photography` | Logs anzeigen |
+| `sudo docker compose pull && sudo docker compose up -d` | Manuelles Update |
+
+### Deployment-Workflow (automatisch!)
+1. Aenderungen lokal machen (Code, Design)
+2. `gulp build` ausfuehren (JS + CSS minifizieren)
+3. Git commit & push nach master
+4. Gitea Actions baut automatisch Docker Image -> lokales Gitea Container Registry
+5. Watchtower auf NAS erkennt neues Image (alle 5 Min)
+6. Watchtower aktualisiert Container automatisch
+7. Fertig - Website aktualisiert ohne SSH!
+
+## Anpassungs-Roadmap
+
+- [x] Titel, Subtitle, Autor anpassen (Mischkomposition / Tom Misch)
+- [x] Social Media Links aktualisieren (Instagram: @Mischkomposition)
+- [x] Sponsor-Bereich entfernen
+- [x] Kontaktformular auf tom@mischlabs.de konfiguriert (Formsubmit.co)
+- [x] Google Analytics vom Originalautor entfernt
+- [x] Custom Domain: tom.mischlabs.de eingetragen
+- [x] Nextcloud-Integration (Bilder aus Websitebilder-Ordner)
+- [x] Dynamische Galerie (JS statt Jekyll-Loop)
+- [x] Automatische Thumbnail-Generierung
+- [x] Login-System mit JWT + SQLite
+- [x] Admin-Dashboard (Nutzerverwaltung, Bildzuweisung)
+- [x] Kunden-Portal (zugewiesene Bilder + Download)
+- [x] Watchtower fuer automatische Updates
+- [x] 3-Rollen-System (admin/user/client mit 30-Tage-Ablauf)
+- [x] Ordner-basierte Bildzuweisung (Unterordner = private Kundenbilder)
+- [x] Passwort-Aenderung (Self-Service + Admin kann fuer andere aendern)
+- [ ] Footer Bio-Text schreiben (optional)
+- [ ] Farbschema/Design anpassen
+- [ ] Eigene Kategorien/Alben erstellen (Feature-Erweiterung)
+- [ ] Admin-Passwort aendern!
+
+---
+
+*Letzte Aktualisierung: 2026-04-12*
diff --git a/package.json b/package.json
index 6b3e739..c74d6e7 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"cookie-parser": "^1.4.6",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
+ "openid-client": "^5.6.0",
"sqlite3": "^5.1.7"
},
"devDependencies": {