mira, tengo esto, necesito que me ayudes a hacer las pruebas de los endpoinst en postman:

Folder Structure:

FastAPI/
├─ requirements.txt
├─ Dockerfile
├─ app/
│ app/
│ ├─ alembic/
│ │ alembic/
│ │ ├─ env.py
│ │ ├─ README
│ │ ├─ script.py.mako
│ │ ├─ pycache/
│ │ │ pycache/
│ │ │ └─ env.cpython-312.pyc
│ │ └─ versions/
│ │ versions/
│ │ ├─ 905731c6a76a_first_migrations.py
│ │ └─ pycache/
│ │ pycache/
│ │ └─ 905731c6a76a_first_migrations.cpython-312.pyc
│ ├─ migrations.py
│ ├─ services/
│ │ services/
│ │ ├─ init.py
│ │ ├─ task_service.py
│ │ ├─ auth_service.py
│ │ ├─ pycache/
│ │ │ pycache/
│ │ │ ├─ auth_service.cpython-312.pyc
│ │ │ ├─ task_service.cpython-312.pyc
│ │ │ ├─ init.cpython-312.pyc
│ │ │ └─ user_service.cpython-312.pyc
│ │ └─ user_service.py
│ ├─ models/
│ │ models/
│ │ ├─ status.py
│ │ ├─ init.py
│ │ ├─ role.py
│ │ ├─ pycache/
│ │ │ pycache/
│ │ │ ├─ task.cpython-312.pyc
│ │ │ ├─ role.cpython-312.pyc
│ │ │ ├─ change.cpython-312.pyc
│ │ │ ├─ user.cpython-312.pyc
│ │ │ └─ init.cpython-312.pyc
│ │ ├─ user.py
│ │ ├─ change.py
│ │ └─ task.py
│ ├─ main.py
│ ├─ .env
│ ├─ pycache/
│ │ pycache/
│ │ ├─ main.cpython-312.pyc
│ │ └─ migrations.cpython-312.pyc
│ ├─ config/
│ │ config/
│ │ ├─ init.py
│ │ ├─ database.py
│ │ ├─ pycache/
│ │ │ pycache/
│ │ │ ├─ settings.cpython-312.pyc
│ │ │ ├─ database.cpython-312.pyc
│ │ │ └─ init.cpython-312.pyc
│ │ └─ settings.py
│ ├─ routes/
│ │ routes/
│ │ ├─ init.py
│ │ ├─ auth_routes.py
│ │ ├─ pycache/
│ │ │ pycache/
│ │ │ ├─ auth_routes.cpython-312.pyc
│ │ │ ├─ task_routes.cpython-312.pyc
│ │ │ ├─ user_management_routes.cpython-312.pyc
│ │ │ └─ init.cpython-312.pyc
│ │ ├─ task_routes.py
│ │ └─ user_management_routes.py
│ ├─ alembic.ini
│ └─ utils/
│ utils/
│ ├─ init.py
│ ├─ pycache/
│ │ pycache/
│ │ ├─ dependencies.cpython-312.pyc
│ │ └─ init.cpython-312.pyc
│ └─ dependencies.py
└─ alembic.ini

File Contents:
File: FastAPI/requirements.txt

alembic==1.13.3
annotated-types==0.7.0
anyio==4.6.0
astroid==3.3.5
black==24.10.0
cffi==1.17.1
click==8.1.7
cryptography==43.0.1
dill==0.3.9
dnspython==2.7.0
ecdsa==0.19.0
email_validator==2.2.0
fastapi==0.115.0
greenlet==3.1.1
h11==0.14.0
idna==3.10
isort==5.13.2
python-multipart==0.0.5

Mako==1.3.5
MarkupSafe==3.0.1
mccabe==0.7.0
mypy-extensions==1.0.0
packaging==24.1
passlib==1.7.4
pathspec==0.12.1
peewee==3.17.6
platformdirs==4.3.6
pyasn1==0.6.1
pycparser==2.22
pydantic==2.9.2
pydantic_core==2.23.4
pylint==3.3.1
PyMySQL==1.1.1
python-dotenv==1.0.1
python-jose==3.3.0
rsa==4.9
six==1.16.0
sniffio==1.3.1
SQLAlchemy==2.0.35
starlette==0.38.6
tomlkit==0.13.2
typing_extensions==4.12.2
uvicorn==0.31.0
File: FastAPI/Dockerfile

FROM python:3.12

COPY ./app /app
COPY ./requirements.txt /app

WORKDIR /app

RUN apt-get update && apt-get upgrade -y && apt-get install -y mariadb-client && pip install mysqlclient

RUN pip install --upgrade pip
RUN pip install -r requirements.txt

CMD [ "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80" ]
File: FastAPI/app/alembic/env.py

"""
Este módulo configura Alembic para manejar migraciones de base de datos.

Se encarga de establecer la conexión a la base de datos y definir
el comportamiento de las migraciones, tanto en modo 'offline' como 'online'.
"""

from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from migrations import Base, DATABASE_URL # pylint: disable=import-error
Este objeto de configuración de Alembic proporciona acceso a los
valores dentro del archivo .ini en uso.

config = context.config # pylint: disable=no-member
config.set_main_option("sqlalchemy.url", DATABASE_URL)
Interpreta el archivo de configuración para la configuración de registro.
Esta línea configura los registradores en esencia.

if config.config_file_name is not None:
fileConfig(config.config_file_name)
Agrega el objeto MetaData de tu modelo aquí para el soporte de 'autogenerar'

