From a59457a8bb4f43b18540c78bd19a6f66af493fe6 Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 10:35:05 +0200 Subject: [PATCH 01/10] Secure image access and bootstrap admin --- README.md | 12 ++++++++ assets/js/admin.js | 64 ++++++++++++++++++++++++++-------------- backend/database.js | 43 +++++++++++++++++++++++---- backend/server.js | 71 ++++++++++++++++++++++++++++++++++++++++++--- docker-compose.yml | 6 ++++ 5 files changed, 165 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 60c676b..59c2591 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ services: container_name: photography-website ports: - "8090:8090" + environment: + - JWT_SECRET=replace-with-a-long-random-secret + - INITIAL_ADMIN_USERNAME=MrDiderot + - INITIAL_ADMIN_PASSWORD=Start123 + - FULLS_DIR=/app/public/images/fulls + - THUMBS_DIR=/app/public/images/thumbs volumes: - /path/to/nextcloud/data:/app/public/images/fulls:ro - thumbs:/app/public/images/thumbs @@ -32,6 +38,12 @@ services: Run `docker compose up -d` to launch the site. +Change the initial admin password after the first login. If you prefer not to store the initial password in `docker-compose.yml`, create a bcrypt hash and use `INITIAL_ADMIN_PASSWORD_HASH` instead: + +```bash +node -e "const bcrypt = require('bcryptjs'); console.log(bcrypt.hashSync('your-admin-password', 10));" +``` + ## Credits & License - Originally based on a Jekyll template. - UI Design by [AJ / HTML5 UP](https://html5up.net). diff --git a/assets/js/admin.js b/assets/js/admin.js index 01592e8..f1570c0 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -27,32 +27,42 @@ $(document).ready(function() { $('#user-select').html(''); users.forEach(function(u) { - var badge = '' + u.role + ''; + var badge = $('').addClass('badge badge-' + u.role).text(u.role); 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 = 'abgelaufen'; + badge = $('').addClass('badge badge-expired').text('abgelaufen'); info = 'Abgelaufen: ' + expDate; } else { info = 'Bis: ' + expDate; } } - var actions = ''; - actions += ''; + var row = $(''); + var userCell = $('').text(u.username + ' ').append(badge); + var actions = $(''); + $('') + .data('user-id', u.id) + .data('username', u.username) + .appendTo(actions); + $('') + .data('user-id', u.id) + .data('username', u.username) + .appendTo(actions); - $('#users-tbody').append( - '' + u.id + '' + u.username + ' ' + badge + - '' + u.role + '' + info + - '' + actions + '' - ); + row.append($('').text(u.id)); + row.append(userCell); + row.append($('').text(u.role)); + row.append($('').text(info)); + row.append(actions); + $('#users-tbody').append(row); // Dropdown fuer Ordner-Zuweisung (nur non-admin) if (u.role !== 'admin') { - $('#user-select').append(''); + $('#user-select').append($('').val(u.id).text(u.username + ' (' + u.role + ')')); } }); }); @@ -75,16 +85,20 @@ $(document).ready(function() { }); // Nutzer loeschen - window.deleteUser = function(id, name) { + function deleteUser(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'); }); } - }; + } + + $('#users-tbody').on('click', '.user-delete', function() { + deleteUser($(this).data('user-id'), $(this).data('username')); + }); // Passwort aendern (Admin setzt fuer anderen User) - window.changePassword = function(id, name) { + function changePassword(id, name) { var newPw = prompt('Neues Passwort fuer "' + name + '":'); if (!newPw) return; $.ajax({ @@ -97,7 +111,11 @@ $(document).ready(function() { }).fail(function(err) { alert(err.responseJSON ? err.responseJSON.error : 'Fehler'); }); - }; + } + + $('#users-tbody').on('click', '.user-password', function() { + changePassword($(this).data('user-id'), $(this).data('username')); + }); // ==================== ORDNER-ZUWEISUNG ==================== @@ -109,12 +127,14 @@ $(document).ready(function() { return; } folders.forEach(function(f) { - $('#folder-checkboxes').append( - '
' - ); + var wrapper = $('
'); + var label = $(''); + $('').val(f).appendTo(label); + label.append(' '); + $('').appendTo(label); + label.append(' ' + f); + wrapper.append(label); + $('#folder-checkboxes').append(wrapper); }); }); } @@ -126,7 +146,9 @@ $(document).ready(function() { if (!uid) return; $.get('/api/admin/assign?user_id=' + uid).done(function(assignedFolders) { assignedFolders.forEach(function(f) { - $('.folder-cb[value="' + f + '"]').prop('checked', true); + $('.folder-cb').filter(function() { + return $(this).val() === f; + }).prop('checked', true); }); }); }); diff --git a/backend/database.js b/backend/database.js index f25e366..ad1b9a2 100644 --- a/backend/database.js +++ b/backend/database.js @@ -38,13 +38,44 @@ db.serialize(() => { UNIQUE(user_id, image_name) )`); - // 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: admin / admin123'); + // Initialen Admin nur mit expliziten Credentials aus der Umgebung anlegen. + db.serialize(() => { + const username = process.env.INITIAL_ADMIN_USERNAME; + const password = process.env.INITIAL_ADMIN_PASSWORD; + const passwordHash = process.env.INITIAL_ADMIN_PASSWORD_HASH || (password ? bcrypt.hashSync(password, 10) : null); + if (!username || !passwordHash) { + console.warn('Set INITIAL_ADMIN_USERNAME with INITIAL_ADMIN_PASSWORD or INITIAL_ADMIN_PASSWORD_HASH to bootstrap an admin.'); + return; } + + db.get('SELECT id FROM users WHERE username = ?', [username], (err, row) => { + if (err || row) return; + + db.run( + 'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', + [username, passwordHash, 'admin'], + (insertErr) => { + if (insertErr) { + console.error('Failed to create initial admin:', insertErr.message); + return; + } + console.log(`Initial admin created: ${username}`); + } + ); + }); + + db.get('SELECT id, role FROM users WHERE username = ?', [username], (err, row) => { + if (err || !row || row.role === 'admin') return; + + db.run('UPDATE users SET role = ? WHERE id = ?', ['admin', row.id], (updateErr) => { + if (updateErr) { + console.error('Failed to promote initial admin:', updateErr.message); + return; + } + console.log(`Initial admin promoted: ${username}`); + } + ); + }); }); }); diff --git a/backend/server.js b/backend/server.js index c5ee595..55f6323 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7,10 +7,22 @@ const path = require('path'); 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 JWT_SECRET = process.env.JWT_SECRET; +if (!JWT_SECRET) { + throw new Error('JWT_SECRET must be set before starting the server'); +} +const PUBLIC_DIR = path.join(__dirname, '../public'); +const DEFAULT_FULLS_DIR = path.join(PUBLIC_DIR, 'images/fulls'); +const DEFAULT_THUMBS_DIR = path.join(PUBLIC_DIR, 'images/thumbs'); +const DEV_FULLS_DIR = path.join(__dirname, '../images/fulls'); +const DEV_THUMBS_DIR = path.join(__dirname, '../images/thumbs'); +const FULLS_DIR = process.env.FULLS_DIR || (fs.existsSync(DEFAULT_FULLS_DIR) ? DEFAULT_FULLS_DIR : DEV_FULLS_DIR); +const THUMBS_DIR = process.env.THUMBS_DIR || (fs.existsSync(DEFAULT_THUMBS_DIR) ? DEFAULT_THUMBS_DIR : DEV_THUMBS_DIR); const IMAGE_RE = /\.(jpg|jpeg|png|gif|webp)$/i; +console.log('Using full-size images at', FULLS_DIR); +console.log('Using thumbnails at', THUMBS_DIR); + app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser()); @@ -73,6 +85,55 @@ function listSubfolders(dir) { } } +function isSafePath(baseDir, requestedPath) { + const resolvedBase = path.resolve(baseDir); + const resolvedPath = path.resolve(resolvedBase, requestedPath); + return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path.sep); +} + +function hasAssignedFolder(userId, folder, callback) { + db.get( + 'SELECT 1 FROM user_folders WHERE user_id = ? AND folder_name = ?', + [userId, folder], + (err, row) => callback(!err && !!row) + ); +} + +function serveProtectedImage(req, res) { + const kind = req.params[0]; + const relativePath = req.params[1] || ''; + const baseDir = kind === 'thumbs' ? THUMBS_DIR : FULLS_DIR; + const segments = relativePath.split('/').filter(Boolean); + const fileName = segments[segments.length - 1]; + + if (!fileName || !IMAGE_RE.test(fileName) || !isSafePath(baseDir, relativePath)) { + return res.status(404).end(); + } + + const absolutePath = path.resolve(baseDir, relativePath); + if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) { + return res.status(404).end(); + } + + // Bilder direkt im Root-Verzeichnis sind oeffentlich. Kundenordner sind geschuetzt. + if (segments.length === 1) { + return res.sendFile(absolutePath); + } + + if (!req.user) { + return res.status(401).json({ error: 'Not logged in' }); + } + + if (req.user.role === 'admin') { + return res.sendFile(absolutePath); + } + + hasAssignedFolder(req.user.id, segments[0], (allowed) => { + if (!allowed) return res.status(403).json({ error: 'Forbidden' }); + res.sendFile(absolutePath); + }); +} + // ==================== AUTH API ==================== app.post('/api/auth/login', (req, res) => { @@ -250,10 +311,12 @@ app.post('/api/admin/assign', requireAdmin, (req, res) => { // ==================== STATIC FILES ==================== -app.use(express.static(path.join(__dirname, '../public'))); +app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage); + +app.use(express.static(PUBLIC_DIR)); app.get('*', (req, res) => { - res.sendFile(path.join(__dirname, '../public/index.html')); + res.sendFile(path.join(PUBLIC_DIR, 'index.html')); }); const PORT = 8090; diff --git a/docker-compose.yml b/docker-compose.yml index a34ba04..6597104 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,12 @@ services: container_name: photography-website ports: - "8090:8090" + environment: + - JWT_SECRET=${JWT_SECRET:?set JWT_SECRET in your environment} + - INITIAL_ADMIN_USERNAME=MrDiderot + - INITIAL_ADMIN_PASSWORD=Start123 + - FULLS_DIR=/app/public/images/fulls + - THUMBS_DIR=/app/public/images/thumbs volumes: # Nextcloud-Ordner mit Originalbildern (read-only) - /volume1/nextcloud/data/Tom/files/Websitebilder:/app/public/images/fulls:ro From 55f201da40550691b3ba8f07d164b550f29969a9 Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 10:39:02 +0200 Subject: [PATCH 02/10] Add admin deployment sync --- Dockerfile | 4 ++-- README.md | 4 ++++ admin.html | 7 +++++++ assets/js/admin.js | 24 ++++++++++++++++++++++++ backend/server.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 ++ 6 files changed, 84 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c65a3c5..eb2f3df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,8 @@ RUN bundle exec jekyll build # === Serve-Phase: node.js backend + ImageMagick === FROM node:20-alpine -# ImageMagick fuer Thumbnails, Build-Tools fuer sqlite3 native bindings -RUN apk add --no-cache imagemagick python3 make g++ build-base sqlite-dev +# ImageMagick fuer Thumbnails, Docker CLI fuer Admin-Sync, Build-Tools fuer sqlite3 native bindings +RUN apk add --no-cache imagemagick docker-cli python3 make g++ build-base sqlite-dev WORKDIR /app COPY package*.json ./ diff --git a/README.md b/README.md index 59c2591..b6d726b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ services: - INITIAL_ADMIN_PASSWORD=Start123 - FULLS_DIR=/app/public/images/fulls - THUMBS_DIR=/app/public/images/thumbs + - SYNC_IMAGE=ghcr.io/kroonk/photography:latest + - SYNC_CONTAINER=photography-website volumes: - /path/to/nextcloud/data:/app/public/images/fulls:ro - thumbs:/app/public/images/thumbs @@ -44,6 +46,8 @@ Change the initial admin password after the first login. If you prefer not to st node -e "const bcrypt = require('bcryptjs'); console.log(bcrypt.hashSync('your-admin-password', 10));" ``` +The admin dashboard includes a sync button that pulls `SYNC_IMAGE` and restarts `SYNC_CONTAINER`. This requires the Docker socket mount shown above. + ## Credits & License - Originally based on a Jekyll template. - UI Design by [AJ / HTML5 UP](https://html5up.net). diff --git a/admin.html b/admin.html index 1f7835d..9ce97d4 100644 --- a/admin.html +++ b/admin.html @@ -74,6 +74,13 @@ +
+

