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
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 33s
This commit is contained in:
12
Brain.md
12
Brain.md
@@ -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!
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
10
README.md
10
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.
|
- **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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user