Merge branch 'develop' into feature/frontend-v3
This commit is contained in:
@@ -5,6 +5,8 @@ __pycache__/
|
|||||||
|
|
||||||
# Variables de entorno
|
# Variables de entorno
|
||||||
.env
|
.env
|
||||||
|
.env*
|
||||||
|
|
||||||
|
|
||||||
# Configuraciones del editor
|
# Configuraciones del editor
|
||||||
.vscode/
|
.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.history import router as history_router
|
||||||
from api.routers.test_mongo import router as test_mongo_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.docit2mf_build import router as docit2mf_router
|
||||||
|
from api.routers.google_auth import router as google_auth_router
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
# Aquí podrías hacer comprobaciones si quieres
|
|
||||||
yield
|
yield
|
||||||
# No hace falta cerrar nada con Motor
|
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
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(auth_router, prefix="/api")
|
||||||
app.include_router(history_router, prefix="/api")
|
app.include_router(history_router, prefix="/api")
|
||||||
app.include_router(docit2mf_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
|
# api/routers/docit2mf_build.py
|
||||||
|
|
||||||
|
import logging
|
||||||
from fastapi import APIRouter, HTTPException
|
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.models.docit2mf_models import DoCIT2MFMultiRequest
|
||||||
from api.services.docit2mf_build_service import build_it2mf_from_level
|
from api.services.docit2mf_build_service import build_it2mf_from_level
|
||||||
|
|
||||||
router = APIRouter(prefix="/criteria", tags=["criteria"])
|
router = APIRouter(prefix="/criteria", tags=["criteria"])
|
||||||
|
limiter = Limiter(key_func=get_remote_address)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/doc-it2mf/build")
|
@router.post("/doc-it2mf/build")
|
||||||
|
@limiter.limit("10/minute")
|
||||||
async def build_doc_it2mf(request: DoCIT2MFMultiRequest):
|
async def build_doc_it2mf(request: DoCIT2MFMultiRequest):
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
@@ -15,6 +21,10 @@ async def build_doc_it2mf(request: DoCIT2MFMultiRequest):
|
|||||||
for level in request.levels:
|
for level in request.levels:
|
||||||
results.append(build_it2mf_from_level(level))
|
results.append(build_it2mf_from_level(level))
|
||||||
except ValueError as e:
|
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}
|
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 = []
|
result = []
|
||||||
for item in values:
|
for item in values:
|
||||||
if isinstance(item, int):
|
if isinstance(item, int):
|
||||||
# valor fijo → mismo para LMF y UMF
|
|
||||||
result.append(item)
|
result.append(item)
|
||||||
else:
|
else:
|
||||||
lo, hi = item
|
lo, hi = item
|
||||||
@@ -23,8 +22,45 @@ def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> Li
|
|||||||
return result
|
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):
|
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)
|
# LMF (mínimos)
|
||||||
|
# -------------------------
|
||||||
left_min = _extract_bounds(level.left_blank_cards, "min")
|
left_min = _extract_bounds(level.left_blank_cards, "min")
|
||||||
right_min = _extract_bounds(level.right_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)
|
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)
|
# UMF (máximos)
|
||||||
|
# -------------------------
|
||||||
left_max = _extract_bounds(level.left_blank_cards, "max")
|
left_max = _extract_bounds(level.left_blank_cards, "max")
|
||||||
right_max = _extract_bounds(level.right_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)
|
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 {
|
return {
|
||||||
"term": level.term,
|
"term": level.term,
|
||||||
"lower": lower,
|
"lower": lower,
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ from fastapi import Depends, HTTPException, status
|
|||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
from api.database.mongodb import users_collection
|
from api.database.mongodb import users_collection
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
|
import os
|
||||||
|
import jwt
|
||||||
|
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
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:
|
def generate_token() -> str:
|
||||||
return secrets.token_hex(32) # 64 caracteres seguros
|
return secrets.token_hex(32)
|
||||||
|
|
||||||
|
|
||||||
security_scheme = HTTPBearer()
|
security_scheme = HTTPBearer()
|
||||||
@@ -35,6 +39,8 @@ async def get_current_user(
|
|||||||
detail="Token inválido o usuario no autenticado",
|
detail="Token inválido o usuario no autenticado",
|
||||||
)
|
)
|
||||||
|
|
||||||
# devolvemos el documento tal cual (dict)
|
|
||||||
user["id"] = str(user["_id"])
|
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
|
passlib
|
||||||
bcrypt==4.0.1
|
bcrypt==4.0.1
|
||||||
email-validator
|
email-validator
|
||||||
|
slowapi
|
||||||
|
httpx
|
||||||
|
PyJWT
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,3 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
@@ -11,6 +9,8 @@ services:
|
|||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
env_file:
|
||||||
|
- backend\.env
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
|
|||||||
Reference in New Issue
Block a user