target_metadata = Base.metadata

def run_migrations_offline() -> None:
"""Ejecuta migraciones en modo 'offline'.

Esto configura el contexto solo con una URL y no con un Engine,
aunque un Engine es aceptable aquí también. Al omitir la creación
del Engine, ni siquiera necesitamos que un DBAPI esté disponible.

Las llamadas a context.execute() aquí emiten la cadena dada en
la salida del script.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure( # pylint: disable=no-member
    url=url,
    target_metadata=target_metadata,
    literal_binds=True,
    dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction(): # pylint: disable=no-member
    context.run_migrations() # pylint: disable=no-member

def run_migrations_online() -> None:
"""Ejecuta migraciones en modo 'online'.

En este escenario, necesitamos crear un Engine y asociar una
conexión con el contexto.
"""
connectable = engine_from_config(
    config.get_section(config.config_ini_section, {}),
    prefix="sqlalchemy.",
    poolclass=pool.NullPool,
)

with connectable.connect() as connection:
    context.configure(connection=connection, target_metadata=target_metadata) # pylint: disable=no-member

    with context.begin_transaction(): # pylint: disable=no-member
        context.run_migrations() # pylint: disable=no-member

if context.is_offline_mode(): # pylint: disable=no-member
run_migrations_offline()
else:
run_migrations_online()
File: FastAPI/app/alembic/README

Contenido binario o no legible.
File: FastAPI/app/alembic/script.py.mako

Contenido binario o no legible.
File: FastAPI/app/alembic/pycache/env.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/alembic/versions/905731c6a76a_first_migrations.py

"""first_migrations

Revision ID: 905731c6a76a
Revises:
Create Date: 2024-11-06 18:26:01.991411

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
revision identifiers, used by Alembic.

