feat: add e-mail change with confirmation and undo

Page at /email asks for the new address and the current password (shared failure counter with the password page, five requests per hour) and mails a one-hour confirmation link to the new address. Confirming writes the address and mails the old address a 24-hour undo link; undoing restores it, ends every session of the user and notifies the Admin (ADMIN_NOTIFY_EMAIL). Link pages show on GET and apply on POST; tokens are stored only as hashes, single use, and are blanked from every log line. New table owned by profile, German text, tests; review findings logged in the plan.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FocPJ75XPma7TcuYEMy8zm
This commit is contained in:
Ebbe Baß
2026-10-19 10:05:41 +02:00
co-authored by Claude Sonnet 5.5
parent 3fd8f27127
commit 94c1d39c9d
35 changed files with 1792 additions and 14 deletions
+25
View File
@@ -0,0 +1,25 @@
from django.db import models
class PendingEmailChange(models.Model):
"""A one-time link for an e-mail change. Owned by `profile` (AD-19).
The token itself is never stored, only its SHA-256 hash (AD-9). A `confirm` row holds the
address the user asked for (`new_address`) and the one they had then (`old_address`). An
`undo` row holds the address to restore (`old_address`) and the one that was confirmed
(`new_address`).
"""
class Kind(models.TextChoices):
CONFIRM = "confirm"
UNDO = "undo"
user_key = models.CharField(max_length=256, db_index=True) # lower case
sam = models.CharField(max_length=256) # exact sAMAccountName, for directory calls
kind = models.CharField(max_length=10, choices=Kind.choices)
token_hash = models.CharField(max_length=64, unique=True)
old_address = models.CharField(max_length=254, blank=True)
new_address = models.CharField(max_length=254)
created_at = models.DateTimeField()
expires_at = models.DateTimeField()
used_at = models.DateTimeField(null=True, blank=True)