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:
1 parent
e12936444d
commit
fd44db2213
2357 files changed
+588
-514948
No files matched your search
@@ -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 = '';
|
||||
|
||||
Reference in new issue
Block a user