From 9dd97b34f27ca12484c21a4b9351d68119e8d756 Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Tue, 14 Apr 2026 01:00:37 +0200 Subject: [PATCH] fix --- .env | 8 +- .gitignore | 1 + README.md | 67 +++++++++++++++ apps/backend_admin/actions.py | 39 ++++++--- apps/backend_admin/migrations/__init__.py | 0 apps/backend_admin/models.py | 25 ++++++ apps/backend_admin/views.py | 69 +++++++++++++++- apps/common/apps.py | 3 +- apps/promociones/urls.py | 6 +- apps/promociones/views.py | 99 +++++++++-------------- core/settings.py | 81 +++++++++++++------ core/urls.py | 11 ++- deployments/Dockerfile | 3 +- deployments/docker-compose.yml | 53 +++++++----- deployments/requirements.txt | 7 +- requirements.txt | 4 - 16 files changed, 339 insertions(+), 137 deletions(-) create mode 100644 apps/backend_admin/migrations/__init__.py create mode 100644 apps/backend_admin/models.py delete mode 100644 requirements.txt diff --git a/.env b/.env index f8f4030..5be865a 100644 --- a/.env +++ b/.env @@ -3,8 +3,8 @@ DEBUG=True SECRET_KEY=una-clave-muy-secreta-y-larga-123456 # Base de Datos (Conectando al PostgreSQL que instalamos) -DB_NAME=gitea -DB_USER=gitea -DB_PASSWORD=gitea -DB_HOST=gitea-db +DB_NAME=django_test +DB_USER=django_user +DB_PASSWORD=django_password +DB_HOST=db DB_PORT=5432 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 51deba6..0bba40d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ deployments/docker-compose.yml postgres_data/ local_postgres_data/ *.pyc +apps/backend_admin/migrations/0001_initial.py diff --git a/README.md b/README.md index ba20b40..2e8bb40 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,72 @@ // V-Encore Lab: Sistema Automatizado v1.0.4 +## 🚀 Inicio Rápido (Desarrollo Local) +### 1. Clonar y Configurar +```bash +git clone https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base.git +cd django-core-base +cp .env.example .env +``` +### 2. Instalar Dependencias +```bash +pip install -r deployments/requirements.txt +``` + +### 3. Migraciones de Base de Datos +```bash +# Crear y aplicar migraciones para todos los modelos +python manage.py makemigrations +python manage.py migrate + +# Opcional: Crear superusuario +python manage.py createsuperuser +``` + +### 4. Correr el Servidor +```bash +python manage.py runserver +``` + +Abrir http://localhost:8000 + +## 🐳 Docker (Producción/Desarrollo) + +```bash +docker-compose up --build +``` + +Acceder a: +- App: http://localhost:8000 +- Admin: http://localhost:8000/admin/ +- DB: localhost:5432 (Postgres) + +## 📋 Comandos Django Comunes + +```bash +# Verificar configuración +python manage.py check + +# Recopilar static files +python manage.py collectstatic --noinput + +# Test +python manage.py test +``` + +## 🔧 Estructura del Proyecto + +``` +├── apps/ # Aplicaciones Django +│ ├── backend_admin/ +│ ├── common/ +│ └── promociones/ +├── core/ # Configuración principal +├── deployments/ # Docker, requirements prod +└── manage.py +``` + +## .env Variables +Ver `.env.example` para configuración. \ No newline at end of file diff --git a/apps/backend_admin/actions.py b/apps/backend_admin/actions.py index 680a1f5..641c5bf 100644 --- a/apps/backend_admin/actions.py +++ b/apps/backend_admin/actions.py @@ -1,13 +1,34 @@ +from django.contrib.auth import authenticate +from rest_framework.authtoken.models import Token +from rest_framework_simplejwt.tokens import RefreshToken + + class Admin: def get_status_action(self): + # Tu lógica de status que ya tenías + return {"status": "ok", "service": "Admin Infrastructure"} + + + def obtener_token_action(self, params): """ - Lógica para comprobar la salud del sistema. - Devuelve el estado básico del entorno. + Capa Action: Valida credenciales y genera un par de tokens JWT. """ - # En el futuro, podrías usar get_parameterized aquí si quisieras - # consultar estados en la base de datos. - return { - "status": "ok", - "message": "V-Encore API System is active", - "environment": "dev" # Esto podría venir de una variable de entorno - } \ No newline at end of file + username = params.get('username') + password = params.get('password') + + # 1. Autenticación + user = authenticate(username=username, password=password) + + if user is not None: + # 2. Generación de JWT (Access & Refresh) + refresh = RefreshToken.for_user(user) + + return { + 'refresh': str(refresh), + 'access': str(refresh.access_token), + 'user': user.username, + 'status': 'success' + } + + return None + \ No newline at end of file diff --git a/apps/backend_admin/migrations/__init__.py b/apps/backend_admin/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/backend_admin/models.py b/apps/backend_admin/models.py new file mode 100644 index 0000000..a4ba0b7 --- /dev/null +++ b/apps/backend_admin/models.py @@ -0,0 +1,25 @@ +from django.db import models +from django.utils import timezone + +class Log(models.Model): + # Usamos BigAutoField para el BIGINT id de tu tabla + id = models.BigAutoField(primary_key=True) + user_id = models.IntegerField(default=0) + user = models.CharField(max_length=255, default='anonimo') + app_id = models.IntegerField(default=0) + # GenericIPAddressField para el tipo INET de Postgres + remote_address = models.GenericIPAddressField(null=True, blank=True) + request = models.JSONField(null=True, blank=True) # Para JSONB + response = models.JSONField(null=True, blank=True) # Para JSONB + status_code = models.CharField(max_length=10, default='0') + path = models.CharField(max_length=255) + method = models.CharField(max_length=10) + createdAt = models.DateTimeField(default=timezone.now) + updatedAt = models.DateTimeField(auto_now=True) + + class Meta: + db_table = 'audit_logs' + managed = False # Al estar la tabla ya creada, Django no intentará modificarla + + def __str__(self): + return f"{self.method} {self.path} ({self.status_code})" \ No newline at end of file diff --git a/apps/backend_admin/views.py b/apps/backend_admin/views.py index bf15dde..fa0318b 100644 --- a/apps/backend_admin/views.py +++ b/apps/backend_admin/views.py @@ -1,6 +1,12 @@ from django.http import JsonResponse from .actions import Admin import logging +import json +from django.views.decorators.csrf import csrf_exempt + +from django.utils import timezone +from .models import Log + logger = logging.getLogger(__name__) @@ -25,4 +31,65 @@ def status_view(request): # BLOQUE 4: Log de cierre y retorno logger.info(f"FIN - Health Check completado. Status: {status_code}") - return JsonResponse(response_data, status=status_code) \ No newline at end of file + return JsonResponse(response_data, status=status_code) + +@csrf_exempt +@staticmethod +def api_token(request): + """ + Endpoint: api/token/ + Patrón: 4 bloques con persistencia en Log DB. + """ + # --- BLOQUE 1: LOG INITIATION --- + logger.info("INICIO - Petición de JWT (api/token/)") + + # Iniciamos el registro en la base de datos (Estándar compañeros) + log_entry = Log.objects.create( + user='anonimo', + path='api/token/', + method='POST', + createdAt=timezone.now(), + status_code='0' + ) + + try: + if request.method == 'POST': + # --- BLOQUE 2: DATA CLEANING --- + body_data = json.loads(request.body) + log_entry.request = body_data # Guardamos lo que entró + log_entry.save() + + params = { + 'username': body_data.get('username'), + 'password': body_data.get('password') + } + + # --- BLOQUE 3: ACTION CALL --- + admin_logic = Admin() + resultado = admin_logic.obtener_token_action(params) + + # --- BLOQUE 4: LOG CLOSURE & RESPONSE --- + if resultado: + status = 200 + log_entry.user = resultado['user'] + log_entry.response = resultado + log_entry.status_code = str(status) + log_entry.updatedAt = timezone.now() + log_entry.save() + + logger.info(f"FIN - JWT generado para: {log_entry.user}") + return JsonResponse(resultado, status=status) + else: + status = 401 + response_error = {"error": "Credenciales inválidas"} + log_entry.status_code = str(status) + log_entry.response = response_error + log_entry.save() + return JsonResponse(response_error, status=status) + + except Exception as e: + logger.error(f"ERROR CRÍTICO en api_token: {str(e)}") + log_entry.status_code = '500' + log_entry.response = {'error': str(e)} + log_entry.save() + return JsonResponse({'error': 'Error interno'}, status=500) \ No newline at end of file diff --git a/apps/common/apps.py b/apps/common/apps.py index 0c3ab12..7e49969 100644 --- a/apps/common/apps.py +++ b/apps/common/apps.py @@ -1,5 +1,6 @@ +# apps/common/apps.py from django.apps import AppConfig class CommonConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.common' \ No newline at end of file + name = 'apps.common' # <--- ESTO ES LO QUE DEBE PONER \ No newline at end of file diff --git a/apps/promociones/urls.py b/apps/promociones/urls.py index 6ec526f..ab2ec87 100644 --- a/apps/promociones/urls.py +++ b/apps/promociones/urls.py @@ -1,8 +1,6 @@ from django.urls import path -from .views import get_promocion_view, status_view +from .views import PromocionObtener urlpatterns = [ - # Capa 1: Definición del endpoint - path('obtener/', get_promocion_view, name='get_promocion'), - + path('obtener/', PromocionObtener.as_view(), name='obtener_promocion'), ] \ No newline at end of file diff --git a/apps/promociones/views.py b/apps/promociones/views.py index 85cb46a..a5c4c49 100644 --- a/apps/promociones/views.py +++ b/apps/promociones/views.py @@ -1,73 +1,46 @@ -import logging +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework_simplejwt.authentication import JWTAuthentication from django.http import JsonResponse +from apps.backend_admin.models import Log from .actions import getData +from django.utils import timezone # Esta es la forma correcta +class PromocionObtener(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] -from django.http import JsonResponse -from .actions import get_status_action + def post(self, request): + # --- BLOQUE 1: LOG INITIATION --- + log_entry = Log.objects.create( + user=request.user.username, + path='promociones/obtener/', + method='POST', + status_code='0' + ) + print('llega a despues de log entry') -# Configuración del logger para rastrear la ejecución -logger = logging.getLogger(__name__) + try: + # --- BLOQUE 2: DATA CLEANING --- + data = request.data + log_entry.request = data + log_entry.save() -def get_promocion_view(request): - """ - Vista estandarizada para la obtención de una promoción. - """ - - # --------------------------------------------------------- - # BLOQUE 1: Log de iniciación - # --------------------------------------------------------- - logger.info("[START] Iniciando ejecución de get_promocion_view") + params = {'id': data.get('id'), 'activo': data.get('activo')} - try: - # --------------------------------------------------------- - # BLOQUE 2: Limpieza de datos (Data Cleaning) - # --------------------------------------------------------- - # Extraemos los parámetros del request y preparamos el diccionario - raw_data = request.GET.dict() - - # Aquí es donde ella aplicaría validaciones adicionales si fuera necesario - clean_params = { - 'id': raw_data.get('id'), - 'activo': raw_data.get('activo', True) # Valor por defecto - } + # --- BLOQUE 3: ACTION CALL --- + # Aquí ya recibimos las fechas como strings gracias al paso 1 + resultado = getData(params) - # --------------------------------------------------------- - # BLOQUE 3: Llamada a la Action (Execution) - # --------------------------------------------------------- - # La lógica de SQL y parametrización vive dentro de esta llamada - resultado_db = getData(clean_params) + # --- BLOQUE 4: LOG CLOSURE & RESPONSE --- + log_entry.response = {"count": len(resultado)} # No guardes todo el JSON si es muy grande + log_entry.status_code = '200' + log_entry.save() - # --------------------------------------------------------- - # BLOQUE 4: Log de cierre y respuesta (Closure) - # --------------------------------------------------------- - logger.info(f"[SUCCESS] get_promocion_view finalizada. Registros encontrados: {len(resultado_db)}") - - return JsonResponse({ - 'status': 'success', - 'data': resultado_db - }, status=200) + return JsonResponse(resultado, safe=False, status=200) - except Exception as e: - # Log de error detallado en caso de fallo - logger.error(f"[ERROR] Fallo crítico en get_promocion_view: {str(e)}") - - return JsonResponse({ - 'status': 'error', - 'message': 'Error interno del servidor' - }, status=500) - -# En views.py -def status_view(request): - # BLOQUE 1: Log de iniciación - logger.info("Iniciando petición de status...") - - # BLOQUE 2: Limpieza y validación de datos (En este caso no hay datos de entrada) - data_cleaned = {} - - # BLOQUE 3: Llamada a la acción - response_data = get_status_action() - - # BLOQUE 4: Log de cierre y retorno - logger.info("Status enviado correctamente.") - return JsonResponse(response_data, status=200) \ No newline at end of file + except Exception as e: + log_entry.status_code = '500' + log_entry.response = {'error': str(e)} + log_entry.save() + return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/core/settings.py b/core/settings.py index 9d07618..13c8d2d 100644 --- a/core/settings.py +++ b/core/settings.py @@ -1,6 +1,7 @@ -from dotenv import load_dotenv -import os from pathlib import Path +import os +from dotenv import load_dotenv +from datetime import timedelta # Cargar variables desde el archivo .env load_dotenv() @@ -8,9 +9,19 @@ load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent +# SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-change-me-for-production') + +# SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG', 'True').lower() == 'true' +# Constants from .env +DB_NAME = os.getenv('DB_NAME') +DB_USER = os.getenv('DB_USER') +DB_PASSWORD = os.getenv('DB_PASSWORD') +DB_HOST = os.getenv('DB_HOST') +DB_PORT = os.getenv('DB_PORT') + ALLOWED_HOSTS = [ 'v-encore-lab.com', 'dev.v-encore-lab.com', @@ -18,9 +29,12 @@ ALLOWED_HOSTS = [ 'localhost', '127.0.0.1', 'django_app_dev', - 'django_app_master' + 'django_app_master', + 'rest_framework', + 'rest_framework.authtoken' ] +# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -28,11 +42,16 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework.authtoken', 'apps.promociones', + 'apps.backend_admin', 'apps.common', + 'corsheaders', ] MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -62,26 +81,17 @@ TEMPLATES = [ WSGI_APPLICATION = 'core.wsgi.application' -# Para desarrollo local: SQLite -if DEBUG: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } - } -else: - # Producción: PostgreSQL desde .env - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('DB_NAME'), - 'USER': os.getenv('DB_USER'), - 'PASSWORD': os.getenv('DB_PASSWORD'), - 'HOST': os.getenv('DB_HOST'), - 'PORT': os.getenv('DB_PORT'), - } +# Database +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': DB_NAME, + 'USER': DB_USER, + 'PASSWORD': DB_PASSWORD, + 'HOST': DB_HOST, + 'PORT': DB_PORT, } +} # Internationalization LANGUAGE_CODE = 'es-es' @@ -89,13 +99,36 @@ TIME_ZONE = 'Europe/Madrid' USE_I18N = True USE_TZ = True -# Static files +# Static files (CSS, JavaScript, Images) STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -# Logging básico +# REST Framework +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), +} + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(hours=1), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), +} + +# CORS +CORS_ALLOW_ALL_ORIGINS = True +CSRF_TRUSTED_ORIGINS = [ + 'chrome-extension://amknoiejhlmhancpahfcfcfhllgkpbld', + 'http://localhost:8000', + 'http://127.0.0.1:8000', +] + +# Logging LOGGING = { 'version': 1, 'disable_existing_loggers': False, diff --git a/core/urls.py b/core/urls.py index d3fff72..8abc299 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,10 @@ from django.urls import path, include +from django.urls import path, include +from apps.backend_admin import views as admin_views urlpatterns = [ - # Redirigimos todas las peticiones de /api/promociones/ a nuestra app - path('api/promociones/', include('apps.promociones.urls')), - path('admin/', include('apps.backend_admin.urls')), # El tuyo -] + path('admin/', include('apps.backend_admin.urls')), + # AÑADE ESTA LÍNEA AQUÍ PARA QUE SEA UNA RUTA PRINCIPAL: + path('promociones/', include('apps.promociones.urls')), + path('api/token/', admin_views.api_token, name='token_obtain_pair'), +] \ No newline at end of file diff --git a/deployments/Dockerfile b/deployments/Dockerfile index 3dcc296..af963c3 100644 --- a/deployments/Dockerfile +++ b/deployments/Dockerfile @@ -4,7 +4,8 @@ FROM python:3.12-slim # Evitar que Python genere archivos .pyc y que el buffer se sature ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 - +# Definimos la zona horaria como variable de entorno +ENV TZ=Europe/Madrid # Directorio de trabajo WORKDIR /app diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml index bee6a3f..4d2c46c 100644 --- a/deployments/docker-compose.yml +++ b/deployments/docker-compose.yml @@ -1,25 +1,36 @@ services: - web: - build: - context: .. - dockerfile: deployments/Dockerfile - container_name: ${CONTAINER_NAME} - restart: always - working_dir: /app + db: + image: postgres:15 + container_name: django_db_local + # Cargamos el archivo directamente + env_file: + - ../.env environment: - - DEBUG=${DEBUG_MODE} - - PYTHONPATH=/app - - DB_NAME=gitea - - DB_USER=gitea - - DB_PASSWORD=gitea # <-- Asegúrate de que esta sea la que pusiste en la web de Gitea - - DB_HOST=gitea-db # <-- IMPORTANTE: Nombre del servicio, sin el "-1" - - DB_PORT=5432 - networks: - - gitea_bridge # <-- Conectamos el servicio a nuestro puente + # POSTGRES_DB espera estas variables exactas, + # así que las mapeamos a lo que tienes en tu .env + - POSTGRES_DB=${DB_NAME} + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASSWORD} ports: - - "${PORT}:8000" + - "5432:5432" + volumes: + - local_postgres_data:/var/lib/postgresql/data -networks: - gitea_bridge: # <-- Definimos el puente - external: true - name: root_gitea_gitea # <-- ESTE es el nombre real que sale en tu 'docker network ls' \ No newline at end of file + web: + build: . + container_name: django_app_dev + volumes: + - ..:/app + # Cargamos el archivo directamente aquí también + env_file: + - ../.env + environment: + - DB_HOST=db + - DB_PORT=5432 + ports: + - "8000:8000" + depends_on: + - db + +volumes: + local_postgres_data: \ No newline at end of file diff --git a/deployments/requirements.txt b/deployments/requirements.txt index f6ec772..735e271 100644 --- a/deployments/requirements.txt +++ b/deployments/requirements.txt @@ -1,2 +1,7 @@ Django==5.0.3 -psycopg2-binary==2. \ No newline at end of file +psycopg2-binary==2.9.9 +gunicorn==21.2.0 +python-dotenv==1.0.1 +djangorestframework +django-cors-headers +djangorestframework-simplejwt \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 99cc37b..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Django==5.0.3 -psycopg2-binary==2.9.9 -gunicorn==21.2.0 -python-dotenv==1.0.1 \ No newline at end of file