diff --git a/WIEDERHERSTELLUNG.md b/WIEDERHERSTELLUNG.md index 55367a21..44308192 100644 --- a/WIEDERHERSTELLUNG.md +++ b/WIEDERHERSTELLUNG.md @@ -27,6 +27,47 @@ jede Nacht ab 3 Uhr, geprüft direkt danach. --- +## Fall 0: „Ich komme nicht mehr rein" + +Der häufigste Notfall — und der einzige, bei dem nichts kaputt ist. Passiert +am 02.09.2026: neuen Code für sich selbst erzeugt, im selben Moment abgemeldet, +Code weg, bevor er abgeschrieben war. + +**Der alte Code ist nicht wiederherstellbar.** In der Datenbank steht nur seine +Prüfsumme, nirgends der Klartext — auch nicht in den Sicherungen. Es gibt nur +den Weg nach vorn: einen neuen setzen. + +Auf dem Server, angemeldet als `dogi`, **ein Block**: + +``` +sudo -u dogiweb bash /home/dogiweb/dogfather-universe/tools/notfall-code.sh +``` + +Für jemand anderen den Namen anhängen, z. B. +`... notfall-code.sh "Cigdem"`. Ohne Namen nimmt es DogFather. Ein falscher +Name schadet nichts — das Skript listet dann auf, wen es gibt. + +Das Skript setzt einen neuen Code, zeigt ihn an und löscht die Sperre nach +acht Fehlversuchen gleich mit (die hätte sonst 10 Minuten gekostet). + +**Das `-u dogiweb` muss dranbleiben.** Als `root` ausgeführt gehören die +Hilfsdateien der Datenbank danach `root`, und der Dienst kann nicht mehr +schreiben — dann ist wirklich etwas kaputt. + +Danach: Code **sofort nach Bitwarden**, nicht in eine Notiz, nicht in einen Chat. + +**Warum es keinen fest hinterlegten Notfall-Code in der Anwendung gibt:** Der +wäre eine Hintertür, die dauerhaft offensteht — jeden Tag, für jeden, der sie +findet, ohne dass es auffällt. Dieser Weg verlangt Zugang zum Server, +hinterlässt einen Protokolleintrag, und es gibt nichts zu erraten. + +**Seit dem 02.09.2026 sollte er kaum noch nötig sein:** Wer seinen eigenen Code +erneuert, bleibt in dieser Sitzung angemeldet (alle anderen Geräte fliegen +weiterhin hinaus), und der Kasten mit dem Code verschwindet nicht mehr von +selbst. Festgehalten in `server/pruef-code.mjs`. + +--- + ## Fall 1: „Ich habe aus Versehen etwas gelöscht" Nichts überstürzen. Die Sicherung von heute Nacht hat es noch. diff --git a/server/pruef-code.mjs b/server/pruef-code.mjs new file mode 100644 index 00000000..53e3ebf9 --- /dev/null +++ b/server/pruef-code.mjs @@ -0,0 +1,190 @@ +/* ===================================================================== + Prüfung: einen neuen Zugangscode erzeugen, ohne sich auszusperren. + + ANLASS (02.09.2026, echter Vorfall): Filipe hat sich einen neuen Code + erzeugt. Der Server beendete daraufhin ALLE Sitzungen -- auch die, in + der der Code gerade auf dem Bildschirm stand. Der nächste Aufruf lief + in ein "nicht angemeldet", die Seite sprang zur Anmeldung, und der + Code war weg, bevor er abgeschrieben war. Danach kam er nur noch über + einen Eingriff auf dem Server wieder hinein. + + Diese Datei hält beide Enden fest: + + * Wer seinen EIGENEN Code erneuert, bleibt in dieser Sitzung + angemeldet -- und nur in dieser. + * Alle ANDEREN Geräte fliegen weiterhin hinaus. Das ist der Sinn + eines neuen Codes und darf nicht mit verlorengehen. + + Die zweite Zeile ist die wichtigere. Ohne sie wäre die Reparatur + ("Sitzung bleibt bestehen") auf dem besten Weg, aus einem neuen Code + eine Formalie zu machen, die niemanden mehr aussperrt. + ===================================================================== */ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-code-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); +process.env.PORT = "4195"; +process.env.SITE_ACCESS_SECRET = "lokaler-test"; +process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; +process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; +process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; + +const express = (await import("express")).default; +const ec = express.response.cookie; +express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; +await import("./index.js"); +await new Promise((r) => setTimeout(r, 700)); +const BASIS = "http://127.0.0.1:4195"; +setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 240_000).unref?.(); + +let fehler = 0; +let geprueft = 0; +const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +const { DatabaseSync } = await import("node:sqlite"); +const { scryptSync, randomBytes } = await import("node:crypto"); +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); + +function anlegen(name, rolle, code) { + const salt = randomBytes(16).toString("hex"); + const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); + d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") + .run(name, rolle, hash, salt, 32768, jetzt); + return d.prepare("SELECT last_insert_rowid() AS id").get().id; +} +const idDogi = anlegen("Dogfather", "admin", "CODE-DOGI-0001"); +const idTili = anlegen("Tili", "creator", "CODE-TILI-0001"); +const zahl = (sql, ...w) => d.prepare(sql).get(...w)?.n ?? 0; + +const { chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); +const browser = await chromium.launch(); + +async function anmelden(rolle, code) { + const kontext = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); + const seite = await kontext.newPage(); + await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); + await seite.click(`.rolle[data-rolle="${rolle}"]`); + await seite.fill("#code", code); + await seite.click("button[type=submit]"); + await seite.waitForURL("**/start.html", { timeout: 15000 }); + return seite; +} + +const api = (seite, pfad, optionen) => seite.evaluate(async ([p, o]) => { + const a = await fetch(p, { + credentials: "same-origin", + ...(o || {}), + headers: o?.koerper ? { "Content-Type": "application/json" } : undefined, + body: o?.koerper ? JSON.stringify(o.koerper) : undefined, + }); + return { status: a.status, daten: await a.json().catch(() => ({})) }; +}, [pfad, optionen]); + +/* Zwei Geräte derselben Person: der Rechner, an dem gearbeitet wird, + und ein zweites (Handy). Genau diese Unterscheidung ist der Kern. */ +const rechner = await anmelden("admin", "CODE-DOGI-0001"); +const handy = await anmelden("admin", "CODE-DOGI-0001"); +ok(zahl("SELECT COUNT(*) AS n FROM sitzungen WHERE person_id = ?", idDogi) === 2, + "zwei offene Sitzungen derselben Person"); + +console.log("\n— Den eigenen Code erneuern —"); + +const abgelehnt = await api(rechner, `/workspace/api/verwaltung/personen/${idDogi}/code`, { + method: "POST", koerper: {}, +}); +ok(abgelehnt.status === 400, + "ohne ausdrückliche Bestätigung wird der eigene Code nicht getauscht"); + +const neu = await api(rechner, `/workspace/api/verwaltung/personen/${idDogi}/code`, { + method: "POST", koerper: { auch_mich: true }, +}); +ok(neu.status === 200 && /^[A-Z0-9]{4}(-[A-Z0-9]{4}){3}$/.test(neu.daten.code || ""), + `der neue Code kommt zurück (${neu.daten.code})`); + +/* DAS IST DER FEHLER VON DAMALS. */ +const nachher = await api(rechner, "/workspace/api/ich"); +ok(nachher.status === 200, + "diese Sitzung ist NOCH GÜLTIG – hier ging der Zugang verloren"); + +/* GEGENPROBE: Ohne sie wäre die Zeile darüber auch dann grün, wenn gar + keine Sitzung mehr beendet würde -- und ein neuer Code hätte + aufgehört, alte Zugänge zu schließen. */ +const aufDemHandy = await api(handy, "/workspace/api/ich"); +ok(aufDemHandy.status === 401, + "GEGENPROBE: das zweite Gerät ist abgemeldet – der neue Code sperrt weiterhin aus"); +ok(zahl("SELECT COUNT(*) AS n FROM sitzungen WHERE person_id = ?", idDogi) === 1, + "genau EINE Sitzung ist übrig, nicht null und nicht zwei"); + +console.log("\n— Der neue Code gilt, der alte nicht mehr —"); + +const mitNeuem = await anmelden("admin", neu.daten.code); +ok(!!mitNeuem, "mit dem neuen Code kommt man herein"); + +const alt = await rechner.evaluate(async ([basis, code]) => { + const a = await fetch(basis + "/workspace/api/anmelden", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ rolle: "admin", code }), + }); + return a.status; +}, [BASIS, "CODE-DOGI-0001"]); +ok(alt === 401, "mit dem alten Code nicht mehr"); + +console.log("\n— Fremden Code erneuern: dort fliegt alles hinaus —"); + +const tiliSeite = await anmelden("creator", "CODE-TILI-0001"); +ok(zahl("SELECT COUNT(*) AS n FROM sitzungen WHERE person_id = ?", idTili) === 1, + "Tili ist angemeldet"); +const fremd = await api(rechner, `/workspace/api/verwaltung/personen/${idTili}/code`, { + method: "POST", koerper: {}, +}); +ok(fremd.status === 200, "DogFather erneuert Tilis Code"); +ok(zahl("SELECT COUNT(*) AS n FROM sitzungen WHERE person_id = ?", idTili) === 0, + "Tilis Sitzungen sind ALLE beendet – die Ausnahme gilt nur für einen selbst"); +ok((await api(tiliSeite, "/workspace/api/ich")).status === 401, + "und ihr Gerät merkt es sofort"); + +console.log("\n— Der Kasten mit dem Code bleibt stehen —"); + +await rechner.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); +await rechner.waitForTimeout(800); +/* Die Rückfrage bestätigen, ohne sie anzuklicken. */ +rechner.on("dialog", (dlg) => dlg.accept()); + +const knopf = rechner.locator('.person', { hasText: "Dogfather" }) + .locator('button', { hasText: "Neuer Code" }).first(); +await knopf.click(); +await rechner.waitForTimeout(1500); + +const kasten = await rechner.evaluate(() => ({ + sichtbar: !document.getElementById("code-kasten").hidden, + code: document.getElementById("code-wert")?.textContent || "", + adresse: location.pathname, +})); +ok(kasten.sichtbar, "der Kasten mit dem Code ist zu sehen"); +ok(/^[A-Z0-9]{4}(-[A-Z0-9]{4}){3}$/.test(kasten.code.trim()), + `und der Code steht wirklich darin (${kasten.code.trim()})`); +ok(kasten.adresse.endsWith("/personen.html"), + "die Seite ist NICHT zur Anmeldung gesprungen – genau das war der Vorfall"); + +/* Auch nach dem Neuladen der Liste bleibt er stehen. */ +await rechner.waitForTimeout(1200); +const nochDa = await rechner.evaluate(() => + !document.getElementById("code-kasten").hidden + && (document.getElementById("code-wert")?.textContent || "").length > 8); +ok(nochDa, "und er steht auch noch da, nachdem die Liste neu geladen wurde"); + +await rechner.screenshot({ path: "pruef-code.png", fullPage: false }); + +const ERWARTET = 16; +console.log(""); +ok(geprueft >= ERWARTET, `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`); + +await browser.close(); +d.close(); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-code.png b/server/pruef-code.png new file mode 100644 index 00000000..9464944e Binary files /dev/null and b/server/pruef-code.png differ diff --git a/server/pruef-personen.png b/server/pruef-personen.png new file mode 100644 index 00000000..eed12a12 Binary files /dev/null and b/server/pruef-personen.png differ diff --git a/server/pruef-rollen-creator.png b/server/pruef-rollen-creator.png index 9df28f5d..4df57f82 100644 Binary files a/server/pruef-rollen-creator.png and b/server/pruef-rollen-creator.png differ diff --git a/server/pruef-rollen-scout.png b/server/pruef-rollen-scout.png index eeced4a5..1be40d4f 100644 Binary files a/server/pruef-rollen-scout.png and b/server/pruef-rollen-scout.png differ diff --git a/server/workspace-personen.js b/server/workspace-personen.js index 2cb4481f..8e9259dd 100644 --- a/server/workspace-personen.js +++ b/server/workspace-personen.js @@ -15,7 +15,7 @@ import express from "express"; import { - db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, ROLLEN_SORTIERUNG, ROLLEN_REIHE, + db, protokolliere, echteIp, sitzungLesen, personAnlegen, codeNeu, sitzungToken, personSperren, betreuungSetzen, scoutZuteilungSetzen, istLeitung, istDogFather, ROLLEN_SORTIERUNG, ROLLEN_REIHE, } from "./workspace.js"; import { sicherungJetzt } from "./workspace-sicherung.js"; @@ -297,15 +297,21 @@ personenRouter.post("/workspace/api/verwaltung/personen/:id/code", gleicheHerkun try { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ fehler: "ungueltig" }); - if (id === req.person.id) { - /* Sich selbst den Code zu tauschen wirft einen sofort hinaus (alle - Sitzungen werden beendet). Erlaubt, aber nur bewusst. */ + const eigener = id === req.person.id; + if (eigener) { + /* Den eigenen Code zu tauschen bleibt eine bewusste Entscheidung -- + alle anderen Geräte werden abgemeldet, und der alte Code gilt + nicht mehr. Seit dem 02.09.2026 wirft es einen aber NICHT mehr + selbst hinaus: Diese eine Sitzung bleibt, damit man den neuen + Code in Ruhe abschreiben kann (siehe codeNeu). */ if (!req.body?.auch_mich) { return res.status(400).json({ fehler: "eigener_code", hinweis: - "Das beendet deine eigene Sitzung sofort." }); + "Der alte Code gilt danach nicht mehr, und alle anderen Geräte " + + "werden abgemeldet. Hier bleibst du angemeldet." }); } } - const neu = codeNeu(id, { ...req.person, ip: echteIp(req) }); + const neu = codeNeu(id, { ...req.person, ip: echteIp(req) }, + eigener ? sitzungToken(req) : null); res.json({ id: neu.id, name: neu.name, rolle: neu.rolle, code: neu.code }); } catch (fehler) { if (/Keine Person/.test(fehler?.message || "")) { diff --git a/server/workspace.js b/server/workspace.js index bae6fb2f..3fd97508 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1401,16 +1401,49 @@ export function personAnlegen(name, rolle, akteur = null) { return { id: Number(lastInsertRowid), name, rolle, code }; } -export function codeNeu(id, akteur = null) { +/** Das Sitzungs-Merkmal aus der Anfrage -- roh, wie es im Keks steht. + * Wird gebraucht, um GENAU DIESE eine Sitzung am Leben zu lassen. */ +export const sitzungToken = (req) => req?.cookies?.[COOKIE] || null; + +/** + * Neuen Code setzen. + * + * @param behalteToken Die eine Sitzung, die NICHT beendet wird. + * Gedacht für den Fall "ich erneuere meinen eigenen Code": Wer das tut, + * ist gerade angemeldet und hält diese Sitzung in der Hand. + */ +export function codeNeu(id, akteur = null, behalteToken = null) { const person = db().prepare("SELECT id, name, rolle FROM personen WHERE id = ?").get(id); if (!person) throw new Error(`Keine Person mit Nummer ${id}`); const code = codeErzeugen(); const salt = randomBytes(16).toString("hex"); db().prepare("UPDATE personen SET code_hash = ?, code_salt = ?, code_n = ? WHERE id = ?") .run(hashe(code, salt, SCRYPT.N), salt, SCRYPT.N, id); - /* Alle offenen Sitzungen beenden -- ein neuer Code soll den alten Zugang - wirklich beenden, nicht nur die nächste Anmeldung betreffen. */ - db().prepare("DELETE FROM sitzungen WHERE person_id = ?").run(id); + + /* Offene Sitzungen beenden -- ein neuer Code soll den alten Zugang + wirklich beenden, nicht nur die nächste Anmeldung betreffen. + + MIT EINER AUSNAHME, seit dem 02.09.2026: die Sitzung, aus der heraus + der Code gerade erneuert wird. + + Vorher flog man beim eigenen Code sofort hinaus -- und zwar in + derselben Sekunde, in der der neue Code auf dem Bildschirm erschien. + Der nächste Aufruf der Seite lief in ein "nicht angemeldet" und + leitete zur Anmeldung um; der Code war weg, bevor man ihn kopieren + konnte. Genau so ist es Filipe am 02.09.2026 passiert, und danach kam + er nur noch über einen Eingriff auf dem Server wieder hinein. + + Sicherheitlich kostet die Ausnahme nichts: Wer den Code erneuert, hat + sich mit genau dieser Sitzung soeben ausgewiesen und hält sie in + Händen. Alle ANDEREN Geräte fliegen weiterhin hinaus -- das ist der + Sinn der Übung. */ + const behalten = behalteToken ? tokenHash(behalteToken) : null; + if (behalten) { + db().prepare("DELETE FROM sitzungen WHERE person_id = ? AND token_hash <> ?") + .run(id, behalten); + } else { + db().prepare("DELETE FROM sitzungen WHERE person_id = ?").run(id); + } protokolliere("code_erneuert", { personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null, ip: akteur?.ip ?? null, detail: `für ${person.name}`, diff --git a/tools/notfall-code.sh b/tools/notfall-code.sh new file mode 100644 index 00000000..e48a9b8b --- /dev/null +++ b/tools/notfall-code.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# ===================================================================== +# notfall-code.sh — der Weg zurück, wenn niemand mehr hineinkommt. +# +# ANLASS (02.09.2026): Filipe hat sich einen neuen Zugangscode erzeugt +# und wurde in derselben Sekunde abgemeldet, in der der Code auf dem +# Bildschirm erschien. Der Code war weg, bevor er ihn abschreiben +# konnte -- und damit auch der Zugang zum ganzen Workspace. +# +# Beides ist repariert (die eigene Sitzung bleibt jetzt bestehen, und +# der Kasten mit dem Code verschwindet nicht mehr von selbst). Trotzdem +# gehört dieser Weg hierher: Ein System, aus dem man sich aussperren +# kann, braucht eine Tür, die von außen aufgeht. +# +# WARUM KEIN "NOTFALL-CODE" IN DER ANWENDUNG: +# Ein zweiter, fest hinterlegter Code wäre eine Hintertür, die dauerhaft +# offensteht -- jeden Tag, für jeden, der ihn findet, ohne dass es +# jemandem auffällt. Dieser Weg hier ist genau umgekehrt: Er verlangt +# Zugang zum Server, hinterlässt einen Eintrag im Protokoll, und es gibt +# nichts, was man erraten könnte. +# +# AUFRUF (auf dem Server, als dogi): +# sudo -u dogiweb bash /home/dogiweb/dogfather-universe/tools/notfall-code.sh +# +# Ohne Namen nimmt es DogFather. Für jemand anderen: +# ... notfall-code.sh "Cigdem" +# +# WICHTIG: Das "-u dogiweb" muss dranbleiben. Als root ausgeführt gehören +# die entstehenden Hilfsdateien der Datenbank danach root, und der Dienst +# kann nicht mehr schreiben -- dann ist mehr kaputt als vorher. +# ===================================================================== +set -euo pipefail + +WER="${1:-Dogfather}" +SERVER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../server" && pwd)" +# Das Hilfsskript liegt NEBEN workspace.js, nicht in /tmp: Ein +# ES-Modul loest "./workspace.js" gegen seinen eigenen Ort auf, nicht +# gegen das Arbeitsverzeichnis. Aus /tmp heraus fand es die Anwendung +# nicht -- und ein absoluter Pfad im Import ist nicht auf jedem System +# gleich (unter Windows/Git-Bash sieht er anders aus als unter Linux). +SKRIPT="$SERVER_DIR/.notfall-code-$$.mjs" +trap 'rm -f "$SKRIPT"' EXIT + +if [ "$(id -un)" = "root" ]; then + echo "ABBRUCH: Nicht als root ausführen." >&2 + echo "So ist es richtig: sudo -u dogiweb bash $0 ${1:-}" >&2 + exit 1 +fi + +cat > "$SKRIPT" < p.name.toLowerCase() === gesucht.toLowerCase()); + +if (!person) { + console.log("\nKeine aktive Person namens \"" + gesucht + "\"."); + console.log("Vorhanden sind:"); + for (const p of alle) console.log(" " + p.name + " (" + p.rolle + ")"); + process.exit(1); +} + +const neu = codeNeu(person.id, { id: null, rolle: null, ip: "notfall-skript" }); + +/* Die Sperre nach acht Fehlversuchen mit aufheben. Wer sich gerade + ausgesperrt hat, hat es meist mehrfach versucht -- und stünde sonst + mit einem gültigen Code vor einer verschlossenen Tür. */ +const gesperrt = db().prepare("SELECT COUNT(*) AS n FROM versuche").get().n; +db().prepare("DELETE FROM versuche").run(); + +console.log(""); +console.log("========================================================="); +console.log(" NEUER ZUGANGSCODE"); +console.log(""); +console.log(" Person: " + neu.name); +console.log(" Rolle: " + neu.rolle); +console.log(""); +console.log(" " + neu.code); +console.log(""); +console.log(" Genau so eintippen - mit Bindestrichen, Grossbuchstaben."); +console.log(" Er wird zeichengenau geprueft, ohne Korrektur."); +console.log(""); +console.log(" Danach sofort nach Bitwarden. Nicht in eine Notiz."); +if (gesperrt) console.log(" (" + gesperrt + " Fehlversuche geloescht - keine Wartezeit.)"); +console.log("========================================================="); +console.log(""); +SKRIPTENDE + +node "$SKRIPT" "$WER" diff --git a/workspace/assets/js/personen.js b/workspace/assets/js/personen.js index 772da462..28b7877d 100644 --- a/workspace/assets/js/personen.js +++ b/workspace/assets/js/personen.js @@ -78,9 +78,32 @@ const melde = (t) => { $('fehler').textContent = t || ''; }; + /* Solange ein frisch erzeugter Code auf dem Bildschirm steht, darf die + Seite NICHT von selbst zur Anmeldung springen. + + Genau das ist am 02.09.2026 passiert: Der Code erschien, die Sitzung + war im selben Moment beendet, der nächste Aufruf lief in ein "nicht + angemeldet" -- und die Umleitung nahm den Code mit, bevor er + abgeschrieben war. Danach kam niemand mehr hinein. + + Der Serverteil ist repariert (die eigene Sitzung bleibt jetzt + bestehen). Diese Sperre hier ist der zweite Riegel: Selbst wenn eine + Sitzung aus einem anderen Grund endet -- Zeitablauf, ein zweiter + Bildschirm, ein Neustart --, bleibt der Code stehen, bis er + weggeklickt wird. Ein Code, den man nicht mehr lesen kann, ist + schlimmer als gar keiner. */ + let codeOffen = false; + async function hole(pfad, optionen = {}) { const a = await fetch(pfad, { credentials: 'same-origin', ...optionen }); - if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } + if (a.status === 401) { + if (codeOffen) { + melde('Deine Sitzung ist beendet. Schreib den Code oben JETZT ab – ' + + 'die Seite bleibt stehen, bis du „Verstanden" drückst.'); + throw new Error('abgemeldet, Code steht noch'); + } + location.assign('/workspace/'); throw new Error('abgemeldet'); + } if (a.status === 404 && pfad.includes('/verwaltung/')) { location.assign('/workspace/start.html'); throw new Error('nicht erlaubt'); } @@ -95,11 +118,18 @@ $('code-name').textContent = `${p.name} · ${ROLLENNAME[p.rolle] || p.rolle}`; $('code-wert').textContent = p.code; $('code-kasten').hidden = false; + /* Ab jetzt springt die Seite nicht mehr von selbst weg -- siehe + die Begruendung bei hole(). */ + codeOffen = true; $('code-kasten').scrollIntoView({ behavior: 'smooth', block: 'center' }); + /* Den Zeiger in den Kasten setzen: Wer mit der Tastatur arbeitet, + landet sonst irgendwo in der Liste und findet den Code nicht. */ + $('code-kopieren').focus(); } $('code-schliessen').addEventListener('click', () => { $('code-kasten').hidden = true; + codeOffen = false; /* Den Code auch aus dem Dokument entfernen, damit er nicht in einer Bildschirmaufnahme oder im Verlauf hängen bleibt. */ $('code-wert').textContent = ''; @@ -275,7 +305,15 @@ knoepfe.append(tuKnopf('Neuer Code', async () => { const eigen = p.id === ich.id; const frage = eigen - ? `Neuer Code für dich selbst?\n\nDeine aktuelle Sitzung wird sofort beendet und du musst dich mit dem neuen Code neu anmelden.` + /* Der Satz stimmte bis zum 02.09.2026 -- und beschrieb genau die + Falle: Man bestätigte, der Code erschien, und im selben Moment + war die Sitzung weg. Jetzt bleibt sie bestehen, und der Text + sagt, was wirklich passiert. */ + ? 'Neuer Code für dich selbst?\n\n' + + 'Der alte Code gilt danach nicht mehr, und alle ANDEREN Geräte ' + + 'werden abgemeldet.\n\n' + + 'Hier bleibst du angemeldet – der neue Code steht danach auf dem ' + + 'Bildschirm, bis du ihn selbst wegklickst.' : `Neuer Code für ${p.name}?\n\nDer alte Code gilt dann nicht mehr, offene Sitzungen werden beendet.`; if (!confirm(frage)) return; const a = await hole(`/workspace/api/verwaltung/personen/${p.id}/code`, { diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 1f725f05..a2bef728 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -7,9 +7,9 @@ - - - + + + @@ -152,9 +152,9 @@ - - - - + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 08b650d0..cd7634a5 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -132,9 +132,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index 1c750919..7b46efef 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -115,13 +115,13 @@ - - - - - - - - + + + + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 2a4ed7bc..7a5106f9 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -74,10 +74,10 @@ - - - - - + + + + + diff --git a/workspace/content.html b/workspace/content.html index 67473515..7f863a62 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -164,11 +164,11 @@ - - - - - - + + + + + + diff --git a/workspace/dateien.html b/workspace/dateien.html index be85d9cb..6b2ac5b1 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -93,9 +93,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index ec253e70..82d9740d 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -25,7 +25,7 @@ type="image/webp" media="(max-width: 899px)" /> - + @@ -143,6 +143,6 @@

- + diff --git a/workspace/kalender.html b/workspace/kalender.html index 9266e71e..5f253492 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -178,9 +178,9 @@ - - - - + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 8a4ab33c..2691a755 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -121,9 +121,9 @@ - - - - + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 3fa208a1..7659a467 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -193,10 +193,10 @@ - - - - - + + + + + diff --git a/workspace/report.html b/workspace/report.html index 07a1c9e0..b76a2792 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -91,10 +91,10 @@ - - - - - + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index b779e82a..ff959bf4 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -124,9 +124,9 @@ - - - - + + + + diff --git a/workspace/start.html b/workspace/start.html index 4b87f67f..8edd4368 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -7,8 +7,8 @@ - - + + @@ -99,9 +99,9 @@

- - - - + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index b8c00536..e2890d20 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -70,10 +70,10 @@ - - - - - + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index 98032152..38bde21e 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -111,9 +111,9 @@ - - - - + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index c3255f61..62f40544 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -7,9 +7,9 @@ - - - + + + @@ -57,9 +57,9 @@ - + - - + + diff --git a/workspace/wissen.html b/workspace/wissen.html index d169cfe7..41ccc188 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -7,10 +7,10 @@ - - - - + + + + @@ -175,9 +175,9 @@ - - - - + + + +