"""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}, )