Update über den Git-Server, Tests, Repo ohne node_modules und Sicherungen

- Admin → Update: neuere Versionen (Tags) vom Git-Server anzeigen und per
  Knopf einspielen; ausgeführt vom Dienst auf dem Rechner
  (Docker/update-einrichten.sh, Docker/update-ausfuehren.sh), nur vorwärts,
  Fehler bei ausbleibendem Neustart, abgebrochene Läufe sperren nicht
- Tests (npm test): Zusammenführen paralleler Änderungen, Rechteprüfung
  beim Speichern; SESSIONS_DIR lenkt den Datenordner für Testserver um
- node_modules, Sicherungen und Uploads aus dem Repo genommen
- cors und requireRole() entfernt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-11-10 11:10:56 +01:00
1 parent e12936444d
commit fd44db2213
2357 files changed
+588 -514948

No files matched your search

+99 -14
View File
@@ -167,8 +167,8 @@ function findInvalidAuftragsstatus(week){
return null;
}
// Globale Middleware: CORS, JSON-Body-Limit (2 MB) und statisches Serving von public/ ohne Cache für Skripte/Styles
app.use(require('cors')());
// Globale Middleware: JSON-Body-Limit und statisches Serving von public/.
// Kein CORS: die App ruft nur sich selbst auf (baseUrl() = location.origin).
// Der JSON-Parser darf NICHT über die Routen mit rohem Binär-Body laufen:
// Der Browser schickt beim Datei-Upload den Content-Type der Datei mit, und
// bei einer .json-Datei würde express.json() den Datenstrom verbrauchen, bevor
@@ -278,7 +278,9 @@ function appSenden(req, res){
}
// Datenverzeichnis sessions/ bei Bedarf anlegen (bind-mounted in Docker, enthält die DB)
const SESSIONS_DIR = path.join(__dirname, 'sessions');
// SESSIONS_DIR: Datenordner (Sicherungen, Uploads, Update-Auftrag). Für Tests
// umlenkbar, sonst schriebe ein Testserver Sicherungen in den echten Ordner.
const SESSIONS_DIR = process.env.SESSIONS_DIR ? path.resolve(process.env.SESSIONS_DIR) : path.join(__dirname, 'sessions');
if(!fs.existsSync(SESSIONS_DIR)) fs.mkdirSync(SESSIONS_DIR, { recursive: true });
// SQLite-Datenbank öffnen (wird angelegt, falls nicht vorhanden). Der Pfad ist
@@ -781,7 +783,7 @@ const RECHTE = [
['a.checker', 'Plan-Checker-Merkmale', ''], ['a.betrieb', 'Betrieb & Sicherheit', ''],
['a.papierkorb', 'Papierkorb', ''], ['a.backup', 'Datenbank-Backup', ''],
['a.import', 'Datenbank-Import', ''], ['a.rollen', 'Rollen & Rechte', ''],
['a.teams', 'Teams-Anbindung', ''] ]},
['a.teams', 'Teams-Anbindung', ''], ['a.update', 'Update einspielen', ''] ]},
];
const RECHT_LABEL = new Map(RECHTE.flatMap(g => g.rechte.map(r => [r[0], r[1]])));
const RECHT_KEYS = new Set(RECHT_LABEL.keys());
@@ -3776,9 +3778,7 @@ const sitzungZuletztGeschrieben = new Map();
// (Registrierung via app.use(attachUser) erfolgt bewusst weiter oben, direkt
// nach express.json() und vor express.static — siehe dort.)
// requireAuth / requireRole: Middleware-Fabriken für spätere Phasen, die damit
// bestehende Routen absichern. In dieser Phase werden sie auf KEINE bestehende
// Route angewendet.
// requireAuth: angemeldet und nicht nur Wandtafel. Rechte prüft requireRecht().
function requireAuth(req, res, next){
if(!req.user) return res.status(401).json({ ok:false, error:'Nicht angemeldet.' });
// Rollen ohne „Anwendung nutzen“ (früher fest: 'kiosk') sind überall, wo
@@ -3801,13 +3801,6 @@ function requireAuthOderKiosk(req, res, next){
if(!req.user) return res.status(401).json({ ok:false, error:'Nicht angemeldet.' });
next();
}
function requireRole(role){
return function(req, res, next){
if(!req.user) return res.status(401).json({ ok:false, error:'Nicht angemeldet.' });
if(req.user.role !== role) return res.status(403).json({ ok:false, error:'Keine Berechtigung.' });
next();
};
}
// Route: Login — prüft Benutzername/Passwort, legt bei Erfolg eine neue Session
// an und setzt das Session-Cookie. Liefert bei falschem Benutzernamen, falschem
@@ -4848,6 +4841,98 @@ function teamsNeueAufgaben(weekLabel, neu, user, req){
});
}
// ---------- Update über den Git-Server (Admin → Update) ----------
//
// Versionen sind die Tags vX.Y.Z des Repos. GET fragt den Git-Server (Gitea,
// Adresse aus UPDATE_REPO_API, z. B. http://host:3000/api/v1/repos/erik/
// projektuebersicht) nur auf Anfrage nach neueren. POST legt einen Auftrag
// in sessions/update/auftrag.json ab — ausgeführt wird er NICHT im Container
// (der darf kein Docker steuern), sondern vom Dienst auf dem Rechner selbst
// (Docker/update-einrichten.sh → Docker/update-ausfuehren.sh). Der schreibt
// status.json (Fortschritt) und installiert.json (laufende Version).
const UPDATE_DIR = path.join(SESSIONS_DIR, 'update');
const UPDATE_VERSION_MUSTER = /^v\d+\.\d+\.\d+$/;
function updateJsonLesen(name){
try { return JSON.parse(fs.readFileSync(path.join(UPDATE_DIR, name), 'utf8')); } catch(e){ return null; }
}
// updateLaeuft: läuft gerade ein Update? Ein Eintrag älter als eine Stunde
// gilt als abgebrochen (z. B. Neustart des Pi mitten im Bauen) — sonst
// sperrte er alle weiteren Updates für immer.
const UPDATE_MAX_MS = 60 * 60 * 1000;
function updateLaeuft(status){
return !!(status && status.laeuft && Date.now() - Date.parse(status.am || 0) < UPDATE_MAX_MS);
}
// versionVergleich: "v1.10.0" > "v1.9.3". Rückgabe: <0, 0, >0.
function versionVergleich(a, b){
const z = v => String(v).replace(/^v/, '').split('.').map(Number);
const x = z(a), y = z(b);
for(let i = 0; i < 3; i++){ if((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) - (y[i] || 0); }
return 0;
}
// updateTags: Versionen vom Git-Server, neueste zuerst.
async function updateTags(){
const api = String(process.env.UPDATE_REPO_API || '').replace(/\/+$/, '');
if(!api) return null;
const r = await fetch(api + '/tags?limit=50', { signal: AbortSignal.timeout(8000) });
if(!r.ok) throw new Error('Git-Server antwortet mit ' + r.status);
const liste = await r.json();
return (Array.isArray(liste) ? liste : [])
.filter(t => UPDATE_VERSION_MUSTER.test(t && t.name))
.map(t => ({ version: t.name, text: String(t.message || '').trim(), datum: (t.commit && t.commit.created) || '' }))
.sort((a, b) => versionVergleich(b.version, a.version));
}
app.get('/admin/update', requireRecht('a.update'), async (req, res) => {
const installiert = updateJsonLesen('installiert.json');
let status = updateJsonLesen('status.json');
if(status && status.laeuft && !updateLaeuft(status)){
status = Object.assign({}, status, { laeuft: false, fehler: 'Abgebrochen (keine Rückmeldung seit über einer Stunde). Bitte update.log auf dem Rechner prüfen.' });
}
const auftrag = updateJsonLesen('auftrag.json');
const basis = { ok: true, installiert, status, auftragOffen: !!auftrag,
dienst: !!updateJsonLesen('dienst.json'), eingerichtet: !!process.env.UPDATE_REPO_API };
if(status && status.fehler){
try { basis.log = fs.readFileSync(path.join(UPDATE_DIR, 'update.log'), 'utf8').split('\n').slice(-20).join('\n'); } catch(e){ /* kein Protokoll */ }
}
if(!req.query.suchen) return res.json(basis);
try {
const tags = await updateTags();
const aktuell = installiert && installiert.version;
const neuer = (tags || []).filter(t => !aktuell || versionVergleich(t.version, aktuell) > 0);
res.json(Object.assign(basis, { neueste: tags && tags[0] ? tags[0].version : '', neuer }));
} catch(err){
res.json(Object.assign(basis, { fehler: 'Git-Server nicht erreichbar: ' + (err.message || err) }));
}
});
// POST {version}: Auftrag für den Update-Dienst. Nur Versionen, die der
// Git-Server tatsächlich kennt; nicht, solange ein Update läuft.
app.post('/admin/update', requireRecht('a.update'), async (req, res) => {
try {
const version = String((req.body || {}).version || '');
if(!UPDATE_VERSION_MUSTER.test(version)) return res.status(400).json({ ok: false, error: 'Ungültige Version.' });
if(!updateJsonLesen('dienst.json')) return res.status(409).json({ ok: false, error: 'Der Update-Dienst ist auf diesem Rechner nicht eingerichtet.' });
if(updateJsonLesen('auftrag.json') || updateLaeuft(updateJsonLesen('status.json'))) return res.status(409).json({ ok: false, error: 'Es läuft bereits ein Update.' });
// Nur vorwärts: Älterer Code kennt neuere Spalten nicht und schriebe sie
// beim nächsten Speichern mit Vorgabewerten zurück (Datenverlust).
const inst = updateJsonLesen('installiert.json');
if(!inst || !UPDATE_VERSION_MUSTER.test(inst.version || '')) return res.status(409).json({ ok: false, error: 'Die installierte Version ist unbekannt. Bitte den Update-Dienst neu einrichten (Docker/update-einrichten.sh).' });
if(versionVergleich(version, inst.version) <= 0) return res.status(409).json({ ok: false, error: 'Nur neuere Versionen als ' + inst.version + ' lassen sich installieren.' });
const tags = await updateTags();
if(!tags || !tags.some(t => t.version === version)) return res.status(400).json({ ok: false, error: 'Diese Version gibt es auf dem Git-Server nicht.' });
fs.mkdirSync(UPDATE_DIR, { recursive: true });
fs.writeFileSync(path.join(UPDATE_DIR, 'auftrag.json'), JSON.stringify({
version, von: req.user.displayName || req.user.username, am: new Date().toISOString()
}));
console.log('Update auf ' + version + ' angefordert von ' + (req.user.username || req.user.id));
res.json({ ok: true });
} catch(err){
console.error('Update anfordern fehlgeschlagen:', err);
res.status(500).json({ ok: false, error: String(err.message || err) });
}
});
app.get('/admin/teams', requireRecht('a.teams'), (req, res) => {
const url = integrationLesen('teams_webhook');
let host = '';