JSON WEBTOKENS!!

This commit is contained in:
2026-04-26 16:24:29 -06:00
parent 30efa0e098
commit 0e85231bae
12 changed files with 628 additions and 13 deletions

371
JWT_AUTHENTICATION.md Normal file
View File

@@ -0,0 +1,371 @@
# JWT Authentication for VoxPopuli Users API
## Overview
La API de Usuarios de VoxPopuli ahora incluye autenticación basada en **JSON Web Tokens (JWT)**. Esta documentación describe cómo usar e implementar esta característica.
## Features
**Autenticación segura con JWT** - Tokens firmados con HS256
**Hashing de contraseñas** - Usando bcrypt para máxima seguridad
**Contraseña por defecto** - "passwd123" para usuarios sin contraseña especificada
**Expiración configurable** - Tokens con tiempo de vida configurable (default: 24 horas)
## Configuration
### Environment Variables
Agregue las siguientes variables al archivo `.env`:
```env
# JWT Configuration
JWT_SECRET_KEY=your-super-secret-key-change-in-production
JWT_ALGORITHM=HS256
JWT_EXPIRATION_HOURS=24
```
**Importante:** En producción, cambiar `JWT_SECRET_KEY` por una clave segura y aleatoria.
### Installation
Se han agregado las siguientes dependencias a `requirements.txt`:
```
PyJWT # Para crear y verificar JWT tokens
passlib[bcrypt] # Para hashing seguro de contraseñas
python-multipart # Para manejo de formularios en login
```
Para instalar las dependencias:
```bash
pip install -r requirements.txt
```
## API Endpoints
### 1. Crear Usuario (Registro)
**POST** `/api/v1/users/`
Crea un nuevo usuario. La contraseña es **opcional**; si no se proporciona, se usa "passwd123" por defecto.
#### Request Body
```json
{
"nombre": "Juan",
"apellido": "Pérez",
"email": "juan@example.com",
"contraseña": "miContraseñaSegura123", // Opcional
"fecha_nacimiento": "1990-01-15T00:00:00",
"url_foto_perfil": "https://example.com/foto.jpg", // Opcional
"biografia": "Soy reportero comunitario" // Opcional
}
```
#### Response (202 Accepted)
```json
{
"status": "queued",
"message": "Usuario enviado a cola para procesamiento",
"email": "juan@example.com"
}
```
#### Casos de Error
- **400 Bad Request**: Validación fallida (email inválido, campos vacíos)
- **409 Conflict**: Email ya está registrado
---
### 2. Login de Usuario (Autenticación)
**POST** `/api/v1/users/login`
Autentica un usuario y retorna un JWT token.
#### Request Body
```json
{
"email": "juan@example.com",
"contraseña": "miContraseñaSegura123"
}
```
#### Response (200 OK)
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"user_id": 1,
"email": "juan@example.com"
}
```
#### Casos de Error
- **401 Unauthorized**: Email o contraseña incorrectos
---
## JWT Token Usage
### Token Structure
Cada token JWT contiene el siguiente payload:
```json
{
"user_id": 1,
"email": "juan@example.com",
"exp": 1704067200, // Unix timestamp de expiración
"iat": 1703980800 // Unix timestamp de creación
}
```
### Authorization Header
Para usar el token en requests a endpoints protegidos:
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
### Example cURL
```bash
curl -X POST "http://localhost:8000/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{
"email": "juan@example.com",
"contraseña": "miContraseñaSegura123"
}'
# Respuesta:
# {
# "access_token": "eyJhbGc...",
# "token_type": "bearer",
# "user_id": 1,
# "email": "juan@example.com"
# }
```
---
## Password Security
### Password Hashing
Las contraseñas se hashean usando **bcrypt** antes de guardarse en la base de datos:
1. Usuario envía contraseña en texto plano
2. Se hashea con bcrypt (rounds=12 por defecto)
3. Se almacena solo el hash en la base de datos
4. Durante login, se verifica comparando el hash
### Default Password
Si un usuario no especifica contraseña durante el registro:
```
Contraseña por defecto: "passwd123"
Hash bcrypt: $2b$12$R9h7cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ee3GzxQ2n8/N7kDi
```
---
## Database Schema
El modelo de usuario se ha actualizado con:
```sql
ALTER TABLE usuarios ADD COLUMN contraseña_hash VARCHAR(255) NOT NULL DEFAULT '$2b$12$R9h7cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ee3GzxQ2n8/N7kDi';
```
Campos relevantes:
| Campo | Tipo | Descripción |
|-------|------|-------------|
| `user_id` | INT | Identificador único (PK) |
| `email` | VARCHAR(255) | Email único |
| `contraseña_hash` | VARCHAR(255) | Hash bcrypt de la contraseña |
| `fecha_creacion` | DATETIME | Fecha de registro |
---
## Services Architecture
### AuthService (`src/infrastructure/api/users/auth_service.py`)
Responsable de:
- **hash_password()** - Hashea contraseñas con bcrypt
- **verify_password()** - Verifica contraseña contra hash
- **create_access_token()** - Genera JWT tokens
- **verify_token()** - Valida y decodifica tokens
- **get_default_password_hash()** - Retorna hash de "passwd123"
### Example Usage
```python
from infrastructure.api.users.auth_service import auth_service
# Hashear contraseña
hashed = auth_service.hash_password("miContraseña")
# Verificar contraseña
is_valid = auth_service.verify_password("miContraseña", hashed)
# Crear token
token = auth_service.create_access_token(user_id=1, email="user@example.com")
# Verificar token
payload = auth_service.verify_token(token)
# payload = {"user_id": 1, "email": "user@example.com", "exp": ..., "iat": ...}
```
---
## RabbitMQ Message Format
El mensaje de creación de usuario ahora incluye el hash de contraseña:
```python
@dataclass
class UserMessage:
event_type: UserEventType
user_id: Optional[int] = None
nombre: Optional[str] = None
apellido: Optional[str] = None
email: Optional[str] = None
contraseña_hash: Optional[str] = None # ✨ Nuevo
fecha_nacimiento: Optional[str] = None
fecha_creacion: Optional[str] = None
# ... campos adicionales
```
---
## Example Workflow
### 1. Registrar usuario
```bash
curl -X POST "http://localhost:8000/api/v1/users/" \
-H "Content-Type: application/json" \
-d '{
"nombre": "Carlos",
"apellido": "López",
"email": "carlos@example.com",
"contraseña": "MiPass@2024",
"fecha_nacimiento": "1995-06-20T00:00:00"
}'
```
### 2. Login
```bash
curl -X POST "http://localhost:8000/api/v1/users/login" \
-H "Content-Type: application/json" \
-d '{
"email": "carlos@example.com",
"contraseña": "MiPass@2024"
}'
# Response:
# {
# "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
# "token_type": "bearer",
# "user_id": 1,
# "email": "carlos@example.com"
# }
```
### 3. Usar token
```bash
curl -X GET "http://localhost:8000/api/v1/users/1" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
---
## Security Best Practices
### ✅ En Desarrollo
- Se proporciona `JWT_SECRET_KEY` por defecto para facilitar testing
- Cambiar en `.env` si es necesario
### ✅ En Producción
1. **Cambiar `JWT_SECRET_KEY`**
```bash
# Generar clave aleatoria
python -c "import secrets; print(secrets.token_urlsafe(32))"
```
2. **Usar HTTPS**
- Los tokens deben viajar solo por HTTPS
3. **Secrets Management**
- Usar servicios como AWS Secrets Manager, HashiCorp Vault
- No guardar secretos en código
4. **Token Rotation**
- Considerar refresh tokens con corta expiración
- Implementar revocation list (blacklist)
5. **CORS Configuration**
- Configurar CORS para permitir solo dominios autorizados
---
## Troubleshooting
### Error: "Token expired"
El token tiene más de 24 horas (configurable). Hacer login nuevamente.
### Error: "Email o contraseña incorrectos"
Verificar:
- El email existe en la base de datos
- La contraseña es correcta
- El usuario ha sido procesado por el consumer (status "queued" → guardado)
### Error: "Secret key not found"
Asegurar que `JWT_SECRET_KEY` está configurado en:
1. `.env` file
2. Variable de entorno del sistema
3. O usando el default en `core/config.py`
---
## Future Enhancements
- [ ] Refresh tokens
- [ ] Token blacklist para logout
- [ ] Multi-factor authentication (MFA)
- [ ] OAuth2 integration
- [ ] Rate limiting en endpoint de login
- [ ] Account lockout después de N intentos fallidos
---
## References
- [JWT.io](https://jwt.io) - JWT documentation
- [PyJWT Documentation](https://pyjwt.readthedocs.io/)
- [Passlib Documentation](https://passlib.readthedocs.io/)
- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/)
---
**Última actualización:** 26 de abril de 2026

View File

@@ -8,3 +8,6 @@ pymongo
python-dotenv python-dotenv
pika pika
Pillow Pillow
PyJWT
passlib[bcrypt]
python-multipart

View File

@@ -2,6 +2,7 @@ from domain.users import User
from application.ports.user_repository import UserRepository from application.ports.user_repository import UserRepository
from infrastructure.adapters.rabbitmq.sender import send_to_queue from infrastructure.adapters.rabbitmq.sender import send_to_queue
from infrastructure.adapters.rabbitmq.messages import UserMessage, UserEventType from infrastructure.adapters.rabbitmq.messages import UserMessage, UserEventType
from infrastructure.api.users.auth_service import auth_service
from datetime import datetime from datetime import datetime
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
import re import re
@@ -14,7 +15,8 @@ class CreateUser:
self.repo = repo self.repo = repo
def execute(self, nombre: str, apellido: str, email: str, def execute(self, nombre: str, apellido: str, email: str,
fecha_nacimiento: datetime, url_foto_perfil: Optional[str] = None, fecha_nacimiento: datetime, contraseña: Optional[str] = None,
url_foto_perfil: Optional[str] = None,
biografia: Optional[str] = None) -> Dict[str, Any]: biografia: Optional[str] = None) -> Dict[str, Any]:
""" """
Sends a create user message to RabbitMQ. Sends a create user message to RabbitMQ.
@@ -25,6 +27,15 @@ class CreateUser:
- Email no duplicado - Email no duplicado
- Campos no vacíos - Campos no vacíos
Args:
nombre: Nombre del usuario
apellido: Apellido del usuario
email: Email del usuario
fecha_nacimiento: Fecha de nacimiento
contraseña: Contraseña (opcional, se usa 'passwd123' por defecto)
url_foto_perfil: URL de la foto de perfil
biografia: Biografía del usuario
Returns: Returns:
Dictionary with status and message Dictionary with status and message
""" """
@@ -69,12 +80,19 @@ class CreateUser:
fecha_creacion = datetime.now() fecha_creacion = datetime.now()
# Hash de contraseña: usar la proporcionada o la por defecto
if contraseña:
contraseña_hash = auth_service.hash_password(contraseña)
else:
contraseña_hash = auth_service.get_default_password_hash()
# Create message object # Create message object
message = UserMessage( message = UserMessage(
event_type=UserEventType.CREATE, event_type=UserEventType.CREATE,
nombre=nombre.strip(), nombre=nombre.strip(),
apellido=apellido.strip(), apellido=apellido.strip(),
email=email.strip(), email=email.strip(),
contraseña_hash=contraseña_hash,
fecha_nacimiento=fecha_nacimiento.isoformat(), fecha_nacimiento=fecha_nacimiento.isoformat(),
fecha_creacion=fecha_creacion.isoformat(), fecha_creacion=fecha_creacion.isoformat(),
calificacion=50.0, calificacion=50.0,

View File

@@ -69,6 +69,7 @@ class UserConsumer:
nombre=message.nombre, nombre=message.nombre,
apellido=message.apellido, apellido=message.apellido,
email=message.email, email=message.email,
contraseña_hash=message.contraseña_hash,
fecha_nacimiento=fecha_nacimiento, fecha_nacimiento=fecha_nacimiento,
fecha_creacion=fecha_creacion, fecha_creacion=fecha_creacion,
calificacion=message.calificacion, calificacion=message.calificacion,

View File

@@ -26,6 +26,20 @@ class Settings(BaseSettings):
) )
# JWT Configuration
jwt_secret_key: str = Field(
default=os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production"),
description="Clave secreta para firmar JWT tokens"
)
jwt_algorithm: str = Field(
default="HS256",
description="Algoritmo para firmar JWT tokens"
)
jwt_expiration_hours: int = Field(
default=int(os.getenv("JWT_EXPIRATION_HOURS", "24")),
description="Horas de expiración del JWT token"
)
# API # API
api_title: str = "VoxPopuli Microservices" api_title: str = "VoxPopuli Microservices"
api_version: str = "1.0.0" api_version: str = "1.0.0"
@@ -45,7 +59,7 @@ class Settings(BaseSettings):
description="Directorio para imágenes de reportes (dentro de storage_base_path)" description="Directorio para imágenes de reportes (dentro de storage_base_path)"
) )
images_max_size_mb: int = Field( images_max_size_mb: int = Field(
default=int(os.getenv("IMAGES_MAX_SIZE_MB", 4)), default=int(os.getenv("IMAGES_MAX_SIZE_MB", "4")),
description="Tamaño máximo de imagen en MB" description="Tamaño máximo de imagen en MB"
) )
images_allowed_types: list = Field( images_allowed_types: list = Field(
@@ -53,7 +67,7 @@ class Settings(BaseSettings):
description="Tipos MIME permitidos para imágenes" description="Tipos MIME permitidos para imágenes"
) )
images_compression_quality: int = Field( images_compression_quality: int = Field(
default=int(os.getenv("IMAGES_COMPRESSION_QUALITY", 80)), default=int(os.getenv("IMAGES_COMPRESSION_QUALITY", "80")),
description="Calidad de compresión WebP (0-100)" description="Calidad de compresión WebP (0-100)"
) )

