feat: update backend auth and dependencies to use Supabase

This commit is contained in:
Alan Alonso
2026-05-23 01:04:48 -06:00
parent 6d1845c09d
commit 47f4a7d2b1
3 changed files with 119 additions and 68 deletions

View File

@@ -1,77 +1,120 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException, Depends, status from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr
import jwt import jwt
from passlib.context import CryptContext from passlib.context import CryptContext
from app.core.config import settings from app.core.config import settings
from app.db.database import get_connection from app.db.database import get_db
router = APIRouter() router = APIRouter()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class UserRegister(BaseModel): class UserRegister(BaseModel):
email: EmailStr email: EmailStr
phone: str | None = None phone: str | None = None
password: str password: str
class UserLogin(BaseModel): class UserLogin(BaseModel):
email: EmailStr email: EmailStr
password: str password: str
class TokenResponse(BaseModel): class TokenResponse(BaseModel):
access_token: str access_token: str
token_type: str = "bearer" token_type: str = "bearer"
user_id: str
def hash_password(password: str) -> str: def hash_password(password: str) -> str:
return pwd_context.hash(password) return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool: def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed) return pwd_context.verify(plain, hashed)
def create_token(user_id: int) -> str:
def create_token(user_id: str) -> str:
expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
payload = {"sub": str(user_id), "exp": expire} payload = {"sub": user_id, "exp": expire}
return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm) return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
@router.post("/register", response_model=TokenResponse) @router.post("/register", response_model=TokenResponse)
async def register(user: UserRegister): async def register(user: UserRegister):
conn = get_connection() db = get_db()
existing = conn.execute(
"SELECT id FROM users WHERE email = ?", (user.email,) try:
).fetchone() # Verificar si email existe
if existing: existing = db.table("users").select("id").eq("email", user.email).execute()
raise HTTPException(status_code=400, detail="Email already registered") if existing.data:
raise HTTPException(status_code=400, detail="Email already registered")
password_hash = hash_password(user.password)
cursor = conn.execute( # Crear usuario
"INSERT INTO users (email, phone, password_hash) VALUES (?, ?, ?) RETURNING id", password_hash = hash_password(user.password)
(user.email, user.phone, password_hash) user_data = {
) "email": user.email,
user_id = cursor.fetchone()[0] "phone": user.phone,
"password_hash": password_hash,
# Create default preferences }
conn.execute( new_user = db.table("users").insert(user_data).execute()
"INSERT INTO notification_preferences (user_id) VALUES (?)", user_id = new_user.data[0]["id"]
(user_id,)
) # Crear preferencias por defecto
conn.commit() db.table("notification_preferences").insert({
conn.close() "user_id": user_id,
"notify_proximity": True,
token = create_token(user_id) "notify_breakdown": True,
return TokenResponse(access_token=token) "notify_delay": True,
"notify_route_start": True,
}).execute()
token = create_token(user_id)
return TokenResponse(access_token=token, user_id=user_id)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/login", response_model=TokenResponse) @router.post("/login", response_model=TokenResponse)
async def login(user: UserLogin): async def login(user: UserLogin):
conn = get_connection() db = get_db()
db_user = conn.execute(
"SELECT id, password_hash FROM users WHERE email = ?", try:
(user.email,) # Buscar usuario por email
).fetchone() result = db.table("users").select("id, password_hash").eq("email", user.email).execute()
conn.close()
if not result.data:
if not db_user or not verify_password(user.password, db_user[1]): raise HTTPException(status_code=401, detail="Invalid credentials")
raise HTTPException(status_code=401, detail="Invalid credentials")
db_user = result.data[0]
token = create_token(db_user[0])
return TokenResponse(access_token=token) # Verificar password
if not verify_password(user.password, db_user["password_hash"]):
raise HTTPException(status_code=401, detail="Invalid credentials")
user_id = db_user["id"]
token = create_token(user_id)
return TokenResponse(access_token=token, user_id=user_id)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/verify")
async def verify_token(token: str):
"""Verifica si el JWT es válido."""
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="Invalid token")
return {"valid": True, "user_id": user_id}
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")

View File

@@ -7,19 +7,19 @@ from typing import Optional
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Depends from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Depends
from pydantic import BaseModel from pydantic import BaseModel
from app.data.repositories.ruta_repository import SQLiteRutaRepository from app.data.repositories.ruta_repository import SupabaseRutaRepository
from app.services.simulador import obtener_simulador from app.services.simulador import obtener_simulador
from app.services.ws_manager import ws_manager from app.services.ws_manager import ws_manager
from app.core.cache import cached, cache_client, invalidate_route_cache from app.core.cache import cached, cache_client, invalidate_route_cache
from app.core.dependencies import get_current_user from app.core.dependencies import get_current_user
from app.db.database import get_connection from app.db.database import get_db
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _repo() -> SQLiteRutaRepository: def _repo() -> SupabaseRutaRepository:
return SQLiteRutaRepository() return SupabaseRutaRepository()
# ── GET /eta/{address_id} con caché ───────────────────────────────────────── # ── GET /eta/{address_id} con caché ─────────────────────────────────────────
@@ -35,14 +35,15 @@ async def get_eta(
Cacheado por 30 segundos para evitar consultas repetidas. Cacheado por 30 segundos para evitar consultas repetidas.
""" """
# Verificar que el domicilio pertenece al usuario (RBAC) # Verificar que el domicilio pertenece al usuario (RBAC)
conn = get_connection() db = get_db()
addr = conn.execute( try:
"SELECT user_id FROM addresses WHERE id = ?", (address_id,) result = db.table("addresses").select("user_id").eq("id", address_id).execute()
).fetchone() if not result.data or result.data[0]["user_id"] != current_user["id"]:
conn.close() raise HTTPException(status_code=403, detail="No autorizado")
except HTTPException:
if not addr or addr["user_id"] != current_user["id"]: raise
raise HTTPException(status_code=403, detail="No autorizado") except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
resultado = _repo().calcular_eta(address_id) resultado = _repo().calcular_eta(address_id)
if not resultado: if not resultado:

View File

@@ -3,34 +3,41 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt import jwt
from app.core.config import settings from app.core.config import settings
from app.db.database import get_connection from app.db.database import get_db
security = HTTPBearer() security = HTTPBearer()
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)): def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Middleware para validar JWT y retornar usuario actual."""
token = credentials.credentials token = credentials.credentials
try: try:
payload = jwt.decode( payload = jwt.decode(
token, token,
settings.secret_key, settings.secret_key,
algorithms=[settings.algorithm] algorithms=[settings.algorithm]
) )
user_id = payload.get("sub") user_id = payload.get("sub")
if user_id is None: if user_id is None:
raise HTTPException(status_code=401, detail="Invalid token") raise HTTPException(status_code=401, detail="Invalid token")
conn = get_connection() # Obtener usuario de Supabase
user = conn.execute( db = get_db()
"SELECT id, email, phone FROM users WHERE id = ?", try:
(user_id,) result = db.table("users").select("id, email, phone").eq("id", user_id).execute()
).fetchone() if not result.data:
conn.close() raise HTTPException(status_code=401, detail="User not found")
if user is None: user = result.data[0]
raise HTTPException(status_code=401, detail="User not found") return {
"id": user["id"],
return dict(user) "email": user["email"],
"phone": user["phone"]
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"DB error: {str(e)}")
except jwt.ExpiredSignatureError: except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired") raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError: except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token") raise HTTPException(status_code=401, detail="Invalid token")