diff --git a/server/app/api/routes/auth_router.py b/server/app/api/routes/auth_router.py index 5147acd..911cdb9 100644 --- a/server/app/api/routes/auth_router.py +++ b/server/app/api/routes/auth_router.py @@ -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") - - 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] - - # Create default preferences - conn.execute( - "INSERT INTO notification_preferences (user_id) VALUES (?)", - (user_id,) - ) - conn.commit() - conn.close() - - token = create_token(user_id) - return TokenResponse(access_token=token) + db = get_db() + + 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") + + # 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)) + @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() - - if not db_user or not verify_password(user.password, db_user[1]): - raise HTTPException(status_code=401, detail="Invalid credentials") - - token = create_token(db_user[0]) - return TokenResponse(access_token=token) \ No newline at end of file + db = get_db() + + try: + # Buscar usuario por email + result = db.table("users").select("id, password_hash").eq("email", user.email).execute() + + 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") diff --git a/server/app/api/routes/eta_router.py b/server/app/api/routes/eta_router.py index f0d4fb3..f988932 100644 --- a/server/app/api/routes/eta_router.py +++ b/server/app/api/routes/eta_router.py @@ -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: diff --git a/server/app/core/dependencies.py b/server/app/core/dependencies.py index b3bf09d..d535e77 100644 --- a/server/app/core/dependencies.py +++ b/server/app/core/dependencies.py @@ -3,34 +3,41 @@ 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( - token, - settings.secret_key, + token, + settings.secret_key, algorithms=[settings.algorithm] ) user_id = payload.get("sub") 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() - - if user is None: - raise HTTPException(status_code=401, detail="User not found") - - return dict(user) + + # 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") + + 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)}") + except jwt.ExpiredSignatureError: raise HTTPException(status_code=401, detail="Token expired") except jwt.InvalidTokenError: - raise HTTPException(status_code=401, detail="Invalid token") \ No newline at end of file + raise HTTPException(status_code=401, detail="Invalid token")