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 fastapi import APIRouter, HTTPException, Depends, status
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, EmailStr
import jwt
from passlib.context import CryptContext
from app.core.config import settings
from app.db.database import get_connection
from app.db.database import get_db
router = APIRouter()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class UserRegister(BaseModel):
email: EmailStr
phone: str | None = None
password: str
class UserLogin(BaseModel):
email: EmailStr
password: str
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
user_id: str
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
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)
payload = {"sub": str(user_id), "exp": expire}
payload = {"sub": user_id, "exp": expire}
return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
@router.post("/register", response_model=TokenResponse)
async def register(user: UserRegister):
conn = get_connection()
existing = conn.execute(
"SELECT id FROM users WHERE email = ?", (user.email,)
).fetchone()
if existing:
raise HTTPException(status_code=400, detail="Email already registered")
db = get_db()
password_hash = hash_password(user.password)
cursor = conn.execute(
"INSERT INTO users (email, phone, password_hash) VALUES (?, ?, ?) RETURNING id",
(user.email, user.phone, password_hash)
)
user_id = cursor.fetchone()[0]
try:
# Verificar si email existe
existing = db.table("users").select("id").eq("email", user.email).execute()
if existing.data:
raise HTTPException(status_code=400, detail="Email already registered")
# Create default preferences
conn.execute(
"INSERT INTO notification_preferences (user_id) VALUES (?)",
(user_id,)
)
conn.commit()
conn.close()
# Crear usuario
password_hash = hash_password(user.password)
user_data = {
"email": user.email,
"phone": user.phone,
"password_hash": password_hash,
}
new_user = db.table("users").insert(user_data).execute()
user_id = new_user.data[0]["id"]
# Crear preferencias por defecto
db.table("notification_preferences").insert({
"user_id": user_id,
"notify_proximity": True,
"notify_breakdown": True,
"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))
token = create_token(user_id)
return TokenResponse(access_token=token)
@router.post("/login", response_model=TokenResponse)
async def login(user: UserLogin):
conn = get_connection()
db_user = conn.execute(
"SELECT id, password_hash FROM users WHERE email = ?",
(user.email,)
).fetchone()
conn.close()
db = get_db()
if not db_user or not verify_password(user.password, db_user[1]):
raise HTTPException(status_code=401, detail="Invalid credentials")
try:
# Buscar usuario por email
result = db.table("users").select("id, password_hash").eq("email", user.email).execute()
token = create_token(db_user[0])
return TokenResponse(access_token=token)
if not result.data:
raise HTTPException(status_code=401, detail="Invalid credentials")
db_user = result.data[0]
# 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 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.ws_manager import ws_manager
from app.core.cache import cached, cache_client, invalidate_route_cache
from app.core.dependencies import get_current_user
from app.db.database import get_connection
from app.db.database import get_db
router = APIRouter()
logger = logging.getLogger(__name__)
def _repo() -> SQLiteRutaRepository:
return SQLiteRutaRepository()
def _repo() -> SupabaseRutaRepository:
return SupabaseRutaRepository()
# ── GET /eta/{address_id} con caché ─────────────────────────────────────────
@@ -35,14 +35,15 @@ async def get_eta(
Cacheado por 30 segundos para evitar consultas repetidas.
"""
# Verificar que el domicilio pertenece al usuario (RBAC)
conn = get_connection()
addr = conn.execute(
"SELECT user_id FROM addresses WHERE id = ?", (address_id,)
).fetchone()
conn.close()
if not addr or addr["user_id"] != current_user["id"]:
raise HTTPException(status_code=403, detail="No autorizado")
db = get_db()
try:
result = db.table("addresses").select("user_id").eq("id", address_id).execute()
if not result.data or result.data[0]["user_id"] != current_user["id"]:
raise HTTPException(status_code=403, detail="No autorizado")
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
resultado = _repo().calcular_eta(address_id)
if not resultado:

View File

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