Demo-Login fuer Vorfuehrungen (open.md Punkt 1)
- nutzer.ist_demo-Spalte, permanentes Konto demo/demo wird in init_db() automatisch angelegt - Vorausgefuellte fiktive Patientin "Grete Musterfrau" (Pflegegrad 3, 1 Antrag, 2 Leistungen, 2 Aufgaben, 1 Pflegetagebuch-Eintrag) - _demo_seed() - Automatischer Reset alle 6 Stunden per Lazy-Check (kein Cron/Thread noetig): _demo_reset_faellig()/_demo_reset() geprueft bei Login und bei jedem Request (global_auth_check), damit auch eine laufende Sitzung die 6h-Grenze nicht verpasst - Demo-Konto von der Abo-Zahlungspflicht ausgenommen (_hat_aktiven_ zugang) - wurde erst beim Testen entdeckt: bei aktiver Zahlungspflicht waere das Demo-Konto sonst mit "Zahlung erforderlich" ausgesperrt worden, was fuer eine Vorfuehrung inakzeptabel waere - E-Mail-Versand (api/email/senden + /test) fuer das Demo-Konto gesperrt (403) - verhindert Missbrauch der echten SMTP-Konfiguration durch Demo-Besucher - Login-Screen: neuer "Demo ausprobieren"-Button; Topbar zeigt waehrend einer Demo-Sitzung ein "Demo-Modus"-Badge open.md: Reihenfolge sortiert (Docker-Containerisierung und React+ Postgres-Migration als getrennte Aufgaben markiert, da voellig unterschiedlicher Umfang), Punkt 1 als erledigt markiert. Verifiziert: Login, vorausgefuellte Patientendaten, Isolation (Demo sieht nur eigene Patientin), erzwungener Reset (Patient-ID aendert sich, demo_reset_at aktualisiert), E-Mail-Sperre (403), Abo-Ausnahme (Zugriff bleibt moeglich auch bei abo_pflicht=1), Login-Button und Demo-Badge visuell per Playwright bestaetigt.
This commit is contained in:
1 parent
8a2e338e9d
commit
af43371a5b
4 files changed
+153
-4
No files matched your search
@@ -82,13 +82,26 @@ The frontend was originally desktop-only (zero `@media` queries, fixed 228px sid
|
||||
The product owner wants the app to cost **4.99 €/month per account**, provider likely **PayPal** (not yet integrated — no account/API keys exist yet). Since there's no live payment method, the paywall is built but wired to a **global off switch** so it can't lock anyone out before PayPal actually works:
|
||||
|
||||
- `einstellungen` key `abo_pflicht` (`"1"`/`"0"`, default off) — read via `_abo_pflicht_aktiv()`. Toggle lives in the Backend-Einstellungen "Abo & Zahlung" card. **Leave this off until PayPal is actually wired up.**
|
||||
- `nutzer.abo_bis` (date, NULL = never granted) / `nutzer.abo_typ` (`'kostenlos_admin'` | future `'paypal'`) track access per account. `_hat_aktiven_zugang(u)` is the single source of truth: always true for admins, always true while `abo_pflicht` is off, otherwise `abo_bis >= today`.
|
||||
- `nutzer.abo_bis` (date, NULL = never granted) / `nutzer.abo_typ` (`'kostenlos_admin'` | future `'paypal'`) track access per account. `_hat_aktiven_zugang(u)` is the single source of truth: always true for admins, always true for the demo account (`ist_demo`, see "Demo account" below — it must never hit the paywall regardless of `abo_pflicht`), always true while `abo_pflicht` is off, otherwise `abo_bis >= today`.
|
||||
- `global_auth_check()` enforces this on every `/api/*` call except a small allowlist (`/api/abo/status`, `/api/abo/aktivieren`, `/api/auth/logout`) — returns **402 Payment Required** (`code: "abo_erforderlich"`) when blocked, so a blocked user can still see their status and (eventually) pay.
|
||||
- `POST /api/abo/aktivieren` is an intentional **stub** (501, "noch nicht verfügbar") — real PayPal Checkout/Subscriptions integration replaces this later; do not fake a success response here.
|
||||
- Admin can grant free access via `POST /api/admin/nutzer/<uid>/abo-freischalten` (`{monate, begruendung}`, 1–12 months, extends from the later of today/existing `abo_bis`, admin-only, audit-logged like the other admin-dashboard actions) — this is the "1 Monat kostenlos freischalten" capability the operator asked for, surfaced as a "Freischalten" button per row in the Admin-Dashboard.
|
||||
- Frontend: `curAbo`/`aboBlockiert` globals are set in `initApp()` from `GET /api/abo/status`. `go(name)` redirects any navigation to the `meinabo` page while `aboBlockiert` is true, so a blocked account always lands on its own status/pay screen instead of a broken page. `pgMeinAbo()` is that page (status, price, stub PayPal button) and doubles as the always-available "check my subscription" view when unblocked.
|
||||
- When actually integrating PayPal: replace `/api/abo/aktivieren`'s stub body with real Checkout/Subscription creation, add a webhook endpoint to update `abo_bis`/`abo_typ` on payment events, and only then flip `abo_pflicht` to `"1"`.
|
||||
|
||||
## Demo account
|
||||
|
||||
A permanent `demo`/`demo` account (`nutzer.ist_demo=1`) exists for showing the app to prospects, created automatically in `init_db()` if missing. It's a completely ordinary `nutzer`-role account — no special-casing in the isolation model — with two exceptions layered on top:
|
||||
|
||||
- **Immune to the subscription paywall**: `_hat_aktiven_zugang()` returns `True` for `ist_demo` accounts unconditionally, *before* the `abo_pflicht` check. Don't remove this — a demo visitor must never see "Zahlung erforderlich" regardless of the operator's billing settings. (This was caught by testing, not designed in from the start — worth re-checking if `_hat_aktiven_zugang()` is ever refactored.)
|
||||
- **Cannot send real email**: `POST /api/email/senden` and `/api/email/test` both return 403 for `ist_demo` accounts, so a demo visitor can never trigger outbound mail through the operator's real SMTP relay.
|
||||
|
||||
**Auto-reset every 6 hours**, lazily rather than via a background thread/cron (this app has no task scheduler and stays that way — a lazy check fits the existing "one Flask process, no queue" architecture and can't miss a tick across restarts): `_demo_reset_faellig()` compares the `einstellungen` key `demo_reset_at` against `now`; `_demo_reset()` deletes every patient owned by the demo account (cascades to antraege/leistungen/aufgaben/pflegetagebuch/dokumente via the existing `ON DELETE CASCADE` FKs — SQLite enforces this because `get_db()` sets `PRAGMA foreign_keys = ON`), then reseeds one fictional pre-filled patient (`_demo_seed()`: "Grete Musterfrau", Pflegegrad 3, one Antrag, two Leistungen, two Aufgaben, one Pflegetagebuch-Eintrag) and bumps `demo_reset_at`. The check/reset call happens in two places — `api_auth_login()` (fresh state at the start of every demo session) and `global_auth_check()` (catches a session that's still open when the 6h boundary passes mid-visit) — both call the same `_demo_reset_faellig()`/`_demo_reset()` pair, so there's one source of truth for "is a reset due."
|
||||
|
||||
The demo account can create up to 2 more patients itself (same 3-per-account cap as any `nutzer` account) via the normal interview flow — nothing special needed there, it's just an ordinary account under the hood.
|
||||
|
||||
Frontend: `#demo-badge` in the topbar (toggled in `initApp()` from `me.ist_demo`) makes it visually obvious when a session is in demo mode. Login screen has a dedicated "Demo ausprobieren" button (`doDemoLogin()`) that fills the credentials and reuses `doLogin()` — no separate login code path.
|
||||
|
||||
## Known constraints worth knowing before touching auth/security-sensitive code
|
||||
|
||||
- `app.secret_key` in `app.py` is a hardcoded string, not loaded from an environment variable.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
offene Aufgaben: sortiere die Aufgaben nach korrekter Reihenfolge, schreibe nach dieser zeile um für KI-Lesbarkeit, fertige aufgaben durchstreichen und als erledigt markieren.
|
||||
|
||||
Reihenfolge unten nach Abhängigkeit/Aufwand sortiert (nicht nach ursprünglicher Nummerierung). Punkt "Docker" wurde in zwei getrennte Aufgaben aufgeteilt, da "in Docker packen" und "Migration zu React+Postgres" völlig unterschiedlicher Umfang sind und nicht als ein Task behandelt werden sollten.
|
||||
|
||||
- [x] ~~**1. Demo-Login für Vorführungen**~~ *(erledigt)*
|
||||
- ~~Eigener Login, zeigt eine erfundene/fiktive Person als "Familienmitglied"~~
|
||||
- ~~Nutzer kann im Demo-Modus eine Test-Person anlegen~~
|
||||
- ~~Demo-Inhalte müssen komplett vorausgefüllt sein (nicht leer)~~
|
||||
- ~~Demo-Datenbestand setzt sich automatisch alle 6 Stunden zurück~~
|
||||
- Umsetzung: `nutzer.ist_demo`-Konto "demo"/"demo", "Demo ausprobieren"-Button auf dem Login-Screen, "Demo-Modus"-Badge in der Topbar, vorausgefüllte fiktive Patientin "Grete Musterfrau" (Antrag, 2 Leistungen, 2 Aufgaben, Pflegetagebuch), Lazy-Reset alle 6h bei Login/Request (kein Cron/Thread nötig), E-Mail-Versand im Demo-Konto gesperrt (403), Demo-Konto von der Abo-Zahlungspflicht ausgenommen. Details siehe CLAUDE.md.
|
||||
|
||||
- [ ] **2. Docker-Containerisierung der aktuellen App (Flask + SQLite, kein Rewrite)**
|
||||
- App als Docker-Image/Compose-Setup lauffähig machen, so wie sie heute ist
|
||||
- Isolierung zwischen App-Container und Datenbank (getrennte Services/Volumes)
|
||||
- Keine Secrets/Env-Werte in der docker-compose.yml selbst — eigene .env-Datei dafür (keine .env.example nötig, Solo-Projekt)
|
||||
- Ports müssen über Variablen konfigurierbar sein, nicht hart codiert
|
||||
|
||||
- [ ] **3. Update-Benachrichtigung / Changelog-System**
|
||||
- Nach dem ersten Login nach einem Update: kurzer anklickbarer Hinweis, was sich geändert hat
|
||||
- Ein Klick auf "Gelesen" reicht, um den Hinweis zu schließen
|
||||
- Optional: dauerhaftes Icon in der Sidebar, um alle vergangenen Changelogs als Historie nachzulesen
|
||||
- Setzt eine einfache Versionierung des Deployments voraus (siehe Punkt 4 unten), damit die App weiß, "was ist neu seit dem letzten Login"
|
||||
|
||||
- [ ] **4. ⚠️ Separates Grossvorhaben: Migration zu React + PostgreSQL**
|
||||
- Vollständiger Technologiewechsel weg von Flask/vanilla-JS/SQLite — deutlich grösserer Umfang als die Punkte oben, keine Nebenbei-Aufgabe
|
||||
- Braucht eine eigene Versionierungs-/Update-Strategie, da die App online ist und nicht für immer im jetzigen Stack bleiben wird
|
||||
- Bevor hier irgendetwas beginnt: eigene Planungsrunde nötig (Umfang, Reihenfolge der Migration, ob schrittweise oder Neubau, Datenmigration aus der bestehenden SQLite-DB)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ def init_db():
|
||||
"ALTER TABLE patienten ADD COLUMN nutzer_id INTEGER REFERENCES nutzer(id)",
|
||||
"ALTER TABLE nutzer ADD COLUMN abo_bis TEXT",
|
||||
"ALTER TABLE nutzer ADD COLUMN abo_typ TEXT",
|
||||
"ALTER TABLE nutzer ADD COLUMN ist_demo INTEGER DEFAULT 0",
|
||||
"""CREATE TABLE IF NOT EXISTS admin_audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
admin_id INTEGER,
|
||||
@@ -115,6 +116,16 @@ def init_db():
|
||||
("admin", generate_password_hash("admin"), "Administrator", "admin")
|
||||
)
|
||||
db.commit()
|
||||
# Demo-Konto anlegen, falls noch keins existiert (fuer Vorfuehrungen -
|
||||
# eigener Nutzer, ganz normal durch die bestehende Isolation gedeckt)
|
||||
if not db.execute("SELECT id FROM nutzer WHERE benutzername='demo'").fetchone():
|
||||
db.execute(
|
||||
"INSERT INTO nutzer (benutzername, passwort_hash, name, rolle, ist_demo) VALUES (?,?,?,?,1)",
|
||||
("demo", generate_password_hash("demo"), "Demo-Konto", "nutzer")
|
||||
)
|
||||
db.commit()
|
||||
if _demo_reset_faellig():
|
||||
_demo_reset()
|
||||
|
||||
# ─── Settings helper ─────────────────────────────────────────────────────────
|
||||
def get_setting(key, default=None):
|
||||
@@ -131,7 +142,7 @@ def _get_current_user():
|
||||
return None
|
||||
try:
|
||||
return get_db().execute(
|
||||
"SELECT id, benutzername, name, rolle, abo_bis, abo_typ FROM nutzer WHERE id=? AND aktiv=1", (uid,)
|
||||
"SELECT id, benutzername, name, rolle, abo_bis, abo_typ, ist_demo FROM nutzer WHERE id=? AND aktiv=1", (uid,)
|
||||
).fetchone()
|
||||
except Exception:
|
||||
return None
|
||||
@@ -215,6 +226,8 @@ def _hat_aktiven_zugang(u):
|
||||
return False
|
||||
if u["rolle"] == "admin":
|
||||
return True
|
||||
if u["ist_demo"]:
|
||||
return True # Demo-Konto ist immer nutzbar, unabhaengig von der Zahlungspflicht
|
||||
if not _abo_pflicht_aktiv():
|
||||
return True
|
||||
abo_bis = u["abo_bis"] if "abo_bis" in u.keys() else None
|
||||
@@ -234,6 +247,74 @@ def _add_monate(d, monate):
|
||||
tag = min(d.day, tage_im_monat[monat - 1])
|
||||
return date(jahr, monat, tag)
|
||||
|
||||
# ─── Demo-Konto: automatischer Reset alle 6 Stunden ──────────────────────────
|
||||
# Kein Background-Thread/Scheduler noetig - wird "lazy" bei jedem Login bzw.
|
||||
# jedem Request des Demo-Kontos geprueft. Passt zum Rest der App (ein Prozess,
|
||||
# keine Task-Queue) und ist robust gegen Server-Neustarts (kein verpasster Tick).
|
||||
DEMO_RESET_INTERVAL_STUNDEN = 6
|
||||
|
||||
def _demo_reset_faellig():
|
||||
letzter = get_setting("demo_reset_at")
|
||||
if not letzter:
|
||||
return True
|
||||
try:
|
||||
vergangen = (datetime.now() - datetime.strptime(letzter, "%Y-%m-%d %H:%M:%S")).total_seconds()
|
||||
return vergangen > DEMO_RESET_INTERVAL_STUNDEN * 3600
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
def _demo_seed(db, demo_uid):
|
||||
heute = date.today()
|
||||
cur = db.execute("""
|
||||
INSERT INTO patienten (vorname,nachname,geburtsdatum,strasse,plz,ort,
|
||||
telefon,email,krankenkasse,versichertennummer,versicherungsart,
|
||||
pflegegrad,pflegeperson,zielgruppe,notizen,nutzer_id)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""", ("Grete", "Musterfrau", "1946-04-12", "Beispielweg 3", "80331", "München",
|
||||
"089/0000000", "", "AOK Bayern", "D999888777", "GKV",
|
||||
3, "Tochter (privat)", "alterspflege",
|
||||
"Demo-Patientin – wird automatisch alle 6 Stunden zurückgesetzt.", demo_uid))
|
||||
pid = cur.lastrowid
|
||||
db.execute("""
|
||||
INSERT INTO antraege (patient_id,typ,status,antragsdatum,bescheid_datum,
|
||||
beantragter_pflegegrad,zugeteilter_pflegegrad,notizen)
|
||||
VALUES (?,?,?,?,?,?,?,?)
|
||||
""", (pid, "erstantrag", "genehmigt",
|
||||
(heute - timedelta(days=60)).isoformat(), (heute - timedelta(days=20)).isoformat(),
|
||||
3, 3, "Demo-Antrag – Erstantrag genehmigt"))
|
||||
db.execute("""INSERT INTO leistungen (patient_id,typ,bezeichnung,betrag,einheit,aktiv,seit,notizen)
|
||||
VALUES (?,?,?,?,?,1,?,?)""",
|
||||
(pid, "pflegegeld", "Pflegegeld Grad 3", 573, "monatlich",
|
||||
(heute - timedelta(days=20)).isoformat(), "573 €"))
|
||||
db.execute("""INSERT INTO leistungen (patient_id,typ,bezeichnung,betrag,einheit,aktiv,seit,notizen)
|
||||
VALUES (?,?,?,?,?,1,?,?)""",
|
||||
(pid, "entlastung", "Entlastungsbetrag", 131, "monatlich",
|
||||
(heute - timedelta(days=20)).isoformat(), "131 €"))
|
||||
db.execute("""INSERT INTO aufgaben (patient_id,titel,beschreibung,prioritaet,faellig_am)
|
||||
VALUES (?,?,?,?,?)""",
|
||||
(pid, "Pflegetagebuch heute ausfüllen", "Demo-Aufgabe", "hoch", heute.isoformat()))
|
||||
db.execute("""INSERT INTO aufgaben (patient_id,titel,beschreibung,prioritaet,faellig_am)
|
||||
VALUES (?,?,?,?,?)""",
|
||||
(pid, "Entlastungsbetrag abrechnen", "Demo-Aufgabe", "normal", (heute + timedelta(days=5)).isoformat()))
|
||||
db.execute("""INSERT INTO pflegetagebuch (patient_id,datum,koerperpflege,mobilitaet,allgemein)
|
||||
VALUES (?,?,?,?,?)""",
|
||||
(pid, (heute - timedelta(days=1)).isoformat(), "Hilfe beim Waschen benötigt",
|
||||
"Rollator im Innenbereich", "Guter Tag, freundliche Stimmung"))
|
||||
db.commit()
|
||||
return pid
|
||||
|
||||
def _demo_reset():
|
||||
db = get_db()
|
||||
demo = db.execute("SELECT id FROM nutzer WHERE benutzername='demo'").fetchone()
|
||||
if not demo:
|
||||
return
|
||||
db.execute("DELETE FROM patienten WHERE nutzer_id=?", (demo["id"],)) # Kaskade raeumt Antraege/Leistungen/Aufgaben/Tagebuch/Dokumente mit auf
|
||||
db.commit()
|
||||
_demo_seed(db, demo["id"])
|
||||
db.execute("INSERT OR REPLACE INTO einstellungen (schluessel, wert) VALUES ('demo_reset_at', ?)",
|
||||
(datetime.now().strftime("%Y-%m-%d %H:%M:%S"),))
|
||||
db.commit()
|
||||
|
||||
def login_required(f):
|
||||
@functools.wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
@@ -262,6 +343,8 @@ def global_auth_check():
|
||||
u = _get_current_user()
|
||||
if not u:
|
||||
return jsonify({"error": "Nicht angemeldet"}), 401
|
||||
if u["ist_demo"] and _demo_reset_faellig():
|
||||
_demo_reset()
|
||||
# Abo-Gate: nur relevant, solange abo_pflicht aktiv ist (siehe _hat_aktiven_zugang).
|
||||
# Auth/Abo-Endpunkte bleiben immer erreichbar, damit man Status sehen und
|
||||
# (künftig) bezahlen kann, auch wenn der Zugang gerade gesperrt ist.
|
||||
@@ -291,7 +374,7 @@ def api_auth_me():
|
||||
if not u:
|
||||
return jsonify({"error": "Nicht angemeldet"}), 401
|
||||
return jsonify({"id": u["id"], "benutzername": u["benutzername"],
|
||||
"name": u["name"] or u["benutzername"], "rolle": u["rolle"]})
|
||||
"name": u["name"] or u["benutzername"], "rolle": u["rolle"], "ist_demo": bool(u["ist_demo"])})
|
||||
|
||||
@app.route("/api/auth/login", methods=["POST"])
|
||||
def api_auth_login():
|
||||
@@ -304,7 +387,9 @@ def api_auth_login():
|
||||
if not u or not check_password_hash(u["passwort_hash"], passwort):
|
||||
return jsonify({"error": "Ungültige Zugangsdaten"}), 401
|
||||
session["nutzer_id"] = u["id"]
|
||||
return jsonify({"ok": True, "name": u["name"] or u["benutzername"], "rolle": u["rolle"]})
|
||||
if u["ist_demo"] and _demo_reset_faellig():
|
||||
_demo_reset()
|
||||
return jsonify({"ok": True, "name": u["name"] or u["benutzername"], "rolle": u["rolle"], "ist_demo": bool(u["ist_demo"])})
|
||||
|
||||
@app.route("/api/auth/logout", methods=["POST"])
|
||||
def api_auth_logout():
|
||||
@@ -1182,6 +1267,9 @@ def _smtp_send(to, subject, body, pdf_base64="", pdf_dateiname="Dokument.pdf"):
|
||||
|
||||
@app.route("/api/email/senden", methods=["POST"])
|
||||
def api_email_senden():
|
||||
u = _get_current_user()
|
||||
if u and u["ist_demo"]:
|
||||
return jsonify({"error": "E-Mail-Versand ist im Demo-Konto nicht verfügbar."}), 403
|
||||
d = request.json or {}
|
||||
empfaenger = (d.get("empfaenger") or "").strip()
|
||||
betreff = d.get("betreff") or "Nachricht von Pflegepilot"
|
||||
@@ -1204,6 +1292,9 @@ def api_email_senden():
|
||||
|
||||
@app.route("/api/email/test", methods=["POST"])
|
||||
def api_email_test():
|
||||
u = _get_current_user()
|
||||
if u and u["ist_demo"]:
|
||||
return jsonify({"error": "E-Mail-Versand ist im Demo-Konto nicht verfügbar."}), 403
|
||||
try:
|
||||
c = _smtp_cfg()
|
||||
if not c["server"] or not c["user"]:
|
||||
|
||||
@@ -387,6 +387,7 @@ textarea{resize:vertical;min-height:80px}
|
||||
</div>
|
||||
<button class="btn btn-p lo-btn" onclick="doLogin()"><i class="ti ti-login"></i>Anmelden</button>
|
||||
<div id="lo-err" style="color:var(--rdtx);text-align:center;font-size:12.5px;min-height:18px;margin-top:8px"></div>
|
||||
<button class="btn lo-btn" style="margin-top:2px" onclick="doDemoLogin()"><i class="ti ti-eye"></i>Demo ausprobieren</button>
|
||||
<div class="lo-hint"><i class="ti ti-info-circle"></i> Standard-Zugangsdaten: <strong>admin / admin</strong><br>Bitte nach dem ersten Login ändern.</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -433,6 +434,7 @@ textarea{resize:vertical;min-height:80px}
|
||||
<div class="topbar">
|
||||
<button class="sb-toggle btn btn-ico" id="sb-toggle" onclick="toggleSidebar()" aria-label="Menü"><i class="ti ti-menu-2"></i></button>
|
||||
<div class="ptitle" id="ptitle">Dashboard</div>
|
||||
<span class="badge b-a hidden" id="demo-badge" style="margin-right:4px" title="Alle Daten in diesem Modus werden alle 6 Stunden automatisch zurückgesetzt."><i class="ti ti-eye" style="margin-right:3px"></i>Demo-Modus</span>
|
||||
<div id="tactions"></div>
|
||||
</div>
|
||||
<div class="content" id="content"><div class="spin-wrap"><div class="spin"></div>Lade…</div></div>
|
||||
@@ -1153,6 +1155,7 @@ async function initApp(){
|
||||
if(uw){uw.style.display='flex';}
|
||||
const un=document.getElementById('sb-user-name');if(un)un.textContent=me.name||me.benutzername;
|
||||
const ur=document.getElementById('sb-user-role');if(ur)ur.textContent=me.rolle==='admin'?'Administrator':'Nutzer';
|
||||
document.getElementById('demo-badge')?.classList.toggle('hidden',!me.ist_demo);
|
||||
// Admin-nav nur für admin sichtbar
|
||||
const adm=document.querySelector('.sb-admin');
|
||||
if(adm)adm.style.display=me.rolle==='admin'?'':'none';
|
||||
@@ -1184,6 +1187,11 @@ async function doLogin(){
|
||||
if(err)err.textContent=r.error||'Anmeldung fehlgeschlagen';
|
||||
}
|
||||
}
|
||||
async function doDemoLogin(){
|
||||
document.getElementById('lo-user').value='demo';
|
||||
document.getElementById('lo-pw').value='demo';
|
||||
await doLogin();
|
||||
}
|
||||
async function doLogout(){
|
||||
await fetch('/api/auth/logout',{method:'POST'});
|
||||
curUser=null;
|
||||
|
||||
Reference in new issue
Block a user