diff --git a/Brain.md b/Brain.md
index a75d6df..dcf4aa7 100644
--- a/Brain.md
+++ b/Brain.md
@@ -20,12 +20,15 @@ Keine 1:1 Kopie, sondern eine individuell angepasste Version mit eigenen Ansprue
| 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 nginx |
-| Containerisierung | Docker Multi-Stage (Jekyll Build + nginx) |
+| 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) |
@@ -37,9 +40,13 @@ Photography/
├── _layouts/
│ └── default.html # Basis-Layout (Head + Body + Scripts)
├── _includes/
-│ ├── header.html #
Bereich: CSS, Fonts, Google Analytics
+│ ├── header.html # Bereich: CSS, Fonts
│ └── footer.html # Script-Einbindungen (jQuery, Poptrox, EXIF, Main)
-├── index.html # Hauptseite: Header, Galerie-Grid, Footer/About
+├── 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
@@ -47,50 +54,82 @@ Photography/
│ │ ├── components/ # Buttons, Forms, Icons, Panels, Poptrox
│ │ ├── layout/ # Header, Footer, Main, Wrapper
│ │ └── libs/ # Breakpoints, Mixins, Vars, Functions
-│ ├── js/ # JavaScript (jQuery, Poptrox, EXIF, Main)
+│ ├── 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
+├── package.json # Node Dependencies (Express, SQLite, JWT, bcrypt)
├── Gemfile # Ruby Dependencies (jekyll)
-└── CNAME # Custom Domain Config
+├── 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 `/images/fulls/` (read-only)
+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. **nginx autoindex**: Liefert JSON-Liste aller Dateien in `/images/fulls/`
+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 / Admin**: Alle Bilder sichtbar (oeffentliche Galerie)
+- **Eingeloggter Nutzer**: Nur zugewiesene Bilder sichtbar + Download
### Nextcloud-Integration
- **Nextcloud-Pfad**: `/volume1/nextcloud/data/Tom/files/Websitebilder`
-- **Mount im Container**: `/usr/share/nginx/html/images/fulls:ro`
+- **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)
+
+### Standard-Admin
+- **Username**: `admin`
+- **Passwort**: `admin123` (SOFORT AENDERN nach erstem Login!)
+- **Rolle**: admin
+
+### 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/admin/users` | GET/POST | Nutzer auflisten / erstellen |
+| `/api/admin/users/:id` | DELETE | Nutzer loeschen |
+| `/api/admin/assign` | GET/POST | Bildzuweisungen anzeigen / setzen |
+| `/images/fulls/` | GET | Bilderliste (gefiltert nach User-Rolle) |
+
## Wichtige Konfiguration (_config.yml)
-- `baseurl`: URL der Website (leer fuer lokal)
+- `baseurl`: URL der Website
- `title`, `subtitle`, `author`: Seitentitel und Autor
- `header.title/subtitle`: Header-Anzeige
-- `footer.name/bio/github`: Footer-Infos
+- `footer.name/bio`: Footer-Infos
- `social_urls`: Links zu Social Media
- `exif`: Welche EXIF-Tags angezeigt werden (Model, FNumber, ExposureTime, ISO)
-- `google_analytics`: Tracking-ID
## Build-Befehle
| Befehl | Aktion |
|---|---|
| `bundle install` | Ruby Dependencies installieren |
-| `bundle exec jekyll serve` | Lokaler Dev-Server |
+| `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) |
@@ -99,45 +138,51 @@ Photography/
## Docker / NAS Hosting
- **NAS**: UGREEN mit UGOS PRO
-- **Port**: 8090 (Port 80 ist belegt)
-- **Zugriff**: `http://NAS-IP:8090` -> spaeter `https://tom.mischlabs.de`
-- **Domain**: tom.mischlabs.de (Reverse Proxy auf NAS einrichten)
+- **Port**: 8090 (intern und extern)
+- **Domain**: tom.mischlabs.de (via Cloudflare Tunnel)
### Docker-Dateien
-- `Dockerfile` - Multi-Stage: Ruby/Jekyll baut Site -> nginx serviert statische Dateien
-- `docker-compose.yml` - Container-Definition, Port 8090:80, Nextcloud-Volume, auto-restart
-- `entrypoint.sh` - Thumbnail-Generierung + periodischer Check (alle 60s)
-- `nginx.conf` - Webserver-Config mit Caching und Sicherheit
+- `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
+### Docker-Befehle (auf NAS via SSH)
| Befehl | Aktion |
|---|---|
-| `docker compose up -d --build` | Container bauen und starten |
-| `docker compose down` | Container stoppen |
-| `docker compose logs -f` | Logs anzeigen |
-| `docker compose up -d --build --force-recreate` | Neu bauen nach Aenderungen |
+| `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
-1. Aenderungen lokal machen (Bilder, Config, Design)
-2. Git commit & push
-3. Auf NAS: `git pull && docker compose up -d --build`
-4. Fertig - Website aktualisiert
+### Deployment-Workflow (automatisch!)
+1. Aenderungen lokal machen (Code, Design)
+2. `gulp build` ausfuehren (JS + CSS minifizieren)
+3. Git commit & push nach master
+4. GitHub Actions baut automatisch Docker Image -> GHCR
+5. Watchtower auf NAS erkennt neues Image (alle 5 Min)
+6. Watchtower aktualisiert Container automatisch
+7. Fertig - Website aktualisiert ohne SSH!
-## Anpassungs-Roadmap (TODO - mit Besitzer besprechen)
+## 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
+- [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
- [ ] Footer Bio-Text schreiben (optional)
- [ ] Farbschema/Design anpassen
- [ ] Eigene Kategorien/Alben erstellen (Feature-Erweiterung)
+- [ ] Admin-Passwort aendern!
---
diff --git a/Dockerfile b/Dockerfile
index d3a2d8b..c65a3c5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,8 +15,8 @@ RUN bundle exec jekyll build
# === Serve-Phase: node.js backend + ImageMagick ===
FROM node:20-alpine
-# Install build dependencies for sqlite3 native bindings on Alpine
-RUN apk add --no-cache imagemagick docker-cli python3 make g++ build-base sqlite-dev
+# ImageMagick fuer Thumbnails, Build-Tools fuer sqlite3 native bindings
+RUN apk add --no-cache imagemagick python3 make g++ build-base sqlite-dev
WORKDIR /app
COPY package*.json ./
diff --git a/Feature-Request.md b/Feature-Request.md
index d91812d..d9f482f 100644
--- a/Feature-Request.md
+++ b/Feature-Request.md
@@ -3,7 +3,7 @@
## Nutzerverwaltung & Login-System
### Beschreibung
-Die Website soll um ein Login-System mit Nutzerverwaltung erweitert werden.
+Die Website wurde um ein Login-System mit Nutzerverwaltung erweitert.
### Anforderungen
@@ -17,23 +17,22 @@ Die Website soll um ein Login-System mit Nutzerverwaltung erweitert werden.
- Nutzer koennen ihre zugewiesenen Bilder anschauen (Galerie + Lightbox)
- Nutzer koennen ihre zugewiesenen Bilder herunterladen
-### Status: Geplant (noch nicht umgesetzt)
+### Status: Umgesetzt
---
-## Admin-Dashboard: One-Click Update
+## Auto-Update via Watchtower
### Beschreibung
-Im Admin-Bereich soll ein Button vorhanden sein, der per Klick die neueste Version aus dem Git-Repository zieht und den Container automatisch aktualisiert. Kein manuelles SSH oder Terminal noetig.
+Watchtower laeuft als separater Docker-Container und prueft alle 5 Minuten auf neue Images.
+Nach einem `git push` baut GitHub Actions das neue Image und Watchtower aktualisiert den Container automatisch.
### Anforderungen
-- Button im Admin-Dashboard: "Website aktualisieren"
-- Zieht automatisch die neueste Version aus GitHub (git pull oder neues Image)
-- Baut den Container neu / startet ihn neu
-- Zeigt Status-Feedback (Erfolg/Fehler)
+- Automatisches Update ohne SSH oder manuellen Eingriff
+- Alte Images werden automatisch aufgeraeumt (WATCHTOWER_CLEANUP)
-### Status: Geplant (noch nicht umgesetzt)
+### Status: Umgesetzt
---
-*Erstellt: 2026-04-12*
+*Aktualisiert: 2026-04-12*
diff --git a/assets/js/main.js b/assets/js/main.js
index a4a216b..5faed73 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -161,7 +161,7 @@
cache: false,
success: function(files) {
if(files.length === 0) {
- $main.html('Bitte logge dich über den Login-Button unten links ein, um deine zugewiesenen Bilder zu sehen.
');
+ $main.html('Noch keine Bilder vorhanden.
');
return;
}
@@ -279,6 +279,9 @@
return template;
}
+ // Galerie laden
+ loadGallery();
+
// Login Handling
$.get('/api/auth/me').done(function(u) {
$('#login-username, #login-password, #login-submit').hide();
diff --git a/assets/js/main.min.js b/assets/js/main.min.js
index 3d5363b..f6ded1e 100644
--- a/assets/js/main.min.js
+++ b/assets/js/main.min.js
@@ -1 +1 @@
-!function(n){var e,o=n(window),t=n("body");n("#wrapper");(breakpoints({xlarge:["1281px","1680px"],large:["981px","1280px"],medium:["737px","980px"],small:["481px","736px"],xsmall:[null,"480px"]}),"ie"==browser.name&&t.addClass("ie"),browser.mobile&&t.addClass("touch"),browser.canUse("transition"))&&(o.on("load",function(){window.setTimeout(function(){t.removeClass("is-preload")},100)}),o.on("resize",function(){window.clearTimeout(e),t.addClass("is-resizing"),e=window.setTimeout(function(){t.removeClass("is-resizing")},100)}));o.scrollTop(0);var i=n(".panel");i.each(function(){var e=n(this),o=n('[href="#'+e.attr("id")+'"]');n('').appendTo(e).on("click",function(n){e.trigger("---hide")}),e.on("click",function(n){n.stopPropagation()}).on("---toggle",function(){e.hasClass("active")?e.triggerHandler("---hide"):e.triggerHandler("---show")}).on("---show",function(){t.hasClass("content-active")&&i.trigger("---hide"),e.addClass("active"),o.addClass("active"),t.addClass("content-active")}).on("---hide",function(){e.removeClass("active"),o.removeClass("active"),t.removeClass("content-active")}),o.removeAttr("href").css("cursor","pointer").on("click",function(n){n.preventDefault(),n.stopPropagation(),e.trigger("---toggle")})}),t.on("click",function(n){t.hasClass("content-active")&&(n.preventDefault(),n.stopPropagation(),i.trigger("---hide"))}),o.on("keyup",function(n){27==n.keyCode&&t.hasClass("content-active")&&(n.preventDefault(),n.stopPropagation(),i.trigger("---hide"))}),n("#header").find("a").each(function(){var e=n(this),o=e.attr("href");o&&"#"!=o.charAt(0)&&e.removeAttr("href").css("cursor","pointer").on("click",function(n){n.preventDefault(),n.stopPropagation(),window.location.href=o})}),n("#footer").find(".copyright").each(function(){var e=n(this),o=e.parent(),t=o.parent().children().last();breakpoints.on("<=medium",function(){e.appendTo(t)}),breakpoints.on(">medium",function(){e.appendTo(o)})});n("#main");n.get("/api/auth/me").done(function(e){n("#login-username, #login-password, #login-submit").hide(),n("#logout-btn").show().css("display","inline-block"),"admin"===e.role&&n("#admin-link").html('Admin-Dashboard'),n("#login-nav").html('Account ')}),n("#login-form").submit(function(e){e.preventDefault(),n.post("/api/auth/login",{username:n("#login-username").val(),password:n("#login-password").val()}).done(function(){window.location.reload()}).fail(function(n){alert("Login fehlgeschlagen. Bitte Zugangsdaten ueberpruefen.")})}),n("#logout-btn").click(function(e){e.preventDefault(),n.post("/api/auth/logout").done(function(){window.location.reload()})})}(jQuery);
\ No newline at end of file
+!function(e){var n,a=e(window),o=e("body");e("#wrapper");(breakpoints({xlarge:["1281px","1680px"],large:["981px","1280px"],medium:["737px","980px"],small:["481px","736px"],xsmall:[null,"480px"]}),"ie"==browser.name&&o.addClass("ie"),browser.mobile&&o.addClass("touch"),browser.canUse("transition"))&&(a.on("load",function(){window.setTimeout(function(){o.removeClass("is-preload")},100)}),a.on("resize",function(){window.clearTimeout(n),o.addClass("is-resizing"),n=window.setTimeout(function(){o.removeClass("is-resizing")},100)}));a.scrollTop(0);var t=e(".panel");t.each(function(){var n=e(this),a=e('[href="#'+n.attr("id")+'"]');e('').appendTo(n).on("click",function(e){n.trigger("---hide")}),n.on("click",function(e){e.stopPropagation()}).on("---toggle",function(){n.hasClass("active")?n.triggerHandler("---hide"):n.triggerHandler("---show")}).on("---show",function(){o.hasClass("content-active")&&t.trigger("---hide"),n.addClass("active"),a.addClass("active"),o.addClass("content-active")}).on("---hide",function(){n.removeClass("active"),a.removeClass("active"),o.removeClass("content-active")}),a.removeAttr("href").css("cursor","pointer").on("click",function(e){e.preventDefault(),e.stopPropagation(),n.trigger("---toggle")})}),o.on("click",function(e){o.hasClass("content-active")&&(e.preventDefault(),e.stopPropagation(),t.trigger("---hide"))}),a.on("keyup",function(e){27==e.keyCode&&o.hasClass("content-active")&&(e.preventDefault(),e.stopPropagation(),t.trigger("---hide"))}),e("#header").find("a").each(function(){var n=e(this),a=n.attr("href");a&&"#"!=a.charAt(0)&&n.removeAttr("href").css("cursor","pointer").on("click",function(e){e.preventDefault(),e.stopPropagation(),window.location.href=a})}),e("#footer").find(".copyright").each(function(){var n=e(this),a=n.parent(),o=a.parent().children().last();breakpoints.on("<=medium",function(){n.appendTo(o)}),breakpoints.on(">medium",function(){n.appendTo(a)})});var i=e("#main"),r={},s=["jpg","jpeg","png","gif","webp"];function l(n){var a=e("#main").data("exif"),o="";for(var t in a){var i=a[t],r=EXIF.getTag(n,i.tag);void 0!==r&&(o+=' '+r+" ")}return o}e.ajax({url:"/images/fulls/",dataType:"json",cache:!1,success:function(n){if(0!==n.length){var a=n.filter(function(e){if("file"!==e.type)return!1;var n=e.name.split(".").pop().toLowerCase();return-1!==s.indexOf(n)});0!==a.length?(a.sort(function(e,n){return e.name.localeCompare(n.name)}),a.forEach(function(n){var a=e(''),o=e(''),t=e('
');o.append(t),a.append(o),i.append(a)}),i.children(".thumb").each(function(){var n,a=e(this),o=a.find(".image"),t=o.children("img");0!=o.length&&(o.css("background-image","url("+t.attr("src")+")"),(n=t.data("position"))&&o.css("background-position",n),t.hide(),t[0].addEventListener("load",function(){EXIF.getData(t[0],function(){r[t.data("name")]=l(this)})}))}),i.poptrox({baseZIndex:2e4,caption:function(e){var n=e.children("img"),a=r[n.data("name")];void 0===a&&EXIF.getData(n[0],function(){a=r[n.data("name")]=l(this)});var o='
Bild Speichern ';return void 0!==a?""+a+o+"
":""+o+"
"},fadeSpeed:300,onPopupClose:function(){o.removeClass("modal-active")},onPopupOpen:function(){o.addClass("modal-active")},overlayOpacity:0,popupCloserText:"",popupHeight:150,popupLoaderText:"",popupSpeed:300,popupWidth:150,selector:".thumb > a.image",usePopupCaption:!0,usePopupCloser:!0,usePopupDefaultStyling:!1,usePopupForceClose:!0,usePopupLoader:!0,usePopupNav:!0,windowMargin:50}),breakpoints.on("<=xsmall",function(){i[0]._poptrox.windowMargin=0}),breakpoints.on(">xsmall",function(){i[0]._poptrox.windowMargin=50})):i.html('Es wurden noch keine passenden Bilder für diesen Account hochgeladen oder zugewiesen.
')}else i.html('Noch keine Bilder vorhanden.
')},error:function(){i.html('Keine Bilder gefunden. Lade Bilder in deinen Nextcloud-Ordner hoch.
')}}),e.get("/api/auth/me").done(function(n){e("#login-username, #login-password, #login-submit").hide(),e("#logout-btn").show().css("display","inline-block"),"admin"===n.role&&e("#admin-link").html('Admin-Dashboard'),e("#login-nav").html('Account ')}),e("#login-form").submit(function(n){n.preventDefault(),e.post("/api/auth/login",{username:e("#login-username").val(),password:e("#login-password").val()}).done(function(){window.location.reload()}).fail(function(e){alert("Login fehlgeschlagen. Bitte Zugangsdaten ueberpruefen.")})}),e("#logout-btn").click(function(n){n.preventDefault(),e.post("/api/auth/logout").done(function(){window.location.reload()})})}(jQuery);
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
index dcc8c66..0e288c3 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -11,6 +11,7 @@ const app = express();
const JWT_SECRET = process.env.JWT_SECRET || 'mischkomposition-super-secret-key-change-me';
app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// Middleware to verify JWT cookie
@@ -63,37 +64,38 @@ app.get('/api/auth/me', (req, res) => {
// ==================== IMAGE PROXY ====================
-// We must override the nginx `/images/fulls/` JSON behavior here:
+// Bilder-Listing: Oeffentlich = alle Bilder, eingeloggt = zugewiesene Bilder
app.get('/images/fulls/', (req, res) => {
- if (!req.user) return res.json([]); // Not logged in -> empty gallery
-
const fullsDir = path.join(__dirname, '../public/images/fulls');
-
+
fs.readdir(fullsDir, (err, files) => {
if (err) return res.json([]);
- let imageFiles = files.filter(f => f.match(/\.(jpg|jpeg|png|gif|webp)$/i));
+ const imageFiles = files.filter(f => f.match(/\.(jpg|jpeg|png|gif|webp)$/i));
- if (req.user.role === 'admin') {
- // Admin sees all
- const result = imageFiles.map(name => {
- const stats = fs.statSync(path.join(fullsDir, name));
- return { name, type: 'file', mtime: stats.mtime.toUTCString(), size: stats.size };
- });
- return res.json(result);
- } else {
- // Normal user sees only assigned
- db.all('SELECT image_name FROM user_images WHERE user_id = ?', [req.user.id], (err, rows) => {
- if (err) return res.json([]);
- const assignedNames = rows.map(r => r.image_name);
- const assignedFiles = imageFiles.filter(name => assignedNames.includes(name));
- const result = assignedFiles.map(name => {
+ function buildResult(names) {
+ return names.map(name => {
+ try {
const stats = fs.statSync(path.join(fullsDir, name));
return { name, type: 'file', mtime: stats.mtime.toUTCString(), size: stats.size };
- });
- return res.json(result);
+ } catch (e) {
+ return { name, type: 'file' };
+ }
});
}
+
+ // Nicht eingeloggt oder Admin: alle Bilder anzeigen
+ if (!req.user || req.user.role === 'admin') {
+ return res.json(buildResult(imageFiles));
+ }
+
+ // Normaler User: nur zugewiesene Bilder
+ db.all('SELECT image_name FROM user_images WHERE user_id = ?', [req.user.id], (err, rows) => {
+ if (err) return res.json([]);
+ const assignedNames = rows.map(r => r.image_name);
+ const assignedFiles = imageFiles.filter(name => assignedNames.includes(name));
+ return res.json(buildResult(assignedFiles));
+ });
});
});
@@ -150,19 +152,18 @@ app.post('/api/admin/assign', requireAdmin, (req, res) => {
});
app.post('/api/admin/update', requireAdmin, (req, res) => {
- console.log("Triggering Docker Compose Update...");
-
- // Wir fuehren den Docker compose pull/down/up aus.
- // ACHTUNG: Der Server stuertzt danach ohnehin im Container ab (da er neu gestartet wird).
- const cmd = "docker compose pull && docker compose down && docker compose up -d";
-
- // Return early to ensure client gets success BEFORE container dies
- res.json({ success: true, message: 'Update gestartet. Container wird neu gestartet...' });
-
+ console.log("Triggering Docker Update...");
+
+ // Neues Image pullen und Container neu starten (ohne docker-compose)
+ const image = 'ghcr.io/kroonk/photography:latest';
+ const cmd = `docker pull ${image} && docker stop photography-website && docker rm photography-website`;
+
+ res.json({ success: true, message: 'Update gestartet. Container wird neu gestartet... Seite laedt in 30 Sekunden neu.' });
+
setTimeout(() => {
- exec(cmd, { cwd: '/app' }, (err, stdout, stderr) => {
+ exec(cmd, (err, stdout, stderr) => {
if (err) console.error("Update failed:", err);
- else console.log("Update success:", stdout);
+ else console.log("Update triggered:", stdout);
});
}, 1000);
});
diff --git a/docker-compose.yml b/docker-compose.yml
index 79fb9ef..c30d5b7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -9,10 +9,20 @@ services:
- /volume1/nextcloud/data/Tom/files/Websitebilder:/app/public/images/fulls:ro
# Thumbnails werden automatisch generiert und hier gespeichert
- thumbs:/app/public/images/thumbs
- # SQLite Database Volume
+ # SQLite Database (persistent)
- database:/app/data
- # Docker Socket fuer One-Click Update
+ restart: unless-stopped
+
+ # Watchtower: Prueft automatisch auf neue Docker-Images und aktualisiert den Container
+ watchtower:
+ image: containrrr/watchtower
+ container_name: watchtower
+ volumes:
- /var/run/docker.sock:/var/run/docker.sock
+ environment:
+ - WATCHTOWER_CLEANUP=true
+ - WATCHTOWER_POLL_INTERVAL=300
+ - WATCHTOWER_LABEL_ENABLE=false
restart: unless-stopped
volumes:
diff --git a/runs.json b/runs.json
deleted file mode 100644
index 76cb3e3..0000000
Binary files a/runs.json and /dev/null differ