aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api/groups.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-31 17:19:17 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-31 17:19:17 +0200
commitc6fd7ea89b6e0a96eb1d81989de891b4768b1044 (patch)
tree12e869044c80c889f83b588210cc0ac500cd7a6a /packages/meshbay-hub/src/meshbay_hub/api/groups.py
parentf4c6628c8e85513d9fd110ead95682368a15a0fd (diff)
downloadmeshbay-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.py53
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"}
+
+