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:
adminandClaude Opus 5.5 committed 2026-10-24 10:52:15 +02:00
1 parent 4e701cdf82
commit 8937c28028
13 files changed
+229 -236

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+47 -15
View File
@@ -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>
+1
View File
@@ -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>
+4 -4
View File
@@ -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 %}
-17
View File
@@ -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 %}
+28
View File
@@ -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 %}
-49
View File
@@ -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 %}
+10
View File
@@ -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 %}
+24
View File
@@ -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 %}