From c6fd7ea89b6e0a96eb1d81989de891b4768b1044 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 31 Aug 2026 17:19:17 +0200 Subject: feat: email verification for registration, email change, and invitations Registration now creates a pending account and sends a 6-digit code via email; the account activates only after verification. Email changes on the profile page follow the same flow. Group invitations send a notification email to the invitee (without revealing their address to the inviter) containing the invite code and hub link. Backend: blind HMAC-SHA256 email index for uniqueness without decryption, mail.py for localhost Postfix delivery, verification endpoints, cleanup of expired codes and stale pending accounts, startup backfill of email_hash for existing users. Frontend: 3-phase register page, inline email change verification on profile, invite-notify call with status display. All 10 locales updated. Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-hub/src/meshbay_hub/auth.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/auth.py') diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py index 28f13a5..2bf59db 100644 --- a/packages/meshbay-hub/src/meshbay_hub/auth.py +++ b/packages/meshbay-hub/src/meshbay_hub/auth.py @@ -189,6 +189,21 @@ def decrypt_email(stored: str) -> str: return AESGCM(_email_key).decrypt(nonce, ct, None).decode() +def hash_email_blind(email: str) -> str: + """Deterministic HMAC-SHA256 of the lowercased email for uniqueness checks. + + The encrypted email uses a random nonce, so two encryptions of the same + address produce different ciphertexts. This blind index allows a DB-level + uniqueness constraint without decrypting every row. + """ + if _email_key is None: + raise RuntimeError("Hub keypair not loaded") + import hmac as _hmac + return _hmac.new( + _email_key, email.strip().lower().encode(), hashlib.sha256, + ).hexdigest() + + # ── Refresh tokens ──────────────────────────────────────────────────────────── def generate_refresh_token() -> tuple[str, str]: -- cgit v1.2.3