diff --git a/backend/src/Titulares.Api/Dockerfile b/backend/src/Titulares.Api/Dockerfile new file mode 100644 index 0000000..313cd48 --- /dev/null +++ b/backend/src/Titulares.Api/Dockerfile @@ -0,0 +1,30 @@ +# Etapa 1: Compilación +# Usamos la imagen del SDK de .NET 8 para compilar la aplicación +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copiamos el archivo de proyecto y restauramos las dependencias primero +# Esto aprovecha el caché de capas de Docker +COPY ["Titulares.Api/Titulares.Api.csproj", "Titulares.Api/"] +RUN dotnet restore "Titulares.Api/Titulares.Api.csproj" + +# Copiamos el resto del código fuente +COPY . . +WORKDIR "/src/Titulares.Api" +# Compilamos la aplicación en modo Release y la publicamos en la carpeta /app/publish +RUN dotnet publish "Titulares.Api.csproj" -c Release -o /app/publish --no-restore + +# Etapa 2: Imagen Final +# Usamos la imagen de runtime de ASP.NET, que es mucho más ligera +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final +WORKDIR /app +COPY --from=build /app/publish . + +# Exponemos el puerto 8080. El backend escuchará en este puerto dentro del contenedor. +EXPOSE 8080 + +# Establecemos el entorno a Producción para que se use appsettings.Production.json +ENV ASPNETCORE_ENVIRONMENT=Production + +# Punto de entrada para ejecutar la aplicación +ENTRYPOINT ["dotnet", "Titulares.Api.dll"] \ No newline at end of file diff --git a/backend/src/Titulares.Api/Program.cs b/backend/src/Titulares.Api/Program.cs index dfbd62c..5d54640 100644 --- a/backend/src/Titulares.Api/Program.cs +++ b/backend/src/Titulares.Api/Program.cs @@ -23,12 +23,15 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +// Obtener los orígenes permitidos desde la configuración +var allowedOrigins = builder.Configuration.GetValue("AllowedOrigins")?.Split(',') ?? new[] { "http://localhost:5173" }; + // Añadimos la política de CORS builder.Services.AddCors(options => { - options.AddPolicy("AllowReactApp", builder => + options.AddPolicy("AllowReactApp", policyBuilder => { - builder.WithOrigins("http://localhost:5173") + policyBuilder.WithOrigins(allowedOrigins) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); diff --git a/backend/src/Titulares.Api/appsettings.Production.json b/backend/src/Titulares.Api/appsettings.Production.json new file mode 100644 index 0000000..b08cdd2 --- /dev/null +++ b/backend/src/Titulares.Api/appsettings.Production.json @@ -0,0 +1,11 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=db-sqlserver;Database=TitularesDB;User Id=titularesApi;Password=PTP847Titulares;TrustServerCertificate=True;" + }, + "AllowedOrigins": "http://192.168.5.128:8905", + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} \ No newline at end of file diff --git a/backend/src/Titulares.Api/appsettings.json b/backend/src/Titulares.Api/appsettings.json index 6de4d08..de713a2 100644 --- a/backend/src/Titulares.Api/appsettings.json +++ b/backend/src/Titulares.Api/appsettings.json @@ -6,6 +6,7 @@ } }, "AllowedHosts": "*", + "AllowedOrigins": "http://localhost:5173", "ConnectionStrings": { "DefaultConnection": "Server=TECNICA3;Database=TitularesDB;User Id=titularesApi;Password=PTP847Titulares;Trusted_Connection=True;TrustServerCertificate=True;" } diff --git a/backend/src/Titulares.Api/configuracion.json b/backend/src/Titulares.Api/configuracion.json index 2d728f9..863756b 100644 --- a/backend/src/Titulares.Api/configuracion.json +++ b/backend/src/Titulares.Api/configuracion.json @@ -1,5 +1,5 @@ { - "RutaCsv": "C:\\temp\\titulares.csv", + "RutaCsv": "/data/titulares.csv", "IntervaloMinutos": 15, "CantidadTitularesAScrapear": 4, "ScrapingActivo": false diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..96e3e3a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +services: + # Servicio del Backend + backend: + build: + context: ./backend/src/Titulares.Api + dockerfile: Dockerfile + container_name: titulares-api + # No exponemos puertos al exterior, el proxy se encarga de eso. + environment: + # Le decimos a ASP.NET Core que escuche en el puerto 8080 en todas las interfaces de red + - ASPNETCORE_URLS=http://+:8080 + networks: + - app-net + - shared-net # Conectamos a la red externa para la DB + restart: unless-stopped + + # Servicio de Nginx (Proxy Inverso + Servidor Frontend) + nginx-proxy: + build: + context: ./frontend # Construimos desde la carpeta del frontend... + dockerfile: Dockerfile # ...usando su Dockerfile para generar los estáticos + container_name: titulares-proxy + # Montamos nuestra configuración personalizada de Nginx + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf + ports: + # Mapeamos el puerto 8905 del host al puerto 80 del contenedor Nginx + - "8905:80" + networks: + - app-net + depends_on: + - backend # Aseguramos que el backend se inicie antes que el proxy + restart: unless-stopped + +networks: + # Red interna para la comunicación entre el proxy y el backend + app-net: + driver: bridge + # Definimos la red compartida como externa, ya que fue creada por otro stack + shared-net: + external: true \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..b7c501f --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:5174 \ No newline at end of file diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..2d1bf5f --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1 @@ +VITE_API_BASE_URL= \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..d69c90f --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,30 @@ +# Etapa 1: Construcción de la aplicación React +# Usamos una imagen de Node.js (versión Alpine por ser ligera) +FROM node:20-alpine AS build +WORKDIR /app + +# Copiamos package.json y package-lock.json para instalar dependencias +COPY package*.json ./ +RUN npm install + +# Copiamos el resto de los archivos del frontend +COPY . . +# Construimos la aplicación para producción. Los archivos resultantes irán a /app/dist +RUN npm run build + +# Etapa 2: Servidor Nginx +# Usamos la imagen oficial de Nginx (versión Alpine por ser ligera) +FROM nginx:stable-alpine +WORKDIR /usr/share/nginx/html + +# Eliminamos el contenido por defecto de Nginx +RUN rm -rf ./* + +# Copiamos los archivos estáticos construidos en la etapa anterior +COPY --from=build /app/dist . + +# Exponemos el puerto 80, que es el puerto por defecto de Nginx +EXPOSE 80 + +# El comando por defecto de la imagen de Nginx es suficiente para iniciar el servidor +# CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/frontend/src/hooks/useSignalR.ts b/frontend/src/hooks/useSignalR.ts index e23f901..817e9a6 100644 --- a/frontend/src/hooks/useSignalR.ts +++ b/frontend/src/hooks/useSignalR.ts @@ -3,7 +3,10 @@ import { useEffect, useRef, useState } from 'react'; import * as signalR from '@microsoft/signalr'; -const HUB_URL = 'http://localhost:5174/titularesHub'; +// La URL base viene de la variable de entorno +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; +// Construimos la URL completa del Hub +const HUB_URL = `${API_BASE_URL}/titularesHub`; // Definimos un tipo para el estado de la conexión para más claridad export type ConnectionStatus = 'Connecting' | 'Connected' | 'Disconnected' | 'Reconnecting'; diff --git a/frontend/src/services/apiService.ts b/frontend/src/services/apiService.ts index 07c56e2..fecd005 100644 --- a/frontend/src/services/apiService.ts +++ b/frontend/src/services/apiService.ts @@ -3,10 +3,12 @@ import axios from 'axios'; import type { Configuracion, Titular } from '../types'; -const API_URL = 'http://localhost:5174/api'; +// La URL base viene de la variable de entorno +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; const apiClient = axios.create({ - baseURL: API_URL, + // Construimos la URL completa para las llamadas a la API REST + baseURL: `${API_BASE_URL}/api`, headers: { 'Content-Type': 'application/json' }, }); diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..8578a36 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,46 @@ +# Define un "upstream" que apunta a nuestro servicio de backend. +# 'backend' será el nombre del servicio en docker-compose, y 8080 es el puerto +# que expusimos en su Dockerfile. +upstream backend_api { + server backend:8080; +} + +server { + # Nginx escuchará en el puerto 80 DENTRO del contenedor. + listen 80; + server_name 192.168.5.128; # Opcional, puedes usar localhost o tu dominio + + # Ubicación raíz: sirve los archivos de la aplicación React. + location / { + root /usr/share/nginx/html; + try_files $uri /index.html; # Clave para Single Page Applications (SPA) + } + + # Ubicación para la API REST. + # Todas las peticiones a http://.../api/... serán redirigidas al backend. + location /api { + proxy_pass http://backend_api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # ----------- CONFIGURACIÓN CRÍTICA PARA SIGNALR ----------- + # Ubicación para el Hub de SignalR. + location /titularesHub { + proxy_pass http://backend_api; + + # Requerido para que la conexión WebSocket funcione + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + + # Aumenta el timeout para conexiones de larga duración + proxy_read_timeout 90s; + + # Evita que el proxy almacene en caché la negociación de WebSockets + proxy_cache_bypass $http_upgrade; + } +} \ No newline at end of file