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": {