View File

@@ -9,9 +9,10 @@ class User:
nombre: str nombre: str
apellido: str apellido: str
email: str email: str
fecha_nacimiento: datetime contraseña_hash: Optional[str] = None # Hash bcrypt de la contraseña
fecha_creacion: datetime fecha_nacimiento: datetime = None
calificacion: float # 0-100 fecha_creacion: datetime = None
numero_reportes: int calificacion: float = 50.0 # 0-100
url_foto_perfil: Optional[str] numero_reportes: int = 0
biografia: Optional[str] url_foto_perfil: Optional[str] = None
biografia: Optional[str] = None

View File

@@ -10,6 +10,7 @@ class UserModel(Base):
nombre = Column(String(100), nullable=False, index=True) nombre = Column(String(100), nullable=False, index=True)
apellido = Column(String(100), nullable=False, index=True) apellido = Column(String(100), nullable=False, index=True)
email = Column(String(255), unique=True, nullable=False, index=True) email = Column(String(255), unique=True, nullable=False, index=True)
contraseña_hash = Column(String(255), nullable=False, default="$2b$12$R9h7cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ee3GzxQ2n8/N7kDi") # Hash de "passwd123"
fecha_nacimiento = Column(DateTime, nullable=False) fecha_nacimiento = Column(DateTime, nullable=False)
fecha_creacion = Column(DateTime, default=datetime.utcnow, nullable=False) fecha_creacion = Column(DateTime, default=datetime.utcnow, nullable=False)
calificacion = Column(Float, default=50.0, nullable=False) # 0-100 calificacion = Column(Float, default=50.0, nullable=False) # 0-100

