diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-31 17:19:17 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-31 17:19:17 +0200 |
| commit | c6fd7ea89b6e0a96eb1d81989de891b4768b1044 (patch) | |
| tree | 12e869044c80c889f83b588210cc0ac500cd7a6a /packages/meshbay-hub/src/meshbay_hub/api/groups.py | |
| parent | f4c6628c8e85513d9fd110ead95682368a15a0fd (diff) | |
| download | meshbay-c6fd7ea89b6e0a96eb1d81989de891b4768b1044.tar.gz | |
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/groups.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/groups.py | 53 |
1 files changed, 52 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py index 49902de..fcf0360 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py @@ -7,7 +7,8 @@ from sqlalchemy import func, or_, select, update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_hub import hub_settings +from meshbay_hub import hub_settings, mail +from meshbay_hub.auth import decrypt_email from meshbay_hub.api.deps import get_current_user, require_user_scope from meshbay_hub.api.netutil import client_ip from meshbay_hub.db.engine import get_db @@ -671,3 +672,53 @@ async def delete_group( return {"status": "deleted", "group_id": group_id} +class InviteNotifyRequest(BaseModel): + username: str + code: str + group_name: str + + +@router.post("/{group_id}/invite-notify") +async def invite_notify( + group_id: str, + body: InviteNotifyRequest, + current_user: User = Depends(require_user_scope), + db: AsyncSession = Depends(get_db), +): + """Send an invitation email to a member who was just invited. + + The invite code was created on the node — the hub only knows about it + because the inviter's browser sends it here. The hub looks up the + invitee's encrypted email, decrypts it, and sends the notification. + The inviter never sees the email address. + """ + group = await db.get(Group, group_id) + if not group: + raise HTTPException(status_code=404, detail="Group not found") + if group.admin_id != current_user.id: + raise HTTPException(status_code=403, + detail="Only the group owner can send invitations") + + target = (await db.execute( + select(User).where(User.username == body.username))).scalar_one_or_none() + if not target: + raise HTTPException(status_code=404, detail="User not found") + + email = "" + try: + email = decrypt_email(target.email) if target.email else "" + except Exception: + pass + + if not email: + return {"status": "no_email"} + + try: + mail.send_invite_notification( + email, body.code, current_user.username, body.group_name) + except Exception: + return {"status": "send_failed"} + + return {"status": "sent"} + + |