Passwort vergessen per Adresse (ADR-0041, 24.10.2026)

Je eingerichtetem Konto mit der Adresse ein Link, auch auf mehreren
Geräten; Begrenzung je Konto mit dem Kiosk-Weg geteilt, Konten an der
Grenze still übergangen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-24 11:09:56 +02:00
1 parent bcfe411290
commit da44fd1c6a
1 file changed
+67 -17
+67 -17
View File
@@ -6334,30 +6334,36 @@ def vergessen_anfordern(device_id: str, token: str):
Mail, die kundenplattform verschickt.
"""
jetzt = datetime.now(timezone.utc)
with get_database_connection() as conn:
with conn.cursor() as cur:
row = _vergessen_konto(cur, device_id, token)
if row is None:
return None, "Auf diesem Gerät ist kein persönliches Konto eingerichtet."
pid, anmeldename, email, mails, hostname, org_name = row
cur.execute("SELECT 1 FROM personalisierungen WHERE id = %s FOR UPDATE", (pid,))
return _vergessen_link(cur, row)
letzte = [m for m in (mails or []) if m > jetzt - timedelta(days=1)]
if letzte and max(letzte) > jetzt - VERGESSEN_ABSTAND:
return None, "Es wurde gerade erst ein Link verschickt. Bitte im Postfach (auch im Spam-Ordner) nachsehen oder in 10 Minuten erneut versuchen."
if len(letzte) >= VERGESSEN_JE_TAG:
return None, "Heute wurden schon mehrere Links verschickt. Bitte morgen erneut versuchen oder an die Verwaltung wenden."
link_token = secrets.token_urlsafe(32)
cur.execute(
"""
UPDATE personalisierungen
SET vergessen_token_hash = %s, vergessen_bis = %s, vergessen_mails = %s
WHERE id = %s
""",
(personalisierung_token_hash(link_token), jetzt + VERGESSEN_LINK_GUELTIG, letzte + [jetzt], pid),
)
def _vergessen_link(cur, row):
"""Neuer Link für ein Konto, sofern die Begrenzung es zulässt. Gibt (Ergebnis, Fehler) zurück."""
jetzt = datetime.now(timezone.utc)
pid, anmeldename, email, mails, hostname, org_name = row
cur.execute("SELECT 1 FROM personalisierungen WHERE id = %s FOR UPDATE", (pid,))
letzte = [m for m in (mails or []) if m > jetzt - timedelta(days=1)]
if letzte and max(letzte) > jetzt - VERGESSEN_ABSTAND:
return None, "Es wurde gerade erst ein Link verschickt. Bitte im Postfach (auch im Spam-Ordner) nachsehen oder in 10 Minuten erneut versuchen."
if len(letzte) >= VERGESSEN_JE_TAG:
return None, "Heute wurden schon mehrere Links verschickt. Bitte morgen erneut versuchen oder an die Verwaltung wenden."
link_token = secrets.token_urlsafe(32)
cur.execute(
"""
UPDATE personalisierungen
SET vergessen_token_hash = %s, vergessen_bis = %s, vergessen_mails = %s
WHERE id = %s
""",
(personalisierung_token_hash(link_token), jetzt + VERGESSEN_LINK_GUELTIG, letzte + [jetzt], pid),
)
return {
"personalisierung_id": str(pid),
@@ -6370,6 +6376,38 @@ def vergessen_anfordern(device_id: str, token: str):
}, None
def vergessen_per_adresse(email: str) -> list[dict]:
"""
"Passwort vergessen?" auf der öffentlichen Seite (ADR-0041, Ergänzung
24.10.2026): je eingerichtetem Konto mit dieser Adresse ein Link, auch auf
mehreren Geräten. Konten an der Begrenzung werden still übergangen; die
Seite antwortet immer gleich und verrät nicht, ob es die Adresse gibt.
"""
email = (email or "").strip()
if not EMAIL_MUSTER.match(email):
return []
links = []
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT p.id, p.anmeldename, p.email, p.vergessen_mails, d.hostname, o.name
FROM personalisierungen p
JOIN devices d ON d.id = p.device_id
JOIN organizations o ON o.id = p.organization_id
WHERE lower(p.email) = lower(%s) AND p.status = 'angelegt' AND NOT p.entfernen AND NOT p.gesperrt
ORDER BY p.angelegt_am
""",
(email,),
)
for row in cur.fetchall():
ergebnis, _fehler = _vergessen_link(cur, row)
if ergebnis:
links.append(ergebnis)
return links
def _neues_passwort_konto(cur, token: str):
cur.execute(
"""
@@ -7071,6 +7109,18 @@ def post_vergessen_anfordern(payload: VergessenRequest, authorization: str | Non
return {"success": True, **ergebnis}
class VergessenPerAdresseRequest(BaseModel):
email: str
@app.post("/api/v1/personalisierung/vergessen/per-adresse")
def post_vergessen_per_adresse(payload: VergessenPerAdresseRequest, authorization: str | None = Header(default=None)):
if not require_service_token(authorization):
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
return {"success": True, "links": vergessen_per_adresse(payload.email)}
@app.post("/api/v1/personalisierung/neues-passwort")
def post_neues_passwort(payload: NeuesPasswortRequest, authorization: str | None = Header(default=None)):
"""Ohne passwort_hash nur prüfen (Seite hinter dem Link), mit Hash setzen."""