Workspace: neue Rolle Manager, feste Rollenreihenfolge, echte Rollenwahl
DREI TEILE. 1) ROLLE "MANAGER" Ein Manager darf alles, was DogFather darf -- mit genau zwei Vorbehalten: Er kann keine Leitung ANLEGEN und an keiner Leitung etwas AENDERN. Sonst koennte er sich einen zweiten Vollzugang schaffen oder DogFather aussperren. "Nur DogFather hat alle endgueltigen Rechte" heisst genau das. Umgesetzt ueber istLeitung() an EINER Stelle statt 44 einzelner Vergleiche auf "admin" im Server und 26 im Browser. DATENBANK-UMSTELLUNG: CREATE TABLE IF NOT EXISTS fasst eine vorhandene Tabelle nicht an -- die CHECK-Regel stand also weiter auf den alten drei Rollen, und ein Manager waere daran gescheitert, obwohl der Code stimmt. SQLite kann eine CHECK-Regel nicht aendern, also: neue Tabelle, Daten hinueber, alte weg, umbenennen. Davor schreibt der Server eine vollstaendige Sicherung (VACUUM INTO, in sich konsistent). Ohne Sicherung wird NICHT umgestellt. Geprueft nach der Umstellung: alle 13 Tabellen mit gleicher Zeilenzahl, PRAGMA integrity_check ok, keine verwaisten Verweise. Die einzige Abweichung war eine Sitzung mehr -- die eigene Anmeldung, die die Umstellung ausgeloest hat. 2) EIN SICHERHEITSLOCH, DAS DER TEST GEFUNDEN HAT Der erste Entwurf sicherte "Person anlegen" und "Person sperren" ab -- und liess "neuer Zugangscode" offen. Ein Manager konnte DogFather einen neuen Code ausstellen, bekam ihn angezeigt und haette ihn damit aus seinem eigenen Konto ausgesperrt. Im Test aufgefallen, weil ich den negativen Fall durchgespielt habe. Behoben nicht durch eine dritte Einzelpruefung, sondern durch eine Schranke an JEDEM Weg mit einer :id. Der naechste Weg, der dazukommt, ist damit automatisch mitgeschuetzt. Nachgeprueft: Manager bekommt 403 beim Code-Erneuern und Sperren von DogFather UND von sich selbst, darf aber Creator und Scouts verwalten. 3) FOLGEFEHLER DER MASSENERSETZUNG Die Regel "niemals den letzten aktiven DogFather sperren" hatte durch die Umstellung auf istLeitung() ploetzlich auch Manager blockiert -- gezaehlt werden aber nur DogFather-Zugaenge. Jetzt istDogFather(). Geprueft: DogFather kann einen Manager sperren, sich selbst nicht. 4) REIHENFOLGE UND ROLLENWAHL Ueberall DogFather, Manager, Scout, Creator. "ORDER BY rolle" waere alphabetisch gewesen (admin, creator, manager, scout) -- also fast genau falsch herum. Jetzt ein gemeinsamer Sortierausdruck aus workspace.js. Das Auswahlmenue fuer die Rolle ist weg. Es kam als weisses Windows-Menue mitten in einer dunklen Oberflaeche und schnitt "Creator" zu "Crea" ab -- gestalten laesst sich ein aufgeklapptes Systemmenue nicht. Ersetzt durch vier sichtbare Schalter mit Symbol, Farbe je Rolle und einer Zeile, was die Rolle bedeutet. Bei "Manager" gegen "DogFather" ist das der Unterschied zwischen Raten und Wissen. DogFather und Manager stehen dort nur zur Wahl, wenn DogFather selbst davorsitzt -- ein Knopf, der immer scheitert, gehoert nicht hin. Nebenbei: Das Namensfeld war auf eine von zwoelf Spalten gequetscht, weil seine Umgebung keine .feld-Klasse trug. Alle Formulare daraufhin durchsucht, keine weiteren Faelle.
This commit is contained in:
+111
-10
@@ -52,7 +52,27 @@ const COOKIE = "dfw_sitzung";
|
||||
const SITZUNG_STUNDEN = 12;
|
||||
const VERSUCHE_MAX = 8; // pro IP
|
||||
const VERSUCHE_FENSTER_MIN = 10;
|
||||
const ROLLEN = new Set(["admin", "creator", "scout"]);
|
||||
const ROLLEN = new Set(["admin", "manager", "scout", "creator"]);
|
||||
|
||||
/* Die Reihenfolge, in der Rollen ueberall erscheinen: DogFather zuerst,
|
||||
dann Manager, dann Scout, dann Creator. Steht hier einmal, damit keine
|
||||
Liste eine eigene Reihenfolge erfindet. */
|
||||
export const ROLLEN_REIHE = ["admin", "manager", "scout", "creator"];
|
||||
|
||||
/* Als SQL-Ausdruck fuer ORDER BY. "ORDER BY rolle" waere alphabetisch
|
||||
(admin, creator, manager, scout) -- also fast genau falsch herum. */
|
||||
export const ROLLEN_SORTIERUNG =
|
||||
"CASE rolle WHEN 'admin' THEN 0 WHEN 'manager' THEN 1 WHEN 'scout' THEN 2 ELSE 3 END";
|
||||
|
||||
/* LEITUNG = DogFather und Manager. Ein Manager darf alles, was
|
||||
DogFather darf -- mit genau zwei Ausnahmen, die in
|
||||
workspace-personen.js stehen: Er kann keine Leitung anlegen und keine
|
||||
Leitung veraendern. Sonst koennte er sich selbst zum DogFather machen
|
||||
oder den echten aussperren. "Nur DogFather hat alle endgueltigen
|
||||
Rechte" heisst genau das. */
|
||||
const LEITUNG = new Set(["admin", "manager"]);
|
||||
export const istLeitung = (person) => !!person && LEITUNG.has(person.rolle);
|
||||
export const istDogFather = (person) => !!person && person.rolle === "admin";
|
||||
|
||||
/* Der Rollenschluessel bleibt "admin" -- er steckt in der CHECK-Regel der
|
||||
Datenbank, in jeder Sitzung und in jeder Rechteabfrage. Umbenannt wird
|
||||
@@ -65,8 +85,9 @@ const ROLLEN = new Set(["admin", "creator", "scout"]);
|
||||
die in der Oberflaeche "DogFather" heisst. */
|
||||
export const ROLLEN_NAME = {
|
||||
admin: "DogFather",
|
||||
creator: "Creator",
|
||||
manager: "Manager",
|
||||
scout: "Scout",
|
||||
creator: "Creator",
|
||||
};
|
||||
|
||||
/* scrypt-Parameter. N=2^15 braucht auf diesem Server rund 150 ms — spürbar
|
||||
@@ -78,6 +99,85 @@ const SCRYPT = { N: 32768, r: 8, p: 1, keylen: 64 };
|
||||
let _db = null;
|
||||
let _dbFehler = null;
|
||||
|
||||
/* =====================================================================
|
||||
Umstellungen an bestehenden Datenbanken.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS legt eine Tabelle nur an, wenn sie fehlt --
|
||||
eine vorhandene wird NICHT angefasst. Die CHECK-Regel fuer die Rolle
|
||||
stand also weiterhin auf den alten drei Werten, und ein Manager waere
|
||||
an der Datenbank gescheitert, obwohl im Code alles stimmt.
|
||||
|
||||
SQLite kann eine CHECK-Regel nicht aendern. Der einzige saubere Weg
|
||||
ist: neue Tabelle, Daten hinueber, alte weg, neue umbenennen. Das ist
|
||||
der Moment, in dem eine Datenbank kaputtgehen kann -- deshalb wird
|
||||
vorher eine vollstaendige Sicherung geschrieben (VACUUM INTO, von
|
||||
SQLite selbst und in sich konsistent, anders als ein Dateikopie
|
||||
waehrend laufender Schreibvorgaenge).
|
||||
===================================================================== */
|
||||
function umstellungen(d) {
|
||||
const jetztStempel = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "");
|
||||
|
||||
/* ---- Rolle "manager" erlauben ---- */
|
||||
const bauplan = d.prepare(
|
||||
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'personen'").get()?.sql || "";
|
||||
if (!bauplan.includes("'manager'")) {
|
||||
const sicherung = `${DB_PFAD}.vor-manager-${jetztStempel}`;
|
||||
try {
|
||||
d.exec(`VACUUM INTO '${sicherung.replace(/'/g, "''")}'`);
|
||||
console.log("[workspace] Sicherung vor der Umstellung:", sicherung);
|
||||
} catch (fehler) {
|
||||
/* Ohne Sicherung wird NICHT umgestellt. Lieber laeuft der Manager
|
||||
noch nicht, als dass Daten ohne Netz angefasst werden. */
|
||||
console.error("[workspace] Sicherung fehlgeschlagen, Umstellung abgebrochen:", fehler?.message);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Fremdschluessel muessen aus sein, weil andere Tabellen auf
|
||||
personen(id) zeigen -- und das laesst sich nicht innerhalb einer
|
||||
Transaktion umschalten. */
|
||||
d.exec("PRAGMA foreign_keys = OFF");
|
||||
try {
|
||||
d.exec("BEGIN");
|
||||
d.exec(`
|
||||
CREATE TABLE personen_neu (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
rolle TEXT NOT NULL CHECK (rolle IN ('admin','manager','scout','creator')),
|
||||
code_hash TEXT NOT NULL,
|
||||
code_salt TEXT NOT NULL,
|
||||
code_n INTEGER NOT NULL,
|
||||
aktiv INTEGER NOT NULL DEFAULT 1,
|
||||
erstellt TEXT NOT NULL,
|
||||
letzter_login TEXT
|
||||
);
|
||||
INSERT INTO personen_neu
|
||||
(id, name, rolle, code_hash, code_salt, code_n, aktiv, erstellt, letzter_login)
|
||||
SELECT id, name, rolle, code_hash, code_salt, code_n, aktiv, erstellt, letzter_login
|
||||
FROM personen;
|
||||
DROP TABLE personen;
|
||||
ALTER TABLE personen_neu RENAME TO personen;
|
||||
`);
|
||||
d.exec("COMMIT");
|
||||
|
||||
/* Nach dem Tausch pruefen, ob die Verweise noch stimmen. Findet
|
||||
sich etwas, wird das laut gemeldet -- stillschweigend kaputte
|
||||
Verweise waeren das Schlimmste an dieser Stelle. */
|
||||
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
||||
if (kaputt.length) {
|
||||
console.error("[workspace] ACHTUNG: nach der Umstellung", kaputt.length,
|
||||
"verwaiste Verweise. Sicherung liegt unter", sicherung);
|
||||
} else {
|
||||
console.log("[workspace] Rolle 'manager' freigeschaltet, Verweise geprueft.");
|
||||
}
|
||||
} catch (fehler) {
|
||||
try { d.exec("ROLLBACK"); } catch { /* schon zurueckgerollt */ }
|
||||
console.error("[workspace] Umstellung fehlgeschlagen:", fehler?.message);
|
||||
} finally {
|
||||
d.exec("PRAGMA foreign_keys = ON");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function db() {
|
||||
if (_db) return _db;
|
||||
if (_dbFehler) throw _dbFehler;
|
||||
@@ -92,7 +192,7 @@ export function db() {
|
||||
CREATE TABLE IF NOT EXISTS personen (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
rolle TEXT NOT NULL CHECK (rolle IN ('admin','creator','scout')),
|
||||
rolle TEXT NOT NULL CHECK (rolle IN ('admin','manager','scout','creator')),
|
||||
code_hash TEXT NOT NULL,
|
||||
code_salt TEXT NOT NULL,
|
||||
code_n INTEGER NOT NULL,
|
||||
@@ -369,6 +469,7 @@ export function db() {
|
||||
CREATE INDEX IF NOT EXISTS idx_versuche_ip ON versuche (ip, zeitpunkt);
|
||||
CREATE INDEX IF NOT EXISTS idx_sitzungen_gueltig ON sitzungen (gueltig_bis);
|
||||
`);
|
||||
umstellungen(d);
|
||||
_db = d;
|
||||
return d;
|
||||
} catch (fehler) {
|
||||
@@ -493,13 +594,13 @@ export const workspaceRouter = express.Router();
|
||||
const GESCHUETZT = {
|
||||
"/workspace/start.html": null,
|
||||
"/workspace/aufgaben.html": null,
|
||||
"/workspace/personen.html": ["admin"],
|
||||
"/workspace/profil.html": ["admin", "creator", "scout"],
|
||||
"/workspace/personen.html": ["admin", "manager"],
|
||||
"/workspace/profil.html": ["admin", "manager", "scout", "creator"],
|
||||
"/workspace/kalender.html": null,
|
||||
"/workspace/dateien.html": null,
|
||||
"/workspace/bereich.html": ["admin", "creator", "scout"],
|
||||
"/workspace/report.html": ["admin", "creator", "scout"],
|
||||
"/workspace/scouting.html": ["admin", "scout"],
|
||||
"/workspace/bereich.html": ["admin", "manager", "scout", "creator"],
|
||||
"/workspace/report.html": ["admin", "manager", "scout", "creator"],
|
||||
"/workspace/scouting.html": ["admin", "manager", "scout"],
|
||||
"/workspace/calls.html": null,
|
||||
"/workspace/startcheck.html": null,
|
||||
"/workspace/wissen.html": null,
|
||||
@@ -649,7 +750,7 @@ export function betreutWo(person, spalte) {
|
||||
/* Darf diese Person den Bereich dieses Creators sehen und bearbeiten? */
|
||||
export function darfCreator(person, creatorId) {
|
||||
if (!person || !creatorId) return false;
|
||||
if (person.rolle === "admin") return true;
|
||||
if (istLeitung(person)) return true;
|
||||
if (person.rolle === "creator") return person.id === Number(creatorId);
|
||||
return betreuteIds(person).includes(Number(creatorId));
|
||||
}
|
||||
@@ -721,7 +822,7 @@ export function personenListe() {
|
||||
return db().prepare(`
|
||||
SELECT p.id, p.name, p.rolle, p.aktiv, p.erstellt, p.letzter_login,
|
||||
(SELECT COUNT(*) FROM sitzungen s WHERE s.person_id = p.id) AS sitzungen
|
||||
FROM personen p ORDER BY p.rolle, p.name`).all();
|
||||
FROM personen p ORDER BY ${ROLLEN_SORTIERUNG.replace("rolle", "p.rolle")}, p.name`).all();
|
||||
}
|
||||
|
||||
export function protokollLesen(anzahl = 20) {
|
||||
|
||||
Reference in New Issue
Block a user