Files
microdao-daarion/services/microdao-service/routes_microdao.py
Apple 3de3c8cb36 feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
2025-11-27 00:19:40 -08:00

301 lines
9.3 KiB
Python

"""
MicroDAO CRUD Routes
Phase 7: Backend Completion
"""
from fastapi import APIRouter, HTTPException, Header
from typing import List, Optional
import httpx
import os
from models import MicrodaoCreate, MicrodaoUpdate, MicrodaoRead
from repository_microdao import MicrodaoRepository
router = APIRouter(prefix="/microdao", tags=["microdao"])
# Dependency injection (will be set in main.py)
repo: Optional[MicrodaoRepository] = None
# Service URLs
AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "http://localhost:7011")
PDP_SERVICE_URL = os.getenv("PDP_SERVICE_URL", "http://localhost:7012")
# NATS publisher (will be set in main.py)
nats_publisher = None
# ============================================================================
# Auth & PDP Helpers
# ============================================================================
async def get_actor_from_token(authorization: Optional[str] = Header(None)):
"""
Get ActorIdentity from auth-service
Returns actor dict or raises 401
"""
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing or invalid authorization header")
token = authorization.replace("Bearer ", "")
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{AUTH_SERVICE_URL}/auth/me",
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
return response.json()
except httpx.HTTPError:
raise HTTPException(status_code=401, detail="Invalid or expired token")
async def check_pdp_permission(
action: str,
resource: dict,
context: dict,
actor: dict
) -> bool:
"""
Check permission via pdp-service
Returns True if allowed, False otherwise
"""
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{PDP_SERVICE_URL}/internal/pdp/evaluate",
json={
"action": action,
"resource": resource,
"context": context,
"actor": actor
}
)
response.raise_for_status()
result = response.json()
return result.get("decision") == "ALLOW"
except httpx.HTTPError as e:
print(f"⚠️ PDP error: {e}")
return False # Fail closed
# ============================================================================
# CRUD — List
# ============================================================================
@router.get("", response_model=List[MicrodaoRead])
async def list_microdaos(authorization: Optional[str] = Header(None)):
"""
List microDAOs where current user is a member
Requires: valid auth token
"""
actor = await get_actor_from_token(authorization)
user_id = actor.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid actor identity")
return await repo.list_microdaos_for_user(user_id)
# ============================================================================
# CRUD — Create
# ============================================================================
@router.post("", response_model=MicrodaoRead, status_code=201)
async def create_microdao(
data: MicrodaoCreate,
authorization: Optional[str] = Header(None)
):
"""
Create new microDAO
Requires: MICRODAO_CREATE permission
"""
actor = await get_actor_from_token(authorization)
user_id = actor.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid actor identity")
# Check PDP
allowed = await check_pdp_permission(
action="MICRODAO_CREATE",
resource={"type": "MICRODAO"},
context={"operation": "create"},
actor=actor
)
if not allowed:
raise HTTPException(status_code=403, detail="Permission denied: cannot create microDAO")
# Check if slug already exists
existing = await repo.get_microdao_by_slug(data.slug)
if existing:
raise HTTPException(status_code=409, detail=f"MicroDAO with slug '{data.slug}' already exists")
# Create microDAO
microdao = await repo.create_microdao(data, user_id)
# Publish NATS event
if nats_publisher:
await nats_publisher(
"microdao.event.created",
{
"microdao_id": microdao.id,
"slug": microdao.slug,
"name": microdao.name,
"owner_user_id": user_id,
"actor_id": user_id,
"ts": microdao.created_at.isoformat()
}
)
return microdao
# ============================================================================
# CRUD — Read (by slug)
# ============================================================================
@router.get("/{slug}", response_model=MicrodaoRead)
async def get_microdao(
slug: str,
authorization: Optional[str] = Header(None)
):
"""
Get microDAO by slug
Requires: MICRODAO_READ permission
"""
actor = await get_actor_from_token(authorization)
# Get microDAO
microdao = await repo.get_microdao_by_slug(slug)
if not microdao:
raise HTTPException(status_code=404, detail=f"MicroDAO '{slug}' not found")
# Check PDP
allowed = await check_pdp_permission(
action="MICRODAO_READ",
resource={"type": "MICRODAO", "id": microdao.id},
context={},
actor=actor
)
if not allowed:
raise HTTPException(status_code=403, detail="Permission denied: cannot read this microDAO")
return microdao
# ============================================================================
# CRUD — Update
# ============================================================================
@router.put("/{slug}", response_model=MicrodaoRead)
async def update_microdao(
slug: str,
data: MicrodaoUpdate,
authorization: Optional[str] = Header(None)
):
"""
Update microDAO
Requires: MICRODAO_MANAGE permission
"""
actor = await get_actor_from_token(authorization)
# Get microDAO
microdao = await repo.get_microdao_by_slug(slug)
if not microdao:
raise HTTPException(status_code=404, detail=f"MicroDAO '{slug}' not found")
# Check PDP
allowed = await check_pdp_permission(
action="MICRODAO_MANAGE",
resource={"type": "MICRODAO", "id": microdao.id},
context={"operation": "update"},
actor=actor
)
if not allowed:
raise HTTPException(status_code=403, detail="Permission denied: cannot manage this microDAO")
# Update
updated = await repo.update_microdao(microdao.id, data)
if not updated:
raise HTTPException(status_code=500, detail="Failed to update microDAO")
# Publish NATS event
if nats_publisher:
await nats_publisher(
"microdao.event.updated",
{
"microdao_id": updated.id,
"slug": updated.slug,
"actor_id": actor.get("user_id"),
"changes": data.model_dump(exclude_unset=True),
"ts": updated.updated_at.isoformat()
}
)
return updated
# ============================================================================
# CRUD — Delete (Soft)
# ============================================================================
@router.delete("/{slug}", status_code=204)
async def delete_microdao(
slug: str,
authorization: Optional[str] = Header(None)
):
"""
Soft delete microDAO (set is_active = false)
Requires: MICRODAO_MANAGE permission
"""
actor = await get_actor_from_token(authorization)
# Get microDAO
microdao = await repo.get_microdao_by_slug(slug)
if not microdao:
raise HTTPException(status_code=404, detail=f"MicroDAO '{slug}' not found")
# Check PDP
allowed = await check_pdp_permission(
action="MICRODAO_MANAGE",
resource={"type": "MICRODAO", "id": microdao.id},
context={"operation": "delete"},
actor=actor
)
if not allowed:
raise HTTPException(status_code=403, detail="Permission denied: cannot delete this microDAO")
# Delete
success = await repo.delete_microdao(microdao.id)
if not success:
raise HTTPException(status_code=500, detail="Failed to delete microDAO")
# Publish NATS event
if nats_publisher:
await nats_publisher(
"microdao.event.deleted",
{
"microdao_id": microdao.id,
"slug": microdao.slug,
"actor_id": actor.get("user_id"),
"ts": datetime.now().isoformat()
}
)
return None
# ============================================================================
# Helper: Set repository
# ============================================================================
def set_repository(repository: MicrodaoRepository):
"""Set repository instance (called from main.py)"""
global repo
repo = repository
def set_nats_publisher(publisher_func):
"""Set NATS publisher function (called from main.py)"""
global nats_publisher
nats_publisher = publisher_func