Fix Photography deploy metadata and SQLite migration
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 33s

This commit is contained in:
Kroonk
2026-05-20 18:54:10 +02:00
parent e6a11f2eed
commit 64fe667240
6 changed files with 112 additions and 66 deletions

View File

@@ -10,7 +10,7 @@ Keine 1:1 Kopie, sondern eine individuell angepasste Version mit eigenen Ansprue
- **Domain**: tom.mischlabs.de - **Domain**: tom.mischlabs.de
- **E-Mail**: tom@mischlabs.de - **E-Mail**: tom@mischlabs.de
- **Instagram**: @Mischkomposition - **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 - **Lokaler Pfad**: d:\Vibecoding\Website\Photography
--- ---
@@ -114,10 +114,10 @@ Photography/
- Zuweisung erfolgt auf Ordner-Ebene (nicht einzelne Bilder) - Zuweisung erfolgt auf Ordner-Ebene (nicht einzelne Bilder)
- Tabelle `user_folders` speichert Zuweisungen (user_id, folder_name) - Tabelle `user_folders` speichert Zuweisungen (user_id, folder_name)
### Standard-Admin ### Initialer Admin
- **Username**: `admin` - Es wird kein festes Standardkonto mehr angelegt.
- **Passwort**: `admin123` (SOFORT AENDERN nach erstem Login!) - Bestehende Installationen behalten Nutzer und Rollen im persistenten SQLite-Volume.
- **Rolle**: admin - Fuer frische Installationen kann einmalig `INITIAL_ADMIN_USERNAME=MrDiderot` zusammen mit `INITIAL_ADMIN_PASSWORD_HASH` gesetzt werden.
### API-Endpunkte ### API-Endpunkte
| Endpunkt | Methode | Beschreibung | | Endpunkt | Methode | Beschreibung |
@@ -177,7 +177,7 @@ Photography/
1. Aenderungen lokal machen (Code, Design) 1. Aenderungen lokal machen (Code, Design)
2. `gulp build` ausfuehren (JS + CSS minifizieren) 2. `gulp build` ausfuehren (JS + CSS minifizieren)
3. Git commit & push nach master 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) 5. Watchtower auf NAS erkennt neues Image (alle 5 Min)
6. Watchtower aktualisiert Container automatisch 6. Watchtower aktualisiert Container automatisch
7. Fertig - Website aktualisiert ohne SSH! 7. Fertig - Website aktualisiert ohne SSH!

View File

@@ -51,7 +51,7 @@ Erweitertes Nutzermanagement mit 3 Rollen und ordnerbasierter Bildzuweisung.
### Beschreibung ### Beschreibung
Watchtower laeuft als separater Docker-Container und prueft alle 5 Minuten auf neue Images. 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 ### Anforderungen
- Automatisches Update ohne SSH oder manuellen Eingriff - Automatisches Update ohne SSH oder manuellen Eingriff

View File

@@ -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. - **Light/Dark Mode:** Dynamic theme toggle saving preferences in the browser.
## Deployment Setup (Docker) ## 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: To deploy on a NAS or Linux server, adjust the `docker-compose.yml` to match your Nextcloud mount:
```yaml ```yaml
services: services:
photography-website: photography:
image: git.mischlabs.de/mrdiderot/photography:latest image: git.mischlabs.de/mrdiderot/photography:latest
container_name: photography-website container_name: photography-website
ports: ports:
- "8090:8090" - "8090:8090"
environment: environment:
- JWT_SECRET=replace-with-a-long-random-secret - JWT_SECRET=replace-with-a-long-random-secret
- INITIAL_ADMIN_USERNAME=MrDiderot
- INITIAL_ADMIN_PASSWORD=Start123
- FULLS_DIR=/app/public/images/fulls - FULLS_DIR=/app/public/images/fulls
- THUMBS_DIR=/app/public/images/thumbs - THUMBS_DIR=/app/public/images/thumbs
- SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest - SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest
@@ -40,12 +38,14 @@ services:
Run `docker compose up -d` to launch the site. 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 ```bash
node -e "const bcrypt = require('bcryptjs'); console.log(bcrypt.hashSync('your-admin-password', 10));" 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=<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. 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 ## Credits & License

View File

@@ -7,6 +7,57 @@ const db = new sqlite3.Database(dbPath);
console.log('Using database at', 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(() => { db.serialize(() => {
// Users: mit created_at fuer Client-Ablauf (30 Tage) // Users: mit created_at fuer Client-Ablauf (30 Tage)
db.run(`CREATE TABLE IF NOT EXISTS users ( db.run(`CREATE TABLE IF NOT EXISTS users (
@@ -17,17 +68,53 @@ db.serialize(() => {
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`); )`);
// Migration: Spalten hinzufuegen falls Tabelle schon existiert (alte DB ohne role/created_at) // 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) => { // SQLite erlaubt CURRENT_TIMESTAMP nicht als DEFAULT bei ALTER TABLE, deshalb wird created_at
// Ignoriere Fehler wenn Spalte schon existiert // ohne Default angelegt und danach fuer bestehende Nutzer befuellt.
if (!err) { db.all(`PRAGMA table_info(users)`, (err, columns) => {
// Alte Admins (die einzigen User vor dem Update) als admin markieren if (err) {
db.run(`UPDATE users SET role = 'admin' WHERE id = 1`); console.error('User-Migration fehlgeschlagen:', err.message);
console.log('Migration: role-Spalte hinzugefuegt, User ID 1 als admin gesetzt'); return;
} }
});
db.run(`ALTER TABLE users ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP`, (err) => { const columnNames = new Set(columns.map((column) => column.name));
// Ignoriere Fehler wenn Spalte schon existiert
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) // Ordner-Zuweisung (neu)
@@ -46,45 +133,6 @@ db.serialize(() => {
UNIQUE(user_id, image_name) 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; module.exports = db;

View File

@@ -6,8 +6,6 @@ services:
- "8090:8090" - "8090:8090"
environment: environment:
- JWT_SECRET=${JWT_SECRET:?set JWT_SECRET in your 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 - FULLS_DIR=/app/public/images/fulls
- THUMBS_DIR=/app/public/images/thumbs - THUMBS_DIR=/app/public/images/thumbs
- SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest - SYNC_IMAGE=git.mischlabs.de/mrdiderot/photography:latest

View File

@@ -8,7 +8,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/Kroonk/Photography.git" "url": "git+https://git.mischlabs.de/MrDiderot/Photography.git"
}, },
"keywords": [ "keywords": [
"photography", "photography",
@@ -18,9 +18,9 @@
"author": "Tom Misch", "author": "Tom Misch",
"license": "GPL-3.0", "license": "GPL-3.0",
"bugs": { "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": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",