revision: str = '905731c6a76a'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('role',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=50), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_role_id'), 'role', ['id'], unique=False)
op.create_table('status',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.CheckConstraint("name IN ('TO_DO', 'IN_PROGRESS', 'COMPLETED')", name='check_status_name'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_status_id'), 'status', ['id'], unique=False)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(length=100), nullable=False),
sa.Column('password', sa.String(length=255), nullable=False),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
op.create_table('task',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('date_of_creation', sa.Date(), nullable=False),
sa.Column('expiration_date', sa.Date(), nullable=True),
sa.Column('status_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('is_favorite', sa.Boolean(), nullable=True),
sa.CheckConstraint('expiration_date IS NULL OR expiration_date >= date_of_creation', name='check_expiration_date'),
sa.ForeignKeyConstraint(['status_id'], ['status.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_task_id'), 'task', ['id'], unique=False)
op.create_table('change',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('task_id', sa.Integer(), nullable=False),
sa.Column('timestamp', sa.Date(), nullable=False),
sa.Column('field_changed', sa.String(length=100), nullable=False),
sa.Column('old_value', sa.Text(), nullable=True),
sa.Column('new_value', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['task_id'], ['task.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_change_id'), 'change', ['id'], unique=False)
# ### end Alembic commands ###
op.execute("""
INSERT INTO status (name)
VALUES ('TO_DO'), ('IN_PROGRESS'), ('COMPLETED');
""")

# Insertar los roles por defecto
op.execute("""
INSERT INTO role (name) 
VALUES ('Administrator'), ('User');
""")

def downgrade() -> None:
# Borrar los datos insertados durante la migración
op.execute("""
DELETE FROM status WHERE name IN ('TO_DO', 'IN_PROGRESS', 'COMPLETED');
""")

op.execute("""
DELETE FROM role WHERE name IN ('Administrator', 'User');
""")

def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_change_id'), table_name='change')
op.drop_table('change')
op.drop_index(op.f('ix_task_id'), table_name='task')
op.drop_table('task')
op.drop_index(op.f('ix_user_id'), table_name='user')
op.drop_table('user')
op.drop_index(op.f('ix_status_id'), table_name='status')
op.drop_table('status')
op.drop_index(op.f('ix_role_id'), table_name='role')
op.drop_table('role')
# ### end Alembic commands ###
File: FastAPI/app/alembic/versions/pycache/905731c6a76a_first_migrations.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/migrations.py

"""
Necessary imports to define data models using SQLAlchemy ORM.

    Column: Defines a column in a table.
    Integer: Integer data type for columns.
    String: Variable-length string data type.
    Text: Large text data type for columns.
    Date: Date data type for columns.
    Boolean: Boolean data type for columns.
    ForeignKey: Defines a foreign key constraint.
    CheckConstraint: Adds a check constraint to a column.
    declarative_base: Base class for declarative model definitions.
    relationship: Establishes relationships between models.
    sessionmaker: Factory for creating new Session objects.
    create_engine: Creates a new SQLAlchemy Engine instance.
    """
    from sqlalchemy import Column, Integer, String, Text, Date, Boolean, ForeignKey, CheckConstraint
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import relationship, sessionmaker
    from sqlalchemy import create_engine
    from config.settings import DATABASE

Base declarative class for SQLAlchemy models

Base = declarative_base()

class Role(Base):# pylint: disable=too-few-public-methods
"""Role table for storing user roles."""
tablename = "role"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50),nullable=False)

class Status(Base):# pylint: disable=too-few-public-methods
"""Status table defining the various states for tasks."""
tablename = "status"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False)

__table_args__ = (
    CheckConstraint("name IN ('TO_DO', 'IN_PROGRESS', 'COMPLETED')", name="check_status_name"),
)

class User(Base): # pylint: disable=too-few-public-methods
"""User table for storing user information and their associated roles."""
tablename = "user"
id = Column(Integer, primary_key=True, index=True)
email = Column(String(100), unique=True, nullable=False)
password = Column(String(255)) # Solo aquí se maneja hashed_password
role_id = Column(Integer, ForeignKey('role.id'), nullable=True)
role = relationship("Role", back_populates="users")
is_active = Column(Boolean, default=True)

class Task(Base): # pylint: disable=too-few-public-methods
"""Task table for storing tasks with relationships to users and statuses."""
tablename = "task"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(100), nullable=False)
description = Column(Text, nullable=True)
date_of_creation = Column(Date, nullable=False)
expiration_date = Column(Date, nullable=True)
status_id = Column(Integer, ForeignKey('status.id'), nullable=False)
user_id = Column(Integer, ForeignKey('user.id'), nullable=False)
is_favorite = Column(Boolean, default=False)

status = relationship("Status", back_populates="tasks")
user = relationship("User", back_populates="tasks")

# Constraints for the table (comparison between columns)
__table_args__ = (
    CheckConstraint(
        "expiration_date IS NULL OR expiration_date >= date_of_creation", 
        name="check_expiration_date"
    ),
)

class Change(Base):# pylint: disable=too-few-public-methods
"""Change table for logging modifications to tasks."""
tablename = "change"
id = Column(Integer, primary_key=True, index=True)
task_id = Column(Integer, ForeignKey('task.id'), nullable=False)
timestamp = Column(Date, nullable=False)
field_changed = Column(String(100), nullable=False)
old_value = Column(Text, nullable=True)
new_value = Column(Text, nullable=True)

task = relationship("Task", back_populates="changes")

Configurar la base de datos

DATABASE_URL = (
f"mysql+pymysql://{DATABASE['user']}:{DATABASE['password']}@"
f"{DATABASE['host']}/{DATABASE['name']}"
)
engine = create_engine(DATABASE_URL)
Crear una sesión de base de datos

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
File: FastAPI/app/services/init.py
File: FastAPI/app/services/task_service.py

"""
Module defining services for managing tasks.
"""

from datetime import date, datetime
from typing import List, Optional
from models.task import Task
from models.change import Change
from config.database import TaskModel, ChangeModel

class TaskService:
"""
Module defining the services.
"""

@staticmethod
def create_task(
    title: str,
    description: Optional[str],
    expiration_date: Optional[date],
    status_id: int,
    user_id: int,
) -> TaskModel:  # Devuelve el modelo de la base de datos
    """Creates a new task."""
    task = TaskModel.create(  # Asegúrate de usar el modelo ORM aquí
        title=title,
        description=description,
        expiration_date=expiration_date,
        status_id=status_id,
        user_id=user_id,
    )
    return task

@staticmethod
def get_task_by_id(task_id: int, user_id: int, is_admin: bool) -> Optional[TaskModel]:
    """Retrieves a task by ID, checking user permissions."""
    task = TaskModel.get_or_none(TaskModel.id == task_id)  # Usa el modelo ORM aquí
    if task and (task.user_id == user_id or is_admin):
        return task
    return None

@staticmethod
def update_task(
    task_id: int, user_id: int, is_admin: bool, **updates
) -> Optional[Task]:
    """Updates a task, logging changes, and checking user permissions."""
    task = TaskModel.get_or_none(TaskModel.id == task_id)
    if task and (task.user_id == user_id or is_admin):
        for key, value in updates.items():
            setattr(task, key, value)
        task.save()

        # Log changes to the change history
        for field, new_value in updates.items():
            ChangeModel.create(
                task=task,
                timestamp=datetime.now(),
                field_changed=field,
                old_value=getattr(task, field, None),
                new_value=new_value,
            )
        return task
    return None

@staticmethod
def delete_task(task_id: int, user_id: int, is_admin: bool) -> bool:
    """Deletes a task, checking user permissions."""
    task = TaskModel.get_or_none(TaskModel.id == task_id)
    if task and (task.user_id == user_id or is_admin):
        task.delete_instance()
        return True
    return False

@staticmethod
def list_tasks(
    user_id: int,
    status: Optional[str],
    expiration_date: Optional[date],
    is_admin: bool,
) -> List[Task]:
    """Lists tasks, applying filters and checking user permissions."""
    query = TaskModel.select().where((TaskModel.user_id == user_id) | (is_admin))
    if status:
        query = query.where(TaskModel.status_id == status)
    if expiration_date:
        query = query.where(TaskModel.expiration_date <= expiration_date)
    return list(query)

@staticmethod
def toggle_favorite(task_id: int, user_id: int, is_admin: bool) -> Optional[Task]:
    """Toggles the favorite status of a task, checking user permissions."""
    task = Task.get_or_none(Task.id == task_id)
    if task and (task.user_id == user_id or is_admin):
        task.is_favorite = not task.is_favorite
        task.save()
        return task
    return None

@staticmethod
def get_changes_by_task_id(task_id: int) -> List[Change]:
    """Retrieves changes for a specific task."""
    return list(
        Change.select()
        .where(Change.task == task_id)
        .order_by(Change.timestamp.desc())
    )

File: FastAPI/app/services/auth_service.py

"""
Authentication Service for handling password hashing and JWT.

Includes functions for verifying passwords, hashing passwords,
authenticating users, and creating access tokens.
"""
from datetime import datetime, timedelta
from typing import Optional

from fastapi import HTTPException, status
from jose import JWTError, jwt
from passlib.context import CryptContext
from config.settings import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
from services.user_service import UserService
from models.user import UserRead # Asegúrate de importar UserRead correctamente
from config.database import UserModel
Configurar passlib context para password hashing

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

class AuthService:
"""Authentication Service."""

@staticmethod
def verify_password(plain_password: str, password: str) -> bool:
    """Verifica si la contraseña en texto plano coincide con la contraseña hasheada."""
    return pwd_context.verify(plain_password, password)

@staticmethod
def get_password_hash(password: str) -> str:
    """Hashea una contraseña en texto plano antes de almacenarla."""
    return pwd_context.hash(password)

@staticmethod
def authenticate_user(email: str, password: str) -> Optional[UserModel]:
    """
    Autentica a un usuario utilizando su email y contraseña.
    """
    user = UserService.get_user_by_email_login(email)
    if not user or not AuthService.verify_password(password, user.password):  # Usa `user.password`
        return None
    return user

@staticmethod
def create_access_token(
    data: dict, expires_delta: Optional[timedelta] = None
) -> str:
    """
    Crea un token de acceso JWT.
    """
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

@staticmethod
def get_current_user(token: str) -> Optional[UserRead]:
    """
    Decodes the JWT token and retrieves the user based on the email.
    """
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        # Decodifica el token JWT
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        email: str = payload.get("sub")
        if email is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    # Busca el usuario en la base de datos usando el email
    user = UserService.get_user_by_email(email)
    if user is None:
        raise credentials_exception
    return user

File: FastAPI/app/services/pycache/auth_service.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/services/pycache/task_service.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/services/pycache/init.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/services/pycache/user_service.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/services/user_service.py

"""
Service layer for User operations.

This module contains the business logic for managing users.
It interacts with the UserModel and uses the User model from Pydantic for data validation.
"""

from typing import Optional, List
from peewee import DoesNotExist
from config.database import UserModel, RoleModel
from models.user import UserRead
from models.role import Role

class UserService:
"""Service layer for User operations."""

@staticmethod
def create_user(email: str, password: str, role_id: int) -> UserRead:
    """
    Crear un nuevo usuario.
    """
    try:
        role_instance = RoleModel.get_by_id(role_id)
        user_instance = UserModel.create(email=email, password=password, role=role_instance)
        
        # Preparar datos para Pydantic
        user_data = user_instance.__data__.copy()
        user_data['role_id'] = role_instance.id  # Añadir role_id explícitamente
        
        # Serializar el campo 'role' correctamente
        role_data = {
            "id": role_instance.id,
            "name": role_instance.name
        }
        user_data['role'] = Role.model_validate(role_data)  # Usar el modelo Pydantic Role
        
        return UserRead.model_validate(user_data)
    except DoesNotExist as exc:
        raise ValueError(f"Role with id {role_id} not found") from exc

@staticmethod
def get_user_by_email_login(email: str) -> Optional[UserModel]:
    """
    Recupera el usuario completo desde la base de datos por su email.
    """
    try:
        return UserModel.get(UserModel.email == email)
    except UserModel.DoesNotExist:
        return None


@staticmethod
def get_user_by_email(email: str) -> Optional[UserRead]:
    """
    Obtener un usuario por su email.
    """
    try:
        user_instance = UserModel.get(UserModel.email == email)
        user_data = user_instance.__data__.copy()
        user_data['role_id'] = user_instance.role.id  # Añadir role_id explícitamente
        
        # Serializar el campo 'role' correctamente
        role_instance = user_instance.role
        role_data = {
            "id": role_instance.id,
            "name": role_instance.name
        }
        user_data['role'] = Role.model_validate(role_data)  # Usar el modelo Pydantic Role
        
        return UserRead.model_validate(user_data)
    except DoesNotExist:
        return None

@staticmethod
def get_user_by_id(user_id: int) -> Optional[UserRead]:
    """
    Obtener un usuario por su ID.
    """
    try:
        user_instance = UserModel.get_by_id(user_id)
        user_data = user_instance.__data__.copy()
        user_data['role_id'] = user_instance.role.id  # Añadir role_id explícitamente
        
        # Serializar el campo 'role' correctamente
        role_instance = user_instance.role
        role_data = {
            "id": role_instance.id,
            "name": role_instance.name
        }
        user_data['role'] = Role.model_validate(role_data)  # Usar el modelo Pydantic Role
        
        return UserRead.model_validate(user_data)
    except DoesNotExist:
        return None

@staticmethod
def update_user_status(user_id: int, is_active: bool) -> bool:
    """
    Actualiza el estado activo de un usuario.
    """
    try:
        user_instance = UserModel.get_by_id(user_id)
        user_instance.is_active = is_active
        user_instance.save()
        return True
    except DoesNotExist:
        return False
    
@staticmethod
def update_user(
    user_id: int,
    email: Optional[str] = None,
    password: Optional[str] = None,
    role_id: Optional[int] = None,
) -> Optional[UserRead]:
    """
    Actualizar un usuario existente por su ID.
    """
    try:
        user_instance = UserModel.get_by_id(user_id)

        if email:
            user_instance.email = email
        if password:
            user_instance.password = password
        if role_id:
            try:
                role_instance = RoleModel.get_by_id(role_id)
                user_instance.role = role_instance
            except DoesNotExist as exc:
                raise ValueError(f"Role with id {role_id} not found") from exc

        user_instance.save()

        # Preparar datos para Pydantic
        user_data = user_instance.__data__.copy()
        user_data['role_id'] = user_instance.role.id
        user_data['role'] = user_instance.role

        return UserRead.model_validate(user_data)
    except DoesNotExist:
        return None

@staticmethod
def delete_user(user_id: int) -> bool:
    """
    Eliminar un usuario por su ID.
    """
    try:
        user_instance = UserModel.get_by_id(user_id)
        user_instance.delete_instance()
        return True
    except DoesNotExist:
        return False

@staticmethod
def list_all_users() -> List[UserRead]:
    """
    Obtener una lista de todos los usuarios.
    """
    users = UserModel.select()
    user_list = []
    for user_instance in users:
        user_data = user_instance.__data__.copy()
        user_data['role_id'] = user_instance.role.id
        user_data['role'] = user_instance.role
        user_list.append(UserRead.model_validate(user_data))
    return user_list

File: FastAPI/app/models/status.py

"""
This module defines the Status class, which represents a status model for managing
different statuses that can be assigned to tasks or other entities within an application.

The Status class includes attributes such as the unique identifier and the name of the status.

It is built using Pydantic's BaseModel to ensure data validation and type checking.
"""

from pydantic import BaseModel

class Status(BaseModel):
"""
Represents a status with relevant details.

Attributes:
-----------
id : int
    Unique identifier for the status.
name : str
    The name of the status (e.g., 'pending', 'completed', 'in progress'),
    describing the current state of an entity.
"""

id: int
name: str

File: FastAPI/app/models/init.py
File: FastAPI/app/models/role.py

"""
This module defines the Role class, which represents a role model used to manage
different roles or permissions within an application.

The Role class includes attributes such as the role's unique identifier and name.

It is built using Pydantic's BaseModel to ensure data validation and type checking.
"""

from pydantic import BaseModel

class Role(BaseModel):
"""
Represents a role with relevant details.

Attributes:
-----------
id : int
    Unique identifier for the role.
name : str
    The name of the role (e.g., 'admin', 'user'),
    representing the permissions associated with it.
"""

id: int
name: str

File: FastAPI/app/models/pycache/task.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/models/pycache/role.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/models/pycache/change.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/models/pycache/user.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/models/pycache/init.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/models/user.py

from typing import Optional
from pydantic import BaseModel, EmailStr
from .role import Role

class UserRead(BaseModel):
id: int
email: EmailStr
role_id: int
is_active: bool
role: Role

class Config:
    from_attributes = True

class UserCreate(BaseModel):
email: EmailStr
password: str # Contraseña en texto plano; se hashea antes de almacenar
role_id: int

class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
password: Optional[str] = None
role_id: Optional[int] = None
File: FastAPI/app/models/change.py

"""
Model for logging changes to tasks.
"""

from datetime import datetime
from typing import Optional
from pydantic import BaseModel

class Change(BaseModel):
"""
Represents a record of a change made to a task.

Attributes:
    id (int): Unique identifier of the change.
    task_id (int): ID of the task associated with the change.
    timestamp (datetime): Date and time when the change was made.
    field_changed (str): Field that was changed.
    old_value (Optional[str]): Previous value of the field.
    new_value (Optional[str]): New value of the field.
"""
id: int
task_id: int
timestamp: datetime
field_changed: str
old_value: Optional[str] = None
new_value: Optional[str] = None

class Config: # pylint: disable=too-few-public-methods
    """
    Model Config
    """
    orm_mode = True

File: FastAPI/app/models/task.py

"""
This module defines the Task class, which represents a task model for handling
task-related data within an application.

The Task class includes attributes such as title, description, creation date,
expiration date, and flags for tracking the status and importance of the task.

It is based on the Pydantic BaseModel to ensure data validation and type checking.
"""

from datetime import date
from typing import List, Optional
from pydantic import BaseModel
from .change import Change

class Task(BaseModel):
"""
Represents a task with relevant details.

Attributes:
-----------
id : int
    Unique identifier for the task.
title : str
    Title of the task.
description : str
    Detailed description of the task.
date_of_creation : date
    The date when the task was created.
expiration_date : date
    The deadline or expiration date for the task.
status_id : int
    The status identifier, representing the current state
    of the task (e.g., completed, pending).
user_id : int
    The identifier of the user associated with the task.
is_favorite : bool
    Flag indicating whether the task is marked as a favorite.
"""

id: int
title: str
description: str
date_of_creation: date
expiration_date: date
status_id: int
user_id: int
is_favorite: bool
changes: Optional[List[Change]] = None

class Config:
    from_attributes = True  # Cambia 'orm_mode' a 'from_attributes' para Pydantic v2
    

class TaskCreate(BaseModel):
title: str
description: Optional[str] = None
expiration_date: Optional[date] = None
status_id: int

class TaskUpdate(BaseModel):
"""
This is so that the task can be updated.
"""
title: Optional[str] = None
description: Optional[str] = None
expiration_date: Optional[date] = None
status_id: Optional[int] = None
is_favorite: Optional[bool] = None
File: FastAPI/app/main.py

"""
Main module for the FastAPI application.
"""

from contextlib import asynccontextmanager
from fastapi import FastAPI
from starlette.responses import RedirectResponse
from routes import auth_router, task_router, user_management_router
from config.database import database as connection

@asynccontextmanager
async def lifespan(api: FastAPI):
if connection.is_closed():
connection.connect()
try:
yield
finally:
if not connection.is_closed():
connection.close()

app = FastAPI(lifespan=lifespan)

@app.get("/")
async def docs_redirect():
"""Redirige a la documentación de la API."""
return RedirectResponse(url="/docs")

app.include_router(auth_router)
app.include_router(task_router)
app.include_router(user_management_router)
File: FastAPI/app/.env

Contenido binario o no legible.
File: FastAPI/app/pycache/main.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/pycache/migrations.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/config/init.py
File: FastAPI/app/config/database.py

"""
Necessary imports to define data models using Peewee ORM.

    Model: Base class for all models.
    CharField, TextField, DateField, etc.: Database field types.
    ForeignKeyField: Defines relationships between tables.
    Check: Defines column-level constraints.
    AutoField: Auto-incrementing field used as a primary key.
    fn: Provides SQL functions like CURRENT_TIMESTAMP.
    """
    from datetime import date
    from dotenv import load_dotenv
    from config.settings import DATABASE
    from peewee import (
    MySQLDatabase, Model, CharField, TextField, DateField,
    ForeignKeyField, BooleanField, TimestampField, Check, AutoField, fn, SQL
    )

Load environment variables from the .env file

load_dotenv()
Create a MySQL database instance using environment variables

database = MySQLDatabase(
DATABASE["name"],
user=DATABASE["user"],
passwd=DATABASE["password"],
host=DATABASE["host"],
port=DATABASE["port"],
)

class RoleModel(Model):
"""Role table."""
id = AutoField(primary_key=True)
name = CharField(null=False)

class Meta:# pylint: disable=too-few-public-methods
    """Meta information for the Role model."""
    table_name = "role"
    database = database

class StatusModel(Model):
"""Status table with constraints on the name field."""
id = AutoField(primary_key=True)
name = CharField(
null=False,
constraints=[Check(SQL('"name" IN ('TO_DO', 'IN_PROGRESS', 'COMPLETED')'))]
)

class Meta:# pylint: disable=too-few-public-methods
    """Meta information for the Status model."""
    table_name = "status"
    database = database

class UserModel(Model):
"""User table with a relation to Role."""
id = AutoField(primary_key=True)
email = CharField(unique=True, null=False)
password = CharField(null=False)
role = ForeignKeyField(RoleModel, backref='users', null=True, on_delete='SET NULL')
is_active = BooleanField(default=True)

class Meta:# pylint: disable=too-few-public-methods
    """Meta information for the User model."""
    table_name = "user"
    database = database

class TaskModel(Model):
"""Task table with relationships to User and Status."""
id = AutoField(primary_key=True)
title = CharField(null=False)
description = TextField(null=True)
date_of_creation = DateField(default=date.today, null=False)
expiration_date = DateField(null=True)
status = ForeignKeyField(StatusModel, backref='tasks', on_delete='CASCADE')
user = ForeignKeyField(UserModel, backref='tasks', on_delete='CASCADE')
is_favorite = BooleanField(default=False)

class Meta:# pylint: disable=too-few-public-methods
    """Meta information for the Task model."""
    table_name = "task"
    database = database
    constraints = [
        Check('expiration_date IS NULL OR expiration_date >= date_of_creation')
    ]

class ChangeModel(Model):
"""Change table to log modifications in Task."""
id = AutoField(primary_key=True)
task = ForeignKeyField(TaskModel, backref='changes', on_delete='CASCADE')
timestamp = TimestampField(default=fn.CURRENT_TIMESTAMP)
field_changed = CharField(null=False)
old_value = TextField(null=True)
new_value = TextField(null=True)

class Meta:# pylint: disable=too-few-public-methods
    """Meta information for the Change model."""
    table_name = "change"
    database = database

File: FastAPI/app/config/pycache/settings.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/config/pycache/database.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/config/pycache/init.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/config/settings.py

"""
This module loads the environment configuration for the application.
Uses dotenv to load environment variables from a .env file.
Defines the database configuration based on the application's environment.

Variables:
ENV (str): Specifies the application environment. Defaults to 'dev' if not set.
DATABASE (dict): Dictionary containing the database configuration.
- name: Name of the database.
- engine: Database engine used.
- user: Username for the database connection.
- password: Password for the database connection.
- host: Host address of the database.
- port: Port used for the database connection.
"""

import os
from dotenv import load_dotenv
Load environment variables from the .env file.

load_dotenv()
Set environment to 'dev' by default if 'ENV' is not provided.

ENV = os.getenv("ENV", "dev")

if ENV == "production":
DATABASE = {
"name": os.getenv("MYSQL_DATABASE"),
"engine": "peewee.MySQLDatabase", # MySQL as the database engine
"user": os.getenv("MYSQL_USER"),
"password": os.getenv("MYSQL_PASSWORD"),
"host": os.getenv("MYSQL_HOST"),
"port": int(os.getenv("MYSQL_PORT")),
}
else:
DATABASE = {
"name": os.getenv("MYSQL_DATABASE"),
"engine": "peewee.MySQLDatabase", # MySQL as the database engine
"user": os.getenv("MYSQL_USER"),
"password": os.getenv("MYSQL_PASSWORD"),
"host": os.getenv("MYSQL_HOST"),
"port": int(os.getenv("MYSQL_PORT")),
}
Configuración de JWT

SECRET_KEY = os.getenv("SECRET_KEY")
ALGORITHM = os.getenv("ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
File: FastAPI/app/routes/init.py

"""
init.py
"""
from .auth_routes import router as auth_router
from .task_routes import router as task_router
from .user_management_routes import router as user_management_router

all = ["auth_router", "task_router", "user_management_router"]
File: FastAPI/app/routes/auth_routes.py

"""
Module defining authentication routes.

Includes endpoints for user registration and login,
providing JWT tokens for authentication.
"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Form
from pydantic import BaseModel, EmailStr
from datetime import timedelta
from config.settings import ACCESS_TOKEN_EXPIRE_MINUTES
from models.user import UserRead, UserCreate
from services.auth_service import AuthService
from services.user_service import UserService
from utils.dependencies import OAuth2PasswordRequestFormEmail

router = APIRouter(
tags=["Authentication"],
)

class Token(BaseModel):
access_token: str
token_type: str

class TokenData(BaseModel):
email: Optional[EmailStr] = None

@router.post("/register", response_model=UserRead, status_code=status.HTTP_201_CREATED)
def register(user: UserCreate):
existing_user = UserService.get_user_by_email(user.email)
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
hashed_password = AuthService.get_password_hash(user.password)
try:
new_user = UserService.create_user(
email=user.email,
password=hashed_password,
role_id=user.role_id
)
return new_user
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from e

@router.post("/login", response_model=Token)
def login(form_data: OAuth2PasswordRequestFormEmail = Depends()):
# Autenticación usando el email
user = AuthService.authenticate_user(form_data.email, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = AuthService.create_access_token(
data={"sub": user.email} # Guarda el email en el token
)
return {"access_token": access_token, "token_type": "bearer"}
File: FastAPI/app/routes/pycache/auth_routes.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/routes/pycache/task_routes.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/routes/pycache/user_management_routes.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/routes/pycache/init.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/routes/task_routes.py

"""
Module defining routes for managing tasks.

This module uses FastAPI to define routes that allow
creating, reading, updating, and deleting tasks through a REST API.
"""
from datetime import date
from typing import List, Optional
from fastapi import APIRouter, HTTPException, Depends, status
from models.user import UserRead
from models.task import Task, TaskUpdate, TaskCreate
from models.change import Change
from services.task_service import TaskService
from utils.dependencies import get_current_user

router = APIRouter(
prefix="/tasks",
tags=["tasks"],
)
@router.post("/", response_model=Task, status_code=status.HTTP_201_CREATED)
def create_task(
task_data: TaskCreate,
current_user: UserRead = Depends(get_current_user)
) -> Task:
task_model = TaskService.create_task(
title=task_data.title,
description=task_data.description,
expiration_date=task_data.expiration_date,
status_id=task_data.status_id,
user_id=current_user.id
)
# Convierte el modelo ORM a un modelo Pydantic
return Task.from_orm(task_model)

@router.get("/{task_id}", response_model=Task)
def get_task(
task_id: int,
current_user: UserRead = Depends(get_current_user)
) -> Task:
"""
Get a task by its ID.
"""
is_admin = current_user.role.name == "Administrator"
task = TaskService.get_task_by_id(task_id, user_id=current_user.id, is_admin=is_admin)
if task:
return Task.from_orm(task) # Convierte de ORM a modelo Pydantic
raise HTTPException(status_code=404, detail="Task not found")

@router.put("/{task_id}", response_model=Task)
def update_task(
task_id: int,
task_update: TaskUpdate,
current_user: UserRead = Depends(get_current_user) # Cambiado de User a UserRead
) -> Task:
"""Update an existing task."""
is_admin = current_user.role.name == "Administrator"
task = TaskService.update_task(
task_id=task_id,
user_id=current_user.id,
is_admin=is_admin,
**task_update.model_dump(exclude_unset=True)
)
if task:
return task
raise HTTPException(status_code=404, detail="Task not found")

@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_task(
task_id: int,
current_user: UserRead = Depends(get_current_user) # Cambiado de User a UserRead
):
"""
Delete a task by its ID.
"""
is_admin = current_user.role.name == "Administrator"
success = TaskService.delete_task(
task_id=task_id,
user_id=current_user.id,
is_admin=is_admin
)
if success:
return
raise HTTPException(status_code=404, detail="Task not found")

@router.get("/", response_model=List[Task])
def list_tasks(
task_status: Optional[str] = None,
expiration_date: Optional[date] = None,
current_user: UserRead = Depends(get_current_user) # Cambiado de User a UserRead
) -> List[Task]:
"""
List all tasks for the user with filtering options.
"""
is_admin = current_user.role.name == "Administrator"
tasks = TaskService.list_tasks(
user_id=current_user.id,
status=task_status,
expiration_date=expiration_date,
is_admin=is_admin
)
return tasks

@router.patch("/{task_id}/favorite", response_model=Task)
def toggle_favorite(
task_id: int,
current_user: UserRead = Depends(get_current_user) # Cambiado de User a UserRead
) -> Task:
"""
Toggle the favorite status of a task.
"""
is_admin = current_user.role.name == "Administrator"
task = TaskService.toggle_favorite(
task_id=task_id,
user_id=current_user.id,
is_admin=is_admin
)
if task:
return task
raise HTTPException(status_code=404, detail="Task not found")

@router.get("/{task_id}/changes", response_model=List[Change])
def get_task_changes(
task_id: int,
current_user: UserRead = Depends(get_current_user) # Cambiado de User a UserRead
) -> List[Change]:
"""
Get the change history of a specific task.
"""
is_admin = current_user.role.name == "Administrator"
task = TaskService.get_task_by_id(task_id, user_id=current_user.id, is_admin=is_admin)
if not task:
raise HTTPException(status_code=404, detail="Task not found")

changes = TaskService.get_changes_by_task_id(task_id)
return [Change.from_orm(change) for change in changes]

File: FastAPI/app/routes/user_management_routes.py

"""
Module that defines routes for managing users.

This module uses FastAPI to define the routes that allow
creating, reading, updating, and deleting users through a REST API.
"""
from typing import List
from fastapi import APIRouter, HTTPException, Depends, status
from models.user import UserRead, UserCreate, UserUpdate
from services.user_service import UserService
from utils.dependencies import get_current_admin

router = APIRouter(
prefix="/admin/users",
tags=["admin users"],
)
Helper function to verify admin permissions

def verify_admin(current_admin: UserRead): # Cambiar tipo a UserRead
"""Verifies if the user is an administrator."""
if current_admin.role.name != "Administrator":
raise HTTPException(status_code=403, detail="Insufficient permissions")

@router.get("/", response_model=List[UserRead])
def list_users() -> List[UserRead]:
"""List all registered users."""
return UserService.list_all_users()

@router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED)
def create_user(
user: UserCreate,
current_admin: UserRead = Depends(get_current_admin) # Cambiar tipo a UserRead
) -> UserRead:
# """Create a new user (for administrators only)."""
# verify_admin(current_admin)

try:
    created_user = UserService.create_user(
        email=user.email, password=user.password, role_id=user.role_id
    )
    return created_user
except ValueError as e:
    raise HTTPException(status_code=400, detail=str(e)) from e
except Exception as e:
    raise HTTPException(status_code=500, detail="Error creating user") from e

@router.get("/{user_id}", response_model=UserRead)
def get_user(
user_id: int,
current_admin: UserRead = Depends(get_current_admin) # Cambiar tipo a UserRead
) -> UserRead:
"""Get user information by their ID (for administrators only)."""
verify_admin(current_admin)

user = UserService.get_user_by_id(user_id)
if user:
    return user
raise HTTPException(status_code=404, detail="User not found")

@router.put("/{user_id}", response_model=UserRead)
def update_user(
user_id: int,
user_update: UserUpdate,
current_admin: UserRead = Depends(get_current_admin) # Cambiar tipo a UserRead
) -> UserRead:
"""Update existing user information (for administrators only)."""
verify_admin(current_admin)

updates = {}
if user_update.email is not None:
    updates['email'] = user_update.email
if user_update.password is not None:
    updates['password'] = user_update.password  # Hash the password before calling
if user_update.role_id is not None:
    updates['role_id'] = user_update.role_id

if not updates:
    raise HTTPException(status_code=400, detail="No data to update")

try:
    user = UserService.update_user(
        user_id=user_id,
        email=updates.get("email"),
        password=updates.get("password"),
        role_id=updates.get("role_id")
    )
    if user:
        return user
    raise HTTPException(status_code=404, detail="User not found")
except ValueError as e:
    raise HTTPException(status_code=400, detail=str(e)) from e

@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_user(
user_id: int,
current_admin: UserRead = Depends(get_current_admin) # Cambiar tipo a UserRead
):
"""Deletes a user by their ID (admin only)."""
verify_admin(current_admin)

success = UserService.delete_user(user_id)
if success:
    return
raise HTTPException(status_code=404, detail="User not found")

@router.patch("/{user_id}/active")
def toggle_user_active(
user_id: int,
current_admin: UserRead = Depends(get_current_admin) # Cambiar tipo a UserRead
):
"""Activate or deactivate a user (for administrators only)."""
verify_admin(current_admin)

user = UserService.get_user_by_id(user_id)
if not user:
    raise HTTPException(status_code=404, detail="User not found")

user.is_active = not user.is_active
# Ya que UserRead es de solo lectura, necesitas actualizar el modelo ORM directamente
UserService.update_user_status(user_id, user.is_active)
return {"message": f"User {user_id} is now {'active' if user.is_active else 'inactive'}"}

File: FastAPI/app/alembic.ini

Contenido binario o no legible.
File: FastAPI/app/utils/init.py
File: FastAPI/app/utils/pycache/dependencies.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/utils/pycache/init.cpython-312.pyc

Contenido binario o no legible.
File: FastAPI/app/utils/dependencies.py

"""
Security dependencies for authentication and authorization handling.
Includes functions to obtain the current user and verify roles.
"""
from fastapi import Depends, HTTPException, status, Form
from fastapi.security import OAuth2PasswordBearer

from models.user import UserRead
from services.auth_service import AuthService

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")

class OAuth2PasswordRequestFormEmail:
def init(self, email: str = Form(...), password: str = Form(...)):
self.email = email
self.password = password

def get_current_user(token: str = Depends(oauth2_scheme)) -> UserRead:
"""Retrieves the current user from the JWT token."""
user = AuthService.get_current_user(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user

def get_current_active_user(current_user: UserRead = Depends(get_current_user)) -> UserRead:
"""Verifies that the user is active."""
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user

def get_current_admin(current_user: UserRead = Depends(get_current_user)) -> UserRead:
"""Verifies if the user is an administrator."""
if current_user.role.name != "Administrator":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access forbidden: Administrator access required"
)
return current_user
File: FastAPI/alembic.ini

Contenido binario o no legible.