Website aktualisieren

+

Laedt das neueste Docker-Image und startet den Website-Container neu.

+ + +
+

Eigenes Passwort aendern

diff --git a/assets/js/admin.js b/assets/js/admin.js index f1570c0..f3516c5 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -172,6 +172,30 @@ $(document).ready(function() { }); }); + // ==================== WEBSITE SYNC ==================== + + $('#sync-site').click(function() { + if (!confirm('Neueste Version laden und Website neu starten?')) return; + + var $button = $(this); + var $status = $('#sync-status'); + $button.prop('disabled', true); + $status.text('Sync laeuft...'); + + $.post('/api/admin/sync') + .done(function(res) { + $status.text(res.message || 'Sync gestartet. Website startet neu...'); + window.setTimeout(function() { + window.location.reload(); + }, 12000); + }) + .fail(function(err) { + var msg = err.responseJSON ? err.responseJSON.error : 'Sync fehlgeschlagen'; + $status.text(msg); + $button.prop('disabled', false); + }); + }); + // ==================== EIGENES PASSWORT ==================== $('#admin-pw-form').submit(function(e) { diff --git a/backend/server.js b/backend/server.js index 55f6323..1d24d50 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const fs = require('fs'); const path = require('path'); +const { execFile } = require('child_process'); const db = require('./database'); const app = express(); @@ -18,7 +19,10 @@ const DEV_FULLS_DIR = path.join(__dirname, '../images/fulls'); const DEV_THUMBS_DIR = path.join(__dirname, '../images/thumbs'); const FULLS_DIR = process.env.FULLS_DIR || (fs.existsSync(DEFAULT_FULLS_DIR) ? DEFAULT_FULLS_DIR : DEV_FULLS_DIR); const THUMBS_DIR = process.env.THUMBS_DIR || (fs.existsSync(DEFAULT_THUMBS_DIR) ? DEFAULT_THUMBS_DIR : DEV_THUMBS_DIR); +const SYNC_IMAGE = process.env.SYNC_IMAGE || 'ghcr.io/kroonk/photography:latest'; +const SYNC_CONTAINER = process.env.SYNC_CONTAINER || 'photography-website'; const IMAGE_RE = /\.(jpg|jpeg|png|gif|webp)$/i; +let syncInProgress = false; console.log('Using full-size images at', FULLS_DIR); console.log('Using thumbnails at', THUMBS_DIR); @@ -134,6 +138,12 @@ function serveProtectedImage(req, res) { }); } +function runDocker(args, callback) { + execFile('docker', args, { timeout: 5 * 60 * 1000 }, (err, stdout, stderr) => { + callback(err, `${stdout || ''}${stderr || ''}`.trim()); + }); +} + // ==================== AUTH API ==================== app.post('/api/auth/login', (req, res) => { @@ -309,6 +319,41 @@ app.post('/api/admin/assign', requireAdmin, (req, res) => { }); }); +app.post('/api/admin/sync', requireAdmin, (req, res) => { + if (syncInProgress) { + return res.status(409).json({ error: 'Sync laeuft bereits' }); + } + + syncInProgress = true; + runDocker(['pull', SYNC_IMAGE], (pullErr, pullOutput) => { + syncInProgress = false; + + if (pullErr) { + console.error('Sync pull failed:', pullOutput || pullErr.message); + return res.status(500).json({ + error: 'Docker pull fehlgeschlagen', + details: pullOutput || pullErr.message + }); + } + + res.json({ + success: true, + message: 'Neues Image geladen. Container startet neu.', + output: pullOutput + }); + + setTimeout(() => { + runDocker(['restart', SYNC_CONTAINER], (restartErr, restartOutput) => { + if (restartErr) { + console.error('Sync restart failed:', restartOutput || restartErr.message); + return; + } + console.log('Sync restart complete:', restartOutput); + }); + }, 500); + }); +}); + // ==================== STATIC FILES ==================== app.get(/^\/images\/(fulls|thumbs)\/(.+)$/, serveProtectedImage); diff --git a/docker-compose.yml b/docker-compose.yml index 6597104..ce309ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: - INITIAL_ADMIN_PASSWORD=Start123 - FULLS_DIR=/app/public/images/fulls - THUMBS_DIR=/app/public/images/thumbs + - SYNC_IMAGE=ghcr.io/kroonk/photography:latest + - SYNC_CONTAINER=photography-website volumes: # Nextcloud-Ordner mit Originalbildern (read-only) - /volume1/nextcloud/data/Tom/files/Websitebilder:/app/public/images/fulls:ro From e2c83f5fe4f2cd8ac68c5926ccb51d2373f4d26c Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 10:43:49 +0200 Subject: [PATCH 03/10] Use Mischlabs container registry --- .github/workflows/docker-build.yml | 4 ++-- README.md | 4 ++-- docker-compose.yml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index a50c402..3f5586a 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -5,7 +5,7 @@ on: branches: [master] env: - REGISTRY: ghcr.io + REGISTRY: git.mischlabs.de jobs: build-and-push: @@ -18,7 +18,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Log in to GitHub Container Registry + - name: Log in to package registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} diff --git a/README.md b/README.md index b6d726b..92ab26a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To deploy on a NAS or Linux server, adjust the `docker-compose.yml` to match you ```yaml services: photography-website: - image: ghcr.io/kroonk/photography:latest + image: git.mischlabs.de/mrdiderot/photography:latest container_name: photography-website ports: - "8090:8090" @@ -28,7 +28,7 @@ services: - INITIAL_ADMIN_PASSWORD=Start123 - FULLS_DIR=/app/public/images/fulls - THUMBS_DIR=/app/public/images/thumbs - - SYNC_IMAGE=ghcr.io/kroonk/photography:latest + - SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest - SYNC_CONTAINER=photography-website volumes: - /path/to/nextcloud/data:/app/public/images/fulls:ro diff --git a/docker-compose.yml b/docker-compose.yml index ce309ca..0274133 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: photography: - image: ghcr.io/kroonk/photography:latest + image: git.mischlabs.de/mrdiderot/photography:latest container_name: photography-website ports: - "8090:8090" @@ -10,7 +10,7 @@ services: - INITIAL_ADMIN_PASSWORD=Start123 - FULLS_DIR=/app/public/images/fulls - THUMBS_DIR=/app/public/images/thumbs - - SYNC_IMAGE=ghcr.io/kroonk/photography:latest + - SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest - SYNC_CONTAINER=photography-website volumes: # Nextcloud-Ordner mit Originalbildern (read-only) @@ -32,7 +32,7 @@ services: command: photography-website volumes: - /var/run/docker.sock:/var/run/docker.sock - # GitHub Login von deinem NAS uebernehmen, um ghcr.io lesen zu duerfen + # Registry Login von deinem NAS uebernehmen, um private Images lesen zu duerfen - ~/.docker/config.json:/config.json:ro environment: - WATCHTOWER_CLEANUP=true From cb5c0f2581361bb192877a0d1e63cb414f36b3e1 Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 11:00:00 +0200 Subject: [PATCH 04/10] Install Docker CLI in build workflow --- .github/workflows/docker-build.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3f5586a..3ad053b 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -18,6 +18,28 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install Docker CLI + run: | + if command -v docker >/dev/null 2>&1; then + docker --version + exit 0 + fi + + if command -v apt-get >/dev/null 2>&1; then + apt-get update + apt-get install -y docker.io + elif command -v apk >/dev/null 2>&1; then + apk add --no-cache docker-cli + else + echo "No supported package manager found to install Docker CLI" + exit 1 + fi + + docker --version + + - name: Check Docker daemon + run: docker version + - name: Log in to package registry uses: docker/login-action@v3 with: @@ -25,6 +47,9 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Lowercase image name run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV From 9a2a8dca9f45466f4bc8cb2eeb56b3c9bac6d4c5 Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 11:02:32 +0200 Subject: [PATCH 05/10] Use sh shell for Gitea workflow --- .github/workflows/docker-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3ad053b..6b17a8a 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -10,6 +10,9 @@ env: jobs: build-and-push: runs-on: ubuntu-latest + defaults: + run: + shell: sh permissions: contents: read packages: write From 9512650a838e1a62ee40b8d9469ba2a306ffcff5 Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 11:05:09 +0200 Subject: [PATCH 06/10] Use registry token for image push --- .github/workflows/docker-build.yml | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 6b17a8a..7f3f995 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -6,6 +6,7 @@ on: env: REGISTRY: git.mischlabs.de + REGISTRY_IMAGE: git.mischlabs.de/mrdiderot/photography jobs: build-and-push: @@ -44,23 +45,27 @@ jobs: run: docker version - name: Log in to package registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + if [ -z "$REGISTRY_USERNAME" ] || [ -z "$REGISTRY_TOKEN" ]; then + echo "Missing registry credentials." + echo "Create repository secrets REGISTRY_USERNAME and REGISTRY_TOKEN." + echo "REGISTRY_TOKEN must be a Gitea access token with package write permission." + exit 1 + fi + + echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Lowercase image name - run: echo "IMAGE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV - - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + ${{ env.REGISTRY_IMAGE }}:latest + ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} From 6f879f4d4864dbecc1303f12b5ce29bf3a64a5cf Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 11:08:36 +0200 Subject: [PATCH 07/10] Use existing registry user secret --- .github/workflows/docker-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7f3f995..80a061a 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -46,17 +46,17 @@ jobs: - name: Log in to package registry env: - REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} run: | - if [ -z "$REGISTRY_USERNAME" ] || [ -z "$REGISTRY_TOKEN" ]; then + if [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_TOKEN" ]; then echo "Missing registry credentials." - echo "Create repository secrets REGISTRY_USERNAME and REGISTRY_TOKEN." + echo "Create repository secrets REGISTRY_USER and REGISTRY_TOKEN." echo "REGISTRY_TOKEN must be a Gitea access token with package write permission." exit 1 fi - echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USERNAME" --password-stdin + echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From 0efe3066fccea58307df82aec58cc4380f9f81a7 Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 11:17:24 +0200 Subject: [PATCH 08/10] Reduce Docker image layer size --- .dockerignore | 1 + Dockerfile | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.dockerignore b/.dockerignore index ad321ff..23c61aa 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ _site/ .git/ .claude/ node_modules/ +images/ .sass-cache/ .jekyll-metadata *.md diff --git a/Dockerfile b/Dockerfile index eb2f3df..950af6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,15 +12,26 @@ COPY . . RUN bundle exec jekyll build -# === Serve-Phase: node.js backend + ImageMagick === -FROM node:20-alpine +# === Node-Dependencies: native sqlite3 mit Build-Tools bauen === +FROM node:20-alpine AS node-deps -# ImageMagick fuer Thumbnails, Docker CLI fuer Admin-Sync, Build-Tools fuer sqlite3 native bindings -RUN apk add --no-cache imagemagick docker-cli python3 make g++ build-base sqlite-dev +RUN apk add --no-cache python3 make g++ sqlite-dev WORKDIR /app COPY package*.json ./ -RUN npm install --omit=dev +RUN npm ci --omit=dev && npm cache clean --force + +# === Serve-Phase: node.js backend + Runtime-Tools === +FROM node:20-alpine + +# Getrennte RUNs halten einzelne Registry-Layer klein genug fuer Proxy-Limits. +RUN apk add --no-cache docker-cli +RUN apk add --no-cache imagemagick +RUN apk add --no-cache sqlite-libs + +WORKDIR /app +COPY package*.json ./ +COPY --from=node-deps /app/node_modules ./node_modules COPY backend ./backend COPY --from=builder /site/_site ./public From caf56b411754e26850e499cf7238de9036500c91 Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 15:55:10 +0200 Subject: [PATCH 09/10] Skip image build on GitHub mirror --- .github/workflows/docker-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 80a061a..74cdda6 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -10,6 +10,7 @@ env: jobs: build-and-push: + if: ${{ github.server_url == 'https://git.mischlabs.de' }} runs-on: ubuntu-latest defaults: run: From a588bd36d6467c3f3f39ef1c91550ace18c4948d Mon Sep 17 00:00:00 2001 From: Tom Misch Date: Mon, 11 May 2026 16:09:31 +0200 Subject: [PATCH 10/10] Add admin dashboard card layout --- admin.html | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/admin.html b/admin.html index 459cdad..0a2fb65 100644 --- a/admin.html +++ b/admin.html @@ -6,7 +6,10 @@ @@ -34,7 +41,8 @@ -
+
+

Nutzer erstellen

@@ -62,6 +70,13 @@
+

Website aktualisieren

+

Laedt das neueste Docker-Image und startet den Website-Container neu.

+ + +
+ +

Nutzerverwaltung

@@ -69,7 +84,7 @@
IDUsernameRolleErstelltAktionen
-
+

Ordner-Zuweisung

Waehle einen Nutzer und klicke auf Ordner um sie zuzuweisen oder zu entfernen.