feat: Add Auth Service with JWT authentication

This commit is contained in:
Apple
2025-11-26 11:47:00 -08:00
parent 2c4eb7d432
commit 5aaf6cbf21
23 changed files with 3522 additions and 26 deletions

View File

@@ -0,0 +1,127 @@
"""
API Key management routes
"""
from fastapi import APIRouter, HTTPException, Depends
import asyncpg
from datetime import datetime, timedelta, timezone
import secrets
from models import ApiKeyCreateRequest, ApiKey, ApiKeyResponse, ActorIdentity
from actor_context import require_actor
import json
router = APIRouter(prefix="/auth/api-keys", tags=["api_keys"])
def get_db_pool(request) -> asyncpg.Pool:
"""Get database pool from app state"""
return request.app.state.db_pool
@router.post("", response_model=ApiKey)
async def create_api_key(
request: ApiKeyCreateRequest,
actor: ActorIdentity = Depends(require_actor),
db_pool: asyncpg.Pool = Depends(get_db_pool)
):
"""
Create new API key for current actor
Returns full key only once (on creation)
"""
# Generate key
key = f"dk_{secrets.token_urlsafe(32)}"
key_id = secrets.token_urlsafe(16)
# Calculate expiration
expires_at = None
if request.expires_days:
expires_at = datetime.now(timezone.utc) + timedelta(days=request.expires_days)
# Store in database
async with db_pool.acquire() as conn:
await conn.execute(
"""
INSERT INTO api_keys (id, key, actor_id, actor_data, description, expires_at)
VALUES ($1, $2, $3, $4, $5, $6)
""",
key_id,
key,
actor.actor_id,
json.dumps(actor.model_dump()),
request.description,
expires_at
)
return ApiKey(
id=key_id,
key=key, # Full key shown only once
actor_id=actor.actor_id,
actor=actor,
description=request.description,
created_at=datetime.now(timezone.utc),
expires_at=expires_at,
last_used=None,
is_active=True
)
@router.get("", response_model=list[ApiKeyResponse])
async def list_api_keys(
actor: ActorIdentity = Depends(require_actor),
db_pool: asyncpg.Pool = Depends(get_db_pool)
):
"""List all API keys for current actor"""
async with db_pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT id, key, description, created_at, expires_at, last_used, is_active
FROM api_keys
WHERE actor_id = $1
ORDER BY created_at DESC
""",
actor.actor_id
)
keys = []
for row in rows:
# Show only preview of key
key_preview = row['key'][:8] + "..." if len(row['key']) > 8 else row['key']
keys.append(ApiKeyResponse(
id=row['id'],
key_preview=key_preview,
description=row['description'],
created_at=row['created_at'],
expires_at=row['expires_at'],
last_used=row['last_used'],
is_active=row['is_active']
))
return keys
@router.delete("/{key_id}")
async def delete_api_key(
key_id: str,
actor: ActorIdentity = Depends(require_actor),
db_pool: asyncpg.Pool = Depends(get_db_pool)
):
"""Delete (deactivate) API key"""
async with db_pool.acquire() as conn:
result = await conn.execute(
"""
UPDATE api_keys
SET is_active = false
WHERE id = $1 AND actor_id = $2
""",
key_id,
actor.actor_id
)
if result == "UPDATE 0":
raise HTTPException(404, "API key not found")
return {"status": "deleted", "key_id": key_id}