summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/api/deps.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/deps.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/deps.py40
1 files changed, 30 insertions, 10 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/deps.py b/packages/meshbay-hub/src/meshbay_hub/api/deps.py
index addba30..1bf57a4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/deps.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/deps.py
@@ -1,5 +1,10 @@
"""
FastAPI shared dependencies — injected via Depends().
+
+JWT scope enforcement:
+ - "user" scope (browser login): full access to all endpoints
+ - "node" scope (Ed25519 daemon auth): read-only group access + node operations
+ Node-scoped tokens CANNOT create/delete groups or manage membership.
"""
from fastapi import Depends, Header, HTTPException, status
@@ -18,20 +23,13 @@ def set_admin_usernames(usernames: list[str]) -> None:
_admin_usernames = set(usernames)
-async def get_current_user(
- authorization: str = Header(...),
- db: AsyncSession = Depends(get_db),
-) -> User:
- """
- Verify the JWT bearer token and return the User from the database.
- Node clients: verified locally with hub PK — no DB round-trip needed.
- Hub API (web): must confirm user still exists and is active.
- """
+async def _decode_token(authorization: str = Header(...)) -> dict:
+ """Decode and verify JWT bearer token. Returns full payload."""
try:
scheme, token = authorization.split(None, 1)
if scheme.lower() != "bearer":
raise ValueError
- payload = decode_access_token(token)
+ return decode_access_token(token)
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -39,6 +37,15 @@ async def get_current_user(
headers={"WWW-Authenticate": "Bearer"},
)
+
+async def get_current_user(
+ payload: dict = Depends(_decode_token),
+ db: AsyncSession = Depends(get_db),
+) -> User:
+ """
+ Verify the JWT bearer token and return the User from the database.
+ Accepts both user-scoped and node-scoped tokens.
+ """
result = await db.execute(
select(User).where(User.id == payload["sub"]))
user = result.scalar_one_or_none()
@@ -52,6 +59,19 @@ async def get_current_user(
return user
+async def require_user_scope(
+ payload: dict = Depends(_decode_token),
+ current_user: User = Depends(get_current_user),
+) -> User:
+ """Reject node-scoped tokens — only browser (user-scope) can mutate groups."""
+ if payload.get("scope") == "node":
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Node-scoped token cannot perform this operation — use browser",
+ )
+ return current_user
+
+
async def require_moderator(
current_user: User = Depends(get_current_user),
) -> User: