bLOQUE p1 BACKEND Y SEGURIDAD, AUTENTICACION CON SUPABASE. jwt. RBAC CRUD
This commit is contained in:
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
94
backend/app/api/addresses.py
Normal file
94
backend/app/api/addresses.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from app.schemas.addresses import AddressCreate, AddressResponse
|
||||
from app.core.deps import get_current_user, require_role
|
||||
from app.core.supabase_client import supabase_admin
|
||||
from app.services.simulation import get_colonias
|
||||
|
||||
router = APIRouter(prefix="/addresses", tags=["addresses"])
|
||||
|
||||
|
||||
def _resolve_route_id(colonia: str) -> str:
|
||||
"""Deriva routeId desde colonias-rutas.json; lanza 404 si la colonia no existe."""
|
||||
mapping = get_colonias()
|
||||
match = next(
|
||||
(c for c in mapping if c.get("colonia", "").lower() == colonia.lower()), None
|
||||
)
|
||||
if not match:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Colonia '{colonia}' no encontrada. Usa GET /colonias para ver las opciones.",
|
||||
)
|
||||
return match["routeId"]
|
||||
|
||||
|
||||
@router.post("", response_model=AddressResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_address(
|
||||
body: AddressCreate,
|
||||
current_user: dict = Depends(require_role("citizen", "admin")),
|
||||
):
|
||||
"""Alta de domicilio. El routeId se deriva automáticamente de la colonia elegida."""
|
||||
route_id = _resolve_route_id(body.colonia)
|
||||
|
||||
result = (
|
||||
supabase_admin.table("addresses")
|
||||
.insert(
|
||||
{
|
||||
"user_id": current_user["user_id"],
|
||||
"label": body.label,
|
||||
"calle": body.calle,
|
||||
"colonia": body.colonia,
|
||||
"route_id": route_id,
|
||||
"verified": False,
|
||||
}
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
if not result.data:
|
||||
raise HTTPException(status_code=500, detail="No se pudo guardar el domicilio")
|
||||
|
||||
return result.data[0]
|
||||
|
||||
|
||||
@router.get("", response_model=list[AddressResponse])
|
||||
def list_addresses(current_user: dict = Depends(get_current_user)):
|
||||
"""
|
||||
Lista de domicilios.
|
||||
- Ciudadano: solo sus propios (filtro por user_id en código + RLS en BD).
|
||||
- Admin: todos los domicilios.
|
||||
"""
|
||||
if current_user["role"] == "admin":
|
||||
result = supabase_admin.table("addresses").select("*").execute()
|
||||
else:
|
||||
result = (
|
||||
supabase_admin.table("addresses")
|
||||
.select("*")
|
||||
.eq("user_id", current_user["user_id"])
|
||||
.execute()
|
||||
)
|
||||
|
||||
return result.data or []
|
||||
|
||||
|
||||
@router.get("/{address_id}", response_model=AddressResponse)
|
||||
def get_address(
|
||||
address_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Detalle de un domicilio. El ciudadano solo puede ver los suyos."""
|
||||
result = (
|
||||
supabase_admin.table("addresses")
|
||||
.select("*")
|
||||
.eq("id", address_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
|
||||
if not result.data:
|
||||
raise HTTPException(status_code=404, detail="Domicilio no encontrado")
|
||||
|
||||
address = result.data
|
||||
if current_user["role"] != "admin" and address["user_id"] != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="No tienes acceso a este domicilio")
|
||||
|
||||
return address
|
||||
90
backend/app/api/auth.py
Normal file
90
backend/app/api/auth.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from app.schemas.auth import RegisterRequest, LoginRequest, TokenResponse
|
||||
from app.core.supabase_client import supabase, supabase_admin
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
def _fetch_role(user_id: str) -> str:
|
||||
result = (
|
||||
supabase_admin.table("users")
|
||||
.select("role")
|
||||
.eq("id", user_id)
|
||||
.maybe_single()
|
||||
.execute()
|
||||
)
|
||||
return result.data["role"] if result.data else "citizen"
|
||||
|
||||
|
||||
@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
|
||||
def register(body: RegisterRequest):
|
||||
"""
|
||||
Registro por email o teléfono.
|
||||
- Email: flujo estándar Supabase email+password.
|
||||
- Teléfono: requiere que Supabase tenga configurado un proveedor SMS (Twilio).
|
||||
"""
|
||||
if not body.email and not body.phone:
|
||||
raise HTTPException(status_code=400, detail="Se requiere email o teléfono")
|
||||
|
||||
try:
|
||||
if body.email:
|
||||
resp = supabase.auth.sign_up({"email": body.email, "password": body.password})
|
||||
else:
|
||||
resp = supabase.auth.sign_up({"phone": body.phone, "password": body.password})
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
auth_user = resp.user
|
||||
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
|
||||
supabase_admin.table("users").upsert(
|
||||
{
|
||||
"id": str(auth_user.id),
|
||||
"email": body.email,
|
||||
"phone": body.phone,
|
||||
"role": body.role,
|
||||
}
|
||||
).execute()
|
||||
|
||||
# Si no hubo sesión (email confirmation pendiente) devolvemos token vacío con aviso
|
||||
if not resp.session:
|
||||
raise HTTPException(
|
||||
status_code=202,
|
||||
detail="Cuenta creada. Confirma tu email antes de iniciar sesión.",
|
||||
)
|
||||
|
||||
return TokenResponse(
|
||||
access_token=resp.session.access_token,
|
||||
user_id=str(auth_user.id),
|
||||
role=body.role,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
def login(body: LoginRequest):
|
||||
"""Login por email o teléfono; devuelve JWT de Supabase."""
|
||||
if not body.email and not body.phone:
|
||||
raise HTTPException(status_code=400, detail="Se requiere email o teléfono")
|
||||
|
||||
try:
|
||||
if body.email:
|
||||
resp = supabase.auth.sign_in_with_password(
|
||||
{"email": body.email, "password": body.password}
|
||||
)
|
||||
else:
|
||||
resp = supabase.auth.sign_in_with_password(
|
||||
{"phone": body.phone, "password": body.password}
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=401, detail="Credenciales inválidas")
|
||||
|
||||
auth_user = resp.user
|
||||
role = _fetch_role(str(auth_user.id))
|
||||
|
||||
return TokenResponse(
|
||||
access_token=resp.session.access_token,
|
||||
user_id=str(auth_user.id),
|
||||
role=role,
|
||||
)
|
||||
11
backend/app/api/colonias.py
Normal file
11
backend/app/api/colonias.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.schemas.colonias import ColoniaResponse
|
||||
from app.services.simulation import get_colonias
|
||||
|
||||
router = APIRouter(tags=["colonias"])
|
||||
|
||||
|
||||
@router.get("/colonias", response_model=list[ColoniaResponse])
|
||||
def list_colonias():
|
||||
return get_colonias()
|
||||
@@ -5,11 +5,6 @@ from app.services import simulation
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/colonias")
|
||||
def list_colonias():
|
||||
return simulation.get_colonias()
|
||||
|
||||
|
||||
@router.get("/eta")
|
||||
def get_eta(colonia: Optional[str] = None, routeId: Optional[str] = None):
|
||||
# Resolver routeId a partir de colonia si es necesario
|
||||
|
||||
Reference in New Issue
Block a user