From ffab8ccce69c9d4bb96587ce6db91f6d76ff63be Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Tue, 7 Apr 2026 09:52:36 +0200 Subject: [PATCH 1/4] =?UTF-8?q?A=C3=B1adidas=20modificaciones=20de=20segur?= =?UTF-8?q?idad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/routers/docit2mf_build.py | 12 +++++++++++- backend/requirements.txt | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/api/routers/docit2mf_build.py b/backend/api/routers/docit2mf_build.py index 75660f4..ba4c06a 100644 --- a/backend/api/routers/docit2mf_build.py +++ b/backend/api/routers/docit2mf_build.py @@ -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} diff --git a/backend/requirements.txt b/backend/requirements.txt index 26ebe6e..bb4e897 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,3 +5,4 @@ pydantic passlib bcrypt==4.0.1 email-validator +slowapi From ed44d2f9fd0672a562b619c4cde638e1bb87effd Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Thu, 9 Apr 2026 11:39:00 +0200 Subject: [PATCH 2/4] =?UTF-8?q?A=C3=B1adido=20soporte=20para=20autenticaci?= =?UTF-8?q?=C3=B3n=20con=20Google=20y=20mejoras=20en=20la=20gesti=C3=B3n?= =?UTF-8?q?=20de=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + backend/api/main.py | 8 ++- backend/api/routers/google_auth.py | 101 +++++++++++++++++++++++++++++ backend/api/utils/security.py | 8 ++- backend/requirements.txt | 2 + 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 backend/api/routers/google_auth.py diff --git a/.gitignore b/.gitignore index e327dab..a8ebeb5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ __pycache__/ # Variables de entorno .env +.env* + # Configuraciones del editor .vscode/ diff --git a/backend/api/main.py b/backend/api/main.py index d4c740d..639fca8 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -14,15 +14,19 @@ 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) +app.include_router(google_auth_router) + app.add_middleware( CORSMiddleware, allow_origins=["*"], diff --git a/backend/api/routers/google_auth.py b/backend/api/routers/google_auth.py new file mode 100644 index 0000000..44dc4cf --- /dev/null +++ b/backend/api/routers/google_auth.py @@ -0,0 +1,101 @@ +# api/routers/google_auth.py + +from fastapi import APIRouter, HTTPException, Depends +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 = "http://localhost:8000/api/auth/google/callback" + + +# ----------------------------- +# 1. LOGIN → REDIRECCIÓN A GOOGLE +# ----------------------------- +@router.get("/login") +async def google_login(): + google_auth_url = ( + "https://accounts.google.com/o/oauth2/v2/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(code: str): + + # 1. Intercambiar code por access_token + 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="Error obteniendo token de Google") + + access_token = token_json["access_token"] + + # 2. Obtener datos del usuario desde Google + 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") + + # 3. Buscar usuario en MongoDB + user = await users_collection.find_one({"email": email}) + + if not user: + # Crear usuario nuevo + new_user = { + "username": name, + "email": email, + "password_hash": None, # No hay contraseña + "google_id": google_id, + "history": [], + } + + result = await users_collection.insert_one(new_user) + user_id = result.inserted_id + else: + user_id = user["_id"] + + # 4. Crear JWT de tu sistema + token = create_access_token({"user_id": str(user_id)}) + + # 5. Redirigir al frontend con el token + return {"message": "Login con Google exitoso", "token": token} diff --git a/backend/api/utils/security.py b/backend/api/utils/security.py index 5826361..eb3ad3c 100644 --- a/backend/api/utils/security.py +++ b/backend/api/utils/security.py @@ -17,7 +17,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 +35,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 \ No newline at end of file + return user + +def create_access_token(data: dict): + return jwt.encode(data, SECRET_KEY, algorithm="HS256") diff --git a/backend/requirements.txt b/backend/requirements.txt index bb4e897..6387273 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,3 +6,5 @@ passlib bcrypt==4.0.1 email-validator slowapi +httpx +PyJWT From f216ddea80a2fbe29e4d844cb4b1449d951721a8 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Thu, 9 Apr 2026 13:18:00 +0200 Subject: [PATCH 3/4] =?UTF-8?q?A=C3=B1adida=20funcionalidad=20para=20orden?= =?UTF-8?q?ar=20nodos=20y=20garantizar=20que=20UMF=20no=20sea=20menor=20qu?= =?UTF-8?q?e=20LMF=20en=20la=20construcci=C3=B3n=20de=20funciones=20IT2MF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/services/docit2mf_build_service.py | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/backend/api/services/docit2mf_build_service.py b/backend/api/services/docit2mf_build_service.py index a18d5d8..82083ac 100644 --- a/backend/api/services/docit2mf_build_service.py +++ b/backend/api/services/docit2mf_build_service.py @@ -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, From 040989bdfc43c9c9a224554574a28c621cac786a Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Mon, 13 Apr 2026 11:08:14 +0200 Subject: [PATCH 4/4] =?UTF-8?q?A=C3=B1adido=20soporte=20para=20autenticaci?= =?UTF-8?q?=C3=B3n=20con=20Google=20y=20ajustes=20en=20la=20configuraci?= =?UTF-8?q?=C3=B3n=20del=20entorno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/main.py | 3 +-- backend/api/routers/google_auth.py | 25 +++++++++++-------------- backend/api/utils/security.py | 4 ++++ docker-compose.yaml | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/api/main.py b/backend/api/main.py index 639fca8..775db40 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -25,8 +25,6 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) -app.include_router(google_auth_router) - app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -45,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") \ No newline at end of file diff --git a/backend/api/routers/google_auth.py b/backend/api/routers/google_auth.py index 44dc4cf..3fec964 100644 --- a/backend/api/routers/google_auth.py +++ b/backend/api/routers/google_auth.py @@ -1,6 +1,6 @@ # api/routers/google_auth.py -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, Request from fastapi.responses import RedirectResponse from pydantic import BaseModel from bson import ObjectId @@ -13,10 +13,9 @@ 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 = "http://localhost:8000/api/auth/google/callback" +REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI") # ----------------------------- @@ -25,7 +24,7 @@ REDIRECT_URI = "http://localhost:8000/api/auth/google/callback" @router.get("/login") async def google_login(): google_auth_url = ( - "https://accounts.google.com/o/oauth2/v2/auth" + "https://accounts.google.com/o/oauth2/auth" "?response_type=code" f"&client_id={GOOGLE_CLIENT_ID}" f"&redirect_uri={REDIRECT_URI}" @@ -37,13 +36,17 @@ async def google_login(): return RedirectResponse(google_auth_url) + # ----------------------------- # 2. CALLBACK → GOOGLE DEVUELVE EL CODE # ----------------------------- @router.get("/callback") -async def google_callback(code: str): +async def google_callback(request: Request): + + code = request.query_params.get("code") + if not code: + raise HTTPException(status_code=400, detail="Missing code parameter") - # 1. Intercambiar code por access_token token_url = "https://oauth2.googleapis.com/token" data = { @@ -59,11 +62,10 @@ async def google_callback(code: str): token_json = token_response.json() if "access_token" not in token_json: - raise HTTPException(status_code=400, detail="Error obteniendo token de Google") + raise HTTPException(status_code=400, detail=token_json) access_token = token_json["access_token"] - # 2. Obtener datos del usuario desde Google async with httpx.AsyncClient() as client: userinfo = await client.get( "https://www.googleapis.com/oauth2/v2/userinfo", @@ -76,26 +78,21 @@ async def google_callback(code: str): email = user_data["email"] name = user_data.get("name", "Usuario") - # 3. Buscar usuario en MongoDB user = await users_collection.find_one({"email": email}) if not user: - # Crear usuario nuevo new_user = { "username": name, "email": email, - "password_hash": None, # No hay contraseña + "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"] - # 4. Crear JWT de tu sistema token = create_access_token({"user_id": str(user_id)}) - # 5. Redirigir al frontend con el token return {"message": "Login con Google exitoso", "token": token} diff --git a/backend/api/utils/security.py b/backend/api/utils/security.py index eb3ad3c..f6abe1c 100644 --- a/backend/api/utils/security.py +++ b/backend/api/utils/security.py @@ -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") diff --git a/docker-compose.yaml b/docker-compose.yaml index f0fa360..aead62e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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: