Files
microdao-daarion/services/dao-service/routes_dao.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

374 lines
11 KiB
Python

"""
DAO Routes — CRUD for DAO, Members, Treasury
Phase 8: DAO Dashboard
"""
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Optional
from datetime import datetime
from models import (
DaoCreate, DaoUpdate, DaoRead, DaoOverview,
DaoMember, MemberAdd,
DaoTreasuryItem, TreasuryUpdate
)
from auth_client import get_actor_from_token
from pdp_client import require_permission
router = APIRouter(prefix="/dao", tags=["dao"])
# Repositories (will be injected via dependency)
dao_repo = None
nats_publisher = None
# ============================================================================
# DAO — CRUD
# ============================================================================
@router.get("", response_model=List[DaoRead])
async def list_daos(actor: dict = Depends(get_actor_from_token)):
"""List DAOs where actor is a member"""
user_id = actor.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid actor identity")
return await dao_repo.list_daos_for_user(user_id)
@router.post("", response_model=DaoRead, status_code=201)
async def create_dao(
data: DaoCreate,
actor: dict = Depends(get_actor_from_token)
):
"""Create new DAO"""
user_id = actor.get("user_id")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid actor identity")
# Check PDP
await require_permission(
action="DAO_CREATE",
resource={"type": "DAO"},
context={"operation": "create"},
actor=actor
)
# Check if slug already exists
existing = await dao_repo.get_dao_by_slug(data.slug)
if existing:
raise HTTPException(status_code=409, detail=f"DAO with slug '{data.slug}' already exists")
# Create DAO
dao = await dao_repo.create_dao(data, user_id)
# Publish NATS event
if nats_publisher:
await nats_publisher.publish(
"dao.event.created",
{
"dao_id": dao.id,
"slug": dao.slug,
"name": dao.name,
"microdao_id": dao.microdao_id,
"owner_user_id": user_id,
"actor_id": user_id,
"ts": dao.created_at.isoformat()
}
)
return dao
@router.get("/{slug}", response_model=DaoOverview)
async def get_dao(
slug: str,
actor: dict = Depends(get_actor_from_token)
):
"""Get DAO overview by slug"""
dao_overview = await dao_repo.get_dao_overview_by_slug(slug)
if not dao_overview:
raise HTTPException(status_code=404, detail=f"DAO '{slug}' not found")
# Check PDP
await require_permission(
action="DAO_READ",
resource={"type": "DAO", "id": dao_overview.dao.id},
context={},
actor=actor
)
return dao_overview
@router.put("/{slug}", response_model=DaoRead)
async def update_dao(
slug: str,
data: DaoUpdate,
actor: dict = Depends(get_actor_from_token)
):
"""Update DAO"""
dao = await dao_repo.get_dao_by_slug(slug)
if not dao:
raise HTTPException(status_code=404, detail=f"DAO '{slug}' not found")
# Check PDP
await require_permission(
action="DAO_MANAGE",
resource={"type": "DAO", "id": dao.id},
context={"operation": "update"},
actor=actor
)
# Update
updated = await dao_repo.update_dao(dao.id, data)
if not updated:
raise HTTPException(status_code=500, detail="Failed to update DAO")
# Publish NATS event
if nats_publisher:
await nats_publisher.publish(
"dao.event.updated",
{
"dao_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
@router.delete("/{slug}", status_code=204)
async def delete_dao(
slug: str,
actor: dict = Depends(get_actor_from_token)
):
"""Soft delete DAO"""
dao = await dao_repo.get_dao_by_slug(slug)
if not dao:
raise HTTPException(status_code=404, detail=f"DAO '{slug}' not found")
# Check PDP
await require_permission(
action="DAO_MANAGE",
resource={"type": "DAO", "id": dao.id},
context={"operation": "delete"},
actor=actor
)
# Delete
success = await dao_repo.delete_dao(dao.id)
if not success:
raise HTTPException(status_code=500, detail="Failed to delete DAO")
# Publish NATS event
if nats_publisher:
await nats_publisher.publish(
"dao.event.deleted",
{
"dao_id": dao.id,
"slug": dao.slug,
"actor_id": actor.get("user_id"),
"ts": datetime.now().isoformat()
}
)
return None
# ============================================================================
# Members
# ============================================================================
@router.get("/{slug}/members", response_model=List[DaoMember])
async def list_members(
slug: str,
actor: dict = Depends(get_actor_from_token)
):
"""List DAO members"""
dao = await dao_repo.get_dao_by_slug(slug)
if not dao:
raise HTTPException(status_code=404, detail=f"DAO '{slug}' not found")
# Check PDP
await require_permission(
action="DAO_READ",
resource={"type": "DAO", "id": dao.id},
context={},
actor=actor
)
return await dao_repo.list_members(dao.id)
@router.post("/{slug}/members", response_model=DaoMember, status_code=201)
async def add_member(
slug: str,
data: MemberAdd,
actor: dict = Depends(get_actor_from_token)
):
"""Add member to DAO"""
dao = await dao_repo.get_dao_by_slug(slug)
if not dao:
raise HTTPException(status_code=404, detail=f"DAO '{slug}' not found")
# Check PDP
await require_permission(
action="DAO_MANAGE_MEMBERS",
resource={"type": "DAO", "id": dao.id},
context={"operation": "add_member"},
actor=actor
)
# Add member
member = await dao_repo.add_member(dao.id, data.user_id, data.role)
# Publish NATS event
if nats_publisher:
await nats_publisher.publish(
"dao.event.member_added",
{
"dao_id": dao.id,
"slug": dao.slug,
"member_id": member.id,
"user_id": data.user_id,
"role": data.role,
"actor_id": actor.get("user_id"),
"ts": datetime.now().isoformat()
}
)
return member
@router.delete("/{slug}/members/{member_id}", status_code=204)
async def remove_member(
slug: str,
member_id: str,
actor: dict = Depends(get_actor_from_token)
):
"""Remove member from DAO"""
dao = await dao_repo.get_dao_by_slug(slug)
if not dao:
raise HTTPException(status_code=404, detail=f"DAO '{slug}' not found")
# Check PDP
await require_permission(
action="DAO_MANAGE_MEMBERS",
resource={"type": "DAO", "id": dao.id},
context={"operation": "remove_member"},
actor=actor
)
# Get member info
members = await dao_repo.list_members(dao.id)
member = next((m for m in members if m.id == member_id), None)
if not member:
raise HTTPException(status_code=404, detail=f"Member '{member_id}' not found")
# Prevent removing last owner
if member.role == "owner":
owners = [m for m in members if m.role == "owner"]
if len(owners) <= 1:
raise HTTPException(
status_code=400,
detail="Cannot remove the last owner"
)
# Remove
success = await dao_repo.remove_member(member_id)
if not success:
raise HTTPException(status_code=500, detail="Failed to remove member")
# Publish NATS event
if nats_publisher:
await nats_publisher.publish(
"dao.event.member_removed",
{
"dao_id": dao.id,
"slug": dao.slug,
"member_id": member_id,
"user_id": member.user_id,
"role": member.role,
"actor_id": actor.get("user_id"),
"ts": datetime.now().isoformat()
}
)
return None
# ============================================================================
# Treasury
# ============================================================================
@router.get("/{slug}/treasury", response_model=List[DaoTreasuryItem])
async def get_treasury(
slug: str,
actor: dict = Depends(get_actor_from_token)
):
"""Get treasury balances"""
dao = await dao_repo.get_dao_by_slug(slug)
if not dao:
raise HTTPException(status_code=404, detail=f"DAO '{slug}' not found")
# Check PDP
await require_permission(
action="DAO_READ_TREASURY",
resource={"type": "DAO", "id": dao.id},
context={},
actor=actor
)
return await dao_repo.get_treasury_items(dao.id)
@router.post("/{slug}/treasury", response_model=DaoTreasuryItem)
async def update_treasury(
slug: str,
data: TreasuryUpdate,
actor: dict = Depends(get_actor_from_token)
):
"""Apply delta to treasury balance"""
dao = await dao_repo.get_dao_by_slug(slug)
if not dao:
raise HTTPException(status_code=404, detail=f"DAO '{slug}' not found")
# Check PDP
await require_permission(
action="DAO_MANAGE_TREASURY",
resource={"type": "DAO", "id": dao.id},
context={"operation": "update_balance"},
actor=actor
)
# Apply delta
try:
item = await dao_repo.apply_treasury_delta(dao.id, data.token_symbol, data.delta)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# Publish NATS event
if nats_publisher:
await nats_publisher.publish(
"dao.event.treasury_updated",
{
"dao_id": dao.id,
"slug": dao.slug,
"token_symbol": data.token_symbol,
"delta": str(data.delta),
"new_balance": str(item.balance),
"actor_id": actor.get("user_id"),
"ts": datetime.now().isoformat()
}
)
return item
# ============================================================================
# Helpers
# ============================================================================
def set_dao_repository(repo):
"""Set DAO repository (called from main.py)"""
global dao_repo
dao_repo = repo
def set_nats_publisher(publisher):
"""Set NATS publisher (called from main.py)"""
global nats_publisher
nats_publisher = publisher