262 lines
8.6 KiB
Python
262 lines
8.6 KiB
Python
"""Admin router: user management, system settings, data cleanup, job control.
|
|
|
|
All endpoints require admin role.
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.dependencies import get_db, require_admin
|
|
from app.models.user import User
|
|
from app.schemas.admin import (
|
|
CreateUserRequest,
|
|
DataCleanupRequest,
|
|
JobToggle,
|
|
RecommendationConfigUpdate,
|
|
PasswordReset,
|
|
RegistrationToggle,
|
|
SystemSettingUpdate,
|
|
TickerUniverseUpdate,
|
|
UserManagement,
|
|
)
|
|
from app.schemas.common import APIEnvelope
|
|
from app.services import admin_service
|
|
from app.services import ticker_universe_service
|
|
|
|
router = APIRouter(tags=["admin"])
|
|
|
|
|
|
def _user_dict(user: User) -> dict:
|
|
return {
|
|
"id": user.id,
|
|
"username": user.username,
|
|
"role": user.role,
|
|
"has_access": user.has_access,
|
|
"created_at": user.created_at.isoformat() if user.created_at else None,
|
|
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# User management
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/admin/users", response_model=APIEnvelope)
|
|
async def list_users(
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List all user accounts."""
|
|
users = await admin_service.list_users(db)
|
|
return APIEnvelope(status="success", data=[_user_dict(u) for u in users])
|
|
|
|
|
|
@router.post("/admin/users", response_model=APIEnvelope, status_code=201)
|
|
async def create_user(
|
|
body: CreateUserRequest,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Create a new user account."""
|
|
user = await admin_service.create_user(
|
|
db, body.username, body.password, body.role, body.has_access
|
|
)
|
|
return APIEnvelope(status="success", data=_user_dict(user))
|
|
|
|
|
|
@router.put("/admin/users/{user_id}/access", response_model=APIEnvelope)
|
|
async def set_user_access(
|
|
user_id: int,
|
|
body: UserManagement,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Grant or revoke API access for a user."""
|
|
user = await admin_service.set_user_access(db, user_id, body.has_access)
|
|
return APIEnvelope(status="success", data=_user_dict(user))
|
|
|
|
|
|
@router.put("/admin/users/{user_id}/password", response_model=APIEnvelope)
|
|
async def reset_password(
|
|
user_id: int,
|
|
body: PasswordReset,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Reset a user's password."""
|
|
user = await admin_service.reset_password(db, user_id, body.new_password)
|
|
return APIEnvelope(status="success", data=_user_dict(user))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registration toggle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.put("/admin/settings/registration", response_model=APIEnvelope)
|
|
async def toggle_registration(
|
|
body: RegistrationToggle,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Enable or disable user registration."""
|
|
setting = await admin_service.toggle_registration(db, body.enabled)
|
|
return APIEnvelope(
|
|
status="success",
|
|
data={"key": setting.key, "value": setting.value},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# System settings
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/admin/settings", response_model=APIEnvelope)
|
|
async def list_settings(
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List all system settings."""
|
|
settings_list = await admin_service.list_settings(db)
|
|
return APIEnvelope(
|
|
status="success",
|
|
data=[
|
|
{"key": s.key, "value": s.value, "updated_at": s.updated_at.isoformat() if s.updated_at else None}
|
|
for s in settings_list
|
|
],
|
|
)
|
|
|
|
|
|
@router.get("/admin/settings/recommendations", response_model=APIEnvelope)
|
|
async def get_recommendation_settings(
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
config = await admin_service.get_recommendation_config(db)
|
|
return APIEnvelope(status="success", data=config)
|
|
|
|
|
|
@router.put("/admin/settings/recommendations", response_model=APIEnvelope)
|
|
async def update_recommendation_settings(
|
|
body: RecommendationConfigUpdate,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
updated = await admin_service.update_recommendation_config(
|
|
db,
|
|
body.model_dump(exclude_unset=True),
|
|
)
|
|
return APIEnvelope(status="success", data=updated)
|
|
|
|
|
|
@router.get("/admin/settings/ticker-universe", response_model=APIEnvelope)
|
|
async def get_ticker_universe_setting(
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
data = await admin_service.get_ticker_universe_default(db)
|
|
return APIEnvelope(status="success", data=data)
|
|
|
|
|
|
@router.put("/admin/settings/ticker-universe", response_model=APIEnvelope)
|
|
async def update_ticker_universe_setting(
|
|
body: TickerUniverseUpdate,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
data = await admin_service.update_ticker_universe_default(db, body.universe)
|
|
return APIEnvelope(status="success", data=data)
|
|
|
|
|
|
@router.put("/admin/settings/{key}", response_model=APIEnvelope)
|
|
async def update_setting(
|
|
key: str,
|
|
body: SystemSettingUpdate,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Create or update a system setting."""
|
|
setting = await admin_service.update_setting(db, key, body.value)
|
|
return APIEnvelope(
|
|
status="success",
|
|
data={"key": setting.key, "value": setting.value, "updated_at": setting.updated_at.isoformat() if setting.updated_at else None},
|
|
)
|
|
|
|
|
|
@router.post("/admin/tickers/bootstrap", response_model=APIEnvelope)
|
|
async def bootstrap_tickers(
|
|
universe: str = Query("sp500", pattern="^(sp500|nasdaq100|nasdaq_all)$"),
|
|
prune_missing: bool = Query(False),
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await ticker_universe_service.bootstrap_universe(
|
|
db,
|
|
universe,
|
|
prune_missing=prune_missing,
|
|
)
|
|
return APIEnvelope(status="success", data=result)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Data cleanup
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.post("/admin/data/cleanup", response_model=APIEnvelope)
|
|
async def cleanup_data(
|
|
body: DataCleanupRequest,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Delete OHLCV, sentiment, and fundamental data older than N days."""
|
|
counts = await admin_service.cleanup_data(db, body.older_than_days)
|
|
return APIEnvelope(status="success", data=counts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Job control
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/admin/jobs", response_model=APIEnvelope)
|
|
async def list_jobs(
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List all scheduled jobs with their current status."""
|
|
jobs = await admin_service.list_jobs(db)
|
|
return APIEnvelope(status="success", data=jobs)
|
|
|
|
|
|
@router.get("/admin/pipeline/readiness", response_model=APIEnvelope)
|
|
async def get_pipeline_readiness(
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
data = await admin_service.get_pipeline_readiness(db)
|
|
return APIEnvelope(status="success", data=data)
|
|
|
|
|
|
@router.post("/admin/jobs/{job_name}/trigger", response_model=APIEnvelope)
|
|
async def trigger_job(
|
|
job_name: str,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Trigger a manual job run (placeholder)."""
|
|
result = await admin_service.trigger_job(db, job_name)
|
|
return APIEnvelope(status="success", data=result)
|
|
|
|
|
|
@router.put("/admin/jobs/{job_name}/toggle", response_model=APIEnvelope)
|
|
async def toggle_job(
|
|
job_name: str,
|
|
body: JobToggle,
|
|
_admin: User = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Enable or disable a scheduled job (placeholder)."""
|
|
setting = await admin_service.toggle_job(db, job_name, body.enabled)
|
|
return APIEnvelope(
|
|
status="success",
|
|
data={"key": setting.key, "value": setting.value},
|
|
)
|