JSON WEBTOKENS!!
This commit is contained in:
371
JWT_AUTHENTICATION.md
Normal file
371
JWT_AUTHENTICATION.md
Normal 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
|
||||
@@ -8,3 +8,6 @@ pymongo
|
||||
python-dotenv
|
||||
pika
|
||||
Pillow
|
||||
PyJWT
|
||||
passlib[bcrypt]
|
||||
python-multipart
|
||||
|
||||
@@ -2,6 +2,7 @@ from domain.users import User
|
||||
from application.ports.user_repository import UserRepository
|
||||
from infrastructure.adapters.rabbitmq.sender import send_to_queue
|
||||
from infrastructure.adapters.rabbitmq.messages import UserMessage, UserEventType
|
||||
from infrastructure.api.users.auth_service import auth_service
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
import re
|
||||
@@ -14,7 +15,8 @@ class CreateUser:
|
||||
self.repo = repo
|
||||
|
||||
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]:
|
||||
"""
|
||||
Sends a create user message to RabbitMQ.
|
||||
@@ -25,6 +27,15 @@ class CreateUser:
|
||||
- Email no duplicado
|
||||
- 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:
|
||||
Dictionary with status and message
|
||||
"""
|
||||
@@ -69,12 +80,19 @@ class CreateUser:
|
||||
|
||||
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
|
||||
message = UserMessage(
|
||||
event_type=UserEventType.CREATE,
|
||||
nombre=nombre.strip(),
|
||||
apellido=apellido.strip(),
|
||||
email=email.strip(),
|
||||
contraseña_hash=contraseña_hash,
|
||||
fecha_nacimiento=fecha_nacimiento.isoformat(),
|
||||
fecha_creacion=fecha_creacion.isoformat(),
|
||||
calificacion=50.0,
|
||||
|
||||
@@ -69,6 +69,7 @@ class UserConsumer:
|
||||
nombre=message.nombre,
|
||||
apellido=message.apellido,
|
||||
email=message.email,
|
||||
contraseña_hash=message.contraseña_hash,
|
||||
fecha_nacimiento=fecha_nacimiento,
|
||||
fecha_creacion=fecha_creacion,
|
||||
calificacion=message.calificacion,
|
||||
|
||||
@@ -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_title: str = "VoxPopuli Microservices"
|
||||
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)"
|
||||
)
|
||||
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"
|
||||
)
|
||||
images_allowed_types: list = Field(
|
||||
@@ -53,7 +67,7 @@ class Settings(BaseSettings):
|
||||
description="Tipos MIME permitidos para imágenes"
|
||||
)
|
||||
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)"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ class User:
|
||||
nombre: str
|
||||
apellido: str
|
||||
email: str
|
||||
fecha_nacimiento: datetime
|
||||
fecha_creacion: datetime
|
||||
calificacion: float # 0-100
|
||||
numero_reportes: int
|
||||
url_foto_perfil: Optional[str]
|
||||
biografia: Optional[str]
|
||||
contraseña_hash: Optional[str] = None # Hash bcrypt de la contraseña
|
||||
fecha_nacimiento: datetime = None
|
||||
fecha_creacion: datetime = None
|
||||
calificacion: float = 50.0 # 0-100
|
||||
numero_reportes: int = 0
|
||||
url_foto_perfil: Optional[str] = None
|
||||
biografia: Optional[str] = None
|
||||
|
||||
@@ -10,6 +10,7 @@ class UserModel(Base):
|
||||
nombre = 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)
|
||||
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_creacion = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
calificacion = Column(Float, default=50.0, nullable=False) # 0-100
|
||||
|
||||
@@ -20,6 +20,7 @@ class UserRepositorySQL(UserRepository):
|
||||
nombre=user.nombre,
|
||||
apellido=user.apellido,
|
||||
email=user.email,
|
||||
contraseña_hash=user.contraseña_hash,
|
||||
fecha_nacimiento=user.fecha_nacimiento,
|
||||
fecha_creacion=user.fecha_creacion,
|
||||
calificacion=user.calificacion,
|
||||
@@ -59,6 +60,15 @@ class UserRepositorySQL(UserRepository):
|
||||
logger.error(f"Error al buscar usuario por email {email}: {e}", exc_info=True)
|
||||
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]:
|
||||
"""Obtiene todos los usuarios"""
|
||||
try:
|
||||
|
||||
@@ -28,6 +28,7 @@ class UserMessage:
|
||||
nombre: Optional[str] = None
|
||||
apellido: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
contraseña_hash: Optional[str] = None
|
||||
fecha_nacimiento: Optional[str] = None # ISO format datetime string
|
||||
fecha_creacion: Optional[str] = None # ISO format datetime string
|
||||
calificacion: Optional[float] = None
|
||||
|
||||
103
src/infrastructure/api/users/auth_service.py
Normal file
103
src/infrastructure/api/users/auth_service.py
Normal 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()
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@@ -7,6 +7,10 @@ class UserCreateRequest(BaseModel):
|
||||
nombre: str
|
||||
apellido: 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
|
||||
url_foto_perfil: Optional[str] = None
|
||||
biografia: Optional[str] = None
|
||||
@@ -18,6 +22,18 @@ class UserUpdateRequest(BaseModel):
|
||||
url_foto_perfil: 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):
|
||||
"""Respuesta con datos de usuario"""
|
||||
user_id: int
|
||||
@@ -33,3 +49,4 @@ class UserResponse(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@@ -1,24 +1,99 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from infrastructure.api.users.schemas import UserCreateRequest, UserUpdateRequest, UserResponse
|
||||
from fastapi import APIRouter, HTTPException, status, Depends
|
||||
from infrastructure.api.users.schemas import (
|
||||
UserCreateRequest, UserUpdateRequest, UserResponse,
|
||||
UserLoginRequest, UserLoginResponse
|
||||
)
|
||||
from application.services.user_services import (
|
||||
CreateUser, GetUserById, GetUserByEmail, ListAllUsers, UpdateUser, DeleteUser
|
||||
)
|
||||
from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL
|
||||
from infrastructure.api.users.auth_service import auth_service
|
||||
import logging
|
||||
|
||||
router = APIRouter()
|
||||
user_repo = UserRepositorySQL()
|
||||
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)
|
||||
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:
|
||||
create_use_case = CreateUser(user_repo)
|
||||
result = create_use_case.execute(
|
||||
nombre=user_data.nombre,
|
||||
apellido=user_data.apellido,
|
||||
email=user_data.email,
|
||||
contraseña=user_data.contraseña,
|
||||
fecha_nacimiento=user_data.fecha_nacimiento,
|
||||
url_foto_perfil=user_data.url_foto_perfil,
|
||||
biografia=user_data.biografia
|
||||
|
||||
Reference in New Issue
Block a user