diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-11 14:55:52 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-11 14:55:52 +0200 |
| commit | cc90dc943fcd0e7bbf52674fb3f95ff097026f4a (patch) | |
| tree | 6315a5e966c027c50b92d59e60c4b506a418143e /packages/meshbay-hub/src/meshbay_hub/api | |
| parent | edde9e441fb6b84e9d56215d6e2a8d9338b8f962 (diff) | |
| download | meshbay-cc90dc943fcd0e7bbf52674fb3f95ff097026f4a.tar.gz | |
feat: Phase 10b — Self-service UI (group create/join, upload, IndexedDB, search)
Six self-service features for the web SPA:
- Group creation UI with GEK auto-generation (AES-256-GCM ECIES)
- Member management + invite by username (GEK wrapping for invitee)
- Open group self-join flow (POST /v1/groups/{id}/join)
- File upload client→node (FILE_UPLOAD MNP type, .uploads/ staging)
- IndexedDB caching of group file indexes (instant display on revisit)
- Cross-group file search (SearchPage, pure client-side on cached indexes)
11 new tests (166 total): 8 group self-service + 3 AES GEK wrap/unwrap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/groups.py | 53 |
1 files changed, 53 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py index a8a45c9..e0ea016 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py @@ -162,6 +162,59 @@ async def swarm_sources( } +@router.get("/{group_id}/members") +async def group_members( + group_id: str, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + group = await db.get(Group, group_id) + if not group: + raise HTTPException(status_code=404, detail="Group not found") + + mem = await db.get(GroupMember, (group_id, current_user.id)) + if not mem: + raise HTTPException(status_code=403, detail="Not a member") + + result = await db.execute( + select(User.id, User.username) + .join(GroupMember, User.id == GroupMember.user_id) + .where(GroupMember.group_id == group_id) + ) + members = [{"user_id": uid, "username": uname} for uid, uname in result.all()] + return { + "group_id": group_id, + "admin_id": group.admin_id, + "members": members, + } + + +@router.post("/{group_id}/join") +async def join_group( + group_id: str, + request: Request, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + group = await db.get(Group, group_id) + if not group: + raise HTTPException(status_code=404, detail="Group not found") + if group.status != "active": + raise HTTPException(status_code=403, detail="Group is not active") + if group.join_policy != "open": + raise HTTPException(status_code=403, detail="Group does not allow open joining") + + existing = await db.get(GroupMember, (group_id, current_user.id)) + if existing: + raise HTTPException(status_code=409, detail="Already a member") + + db.add(GroupMember(group_id=group_id, user_id=current_user.id)) + db.add(IPLog(user_id=current_user.id, event="group_join", + ip_address=_ip(request), detail=group.name)) + await db.commit() + return {"status": "joined", "group_id": group_id, "name": group.name} + + class GroupCreateRequest(BaseModel): name: str visibility: str = "private" # public|private |