aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-11 14:55:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-11 14:55:52 +0200
commitcc90dc943fcd0e7bbf52674fb3f95ff097026f4a (patch)
tree6315a5e966c027c50b92d59e60c4b506a418143e /packages/meshbay-hub/src/meshbay_hub/api
parentedde9e441fb6b84e9d56215d6e2a8d9338b8f962 (diff)
downloadmeshbay-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.py53
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