Personalisierung per Einladung statt Aktivierungscode (ADR-0041, 24.10.2026)
Geräte-Info: Adressfeld (Domänenliste, nur mit Merkmal), erneut senden, zurückziehen. Öffentlich: Einladungsseite mit Namensvorschlag, Stand-Seite mit Selbstaktualisierung, Hinweis für den Kiosk älterer Rollen. Code-Formular und Bestätigungsseiten entfernt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e701cdf82
commit
8937c28028
13 files changed
+229
-236
No files matched your search
@@ -36,7 +36,7 @@ if not log.handlers:
|
||||
|
||||
# Wartezeiten vor dem 2., 3. und 4. Versuch. Danach gilt die Mail als
|
||||
# fehlgeschlagen. Zusammen gut 20 Minuten - passt in die Stundenfrist der
|
||||
# Personalisierungs-Bestätigung.
|
||||
# Links für "Passwort vergessen?".
|
||||
WIEDERHOLUNG_NACH_SEKUNDEN = (60, 300, 900)
|
||||
|
||||
SCHLEIFEN_INTERVALL_SEKUNDEN = 10
|
||||
|
||||
+50
-12
@@ -6,7 +6,7 @@ from fastapi.responses import RedirectResponse
|
||||
from anode_client import anode_request
|
||||
from auth import get_current_user
|
||||
from routers.gruppen import fetch_gruppen
|
||||
from routers.personalisierung import _passwort_hash
|
||||
from routers.personalisierung import _passwort_hash, einladung_senden
|
||||
from templating import templates
|
||||
|
||||
|
||||
@@ -151,9 +151,10 @@ async def gruppen_aktualisieren(request: Request, device_id: str, user: dict = D
|
||||
|
||||
|
||||
PERSONALISIERUNG_STATUS = {
|
||||
"eingeladen": "Einladung verschickt, noch nicht eingelöst",
|
||||
"code_offen": "Code erzeugt, noch nicht eingelöst",
|
||||
"eingereicht": "eingelöst, wartet auf Bestätigung per Mail",
|
||||
"bestaetigt": "bestätigt, wird beim nächsten Start eingerichtet",
|
||||
"bestaetigt": "Konto gewählt, wird beim nächsten Start eingerichtet",
|
||||
"angelegt": "eingerichtet",
|
||||
"zurueckgezogen": "zurückgezogen",
|
||||
"verfallen": "verfallen",
|
||||
@@ -178,7 +179,12 @@ def auftragskatalog_ansicht(request: Request, device_id: str, user: dict = Depen
|
||||
|
||||
|
||||
def geraete_info(
|
||||
request: Request, device_id: str, user, neuer_code: dict | None = None, neues_passwort: dict | None = None
|
||||
request: Request,
|
||||
device_id: str,
|
||||
user,
|
||||
einladung: dict | None = None,
|
||||
neues_passwort: dict | None = None,
|
||||
status_code: int = 200,
|
||||
):
|
||||
device = find_device_in_organization(device_id, user.organization_id)
|
||||
|
||||
@@ -195,6 +201,7 @@ def geraete_info(
|
||||
katalog = anreichere_mit_quelle_url(katalog, "/struktur", "/gruppen")
|
||||
|
||||
oe_result = anode_request("GET", f"/api/v1/organizations/{user.organization_id}/organisationseinheiten")
|
||||
org_result = anode_request("GET", f"/api/v1/organizations/{user.organization_id}")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
@@ -209,35 +216,66 @@ def geraete_info(
|
||||
"personalisierungen": personalisierungen,
|
||||
"personalisierung_konten": [p for p in personalisierungen if p["status"] == "angelegt"],
|
||||
"personalisierung_laufend": next(
|
||||
(p for p in personalisierungen if p["status"] in ("code_offen", "eingereicht", "bestaetigt")), None
|
||||
(p for p in personalisierungen if p["status"] in ("eingeladen", "bestaetigt")), None
|
||||
),
|
||||
"personalisierung_status": PERSONALISIERUNG_STATUS,
|
||||
"neuer_code": neuer_code,
|
||||
# Die Adresse lässt sich nur bei Geräten mit dem Merkmal eintragen;
|
||||
# anode prüft das noch einmal.
|
||||
"personalisierung_moeglich": any(
|
||||
e.get("merkmal") == "personalisierung" and e.get("state") == "present" for e in katalog
|
||||
),
|
||||
"email_domaenen": org_result["organization"].get("email_domaenen", []) if org_result.get("success") else [],
|
||||
"einladung": einladung,
|
||||
"neues_passwort": neues_passwort,
|
||||
},
|
||||
status_code=status_code,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/geraete/{device_id}/personalisierung/code")
|
||||
def personalisierung_code_erzeugen(
|
||||
@router.post("/geraete/{device_id}/personalisierung/einladung")
|
||||
def personalisierung_einladen(
|
||||
request: Request,
|
||||
device_id: str,
|
||||
email: str = Form(""),
|
||||
email_lokal: str = Form(""),
|
||||
email_domain: str = Form(""),
|
||||
bestehenden_behalten: str = Form(""),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
ADR-0041, vereinfachter Ablauf (24.10.2026): Adresse des Benutzers
|
||||
eintragen, die Einladung mit dem Link geht sofort raus. "Erneut senden"
|
||||
schickt dieselbe Adresse noch einmal; der alte Link wird ungültig.
|
||||
"""
|
||||
|
||||
if find_device_in_organization(device_id, user.organization_id) is None:
|
||||
raise HTTPException(status_code=404, detail="Gerät wurde nicht gefunden.")
|
||||
|
||||
# Mit Domänenliste nur der Teil vor dem @ plus gewählte Domäne.
|
||||
if email_lokal.strip():
|
||||
lokal = email_lokal.strip()
|
||||
adresse = f"{lokal}@{email_domain.strip().lower()}" if "@" not in lokal and email_domain.strip() else ""
|
||||
else:
|
||||
adresse = email.strip()
|
||||
if not adresse:
|
||||
return geraete_info(request, device_id, user, {"fehler": "Bitte eine E-Mail-Adresse angeben."}, status_code=400)
|
||||
|
||||
result = anode_request(
|
||||
"POST",
|
||||
f"/api/v1/devices/{device_id}/personalisierung",
|
||||
json={"bestehenden_behalten": bool(bestehenden_behalten), "erstellt_von": user.email},
|
||||
json={"email": adresse, "bestehenden_behalten": bool(bestehenden_behalten), "erstellt_von": user.email},
|
||||
)
|
||||
if not result.get("success"):
|
||||
fehler = result.get("message") or "Die Einladung wurde abgelehnt."
|
||||
return geraete_info(request, device_id, user, {"fehler": fehler}, status_code=400)
|
||||
|
||||
# Der Code wird nur in dieser Antwort angezeigt (anode speichert nur
|
||||
# seinen Hash) - deshalb kein Redirect.
|
||||
neuer_code = {"code": result["code"], "gueltig_bis": result["code_gueltig_bis"]} if result.get("success") else None
|
||||
return geraete_info(request, device_id, user, neuer_code)
|
||||
try:
|
||||
einladung_senden(result)
|
||||
except ValueError:
|
||||
anode_request("POST", f"/api/v1/devices/{device_id}/personalisierung/zurueckziehen")
|
||||
return geraete_info(request, device_id, user, {"fehler": "Die E-Mail-Adresse ist ungültig."}, status_code=400)
|
||||
|
||||
return geraete_info(request, device_id, user, {"gesendet_an": adresse})
|
||||
|
||||
|
||||
@router.post("/geraete/{device_id}/personalisierung/{personalisierung_id}/sperren")
|
||||
|
||||
+64
-107
@@ -3,6 +3,7 @@ import time
|
||||
from collections import defaultdict, deque
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
import mail
|
||||
from anode_client import anode_request
|
||||
@@ -10,11 +11,12 @@ from auth import OEFFENTLICHE_URL, _client_ip, _rate_limited
|
||||
from templating import templates
|
||||
|
||||
|
||||
# Personalisierung (ADR-0041, Schritt 2): öffentliches Formular, das der Kiosk
|
||||
# im Konto "Personalisierung" am Gerät öffnet (?kiosk=1 blendet "Abmelden"
|
||||
# ein). Erst der Code (zeigt Einrichtung, Gerät und erlaubte Domänen), dann
|
||||
# E-Mail, Anmeldename und Passwort. Das Passwort wird hier sofort gehasht,
|
||||
# anode bekommt nur den SHA-512-crypt-Hash. Bestätigt wird per Mail-Link.
|
||||
# Personalisierung (ADR-0041, vereinfachter Ablauf vom 24.10.2026): Der
|
||||
# Kunden-Admin trägt in der Geräte-Info die Adresse ein (routers/geraete.py),
|
||||
# hier geht die Einladung raus. Über den Link wählt der Benutzer Anmeldename
|
||||
# und Passwort, an jedem Gerät mit Browser. Das Passwort wird hier sofort
|
||||
# gehasht, anode bekommt nur den SHA-512-crypt-Hash. Danach zeigt die
|
||||
# Stand-Seite, ob das Gerät das Konto schon angelegt hat.
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -59,72 +61,67 @@ def _seite(request: Request, vorlage: str, kontext: dict, status_code: int = 200
|
||||
return templates.TemplateResponse(request, vorlage, {"user": None, **kontext}, status_code=status_code)
|
||||
|
||||
|
||||
def _code_formular(request: Request, kiosk: bool, fehler: str | None = None, status_code: int = 200):
|
||||
return _seite(request, "personalisierung_code.html", {"kiosk": kiosk, "fehler": fehler}, status_code)
|
||||
def einladung_senden(ergebnis: dict) -> None:
|
||||
"""Mail mit dem Einladungslink; ergebnis ist die Antwort von anode. ValueError bei ungültiger Adresse."""
|
||||
|
||||
link = f"{OEFFENTLICHE_URL}/personalisierung/einladung?token={ergebnis['token']}"
|
||||
geraet = ergebnis.get("hostname") or "ein Gerät"
|
||||
text = (
|
||||
f"Hallo,\n\n"
|
||||
f"die Verwaltung von {ergebnis['organisation']} richtet dir auf dem Gerät {geraet} "
|
||||
f"ein persönliches Konto ein.\n\n"
|
||||
f"Über diesen Link wählst du deinen Anmeldenamen und dein Passwort, "
|
||||
f"zum Beispiel am Handy:\n\n{link}\n\n"
|
||||
f"Der Link gilt 7 Tage und nur einmal. Danach startest du das Gerät neu und "
|
||||
f"wartest bis zu 2 Minuten; dann kannst du dich mit deinem Anmeldenamen anmelden.\n\n"
|
||||
f"Passwort vergessen? Wende dich an die Verwaltung deiner Einrichtung.\n\n"
|
||||
f"Erwartest du diese Mail nicht, kannst du sie ignorieren.\n\n"
|
||||
f"Tuxflotte\n"
|
||||
)
|
||||
mail.einreihen(ergebnis["email"], "Tuxflotte: dein persönliches Konto", text, "personalisierung", ergebnis["personalisierung_id"])
|
||||
|
||||
|
||||
@router.get("/personalisierung")
|
||||
def personalisierung_start(request: Request, kiosk: int = 0):
|
||||
return _code_formular(request, bool(kiosk))
|
||||
def personalisierung_hinweis(request: Request, kiosk: int = 0):
|
||||
# Der Kiosk älterer Rollenstände öffnet noch diese Adresse.
|
||||
return _seite(request, "personalisierung_hinweis.html", {"kiosk": bool(kiosk)})
|
||||
|
||||
|
||||
@router.post("/personalisierung")
|
||||
def personalisierung_code_pruefen(request: Request, code: str = Form(...), kiosk: int = Form(0)):
|
||||
if _gesperrt(request):
|
||||
return _code_formular(request, bool(kiosk), "Zu viele Fehlversuche. Bitte später erneut versuchen.", 429)
|
||||
|
||||
ergebnis = anode_request("POST", "/api/v1/personalisierung/pruefen", json={"code": code})
|
||||
if not ergebnis.get("success"):
|
||||
_fehlversuch(request)
|
||||
return _code_formular(request, bool(kiosk), ergebnis.get("message"), 400)
|
||||
|
||||
return _seite(
|
||||
request,
|
||||
"personalisierung_formular.html",
|
||||
{"kiosk": bool(kiosk), "code": code.strip(), "pruefung": ergebnis, "fehler": None, "werte": {}},
|
||||
)
|
||||
def _einladung_seite(request: Request, kontext: dict, status_code: int = 200):
|
||||
return _seite(request, "personalisierung_einladung.html",
|
||||
{"pruefung": None, "fehler": None, "token": "", "werte": {}, **kontext}, status_code)
|
||||
|
||||
|
||||
@router.post("/personalisierung/einreichen")
|
||||
def personalisierung_einreichen(
|
||||
@router.get("/personalisierung/einladung")
|
||||
def personalisierung_einladung(request: Request, token: str = ""):
|
||||
# Nur anzeigen; Mailprogramme rufen Links vorab ab, eingelöst wird per POST.
|
||||
pruefung = anode_request("POST", "/api/v1/personalisierung/einladung", json={"token": token})
|
||||
if not pruefung.get("success"):
|
||||
return _einladung_seite(request, {"fehler": pruefung.get("message")}, 400)
|
||||
werte = {"anmeldename": pruefung.get("vorschlag", "")}
|
||||
return _einladung_seite(request, {"pruefung": pruefung, "token": token, "werte": werte})
|
||||
|
||||
|
||||
@router.post("/personalisierung/einladung")
|
||||
def personalisierung_einladung_einloesen(
|
||||
request: Request,
|
||||
code: str = Form(...),
|
||||
kiosk: int = Form(0),
|
||||
email_lokal: str = Form(""),
|
||||
email_domain: str = Form(""),
|
||||
email: str = Form(""),
|
||||
token: str = Form(""),
|
||||
anmeldename: str = Form(...),
|
||||
passwort: str = Form(...),
|
||||
passwort_wiederholung: str = Form(...),
|
||||
):
|
||||
if _gesperrt(request):
|
||||
return _code_formular(request, bool(kiosk), "Zu viele Fehlversuche. Bitte später erneut versuchen.", 429)
|
||||
return _einladung_seite(request, {"fehler": "Zu viele Fehlversuche. Bitte später erneut versuchen."}, 429)
|
||||
|
||||
pruefung = anode_request("POST", "/api/v1/personalisierung/pruefen", json={"code": code})
|
||||
pruefung = anode_request("POST", "/api/v1/personalisierung/einladung", json={"token": token})
|
||||
if not pruefung.get("success"):
|
||||
_fehlversuch(request)
|
||||
return _code_formular(request, bool(kiosk), pruefung.get("message"), 400)
|
||||
|
||||
# Mit Domänenliste nur der Teil vor dem @ plus gewählte Domäne.
|
||||
if pruefung["domaenen"]:
|
||||
lokal = email_lokal.strip()
|
||||
domain = email_domain.strip().lower() or pruefung["domaenen"][0]
|
||||
adresse = f"{lokal}@{domain}" if lokal and "@" not in lokal else ""
|
||||
else:
|
||||
adresse = email.strip()
|
||||
|
||||
werte = {"email_lokal": email_lokal, "email_domain": email_domain, "email": email, "anmeldename": anmeldename}
|
||||
return _einladung_seite(request, {"fehler": pruefung.get("message")}, 400)
|
||||
|
||||
def formular(fehler: str):
|
||||
return _seite(
|
||||
request,
|
||||
"personalisierung_formular.html",
|
||||
{"kiosk": bool(kiosk), "code": code, "pruefung": pruefung, "fehler": fehler, "werte": werte},
|
||||
status_code=400,
|
||||
)
|
||||
kontext = {"pruefung": pruefung, "token": token, "werte": {"anmeldename": anmeldename}, "fehler": fehler}
|
||||
return _einladung_seite(request, kontext, 400)
|
||||
|
||||
if not adresse:
|
||||
return formular("Bitte eine E-Mail-Adresse angeben.")
|
||||
if passwort != passwort_wiederholung:
|
||||
return formular("Die Wiederholung stimmt nicht mit dem Passwort überein.")
|
||||
if not PASSWORT_MIN <= len(passwort) <= PASSWORT_MAX:
|
||||
@@ -134,69 +131,29 @@ def personalisierung_einreichen(
|
||||
|
||||
ergebnis = anode_request(
|
||||
"POST",
|
||||
"/api/v1/personalisierung/einreichen",
|
||||
json={
|
||||
"code": code,
|
||||
"email": adresse,
|
||||
"anmeldename": anmeldename.strip(),
|
||||
"passwort_hash": _passwort_hash(passwort),
|
||||
},
|
||||
"/api/v1/personalisierung/einladung",
|
||||
json={"token": token, "anmeldename": anmeldename.strip(), "passwort_hash": _passwort_hash(passwort)},
|
||||
)
|
||||
if not ergebnis.get("success"):
|
||||
return formular(ergebnis.get("message") or "Die Personalisierung wurde abgelehnt.")
|
||||
return formular(ergebnis.get("message") or "Das Konto wurde abgelehnt.")
|
||||
|
||||
link = f"{OEFFENTLICHE_URL}/personalisierung/bestaetigen?token={ergebnis['token']}"
|
||||
geraet = ergebnis.get("hostname") or "dein Gerät"
|
||||
text = (
|
||||
f"Hallo,\n\n"
|
||||
f"auf dem Gerät {geraet} ({ergebnis['organisation']}) wurde ein persönliches Konto "
|
||||
f"mit dem Anmeldenamen „{anmeldename.strip()}“ beantragt.\n\n"
|
||||
f"Bitte bestätige das innerhalb einer Stunde über diesen Link:\n\n{link}\n\n"
|
||||
f"Danach das Gerät neu starten; das Konto wird beim Start eingerichtet und "
|
||||
f"erscheint nach bis zu 2 Minuten am Anmeldebildschirm.\n\n"
|
||||
f"Warst du das nicht, kannst du diese Mail ignorieren. Ohne Bestätigung "
|
||||
f"verfällt der Antrag.\n\n"
|
||||
f"Tuxflotte\n"
|
||||
)
|
||||
try:
|
||||
mail.einreihen(adresse, "Tuxflotte: persönliches Konto bestätigen", text, "personalisierung", ergebnis["personalisierung_id"])
|
||||
except ValueError:
|
||||
return formular("Die E-Mail-Adresse ist ungültig.")
|
||||
|
||||
return _seite(
|
||||
request,
|
||||
"personalisierung_eingereicht.html",
|
||||
{"kiosk": bool(kiosk), "adresse": adresse, "anmeldename": anmeldename.strip()},
|
||||
return RedirectResponse(
|
||||
f"/personalisierung/stand?id={ergebnis['personalisierung_id']}&t={ergebnis['stand_token']}", status_code=303
|
||||
)
|
||||
|
||||
|
||||
@router.get("/personalisierung/bestaetigen")
|
||||
def personalisierung_bestaetigen_seite(request: Request, token: str = ""):
|
||||
return _seite(request, "personalisierung_bestaetigen.html", {"token": token, "ergebnis": None, "fehler": None})
|
||||
# Solange das Gerät das Konto noch nicht angelegt hat, lädt die Seite sich
|
||||
# selbst neu (head-Block in base.html).
|
||||
STAND_NEU_LADEN_SEKUNDEN = 10
|
||||
|
||||
|
||||
@router.post("/personalisierung/bestaetigen")
|
||||
def personalisierung_bestaetigen(request: Request, token: str = Form("")):
|
||||
ip = _client_ip(request)
|
||||
if _rate_limited(_bestaetigungen_je_ip, ip, BESTAETIGUNG_JE_IP, BESTAETIGUNG_FENSTER_SEKUNDEN):
|
||||
return _seite(
|
||||
request,
|
||||
"personalisierung_bestaetigen.html",
|
||||
{"token": token, "ergebnis": None, "fehler": "Zu viele Anfragen. Bitte später erneut versuchen."},
|
||||
status_code=429,
|
||||
)
|
||||
_bestaetigungen_je_ip[ip].append(time.monotonic())
|
||||
|
||||
ergebnis = anode_request("POST", "/api/v1/personalisierung/bestaetigen", json={"token": token})
|
||||
if not ergebnis.get("success"):
|
||||
return _seite(
|
||||
request,
|
||||
"personalisierung_bestaetigen.html",
|
||||
{"token": "", "ergebnis": None, "fehler": ergebnis.get("message")},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
return _seite(request, "personalisierung_bestaetigen.html", {"token": "", "ergebnis": ergebnis, "fehler": None})
|
||||
@router.get("/personalisierung/stand")
|
||||
def personalisierung_stand(request: Request, id: str = "", t: str = ""):
|
||||
stand = anode_request("POST", "/api/v1/personalisierung/stand", json={"id": id, "token": t})
|
||||
if not stand.get("success"):
|
||||
return _seite(request, "personalisierung_stand.html", {"stand": None, "neu_laden": None}, 404)
|
||||
neu_laden = STAND_NEU_LADEN_SEKUNDEN if stand["status"] == "bestaetigt" else None
|
||||
return _seite(request, "personalisierung_stand.html", {"stand": stand, "neu_laden": neu_laden})
|
||||
|
||||
|
||||
# "Passwort vergessen?" (ADR-0041, Ergänzung 18.10.2026): Nach der
|
||||
|
||||
@@ -51,17 +51,18 @@
|
||||
<section id="personalisierung">
|
||||
<h3>Personalisierung</h3>
|
||||
<p><small>
|
||||
Mit einem Aktivierungscode richtet sich jemand an diesem Gerät ein
|
||||
persönliches Konto ein (Anmeldebildschirm: „Personalisierung“). Das
|
||||
Trage die E-Mail-Adresse der Person ein, die dieses Gerät bekommt. Sie
|
||||
erhält einen Link, wählt darüber Anmeldenamen und Passwort und startet
|
||||
das Gerät danach neu; nach bis zu 2 Minuten kann sie sich anmelden. Das
|
||||
Gerät braucht dafür das Merkmal „Personalisierung möglich“.
|
||||
</small></p>
|
||||
|
||||
{% if neuer_code %}
|
||||
{% if einladung and einladung.gesendet_an %}
|
||||
<div class="highlight-box">
|
||||
<p class="einmalig-hinweis">⚠ Jetzt notieren – der Code wird nicht wieder angezeigt.</p>
|
||||
<p>Aktivierungscode: <strong style="font-size: 1.4rem; letter-spacing: 0.1em;"><code>{{ neuer_code.code }}</code></strong></p>
|
||||
<p><small>Gültig bis {{ neuer_code.gueltig_bis | datum }}. Bitte notieren oder weitergeben.</small></p>
|
||||
<p>Einladung an <strong>{{ einladung.gesendet_an }}</strong> verschickt. Der Link gilt 7 Tage.</p>
|
||||
</div>
|
||||
{% elif einladung and einladung.fehler %}
|
||||
<p><mark>{{ einladung.fehler }}</mark></p>
|
||||
{% endif %}
|
||||
|
||||
{% if neues_passwort %}
|
||||
@@ -110,27 +111,58 @@
|
||||
{% endif %}
|
||||
|
||||
{% if personalisierung_laufend %}
|
||||
<p>Laufend: {{ personalisierung_status[personalisierung_laufend.status] }}
|
||||
{% if personalisierung_laufend.anmeldename %} – <strong>{{ personalisierung_laufend.anmeldename }}</strong> ({{ personalisierung_laufend.email }}){% endif %}
|
||||
{% if personalisierung_laufend.status == "code_offen" %}<br><small>Code gültig bis {{ personalisierung_laufend.code_gueltig_bis | datum }}</small>{% endif %}
|
||||
{% set l = personalisierung_laufend %}
|
||||
<p>Laufend: {{ personalisierung_status[l.status] }} – {{ l.email }}
|
||||
{% if l.anmeldename %}, Anmeldename <strong>{{ l.anmeldename }}</strong>{% endif %}
|
||||
{% if l.status == "eingeladen" %}<br><small>Link gültig bis {{ l.bestaetigung_bis | datum }}</small>{% endif %}
|
||||
</p>
|
||||
<form method="post" action="/geraete/{{ device.id }}/personalisierung/zurueckziehen">
|
||||
<button type="submit" class="secondary outline">Code zurückziehen</button>
|
||||
</form>
|
||||
<div class="grid">
|
||||
{% if l.status == "eingeladen" %}
|
||||
<form method="post" action="/geraete/{{ device.id }}/personalisierung/einladung">
|
||||
<input type="hidden" name="email" value="{{ l.email }}">
|
||||
{% if l.bestehenden_behalten %}<input type="hidden" name="bestehenden_behalten" value="1">{% endif %}
|
||||
<button type="submit" class="secondary outline">Einladung erneut senden</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/geraete/{{ device.id }}/personalisierung/zurueckziehen">
|
||||
<button type="submit" class="secondary outline">Zurückziehen</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/geraete/{{ device.id }}/personalisierung/code">
|
||||
{% if personalisierung_moeglich %}
|
||||
<form method="post" action="/geraete/{{ device.id }}/personalisierung/einladung">
|
||||
{% if email_domaenen %}
|
||||
<label for="email_lokal">E-Mail-Adresse</label>
|
||||
<fieldset role="group">
|
||||
<input type="text" id="email_lokal" name="email_lokal" required autocomplete="off" placeholder="vorname.nachname">
|
||||
{% if email_domaenen | length == 1 %}
|
||||
<input type="hidden" name="email_domain" value="{{ email_domaenen[0] }}">
|
||||
<input type="text" value="@{{ email_domaenen[0] }}" readonly tabindex="-1" aria-label="Domäne">
|
||||
{% else %}
|
||||
<select name="email_domain" aria-label="Domäne">
|
||||
{% for d in email_domaenen %}<option value="{{ d }}">@{{ d }}</option>{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<label for="email">E-Mail-Adresse</label>
|
||||
<input type="email" id="email" name="email" required autocomplete="off">
|
||||
{% endif %}
|
||||
{% if personalisierung_konten %}
|
||||
<label>
|
||||
<input type="checkbox" name="bestehenden_behalten" value="1">
|
||||
Bestehenden Zugang behalten
|
||||
</label>
|
||||
<small>Ohne Haken wird das bisherige Konto samt seinen Dateien entfernt, sobald das neue bestätigt ist.</small>
|
||||
<small>Ohne Haken wird das bisherige Konto sofort gesperrt und samt seinen Dateien entfernt, sobald das neue eingerichtet ist.</small>
|
||||
{% endif %}
|
||||
<button type="submit" class="outline">
|
||||
{% if personalisierung_laufend %}Neuen Code erzeugen (ersetzt den laufenden){% elif personalisierung_konten %}Erneute Personalisierung aktivieren{% else %}Aktivierungscode erzeugen{% endif %}
|
||||
{% if personalisierung_laufend %}Neue Einladung senden (ersetzt die laufende){% elif personalisierung_konten %}Erneut personalisieren{% else %}Einladung senden{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p><small>Für dieses Gerät ist „Personalisierung möglich“ nicht gesetzt (Workspace, OE oder Gruppe).</small></p>
|
||||
{% endif %}
|
||||
|
||||
{% if personalisierungen %}
|
||||
<details>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Tuxflotte{% endblock %}</title>
|
||||
{% block head %}{% endblock %}
|
||||
<script src="/static/theme-init.js?v={{ static_version }}"></script>
|
||||
<link rel="stylesheet" href="/static/pico.min.css?v={{ static_version }}">
|
||||
<script src="/static/table-tools.js?v={{ static_version }}" defer></script>
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
<section id="email-domaenen">
|
||||
<h3>E-Mail-Domänen für die Personalisierung</h3>
|
||||
<p><small>
|
||||
Wer sich an einem Gerät ein persönliches Konto einrichtet, muss eine
|
||||
Adresse mit einer dieser Domänen angeben, zum Beispiel die Domäne der
|
||||
Schule und die ByCS-Domäne der Schülerschaft. Solche Adressen sind in
|
||||
der Geräteliste sichtbar. Leer: jede Adresse ist erlaubt.
|
||||
Wer ein Gerät personalisiert, kann nur Adressen mit einer dieser
|
||||
Domänen eintragen, zum Beispiel die Domäne der Schule und die
|
||||
ByCS-Domäne der Schülerschaft. Solche Adressen sind in der Geräteliste
|
||||
sichtbar. Leer: jede Adresse ist erlaubt.
|
||||
</small></p>
|
||||
{% if domaenen_fehler %}<p><mark>{{ domaenen_fehler }}</mark></p>{% endif %}
|
||||
<form method="post" action="/organisation/email-domaenen">
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Personalisierung bestätigen — Tuxflotte{% endblock %}
|
||||
{% block content %}
|
||||
<article style="max-width: 30rem; margin: 3rem auto;">
|
||||
<h2>Persönliches Konto bestätigen</h2>
|
||||
{% if ergebnis %}
|
||||
<p>Bestätigt. Starte das Gerät{% if ergebnis.hostname %} <strong>{{ ergebnis.hostname }}</strong>{% endif %} neu; beim Start wird das Konto <strong>{{ ergebnis.anmeldename }}</strong> eingerichtet und erscheint nach bis zu 2 Minuten am Anmeldebildschirm.</p>
|
||||
{% elif fehler %}
|
||||
<p><mark>{{ fehler }}</mark></p>
|
||||
<p>Für einen neuen Versuch braucht es einen neuen Aktivierungscode von der Verwaltung deiner Einrichtung.</p>
|
||||
{% elif token %}
|
||||
<form method="post" action="/personalisierung/bestaetigen">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
<button type="submit">Jetzt bestätigen</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p><mark>Der Link ist unvollständig.</mark></p>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
@@ -1,17 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Personalisierung — Tuxflotte{% endblock %}
|
||||
{% block content %}
|
||||
<article style="max-width: 30rem; margin: 3rem auto;">
|
||||
<h2>Persönliches Konto einrichten</h2>
|
||||
{% if fehler %}<p><mark>{{ fehler }}</mark></p>{% endif %}
|
||||
<p>Mit einem Aktivierungscode von der Verwaltung deiner Einrichtung bekommst du auf diesem Gerät ein eigenes Konto mit Passwort.</p>
|
||||
<form method="post" action="/personalisierung">
|
||||
<input type="hidden" name="kiosk" value="{{ 1 if kiosk else 0 }}">
|
||||
<label for="code">Aktivierungscode</label>
|
||||
<input type="text" id="code" name="code" required autofocus autocomplete="off"
|
||||
autocapitalize="characters" spellcheck="false" placeholder="XXXX-XXXX-XXXX" maxlength="20">
|
||||
<button type="submit">Weiter</button>
|
||||
</form>
|
||||
{% include "_personalisierung_abmelden.html" %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Personalisierung — Tuxflotte{% endblock %}
|
||||
{% block content %}
|
||||
<article style="max-width: 30rem; margin: 3rem auto;">
|
||||
<h2>Fast geschafft</h2>
|
||||
<p>Wir haben eine Mail an <strong>{{ adresse }}</strong> geschickt. Öffne den Link darin <strong>innerhalb einer Stunde</strong>, zum Beispiel am Handy.</p>
|
||||
<p>Danach das Gerät neu starten. Beim Start wird das Konto <strong>{{ anmeldename }}</strong> eingerichtet und erscheint nach bis zu 2 Minuten am Anmeldebildschirm.</p>
|
||||
<p>Keine Mail erhalten? Bitte auch im Spam-Ordner nachsehen. Ohne Bestätigung verfällt der Antrag, dann braucht es einen neuen Code von der Verwaltung.</p>
|
||||
{% include "_personalisierung_abmelden.html" %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Persönliches Konto — Tuxflotte{% endblock %}
|
||||
{% block content %}
|
||||
<article style="max-width: 30rem; margin: 3rem auto;">
|
||||
<h2>Persönliches Konto einrichten</h2>
|
||||
{% if fehler %}<p><mark>{{ fehler }}</mark></p>{% endif %}
|
||||
{% if pruefung %}
|
||||
<p>{{ pruefung.organisation }}{% if pruefung.hostname %}, Gerät <strong>{{ pruefung.hostname }}</strong>{% endif %}</p>
|
||||
<form method="post" action="/personalisierung/einladung">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
|
||||
<label for="anmeldename">Anmeldename</label>
|
||||
<input type="text" id="anmeldename" name="anmeldename" required autocomplete="off" spellcheck="false"
|
||||
autocapitalize="none" pattern="[a-z][a-z0-9._\-]{1,30}" maxlength="31" value="{{ werte.anmeldename or '' }}">
|
||||
<small>Damit meldest du dich am Gerät an. Kleinbuchstaben, Ziffern, Punkt, Binde- und Unterstrich; beginnt mit einem Buchstaben.</small>
|
||||
|
||||
<label for="passwort">Passwort</label>
|
||||
<input type="password" id="passwort" name="passwort" required minlength="8" maxlength="128" autocomplete="new-password" autofocus>
|
||||
<small>Mindestens 8 Zeichen.</small>
|
||||
<label for="passwort_wiederholung">Passwort (Wiederholung)</label>
|
||||
<input type="password" id="passwort_wiederholung" name="passwort_wiederholung" required minlength="8" maxlength="128" autocomplete="new-password">
|
||||
|
||||
<button type="submit">Konto einrichten</button>
|
||||
</form>
|
||||
<p><small>Die Verwaltung deiner Einrichtung sieht, wer dieses Gerät personalisiert hat.</small></p>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
@@ -1,49 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Personalisierung — Tuxflotte{% endblock %}
|
||||
{% block content %}
|
||||
<article style="max-width: 30rem; margin: 3rem auto;">
|
||||
<h2>Persönliches Konto einrichten</h2>
|
||||
<p>{{ pruefung.organisation }}{% if pruefung.hostname %}, Gerät <strong>{{ pruefung.hostname }}</strong>{% endif %}</p>
|
||||
{% if fehler %}<p><mark>{{ fehler }}</mark></p>{% endif %}
|
||||
<form method="post" action="/personalisierung/einreichen">
|
||||
<input type="hidden" name="kiosk" value="{{ 1 if kiosk else 0 }}">
|
||||
<input type="hidden" name="code" value="{{ code }}">
|
||||
|
||||
{% if pruefung.domaenen %}
|
||||
<label for="email_lokal">E-Mail-Adresse deiner Einrichtung</label>
|
||||
<fieldset role="group">
|
||||
<input type="text" id="email_lokal" name="email_lokal" required autocomplete="off"
|
||||
value="{{ werte.email_lokal or '' }}" placeholder="vorname.nachname">
|
||||
{% if pruefung.domaenen | length == 1 %}
|
||||
<input type="hidden" name="email_domain" value="{{ pruefung.domaenen[0] }}">
|
||||
<input type="text" value="@{{ pruefung.domaenen[0] }}" readonly tabindex="-1" aria-label="Domäne">
|
||||
{% else %}
|
||||
<select name="email_domain" aria-label="Domäne">
|
||||
{% for d in pruefung.domaenen %}
|
||||
<option value="{{ d }}" {% if werte.email_domain == d %}selected{% endif %}>@{{ d }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<label for="email">E-Mail-Adresse</label>
|
||||
<input type="email" id="email" name="email" required autocomplete="off" value="{{ werte.email or '' }}">
|
||||
{% endif %}
|
||||
<small>Dorthin kommt ein Bestätigungslink. Die Verwaltung deiner Einrichtung sieht, wer dieses Gerät personalisiert hat.</small>
|
||||
|
||||
<label for="anmeldename">Anmeldename</label>
|
||||
<input type="text" id="anmeldename" name="anmeldename" required autocomplete="off" spellcheck="false"
|
||||
pattern="[a-z][a-z0-9._\-]{1,30}" maxlength="31" value="{{ werte.anmeldename or '' }}">
|
||||
<small>Kleinbuchstaben, Ziffern, Punkt, Binde- und Unterstrich; beginnt mit einem Buchstaben.</small>
|
||||
|
||||
<label for="passwort">Passwort</label>
|
||||
<input type="password" id="passwort" name="passwort" required minlength="8" maxlength="128" autocomplete="new-password">
|
||||
<small>Mindestens 8 Zeichen. Vergessen? Die Verwaltung kann es zurücksetzen.</small>
|
||||
<label for="passwort_wiederholung">Passwort (Wiederholung)</label>
|
||||
<input type="password" id="passwort_wiederholung" name="passwort_wiederholung" required minlength="8" maxlength="128" autocomplete="new-password">
|
||||
|
||||
<button type="submit">Konto beantragen</button>
|
||||
</form>
|
||||
{% include "_personalisierung_abmelden.html" %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Personalisierung — Tuxflotte{% endblock %}
|
||||
{% block content %}
|
||||
<article style="max-width: 30rem; margin: 3rem auto;">
|
||||
<h2>Persönliches Konto</h2>
|
||||
<p>Ein persönliches Konto richtet jetzt die Verwaltung deiner Einrichtung ein: Sie trägt deine E-Mail-Adresse beim Gerät ein, und du bekommst eine Mail mit einem Link.</p>
|
||||
<p>Über den Link wählst du Anmeldenamen und Passwort, zum Beispiel am Handy. Danach das Gerät neu starten und bis zu 2 Minuten warten.</p>
|
||||
{% include "_personalisierung_abmelden.html" %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Persönliches Konto — Tuxflotte{% endblock %}
|
||||
{% block head %}{% if neu_laden %}<meta http-equiv="refresh" content="{{ neu_laden }}">{% endif %}{% endblock %}
|
||||
{% block content %}
|
||||
<article style="max-width: 30rem; margin: 3rem auto;">
|
||||
<h2>Persönliches Konto</h2>
|
||||
{% if not stand %}
|
||||
<p><mark>Diesen Vorgang gibt es nicht.</mark></p>
|
||||
{% elif stand.status == "bestaetigt" %}
|
||||
<p>Gespeichert. Jetzt das Gerät{% if stand.hostname %} <strong>{{ stand.hostname }}</strong>{% endif %} <strong>neu starten</strong> und bis zu 2 Minuten warten.</p>
|
||||
<p>Beim Start richtet das Gerät das Konto <strong>{{ stand.anmeldename }}</strong> ein. Diese Seite zeigt es an, sobald es fertig ist.</p>
|
||||
<p aria-busy="true">Wartet auf das Gerät …</p>
|
||||
{% elif stand.status == "angelegt" %}
|
||||
<p>Fertig. Das Konto <strong>{{ stand.anmeldename }}</strong> ist auf dem Gerät{% if stand.hostname %} {{ stand.hostname }}{% endif %} eingerichtet.</p>
|
||||
<p>Melde dich dort mit deinem Anmeldenamen und Passwort an.</p>
|
||||
{% elif stand.status == "fehlgeschlagen" %}
|
||||
<p><mark>Das Gerät konnte das Konto <strong>{{ stand.anmeldename }}</strong> nicht einrichten.</mark></p>
|
||||
{% if stand.geraet_meldung %}<p><small>Gerät meldet: {{ stand.geraet_meldung }}</small></p>{% endif %}
|
||||
<p>Bitte wende dich an die Verwaltung von {{ stand.organisation }}. Sie kann dir eine neue Einladung schicken.</p>
|
||||
{% else %}
|
||||
<p>Dieser Vorgang ist abgeschlossen oder wurde von der Verwaltung beendet. Bei Fragen wende dich an die Verwaltung von {{ stand.organisation }}.</p>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user