From b4fc640c1a2927ce54d31da479340095c618fb97 Mon Sep 17 00:00:00 2001 From: "Juan M. Ley" Date: Mon, 4 May 2026 22:07:30 -0600 Subject: [PATCH] stuffies --- ADMIN_SETUP.md | 53 +++++++++++++++++ METRICS_API_EXAMPLES.md | 67 ++++++++++++++++++---- MODERATION_API_EXAMPLES.json | 102 +++++++++++++++++++++++++++------ requirements.txt | 1 + src/infrastructure/api/auth.py | 49 ++++++++++++++++ 5 files changed, 245 insertions(+), 27 deletions(-) create mode 100644 ADMIN_SETUP.md create mode 100644 src/infrastructure/api/auth.py diff --git a/ADMIN_SETUP.md b/ADMIN_SETUP.md new file mode 100644 index 0000000..581cfdd --- /dev/null +++ b/ADMIN_SETUP.md @@ -0,0 +1,53 @@ +# Admin User Setup - SQL Commands + +## 1. Agregar columna a BD (Auto-ejecutado) +La migración SQL se encuentra en: `migrations/001_add_is_admin_to_usuarios.sql` + +Ejecutar manualmente si es necesario: +```sql +ALTER TABLE `voxpopuli_users`.`usuarios` +ADD COLUMN `is_admin` BOOLEAN NOT NULL DEFAULT FALSE AFTER `biografia`, +ADD INDEX `idx_is_admin` (`is_admin`); +``` + +## 2. Promover usuario a admin +```sql +UPDATE `voxpopuli_users`.`usuarios` +SET `is_admin` = TRUE +WHERE `user_id` = 1; +``` + +## 3. Listar usuarios admin +```sql +SELECT `user_id`, `nombre`, `email`, `is_admin` +FROM `voxpopuli_users`.`usuarios` +WHERE `is_admin` = TRUE; +``` + +## 4. Revocar permisos admin +```sql +UPDATE `voxpopuli_users`.`usuarios` +SET `is_admin` = FALSE +WHERE `user_id` = 1; +``` + +## API Usage + +### Endpoints de Moderación (requieren token JWT de admin) +```bash +# Usar token JWT en header Authorization +curl -X POST "http://localhost:8003/moderation/reports/delete" \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "report_id": 123, + "reason": "Contenido ofensivo", + "description": "Violación de políticas" + }' +``` + +### Obtener usuario y verificar is_admin +```bash +GET http://localhost:8000/users/{user_id} +# Response incluye "is_admin": true/false +``` diff --git a/METRICS_API_EXAMPLES.md b/METRICS_API_EXAMPLES.md index 365dc80..0c5befa 100644 --- a/METRICS_API_EXAMPLES.md +++ b/METRICS_API_EXAMPLES.md @@ -1,6 +1,6 @@ # Metrics API Examples -## Registrar métrica manualmente +## Record Metric (Manual) ```bash curl -X POST "http://localhost:8004/metrics/record" \ -H "Content-Type: application/json" \ @@ -13,29 +13,74 @@ curl -X POST "http://localhost:8004/metrics/record" \ }' ``` -## Obtener reporte de últimos 7 días +## Get Analytics Report (Last N Days) ```bash curl "http://localhost:8004/metrics/report?days=7" ``` -## Obtener estadísticas de un día específico +Response: +```json +{ + "start_date": "2026-04-27T00:00:00", + "end_date": "2026-05-04T23:59:59", + "total_events": 245, + "events_by_type": { + "user_created": 12, + "report_created": 45, + "notification_sent": 188 + } +} +``` + +## Get Daily Statistics ```bash curl "http://localhost:8004/metrics/daily-stats?date=2026-05-04T00:00:00" ``` -## Obtener resumen de eventos en rango +Response: +```json +[ + { + "date": "2026-05-04T00:00:00", + "event_type": "user_created", + "count": 3 + }, + { + "date": "2026-05-04T00:00:00", + "event_type": "report_created", + "count": 8 + } +] +``` + +## Get Event Summary (Date Range) ```bash curl "http://localhost:8004/metrics/summary?start_date=2026-04-27T00:00:00&end_date=2026-05-04T23:59:59" ``` -## Health check +Response: +```json +{ + "summary": { + "user_created": 12, + "report_created": 45, + "notification_sent": 188, + "moderation_completed": 5 + }, + "date_range": { + "start_date": "2026-04-27T00:00:00", + "end_date": "2026-05-04T23:59:59" + } +} +``` + +## Health Check ```bash curl "http://localhost:8004/metrics/health" ``` -## Eventos automáticos (RabbitMQ) -El consumer de métricas escucha automáticamente eventos de: -- Users Queue: `user_created`, `user_updated`, `user_deleted` -- Reports Queue: `report_created`, `report_resolved` -- Notifications Queue: `notification_sent` -- Moderations Queue: `moderation_completed` +## Automatic Events (RabbitMQ) +- `user_created`, `user_updated`, `user_deleted` +- `report_created`, `report_resolved` +- `notification_sent` +- `moderation_completed` diff --git a/MODERATION_API_EXAMPLES.json b/MODERATION_API_EXAMPLES.json index dfd4d37..697f206 100644 --- a/MODERATION_API_EXAMPLES.json +++ b/MODERATION_API_EXAMPLES.json @@ -1,13 +1,13 @@ { "moderation_examples": { "delete_report": { - "description": "Eliminar un reporte inapropiado", + "description": "Eliminar un reporte (requiere token JWT de admin)", "endpoint": "POST /moderation/reports/delete", "headers": { - "Content-Type": "application/json" + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_JWT_TOKEN" }, "request_body": { - "moderator_id": 1, "report_id": "550e8400-e29b-41d4-a716-446655440000", "reason": "Contenido violento", "description": "La imagen contiene violencia explícita" @@ -16,40 +16,110 @@ "status": 200, "body": { "status": "success", - "message": "Reporte marcado para eliminación", + "message": "Reporte eliminado", "action_id": "action-uuid-123" } }, - "response_error": { - "status": 400, - "body": { - "detail": "Razón debe tener al menos 5 caracteres" - } + "response_error_401": { + "status": 401, + "body": {"detail": "Token inválido o expirado"} + }, + "response_error_403": { + "status": 403, + "body": {"detail": "Permisos insuficientes"} } }, - "close_account": { - "description": "Cerrar una cuenta de usuario por violación de términos", + "description": "Cerrar cuenta de usuario (requiere token JWT de admin)", "endpoint": "POST /moderation/accounts/close", "headers": { - "Content-Type": "application/json" + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_JWT_TOKEN" }, "request_body": { - "moderator_id": 1, "user_id": 42, - "reason": "Violación grave de términos de servicio", - "description": "Comportamiento acosador y spam sistemático", + "reason": "Violación grave de términos", + "description": "Comportamiento acosador", "is_permanent": true }, "response_success": { "status": 200, "body": { "status": "success", - "message": "Cuenta marcada para cierre", + "message": "Cuenta cerrada", "action_id": "action-uuid-456" } } }, + "ban_user": { + "description": "Banear usuario (requiere token JWT de admin)", + "endpoint": "POST /moderation/users/ban", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_JWT_TOKEN" + }, + "request_body": { + "user_id": 42, + "reason": "Spam sistemático", + "duration_days": 30, + "description": "Múltiples reportes de spam" + }, + "response_success": { + "status": 200, + "body": { + "status": "success", + "message": "Usuario baneado por 30 días", + "action_id": "action-uuid-789" + } + } + }, + "warn_user": { + "description": "Advertir usuario (requiere token JWT de admin)", + "endpoint": "POST /moderation/users/warn", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_JWT_TOKEN" + }, + "request_body": { + "user_id": 42, + "reason": "Lenguaje inapropiado", + "description": "Primera advertencia" + }, + "response_success": { + "status": 200, + "body": { + "status": "success", + "message": "Usuario advertido", + "action_id": "action-uuid-101" + } + } + }, + "review_content": { + "description": "Revisar contenido reportado (requiere token JWT de admin)", + "endpoint": "POST /moderation/content/review", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer YOUR_JWT_TOKEN" + }, + "request_body": { + "report_id": "550e8400-e29b-41d4-a716-446655440000", + "action": "approve", + "reason": "Contenido válido según políticas", + "notes": "Aprobado después de revisión" + }, + "response_success": { + "status": 200, + "body": { + "status": "success", + "message": "Decisión registrada", + "action_id": "action-uuid-202" + } + } + } + } +} + } + }, "ban_user": { "description": "Banear un usuario temporalmente", diff --git a/requirements.txt b/requirements.txt index 5643631..1bba24a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ PyJWT passlib[argon2] python-multipart psycopg2-binary +cryptography diff --git a/src/infrastructure/api/auth.py b/src/infrastructure/api/auth.py new file mode 100644 index 0000000..0cd0824 --- /dev/null +++ b/src/infrastructure/api/auth.py @@ -0,0 +1,49 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer +import jwt +from core.config import ConfSettings +from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL +import logging + +logger = logging.getLogger(__name__) +security = HTTPBearer() + + +async def get_current_admin_user(credentials = Depends(security)): + try: + payload = jwt.decode( + credentials.credentials, + ConfSettings.jwt_secret_key, + algorithms=[ConfSettings.jwt_algorithm] + ) + user_id: int = payload.get("sub") + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token inválido", + headers={"WWW-Authenticate": "Bearer"}, + ) + except jwt.InvalidTokenError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token inválido o expirado", + headers={"WWW-Authenticate": "Bearer"}, + ) + + user_repo = UserRepositorySQL() + user = user_repo.find_by_id(user_id) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Usuario no encontrado", + ) + + if not user.is_admin: + logger.warning(f"Intento de acceso no autorizado a moderación por usuario {user_id}") + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Permisos insuficientes", + ) + + return user