View File

@@ -20,6 +20,7 @@ class UserRepositorySQL(UserRepository):
nombre=user.nombre, nombre=user.nombre,
apellido=user.apellido, apellido=user.apellido,
email=user.email, email=user.email,
contraseña_hash=user.contraseña_hash,
fecha_nacimiento=user.fecha_nacimiento, fecha_nacimiento=user.fecha_nacimiento,
fecha_creacion=user.fecha_creacion, fecha_creacion=user.fecha_creacion,
calificacion=user.calificacion, calificacion=user.calificacion,
@@ -59,6 +60,15 @@ class UserRepositorySQL(UserRepository):
logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True) logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True)
raise raise
def find_by_email_with_password(self, email: str) -> Optional[UserModel]:
"""Obtiene un usuario por email incluyendo el hash de contraseña (para autenticación)"""
try:
db_user = self.db.query(UserModel).filter(UserModel.email == email).first()
return db_user
except Exception as e:
logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True)
raise
def find_all(self) -> List[User]: def find_all(self) -> List[User]:
"""Obtiene todos los usuarios""" """Obtiene todos los usuarios"""
try: try:

View File

@@ -28,6 +28,7 @@ class UserMessage:
nombre: Optional[str] = None nombre: Optional[str] = None
apellido: Optional[str] = None apellido: Optional[str] = None
email: Optional[str] = None email: Optional[str] = None
contraseña_hash: Optional[str] = None
fecha_nacimiento: Optional[str] = None # ISO format datetime string fecha_nacimiento: Optional[str] = None # ISO format datetime string
fecha_creacion: Optional[str] = None # ISO format datetime string fecha_creacion: Optional[str] = None # ISO format datetime string
calificacion: Optional[float] = None calificacion: Optional[float] = None

