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 --- .../src/meshbay_hub/static/group-settings.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-settings.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js index 4f55dfb..1c6ca71 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -793,7 +793,21 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, method: 'POST', token, body: {}, }); - setInviteCode({ username, code: result.code, expires: result.expires_at }); + // Send an email notification to the invitee with the code. + // The hub decrypts their email server-side — the inviter never sees it. + let emailStatus = 'no_email'; + try { + const notif = await hubFetch(`/v1/groups/${groupId}/invite-notify`, { + method: 'POST', token, + body: { username, code: result.code, group_name: group?.name || '' }, + }); + emailStatus = notif.status; + } catch { /* best effort */ } + + setInviteCode({ + username, code: result.code, expires: result.expires_at, + emailSent: emailStatus === 'sent', + }); setInviteUser(''); loadMembers(); } catch (err) { @@ -831,6 +845,10 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef,

${t('members.invite_code_ready', { user: inviteCode.username })}

${inviteCode.code}

+ ${inviteCode.emailSent + ? html`

${t('members.invite_email_sent')}

` + : html`

${t('members.invite_email_failed')}

` + }

${t('members.invite_code_hint')}

`} -- cgit v1.2.3