diff --git a/.env b/.env deleted file mode 100644 index f8f4030..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=gitea -DB_USER=gitea -DB_PASSWORD=gitea -DB_HOST=gitea-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 51deba6..066dafe 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ deployments/docker-compose.yml 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/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..1ff90f5 --- /dev/null +++ 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/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/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/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/.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 9d07618..c12ec3c 100644 --- a/core/settings.py +++ b/core/settings.py @@ -1,26 +1,47 @@ -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() +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',), +} -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +# 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 -SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-change-me-for-production') -DEBUG = os.getenv('DEBUG', 'True').lower() == 'true' +# 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-default-key-change-it') + +# SECURITY WARNING: don't run with debug turned on in production! +# 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' + os.getenv('APP_CONTAINER_NAME', 'django_app_dev'), # Dinámico para Docker ] +# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -28,11 +49,20 @@ 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', ] MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -62,26 +92,18 @@ 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'), - } +# 3. DATABASE +# Extraemos con fallback por si el .env falla +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + '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'), } +} # Internationalization LANGUAGE_CODE = 'es-es' @@ -95,19 +117,58 @@ 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 & CSRF +CORS_ALLOW_ALL_ORIGINS = DEBUG # Solo permitir todo en modo DEBUG +CSRF_TRUSTED_ORIGINS = [ + 'https://v-encore-lab.com', + 'http://localhost:8000', + 'http://127.0.0.1:8000', +] + +# 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/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/db.sqlite3 b/db.sqlite3 index 933e1e8..e3b2ea1 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ 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 3dcc296..c123e9f 100644 --- a/deployments/Dockerfile +++ b/deployments/Dockerfile @@ -1,29 +1,36 @@ -# 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 +# 4. Instalar dependencias del sistema 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/ +# 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 -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 -# 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 +# El entrypoint también está en deployments/ +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 bee6a3f..5b4ed6c 100644 --- a/deployments/docker-compose.yml +++ b/deployments/docker-compose.yml @@ -1,25 +1,45 @@ +version: '3.8' + services: + gitea-db: + image: postgres:15 + # Usará el nombre de tu .env (django_db_local) + container_name: ${DB_CONTAINER_NAME:-django_db_dev} + restart: always + environment: + 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" + # ---------------------------- + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-gitea} -d ${DB_NAME:-gitea}"] + interval: 5s + timeout: 5s + retries: 5 + web: build: context: .. dockerfile: deployments/Dockerfile - container_name: ${CONTAINER_NAME} + container_name: ${APP_CONTAINER_NAME:-django_app_dev} restart: always - working_dir: /app + 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" + - DEBUG=${DEBUG_MODE:-1} + # IMPORTANTE: Este nombre debe coincidir con el nombre del servicio arriba (gitea-db) + - DB_HOST=gitea-db - DB_PORT=5432 - networks: - - gitea_bridge # <-- Conectamos el servicio a nuestro puente ports: - - "${PORT}:8000" + - "${PORT:-8000}:8000" + depends_on: + gitea-db: + condition: service_healthy -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 +volumes: + postgres_data: \ No newline at end of file diff --git a/deployments/entrypoint.sh b/deployments/entrypoint.sh new file mode 100644 index 0000000..fc401c5 --- /dev/null +++ b/deployments/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Salir inmediatamente si un comando falla +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 + +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 +exec python manage.py runserver 0.0.0.0:8000 \ 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