From 9dd97b34f27ca12484c21a4b9351d68119e8d756 Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Tue, 14 Apr 2026 01:00:37 +0200 Subject: [PATCH 1/5] 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 -- 2.49.1 From 039349b5b1ed6a4f7bcc8a1f746486a5df74b154 Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Tue, 14 Apr 2026 21:30:19 +0200 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20integraci=C3=B3n=20final=20de=20Jen?= =?UTF-8?q?kins=20con=20inyecci=C3=B3n=20de=20.env=20segura?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 10 --- .env.example | 10 --- .gitignore | 4 + .../migrations/0002_alter_log_options.py | 17 ++++ core/.env.example | 3 + core/settings.py | 80 +++++++++++------- db.sqlite3 | Bin 16384 -> 155648 bytes deployments/.env.example | 34 ++++++++ deployments/Dockerfile | 18 ++-- deployments/Jenkinsfile | 48 +++++++---- deployments/docker-compose.yml | 36 ++++---- deployments/entrypoint.sh | 15 ++++ 12 files changed, 186 insertions(+), 89 deletions(-) delete mode 100644 .env delete mode 100644 .env.example create mode 100644 apps/backend_admin/migrations/0002_alter_log_options.py create mode 100644 core/.env.example create mode 100644 deployments/.env.example create mode 100644 deployments/entrypoint.sh diff --git a/.env b/.env deleted file mode 100644 index 5be865a..0000000 --- a/.env +++ /dev/null @@ -1,10 +0,0 @@ -# Seguridad -DEBUG=True -SECRET_KEY=una-clave-muy-secreta-y-larga-123456 - -# Base de Datos (Conectando al PostgreSQL que instalamos) -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/.env.example b/.env.example deleted file mode 100644 index f8f4030..0000000 --- a/.env.example +++ /dev/null @@ -1,10 +0,0 @@ -# Seguridad -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_PORT=5432 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0bba40d..066dafe 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ postgres_data/ local_postgres_data/ *.pyc apps/backend_admin/migrations/0001_initial.py +# Bloquear todos los .env en cualquier carpeta +.env +**/core/.env +**/deployments/.env \ No newline at end of file diff --git a/apps/backend_admin/migrations/0002_alter_log_options.py b/apps/backend_admin/migrations/0002_alter_log_options.py new file mode 100644 index 0000000..ce7ca54 --- /dev/null +++ b/apps/backend_admin/migrations/0002_alter_log_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.3 on 2026-04-14 19:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('backend_admin', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='log', + options={'managed': False}, + ), + ] diff --git a/core/.env.example b/core/.env.example new file mode 100644 index 0000000..be7f52a --- /dev/null +++ b/core/.env.example @@ -0,0 +1,3 @@ +# core/.env +APP_CUSTOM_SETTING="Este es un valor privado de la app" +EXTERNAL_SERVICE_API_KEY="sk_test_12345" \ No newline at end of file diff --git a/core/settings.py b/core/settings.py index 13c8d2d..ae2e211 100644 --- a/core/settings.py +++ b/core/settings.py @@ -3,35 +3,30 @@ import os from dotenv import load_dotenv from datetime import timedelta -# Cargar variables desde el archivo .env -load_dotenv() +# 1. RUTA DEL SETTINGS Y CARGA DEL .ENV +# Obtenemos la ruta de la carpeta donde está este archivo (core/) +CURRENT_DIR = Path(__file__).resolve().parent +BASE_DIR = CURRENT_DIR.parent -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +# Cargamos el .env específico de esta carpeta (core/.env) +load_dotenv(dotenv_path=CURRENT_DIR / '.env') # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-change-me-for-production') +SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-default-key-change-it') # 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') +# Mejoramos el parseo de DEBUG para que no falle si viene como string +DEBUG = os.getenv('DEBUG', 'True').lower() in ('true', '1', 't') +# 2. ALLOWED HOSTS +# Limpiamos y centralizamos los hosts permitidos ALLOWED_HOSTS = [ 'v-encore-lab.com', 'dev.v-encore-lab.com', - '185.187.169.109', # Añade la IP aquí + '185.187.169.109', 'localhost', '127.0.0.1', - 'django_app_dev', - 'django_app_master', - 'rest_framework', - 'rest_framework.authtoken' + os.getenv('APP_CONTAINER_NAME', 'django_app_dev'), # Dinámico para Docker ] # Application definition @@ -42,12 +37,16 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + + # Plugins 'rest_framework', 'rest_framework.authtoken', + 'corsheaders', + + # Tus Apps (Asegúrate de que el path sea correcto) 'apps.promociones', 'apps.backend_admin', 'apps.common', - 'corsheaders', ] MIDDLEWARE = [ @@ -81,15 +80,16 @@ TEMPLATES = [ WSGI_APPLICATION = 'core.wsgi.application' -# Database +# 3. DATABASE +# Extraemos con fallback por si el .env falla DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': DB_NAME, - 'USER': DB_USER, - 'PASSWORD': DB_PASSWORD, - 'HOST': DB_HOST, - 'PORT': DB_PORT, + 'NAME': os.getenv('DB_NAME', 'postgres'), + 'USER': os.getenv('DB_USER', 'postgres'), + 'PASSWORD': os.getenv('DB_PASSWORD', ''), + 'HOST': os.getenv('DB_HOST', 'localhost'), + 'PORT': os.getenv('DB_PORT', '5432'), } } @@ -99,7 +99,7 @@ TIME_ZONE = 'Europe/Madrid' USE_I18N = True USE_TZ = True -# Static files (CSS, JavaScript, Images) +# Static files STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' @@ -120,27 +120,43 @@ SIMPLE_JWT = { "REFRESH_TOKEN_LIFETIME": timedelta(days=1), } -# CORS -CORS_ALLOW_ALL_ORIGINS = True +# CORS & CSRF +CORS_ALLOW_ALL_ORIGINS = DEBUG # Solo permitir todo en modo DEBUG CSRF_TRUSTED_ORIGINS = [ - 'chrome-extension://amknoiejhlmhancpahfcfcfhllgkpbld', + 'https://v-encore-lab.com', 'http://localhost:8000', 'http://127.0.0.1:8000', ] -# Logging +# Logging simplificado LOGGING = { 'version': 1, 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {message}', + 'style': '{', + }, + }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', + 'formatter': 'verbose', }, }, 'loggers': { '': { 'handlers': ['console'], - 'level': 'INFO', + 'level': os.getenv('LOG_LEVEL', 'INFO'), }, }, -} \ No newline at end of file +} + +# --- CONFIGURACIONES PERSONALIZADAS DE LA APP --- + +# Leemos la variable del .env (cargado previamente con load_dotenv) +# Ponemos un valor por defecto por si se nos olvida ponerlo en el .env +APP_CUSTOM_SETTING = os.getenv('APP_CUSTOM_SETTING', 'valor_por_defecto_seguro') + +# Ejemplo de otra variable de API +EXTERNAL_SERVICE_API_KEY = os.getenv('EXTERNAL_SERVICE_API_KEY', None) \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index 933e1e8df4fc3fa595d10548dfb9355884a67a2f..e3b2ea187a2b04c9f8cc53b0cda5ad9532339f2c 100644 GIT binary patch literal 155648 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�LaG00ck)1V8`;KmY_l00ck)1m24T&e<)d zfzG6y+EQ}qgq+T*xlDfDWwV&Nd*to%MmbLsSWqbDclXY=kpx$L!g;I3)Ynatv-y;o z&ncza*Unap>GH*rQYw-7lJ@gri^bI6m&)hL#Bq7App*>XJ1iDcR~PlaT~dk;Gr3}S zTQBCf3ujM}OQx!z6tgPxdWN%@T$ii5iO1p|ef~F{elJ!g$_xS^00JNY0w4eaAOHd& z00JNY0`DCHIRF3NscV!J1V8`;KmY_l00ck)1V8`;K;XSg0MGyL-MUA~K>!3m00ck) z1V8`;KmY_l00iDU1nBvHj{ky*{KE$XKmY_l00ck)1V8`;KmY_l00cnb86$9$v%EHD z6T)dl6urrm7)}RM{&YxDf??m9B8!p~N(BUOT2O>=z%L47U|8@CdnLCwJnEB1rNBtY z7xqg*`u?BeziuM`@Bsl3009sH0T2KI5C8!X009sH0T4Jr1cuD79eZb>^8Ei36aSMF zR6s}t0T2KI5C8!X009sH0T2KI5C8!XI5q*Z*=D2X|Jy9zGVx-^@7gW4iMBUe-f2mb z2z)>Q1VG^A5;$mWbC}v(ZKhZHJN;c_W9;vqyxyy(dq>^gm)yN_s;u76lZ$e3Tlw(o zy{Y_mu1wQDEDA#H&$1|`>-i-4>nlp}`rVoN1;a@yQ5Y4aQPDRN3i%`{+$<&R6@mfr zpw;?Q5aRWX2tL0kc@GJpDIsrI^uBej)yas+AN6+j=t$_FkIBhdg%rM*QR*dl3UaBm zlP^+QS|njqRMv`0X@kZod$%`|lPPsZy*;(KKjn?7Q>CfgoIf=_6`0y8tSpV+4v!Ec zAt{zK$xK*KR&HdbHuGvKyObsW?aSjNLw3nWGej3sQfxEgpP7hAGxIyDynIt2w!5+Y zlo(xz(4YS4@!R*(lUwA5Q#;Z9ly_z!v$47v-HmRp3(?7?t(nRAPHZ!?5uKdcC-yt> zg^kVV9u>8;zq)dJOLIpvdCrCGEeX)flb#)?>C8gY2-JIKsF0882<(r#P@%BbkjPA!KXGy-r zU3!o7N-UGggbS;*IyNKf+U!VhV`_f(?v3pm`^!;pWML`0H@|T^Gc_?2Qn#13@7$2@ zPYAo&`*(6XDOzaSD)h=H5v8(R-ewd^+Z7JN>CeIjX>4b76I zgh{jXg+jrDms?o>=y~NA^^WQdiX-08h%n-PNS9TL#XQqEliXBNWp{qfP5(%eJ1v(L zw_0-N@@4n!`I(rT6h)OY>b{)JD7_!{AGd##!;(^P#21jfqzw8QO49SSl(69S2M%7g zJgfQxqe5^b5C}@)hVjW;NeK&~K=`e5=4X6ZAl-RC)?`?S?nk})uyA`bMmovr!j?b2 z92LkAAw(CVJ2T5SNrzeAiHmV>bbligoxGhrYFG#h3%==l_r~t+F5R5JwZ1krGwaK4 zOy61$B{p;W`%`o4n>We}h3Ir-@&A8V*o|$n(lrhX`_cVHeOSmwr8pTX;>+>f_%b7~ zznYEsR+pDD@#V-~Or*mI@sy2-M+^(3Sh4jR`-(WXyCzD$`S^yUOy3NS%?EZz!&CYBwbK5?STUVWE|yoyd-18zT5k5I&@4%(qor$XmSj$j7@FmrI{Ulot>ra z{k84c;8^}xm;9}+hh(K(AJ&_ qHp3=1cghL6Y|ZBe*ev$2xmO5}ilLF9KNKXZ!n)FEDWO0}2>l-_t~e+F delta 83 zcmZoTz}e8iI6+#Fg@J(qgkgYbqK>gJ3xi%+CofQlnNNm+?>FB{KADY;t9ZA|_%S|Z e+{`GzEWp9U|B!+I1OLO#f(BRlH$T*u7XSc-d=lgU diff --git a/deployments/.env.example b/deployments/.env.example new file mode 100644 index 0000000..a71f24d --- /dev/null +++ b/deployments/.env.example @@ -0,0 +1,34 @@ +# ================================================================= +# 🏗️ INFRAESTRUCTURA (Docker & Jenkins) +# ================================================================= +# Nombres que tomarán los contenedores en Docker +APP_CONTAINER_NAME= +DB_CONTAINER_NAME= + +# Nombre del proyecto para Docker Compose (usado en Jenkins) +PROJECT_NAME= + +# Puertos que se abrirán al exterior (Host) +PORT= +DATABASE_EXPOSE_PORT= + +# ================================================================= +# 🐍 CONFIGURACIÓN DE DJANGO +# ================================================================= +# True para desarrollo, False para producción +DEBUG= +# Genera una clave segura: https://djecrety.ir/ +SECRET_KEY= +# Lista separada por comas: localhost,127.0.0.1,tudominio.com +ALLOWED_HOSTS= + +# ================================================================= +# 🗄️ BASE DE DATOS (PostgreSQL) +# ================================================================= +DB_NAME= +DB_USER= +DB_PASSWORD= + +# El HOST debe ser "db" cuando se usa Docker Compose +DB_HOST= +DB_PORT= \ No newline at end of file diff --git a/deployments/Dockerfile b/deployments/Dockerfile index af963c3..fd3fd91 100644 --- a/deployments/Dockerfile +++ b/deployments/Dockerfile @@ -4,27 +4,31 @@ 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 -# Instalar dependencias del sistema necesarias +# Instalar dependencias del sistema necesarias y limpiar caché en un solo paso RUN apt-get update && apt-get install -y \ libpq-dev \ gcc \ gettext \ && rm -rf /var/lib/apt/lists/* -# Instalar dependencias de Python -COPY requirements.txt /app/ +# Copiar dependencias (Ajustado: asumiendo que están en deployments/) +COPY deployments/requirements.txt /app/ RUN pip install --no-cache-dir -r requirements.txt -# Copiar el resto del código +# Copiar el resto del código del proyecto +# El contexto de docker-compose suele ser la raíz, por lo que copiamos todo a /app COPY . /app/ # Exponer el puerto de Django EXPOSE 8000 -# Comando por defecto para arrancar (usaremos manage.py en dev y gunicorn en prod) -CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] \ No newline at end of file +# Script de entrada (lo crearemos en el siguiente paso) +COPY deployments/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/deployments/Jenkinsfile b/deployments/Jenkinsfile index 7bb4a3a..fcd0924 100644 --- a/deployments/Jenkinsfile +++ b/deployments/Jenkinsfile @@ -5,37 +5,55 @@ pipeline { stage('Configurar Entorno') { steps { script { + // Seleccionamos ID de credencial y config según la rama if (env.BRANCH_NAME == 'master') { env.PROJECT_NAME = "django_master" - env.CONTAINER_NAME = "django_app_master" + env.APP_CONTAINER_NAME = "django_app_master" env.PORT = "8001" env.DEBUG_MODE = "0" + env.ENV_CREDENTIAL_ID = "2" // Tu ID para master } else { env.PROJECT_NAME = "django_dev" - env.CONTAINER_NAME = "django_app_dev" + env.APP_CONTAINER_NAME = "django_app_dev" env.PORT = "8000" env.DEBUG_MODE = "1" + env.ENV_CREDENTIAL_ID = "1" // Tu ID para dev } } } } - stage('Despliegue') { + stage('Fase Final: Containerización') { when { anyOf { branch 'dev'; branch 'master' } } steps { - echo "DESPLEGANDO: ${env.CONTAINER_NAME} en el puerto ${env.PORT}" - - // CAMBIAMOS docker-compose (guion) por docker compose (espacio) - sh """ - CONTAINER_NAME=${env.CONTAINER_NAME} \ - PORT=${env.PORT} \ - DEBUG_MODE=${env.DEBUG_MODE} \ - docker compose -p ${env.PROJECT_NAME} -f deployments/docker-compose.yml up -d --build web - """ - - echo "Ejecutando migraciones en ${env.CONTAINER_NAME}..." - sh "docker exec ${env.CONTAINER_NAME} python manage.py migrate --noinput" + // Bloque mágico: extrae el archivo .env de la bóveda de Jenkins + withCredentials([file(credentialsId: env.ENV_CREDENTIAL_ID, variable: 'SECRET_ENV')]) { + sh """ + echo "Copiando configuración segura..." + cp \$SECRET_ENV deployments/.env + + echo "🚀 DESPLEGANDO: ${env.APP_CONTAINER_NAME} en puerto ${env.PORT}" + + export APP_CONTAINER_NAME=${env.APP_CONTAINER_NAME} + export PORT=${env.PORT} + export DEBUG_MODE=${env.DEBUG_MODE} + + docker compose -p ${env.PROJECT_NAME} -f deployments/docker-compose.yml up -d --build web + """ + } } } } + + post { + success { + echo "✅ Despliegue completado con éxito." + // Limpieza preventiva: borramos el .env físico después del despliegue + sh "rm -f deployments/.env" + } + failure { + echo "❌ Error en el despliegue. Revisa los logs." + sh "rm -f deployments/.env" + } + } } \ No newline at end of file diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml index 4d2c46c..a3cdb79 100644 --- a/deployments/docker-compose.yml +++ b/deployments/docker-compose.yml @@ -1,36 +1,42 @@ services: db: image: postgres:15 - container_name: django_db_local - # Cargamos el archivo directamente - env_file: - - ../.env + container_name: ${DB_CONTAINER_NAME:-django_db_local} + restart: always + # Solo usamos el .env de esta carpeta + env_file: .env environment: - # 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: - - "5432:5432" + - "${DATABASE_EXPOSE_PORT:-5432}:5432" volumes: - local_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] + interval: 5s + timeout: 5s + retries: 5 web: - build: . - container_name: django_app_dev + build: + context: .. + dockerfile: deployments/Dockerfile + container_name: ${APP_CONTAINER_NAME:-django_app_dev} + restart: always volumes: - ..:/app - # Cargamos el archivo directamente aquí también - env_file: - - ../.env + ports: + - "${PORT:-8000}:8000" + # Inyectamos el .env local al contenedor web + env_file: .env environment: - DB_HOST=db - DB_PORT=5432 - ports: - - "8000:8000" depends_on: - - db + db: + condition: service_healthy volumes: local_postgres_data: \ No newline at end of file diff --git a/deployments/entrypoint.sh b/deployments/entrypoint.sh new file mode 100644 index 0000000..115690f --- /dev/null +++ b/deployments/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Salir inmediatamente si un comando falla +set -e + +echo "--> Ejecutando migraciones..." +python manage.py makemigrations --noinput +python manage.py migrate --noinput + +# En el futuro, aquí podrías añadir: +# python manage.py collectstatic --noinput + +echo "--> Arrancando el servidor Django..." +# Usamos exec para que Django sea el proceso principal (PID 1) y reciba señales de Docker +exec python manage.py runserver 0.0.0.0:8000 \ No newline at end of file -- 2.49.1 From bf5a38d425b7c5d675a1778a002fa33a19ea2f24 Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Tue, 14 Apr 2026 22:46:18 +0200 Subject: [PATCH 3/5] cambios en migraciones --- apps/backend_admin/migrations/__init__.py | 1 + apps/common/migrations/__init__.py | 1 + apps/promociones/fixtures/semillas.json | 35 +++++++++++++++++++ apps/promociones/migrations/__init__.py | 1 + deployments/Dockerfile | 24 +++++++------ deployments/docker-compose.yml | 41 ++++++++++++----------- deployments/entrypoint.sh | 7 ++-- 7 files changed, 78 insertions(+), 32 deletions(-) create mode 100644 apps/common/migrations/__init__.py create mode 100644 apps/promociones/fixtures/semillas.json create mode 100644 apps/promociones/migrations/__init__.py diff --git a/apps/backend_admin/migrations/__init__.py b/apps/backend_admin/migrations/__init__.py index e69de29..1ff90f5 100644 --- a/apps/backend_admin/migrations/__init__.py +++ b/apps/backend_admin/migrations/__init__.py @@ -0,0 +1 @@ +# Archivo para marcar esta carpeta como paquete de migraciones \ No newline at end of file diff --git a/apps/common/migrations/__init__.py b/apps/common/migrations/__init__.py new file mode 100644 index 0000000..1ff90f5 --- /dev/null +++ b/apps/common/migrations/__init__.py @@ -0,0 +1 @@ +# Archivo para marcar esta carpeta como paquete de migraciones \ No newline at end of file diff --git a/apps/promociones/fixtures/semillas.json b/apps/promociones/fixtures/semillas.json new file mode 100644 index 0000000..5cf3d5a --- /dev/null +++ b/apps/promociones/fixtures/semillas.json @@ -0,0 +1,35 @@ +[ + { + "model": "promociones.promocion", + "pk": 1, + "fields": { + "nombre": "Oferta de Bienvenida", + "fecha_inicio": "2026-04-01", + "descripcion": "Descuento para nuevos usuarios", + "activo": true, + "categoria_id": 1 + } + }, + { + "model": "promociones.promocion", + "pk": 2, + "fields": { + "nombre": "Promo Primavera", + "fecha_inicio": "2026-05-01", + "descripcion": "Todo al 20% de descuento", + "activo": true, + "categoria_id": 2 + } + }, + { + "model": "promociones.promocion", + "pk": 3, + "fields": { + "nombre": "Liquidación Stock", + "fecha_inicio": "2026-04-14", + "descripcion": "Últimas unidades", + "activo": false, + "categoria_id": 1 + } + } +] \ No newline at end of file diff --git a/apps/promociones/migrations/__init__.py b/apps/promociones/migrations/__init__.py new file mode 100644 index 0000000..1ff90f5 --- /dev/null +++ b/apps/promociones/migrations/__init__.py @@ -0,0 +1 @@ +# Archivo para marcar esta carpeta como paquete de migraciones \ No newline at end of file diff --git a/deployments/Dockerfile b/deployments/Dockerfile index fd3fd91..c123e9f 100644 --- a/deployments/Dockerfile +++ b/deployments/Dockerfile @@ -1,33 +1,35 @@ -# Usamos una imagen ligera de Python +# 1. Usamos una imagen ligera de Python FROM python:3.12-slim -# Evitar que Python genere archivos .pyc y que el buffer se sature +# 2. Evitar archivos .pyc y saturación del buffer ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 ENV TZ=Europe/Madrid -# Directorio de trabajo +# 3. Directorio de trabajo interno del contenedor WORKDIR /app -# Instalar dependencias del sistema necesarias y limpiar caché en un solo paso +# 4. Instalar dependencias del sistema RUN apt-get update && apt-get install -y \ libpq-dev \ gcc \ gettext \ && rm -rf /var/lib/apt/lists/* -# Copiar dependencias (Ajustado: asumiendo que están en deployments/) -COPY deployments/requirements.txt /app/ +# 5. COPIAR REQUISITOS +# OJO: Ahora el contexto es la RAÍZ, así que el archivo está en deployments/ +COPY deployments/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Copiar el resto del código del proyecto -# El contexto de docker-compose suele ser la raíz, por lo que copiamos todo a /app -COPY . /app/ +# 6. COPIAR EL CÓDIGO +# Copiamos todo el contenido de la raíz (.) al directorio de trabajo (/app) +# Esto incluirá apps/, core/, manage.py, etc. +COPY . . -# Exponer el puerto de Django +# 7. EXPOSICIÓN Y SCRIPT DE ENTRADA EXPOSE 8000 -# Script de entrada (lo crearemos en el siguiente paso) +# El entrypoint también está en deployments/ COPY deployments/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml index a3cdb79..5b4ed6c 100644 --- a/deployments/docker-compose.yml +++ b/deployments/docker-compose.yml @@ -1,20 +1,23 @@ +version: '3.8' + services: - db: + gitea-db: image: postgres:15 - container_name: ${DB_CONTAINER_NAME:-django_db_local} + # Usará el nombre de tu .env (django_db_local) + container_name: ${DB_CONTAINER_NAME:-django_db_dev} restart: always - # Solo usamos el .env de esta carpeta - env_file: .env environment: - - POSTGRES_DB=${DB_NAME} - - POSTGRES_USER=${DB_USER} - - POSTGRES_PASSWORD=${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME:-gitea} + POSTGRES_USER: ${DB_USER:-gitea} + POSTGRES_PASSWORD: ${DB_PASSWORD:-gitea} + volumes: + - postgres_data:/var/lib/postgresql/data + # --- ESTO ES LO QUE FALTA --- ports: - "${DATABASE_EXPOSE_PORT:-5432}:5432" - volumes: - - local_postgres_data:/var/lib/postgresql/data + # ---------------------------- healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-gitea} -d ${DB_NAME:-gitea}"] interval: 5s timeout: 5s retries: 5 @@ -25,18 +28,18 @@ services: dockerfile: deployments/Dockerfile container_name: ${APP_CONTAINER_NAME:-django_app_dev} restart: always - volumes: - - ..:/app + env_file: + - .env + environment: + - DEBUG=${DEBUG_MODE:-1} + # IMPORTANTE: Este nombre debe coincidir con el nombre del servicio arriba (gitea-db) + - DB_HOST=gitea-db + - DB_PORT=5432 ports: - "${PORT:-8000}:8000" - # Inyectamos el .env local al contenedor web - env_file: .env - environment: - - DB_HOST=db - - DB_PORT=5432 depends_on: - db: + gitea-db: condition: service_healthy volumes: - local_postgres_data: \ No newline at end of file + postgres_data: \ No newline at end of file diff --git a/deployments/entrypoint.sh b/deployments/entrypoint.sh index 115690f..fc401c5 100644 --- a/deployments/entrypoint.sh +++ b/deployments/entrypoint.sh @@ -4,11 +4,14 @@ set -e echo "--> Ejecutando migraciones..." +# Esto asegura que si hay cambios en models.py, se generen y apliquen las tablas python manage.py makemigrations --noinput python manage.py migrate --noinput -# En el futuro, aquí podrías añadir: -# python manage.py collectstatic --noinput +echo "--> Cargando datos de prueba..." +# Este comando busca archivos JSON en las carpetas 'fixtures' de tus apps +# Usamos || true para que si el archivo no existe o ya están cargados, el contenedor no se detenga +python manage.py loaddata semillas || echo "Aviso: No se pudieron cargar las semillas (fichero no encontrado o error de formato)." echo "--> Arrancando el servidor Django..." # Usamos exec para que Django sea el proceso principal (PID 1) y reciba señales de Docker -- 2.49.1 From 7ba79c973999e1023aaaa8dbd559142a9ea3b161 Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Tue, 14 Apr 2026 23:02:45 +0200 Subject: [PATCH 4/5] Delete 0002_alter_log_options.py --- .../migrations/0002_alter_log_options.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 apps/backend_admin/migrations/0002_alter_log_options.py diff --git a/apps/backend_admin/migrations/0002_alter_log_options.py b/apps/backend_admin/migrations/0002_alter_log_options.py deleted file mode 100644 index ce7ca54..0000000 --- a/apps/backend_admin/migrations/0002_alter_log_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.0.3 on 2026-04-14 19:11 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('backend_admin', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='log', - options={'managed': False}, - ), - ] -- 2.49.1 From c32092d216fa2d59b4bbe6e5c21a4237e054fb0b Mon Sep 17 00:00:00 2001 From: minguezsanzjuanjose Date: Tue, 14 Apr 2026 23:24:27 +0200 Subject: [PATCH 5/5] Update settings.py --- core/settings.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/settings.py b/core/settings.py index ae2e211..c12ec3c 100644 --- a/core/settings.py +++ b/core/settings.py @@ -3,6 +3,18 @@ import os from dotenv import load_dotenv from datetime import timedelta +SIMPLE_JWT = { + # Cambiamos el tiempo de acceso a 3 horas + 'ACCESS_TOKEN_LIFETIME': timedelta(hours=3), + + # El tiempo del refresh token suele ser mayor (por ejemplo, 1 día) + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + + # Otras configuraciones que ya tengas... + 'ALGORITHM': 'HS256', + 'AUTH_HEADER_TYPES': ('Bearer',), +} + # 1. RUTA DEL SETTINGS Y CARGA DEL .ENV # Obtenemos la ruta de la carpeta donde está este archivo (core/) CURRENT_DIR = Path(__file__).resolve().parent -- 2.49.1