View File

@@ -0,0 +1,103 @@
import jwt
from datetime import datetime, timedelta
from passlib.context import CryptContext
from typing import Optional, Dict
from core.config import ConfSettings
# Configurar contexto para hashing de contraseñas
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class AuthService:
"""Servicio de autenticación con JWT y hashing de contraseñas"""
def __init__(self):
self.secret_key = ConfSettings.jwt_secret_key
self.algorithm = ConfSettings.jwt_algorithm
self.expiration_hours = ConfSettings.jwt_expiration_hours
def hash_password(self, password: str) -> str:
"""
Hashea una contraseña usando bcrypt
Args:
password: Contraseña en texto plano
Returns:
Hash bcrypt de la contraseña
"""
return pwd_context.hash(password)
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
"""
Verifica si una contraseña coincide con su hash
Args:
plain_password: Contraseña en texto plano
hashed_password: Hash bcrypt para verificar
Returns:
True si la contraseña es correcta, False en caso contrario
"""
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(self, user_id: int, email: str) -> str:
"""
Crea un JWT token de acceso
Args:
user_id: ID del usuario
email: Email del usuario
Returns:
Token JWT firmado
"""
payload = {
"user_id": user_id,
"email": email,
"exp": datetime.utcnow() + timedelta(hours=self.expiration_hours),
"iat": datetime.utcnow()
}
token = jwt.encode(
payload,
self.secret_key,
algorithm=self.algorithm
)
return token
def verify_token(self, token: str) -> Optional[Dict]:
"""
Verifica y decodifica un JWT token
Args:
token: Token JWT a verificar
Returns:
Payload decodificado si el token es válido, None en caso contrario
"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
return payload
except jwt.ExpiredSignatureError:
return None # Token expirado
except jwt.InvalidTokenError:
return None # Token inválido
def get_default_password_hash(self) -> str:
"""
Retorna el hash de la contraseña por defecto 'passwd123'
Returns:
Hash bcrypt de 'passwd123'
"""
return self.hash_password("passwd123")
# Instancia global
auth_service = AuthService()

View File

@@ -1,4 +1,4 @@
from pydantic import BaseModel from pydantic import BaseModel, Field
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@@ -7,6 +7,10 @@ class UserCreateRequest(BaseModel):
nombre: str nombre: str
apellido: str apellido: str
email: str email: str
contraseña: Optional[str] = Field(
default=None,
description="Contraseña del usuario. Si no se proporciona, se usa 'passwd123' por defecto"
)
fecha_nacimiento: datetime fecha_nacimiento: datetime
url_foto_perfil: Optional[str] = None url_foto_perfil: Optional[str] = None
biografia: Optional[str] = None biografia: Optional[str] = None
@@ -18,6 +22,18 @@ class UserUpdateRequest(BaseModel):
url_foto_perfil: Optional[str] = None url_foto_perfil: Optional[str] = None
biografia: Optional[str] = None biografia: Optional[str] = None
class UserLoginRequest(BaseModel):
"""Solicitud para login de usuario"""
email: str = Field(..., description="Email del usuario")
contraseña: str = Field(..., description="Contraseña del usuario")
class UserLoginResponse(BaseModel):
"""Respuesta de login con token JWT"""
access_token: str = Field(..., description="Token JWT de acceso")
token_type: str = Field(default="bearer", description="Tipo de token")
user_id: int = Field(..., description="ID del usuario")
email: str = Field(..., description="Email del usuario")
class UserResponse(BaseModel): class UserResponse(BaseModel):
"""Respuesta con datos de usuario""" """Respuesta con datos de usuario"""
user_id: int user_id: int
@@ -33,3 +49,4 @@ class UserResponse(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -1,24 +1,99 @@
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status, Depends
from infrastructure.api.users.schemas import UserCreateRequest, UserUpdateRequest, UserResponse from infrastructure.api.users.schemas import (
UserCreateRequest, UserUpdateRequest, UserResponse,
UserLoginRequest, UserLoginResponse
)
from application.services.user_services import ( from application.services.user_services import (
CreateUser, GetUserById, GetUserByEmail, ListAllUsers, UpdateUser, DeleteUser CreateUser, GetUserById, GetUserByEmail, ListAllUsers, UpdateUser, DeleteUser
) )
from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL
from infrastructure.api.users.auth_service import auth_service
import logging import logging
router = APIRouter() router = APIRouter()
user_repo = UserRepositorySQL() user_repo = UserRepositorySQL()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@router.post("/login", response_model=UserLoginResponse, status_code=status.HTTP_200_OK)
async def login_user(credentials: UserLoginRequest):
"""
Autentica un usuario y retorna un token JWT
**Parámetros:**
- email: Email del usuario
- contraseña: Contraseña del usuario
**Retorna:**
- access_token: Token JWT para usar en requests autenticados
- token_type: Tipo de token (bearer)
- user_id: ID del usuario
- email: Email confirmado
"""
try:
# Obtener usuario por email
get_use_case = GetUserByEmail(user_repo)
user = get_use_case.execute(credentials.email)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email o contraseña incorrectos",
headers={"WWW-Authenticate": "Bearer"}
)
# Verificar contraseña
# Necesitamos obtener el hash de contraseña del modelo
user_model = user_repo.find_by_email_with_password(credentials.email)
if not user_model or not auth_service.verify_password(credentials.contraseña, user_model.contraseña_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email o contraseña incorrectos",
headers={"WWW-Authenticate": "Bearer"}
)
# Crear token JWT
access_token = auth_service.create_access_token(
user_id=user.user_id,
email=user.email
)
return {
"access_token": access_token,
"token_type": "bearer",
"user_id": user.user_id,
"email": user.email
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error en login_user: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error interno del servidor"
)
@router.post("/", status_code=status.HTTP_202_ACCEPTED) @router.post("/", status_code=status.HTTP_202_ACCEPTED)
async def create_user(user_data: UserCreateRequest): async def create_user(user_data: UserCreateRequest):
"""Crea un nuevo usuario - envía a cola de procesamiento con validaciones previas""" """
Crea un nuevo usuario - envía a cola de procesamiento con validaciones previas
**Parámetros:**
- nombre: Nombre del usuario (requerido)
- apellido: Apellido del usuario (requerido)
- email: Email del usuario (requerido)
- contraseña: Contraseña (opcional, por defecto "passwd123")
- fecha_nacimiento: Fecha de nacimiento (requerido)
- url_foto_perfil: URL de la foto de perfil (opcional)
- biografia: Biografía del usuario (opcional)
"""
try: try:
create_use_case = CreateUser(user_repo) create_use_case = CreateUser(user_repo)
result = create_use_case.execute( result = create_use_case.execute(
nombre=user_data.nombre, nombre=user_data.nombre,
apellido=user_data.apellido, apellido=user_data.apellido,
email=user_data.email, email=user_data.email,
contraseña=user_data.contraseña,
fecha_nacimiento=user_data.fecha_nacimiento, fecha_nacimiento=user_data.fecha_nacimiento,
url_foto_perfil=user_data.url_foto_perfil, url_foto_perfil=user_data.url_foto_perfil,
biografia=user_data.biografia biografia=user_data.biografia