- 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
301 lines
9.3 KiB
Python
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
|
|
|