from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status, Request from sqlalchemy.orm import Session from slowapi import Limiter from slowapi.util import get_remote_address from api.auth import get_current_user from config import settings from core.security import get_password_hash from db.database import get_db from db.crud import ( get_user_by_id, get_user_by_username, get_user_by_email, list_users, create_user_by_admin, update_user, delete_user ) from schemas.user import User, UserCreateByAdmin, UserUpdate, UserListResponse, AliyunKeyUpdate, AliyunKeyVerifyResponse from schemas.audiobook import LLMConfigUpdate, LLMConfigResponse, NsfwSynopsisGenerationRequest, NsfwScriptGenerationRequest router = APIRouter(prefix="/users", tags=["users"]) limiter = Limiter(key_func=get_remote_address) async def require_superuser( current_user: Annotated[User, Depends(get_current_user)] ) -> User: if not current_user.is_superuser: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Superuser access required" ) return current_user @router.get("", response_model=UserListResponse) @limiter.limit("30/minute") async def get_users( request: Request, skip: int = 0, limit: int = 100, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): users, total = list_users(db, skip=skip, limit=limit) return UserListResponse(users=users, total=total, skip=skip, limit=limit) @router.post("", response_model=User, status_code=status.HTTP_201_CREATED) @limiter.limit("10/minute") async def create_user( request: Request, user_data: UserCreateByAdmin, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): existing_user = get_user_by_username(db, username=user_data.username) if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Username already registered" ) existing_email = get_user_by_email(db, email=user_data.email) if existing_email: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered" ) hashed_password = get_password_hash(user_data.password) user = create_user_by_admin( db, username=user_data.username, email=user_data.email, hashed_password=hashed_password, is_superuser=user_data.is_superuser, can_use_local_model=user_data.can_use_local_model ) return user @router.get("/me", response_model=User) @limiter.limit("30/minute") async def get_current_user_info( request: Request, current_user: User = Depends(get_current_user) ): return current_user @router.get("/{user_id}", response_model=User) @limiter.limit("30/minute") async def get_user( request: Request, user_id: int, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): user = get_user_by_id(db, user_id=user_id) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) return user @router.put("/{user_id}", response_model=User) @limiter.limit("10/minute") async def update_user_info( request: Request, user_id: int, user_data: UserUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_superuser) ): existing_user = get_user_by_id(db, user_id=user_id) if not existing_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) if user_data.username is not None: username_exists = get_user_by_username(db, username=user_data.username) if username_exists and username_exists.id != user_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Username already taken" ) if user_data.email is not None: email_exists = get_user_by_email(db, email=user_data.email) if email_exists and email_exists.id != user_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already taken" ) hashed_password = None if user_data.password is not None: hashed_password = get_password_hash(user_data.password) user = update_user( db, user_id=user_id, username=user_data.username, email=user_data.email, hashed_password=hashed_password, is_active=user_data.is_active, is_superuser=user_data.is_superuser, can_use_local_model=user_data.can_use_local_model, can_use_nsfw=user_data.can_use_nsfw, ) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) return user @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) @limiter.limit("10/minute") async def delete_user_by_id( request: Request, user_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_superuser) ): if user_id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot delete yourself" ) success = delete_user(db, user_id=user_id) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) @router.post("/system/aliyun-key") @limiter.limit("5/minute") async def set_system_aliyun_key( request: Request, key_data: AliyunKeyUpdate, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): from core.security import encrypt_api_key from core.tts_service import AliyunTTSBackend from db.crud import set_system_setting api_key = key_data.api_key.strip() aliyun_backend = AliyunTTSBackend(api_key=api_key, region=settings.ALIYUN_REGION) health = await aliyun_backend.health_check() if not health.get("available", False): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid Aliyun API key.") set_system_setting(db, "aliyun_api_key", encrypt_api_key(api_key)) return {"message": "Aliyun API key updated"} @router.delete("/system/aliyun-key") @limiter.limit("5/minute") async def delete_system_aliyun_key( request: Request, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): from db.crud import delete_system_setting delete_system_setting(db, "aliyun_api_key") return {"message": "Aliyun API key deleted"} @router.get("/system/aliyun-key/verify", response_model=AliyunKeyVerifyResponse) @limiter.limit("10/minute") async def verify_system_aliyun_key( request: Request, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): from core.security import decrypt_api_key from core.tts_service import AliyunTTSBackend from db.crud import get_system_setting encrypted = get_system_setting(db, "aliyun_api_key") if not encrypted: return AliyunKeyVerifyResponse(valid=False, message="No Aliyun API key configured") api_key = decrypt_api_key(encrypted) if not api_key: return AliyunKeyVerifyResponse(valid=False, message="Failed to decrypt API key") aliyun_backend = AliyunTTSBackend(api_key=api_key, region=settings.ALIYUN_REGION) health = await aliyun_backend.health_check() if health.get("available", False): return AliyunKeyVerifyResponse(valid=True, message="Aliyun API key is valid and working") return AliyunKeyVerifyResponse(valid=False, message="Aliyun API key is not working.") @router.put("/system/llm-config") @limiter.limit("10/minute") async def set_system_llm_config( request: Request, config: LLMConfigUpdate, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): from core.security import encrypt_api_key from core.llm_service import LLMService from db.crud import set_system_setting api_key = config.api_key.strip() base_url = config.base_url.strip() model = config.model.strip() llm = LLMService(base_url=base_url, api_key=api_key, model=model) try: await llm.chat("You are a test assistant.", "Reply with 'ok'.") except Exception as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"LLM API validation failed: {e}") set_system_setting(db, "llm_api_key", encrypt_api_key(api_key)) set_system_setting(db, "llm_base_url", base_url) set_system_setting(db, "llm_model", model) return {"message": "LLM config updated"} @router.get("/system/llm-config", response_model=LLMConfigResponse) @limiter.limit("30/minute") async def get_system_llm_config( request: Request, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): from db.crud import get_system_setting return LLMConfigResponse( base_url=get_system_setting(db, "llm_base_url"), model=get_system_setting(db, "llm_model"), has_key=bool(get_system_setting(db, "llm_api_key")), ) @router.delete("/system/llm-config") @limiter.limit("10/minute") async def delete_system_llm_config( request: Request, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): from db.crud import delete_system_setting delete_system_setting(db, "llm_api_key") delete_system_setting(db, "llm_base_url") delete_system_setting(db, "llm_model") return {"message": "LLM config deleted"} @router.put("/system/grok-config") @limiter.limit("10/minute") async def set_system_grok_config( request: Request, config: LLMConfigUpdate, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): from core.security import encrypt_api_key from core.llm_service import GrokLLMService from db.crud import set_system_setting api_key = config.api_key.strip() base_url = config.base_url.strip() model = config.model.strip() grok = GrokLLMService(base_url=base_url, api_key=api_key, model=model) try: await grok.chat("You are a test assistant.", "Reply with 'ok'.") except Exception as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Grok API validation failed: {e}") set_system_setting(db, "grok_api_key", encrypt_api_key(api_key)) set_system_setting(db, "grok_base_url", base_url) set_system_setting(db, "grok_model", model) return {"message": "Grok config updated"} @router.get("/system/grok-config", response_model=LLMConfigResponse) @limiter.limit("30/minute") async def get_system_grok_config( request: Request, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): from db.crud import get_system_setting return LLMConfigResponse( base_url=get_system_setting(db, "grok_base_url"), model=get_system_setting(db, "grok_model"), has_key=bool(get_system_setting(db, "grok_api_key")), ) @router.delete("/system/grok-config") @limiter.limit("10/minute") async def delete_system_grok_config( request: Request, db: Session = Depends(get_db), _: User = Depends(require_superuser) ): from db.crud import delete_system_setting delete_system_setting(db, "grok_api_key") delete_system_setting(db, "grok_base_url") delete_system_setting(db, "grok_model") return {"message": "Grok config deleted"}