From e79c6df2babdd8b7cc5c1fa330af36babda25d8b Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Wed, 25 Mar 2026 13:42:09 +0100 Subject: [PATCH 1/5] =?UTF-8?q?A=C3=B1adidos=20endpoints=20de=20validaci?= =?UTF-8?q?=C3=B3n,=20organizaci=C3=B3n=20del=20backend=20en=20subcarpetas?= =?UTF-8?q?=20y=20archivos=20y=20a=C3=B1adido=20el=20control=20de=20errore?= =?UTF-8?q?s=20http.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/main.py | 25 +++ backend/api/models/docmf_models.py | 43 ++++ .../models/docmf_simple_validation_models.py | 24 ++ backend/api/models/docmf_validation_models.py | 37 +++ backend/api/models/evaluation_models.py | 35 +++ backend/api/models/value_function_models.py | 36 +++ backend/api/routers/docmf_build.py | 12 + backend/api/routers/docmf_evaluate.py | 12 + .../api/routers/docmf_simple_validation.py | 38 ++++ backend/api/routers/docmf_validation.py | 39 ++++ backend/api/routers/value_function.py | 19 ++ backend/api/routes.py | 210 ------------------ backend/api/services/docmf_build_service.py | 37 +++ .../api/services/docmf_evaluate_service.py | 22 ++ .../docmf_simple_validation_service.py | 25 +++ .../api/services/docmf_validation_service.py | 39 ++++ .../api/services/value_function_service.py | 38 ++++ backend/api/utils/interpolation.py | 8 + 18 files changed, 489 insertions(+), 210 deletions(-) create mode 100644 backend/api/main.py create mode 100644 backend/api/models/docmf_models.py create mode 100644 backend/api/models/docmf_simple_validation_models.py create mode 100644 backend/api/models/docmf_validation_models.py create mode 100644 backend/api/models/evaluation_models.py create mode 100644 backend/api/models/value_function_models.py create mode 100644 backend/api/routers/docmf_build.py create mode 100644 backend/api/routers/docmf_evaluate.py create mode 100644 backend/api/routers/docmf_simple_validation.py create mode 100644 backend/api/routers/docmf_validation.py create mode 100644 backend/api/routers/value_function.py delete mode 100644 backend/api/routes.py create mode 100644 backend/api/services/docmf_build_service.py create mode 100644 backend/api/services/docmf_evaluate_service.py create mode 100644 backend/api/services/docmf_simple_validation_service.py create mode 100644 backend/api/services/docmf_validation_service.py create mode 100644 backend/api/services/value_function_service.py create mode 100644 backend/api/utils/interpolation.py diff --git a/backend/api/main.py b/backend/api/main.py new file mode 100644 index 0000000..f7df1ae --- /dev/null +++ b/backend/api/main.py @@ -0,0 +1,25 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from routers.value_function import router as value_router +from routers.docmf_build import router as docmf_build_router +from routers.docmf_evaluate import router as docmf_eval_router +from routers.docmf_simple_validation import router as simple_validation_router +from routers.docmf_validation import router as validation_router + + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(value_router, prefix="/api/criteria/doc") +app.include_router(docmf_build_router, prefix="/api/criteria/doc-mf") +app.include_router(docmf_eval_router, prefix="/api/criteria/doc-mf") +app.include_router(simple_validation_router, prefix="/api/criteria/doc-mf") +app.include_router(validation_router, prefix="/api/criteria/doc-mf") diff --git a/backend/api/models/docmf_models.py b/backend/api/models/docmf_models.py new file mode 100644 index 0000000..a13b69c --- /dev/null +++ b/backend/api/models/docmf_models.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class DoCMFRequest(BaseModel): + term: str + core: Tuple[float, float] + support: Tuple[float, float] + left_nodes_x: List[float] + left_blank_cards: List[int] + right_nodes_x: List[float] + right_blank_cards: List[int] + + @field_validator("term") + def term_not_empty(cls, v): + if not v.strip(): + raise ValueError("El término no puede estar vacío.") + return v + + @field_validator("core") + def core_valid(cls, v): + a, b = v + if a > b: + raise ValueError("El núcleo debe cumplir a <= b.") + return v + + @field_validator("support") + def support_valid(cls, v, info): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + + core = info.data.get("core") + if core: + a, b = core + if not (c <= a < b <= d): + raise ValueError("El núcleo debe estar dentro del soporte.") + return v + + @field_validator("left_blank_cards", "right_blank_cards") + def cards_valid(cls, v): + if any(c < 0 for c in v): + raise ValueError("Las cartas no pueden ser negativas.") + return v diff --git a/backend/api/models/docmf_simple_validation_models.py b/backend/api/models/docmf_simple_validation_models.py new file mode 100644 index 0000000..246e5d5 --- /dev/null +++ b/backend/api/models/docmf_simple_validation_models.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class SimpleLevelDefinition(BaseModel): + core: Tuple[float, float] + support: Tuple[float, float] + + @field_validator("core") + def validate_core(cls, v): + a, b = v + if a >= b: + raise ValueError("El núcleo debe cumplir a < b.") + return v + + @field_validator("support") + def validate_support(cls, v): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + return v + + +class SimpleValidationRequest(BaseModel): + levels: List[SimpleLevelDefinition] diff --git a/backend/api/models/docmf_validation_models.py b/backend/api/models/docmf_validation_models.py new file mode 100644 index 0000000..e62907d --- /dev/null +++ b/backend/api/models/docmf_validation_models.py @@ -0,0 +1,37 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class LevelDefinition(BaseModel): + core: Tuple[float, float] + support: Tuple[float, float] + left_nodes: List[Tuple[float, float]] + right_nodes: List[Tuple[float, float]] + + @field_validator("core") + def validate_core(cls, v): + a, b = v + if a >= b: + raise ValueError("El núcleo debe cumplir a < b.") + return v + + @field_validator("support") + def validate_support(cls, v): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + return v + + @field_validator("left_nodes", "right_nodes") + def validate_nodes(cls, v): + if len(v) < 2: + raise ValueError("Debe haber al menos 2 nodos.") + xs = [p[0] for p in v] + if xs != sorted(xs): + raise ValueError("Los nodos deben estar ordenados por x.") + if len(xs) != len(set(xs)): + raise ValueError("Los nodos no pueden tener valores x duplicados.") + return v + + +class ValidationRequest(BaseModel): + levels: List[LevelDefinition] diff --git a/backend/api/models/evaluation_models.py b/backend/api/models/evaluation_models.py new file mode 100644 index 0000000..ca18221 --- /dev/null +++ b/backend/api/models/evaluation_models.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class EvaluationRequest(BaseModel): + x: float + core: Tuple[float, float] + support: Tuple[float, float] + left_nodes: List[Tuple[float, float]] + right_nodes: List[Tuple[float, float]] + + @field_validator("core") + def core_valid(cls, v): + a, b = v + if a > b: + raise ValueError("El núcleo debe cumplir a <= b.") + return v + + @field_validator("support") + def support_valid(cls, v, info): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + + core = info.data.get("core") + if core: + a, b = core + if not (c <= a < b <= d): + raise ValueError("El núcleo debe estar dentro del soporte.") + return v + + @field_validator("left_nodes", "right_nodes") + def nodes_valid(cls, v): + if len(v) < 2: + raise ValueError("Debe haber al menos 2 nodos.") + return v diff --git a/backend/api/models/value_function_models.py b/backend/api/models/value_function_models.py new file mode 100644 index 0000000..66aaed5 --- /dev/null +++ b/backend/api/models/value_function_models.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, field_validator +from typing import List, Dict + +class ValueFunctionRequest(BaseModel): + criterion_name: str + levels: List[str] + blank_cards: List[int] + references: Dict[str, float] + + @field_validator("criterion_name") + def name_not_empty(cls, v): + if not v.strip(): + raise ValueError("El nombre no puede estar vacío.") + return v + + @field_validator("levels") + def levels_not_empty(cls, v): + if len(v) < 2: + raise ValueError("Debe haber al menos 2 niveles.") + return v + + @field_validator("blank_cards") + def cards_valid(cls, v, info): + if any(c < 0 for c in v): + raise ValueError("Las cartas no pueden ser negativas.") + + levels = info.data.get("levels") + if levels and len(v) != len(levels) - 1: + raise ValueError("Debe haber uno menos de número de cartas blancas que de niveles.") + return v + + @field_validator("references") + def refs_valid(cls, v): + if len(v) != 2: + raise ValueError("Debe haber 2 referencias.") + return v diff --git a/backend/api/routers/docmf_build.py b/backend/api/routers/docmf_build.py new file mode 100644 index 0000000..f3169b2 --- /dev/null +++ b/backend/api/routers/docmf_build.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, HTTPException +from models.docmf_models import DoCMFRequest +from services.docmf_build_service import build_docmf + +router = APIRouter() + +@router.post("/build") +def build(request: DoCMFRequest): + try: + return build_docmf(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/routers/docmf_evaluate.py b/backend/api/routers/docmf_evaluate.py new file mode 100644 index 0000000..f2aae4a --- /dev/null +++ b/backend/api/routers/docmf_evaluate.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, HTTPException +from models.evaluation_models import EvaluationRequest +from services.docmf_evaluate_service import evaluate_docmf + +router = APIRouter() + +@router.post("/evaluate") +def evaluate(request: EvaluationRequest): + try: + return evaluate_docmf(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/routers/docmf_simple_validation.py b/backend/api/routers/docmf_simple_validation.py new file mode 100644 index 0000000..800ec93 --- /dev/null +++ b/backend/api/routers/docmf_simple_validation.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, HTTPException +from models.docmf_simple_validation_models import SimpleValidationRequest +from services.docmf_simple_validation_service import validate_simple_levels + +router = APIRouter() + +@router.post("/validate-simple") +def validate_simple_docmf(request: SimpleValidationRequest): + results = validate_simple_levels(request.levels) + invalid = [r for r in results if not r["valid"]] + + # Caso: un solo nivel + if len(request.levels) == 1: + if invalid: + raise HTTPException( + status_code=400, + detail={ + "message": "El nivel es incorrecto.", + "errors": invalid[0]["errors"] + } + ) + return { + "message": "El nivel es correcto.", + "details": results[0] + } + + # Caso: varios niveles + if invalid: + return { + "message": "Validación completada.", + "valid_levels": [r for r in results if r["valid"]], + "invalid_levels": invalid + } + + return { + "message": "Todos los niveles son correctos.", + "results": results + } diff --git a/backend/api/routers/docmf_validation.py b/backend/api/routers/docmf_validation.py new file mode 100644 index 0000000..a1bb518 --- /dev/null +++ b/backend/api/routers/docmf_validation.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, HTTPException +from models.docmf_validation_models import ValidationRequest +from services.docmf_validation_service import validate_levels + +router = APIRouter() + +@router.post("/validate") +def validate_docmf_levels(request: ValidationRequest): + results = validate_levels(request.levels) + + invalid = [r for r in results if not r["valid"]] + + if len(request.levels) == 1: + # Caso de un solo nivel + if invalid: + raise HTTPException( + status_code=400, + detail={ + "message": "El nivel es incorrecto.", + "errors": invalid[0]["errors"] + } + ) + return { + "message": "El nivel es correcto.", + "details": results[0] + } + + # Caso de varios niveles + if invalid: + return { + "message": "Validación completada.", + "valid_levels": [r for r in results if r["valid"]], + "invalid_levels": invalid + } + + return { + "message": "Todos los niveles son correctos.", + "results": results + } diff --git a/backend/api/routers/value_function.py b/backend/api/routers/value_function.py new file mode 100644 index 0000000..c30022e --- /dev/null +++ b/backend/api/routers/value_function.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, HTTPException +from models.value_function_models import ValueFunctionRequest +from services.value_function_service import compute_value_function, compute_points + +router = APIRouter() + +@router.post("/value-function") +def value_function(request: ValueFunctionRequest): + try: + return compute_value_function(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/value-function/points") +def value_function_points(request: ValueFunctionRequest): + try: + return compute_points(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/routes.py b/backend/api/routes.py deleted file mode 100644 index 10544bc..0000000 --- a/backend/api/routes.py +++ /dev/null @@ -1,210 +0,0 @@ -from fastapi import FastAPI -from pydantic import BaseModel -from typing import List, Dict, Tuple -from fastapi.middleware.cors import CORSMiddleware - -app = FastAPI() - -# Configuración CORS para permitir peticiones desde React -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Permite cualquier origen (perfecto para desarrollo local) - allow_credentials=True, - allow_methods=["*"], # Permite POST, GET, OPTIONS, etc. - allow_headers=["*"], # Permite cualquier cabecera -) - -# ----------------------------- -# MODELOS -# ----------------------------- - -class ValueFunctionRequest(BaseModel): - criterion_name: str - levels: List[str] - blank_cards: List[int] - references: Dict[str, float] - -class DoCMFRequest(BaseModel): - term: str - core: Tuple[float, float] # [a, b] - support: Tuple[float, float] # [c, d] - left_nodes_x: List[float] - left_blank_cards: List[int] - right_nodes_x: List[float] - right_blank_cards: List[int] - -class EvaluationRequest(BaseModel): - x: float - left_nodes: List[Tuple[float, float]] - right_nodes: List[Tuple[float, float]] - core: Tuple[float, float] - support: Tuple[float, float] - - -# ----------------------------- -# ENDPOINT 1: FUNCIÓN DE VALOR DoC -# ----------------------------- - -@app.post("/api/criteria/doc/value-function") -def calcular_value_function(request: ValueFunctionRequest): - levels = request.levels - cards = request.blank_cards - refs = request.references - - ref_indices = sorted(int(k) for k in refs) - p, q = ref_indices - up, uq = refs[str(p)], refs[str(q)] - - total_units = sum(cards[i] + 1 for i in range(p, q)) - alpha = (uq - up) / total_units if total_units else 0 - - values = [0] * len(levels) - values[p] = up - - for i in range(p + 1, len(levels)): - units = sum(cards[r] + 1 for r in range(p, i)) - values[i] = up + alpha * units - - for i in range(p - 1, -1, -1): - units = sum(cards[r] + 1 for r in range(i, p)) - values[i] = up - alpha * units - - return { - "criterion_name": request.criterion_name, - "values": {levels[i]: round(values[i], 4) for i in range(len(levels))} - } - - -# ----------------------------- -# ENDPOINT 2: CONSTRUIR DoC-MF -# ----------------------------- - -@app.post("/api/criteria/doc-mf/build") -def build_doc_mf(request: DoCMFRequest): - - a, b = request.core - c, d = request.support - - # ---- LADO IZQUIERDO ---- - left_x = request.left_nodes_x - left_e = request.left_blank_cards - - TL = sum(e + 1 for e in left_e) - YL = 1 / TL if TL else 0 - - left_nodes = [] - acc = 0 - for i in range(len(left_x)): - if i == 0: - left_nodes.append((left_x[i], 0.0)) - else: - acc += (left_e[i-1] + 1) - left_nodes.append((left_x[i], round(acc * YL, 4))) - - # ---- LADO DERECHO ---- - right_x = request.right_nodes_x - right_e = request.right_blank_cards - - TR = sum(e + 1 for e in right_e) - YR = 1 / TR if TR else 0 - - right_nodes = [] - acc = 0 - for i in range(len(right_x)): - if i == 0: - right_nodes.append((right_x[i], 1.0)) - else: - acc += (right_e[i-1] + 1) - right_nodes.append((right_x[i], round(1 - acc * YR, 4))) - - return { - "term": request.term, - "core": request.core, - "support": request.support, - "left_nodes": left_nodes, - "right_nodes": right_nodes - } - - -# ----------------------------- -# ENDPOINT 3: EVALUAR UN VALOR x -# ----------------------------- - -def linear_interpolation(x, nodes): - for i in range(len(nodes) - 1): - x0, y0 = nodes[i] - x1, y1 = nodes[i+1] - if x0 <= x <= x1: - t = (x - x0) / (x1 - x0) - return y0 + t * (y1 - y0) - return 0.0 - -@app.post("/api/criteria/doc-mf/evaluate") -def evaluate_doc_mf(request: EvaluationRequest): - x = request.x - a, b = request.core - c, d = request.support - - if x < c or x > d: - return { - "membership": 0.0, - "explanation": "El valor está fuera del soporte, por eso la pertenencia es 0." - } - - if a <= x <= b: - return { - "membership": 1.0, - "explanation": "El valor está dentro del núcleo, por eso la pertenencia es 1." - } - - if c <= x < a: - mu = linear_interpolation(x, request.left_nodes) - return { - "membership": mu, - "explanation": "El valor está en el lado izquierdo y se interpola entre nodos." - } - - if b < x <= d: - mu = linear_interpolation(x, request.right_nodes) - return { - "membership": mu, - "explanation": "El valor está en el lado derecho y se interpola entre nodos." - } - - return { - "membership": 0.0, - "explanation": "No se pudo determinar la pertenencia." - } - - -# ----------------------------- -# ENDPOINT 4: PUNTOS (x,y) DE LA FUNCIÓN DE VALOR -# ----------------------------- - -@app.post("/api/criteria/doc/value-function/points") -def value_function_points(request: ValueFunctionRequest): - levels = request.levels - cards = request.blank_cards - refs = request.references - - ref_indices = sorted(int(k) for k in refs) - p, q = ref_indices - up, uq = refs[str(p)], refs[str(q)] - - total_units = sum(cards[i] + 1 for i in range(p, q)) - alpha = (uq - up) / total_units if total_units else 0 - - values = [0] * len(levels) - values[p] = up - - for i in range(p + 1, len(levels)): - units = sum(cards[r] + 1 for r in range(p, i)) - values[i] = up + alpha * units - - for i in range(p - 1, -1, -1): - units = sum(cards[r] + 1 for r in range(i, p)) - values[i] = up - alpha * units - - points = [{"x": i, "y": round(values[i], 4)} for i in range(len(levels))] - - return {"points": points} diff --git a/backend/api/services/docmf_build_service.py b/backend/api/services/docmf_build_service.py new file mode 100644 index 0000000..0cdc6eb --- /dev/null +++ b/backend/api/services/docmf_build_service.py @@ -0,0 +1,37 @@ +def build_docmf(request): + a, b = request.core + c, d = request.support + + # LEFT + TL = sum(e + 1 for e in request.left_blank_cards) + YL = 1 / TL + left_nodes = [] + acc = 0 + + for i, x in enumerate(request.left_nodes_x): + if i == 0: + left_nodes.append((x, 0.0)) + else: + acc += request.left_blank_cards[i-1] + 1 + left_nodes.append((x, round(acc * YL, 4))) + + # RIGHT + TR = sum(e + 1 for e in request.right_blank_cards) + YR = 1 / TR + right_nodes = [] + acc = 0 + + for i, x in enumerate(request.right_nodes_x): + if i == 0: + right_nodes.append((x, 1.0)) + else: + acc += request.right_blank_cards[i-1] + 1 + right_nodes.append((x, round(1 - acc * YR, 4))) + + return { + "term": request.term, + "core": request.core, + "support": request.support, + "left_nodes": left_nodes, + "right_nodes": right_nodes + } diff --git a/backend/api/services/docmf_evaluate_service.py b/backend/api/services/docmf_evaluate_service.py new file mode 100644 index 0000000..37a3562 --- /dev/null +++ b/backend/api/services/docmf_evaluate_service.py @@ -0,0 +1,22 @@ +from utils.interpolation import linear_interpolation + +def evaluate_docmf(request): + x = request.x + a, b = request.core + c, d = request.support + + if x < c or x > d: + return {"membership": 0.0, "explanation": "Fuera del soporte."} + + if a <= x <= b: + return {"membership": 1.0, "explanation": "Dentro del núcleo."} + + if c <= x < a: + mu = linear_interpolation(x, request.left_nodes) + return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.left_nodes} del lado izquierdo."} + + if b < x <= d: + mu = linear_interpolation(x, request.right_nodes) + return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.right_nodes} del lado derecho."} + + raise ValueError("No se pudo evaluar el valor.") diff --git a/backend/api/services/docmf_simple_validation_service.py b/backend/api/services/docmf_simple_validation_service.py new file mode 100644 index 0000000..253f910 --- /dev/null +++ b/backend/api/services/docmf_simple_validation_service.py @@ -0,0 +1,25 @@ +def validate_simple_level(level: dict): + errors = [] + + a, b = level["core"] + c, d = level["support"] + + # Validación: núcleo dentro del soporte + if not (c <= a < b <= d): + errors.append("El núcleo debe estar completamente dentro del soporte.") + + return errors + + +def validate_simple_levels(levels): + results = [] + + for idx, level in enumerate(levels): + errors = validate_simple_level(level.dict()) + results.append({ + "level_index": idx, + "valid": len(errors) == 0, + "errors": errors + }) + + return results diff --git a/backend/api/services/docmf_validation_service.py b/backend/api/services/docmf_validation_service.py new file mode 100644 index 0000000..97ccfa5 --- /dev/null +++ b/backend/api/services/docmf_validation_service.py @@ -0,0 +1,39 @@ +def validate_single_level(level: dict): + errors = [] + + a, b = level["core"] + c, d = level["support"] + + # Core dentro del soporte + if not (c <= a < b <= d): + errors.append("El núcleo debe estar completamente dentro del soporte.") + + # Nodos cubren correctamente el soporte + left = level["left_nodes"] + right = level["right_nodes"] + + if left[0][0] != c: + errors.append("El primer nodo izquierdo debe coincidir con el inicio del soporte.") + if left[-1][0] != a: + errors.append("El último nodo izquierdo debe coincidir con el inicio del núcleo.") + + if right[0][0] != b: + errors.append("El primer nodo derecho debe coincidir con el final del núcleo.") + if right[-1][0] != d: + errors.append("El último nodo derecho debe coincidir con el final del soporte.") + + return errors + + +def validate_levels(levels): + results = [] + + for idx, level in enumerate(levels): + errors = validate_single_level(level.dict()) + results.append({ + "level_index": idx, + "valid": len(errors) == 0, + "errors": errors + }) + + return results diff --git a/backend/api/services/value_function_service.py b/backend/api/services/value_function_service.py new file mode 100644 index 0000000..07039b0 --- /dev/null +++ b/backend/api/services/value_function_service.py @@ -0,0 +1,38 @@ +def compute_value_function(request): + levels = request.levels + cards = request.blank_cards + refs = request.references + + p, q = sorted(int(k) for k in refs) + up, uq = refs[str(p)], refs[str(q)] + + if len(levels) < 3: + raise ValueError("Mínimo debe haber 3 niveles para esta funcionalidad.") + + total_units = sum(cards[i] + 1 for i in range(p, q)) + if total_units == 0: + raise ValueError("Las cartas no pueden generar 0 unidades.") + + alpha = (uq - up) / total_units + + values = [0] * len(levels) + values[p] = up + + for i in range(p + 1, len(levels)): + units = sum(cards[r] + 1 for r in range(p, i)) + values[i] = up + alpha * units + + for i in range(p - 1, -1, -1): + units = sum(cards[r] + 1 for r in range(i, p)) + values[i] = up - alpha * units + + return { + "criterion_name": request.criterion_name, + "values": {levels[i]: round(values[i], 4) for i in range(len(levels))} + } + + +def compute_points(request): + result = compute_value_function(request) + values = list(result["values"].values()) + return {"points": [{"x": i, "y": values[i]} for i in range(len(values))]} diff --git a/backend/api/utils/interpolation.py b/backend/api/utils/interpolation.py new file mode 100644 index 0000000..ccfc35f --- /dev/null +++ b/backend/api/utils/interpolation.py @@ -0,0 +1,8 @@ +def linear_interpolation(x, nodes): + for i in range(len(nodes) - 1): + x0, y0 = nodes[i] + x1, y1 = nodes[i+1] + if x0 <= x <= x1: + t = (x - x0) / (x1 - x0) + return y0 + t * (y1 - y0) + return 0.0 From 30f87732f8b3591da80fcd2195e40f8c4cebf8e7 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Wed, 25 Mar 2026 14:03:48 +0100 Subject: [PATCH 2/5] Corregido endpoint /build --- backend/api/models/docmf_models.py | 4 ++++ backend/api/routers/docmf_build.py | 8 ++++---- backend/api/services/docmf_build_service.py | 10 +++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/backend/api/models/docmf_models.py b/backend/api/models/docmf_models.py index a13b69c..0cf9342 100644 --- a/backend/api/models/docmf_models.py +++ b/backend/api/models/docmf_models.py @@ -41,3 +41,7 @@ class DoCMFRequest(BaseModel): if any(c < 0 for c in v): raise ValueError("Las cartas no pueden ser negativas.") return v + + +class DoCMFMultiRequest(BaseModel): + levels: List[DoCMFRequest] diff --git a/backend/api/routers/docmf_build.py b/backend/api/routers/docmf_build.py index f3169b2..3fc355f 100644 --- a/backend/api/routers/docmf_build.py +++ b/backend/api/routers/docmf_build.py @@ -1,12 +1,12 @@ from fastapi import APIRouter, HTTPException -from models.docmf_models import DoCMFRequest -from services.docmf_build_service import build_docmf +from models.docmf_models import DoCMFMultiRequest +from services.docmf_build_service import build_docmf_multi router = APIRouter() @router.post("/build") -def build(request: DoCMFRequest): +def build(request: DoCMFMultiRequest): try: - return build_docmf(request) + return build_docmf_multi(request) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/services/docmf_build_service.py b/backend/api/services/docmf_build_service.py index 0cdc6eb..7c88660 100644 --- a/backend/api/services/docmf_build_service.py +++ b/backend/api/services/docmf_build_service.py @@ -1,4 +1,4 @@ -def build_docmf(request): +def build_single_docmf(request): a, b = request.core c, d = request.support @@ -35,3 +35,11 @@ def build_docmf(request): "left_nodes": left_nodes, "right_nodes": right_nodes } + + +def build_docmf_multi(request): + results = [] + for level in request.levels: + result = build_single_docmf(level) + results.append(result) + return {"results": results} From e9a3bf6c21810fd88ff30c94604f9b607372e6e6 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Thu, 26 Mar 2026 09:17:41 +0100 Subject: [PATCH 3/5] Modificado el Docker-compose --- docker-compose.yaml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 38e74c6..2d3b0f5 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,17 +1,44 @@ +version: "3.9" + services: backend: build: context: ./backend + container_name: backend ports: - "8000:8000" volumes: - ./backend:/app + depends_on: + - db + environment: + DB_HOST: db + DB_PORT: 3306 + DB_USER: root + DB_PASSWORD: root + DB_NAME: deckofcards frontend: build: context: ./frontend + container_name: frontend ports: - "5173:5173" volumes: - ./frontend:/app - - /app/node_modules \ No newline at end of file + - /app/node_modules + + db: + image: mysql:8.0 + container_name: mysql_db + restart: always + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: deckofcards + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + +volumes: + mysql_data: \ No newline at end of file From 57b64b4f1e942761c419d24f56498ee04209eb96 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Fri, 27 Mar 2026 09:02:28 +0100 Subject: [PATCH 4/5] Backend con base de datos relacional MySQL --- backend/Dockerfile | 8 +++++-- backend/api/database/__init__.py | 0 backend/api/database/connection.py | 18 ++++++++++++++ backend/api/database/init_db.py | 5 ++++ backend/api/database/models.py | 14 +++++++++++ backend/api/database/session.py | 15 ++++++++++++ backend/api/main.py | 24 ++++++++++++++----- backend/api/routers/docmf_build.py | 4 ++-- backend/api/routers/docmf_evaluate.py | 4 ++-- .../api/routers/docmf_simple_validation.py | 4 ++-- backend/api/routers/docmf_validation.py | 4 ++-- backend/api/routers/test_db.py | 13 ++++++++++ backend/api/routers/value_function.py | 4 ++-- .../api/services/docmf_evaluate_service.py | 2 +- backend/requirements.txt | 6 +++++ 15 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 backend/api/database/__init__.py create mode 100644 backend/api/database/connection.py create mode 100644 backend/api/database/init_db.py create mode 100644 backend/api/database/models.py create mode 100644 backend/api/database/session.py create mode 100644 backend/api/routers/test_db.py create mode 100644 backend/requirements.txt diff --git a/backend/Dockerfile b/backend/Dockerfile index b862b2b..3639ea9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,6 +2,10 @@ FROM python:3.10-slim WORKDIR /app -RUN pip install fastapi uvicorn +COPY requirements.txt . -CMD ["uvicorn", "api.routes:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/backend/api/database/__init__.py b/backend/api/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/database/connection.py b/backend/api/database/connection.py new file mode 100644 index 0000000..640fac4 --- /dev/null +++ b/backend/api/database/connection.py @@ -0,0 +1,18 @@ +import os +from sqlalchemy import create_engine + +DB_USER = "root" +DB_PASSWORD = "root" +DB_HOST = "db" +DB_PORT = "3306" +DB_NAME = "deckofcards" + +DATABASE_URL = ( + f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" +) + +engine = create_engine( + DATABASE_URL, + pool_pre_ping=True, + echo=False +) diff --git a/backend/api/database/init_db.py b/backend/api/database/init_db.py new file mode 100644 index 0000000..4fd4e1f --- /dev/null +++ b/backend/api/database/init_db.py @@ -0,0 +1,5 @@ +from .connection import engine +from .models import Base + +def init_db(): + Base.metadata.create_all(bind=engine) diff --git a/backend/api/database/models.py b/backend/api/database/models.py new file mode 100644 index 0000000..6674aab --- /dev/null +++ b/backend/api/database/models.py @@ -0,0 +1,14 @@ +from sqlalchemy.orm import declarative_base +from sqlalchemy import Column, Integer, String, Float + +Base = declarative_base() + +class DoCMFLevel(Base): + __tablename__ = "docmf_levels" + + id = Column(Integer, primary_key=True, index=True) + term = Column(String(50), nullable=False) + core_a = Column(Float, nullable=False) + core_b = Column(Float, nullable=False) + support_c = Column(Float, nullable=False) + support_d = Column(Float, nullable=False) diff --git a/backend/api/database/session.py b/backend/api/database/session.py new file mode 100644 index 0000000..9ba837b --- /dev/null +++ b/backend/api/database/session.py @@ -0,0 +1,15 @@ +from sqlalchemy.orm import sessionmaker +from .connection import engine + +SessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine +) + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/api/main.py b/backend/api/main.py index f7df1ae..897ab2e 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -1,14 +1,25 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager -from routers.value_function import router as value_router -from routers.docmf_build import router as docmf_build_router -from routers.docmf_evaluate import router as docmf_eval_router -from routers.docmf_simple_validation import router as simple_validation_router -from routers.docmf_validation import router as validation_router +from api.database.init_db import init_db + +# Routers +from api.routers.test_db import router as test_db_router +from api.routers.value_function import router as value_router +from api.routers.docmf_build import router as docmf_build_router +from api.routers.docmf_evaluate import router as docmf_eval_router +from api.routers.docmf_simple_validation import router as simple_validation_router +from api.routers.docmf_validation import router as validation_router -app = FastAPI() +@asynccontextmanager +async def lifespan(app: FastAPI): + init_db() + yield + + +app = FastAPI(lifespan=lifespan) app.add_middleware( CORSMiddleware, @@ -18,6 +29,7 @@ app.add_middleware( allow_headers=["*"], ) +app.include_router(test_db_router, prefix="/api") app.include_router(value_router, prefix="/api/criteria/doc") app.include_router(docmf_build_router, prefix="/api/criteria/doc-mf") app.include_router(docmf_eval_router, prefix="/api/criteria/doc-mf") diff --git a/backend/api/routers/docmf_build.py b/backend/api/routers/docmf_build.py index 3fc355f..24467c4 100644 --- a/backend/api/routers/docmf_build.py +++ b/backend/api/routers/docmf_build.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, HTTPException -from models.docmf_models import DoCMFMultiRequest -from services.docmf_build_service import build_docmf_multi +from api.models.docmf_models import DoCMFMultiRequest +from api.services.docmf_build_service import build_docmf_multi router = APIRouter() diff --git a/backend/api/routers/docmf_evaluate.py b/backend/api/routers/docmf_evaluate.py index f2aae4a..3024fe8 100644 --- a/backend/api/routers/docmf_evaluate.py +++ b/backend/api/routers/docmf_evaluate.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, HTTPException -from models.evaluation_models import EvaluationRequest -from services.docmf_evaluate_service import evaluate_docmf +from api.models.evaluation_models import EvaluationRequest +from api.services.docmf_evaluate_service import evaluate_docmf router = APIRouter() diff --git a/backend/api/routers/docmf_simple_validation.py b/backend/api/routers/docmf_simple_validation.py index 800ec93..425384a 100644 --- a/backend/api/routers/docmf_simple_validation.py +++ b/backend/api/routers/docmf_simple_validation.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, HTTPException -from models.docmf_simple_validation_models import SimpleValidationRequest -from services.docmf_simple_validation_service import validate_simple_levels +from api.models.docmf_simple_validation_models import SimpleValidationRequest +from api.services.docmf_simple_validation_service import validate_simple_levels router = APIRouter() diff --git a/backend/api/routers/docmf_validation.py b/backend/api/routers/docmf_validation.py index a1bb518..92eda8d 100644 --- a/backend/api/routers/docmf_validation.py +++ b/backend/api/routers/docmf_validation.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, HTTPException -from models.docmf_validation_models import ValidationRequest -from services.docmf_validation_service import validate_levels +from api.models.docmf_validation_models import ValidationRequest +from api.services.docmf_validation_service import validate_levels router = APIRouter() diff --git a/backend/api/routers/test_db.py b/backend/api/routers/test_db.py new file mode 100644 index 0000000..d554bfd --- /dev/null +++ b/backend/api/routers/test_db.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter, Depends +from sqlalchemy import text +from api.database.session import get_db + +router = APIRouter() + +@router.get("/test-db") +def test_db_connection(db=Depends(get_db)): + try: + db.execute(text("SELECT 1")) + return {"status": "ok", "message": "Conexión a MySQL correcta"} + except Exception as e: + return {"status": "error", "message": str(e)} diff --git a/backend/api/routers/value_function.py b/backend/api/routers/value_function.py index c30022e..0dfc5a4 100644 --- a/backend/api/routers/value_function.py +++ b/backend/api/routers/value_function.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, HTTPException -from models.value_function_models import ValueFunctionRequest -from services.value_function_service import compute_value_function, compute_points +from api.models.value_function_models import ValueFunctionRequest +from api.services.value_function_service import compute_value_function, compute_points router = APIRouter() diff --git a/backend/api/services/docmf_evaluate_service.py b/backend/api/services/docmf_evaluate_service.py index 37a3562..5b31eb0 100644 --- a/backend/api/services/docmf_evaluate_service.py +++ b/backend/api/services/docmf_evaluate_service.py @@ -1,4 +1,4 @@ -from utils.interpolation import linear_interpolation +from api.utils.interpolation import linear_interpolation def evaluate_docmf(request): x = request.x diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..bf665b8 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn +sqlalchemy +pymysql +pydantic +cryptography \ No newline at end of file From e19e971cd6da50640eda9d8a8933c6428134863e Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Fri, 27 Mar 2026 11:16:44 +0100 Subject: [PATCH 5/5] =?UTF-8?q?Backend=20totalmente=20hecho=20con=20mongod?= =?UTF-8?q?b,=20a=C3=B1adida=20la=20funcionalidad=20de=20usuarios=20con=20?= =?UTF-8?q?historial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/database/__init__.py | 0 backend/api/database/connection.py | 18 ------ backend/api/database/init_db.py | 5 -- backend/api/database/models.py | 14 ----- backend/api/database/mongodb.py | 9 +++ backend/api/database/session.py | 15 ----- backend/api/main.py | 18 +++--- backend/api/models/user_models.py | 44 +++++++++++++++ backend/api/routers/auth.py | 89 ++++++++++++++++++++++++++++++ backend/api/routers/history.py | 62 +++++++++++++++++++++ backend/api/routers/test_db.py | 13 ----- backend/api/routers/test_mongo.py | 12 ++++ backend/api/utils/security.py | 16 ++++++ backend/requirements.txt | 7 ++- docker-compose.yaml | 19 ++----- 15 files changed, 252 insertions(+), 89 deletions(-) delete mode 100644 backend/api/database/__init__.py delete mode 100644 backend/api/database/connection.py delete mode 100644 backend/api/database/init_db.py delete mode 100644 backend/api/database/models.py create mode 100644 backend/api/database/mongodb.py delete mode 100644 backend/api/database/session.py create mode 100644 backend/api/models/user_models.py create mode 100644 backend/api/routers/auth.py create mode 100644 backend/api/routers/history.py delete mode 100644 backend/api/routers/test_db.py create mode 100644 backend/api/routers/test_mongo.py create mode 100644 backend/api/utils/security.py diff --git a/backend/api/database/__init__.py b/backend/api/database/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/api/database/connection.py b/backend/api/database/connection.py deleted file mode 100644 index 640fac4..0000000 --- a/backend/api/database/connection.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -from sqlalchemy import create_engine - -DB_USER = "root" -DB_PASSWORD = "root" -DB_HOST = "db" -DB_PORT = "3306" -DB_NAME = "deckofcards" - -DATABASE_URL = ( - f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" -) - -engine = create_engine( - DATABASE_URL, - pool_pre_ping=True, - echo=False -) diff --git a/backend/api/database/init_db.py b/backend/api/database/init_db.py deleted file mode 100644 index 4fd4e1f..0000000 --- a/backend/api/database/init_db.py +++ /dev/null @@ -1,5 +0,0 @@ -from .connection import engine -from .models import Base - -def init_db(): - Base.metadata.create_all(bind=engine) diff --git a/backend/api/database/models.py b/backend/api/database/models.py deleted file mode 100644 index 6674aab..0000000 --- a/backend/api/database/models.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlalchemy.orm import declarative_base -from sqlalchemy import Column, Integer, String, Float - -Base = declarative_base() - -class DoCMFLevel(Base): - __tablename__ = "docmf_levels" - - id = Column(Integer, primary_key=True, index=True) - term = Column(String(50), nullable=False) - core_a = Column(Float, nullable=False) - core_b = Column(Float, nullable=False) - support_c = Column(Float, nullable=False) - support_d = Column(Float, nullable=False) diff --git a/backend/api/database/mongodb.py b/backend/api/database/mongodb.py new file mode 100644 index 0000000..fbe5848 --- /dev/null +++ b/backend/api/database/mongodb.py @@ -0,0 +1,9 @@ +from motor.motor_asyncio import AsyncIOMotorClient + +MONGO_URL = "mongodb://mongo:27017" +DB_NAME = "deckofcards" + +client = AsyncIOMotorClient(MONGO_URL) +db = client[DB_NAME] + +users_collection = db["users"] diff --git a/backend/api/database/session.py b/backend/api/database/session.py deleted file mode 100644 index 9ba837b..0000000 --- a/backend/api/database/session.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy.orm import sessionmaker -from .connection import engine - -SessionLocal = sessionmaker( - autocommit=False, - autoflush=False, - bind=engine -) - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() diff --git a/backend/api/main.py b/backend/api/main.py index 897ab2e..1edd7e5 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -1,23 +1,24 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager - -from api.database.init_db import init_db +from api.database.mongodb import db # Routers -from api.routers.test_db import router as test_db_router +from api.routers.test_mongo import router as test_mongo_router from api.routers.value_function import router as value_router from api.routers.docmf_build import router as docmf_build_router from api.routers.docmf_evaluate import router as docmf_eval_router from api.routers.docmf_simple_validation import router as simple_validation_router from api.routers.docmf_validation import router as validation_router - +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 @asynccontextmanager async def lifespan(app: FastAPI): - init_db() + # Aquí podrías hacer comprobaciones si quieres yield - + # No hace falta cerrar nada con Motor app = FastAPI(lifespan=lifespan) @@ -29,9 +30,12 @@ app.add_middleware( allow_headers=["*"], ) -app.include_router(test_db_router, prefix="/api") +app.include_router(test_mongo_router, prefix="/api") app.include_router(value_router, prefix="/api/criteria/doc") app.include_router(docmf_build_router, prefix="/api/criteria/doc-mf") app.include_router(docmf_eval_router, prefix="/api/criteria/doc-mf") app.include_router(simple_validation_router, prefix="/api/criteria/doc-mf") app.include_router(validation_router, prefix="/api/criteria/doc-mf") +app.include_router(test_mongo_router, prefix="/api") +app.include_router(auth_router, prefix="/api") +app.include_router(history_router, prefix="/api") \ No newline at end of file diff --git a/backend/api/models/user_models.py b/backend/api/models/user_models.py new file mode 100644 index 0000000..c611c6f --- /dev/null +++ b/backend/api/models/user_models.py @@ -0,0 +1,44 @@ +from typing import List, Optional +from pydantic import BaseModel, EmailStr, Field +from datetime import datetime + + +class FuzzyTerm(BaseModel): + term: str + core: List[float] + support: List[float] + left_nodes: List[List[float]] + right_nodes: List[List[float]] + + +class HistoryItem(BaseModel): + id: Optional[str] = Field(default=None, alias="_id") + name: str + created_at: datetime + results: List[FuzzyTerm] + + +class HistoryCreateRequest(BaseModel): + name: str + results: List[FuzzyTerm] + + +class UserCreate(BaseModel): + username: str + email: EmailStr + password: str + + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +class UserInDB(BaseModel): + id: Optional[str] = Field(default=None, alias="_id") + username: str + email: EmailStr + password_hash: str + token: Optional[str] = None + history: List[HistoryItem] = [] + diff --git a/backend/api/routers/auth.py b/backend/api/routers/auth.py new file mode 100644 index 0000000..2287379 --- /dev/null +++ b/backend/api/routers/auth.py @@ -0,0 +1,89 @@ +from fastapi import APIRouter, HTTPException, status +from api.database.mongodb import users_collection +from api.models.user_models import UserCreate, UserLogin +from api.utils.security import hash_password, verify_password, generate_token +from bson import ObjectId + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/register") +async def register_user(user: UserCreate): + existing_username = await users_collection.find_one({"username": user.username}) + if existing_username: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="El nombre de usuario ya está en uso", + ) + + existing_email = await users_collection.find_one({"email": user.email}) + if existing_email: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="El email ya está registrado", + ) + + token = generate_token() + + user_doc = { + "username": user.username, + "email": user.email, + "password_hash": hash_password(user.password), + "token": token, + "history": [], + } + + result = await users_collection.insert_one(user_doc) + + return { + "message": "Usuario registrado correctamente", + "user_id": str(result.inserted_id), + "token": token, + } + + +@router.post("/login") +async def login_user(credentials: UserLogin): + user = await users_collection.find_one({"email": credentials.email}) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Credenciales inválidas", + ) + + if not verify_password(credentials.password, user["password_hash"]): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Credenciales inválidas", + ) + + new_token = generate_token() + + await users_collection.update_one( + {"_id": user["_id"]}, + {"$set": {"token": new_token}} + ) + + return { + "message": "Login correcto", + "user_id": str(user["_id"]), + "username": user["username"], + "token": new_token, + } + + +@router.post("/logout/{user_id}") +async def logout_user(user_id: str): + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Usuario no encontrado", + ) + + await users_collection.update_one( + {"_id": ObjectId(user_id)}, + {"$set": {"token": None}} + ) + + return {"message": "Sesión cerrada correctamente"} diff --git a/backend/api/routers/history.py b/backend/api/routers/history.py new file mode 100644 index 0000000..fadd769 --- /dev/null +++ b/backend/api/routers/history.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, HTTPException, status +from typing import List +from datetime import datetime +from bson import ObjectId + +from api.database.mongodb import users_collection +from api.models.user_models import FuzzyTerm, HistoryCreateRequest + +router = APIRouter(prefix="/history", tags=["history"]) + + +@router.post("/{user_id}/add") +async def add_history_item(user_id: str, data: HistoryCreateRequest): + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Usuario no encontrado", + ) + + history_item_id = ObjectId() + + history_item = { + "_id": history_item_id, + "name": data.name, # ← nuevo campo + "created_at": datetime.utcnow(), + "results": [r.dict() for r in data.results], + } + + await users_collection.update_one( + {"_id": ObjectId(user_id)}, + {"$push": {"history": history_item}}, + ) + + return { + "message": "Elemento añadido al historial", + "history_item_id": str(history_item_id), + } + + + +@router.delete("/{user_id}/delete/{history_item_id}") +async def delete_history_item(user_id: str, history_item_id: str): + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Usuario no encontrado", + ) + + result = await users_collection.update_one( + {"_id": ObjectId(user_id)}, + {"$pull": {"history": {"_id": ObjectId(history_item_id)}}}, + ) + + if result.modified_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Elemento de historial no encontrado", + ) + + return {"message": "Elemento eliminado del historial"} diff --git a/backend/api/routers/test_db.py b/backend/api/routers/test_db.py deleted file mode 100644 index d554bfd..0000000 --- a/backend/api/routers/test_db.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy import text -from api.database.session import get_db - -router = APIRouter() - -@router.get("/test-db") -def test_db_connection(db=Depends(get_db)): - try: - db.execute(text("SELECT 1")) - return {"status": "ok", "message": "Conexión a MySQL correcta"} - except Exception as e: - return {"status": "error", "message": str(e)} diff --git a/backend/api/routers/test_mongo.py b/backend/api/routers/test_mongo.py new file mode 100644 index 0000000..43dd15d --- /dev/null +++ b/backend/api/routers/test_mongo.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter +from api.database.mongodb import db + +router = APIRouter() + +@router.get("/test-mongo") +async def test_mongo(): + try: + await db.command("ping") + return {"status": "ok", "message": "Conexión a MongoDB correcta"} + except Exception as e: + return {"status": "error", "message": str(e)} diff --git a/backend/api/utils/security.py b/backend/api/utils/security.py new file mode 100644 index 0000000..6dd73c5 --- /dev/null +++ b/backend/api/utils/security.py @@ -0,0 +1,16 @@ +import secrets +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def generate_token() -> str: + return secrets.token_hex(32) # 64 caracteres seguros diff --git a/backend/requirements.txt b/backend/requirements.txt index bf665b8..26ebe6e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,7 @@ fastapi uvicorn -sqlalchemy -pymysql +motor pydantic -cryptography \ No newline at end of file +passlib +bcrypt==4.0.1 +email-validator diff --git a/docker-compose.yaml b/docker-compose.yaml index 2d3b0f5..f0fa360 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,12 +11,6 @@ services: - ./backend:/app depends_on: - db - environment: - DB_HOST: db - DB_PORT: 3306 - DB_USER: root - DB_PASSWORD: root - DB_NAME: deckofcards frontend: build: @@ -29,16 +23,13 @@ services: - /app/node_modules db: - image: mysql:8.0 - container_name: mysql_db + image: mongo:6 + container_name: mongo restart: always - environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: deckofcards ports: - - "3306:3306" + - "27018:27017" volumes: - - mysql_data:/var/lib/mysql + - mongo_data:/data/db volumes: - mysql_data: \ No newline at end of file + mongo_data: