from fastapi import APIRouter, Depends, Query, UploadFile, File from typing import Optional, List from sqlalchemy.ext.asyncio import AsyncSession from auth.models import TokenPayload from auth.dependencies import require_permission from users.models import ( UserCreate, UserUpdate, UserInDB, UserListResponse, SetPasswordRequest, ResetPasswordRequest, ) from users import service from database.postgres import get_pg_session from shared.audit import log_action router = APIRouter(prefix="/api/users", tags=["users"]) @router.get("", response_model=UserListResponse) async def list_users( search: Optional[str] = Query(None), status: Optional[str] = Query(None), _user: TokenPayload = Depends(require_permission("app_users", "view")), ): users = service.list_users(search=search, status=status) return UserListResponse(users=users, total=len(users)) @router.get("/{user_id}", response_model=UserInDB) async def get_user( user_id: str, _user: TokenPayload = Depends(require_permission("app_users", "view")), ): return service.get_user(user_id) @router.post("", response_model=UserInDB, status_code=201) async def create_user( body: UserCreate, _user: TokenPayload = Depends(require_permission("app_users", "add")), db: AsyncSession = Depends(get_pg_session), ): app_user = service.create_user(body) await log_action(db, _user.sub, _user.name or _user.email, "CREATE", "app_user", app_user.id, app_user.display_name or app_user.email or app_user.id) return app_user @router.put("/{user_id}", response_model=UserInDB) async def update_user( user_id: str, body: UserUpdate, _user: TokenPayload = Depends(require_permission("app_users", "edit")), db: AsyncSession = Depends(get_pg_session), ): old = service.get_user(user_id) app_user = service.update_user(user_id, body) _SKIP = {"updated_at", "id", "photo_url"} changes = { k: {"old": getattr(old, k, None), "new": getattr(app_user, k, None)} for k in body.model_fields_set if k not in _SKIP and getattr(old, k, None) != getattr(app_user, k, None) } await log_action(db, _user.sub, _user.name or _user.email, "UPDATE", "app_user", user_id, app_user.display_name or app_user.email or user_id, changes=changes or None) return app_user @router.delete("/{user_id}", status_code=204) async def delete_user( user_id: str, _user: TokenPayload = Depends(require_permission("app_users", "delete")), db: AsyncSession = Depends(get_pg_session), ): service.delete_user(user_id) await log_action(db, _user.sub, _user.name or _user.email, "DELETE", "app_user", user_id, user_id) @router.post("/{user_id}/block", response_model=UserInDB) async def block_user( user_id: str, _user: TokenPayload = Depends(require_permission("app_users", "edit")), db: AsyncSession = Depends(get_pg_session), ): app_user = service.block_user(user_id) await log_action(db, _user.sub, _user.name or _user.email, "STATUS_CHANGE", "app_user", user_id, app_user.display_name or app_user.email or user_id, meta={"status": "blocked"}) return app_user @router.post("/{user_id}/unblock", response_model=UserInDB) async def unblock_user( user_id: str, _user: TokenPayload = Depends(require_permission("app_users", "edit")), db: AsyncSession = Depends(get_pg_session), ): app_user = service.unblock_user(user_id) await log_action(db, _user.sub, _user.name or _user.email, "STATUS_CHANGE", "app_user", user_id, app_user.display_name or app_user.email or user_id, meta={"status": "unblocked"}) return app_user @router.get("/{user_id}/devices", response_model=List[dict]) async def get_user_devices( user_id: str, _user: TokenPayload = Depends(require_permission("app_users", "view")), ): return service.get_user_devices(user_id) @router.post("/{user_id}/devices/{device_id}", response_model=UserInDB) async def assign_device( user_id: str, device_id: str, _user: TokenPayload = Depends(require_permission("app_users", "edit")), ): return service.assign_device(user_id, device_id) @router.delete("/{user_id}/devices/{device_id}", response_model=UserInDB) async def unassign_device( user_id: str, device_id: str, _user: TokenPayload = Depends(require_permission("app_users", "edit")), ): return service.unassign_device(user_id, device_id) @router.post("/{user_id}/set-password", status_code=204) async def set_password( user_id: str, body: SetPasswordRequest, _user: TokenPayload = Depends(require_permission("app_users", "full_edit")), ): """Set a new password for the user via Firebase Auth (requires uid on the user doc).""" service.set_password(user_id, body.password) @router.post("/{user_id}/reset-password", status_code=204) async def reset_password( user_id: str, body: ResetPasswordRequest, _user: TokenPayload = Depends(require_permission("app_users", "full_edit")), ): """Reset a user's password to the supplied value (default: Bell1234!).""" service.set_password(user_id, body.new_password) @router.post("/{user_id}/photo") async def upload_photo( user_id: str, file: UploadFile = File(...), _user: TokenPayload = Depends(require_permission("app_users", "edit")), ): contents = await file.read() content_type = file.content_type or "image/jpeg" url = service.upload_photo(user_id, contents, file.filename, content_type) return {"photo_url": url}