From e19e971cd6da50640eda9d8a8933c6428134863e Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Fri, 27 Mar 2026 11:16:44 +0100 Subject: [PATCH] =?UTF-8?q?Backend=20totalmente=20hecho=20con=20mongodb,?= =?UTF-8?q?=20a=C3=B1adida=20la=20funcionalidad=20de=20usuarios=20con=20hi?= =?UTF-8?q?storial?= 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: