Files
microdao-daarion/services/auth-service/actor_context.py
Apple 6bd769ef40 feat(city-map): Add 2D City Map with coordinates and agent presence
- Add migration 013_city_map_coordinates.sql with map coordinates, zones, and agents table
- Add /city/map API endpoint in city-service
- Add /city/agents and /city/agents/online endpoints
- Extend presence aggregator to include agents[] in snapshot
- Add AgentsSource for fetching agent data from DB
- Create CityMap component with interactive room tiles
- Add useCityMap hook for fetching map data
- Update useGlobalPresence to include agents
- Add map/list view toggle on /city page
- Add agent badges to room cards and map tiles
2025-11-27 07:00:47 -08:00

131 lines
3.8 KiB
Python

"""
Actor Context Builder
Extracts ActorIdentity from request (session token or API key)
"""
import asyncpg
from fastapi import Header, HTTPException, Cookie
from typing import Optional
from models import ActorIdentity, ActorType
import json
async def build_actor_context(
db_pool: asyncpg.Pool,
authorization: Optional[str] = Header(None),
session_token: Optional[str] = Cookie(None),
x_api_key: Optional[str] = Header(None, alias="X-API-Key")
) -> ActorIdentity:
"""
Build ActorIdentity from request
Priority:
1. X-API-Key header
2. Authorization header (Bearer token)
3. session_token cookie
Raises HTTPException(401) if no valid credentials
"""
# Try API Key first
if x_api_key:
actor = await get_actor_from_api_key(db_pool, x_api_key)
if actor:
return actor
# Try Authorization header
if authorization and authorization.startswith("Bearer "):
token = authorization.replace("Bearer ", "")
actor = await get_actor_from_session(db_pool, token)
if actor:
return actor
# Try session cookie
if session_token:
actor = await get_actor_from_session(db_pool, session_token)
if actor:
return actor
raise HTTPException(
status_code=401,
detail="Unauthorized: No valid session token or API key"
)
async def get_actor_from_session(db_pool: asyncpg.Pool, token: str) -> Optional[ActorIdentity]:
"""Get ActorIdentity from session token"""
async with db_pool.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT actor_id, actor_data, expires_at
FROM sessions
WHERE token = $1 AND is_valid = true
""",
token
)
if not row:
return None
# Check expiration
from datetime import datetime, timezone
if row['expires_at'] < datetime.now(timezone.utc):
# Expired
await conn.execute("UPDATE sessions SET is_valid = false WHERE token = $1", token)
return None
# Parse actor data
actor_data = row['actor_data']
return ActorIdentity(**actor_data)
async def get_actor_from_api_key(db_pool: asyncpg.Pool, key: str) -> Optional[ActorIdentity]:
"""Get ActorIdentity from API key"""
async with db_pool.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT actor_id, actor_data, expires_at, last_used
FROM api_keys
WHERE key = $1 AND is_active = true
""",
key
)
if not row:
return None
# Check expiration
from datetime import datetime, timezone
if row['expires_at'] and row['expires_at'] < datetime.now(timezone.utc):
# Expired
await conn.execute("UPDATE api_keys SET is_active = false WHERE key = $1", key)
return None
# Update last_used
await conn.execute(
"UPDATE api_keys SET last_used = NOW() WHERE key = $1",
key
)
# Parse actor data
actor_data = row['actor_data']
return ActorIdentity(**actor_data)
async def require_actor(
db_pool: asyncpg.Pool,
authorization: Optional[str] = Header(None),
session_token: Optional[str] = Cookie(None),
x_api_key: Optional[str] = Header(None, alias="X-API-Key")
) -> ActorIdentity:
"""
Dependency for routes that require authentication
Usage:
@app.get("/protected")
async def protected_route(actor: ActorIdentity = Depends(require_actor)):
...
"""
return await build_actor_context(db_pool, authorization, session_token, x_api_key)