Merge branch 'develop' into feature/frontend-v3
This commit is contained in:
@@ -5,6 +5,8 @@ __pycache__/
|
||||
|
||||
# Variables de entorno
|
||||
.env
|
||||
.env*
|
||||
|
||||
|
||||
# Configuraciones del editor
|
||||
.vscode/
|
||||
|
||||
+5
-2
@@ -14,12 +14,14 @@ from api.routers.auth import router as auth_router
|
||||
from api.routers.history import router as history_router
|
||||
from api.routers.test_mongo import router as test_mongo_router
|
||||
from api.routers.docit2mf_build import router as docit2mf_router
|
||||
from api.routers.google_auth import router as google_auth_router
|
||||
|
||||
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Aquí podrías hacer comprobaciones si quieres
|
||||
yield
|
||||
# No hace falta cerrar nada con Motor
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
@@ -41,3 +43,4 @@ app.include_router(test_mongo_router, prefix="/api")
|
||||
app.include_router(auth_router, prefix="/api")
|
||||
app.include_router(history_router, prefix="/api")
|
||||
app.include_router(docit2mf_router, prefix="/api")
|
||||
app.include_router(google_auth_router, prefix="/api")
|
||||
@@ -1,13 +1,19 @@
|
||||
# api/routers/docit2mf_build.py
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from api.models.docit2mf_models import DoCIT2MFMultiRequest
|
||||
from api.services.docit2mf_build_service import build_it2mf_from_level
|
||||
|
||||
router = APIRouter(prefix="/criteria", tags=["criteria"])
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.post("/doc-it2mf/build")
|
||||
@limiter.limit("10/minute")
|
||||
async def build_doc_it2mf(request: DoCIT2MFMultiRequest):
|
||||
results = []
|
||||
|
||||
@@ -15,6 +21,10 @@ async def build_doc_it2mf(request: DoCIT2MFMultiRequest):
|
||||
for level in request.levels:
|
||||
results.append(build_it2mf_from_level(level))
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
logger.warning(f"Validation error in doc-it2mf/build: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail="Invalid input data")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in doc-it2mf/build: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return {"levels": results}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# api/routers/google_auth.py
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from pydantic import BaseModel
|
||||
from bson import ObjectId
|
||||
import httpx
|
||||
import os
|
||||
import jwt
|
||||
|
||||
from api.database.mongodb import users_collection
|
||||
from api.utils.security import create_access_token
|
||||
|
||||
router = APIRouter(prefix="/auth/google", tags=["auth"])
|
||||
|
||||
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
|
||||
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
|
||||
REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI")
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 1. LOGIN → REDIRECCIÓN A GOOGLE
|
||||
# -----------------------------
|
||||
@router.get("/login")
|
||||
async def google_login():
|
||||
google_auth_url = (
|
||||
"https://accounts.google.com/o/oauth2/auth"
|
||||
"?response_type=code"
|
||||
f"&client_id={GOOGLE_CLIENT_ID}"
|
||||
f"&redirect_uri={REDIRECT_URI}"
|
||||
"&scope=openid%20email%20profile"
|
||||
"&access_type=offline"
|
||||
"&prompt=consent"
|
||||
)
|
||||
|
||||
return RedirectResponse(google_auth_url)
|
||||
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 2. CALLBACK → GOOGLE DEVUELVE EL CODE
|
||||
# -----------------------------
|
||||
@router.get("/callback")
|
||||
async def google_callback(request: Request):
|
||||
|
||||
code = request.query_params.get("code")
|
||||
if not code:
|
||||
raise HTTPException(status_code=400, detail="Missing code parameter")
|
||||
|
||||
token_url = "https://oauth2.googleapis.com/token"
|
||||
|
||||
data = {
|
||||
"code": code,
|
||||
"client_id": GOOGLE_CLIENT_ID,
|
||||
"client_secret": GOOGLE_CLIENT_SECRET,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
"grant_type": "authorization_code",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
token_response = await client.post(token_url, data=data)
|
||||
token_json = token_response.json()
|
||||
|
||||
if "access_token" not in token_json:
|
||||
raise HTTPException(status_code=400, detail=token_json)
|
||||
|
||||
access_token = token_json["access_token"]
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
userinfo = await client.get(
|
||||
"https://www.googleapis.com/oauth2/v2/userinfo",
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
|
||||
user_data = userinfo.json()
|
||||
|
||||
google_id = user_data["id"]
|
||||
email = user_data["email"]
|
||||
name = user_data.get("name", "Usuario")
|
||||
|
||||
user = await users_collection.find_one({"email": email})
|
||||
|
||||
if not user:
|
||||
new_user = {
|
||||
"username": name,
|
||||
"email": email,
|
||||
"password_hash": None,
|
||||
"google_id": google_id,
|
||||
"history": [],
|
||||
}
|
||||
result = await users_collection.insert_one(new_user)
|
||||
user_id = result.inserted_id
|
||||
else:
|
||||
user_id = user["_id"]
|
||||
|
||||
token = create_access_token({"user_id": str(user_id)})
|
||||
|
||||
return {"message": "Login con Google exitoso", "token": token}
|
||||
@@ -15,7 +15,6 @@ def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> Li
|
||||
result = []
|
||||
for item in values:
|
||||
if isinstance(item, int):
|
||||
# valor fijo → mismo para LMF y UMF
|
||||
result.append(item)
|
||||
else:
|
||||
lo, hi = item
|
||||
@@ -23,8 +22,45 @@ def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> Li
|
||||
return result
|
||||
|
||||
|
||||
def _sort_nodes(nodes):
|
||||
"""Ordena los nodos por su coordenada X."""
|
||||
return sorted(nodes, key=lambda p: p[0])
|
||||
|
||||
|
||||
def _enforce_upper_ge_lower(lower, upper):
|
||||
"""
|
||||
Garantiza que la UMF (upper) nunca quede por debajo de la LMF (lower).
|
||||
Ajusta los valores de pertenencia si es necesario.
|
||||
"""
|
||||
# left nodes
|
||||
for i in range(len(lower["left_nodes"])):
|
||||
lx, ly = lower["left_nodes"][i]
|
||||
ux, uy = upper["left_nodes"][i]
|
||||
upper["left_nodes"][i][1] = max(uy, ly)
|
||||
|
||||
# right nodes
|
||||
for i in range(len(lower["right_nodes"])):
|
||||
lx, ly = lower["right_nodes"][i]
|
||||
ux, uy = upper["right_nodes"][i]
|
||||
upper["right_nodes"][i][1] = max(uy, ly)
|
||||
|
||||
return upper
|
||||
|
||||
|
||||
def build_it2mf_from_level(level: DoCIT2MFRequest):
|
||||
"""
|
||||
Construye una función IT2MF a partir de un nivel con intervalos de cartas blancas.
|
||||
Devuelve:
|
||||
{
|
||||
"term": ...,
|
||||
"lower": {...},
|
||||
"upper": {...}
|
||||
}
|
||||
"""
|
||||
|
||||
# -------------------------
|
||||
# LMF (mínimos)
|
||||
# -------------------------
|
||||
left_min = _extract_bounds(level.left_blank_cards, "min")
|
||||
right_min = _extract_bounds(level.right_blank_cards, "min")
|
||||
|
||||
@@ -39,7 +75,13 @@ def build_it2mf_from_level(level: DoCIT2MFRequest):
|
||||
)
|
||||
lower = build_doc_mf_level(lower_level)
|
||||
|
||||
# Ordenar nodos LMF
|
||||
lower["left_nodes"] = _sort_nodes(lower["left_nodes"])
|
||||
lower["right_nodes"] = _sort_nodes(lower["right_nodes"])
|
||||
|
||||
# -------------------------
|
||||
# UMF (máximos)
|
||||
# -------------------------
|
||||
left_max = _extract_bounds(level.left_blank_cards, "max")
|
||||
right_max = _extract_bounds(level.right_blank_cards, "max")
|
||||
|
||||
@@ -54,6 +96,15 @@ def build_it2mf_from_level(level: DoCIT2MFRequest):
|
||||
)
|
||||
upper = build_doc_mf_level(upper_level)
|
||||
|
||||
# Ordenar nodos UMF
|
||||
upper["left_nodes"] = _sort_nodes(upper["left_nodes"])
|
||||
upper["right_nodes"] = _sort_nodes(upper["right_nodes"])
|
||||
|
||||
# -------------------------
|
||||
# FIX: evitar inversión vertical (UMF < LMF)
|
||||
# -------------------------
|
||||
upper = _enforce_upper_ge_lower(lower, upper)
|
||||
|
||||
return {
|
||||
"term": level.term,
|
||||
"lower": lower,
|
||||
|
||||
@@ -4,6 +4,10 @@ from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from api.database.mongodb import users_collection
|
||||
from bson import ObjectId
|
||||
import os
|
||||
import jwt
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
@@ -17,7 +21,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
|
||||
|
||||
def generate_token() -> str:
|
||||
return secrets.token_hex(32) # 64 caracteres seguros
|
||||
return secrets.token_hex(32)
|
||||
|
||||
|
||||
security_scheme = HTTPBearer()
|
||||
@@ -35,6 +39,8 @@ async def get_current_user(
|
||||
detail="Token inválido o usuario no autenticado",
|
||||
)
|
||||
|
||||
# devolvemos el documento tal cual (dict)
|
||||
user["id"] = str(user["_id"])
|
||||
return user
|
||||
return user
|
||||
|
||||
def create_access_token(data: dict):
|
||||
return jwt.encode(data, SECRET_KEY, algorithm="HS256")
|
||||
|
||||
@@ -5,3 +5,6 @@ pydantic
|
||||
passlib
|
||||
bcrypt==4.0.1
|
||||
email-validator
|
||||
slowapi
|
||||
httpx
|
||||
PyJWT
|
||||
|
||||
+2
-2
@@ -1,5 +1,3 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
@@ -11,6 +9,8 @@ services:
|
||||
- ./backend:/app
|
||||
depends_on:
|
||||
- db
|
||||
env_file:
|
||||
- backend\.env
|
||||
|
||||
frontend:
|
||||
build:
|
||||
|
||||
Reference in New Issue
Block a user