"""Admin API: user management.""" from datetime import datetime from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from pydantic import BaseModel from ...database import get_db from ...models.user import User, UserCatalogEntry, UserCustomCatalogEntry, UserProviderPreference from ...models.channel import Category from ...models.provider import ProviderAccount from ...models.custom_category import CustomCategory from ..auth import get_current_admin router = APIRouter(prefix="/users", tags=["users"]) class UserCreate(BaseModel): username: str password: str max_connections: int = 1 expiry_date: datetime | None = None is_active: bool = True is_priority: bool = False class UserUpdate(BaseModel): password: str | None = None max_connections: int | None = None expiry_date: datetime | None = None is_active: bool | None = None is_priority: bool | None = None class UserOut(BaseModel): id: int username: str max_connections: int expiry_date: datetime | None is_active: bool is_priority: bool created_at: datetime class Config: from_attributes = True @router.get("/", response_model=list[UserOut]) async def list_users(db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): result = await db.execute(select(User).order_by(User.id)) return result.scalars().all() @router.post("/", response_model=UserOut) async def create_user(data: UserCreate, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): existing = await db.execute(select(User).where(User.username == data.username)) if existing.scalar_one_or_none(): raise HTTPException(400, f"Username '{data.username}' already exists") user = User( username=data.username, max_connections=data.max_connections, expiry_date=data.expiry_date, is_active=data.is_active, is_priority=data.is_priority, password_hash="", ) user.set_password(data.password) db.add(user) await db.commit() await db.refresh(user) return user @router.put("/{user_id}", response_model=UserOut) async def update_user(user_id: int, data: UserUpdate, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(404, "User not found") if data.password is not None: user.set_password(data.password) if data.max_connections is not None: user.max_connections = data.max_connections if data.expiry_date is not None: user.expiry_date = data.expiry_date if data.is_active is not None: user.is_active = data.is_active if data.is_priority is not None: user.is_priority = data.is_priority await db.commit() await db.refresh(user) return user @router.delete("/{user_id}") async def delete_user(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(404, "User not found") await db.delete(user) await db.commit() return {"ok": True} @router.get("/{user_id}/catalog") async def get_user_catalog(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): result = await db.execute( select(UserCatalogEntry) .options(selectinload(UserCatalogEntry.category)) .where(UserCatalogEntry.user_id == user_id) ) entries = result.scalars().all() return [ {"id": e.id, "category_id": e.category_id, "category_name": e.category.name, "type": e.category.type} for e in entries ] @router.post("/{user_id}/catalog") async def assign_categories(user_id: int, category_ids: list[int], db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): result = await db.execute(select(User).where(User.id == user_id)) if not result.scalar_one_or_none(): raise HTTPException(404, "User not found") # Remove existing entries existing = await db.execute(select(UserCatalogEntry).where(UserCatalogEntry.user_id == user_id)) for entry in existing.scalars().all(): await db.delete(entry) # Add new entries added = 0 for cat_id in category_ids: cat_result = await db.execute(select(Category).where(Category.id == cat_id)) if cat_result.scalar_one_or_none(): db.add(UserCatalogEntry(user_id=user_id, category_id=cat_id)) added += 1 await db.commit() return {"ok": True, "assigned": added} @router.delete("/{user_id}/catalog/{category_id}") async def remove_category(user_id: int, category_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): result = await db.execute( select(UserCatalogEntry).where( UserCatalogEntry.user_id == user_id, UserCatalogEntry.category_id == category_id, ) ) entry = result.scalar_one_or_none() if not entry: raise HTTPException(404, "Catalog entry not found") await db.delete(entry) await db.commit() return {"ok": True} @router.get("/{user_id}/custom-catalog") async def get_user_custom_catalog(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): result = await db.execute( select(UserCustomCatalogEntry.custom_category_id).where(UserCustomCatalogEntry.user_id == user_id) ) return [row[0] for row in result.all()] @router.post("/{user_id}/custom-catalog") async def assign_custom_categories(user_id: int, custom_category_ids: list[int], db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): result = await db.execute(select(User).where(User.id == user_id)) if not result.scalar_one_or_none(): raise HTTPException(404, "User not found") existing = await db.execute(select(UserCustomCatalogEntry).where(UserCustomCatalogEntry.user_id == user_id)) for entry in existing.scalars().all(): await db.delete(entry) added = 0 for cc_id in custom_category_ids: cc_result = await db.execute(select(CustomCategory).where(CustomCategory.id == cc_id)) if cc_result.scalar_one_or_none(): db.add(UserCustomCatalogEntry(user_id=user_id, custom_category_id=cc_id)) added += 1 await db.commit() return {"ok": True, "assigned": added} # --------------------------------------------------------------------------- # Provider preferences # --------------------------------------------------------------------------- class ProviderPrefIn(BaseModel): provider_account_id: int priority: int = 0 @router.get("/{user_id}/providers") async def get_user_provider_prefs(user_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin)): result = await db.execute( select(UserProviderPreference) .options(selectinload(UserProviderPreference.provider_account)) .where(UserProviderPreference.user_id == user_id) .order_by(UserProviderPreference.priority) ) prefs = result.scalars().all() return [ { "id": p.id, "provider_account_id": p.provider_account_id, "provider_name": p.provider_account.name, "priority": p.priority, } for p in prefs ] @router.post("/{user_id}/providers") async def add_user_provider_pref( user_id: int, data: ProviderPrefIn, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin) ): user_result = await db.execute(select(User).where(User.id == user_id)) if not user_result.scalar_one_or_none(): raise HTTPException(404, "User not found") provider_result = await db.execute(select(ProviderAccount).where(ProviderAccount.id == data.provider_account_id)) if not provider_result.scalar_one_or_none(): raise HTTPException(404, "Provider account not found") # Upsert existing = await db.execute( select(UserProviderPreference).where( UserProviderPreference.user_id == user_id, UserProviderPreference.provider_account_id == data.provider_account_id, ) ) pref = existing.scalar_one_or_none() if pref: pref.priority = data.priority else: pref = UserProviderPreference( user_id=user_id, provider_account_id=data.provider_account_id, priority=data.priority, ) db.add(pref) await db.commit() return {"ok": True} @router.delete("/{user_id}/providers/{provider_account_id}") async def remove_user_provider_pref( user_id: int, provider_account_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_admin) ): result = await db.execute( select(UserProviderPreference).where( UserProviderPreference.user_id == user_id, UserProviderPreference.provider_account_id == provider_account_id, ) ) pref = result.scalar_one_or_none() if not pref: raise HTTPException(404, "Preference not found") await db.delete(pref) await db.commit() return {"ok": True}