- 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
374 lines
11 KiB
Python
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
|
|
|