Compare commits
2 Commits
f837f446b9
...
d8d7e5c2eb
| Author | SHA1 | Date | |
|---|---|---|---|
| d8d7e5c2eb | |||
| 3135241aaa |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -88,7 +88,6 @@ coverage/
|
|||||||
|
|
||||||
#Documentación
|
#Documentación
|
||||||
*.pdf
|
*.pdf
|
||||||
*.txt
|
|
||||||
|
|
||||||
#Directorio de Imagenes
|
#Directorio de Imagenes
|
||||||
Backend/MotoresArgentinosV2.API/wwwroot
|
Backend/MotoresArgentinosV2.API/wwwroot
|
||||||
@@ -119,6 +119,7 @@ builder.Services.AddScoped<IEmailService, SmtpEmailService>();
|
|||||||
builder.Services.AddScoped<IImageStorageService, ImageStorageService>();
|
builder.Services.AddScoped<IImageStorageService, ImageStorageService>();
|
||||||
builder.Services.AddHostedService<AdExpirationService>();
|
builder.Services.AddHostedService<AdExpirationService>();
|
||||||
builder.Services.AddHostedService<TokenCleanupService>();
|
builder.Services.AddHostedService<TokenCleanupService>();
|
||||||
|
builder.Services.AddHostedService<SitemapGeneratorService>();
|
||||||
|
|
||||||
// 🔒 JWT AUTH
|
// 🔒 JWT AUTH
|
||||||
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key Missing");
|
var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key Missing");
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using MotoresArgentinosV2.Core.Entities;
|
||||||
|
using MotoresArgentinosV2.Infrastructure.Data;
|
||||||
|
|
||||||
|
namespace MotoresArgentinosV2.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Servicio de fondo que genera sitemap.xml dinámicamente.
|
||||||
|
/// Ejecuta al iniciar la app y luego cada 6 horas.
|
||||||
|
/// Incluye rutas estáticas del frontend + vehículos activos.
|
||||||
|
/// </summary>
|
||||||
|
public class SitemapGeneratorService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<SitemapGeneratorService> _logger;
|
||||||
|
private static readonly TimeSpan Interval = TimeSpan.FromHours(6);
|
||||||
|
|
||||||
|
private const string BaseUrl = "https://motoresargentinos.com";
|
||||||
|
|
||||||
|
// Rutas estáticas cuyo contenido cambia con cada aviso nuevo (usan lastmod dinámico)
|
||||||
|
private static readonly (string Path, string Priority, string ChangeFreq)[] DynamicDateRoutes =
|
||||||
|
[
|
||||||
|
("/", "1.0", "daily"),
|
||||||
|
("/explorar", "0.8", "daily"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Rutas estáticas cuyo contenido rara vez cambia (sin lastmod para no mentirle a Google)
|
||||||
|
private static readonly (string Path, string Priority, string ChangeFreq)[] FixedRoutes =
|
||||||
|
[
|
||||||
|
("/publicar", "0.6", "monthly"),
|
||||||
|
("/vender", "0.6", "monthly"),
|
||||||
|
("/condiciones", "0.3", "yearly"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public SitemapGeneratorService(IServiceProvider serviceProvider, ILogger<SitemapGeneratorService> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("SitemapGeneratorService iniciado.");
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await GenerateSitemapAsync();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error generando sitemap.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(Interval, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GenerateSitemapAsync()
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var context = scope.ServiceProvider.GetRequiredService<MotoresV2DbContext>();
|
||||||
|
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||||
|
|
||||||
|
// Ruta de salida: configurable para Docker (volumen compartido) o dev local
|
||||||
|
var outputPath = config["AppSettings:SitemapOutputPath"] ?? Path.Combine("wwwroot", "sitemap.xml");
|
||||||
|
|
||||||
|
// Obtener todos los avisos activos
|
||||||
|
var activeAds = await context.Ads
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(a => a.StatusID == (int)AdStatusEnum.Active)
|
||||||
|
.Select(a => new { a.AdID, a.PublishedAt })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Fecha del aviso más reciente (para rutas cuyo contenido cambia con nuevos avisos)
|
||||||
|
var latestPublished = activeAds
|
||||||
|
.Where(a => a.PublishedAt.HasValue)
|
||||||
|
.Max(a => a.PublishedAt)
|
||||||
|
?.ToString("yyyy-MM-dd") ?? DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||||
|
sb.AppendLine("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">");
|
||||||
|
|
||||||
|
// 1. Rutas cuyo contenido cambia con cada aviso nuevo (/, /explorar)
|
||||||
|
foreach (var (path, priority, changeFreq) in DynamicDateRoutes)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" <url>");
|
||||||
|
sb.AppendLine($" <loc>{BaseUrl}{path}</loc>");
|
||||||
|
sb.AppendLine($" <lastmod>{latestPublished}</lastmod>");
|
||||||
|
sb.AppendLine($" <changefreq>{changeFreq}</changefreq>");
|
||||||
|
sb.AppendLine($" <priority>{priority}</priority>");
|
||||||
|
sb.AppendLine(" </url>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Rutas fijas (sin lastmod — no mentirle a Google)
|
||||||
|
foreach (var (path, priority, changeFreq) in FixedRoutes)
|
||||||
|
{
|
||||||
|
sb.AppendLine(" <url>");
|
||||||
|
sb.AppendLine($" <loc>{BaseUrl}{path}</loc>");
|
||||||
|
sb.AppendLine($" <changefreq>{changeFreq}</changefreq>");
|
||||||
|
sb.AppendLine($" <priority>{priority}</priority>");
|
||||||
|
sb.AppendLine(" </url>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Rutas dinámicas (vehículos activos — lastmod = fecha de publicación real)
|
||||||
|
foreach (var ad in activeAds)
|
||||||
|
{
|
||||||
|
var lastmod = ad.PublishedAt?.ToString("yyyy-MM-dd") ?? latestPublished;
|
||||||
|
|
||||||
|
sb.AppendLine(" <url>");
|
||||||
|
sb.AppendLine($" <loc>{BaseUrl}/vehiculo/{ad.AdID}</loc>");
|
||||||
|
sb.AppendLine($" <lastmod>{lastmod}</lastmod>");
|
||||||
|
sb.AppendLine(" <changefreq>weekly</changefreq>");
|
||||||
|
sb.AppendLine(" <priority>0.7</priority>");
|
||||||
|
sb.AppendLine(" </url>");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine("</urlset>");
|
||||||
|
|
||||||
|
// Escritura atómica: escribir en .tmp, luego mover
|
||||||
|
var dir = Path.GetDirectoryName(outputPath);
|
||||||
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempPath = outputPath + ".tmp";
|
||||||
|
await File.WriteAllTextAsync(tempPath, sb.ToString(), Encoding.UTF8);
|
||||||
|
File.Move(tempPath, outputPath, overwrite: true);
|
||||||
|
|
||||||
|
var totalUrls = DynamicDateRoutes.Length + FixedRoutes.Length + activeAds.Count;
|
||||||
|
_logger.LogInformation("Sitemap generado con {TotalUrls} URLs ({StaticCount} estáticas + {DynamicCount} vehículos). Archivo: {Path}",
|
||||||
|
totalUrls, DynamicDateRoutes.Length + FixedRoutes.Length, activeAds.Count, outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,11 @@ server {
|
|||||||
# Seguridad: Limitar tamaño de subida para prevenir DoS
|
# Seguridad: Limitar tamaño de subida para prevenir DoS
|
||||||
client_max_body_size 20M;
|
client_max_body_size 20M;
|
||||||
|
|
||||||
|
# Sitemap dinámico (generado por backend en volumen compartido)
|
||||||
|
location = /sitemap.xml {
|
||||||
|
alias /usr/share/nginx/html/sitemap-data/sitemap.xml;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
|
|||||||
15
Frontend/public/robots.txt
Normal file
15
Frontend/public/robots.txt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Disallow: /perfil
|
||||||
|
Disallow: /seguridad
|
||||||
|
Disallow: /mis-avisos
|
||||||
|
Disallow: /admin
|
||||||
|
Disallow: /restablecer-clave
|
||||||
|
Disallow: /verificar-email
|
||||||
|
Disallow: /confirmar-cambio-email
|
||||||
|
Disallow: /baja/
|
||||||
|
Disallow: /pago-confirmado
|
||||||
|
Disallow: /baja-exitosa
|
||||||
|
Disallow: /baja-error
|
||||||
|
|
||||||
|
Sitemap: https://motoresargentinos.com/sitemap.xml
|
||||||
@@ -5,38 +5,41 @@ services:
|
|||||||
dockerfile: Backend/Dockerfile.API
|
dockerfile: Backend/Dockerfile.API
|
||||||
container_name: motores-backend
|
container_name: motores-backend
|
||||||
restart: always
|
restart: always
|
||||||
# Eliminamos ports para que NO sea accesible desde afuera, solo por motores-frontend
|
|
||||||
env_file:
|
env_file:
|
||||||
- Backend/MotoresArgentinosV2.API/.env
|
- Backend/MotoresArgentinosV2.API/.env
|
||||||
environment:
|
environment:
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
- ASPNETCORE_HTTP_PORTS=8080
|
- ASPNETCORE_HTTP_PORTS=8080
|
||||||
# Soportamos ambos: el dominio final y la IP de pruebas para CORS
|
- AppSettings__FrontendUrl=https://motoresargentinos.com,https://www.motoresargentinos.com,http://192.168.5.129:8086,http://localhost:5173,https://clasificados.eldia.com
|
||||||
- AppSettings__FrontendUrl=https://motoresargentinos.com,http://192.168.5.129:8086,http://localhost:5173,https://clasificados.eldia.com
|
|
||||||
# Para links generados (pagos/confirmaciones), usamos la IP por ahora si vas a probar sin dominio
|
|
||||||
- AppSettings__BaseUrl=http://192.168.5.129:8086/api
|
- AppSettings__BaseUrl=http://192.168.5.129:8086/api
|
||||||
|
- AppSettings__SitemapOutputPath=/app/sitemap-output/sitemap.xml
|
||||||
networks:
|
networks:
|
||||||
- motores-network
|
- motores-network
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/MotoresImg:/app/wwwroot/uploads
|
- /mnt/MotoresImg:/app/wwwroot/uploads
|
||||||
|
- sitemap-data:/app/sitemap-output
|
||||||
|
|
||||||
motores-frontend:
|
motores-frontend:
|
||||||
build:
|
build:
|
||||||
context: ./Frontend
|
context: ./Frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
# Al usar Nginx como proxy, podemos usar rutas relativas desde el navegador
|
|
||||||
- VITE_API_BASE_URL=/api
|
- VITE_API_BASE_URL=/api
|
||||||
- VITE_STATIC_BASE_URL=
|
- VITE_STATIC_BASE_URL=
|
||||||
- VITE_MP_PUBLIC_KEY=APP_USR-12bbd874-5ea7-49cf-b9d9-0f3e7df089b3
|
- VITE_MP_PUBLIC_KEY=APP_USR-12bbd874-5ea7-49cf-b9d9-0f3e7df089b3
|
||||||
container_name: motores-frontend
|
container_name: motores-frontend
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8086:80" # Puerto libre detectado en el análisis de Portainer
|
- "8086:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- motores-backend
|
- motores-backend
|
||||||
networks:
|
networks:
|
||||||
- motores-network
|
- motores-network
|
||||||
|
volumes:
|
||||||
|
- sitemap-data:/usr/share/nginx/html/sitemap-data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
sitemap-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
motores-network:
|
motores-network:
|
||||||
|
|||||||
Reference in New Issue
Block a user