Compare commits
10 Commits
567cb67763
...
5c81e93455
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c81e93455 | ||
|
|
eaa5945611 | ||
|
|
33db10f32d | ||
|
|
ab06284beb | ||
|
|
67044add84 | ||
|
|
61bd512cd8 | ||
|
|
941d041b5d | ||
|
|
a4f3cf738c | ||
|
|
15e74f42c2 | ||
|
|
7509253ee7 |
132
Brain.md
132
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 # <head> Bereich: CSS, Fonts, Google Analytics
|
||||
│ ├── header.html # <head> 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,100 @@ 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 `<article class="thumb">` 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**: `/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)
|
||||
|
||||
### 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)
|
||||
|
||||
### 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/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 (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 +156,54 @@ 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
|
||||
- [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!
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 ./
|
||||
@@ -33,6 +33,6 @@ RUN chmod +x /entrypoint.sh
|
||||
# Verzeichnisse anlegen
|
||||
RUN mkdir -p /app/public/images/fulls /app/public/images/thumbs /app/data
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8090
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
|
||||
@@ -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,48 @@ 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
|
||||
## Rollen-System & Ordner-Zuweisung
|
||||
|
||||
### 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.
|
||||
Erweitertes Nutzermanagement mit 3 Rollen und ordnerbasierter Bildzuweisung.
|
||||
|
||||
### 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)
|
||||
|
||||
### Status: Geplant (noch nicht umgesetzt)
|
||||
**3 Rollen:**
|
||||
- Admin: Voller Zugriff auf alle Bilder und Nutzerverwaltung
|
||||
- User: Permanenter Account, sieht Root-Bilder + zugewiesene Ordner
|
||||
- Client: Temporaerer Account (30 Tage), sieht Root-Bilder + zugewiesene Ordner
|
||||
|
||||
**Ordner-basierte Zuweisung:**
|
||||
- Bilder direkt im Websitebilder-Ordner = oeffentlich fuer alle
|
||||
- Unterordner (z.B. Hochzeit, Portrait) = nur fuer zugewiesene Nutzer
|
||||
- Admin weist ganze Ordner einem Nutzer zu (nicht einzelne Bilder)
|
||||
|
||||
**Passwort-Aenderung:**
|
||||
- Jeder Nutzer kann sein eigenes Passwort aendern
|
||||
- Admin kann Passwoerter beliebiger Nutzer aendern
|
||||
- Neue Admins erstellen und bestehende loeschen moeglich
|
||||
|
||||
### Status: Umgesetzt
|
||||
|
||||
---
|
||||
|
||||
*Erstellt: 2026-04-12*
|
||||
## Auto-Update via Watchtower
|
||||
|
||||
### Beschreibung
|
||||
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
|
||||
- Automatisches Update ohne SSH oder manuellen Eingriff
|
||||
- Alte Images werden automatisch aufgeraeumt (WATCHTOWER_CLEANUP)
|
||||
|
||||
### Status: Umgesetzt
|
||||
|
||||
---
|
||||
|
||||
*Aktualisiert: 2026-04-12*
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
|
||||
<!-- Load resources -->
|
||||
<link rel="stylesheet" href="assets/css/main.min.css" />
|
||||
<noscript><link rel="stylesheet" href="assets/css/noscript.min.css" /></noscript>
|
||||
<link rel="stylesheet" id="theme-style" href="assets/css/main.min.css" />
|
||||
<script>
|
||||
var theme = localStorage.getItem('mischkomposition_theme');
|
||||
if (theme === 'light') {
|
||||
document.getElementById('theme-style').href = 'assets/css/light.min.css';
|
||||
}
|
||||
</script>
|
||||
<noscript><link rel="stylesheet" href="assets/css/main.min.css" /></noscript>
|
||||
<link rel="stylesheet" href="assets/css/custom.min.css" />
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/fontawesome-all.min.css">
|
||||
|
||||
76
admin.html
76
admin.html
@@ -7,48 +7,88 @@
|
||||
<link rel="stylesheet" href="/assets/css/main.min.css" />
|
||||
<style>
|
||||
.admin-section { background: rgba(0,0,0,0.5); padding: 2em; margin-bottom: 2em; border-radius: 4px; }
|
||||
.img-check { display: inline-block; width: 100px; margin: 5px; text-align: center; }
|
||||
.img-check img { width: 100px; height: 100px; object-fit: cover; }
|
||||
.folder-check { display: inline-block; margin: 5px 10px; padding: 8px 12px; background: rgba(255,255,255,0.1); border-radius: 4px; }
|
||||
.folder-check label { cursor: pointer; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.2); }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 0.8em; }
|
||||
.badge-admin { background: #e44d26; }
|
||||
.badge-user { background: #2196F3; }
|
||||
.badge-client { background: #4CAF50; }
|
||||
.badge-expired { background: #666; }
|
||||
.btn-small { padding: 4px 10px; font-size: 0.85em; margin: 2px; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="is-preload">
|
||||
<div id="wrapper">
|
||||
<header id="header">
|
||||
<h1><a href="/index.html"><strong>Mischkomposition</strong> Admin</a></h1>
|
||||
<nav><ul><li><a href="#" id="logout-btn">Logout <i class="fa fa-sign-out"></i></a></li></ul></nav>
|
||||
<nav><ul>
|
||||
<li><a href="#" id="logout-btn">Logout <i class="fa fa-sign-out"></i></a></li>
|
||||
</ul></nav>
|
||||
</header>
|
||||
|
||||
<div id="main" style="display:block; padding: 2em; max-width: 900px; margin: 0 auto; width: 100%;">
|
||||
<div id="main" style="display:block; padding: 2em; max-width: 1000px; margin: 0 auto; width: 100%;">
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>System</h2>
|
||||
<p>Aktualisiert den Docker-Container aus dem GitHub Repository. Container startet danach neu.</p>
|
||||
<button id="update-btn" class="primary">Website Aktualisieren (Docker Pull)</button>
|
||||
<p id="update-status" style="margin-top: 10px;"></p>
|
||||
<h2>Nutzer erstellen</h2>
|
||||
<form id="create-user-form" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
||||
<div>
|
||||
<label>Username</label><br/>
|
||||
<input type="text" id="new-username" placeholder="Nutzername" required />
|
||||
</div>
|
||||
<div>
|
||||
<label>Passwort</label><br/>
|
||||
<input type="password" id="new-password" placeholder="Passwort" required />
|
||||
</div>
|
||||
<div>
|
||||
<label>Rolle</label><br/>
|
||||
<select id="new-role">
|
||||
<option value="client">Client (30 Tage)</option>
|
||||
<option value="user">User (permanent)</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">Erstellen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Nutzerverwaltung</h2>
|
||||
<form id="create-user-form" style="display:flex; gap:10px; margin-bottom:1em;">
|
||||
<input type="text" id="new-username" placeholder="Nutzername" required />
|
||||
<input type="password" id="new-password" placeholder="Passwort" required />
|
||||
<button type="submit">Erstellen</button>
|
||||
</form>
|
||||
<table>
|
||||
<thead><tr><th>ID</th><th>Username</th><th>Aktion</th></tr></thead>
|
||||
<thead><tr><th>ID</th><th>Username</th><th>Rolle</th><th>Erstellt</th><th>Aktionen</th></tr></thead>
|
||||
<tbody id="users-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>BildZuweisung</h2>
|
||||
<h2>Ordner-Zuweisung</h2>
|
||||
<p>Weise Unterordner aus dem Websitebilder-Verzeichnis einem Nutzer zu.</p>
|
||||
<select id="user-select" style="margin-bottom: 1em;">
|
||||
<option value="">Nutzer wählen...</option>
|
||||
<option value="">Nutzer waehlen...</option>
|
||||
</select>
|
||||
<div id="image-checkboxes" style="display:flex; flex-wrap:wrap; gap:10px;"></div>
|
||||
<button id="save-assignments" style="margin-top:1em;">Zuweisung Speichern</button>
|
||||
<div id="folder-checkboxes" style="display:flex; flex-wrap:wrap; gap:5px; margin-bottom:1em;"></div>
|
||||
<button id="save-assignments">Zuweisung Speichern</button>
|
||||
<span id="assign-status" style="margin-left:10px;"></span>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Eigenes Passwort aendern</h2>
|
||||
<form id="admin-pw-form" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
||||
<div>
|
||||
<label>Altes Passwort</label><br/>
|
||||
<input type="password" id="admin-pw-old" required />
|
||||
</div>
|
||||
<div>
|
||||
<label>Neues Passwort</label><br/>
|
||||
<input type="password" id="admin-pw-new" required />
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">Aendern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
1
assets/css/light.min.css
vendored
Normal file
1
assets/css/light.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
assets/css/main.min.css
vendored
2
assets/css/main.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,101 +1,167 @@
|
||||
$(document).ready(function() {
|
||||
// Check if admin
|
||||
// Auth check
|
||||
$.get('/api/auth/me')
|
||||
.done(function(user) {
|
||||
if(user.role !== 'admin') {
|
||||
alert("Kein Zugriff!");
|
||||
alert('Kein Zugriff!');
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
loadUsers();
|
||||
loadAssignmentImages();
|
||||
loadFolders();
|
||||
$('body').removeClass('is-preload');
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
window.location.href = '/';
|
||||
});
|
||||
.fail(function() { window.location.href = '/'; });
|
||||
|
||||
// Logout
|
||||
$('#logout-btn').click(function(e) {
|
||||
e.preventDefault();
|
||||
$.post('/api/auth/logout').done(function() { window.location.href = '/'; });
|
||||
});
|
||||
|
||||
// ==================== NUTZER ====================
|
||||
|
||||
function loadUsers() {
|
||||
$.get('/api/admin/users').done(function(users) {
|
||||
$('#users-tbody').empty();
|
||||
$('#user-select').html('<option value="">Nutzer wählen...</option>');
|
||||
$('#user-select').html('<option value="">Nutzer waehlen...</option>');
|
||||
|
||||
users.forEach(function(u) {
|
||||
var badge = '<span class="badge badge-' + u.role + '">' + u.role + '</span>';
|
||||
var created = u.created_at ? new Date(u.created_at).toLocaleDateString('de-DE') : '-';
|
||||
var info = created;
|
||||
|
||||
if (u.role === 'client' && u.expires_at) {
|
||||
var expDate = new Date(u.expires_at).toLocaleDateString('de-DE');
|
||||
if (u.expired) {
|
||||
badge = '<span class="badge badge-expired">abgelaufen</span>';
|
||||
info = 'Abgelaufen: ' + expDate;
|
||||
} else {
|
||||
info = 'Bis: ' + expDate;
|
||||
}
|
||||
}
|
||||
|
||||
var actions = '<button class="btn-small" onclick="changePassword(' + u.id + ', \'' + u.username.replace(/'/g, "\\'") + '\')">PW</button>';
|
||||
actions += '<button class="btn-small" onclick="deleteUser(' + u.id + ', \'' + u.username.replace(/'/g, "\\'") + '\')">X</button>';
|
||||
|
||||
$('#users-tbody').append(
|
||||
'<tr>' +
|
||||
'<td>' + u.id + '</td>' +
|
||||
'<td>' + u.username + ' ' + (u.role === 'admin' ? '(Admin)' : '') + '</td>' +
|
||||
'<td>' + (u.id !== 1 ? '<button onclick="deleteUser(' + u.id + ')">Löschen</button>' : '') + '</td>' +
|
||||
'</tr>'
|
||||
'<tr><td>' + u.id + '</td><td>' + u.username + ' ' + badge +
|
||||
'</td><td>' + u.role + '</td><td>' + info +
|
||||
'</td><td>' + actions + '</td></tr>'
|
||||
);
|
||||
if(u.role !== 'admin') {
|
||||
$('#user-select').append('<option value="' + u.id + '">' + u.username + '</option>');
|
||||
|
||||
// Dropdown fuer Ordner-Zuweisung (nur non-admin)
|
||||
if (u.role !== 'admin') {
|
||||
$('#user-select').append('<option value="' + u.id + '">' + u.username + ' (' + u.role + ')</option>');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.deleteUser = function(id) {
|
||||
if(confirm("Nutzer loeschen?")) {
|
||||
$.ajax({url: '/api/admin/users/'+id, type: 'DELETE'}).done(loadUsers);
|
||||
}
|
||||
};
|
||||
|
||||
// Nutzer erstellen
|
||||
$('#create-user-form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
$.post('/api/admin/users', { username: $('#new-username').val(), password: $('#new-password').val(), role: 'user' })
|
||||
.done(function() {
|
||||
$.post('/api/admin/users', {
|
||||
username: $('#new-username').val(),
|
||||
password: $('#new-password').val(),
|
||||
role: $('#new-role').val()
|
||||
}).done(function() {
|
||||
$('#new-username').val('');
|
||||
$('#new-password').val('');
|
||||
loadUsers();
|
||||
}).fail(function(err) { alert("Fehler: " + err.responseJSON.error); });
|
||||
}).fail(function(err) {
|
||||
alert('Fehler: ' + (err.responseJSON ? err.responseJSON.error : 'Unbekannt'));
|
||||
});
|
||||
});
|
||||
|
||||
function loadAssignmentImages() {
|
||||
$.get('/images/fulls/').done(function(files) {
|
||||
$('#image-checkboxes').empty();
|
||||
files.forEach(function(f) {
|
||||
$('#image-checkboxes').append(
|
||||
'<label class="img-check">' +
|
||||
'<img src="/images/thumbs/' + encodeURIComponent(f.name) + '" />' +
|
||||
'<br/><input type="checkbox" value="' + f.name + '" class="img-cb"/>' +
|
||||
'</label>'
|
||||
// Nutzer loeschen
|
||||
window.deleteUser = function(id, name) {
|
||||
if (confirm('Nutzer "' + name + '" wirklich loeschen?')) {
|
||||
$.ajax({ url: '/api/admin/users/' + id, type: 'DELETE' })
|
||||
.done(loadUsers)
|
||||
.fail(function(err) { alert(err.responseJSON ? err.responseJSON.error : 'Fehler'); });
|
||||
}
|
||||
};
|
||||
|
||||
// Passwort aendern (Admin setzt fuer anderen User)
|
||||
window.changePassword = function(id, name) {
|
||||
var newPw = prompt('Neues Passwort fuer "' + name + '":');
|
||||
if (!newPw) return;
|
||||
$.ajax({
|
||||
url: '/api/admin/users/' + id + '/password',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ new_password: newPw })
|
||||
}).done(function() {
|
||||
alert('Passwort geaendert!');
|
||||
}).fail(function(err) {
|
||||
alert(err.responseJSON ? err.responseJSON.error : 'Fehler');
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== ORDNER-ZUWEISUNG ====================
|
||||
|
||||
function loadFolders() {
|
||||
$.get('/api/admin/folders').done(function(folders) {
|
||||
$('#folder-checkboxes').empty();
|
||||
if (folders.length === 0) {
|
||||
$('#folder-checkboxes').html('<p>Keine Unterordner in Websitebilder gefunden. Erstelle Ordner in Nextcloud.</p>');
|
||||
return;
|
||||
}
|
||||
folders.forEach(function(f) {
|
||||
$('#folder-checkboxes').append(
|
||||
'<div class="folder-check"><label>' +
|
||||
'<input type="checkbox" value="' + f + '" class="folder-cb"/> ' +
|
||||
'<i class="fa fa-folder"></i> ' + f +
|
||||
'</label></div>'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Bei User-Auswahl: zugewiesene Ordner laden
|
||||
$('#user-select').change(function() {
|
||||
var uid = $(this).val();
|
||||
$('.img-cb').prop('checked', false);
|
||||
if(!uid) return;
|
||||
$.get('/api/admin/assign').done(function(assignments) {
|
||||
assignments.filter(function(a) { return a.user_id == uid; }).forEach(function(a) {
|
||||
$('.img-cb[value="' + a.image_name + '"]').prop('checked', true);
|
||||
$('.folder-cb').prop('checked', false);
|
||||
if (!uid) return;
|
||||
$.get('/api/admin/assign?user_id=' + uid).done(function(assignedFolders) {
|
||||
assignedFolders.forEach(function(f) {
|
||||
$('.folder-cb[value="' + f + '"]').prop('checked', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Zuweisung speichern
|
||||
$('#save-assignments').click(function() {
|
||||
var uid = $('#user-select').val();
|
||||
if(!uid) return alert('Bitte Nutzer waehlen');
|
||||
if (!uid) return alert('Bitte Nutzer waehlen');
|
||||
var selected = [];
|
||||
$('.img-cb:checked').each(function() { selected.push($(this).val()); });
|
||||
$('.folder-cb:checked').each(function() { selected.push($(this).val()); });
|
||||
|
||||
$.post('/api/admin/assign', { user_id: uid, image_names: selected })
|
||||
.done(function() { alert("Zuweisung gespeichert!"); });
|
||||
$.ajax({
|
||||
url: '/api/admin/assign',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({ user_id: uid, folders: selected })
|
||||
}).done(function() {
|
||||
$('#assign-status').text('Gespeichert!').fadeIn().delay(2000).fadeOut();
|
||||
}).fail(function() {
|
||||
alert('Fehler beim Speichern');
|
||||
});
|
||||
});
|
||||
|
||||
$('#update-btn').click(function() {
|
||||
if(!confirm("Achtung: Dies startet den Server auf dem NAS neu. Fortfahren?")) return;
|
||||
$('#update-status').text('Update laeuft...');
|
||||
$.post('/api/admin/update').done(function(res) {
|
||||
$('#update-status').text(res.message);
|
||||
setTimeout(function() { window.location.reload(); }, 5000);
|
||||
// ==================== EIGENES PASSWORT ====================
|
||||
|
||||
$('#admin-pw-form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
$.post('/api/auth/change-password', {
|
||||
old_password: $('#admin-pw-old').val(),
|
||||
new_password: $('#admin-pw-new').val()
|
||||
}).done(function() {
|
||||
alert('Passwort geaendert!');
|
||||
$('#admin-pw-old, #admin-pw-new').val('');
|
||||
}).fail(function(err) {
|
||||
alert(err.responseJSON ? err.responseJSON.error : 'Fehler');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
2
assets/js/admin.min.js
vendored
2
assets/js/admin.min.js
vendored
@@ -1 +1 @@
|
||||
$(document).ready(function(){function e(){$.get("/api/admin/users").done(function(e){$("#users-tbody").empty(),$("#user-select").html('<option value="">Nutzer wählen...</option>'),e.forEach(function(e){$("#users-tbody").append("<tr><td>"+e.id+"</td><td>"+e.username+" "+("admin"===e.role?"(Admin)":"")+"</td><td>"+(1!==e.id?'<button onclick="deleteUser('+e.id+')">Löschen</button>':"")+"</td></tr>"),"admin"!==e.role&&$("#user-select").append('<option value="'+e.id+'">'+e.username+"</option>")})})}$.get("/api/auth/me").done(function(n){"admin"!==n.role?(alert("Kein Zugriff!"),window.location.href="/"):(e(),$.get("/images/fulls/").done(function(e){$("#image-checkboxes").empty(),e.forEach(function(e){$("#image-checkboxes").append('<label class="img-check"><img src="/images/thumbs/'+encodeURIComponent(e.name)+'" /><br/><input type="checkbox" value="'+e.name+'" class="img-cb"/></label>')})}),$("body").removeClass("is-preload"))}).fail(function(){window.location.href="/"}),$("#logout-btn").click(function(e){e.preventDefault(),$.post("/api/auth/logout").done(function(){window.location.href="/"})}),window.deleteUser=function(n){confirm("Nutzer loeschen?")&&$.ajax({url:"/api/admin/users/"+n,type:"DELETE"}).done(e)},$("#create-user-form").submit(function(n){n.preventDefault(),$.post("/api/admin/users",{username:$("#new-username").val(),password:$("#new-password").val(),role:"user"}).done(function(){$("#new-username").val(""),$("#new-password").val(""),e()}).fail(function(e){alert("Fehler: "+e.responseJSON.error)})}),$("#user-select").change(function(){var e=$(this).val();$(".img-cb").prop("checked",!1),e&&$.get("/api/admin/assign").done(function(n){n.filter(function(n){return n.user_id==e}).forEach(function(e){$('.img-cb[value="'+e.image_name+'"]').prop("checked",!0)})})}),$("#save-assignments").click(function(){var e=$("#user-select").val();if(!e)return alert("Bitte Nutzer waehlen");var n=[];$(".img-cb:checked").each(function(){n.push($(this).val())}),$.post("/api/admin/assign",{user_id:e,image_names:n}).done(function(){alert("Zuweisung gespeichert!")})}),$("#update-btn").click(function(){confirm("Achtung: Dies startet den Server auf dem NAS neu. Fortfahren?")&&($("#update-status").text("Update laeuft..."),$.post("/api/admin/update").done(function(e){$("#update-status").text(e.message),setTimeout(function(){window.location.reload()},5e3)}))})});
|
||||
$(document).ready(function(){function e(){$.get("/api/admin/users").done(function(e){$("#users-tbody").empty(),$("#user-select").html('<option value="">Nutzer waehlen...</option>'),e.forEach(function(e){var n='<span class="badge badge-'+e.role+'">'+e.role+"</span>",a=e.created_at?new Date(e.created_at).toLocaleDateString("de-DE"):"-";if("client"===e.role&&e.expires_at){var t=new Date(e.expires_at).toLocaleDateString("de-DE");e.expired?(n='<span class="badge badge-expired">abgelaufen</span>',a="Abgelaufen: "+t):a="Bis: "+t}var r='<button class="btn-small" onclick="changePassword('+e.id+", '"+e.username.replace(/'/g,"\\'")+"')\">PW</button>";r+='<button class="btn-small" onclick="deleteUser('+e.id+", '"+e.username.replace(/'/g,"\\'")+"')\">X</button>",$("#users-tbody").append("<tr><td>"+e.id+"</td><td>"+e.username+" "+n+"</td><td>"+e.role+"</td><td>"+a+"</td><td>"+r+"</td></tr>"),"admin"!==e.role&&$("#user-select").append('<option value="'+e.id+'">'+e.username+" ("+e.role+")</option>")})})}$.get("/api/auth/me").done(function(n){"admin"!==n.role?(alert("Kein Zugriff!"),window.location.href="/"):(e(),$.get("/api/admin/folders").done(function(e){$("#folder-checkboxes").empty(),0!==e.length?e.forEach(function(e){$("#folder-checkboxes").append('<div class="folder-check"><label><input type="checkbox" value="'+e+'" class="folder-cb"/> <i class="fa fa-folder"></i> '+e+"</label></div>")}):$("#folder-checkboxes").html("<p>Keine Unterordner in Websitebilder gefunden. Erstelle Ordner in Nextcloud.</p>")}),$("body").removeClass("is-preload"))}).fail(function(){window.location.href="/"}),$("#logout-btn").click(function(e){e.preventDefault(),$.post("/api/auth/logout").done(function(){window.location.href="/"})}),$("#create-user-form").submit(function(n){n.preventDefault(),$.post("/api/admin/users",{username:$("#new-username").val(),password:$("#new-password").val(),role:$("#new-role").val()}).done(function(){$("#new-username").val(""),$("#new-password").val(""),e()}).fail(function(e){alert("Fehler: "+(e.responseJSON?e.responseJSON.error:"Unbekannt"))})}),window.deleteUser=function(n,a){confirm('Nutzer "'+a+'" wirklich loeschen?')&&$.ajax({url:"/api/admin/users/"+n,type:"DELETE"}).done(e).fail(function(e){alert(e.responseJSON?e.responseJSON.error:"Fehler")})},window.changePassword=function(e,n){var a=prompt('Neues Passwort fuer "'+n+'":');a&&$.ajax({url:"/api/admin/users/"+e+"/password",type:"PUT",contentType:"application/json",data:JSON.stringify({new_password:a})}).done(function(){alert("Passwort geaendert!")}).fail(function(e){alert(e.responseJSON?e.responseJSON.error:"Fehler")})},$("#user-select").change(function(){var e=$(this).val();$(".folder-cb").prop("checked",!1),e&&$.get("/api/admin/assign?user_id="+e).done(function(e){e.forEach(function(e){$('.folder-cb[value="'+e+'"]').prop("checked",!0)})})}),$("#save-assignments").click(function(){var e=$("#user-select").val();if(!e)return alert("Bitte Nutzer waehlen");var n=[];$(".folder-cb:checked").each(function(){n.push($(this).val())}),$.ajax({url:"/api/admin/assign",type:"POST",contentType:"application/json",data:JSON.stringify({user_id:e,folders:n})}).done(function(){$("#assign-status").text("Gespeichert!").fadeIn().delay(2e3).fadeOut()}).fail(function(){alert("Fehler beim Speichern")})}),$("#admin-pw-form").submit(function(e){e.preventDefault(),$.post("/api/auth/change-password",{old_password:$("#admin-pw-old").val(),new_password:$("#admin-pw-new").val()}).done(function(){alert("Passwort geaendert!"),$("#admin-pw-old, #admin-pw-new").val("")}).fail(function(e){alert(e.responseJSON?e.responseJSON.error:"Fehler")})})});
|
||||
@@ -161,7 +161,7 @@
|
||||
cache: false,
|
||||
success: function(files) {
|
||||
if(files.length === 0) {
|
||||
$main.html('<p style="text-align:center;color:#fff;padding:2em;width:100%">Bitte logge dich über den Login-Button unten links ein, um deine zugewiesenen Bilder zu sehen.</p>');
|
||||
$main.html('<p style="text-align:center;color:#fff;padding:2em;width:100%">Noch keine Bilder vorhanden.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,8 +182,11 @@
|
||||
|
||||
images.forEach(function(file) {
|
||||
var article = $('<article class="thumb"></article>');
|
||||
var link = $('<a class="image" href="/images/fulls/' + encodeURIComponent(file.name) + '"></a>');
|
||||
var img = $('<img src="/images/thumbs/' + encodeURIComponent(file.name) + '" alt="" data-name="' + file.name.replace(/"/g, '"') + '" />');
|
||||
var prefix = file.folder ? encodeURIComponent(file.folder) + '/' : '';
|
||||
var fullPath = '/images/fulls/' + prefix + encodeURIComponent(file.name);
|
||||
var thumbPath = '/images/thumbs/' + prefix + encodeURIComponent(file.name);
|
||||
var link = $('<a class="image" href="' + fullPath + '"></a>');
|
||||
var img = $('<img src="' + thumbPath + '" alt="" data-name="' + file.name.replace(/"/g, '"') + '" />');
|
||||
link.append(img);
|
||||
article.append(link);
|
||||
$main.append(article);
|
||||
@@ -207,7 +210,7 @@
|
||||
|
||||
if ($image.length == 0) return;
|
||||
|
||||
$image.css("background-image", "url(" + $image_img.attr("src") + ")");
|
||||
$image.css("background-image", "url('" + $image_img.attr("src") + "')");
|
||||
if ((x = $image_img.data("position"))) $image.css("background-position", x);
|
||||
$image_img.hide();
|
||||
|
||||
@@ -279,10 +282,34 @@
|
||||
return template;
|
||||
}
|
||||
|
||||
// Galerie laden
|
||||
loadGallery();
|
||||
|
||||
// Theme Toggle
|
||||
var currentTheme = localStorage.getItem('mischkomposition_theme') || 'main';
|
||||
updateThemeIcon(currentTheme);
|
||||
|
||||
$('#theme-toggle').click(function(e) {
|
||||
e.preventDefault();
|
||||
currentTheme = currentTheme === 'main' ? 'light' : 'main';
|
||||
localStorage.setItem('mischkomposition_theme', currentTheme);
|
||||
$('#theme-style').attr('href', 'assets/css/' + currentTheme + '.min.css');
|
||||
updateThemeIcon(currentTheme);
|
||||
});
|
||||
|
||||
function updateThemeIcon(theme) {
|
||||
if (theme === 'light') {
|
||||
$('#theme-toggle i').removeClass('fa-sun').addClass('fa-moon');
|
||||
} else {
|
||||
$('#theme-toggle i').removeClass('fa-moon').addClass('fa-sun');
|
||||
}
|
||||
}
|
||||
|
||||
// Login Handling
|
||||
$.get('/api/auth/me').done(function(u) {
|
||||
$('#login-username, #login-password, #login-submit').hide();
|
||||
$('#logout-btn').show().css('display', 'inline-block');
|
||||
$('#pw-change-section').show();
|
||||
if(u.role === 'admin') {
|
||||
$('#admin-link').html('<a href="/admin.html" class="button">Admin-Dashboard</a>');
|
||||
}
|
||||
@@ -293,7 +320,13 @@
|
||||
e.preventDefault();
|
||||
$.post('/api/auth/login', { username: $('#login-username').val(), password: $('#login-password').val() })
|
||||
.done(function() { window.location.reload(); })
|
||||
.fail(function(err) { alert("Login fehlgeschlagen. Bitte Zugangsdaten ueberpruefen."); });
|
||||
.fail(function(xhr) {
|
||||
var msg = 'Login fehlgeschlagen.';
|
||||
if (xhr.responseJSON && xhr.responseJSON.error === 'Account abgelaufen') {
|
||||
msg = 'Dein Account ist abgelaufen. Bitte kontaktiere den Administrator.';
|
||||
}
|
||||
alert(msg);
|
||||
});
|
||||
});
|
||||
|
||||
$('#logout-btn').click(function(e) {
|
||||
@@ -301,4 +334,14 @@
|
||||
$.post('/api/auth/logout').done(function() { window.location.reload(); });
|
||||
});
|
||||
|
||||
$('#pw-change-form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
var newPw = $('#pw-new').val();
|
||||
var confirmPw = $('#pw-confirm').val();
|
||||
if (newPw !== confirmPw) { alert('Passwoerter stimmen nicht ueberein.'); return; }
|
||||
$.post('/api/auth/change-password', { old_password: $('#pw-old').val(), new_password: newPw })
|
||||
.done(function() { alert('Passwort geaendert!'); $('#pw-old, #pw-new, #pw-confirm').val(''); })
|
||||
.fail(function(xhr) { alert(xhr.responseJSON ? xhr.responseJSON.error : 'Fehler'); });
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
|
||||
2
assets/js/main.min.js
vendored
2
assets/js/main.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -75,7 +75,7 @@
|
||||
|
||||
.caption {
|
||||
@include padding(2em, 2em);
|
||||
@include vendor('background-image', 'linear-gradient(to top, rgba(16,16,16,0.45) 25%, rgba(16,16,16,0) 100%)');
|
||||
background: rgba(246, 240, 228, 0.95); /* freundliches Beige transparent */
|
||||
bottom: 0;
|
||||
cursor: default;
|
||||
left: 0;
|
||||
@@ -86,10 +86,11 @@
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
margin: 0 0 (_size(element-margin) * 0.25) 0;
|
||||
color: #4a4a4a !important; /* Grau */
|
||||
}
|
||||
|
||||
p {
|
||||
color: _palette(fg-bold);
|
||||
p, span, i, strong, em {
|
||||
color: #5c554e !important; /* Grau */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
assets/sass/light.scss
Normal file
54
assets/sass/light.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
@import 'libs/vars';
|
||||
|
||||
$palette: (
|
||||
bg: #f9f6f0,
|
||||
bg-alt: #eee8dd,
|
||||
bg-overlay: transparentize(#f9f6f0, 0.15),
|
||||
bg-overlay-alt: transparentize(#f9f6f0, 0.05),
|
||||
bg-ie-overlay: transparentize(#f9f6f0, 0.1),
|
||||
bg-ie-overlay-alt: transparentize(#f9f6f0, 0.05),
|
||||
fg: #5c554e,
|
||||
fg-bold: #38332f,
|
||||
fg-medium: #7a746e,
|
||||
fg-light: #9e9790,
|
||||
border: #e6dccc,
|
||||
border-bg: #f0e5d6,
|
||||
border-bg-alt: #e1d1ba,
|
||||
accent1: #e48c71
|
||||
) !global;
|
||||
|
||||
@import 'libs/functions';
|
||||
@import 'libs/mixins';
|
||||
@import 'libs/vendor';
|
||||
@import 'libs/breakpoints';
|
||||
|
||||
@include breakpoints((
|
||||
xlarge: ( 1281px, 1680px ),
|
||||
large: ( 981px, 1280px ),
|
||||
medium: ( 737px, 980px ),
|
||||
small: ( 481px, 736px ),
|
||||
xsmall: ( null, 480px )
|
||||
));
|
||||
|
||||
@include keyframes(spinner) {
|
||||
0% { @include vendor('transform', 'rotate(0deg)'); }
|
||||
100% { @include vendor('transform', 'rotate(359deg)'); }
|
||||
}
|
||||
|
||||
@import 'base/reset';
|
||||
@import 'base/page';
|
||||
@import 'base/typography';
|
||||
@import 'components/button';
|
||||
@import 'components/form';
|
||||
@import 'components/icon';
|
||||
@import 'components/list';
|
||||
@import 'components/actions';
|
||||
@import 'components/icons';
|
||||
@import 'components/table';
|
||||
@import 'components/panel';
|
||||
@import 'components/poptrox-popup';
|
||||
@import 'layout/wrapper';
|
||||
@import 'layout/header';
|
||||
@import 'layout/main';
|
||||
@import 'layout/footer';
|
||||
@import 'custom';
|
||||
@@ -8,13 +8,29 @@ const db = new sqlite3.Database(dbPath);
|
||||
console.log('Using database at', dbPath);
|
||||
|
||||
db.serialize(() => {
|
||||
// Users: mit created_at fuer Client-Ablauf (30 Tage)
|
||||
db.run(`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL
|
||||
role TEXT NOT NULL DEFAULT 'client',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
|
||||
// Migration: created_at hinzufuegen falls Tabelle schon existiert
|
||||
db.run(`ALTER TABLE users ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP`, (err) => {
|
||||
// Ignoriere Fehler wenn Spalte schon existiert
|
||||
});
|
||||
|
||||
// Ordner-Zuweisung (neu)
|
||||
db.run(`CREATE TABLE IF NOT EXISTS user_folders (
|
||||
user_id INTEGER,
|
||||
folder_name TEXT,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id),
|
||||
UNIQUE(user_id, folder_name)
|
||||
)`);
|
||||
|
||||
// Alte Tabelle behalten fuer Kompatibilitaet, aber nicht mehr nutzen
|
||||
db.run(`CREATE TABLE IF NOT EXISTS user_images (
|
||||
user_id INTEGER,
|
||||
image_name TEXT,
|
||||
@@ -22,12 +38,12 @@ db.serialize(() => {
|
||||
UNIQUE(user_id, image_name)
|
||||
)`);
|
||||
|
||||
// Create default admin user if none exists
|
||||
// Default-Admin erstellen wenn keiner existiert
|
||||
db.get('SELECT * FROM users WHERE username = ?', ['admin'], (err, row) => {
|
||||
if (!row) {
|
||||
const hash = bcrypt.hashSync('admin123', 10);
|
||||
db.run('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', ['admin', hash, 'admin']);
|
||||
console.log('Default admin created with credentials: admin / admin123');
|
||||
console.log('Default admin created: admin / admin123');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,29 +4,44 @@ const jwt = require('jsonwebtoken');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const db = require('./database');
|
||||
|
||||
const app = express();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'mischkomposition-super-secret-key-change-me';
|
||||
const FULLS_DIR = path.join(__dirname, '../public/images/fulls');
|
||||
const IMAGE_RE = /\.(jpg|jpeg|png|gif|webp)$/i;
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cookieParser());
|
||||
|
||||
// Middleware to verify JWT cookie
|
||||
// ==================== MIDDLEWARE ====================
|
||||
|
||||
function authMiddleware(req, res, next) {
|
||||
const token = req.cookies.token;
|
||||
if (!token) {
|
||||
if (!token) { req.user = null; return next(); }
|
||||
jwt.verify(token, JWT_SECRET, (err, decoded) => {
|
||||
if (err) { req.user = null; return next(); }
|
||||
// Client-Ablauf pruefen (30 Tage)
|
||||
if (decoded.role === 'client' && decoded.created_at) {
|
||||
const expires = new Date(decoded.created_at);
|
||||
expires.setDate(expires.getDate() + 30);
|
||||
if (new Date() > expires) {
|
||||
res.clearCookie('token');
|
||||
req.user = null;
|
||||
return next();
|
||||
}
|
||||
jwt.verify(token, JWT_SECRET, (err, decoded) => {
|
||||
if (err) req.user = null;
|
||||
else req.user = decoded;
|
||||
}
|
||||
req.user = decoded;
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
if (!req.user) return res.status(401).json({ error: 'Not logged in' });
|
||||
next();
|
||||
}
|
||||
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.user || req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
@@ -36,6 +51,28 @@ function requireAdmin(req, res, next) {
|
||||
|
||||
app.use(authMiddleware);
|
||||
|
||||
// ==================== HELPER ====================
|
||||
|
||||
function listImagesInDir(dir) {
|
||||
try {
|
||||
const files = fs.readdirSync(dir);
|
||||
return files.filter(f => IMAGE_RE.test(f) && fs.statSync(path.join(dir, f)).isFile());
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function listSubfolders(dir) {
|
||||
try {
|
||||
return fs.readdirSync(dir).filter(f => {
|
||||
try { return fs.statSync(path.join(dir, f)).isDirectory(); }
|
||||
catch (e) { return false; }
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== AUTH API ====================
|
||||
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
@@ -45,7 +82,18 @@ app.post('/api/auth/login', (req, res) => {
|
||||
if (!user || !bcrypt.compareSync(password, user.password_hash)) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
const token = jwt.sign({ id: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: '7d' });
|
||||
// Client-Ablauf pruefen
|
||||
if (user.role === 'client' && user.created_at) {
|
||||
const expires = new Date(user.created_at);
|
||||
expires.setDate(expires.getDate() + 30);
|
||||
if (new Date() > expires) {
|
||||
return res.status(401).json({ error: 'Account abgelaufen' });
|
||||
}
|
||||
}
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, username: user.username, role: user.role, created_at: user.created_at },
|
||||
JWT_SECRET, { expiresIn: '7d' }
|
||||
);
|
||||
res.cookie('token', token, { httpOnly: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 });
|
||||
res.json({ success: true, role: user.role });
|
||||
});
|
||||
@@ -61,123 +109,154 @@ app.get('/api/auth/me', (req, res) => {
|
||||
res.json(req.user);
|
||||
});
|
||||
|
||||
// ==================== IMAGE PROXY ====================
|
||||
|
||||
// We must override the nginx `/images/fulls/` JSON behavior here:
|
||||
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));
|
||||
|
||||
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 => {
|
||||
const stats = fs.statSync(path.join(fullsDir, name));
|
||||
return { name, type: 'file', mtime: stats.mtime.toUTCString(), size: stats.size };
|
||||
});
|
||||
return res.json(result);
|
||||
});
|
||||
app.post('/api/auth/change-password', requireAuth, (req, res) => {
|
||||
const { old_password, new_password } = req.body;
|
||||
if (!new_password || new_password.length < 4) {
|
||||
return res.status(400).json({ error: 'Passwort muss mindestens 4 Zeichen lang sein' });
|
||||
}
|
||||
db.get('SELECT * FROM users WHERE id = ?', [req.user.id], (err, user) => {
|
||||
if (err || !user) return res.status(500).json({ error: 'DB Error' });
|
||||
if (!bcrypt.compareSync(old_password, user.password_hash)) {
|
||||
return res.status(401).json({ error: 'Altes Passwort ist falsch' });
|
||||
}
|
||||
const hash = bcrypt.hashSync(new_password, 10);
|
||||
db.run('UPDATE users SET password_hash = ? WHERE id = ?', [hash, user.id], (err) => {
|
||||
if (err) return res.status(500).json({ error: 'DB Error' });
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Proxy image files directly, but we let Express serve static later.
|
||||
// BUT we should restrict access to physical image files?
|
||||
// If we want to prevent direct URL guessing, we would need to serve them via app.get('/images/fulls/:name').
|
||||
// For now, let's just use express.static to serve the actual binary files.
|
||||
// Nextcloud folder names are hard to guess anyway.
|
||||
// ==================== IMAGE LISTING ====================
|
||||
|
||||
app.get('/images/fulls/', (req, res) => {
|
||||
// Root-Bilder sind immer oeffentlich
|
||||
const rootImages = listImagesInDir(FULLS_DIR).map(name => ({
|
||||
name, type: 'file', folder: null
|
||||
}));
|
||||
|
||||
// Nicht eingeloggt: nur Root-Bilder
|
||||
if (!req.user) return res.json(rootImages);
|
||||
|
||||
const folders = listSubfolders(FULLS_DIR);
|
||||
|
||||
// Admin: Nur Root-Bilder (damit die Startseite sauber bleibt)
|
||||
if (req.user.role === 'admin') {
|
||||
return res.json(rootImages);
|
||||
}
|
||||
|
||||
// User/Client: NUR zugewiesene Ordner (keine Root-Bilder untermischen)
|
||||
db.all('SELECT folder_name FROM user_folders WHERE user_id = ?', [req.user.id], (err, rows) => {
|
||||
const assignedFolders = err ? [] : rows.map(r => r.folder_name);
|
||||
const result = [];
|
||||
assignedFolders.forEach(folder => {
|
||||
if (folders.includes(folder)) {
|
||||
listImagesInDir(path.join(FULLS_DIR, folder)).forEach(name => {
|
||||
result.push({ name, type: 'file', folder });
|
||||
});
|
||||
}
|
||||
});
|
||||
return res.json(result);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== ADMIN API ====================
|
||||
|
||||
app.get('/api/admin/users', requireAdmin, (req, res) => {
|
||||
db.all('SELECT id, username, role FROM users', (err, rows) => {
|
||||
res.json(rows);
|
||||
db.all('SELECT id, username, role, created_at FROM users', (err, rows) => {
|
||||
if (err) return res.json([]);
|
||||
// Ablaufdatum berechnen fuer Clients
|
||||
const result = rows.map(u => {
|
||||
const out = { ...u };
|
||||
if (u.role === 'client' && u.created_at) {
|
||||
const expires = new Date(u.created_at);
|
||||
expires.setDate(expires.getDate() + 30);
|
||||
out.expires_at = expires.toISOString();
|
||||
out.expired = new Date() > expires;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/admin/users', requireAdmin, (req, res) => {
|
||||
const { username, password, role } = req.body;
|
||||
if (!username || !password) return res.status(400).json({ error: 'Username und Passwort erforderlich' });
|
||||
const validRoles = ['admin', 'user', 'client'];
|
||||
const userRole = validRoles.includes(role) ? role : 'client';
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
db.run('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', [username, hash, role || 'user'], function(err) {
|
||||
db.run('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', [username, hash, userRole], function(err) {
|
||||
if (err) return res.status(400).json({ error: err.message });
|
||||
res.json({ success: true, id: this.lastID });
|
||||
});
|
||||
});
|
||||
|
||||
app.delete('/api/admin/users/:id', requireAdmin, (req, res) => {
|
||||
if (req.params.id == 1) return res.status(400).json({ error: 'Cannot delete primary admin' });
|
||||
db.run('DELETE FROM users WHERE id = ?', [req.params.id], err => {
|
||||
db.run('DELETE FROM user_images WHERE user_id = ?', [req.params.id]);
|
||||
const userId = parseInt(req.params.id);
|
||||
// Sich selbst loeschen verhindern
|
||||
if (userId === req.user.id) {
|
||||
return res.status(400).json({ error: 'Du kannst dich nicht selbst loeschen' });
|
||||
}
|
||||
db.run('DELETE FROM users WHERE id = ?', [userId], (err) => {
|
||||
if (err) return res.status(500).json({ error: 'DB Error' });
|
||||
db.run('DELETE FROM user_folders WHERE user_id = ?', [userId]);
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
app.put('/api/admin/users/:id/password', requireAdmin, (req, res) => {
|
||||
const { new_password } = req.body;
|
||||
if (!new_password || new_password.length < 4) {
|
||||
return res.status(400).json({ error: 'Passwort muss mindestens 4 Zeichen lang sein' });
|
||||
}
|
||||
const hash = bcrypt.hashSync(new_password, 10);
|
||||
db.run('UPDATE users SET password_hash = ? WHERE id = ?', [hash, parseInt(req.params.id)], (err) => {
|
||||
if (err) return res.status(500).json({ error: 'DB Error' });
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
// Ordner-Verwaltung
|
||||
app.get('/api/admin/folders', requireAdmin, (req, res) => {
|
||||
res.json(listSubfolders(FULLS_DIR));
|
||||
});
|
||||
|
||||
app.get('/api/admin/assign', requireAdmin, (req, res) => {
|
||||
db.all('SELECT * FROM user_images', (err, rows) => {
|
||||
res.json(rows);
|
||||
const userId = req.query.user_id;
|
||||
if (userId) {
|
||||
db.all('SELECT folder_name FROM user_folders WHERE user_id = ?', [userId], (err, rows) => {
|
||||
res.json(err ? [] : rows.map(r => r.folder_name));
|
||||
});
|
||||
} else {
|
||||
db.all('SELECT * FROM user_folders', (err, rows) => {
|
||||
res.json(err ? [] : rows);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/admin/assign', requireAdmin, (req, res) => {
|
||||
const { user_id, image_names } = req.body; // image_names is array of strings
|
||||
const { user_id, folders } = req.body;
|
||||
if (!user_id) return res.status(400).json({ error: 'user_id erforderlich' });
|
||||
const folderList = Array.isArray(folders) ? folders : [];
|
||||
db.serialize(() => {
|
||||
db.run('BEGIN TRANSACTION');
|
||||
db.run('DELETE FROM user_images WHERE user_id = ?', [user_id]);
|
||||
const stmt = db.prepare('INSERT INTO user_images (user_id, image_name) VALUES (?, ?)');
|
||||
image_names.forEach(name => {
|
||||
stmt.run(user_id, name);
|
||||
});
|
||||
db.run('DELETE FROM user_folders WHERE user_id = ?', [user_id]);
|
||||
const stmt = db.prepare('INSERT INTO user_folders (user_id, folder_name) VALUES (?, ?)');
|
||||
folderList.forEach(name => stmt.run(user_id, name));
|
||||
stmt.finalize();
|
||||
db.run('COMMIT');
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
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...' });
|
||||
|
||||
setTimeout(() => {
|
||||
exec(cmd, { cwd: '/app' }, (err, stdout, stderr) => {
|
||||
if (err) console.error("Update failed:", err);
|
||||
else console.log("Update success:", stdout);
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
|
||||
// ==================== STATIC FILES ====================
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
|
||||
// Catch all - send index.html
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||
});
|
||||
|
||||
const PORT = 8080;
|
||||
const PORT = 8090;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Backend server listening on port ${PORT}`);
|
||||
console.log(`Mischkomposition Backend on port ${PORT}`);
|
||||
});
|
||||
|
||||
@@ -3,16 +3,33 @@ services:
|
||||
image: ghcr.io/kroonk/photography:latest
|
||||
container_name: photography-website
|
||||
ports:
|
||||
- "8090:8080"
|
||||
- "8090:8090"
|
||||
volumes:
|
||||
# Nextcloud-Ordner mit Originalbildern (read-only)
|
||||
- /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
|
||||
# Zugriff auf Docker & Credentials fuer den One-Click "Website Aktualisieren" Button
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ~/.docker/config.json:/root/.docker/config.json:ro
|
||||
restart: unless-stopped
|
||||
|
||||
# Watchtower: Prueft automatisch auf neue Docker-Images und aktualisiert den Container
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
container_name: watchtower
|
||||
# Nur unsere Webseite updaten, andere UGREEN Container ignorieren!
|
||||
command: photography-website
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# GitHub Login von deinem NAS uebernehmen, um ghcr.io lesen zu duerfen
|
||||
- ~/.docker/config.json:/config.json:ro
|
||||
environment:
|
||||
- WATCHTOWER_CLEANUP=true
|
||||
- WATCHTOWER_POLL_INTERVAL=300
|
||||
- WATCHTOWER_LABEL_ENABLE=false
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -3,36 +3,49 @@
|
||||
FULLS_DIR="/app/public/images/fulls"
|
||||
THUMBS_DIR="/app/public/images/thumbs"
|
||||
|
||||
generate_thumbs() {
|
||||
mkdir -p "$THUMBS_DIR"
|
||||
for img in "$FULLS_DIR"/*; do
|
||||
# Thumbnails fuer einen Quell- und Zielordner generieren
|
||||
generate_thumbs_for_dir() {
|
||||
src_dir="$1"
|
||||
dst_dir="$2"
|
||||
mkdir -p "$dst_dir"
|
||||
for img in "$src_dir"/*; do
|
||||
[ -f "$img" ] || continue
|
||||
|
||||
filename=$(basename "$img")
|
||||
ext=$(echo "${filename##*.}" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Nur Bilddateien verarbeiten
|
||||
case "$ext" in
|
||||
jpg|jpeg|png|gif|webp) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
# Thumbnail nur erstellen wenn noch nicht vorhanden oder aelter als Original
|
||||
if [ ! -f "$THUMBS_DIR/$filename" ] || [ "$img" -nt "$THUMBS_DIR/$filename" ]; then
|
||||
echo "Thumbnail erstellen: $filename"
|
||||
convert "$img" -resize 512x512 -quality 80 "$THUMBS_DIR/$filename" 2>/dev/null
|
||||
if [ ! -f "$dst_dir/$filename" ] || [ "$img" -nt "$dst_dir/$filename" ]; then
|
||||
echo "Thumbnail erstellen: $dst_dir/$filename"
|
||||
convert "$img" -resize 512x512 -quality 80 "$dst_dir/$filename" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
generate_all_thumbs() {
|
||||
# Root-Bilder
|
||||
generate_thumbs_for_dir "$FULLS_DIR" "$THUMBS_DIR"
|
||||
|
||||
# Unterordner (Kunden-Ordner)
|
||||
for dir in "$FULLS_DIR"/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
foldername=$(basename "$dir")
|
||||
generate_thumbs_for_dir "$dir" "$THUMBS_DIR/$foldername"
|
||||
done
|
||||
}
|
||||
|
||||
echo "=== Mischkomposition - Starte Thumbnail-Generierung ==="
|
||||
generate_thumbs
|
||||
generate_all_thumbs
|
||||
echo "=== Thumbnail-Generierung abgeschlossen ==="
|
||||
|
||||
# Hintergrund-Prozess: Alle 60 Sekunden nach neuen Bildern suchen
|
||||
(while true; do
|
||||
sleep 60
|
||||
generate_thumbs
|
||||
generate_all_thumbs
|
||||
done) &
|
||||
|
||||
# Node Server starten
|
||||
|
||||
21
index.html
21
index.html
@@ -10,6 +10,8 @@ layout: default
|
||||
<h1><a href="index.html"><strong>{{ site.header.title }}</strong> {{ site.header.subtitle }}</a></h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" id="theme-toggle" title="Theme wechseln"><i class="fa fa-sun"></i></a></li>
|
||||
<li><a href="#galerie" id="galerie-link">Galerie</a></li>
|
||||
<li><a href="#login-panel" id="login-nav">Login <i class="fa fa-user"></i></a></li>
|
||||
<li><a href="#footer">About <i class="fa fa-info-circle text-fg"></i></a></li>
|
||||
</ul>
|
||||
@@ -130,6 +132,25 @@ layout: default
|
||||
<li id="admin-link"></li>
|
||||
</ul>
|
||||
</form>
|
||||
<div id="pw-change-section" style="display:none; margin-top:1.5em; border-top:1px solid rgba(255,255,255,0.2); padding-top:1.5em;">
|
||||
<h3>Passwort aendern</h3>
|
||||
<form id="pw-change-form">
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<input type="password" id="pw-old" placeholder="Altes Passwort" required />
|
||||
</div>
|
||||
<div class="field half">
|
||||
<input type="password" id="pw-new" placeholder="Neues Passwort" required />
|
||||
</div>
|
||||
<div class="field half">
|
||||
<input type="password" id="pw-confirm" placeholder="Passwort bestaetigen" required />
|
||||
</div>
|
||||
</div>
|
||||
<ul class="actions">
|
||||
<li><input type="submit" value="Passwort aendern" class="primary" /></li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user