""" Endpoints de administración — Solo accesibles para usuarios con role='admin'. Operan directamente contra Supabase (RLS bypaseado por service_role). """ from typing import Optional from fastapi import APIRouter, Depends, HTTPException from app.core.deps import require_role from app.core.supabase_client import supabase_admin from app.schemas.admin import ( AdminUser, AdminUserCreate, AdminUserUpdate, AdminRoute, AdminRouteCreate, AdminRouteUpdate, AdminUnit, AdminUnitCreate, AdminUnitUpdate, AdminDriver, AdminDriverCreate, AdminDriverUpdate, ) router = APIRouter( prefix="/admin", tags=["admin"], dependencies=[Depends(require_role("admin"))], ) # ── Helpers ─────────────────────────────────────────────────────────────────── def _auth_user_map() -> dict[str, dict]: """Devuelve {user_id: {email, phone}} desde Supabase Auth (paginado).""" mapping: dict[str, dict] = {} try: page = 1 while True: resp = supabase_admin.auth.admin.list_users(page=page, per_page=200) users = getattr(resp, "users", None) or ( resp if isinstance(resp, list) else [] ) if not users: break for u in users: mapping[str(u.id)] = { "email": getattr(u, "email", None), "phone": getattr(u, "phone", None), } if len(users) < 200: break page += 1 except Exception as e: print(f"[admin] list_users falló: {e}") return mapping def _auth_user(user_id: str) -> dict: try: resp = supabase_admin.auth.admin.get_user_by_id(user_id) u = getattr(resp, "user", None) or resp return { "email": getattr(u, "email", None), "phone": getattr(u, "phone", None), } except Exception: return {"email": None, "phone": None} # ── Users ───────────────────────────────────────────────────────────────────── @router.get("/users", response_model=list[AdminUser]) def list_users(): res = supabase_admin.table("users").select("id, name, role").execute() rows = res.data or [] auth_map = _auth_user_map() return [ AdminUser( id=str(r["id"]), name=r.get("name"), role=r.get("role", "citizen"), email=auth_map.get(str(r["id"]), {}).get("email"), phone=auth_map.get(str(r["id"]), {}).get("phone"), ) for r in rows ] @router.post("/users", response_model=AdminUser, status_code=201) def create_user(body: AdminUserCreate): if not body.email and not body.phone: raise HTTPException(400, "Se requiere email o teléfono") if len(body.password) < 6: raise HTTPException(400, "La contraseña debe tener al menos 6 caracteres") create_attrs: dict = {"password": body.password} if body.email: create_attrs["email"] = body.email create_attrs["email_confirm"] = True else: create_attrs["phone"] = body.phone create_attrs["phone_confirm"] = True try: resp = supabase_admin.auth.admin.create_user(create_attrs) except Exception as e: raise HTTPException(400, f"Error al crear el usuario en Supabase Auth: {e}") auth_user = getattr(resp, "user", None) if not auth_user: raise HTTPException(500, "Supabase Auth no devolvió un usuario válido") try: supabase_admin.table("users").upsert( {"id": str(auth_user.id), "name": body.name, "role": body.role} ).execute() except Exception as e: raise HTTPException(500, f"Error al guardar el usuario en public.users: {e}") return AdminUser( id=str(auth_user.id), name=body.name, email=body.email, phone=body.phone, role=body.role, ) @router.patch("/users/{user_id}", response_model=AdminUser) def update_user(user_id: str, body: AdminUserUpdate): public_payload: dict = {} if body.name is not None: public_payload["name"] = body.name if body.role is not None: public_payload["role"] = body.role if public_payload: try: supabase_admin.table("users").update(public_payload).eq( "id", user_id ).execute() except Exception as e: raise HTTPException(500, f"Error al actualizar public.users: {e}") if body.email is not None: try: supabase_admin.auth.admin.update_user_by_id( user_id, {"email": body.email} ) except Exception as e: raise HTTPException(500, f"Error al actualizar email: {e}") res = ( supabase_admin.table("users") .select("id, name, role") .eq("id", user_id) .maybe_single() .execute() ) if not res.data: raise HTTPException(404, "Usuario no encontrado") auth = _auth_user(user_id) return AdminUser( id=str(res.data["id"]), name=res.data.get("name"), role=res.data.get("role", "citizen"), email=auth.get("email"), phone=auth.get("phone"), ) @router.delete("/users/{user_id}", status_code=204) def delete_user(user_id: str): try: supabase_admin.auth.admin.delete_user(user_id) except Exception as e: raise HTTPException(500, f"Error al borrar el usuario: {e}") try: supabase_admin.table("users").delete().eq("id", user_id).execute() except Exception: pass return None # ── Routes ──────────────────────────────────────────────────────────────────── _ROUTE_COLS = "id, name, truck_id, turno, status, current_position_id" @router.get("/routes", response_model=list[AdminRoute]) def list_routes(): res = supabase_admin.table("routes").select(_ROUTE_COLS).execute() return [AdminRoute(**r) for r in (res.data or [])] @router.post("/routes", response_model=AdminRoute, status_code=201) def create_route(body: AdminRouteCreate): payload = body.model_dump(exclude_none=True) try: res = ( supabase_admin.table("routes") .insert(payload) .execute() ) except Exception as e: raise HTTPException(400, f"Error al crear la ruta: {e}") row = (res.data or [None])[0] if not row: raise HTTPException(500, "Supabase no devolvió la fila creada") return AdminRoute(**row) @router.patch("/routes/{route_id}", response_model=AdminRoute) def update_route(route_id: str, body: AdminRouteUpdate): payload = body.model_dump(exclude_none=True) if not payload: raise HTTPException(400, "Sin cambios") try: supabase_admin.table("routes").update(payload).eq("id", route_id).execute() except Exception as e: raise HTTPException(400, f"Error al actualizar la ruta: {e}") res = ( supabase_admin.table("routes") .select(_ROUTE_COLS) .eq("id", route_id) .maybe_single() .execute() ) if not res.data: raise HTTPException(404, "Ruta no encontrada") return AdminRoute(**res.data) @router.delete("/routes/{route_id}", status_code=204) def delete_route(route_id: str): try: supabase_admin.table("routes").delete().eq("id", route_id).execute() except Exception as e: raise HTTPException(400, f"Error al borrar la ruta: {e}") return None # ── Units ───────────────────────────────────────────────────────────────────── @router.get("/units", response_model=list[AdminUnit]) def list_units(): res = supabase_admin.table("units").select("id, plate, status").execute() return [AdminUnit(**r) for r in (res.data or [])] @router.post("/units", response_model=AdminUnit, status_code=201) def create_unit(body: AdminUnitCreate): try: res = ( supabase_admin.table("units") .insert(body.model_dump(exclude_none=True)) .execute() ) except Exception as e: raise HTTPException(400, f"Error al crear la unidad: {e}") row = (res.data or [None])[0] if not row: raise HTTPException(500, "Supabase no devolvió la fila creada") return AdminUnit(**row) @router.patch("/units/{unit_id}", response_model=AdminUnit) def update_unit(unit_id: int, body: AdminUnitUpdate): payload = body.model_dump(exclude_none=True) if not payload: raise HTTPException(400, "Sin cambios") try: supabase_admin.table("units").update(payload).eq("id", unit_id).execute() except Exception as e: raise HTTPException(400, f"Error al actualizar la unidad: {e}") res = ( supabase_admin.table("units") .select("id, plate, status") .eq("id", unit_id) .maybe_single() .execute() ) if not res.data: raise HTTPException(404, "Unidad no encontrada") return AdminUnit(**res.data) @router.delete("/units/{unit_id}", status_code=204) def delete_unit(unit_id: int): try: supabase_admin.table("units").delete().eq("id", unit_id).execute() except Exception as e: raise HTTPException(400, f"Error al borrar la unidad: {e}") return None # ── Drivers ─────────────────────────────────────────────────────────────────── def _hydrate_driver(row: dict, users_map: dict[str, dict], units_map: dict[int, dict]) -> AdminDriver: user_id = str(row.get("user_id")) unit_id: Optional[int] = row.get("unit_id") u = users_map.get(user_id, {}) unit = units_map.get(unit_id, {}) if unit_id is not None else {} return AdminDriver( id=str(row["id"]), user_id=user_id, user_name=u.get("name"), user_email=u.get("email"), unit_id=unit_id, plate=unit.get("plate"), ) def _drivers_context() -> tuple[dict[str, dict], dict[int, dict]]: users_res = supabase_admin.table("users").select("id, name").execute() auth_map = _auth_user_map() users_map: dict[str, dict] = { str(u["id"]): { "name": u.get("name"), "email": auth_map.get(str(u["id"]), {}).get("email"), } for u in (users_res.data or []) } units_res = supabase_admin.table("units").select("id, plate").execute() units_map: dict[int, dict] = {u["id"]: u for u in (units_res.data or [])} return users_map, units_map @router.get("/drivers", response_model=list[AdminDriver]) def list_drivers(): res = supabase_admin.table("drivers").select("id, user_id, unit_id").execute() users_map, units_map = _drivers_context() return [_hydrate_driver(r, users_map, units_map) for r in (res.data or [])] @router.post("/drivers", response_model=AdminDriver, status_code=201) def create_driver(body: AdminDriverCreate): try: res = ( supabase_admin.table("drivers") .insert(body.model_dump(exclude_none=True)) .execute() ) except Exception as e: raise HTTPException(400, f"Error al crear el conductor: {e}") row = (res.data or [None])[0] if not row: raise HTTPException(500, "Supabase no devolvió la fila creada") users_map, units_map = _drivers_context() return _hydrate_driver(row, users_map, units_map) @router.patch("/drivers/{driver_id}", response_model=AdminDriver) def update_driver(driver_id: str, body: AdminDriverUpdate): payload = body.model_dump(exclude_none=True) if not payload: raise HTTPException(400, "Sin cambios") try: supabase_admin.table("drivers").update(payload).eq("id", driver_id).execute() except Exception as e: raise HTTPException(400, f"Error al actualizar el conductor: {e}") res = ( supabase_admin.table("drivers") .select("id, user_id, unit_id") .eq("id", driver_id) .maybe_single() .execute() ) if not res.data: raise HTTPException(404, "Conductor no encontrado") users_map, units_map = _drivers_context() return _hydrate_driver(res.data, users_map, units_map) @router.delete("/drivers/{driver_id}", status_code=204) def delete_driver(driver_id: str): try: supabase_admin.table("drivers").delete().eq("id", driver_id).execute() except Exception as e: raise HTTPException(400, f"Error al borrar el conductor: {e}") return None