aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api/groups.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-13 03:56:30 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-13 03:56:30 +0200
commitf0248975908ad670fa8a820f865bf22ea8d0172d (patch)
treef4af64d36cacaccb4f6d13436e001aeb57e861e3 /packages/meshbay-hub/src/meshbay_hub/api/groups.py
parent35130e5528a52161630fd1c93572e1b2b7cd911b (diff)
downloadmeshbay-f0248975908ad670fa8a820f865bf22ea8d0172d.tar.gz
feat: Phase 12 — P2P crypto material, password split, node Ed25519 auth
Baseline commit capturing in-progress Phase 12 work that was already present in the working tree (uncommitted) before the Phase 11.5 security remediation begins. Committed as-is, without review or modification, so that remediation changes arrive as a separable diff. Contents: BundleStore (P2P GEK + keypair bundles), password split (auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin challenge-response, node local admin UI rewrite, browser key persistence. Not authored in this session — captured to establish a baseline. Co-Authored-By: Claude Opus 5 <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.py77
1 files changed, 31 insertions, 46 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py
index e0ea016..88af764 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py
@@ -5,10 +5,10 @@ from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_hub.api.deps import get_current_user
+from meshbay_hub.api.deps import get_current_user, require_user_scope
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import (
- FederatedGroup, GEKBundle, Group, GroupMember,
+ FederatedGroup, Group, GroupMember,
IPLog, SwarmSource, User,
)
@@ -37,6 +37,7 @@ async def my_groups(
"join_policy": g.join_policy,
"created_at": g.created_at.isoformat(),
"is_admin": g.admin_id == current_user.id,
+ "description": g.description or "",
}
for g in groups
]
@@ -56,6 +57,8 @@ async def group_online_nodes(
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 suspended")
node_ids = get_online_nodes_for_group(group_id)
nodes = []
@@ -85,6 +88,7 @@ async def list_public_groups(
groups = [
{
"id": g.id, "name": g.name, "join_policy": g.join_policy,
+ "description": g.description or "",
"created_at": g.created_at.isoformat(), "source": "local",
}
for g in local
@@ -193,7 +197,7 @@ async def group_members(
async def join_group(
group_id: str,
request: Request,
- current_user: User = Depends(get_current_user),
+ current_user: User = Depends(require_user_scope),
db: AsyncSession = Depends(get_db),
):
group = await db.get(Group, group_id)
@@ -219,26 +223,23 @@ class GroupCreateRequest(BaseModel):
name: str
visibility: str = "private" # public|private
join_policy: str = "invite" # open|request|invite
-
-
-class GEKBundleRequest(BaseModel):
- pk_eph_b64: str
- nonce_b64: str
- wrapped_b64: str
+ description: str | None = None
@router.post("", status_code=201)
async def create_group(
body: GroupCreateRequest,
request: Request,
- current_user: User = Depends(get_current_user),
+ current_user: User = Depends(require_user_scope),
db: AsyncSession = Depends(get_db),
):
+ desc = (body.description or "")[:512] if body.description else None
group = Group(
name=body.name,
admin_id=current_user.id,
visibility=body.visibility,
join_policy=body.join_policy,
+ description=desc,
)
db.add(group)
await db.flush() # get group.id
@@ -251,12 +252,11 @@ async def create_group(
return {"group_id": group.id, "name": group.name}
-@router.post("/{group_id}/members/{username}/gek", status_code=201)
-async def store_gek_bundle(
+@router.post("/{group_id}/members/{username}", status_code=201)
+async def add_group_member(
group_id: str,
username: str,
- body: GEKBundleRequest,
- current_user: User = Depends(get_current_user),
+ current_user: User = Depends(require_user_scope),
db: AsyncSession = Depends(get_db),
):
group = await db.get(Group, group_id)
@@ -270,26 +270,11 @@ async def store_gek_bundle(
if not target:
raise HTTPException(status_code=404, detail="User not found")
- # Upsert GEK bundle
- existing = await db.get(GEKBundle, (group_id, target.id))
new_member = False
- if existing:
- existing.pk_eph_b64 = body.pk_eph_b64
- existing.nonce_b64 = body.nonce_b64
- existing.wrapped_b64 = body.wrapped_b64
- else:
- db.add(GEKBundle(
- group_id=group_id,
- user_id=target.id,
- pk_eph_b64=body.pk_eph_b64,
- nonce_b64=body.nonce_b64,
- wrapped_b64=body.wrapped_b64,
- ))
- # Add member if not already in group
- mem = await db.get(GroupMember, (group_id, target.id))
- if not mem:
- db.add(GroupMember(group_id=group_id, user_id=target.id))
- new_member = True
+ mem = await db.get(GroupMember, (group_id, target.id))
+ if not mem:
+ db.add(GroupMember(group_id=group_id, user_id=target.id))
+ new_member = True
if new_member:
from meshbay_hub.api.notifications import create_notification
@@ -303,26 +288,26 @@ async def store_gek_bundle(
return {"status": "stored", "group_id": group_id, "username": username}
-@router.get("/{group_id}/gek")
-async def get_my_gek_bundle(
+@router.delete("/{group_id}")
+async def delete_group(
group_id: str,
- current_user: User = Depends(get_current_user),
+ request: Request,
+ current_user: User = Depends(require_user_scope),
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.admin_id != current_user.id:
+ raise HTTPException(status_code=403, detail="Only the group creator can delete")
- bundle = await db.get(GEKBundle, (group_id, current_user.id))
- if not bundle:
- raise HTTPException(status_code=404, detail="No GEK bundle for this user in this group")
-
- return {
- "group_id": group_id,
- "pk_eph_b64": bundle.pk_eph_b64,
- "nonce_b64": bundle.nonce_b64,
- "wrapped_b64": bundle.wrapped_b64,
- }
+ from sqlalchemy import delete as sa_delete
+ await db.execute(sa_delete(GroupMember).where(GroupMember.group_id == group_id))
+ db.add(IPLog(user_id=current_user.id, event="group_delete",
+ ip_address=_ip(request), detail=group.name))
+ await db.delete(group)
+ await db.commit()
+ return {"status": "deleted", "group_id": group_id}
def _ip(request: Request) -> str: