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/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/main.py b/backend/api/main.py index f7df1ae..1edd7e5 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -1,14 +1,26 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager +from api.database.mongodb import db -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 +# Routers +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): + # Aquí podrías hacer comprobaciones si quieres + yield + # No hace falta cerrar nada con Motor -app = FastAPI() +app = FastAPI(lifespan=lifespan) app.add_middleware( CORSMiddleware, @@ -18,8 +30,12 @@ app.add_middleware( allow_headers=["*"], ) +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/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/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_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/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/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 new file mode 100644 index 0000000..26ebe6e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn +motor +pydantic +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: