feat: Add role-based API tokens for Claudia Docs

- Add api_tokens table with role-based access (researcher, developer, viewer)
- Add POST /auth/token/generate endpoint for creating tokens
- Add GET /auth/tokens endpoint for listing user's tokens
- Add DELETE /auth/tokens/{token_id} endpoint for revoking tokens
- Add agent_type field to documents (research, development, general)
- Implement role-based access control for documents:
  - researcher: access to research and general documents
  - developer: access to development and general documents
  - viewer: read-only access
- Update document model and schemas with agent_type field
- Add comprehensive tests for API token functionality
- All existing tests pass (73 total)
This commit is contained in:
Motoko
2026-03-31 01:46:51 +00:00
parent 5beac2d673
commit 204badb964
10 changed files with 770 additions and 97 deletions

View File

@@ -231,3 +231,86 @@ async def cleanup_expired_blocklist(db: AsyncSession) -> None:
text("DELETE FROM jwt_blocklist WHERE expires_at < datetime('now')")
)
await db.flush()
# =============================================================================
# API Token Management (Role-based)
# =============================================================================
def generate_api_token() -> str:
"""Generate a random API token."""
return secrets.token_urlsafe(48) # ~64 chars
async def create_api_token(
db: AsyncSession,
name: str,
role: str,
agent_id: str,
) -> tuple[str, "ApiToken"]:
"""Create a new API token. Returns (raw_token, token_record)."""
from app.models.api_token import ApiToken
raw_token = generate_api_token()
token_hash = hash_token(raw_token)
token_record = ApiToken(
id=str(uuid.uuid4()),
name=name,
token_hash=token_hash,
role=role,
agent_id=agent_id,
)
db.add(token_record)
await db.flush()
return raw_token, token_record
async def list_api_tokens(db: AsyncSession, agent_id: str) -> list["ApiToken"]:
"""List all API tokens for an agent (without the actual token)."""
from app.models.api_token import ApiToken
result = await db.execute(
select(ApiToken).where(ApiToken.agent_id == agent_id).order_by(ApiToken.created_at.desc())
)
return list(result.scalars().all())
async def revoke_api_token(db: AsyncSession, token_id: str, agent_id: str) -> bool:
"""Revoke an API token. Returns True if found and deleted."""
from app.models.api_token import ApiToken
result = await db.execute(
select(ApiToken).where(
ApiToken.id == token_id,
ApiToken.agent_id == agent_id,
)
)
token = result.scalar_one_or_none()
if not token:
return False
await db.delete(token)
await db.flush()
return True
async def verify_api_token(db: AsyncSession, raw_token: str) -> tuple[str, str] | None:
"""
Verify an API token. Returns (agent_id, role) if valid, None otherwise.
Also updates last_used_at.
"""
from app.models.api_token import ApiToken
token_hash = hash_token(raw_token)
result = await db.execute(
select(ApiToken).where(ApiToken.token_hash == token_hash)
)
token: ApiToken | None = result.scalar_one_or_none()
if not token:
return None
# Update last_used_at
token.last_used_at = datetime.utcnow()
await db.flush()
return token.agent_id, token.role