Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com>
Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com> Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com> modificacion de vistas panel admin, login, animaciones y implementacion de mascota
This commit is contained in:
377
backend/app/api/admin.py
Normal file
377
backend/app/api/admin.py
Normal file
@@ -0,0 +1,377 @@
|
||||
"""
|
||||
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
|
||||
@@ -74,10 +74,10 @@ def register(body: RegisterRequest):
|
||||
if not auth_user:
|
||||
raise HTTPException(status_code=400, detail="No se pudo crear el usuario en Supabase Auth")
|
||||
|
||||
# Crear entrada en public.users con el rol elegido
|
||||
# Crear entrada en public.users con el rol y nombre elegidos
|
||||
try:
|
||||
supabase_admin.table("users").upsert(
|
||||
{"id": str(auth_user.id), "role": body.role}
|
||||
{"id": str(auth_user.id), "role": body.role, "name": body.name}
|
||||
).execute()
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error al guardar el usuario: {e}")
|
||||
|
||||
Reference in New Issue
Block a user