From 64fe667240118c288bfca40efeebb1e0bba1f037 Mon Sep 17 00:00:00 2001 From: Kroonk Date: Wed, 20 May 2026 18:54:10 +0200 Subject: [PATCH] Fix Photography deploy metadata and SQLite migration --- Brain.md | 12 ++-- Feature-Request.md | 2 +- README.md | 10 +-- backend/database.js | 146 +++++++++++++++++++++++++++++--------------- docker-compose.yml | 2 - package.json | 6 +- 6 files changed, 112 insertions(+), 66 deletions(-) diff --git a/Brain.md b/Brain.md index f7f83e3..bb427a4 100644 --- a/Brain.md +++ b/Brain.md @@ -10,7 +10,7 @@ Keine 1:1 Kopie, sondern eine individuell angepasste Version mit eigenen Ansprue - **Domain**: tom.mischlabs.de - **E-Mail**: tom@mischlabs.de - **Instagram**: @Mischkomposition -- **GitHub Repo**: https://github.com/Kroonk/Photography.git +- **Gitea Repo**: https://git.mischlabs.de/MrDiderot/Photography.git - **Lokaler Pfad**: d:\Vibecoding\Website\Photography --- @@ -114,10 +114,10 @@ Photography/ - 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 +### Initialer Admin +- Es wird kein festes Standardkonto mehr angelegt. +- Bestehende Installationen behalten Nutzer und Rollen im persistenten SQLite-Volume. +- Fuer frische Installationen kann einmalig `INITIAL_ADMIN_USERNAME=MrDiderot` zusammen mit `INITIAL_ADMIN_PASSWORD_HASH` gesetzt werden. ### API-Endpunkte | Endpunkt | Methode | Beschreibung | @@ -177,7 +177,7 @@ Photography/ 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 +4. Gitea Actions baut automatisch Docker Image -> lokales Gitea Container Registry 5. Watchtower auf NAS erkennt neues Image (alle 5 Min) 6. Watchtower aktualisiert Container automatisch 7. Fertig - Website aktualisiert ohne SSH! diff --git a/Feature-Request.md b/Feature-Request.md index 2dc3148..0844464 100644 --- a/Feature-Request.md +++ b/Feature-Request.md @@ -51,7 +51,7 @@ Erweitertes Nutzermanagement mit 3 Rollen und ordnerbasierter Bildzuweisung. ### 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. +Nach einem `git push` baut Gitea Actions das neue Image in der lokalen Gitea Container Registry und Watchtower aktualisiert den Container automatisch. ### Anforderungen - Automatisches Update ohne SSH oder manuellen Eingriff diff --git a/README.md b/README.md index 92ab26a..06318a6 100644 --- a/README.md +++ b/README.md @@ -11,21 +11,19 @@ The project features a fast static frontend (based on HTML5 UP Multiverse) power - **Light/Dark Mode:** Dynamic theme toggle saving preferences in the browser. ## Deployment Setup (Docker) -This repository is automatically built into a Docker container via GitHub Actions. +This repository is automatically built into a Docker container via Gitea Actions and pushed to the local Gitea container registry. To deploy on a NAS or Linux server, adjust the `docker-compose.yml` to match your Nextcloud mount: ```yaml services: - photography-website: + photography: image: git.mischlabs.de/mrdiderot/photography:latest 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 - SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest @@ -40,12 +38,14 @@ 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: +For a fresh installation, create the initial admin with explicit environment variables only once. Use a bcrypt hash instead of storing a plain password in `docker-compose.yml`: ```bash node -e "const bcrypt = require('bcryptjs'); console.log(bcrypt.hashSync('your-admin-password', 10));" ``` +Then start once with `INITIAL_ADMIN_USERNAME=MrDiderot` and `INITIAL_ADMIN_PASSWORD_HASH=`. Existing installations keep their SQLite users in the persistent `database` volume. + 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 diff --git a/backend/database.js b/backend/database.js index c373f9e..3964916 100644 --- a/backend/database.js +++ b/backend/database.js @@ -7,6 +7,57 @@ const db = new sqlite3.Database(dbPath); console.log('Using database at', dbPath); +function bootstrapInitialAdmin() { + 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) { + return; + } + if (!username || !passwordHash) { + console.warn('Set INITIAL_ADMIN_USERNAME together with 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}`); + }); + }); +} + +function runMigrations(migrations, index = 0) { + if (index >= migrations.length) { + bootstrapInitialAdmin(); + return; + } + + migrations[index](() => runMigrations(migrations, index + 1)); +} + db.serialize(() => { // Users: mit created_at fuer Client-Ablauf (30 Tage) db.run(`CREATE TABLE IF NOT EXISTS users ( @@ -17,17 +68,53 @@ db.serialize(() => { created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); - // Migration: Spalten hinzufuegen falls Tabelle schon existiert (alte DB ohne role/created_at) - db.run(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'client'`, (err) => { - // Ignoriere Fehler wenn Spalte schon existiert - if (!err) { - // Alte Admins (die einzigen User vor dem Update) als admin markieren - db.run(`UPDATE users SET role = 'admin' WHERE id = 1`); - console.log('Migration: role-Spalte hinzugefuegt, User ID 1 als admin gesetzt'); + // Migration: Spalten hinzufuegen falls Tabelle schon existiert (alte DB ohne role/created_at). + // SQLite erlaubt CURRENT_TIMESTAMP nicht als DEFAULT bei ALTER TABLE, deshalb wird created_at + // ohne Default angelegt und danach fuer bestehende Nutzer befuellt. + db.all(`PRAGMA table_info(users)`, (err, columns) => { + if (err) { + console.error('User-Migration fehlgeschlagen:', err.message); + return; } - }); - db.run(`ALTER TABLE users ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP`, (err) => { - // Ignoriere Fehler wenn Spalte schon existiert + + const columnNames = new Set(columns.map((column) => column.name)); + + const migrations = []; + + if (!columnNames.has('role')) { + migrations.push((done) => db.run(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'client'`, (alterErr) => { + if (alterErr) { + console.error('Migration role fehlgeschlagen:', alterErr.message); + done(); + return; + } + + // Alte Admins (die einzigen User vor dem Update) als admin markieren. + db.run(`UPDATE users SET role = 'admin' WHERE id = 1`, () => { + console.log('Migration: role-Spalte hinzugefuegt, User ID 1 als admin gesetzt'); + done(); + }); + })); + } + + if (!columnNames.has('created_at')) { + migrations.push((done) => db.run(`ALTER TABLE users ADD COLUMN created_at DATETIME`, (alterErr) => { + if (alterErr) { + console.error('Migration created_at fehlgeschlagen:', alterErr.message); + done(); + return; + } + + db.run(`UPDATE users SET created_at = datetime('now') WHERE created_at IS NULL`, () => { + console.log('Migration: created_at-Spalte hinzugefuegt'); + done(); + }); + })); + } else { + migrations.push((done) => db.run(`UPDATE users SET created_at = datetime('now') WHERE created_at IS NULL`, done)); + } + + runMigrations(migrations); }); // Ordner-Zuweisung (neu) @@ -46,45 +133,6 @@ db.serialize(() => { UNIQUE(user_id, image_name) )`); - // 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}`); - } - ); - }); - }); }); module.exports = db; diff --git a/docker-compose.yml b/docker-compose.yml index 0274133..efcee3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,6 @@ services: - "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 - SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest diff --git a/package.json b/package.json index 1b0a15d..6b3e739 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Kroonk/Photography.git" + "url": "git+https://git.mischlabs.de/MrDiderot/Photography.git" }, "keywords": [ "photography", @@ -18,9 +18,9 @@ "author": "Tom Misch", "license": "GPL-3.0", "bugs": { - "url": "https://github.com/Kroonk/Photography/issues" + "url": "https://git.mischlabs.de/MrDiderot/Photography/issues" }, - "homepage": "https://github.com/Kroonk/Photography#readme", + "homepage": "https://tom.mischlabs.de", "dependencies": { "archiver": "^7.0.1", "bcryptjs": "^2.4.3",