commit b9aa8478db993a87e2acc8558edd83d418546429 Author: dmolinari Date: Thu Jan 29 13:43:44 2026 -0300 Init Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb7810c --- /dev/null +++ b/.gitignore @@ -0,0 +1,94 @@ +# ---------------------------------------------------------------------------- +# ## General / Sistema Operativo y Editores ## +# ---------------------------------------------------------------------------- + +# Archivos de caché del sistema operativo +.DS_Store +Thumbs.db + +# Archivos de configuración y caché de IDEs y editores +.idea/ +.vscode/ +*.suo +*.user +*.userosscache +*.sln.docstates + +# Archivos de log +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Archivos de variables de entorno locales +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# ---------------------------------------------------------------------------- +# ## Backend: .NET / C# (Carpeta ChatbotApi/) ## +# ---------------------------------------------------------------------------- + +# Carpetas de compilación y binarios. Se generan al compilar. +[Bb]in/ +[Oo]bj/ + +# Configuración de desarrollo que puede contener secretos. +# Es mejor usar "User Secrets" en desarrollo para las claves. +appsettings.Development.json + +# Archivos de publicación de Visual Studio +[Pp]roperties/[Pp]ublish[Pp]rofiles/ +*.pubxml +*.publish.xml + +# Directorio de paquetes de NuGet (formato antiguo) +[Pp]ackages/ + +# Archivos de caché de Visual Studio +.vs/ + +# Herramientas de .NET +.dotnet/ +tools/ + +# Archivos generados por Entity Framework +# Nota: La carpeta 'Migrations' SÍ se suele incluir en el repositorio, +# pero si tienes algún archivo temporal o generado dentro, puedes añadirlo aquí. + +# ---------------------------------------------------------------------------- +# ## Frontend: React / Vite / Node.js ## +# ---------------------------------------------------------------------------- + +# Dependencias (se instalan con 'npm install' o 'yarn') +node_modules/ + +# Carpetas de build (se generan con 'npm run build') +dist/ +build/ +.next/ +out/ + +# Archivos de caché de herramientas de frontend +.npm/ +.vite/ +.cache/ +.eslintcache + +# Reportes de cobertura de tests +coverage/ + +# Archivos de configuración de proxy de Create React App +.proxyrc.js + +# Backups de DB +.bak + +#Documentación +*.pdf +*.txt + +#Directorio de Imagenes +Backend/MotoresArgentinosV2.API/wwwroot \ No newline at end of file diff --git a/Backend/Dockerfile.API b/Backend/Dockerfile.API new file mode 100644 index 0000000..cbf8258 --- /dev/null +++ b/Backend/Dockerfile.API @@ -0,0 +1,31 @@ +# Etapa de construcción +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# Copiar archivos .sln y .csproj para restaurar dependencias +COPY MotoresArgentinosV2.sln ./ +COPY Backend/MotoresArgentinosV2.API/*.csproj Backend/MotoresArgentinosV2.API/ +COPY Backend/MotoresArgentinosV2.Core/*.csproj Backend/MotoresArgentinosV2.Core/ +COPY Backend/MotoresArgentinosV2.Infrastructure/*.csproj Backend/MotoresArgentinosV2.Infrastructure/ +COPY Backend/MotoresArgentinosV2.MigrationTool/*.csproj Backend/MotoresArgentinosV2.MigrationTool/ + +RUN dotnet restore + +# Copiar el resto del código y construir +COPY . . +WORKDIR "/src/Backend/MotoresArgentinosV2.API" +RUN dotnet build "MotoresArgentinosV2.API.csproj" -c Release -o /app/build + +# Publicar +FROM build AS publish +RUN dotnet publish "MotoresArgentinosV2.API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +# Etapa final: Ejecución +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app +COPY --from=publish /app/publish . + +# Exponer el puerto configurado (ASP.NET 10 usa 8080 por defecto en contenedores) +EXPOSE 8080 + +ENTRYPOINT ["dotnet", "MotoresArgentinosV2.API.dll"] diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AdminController.cs b/Backend/MotoresArgentinosV2.API/Controllers/AdminController.cs new file mode 100644 index 0000000..936bba9 --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/AdminController.cs @@ -0,0 +1,526 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MotoresArgentinosV2.Infrastructure.Data; +using MotoresArgentinosV2.Core.Entities; +using MotoresArgentinosV2.Core.Interfaces; + +using Microsoft.AspNetCore.Authorization; + +using MotoresArgentinosV2.Core.DTOs; + +namespace MotoresArgentinosV2.API.Controllers; + +[Authorize(Roles = "Admin")] +[ApiController] +[Route("api/[controller]")] +public class AdminController : ControllerBase +{ + private readonly MotoresV2DbContext _context; + private readonly IAdSyncService _syncService; + private readonly INotificationService _notificationService; + + public AdminController(MotoresV2DbContext context, IAdSyncService syncService, INotificationService notificationService) + { + _context = context; + _syncService = syncService; + _notificationService = notificationService; + } + + // --- MODERACIÓN --- + + // GESTIÓN GLOBAL DE AVISOS (Buscador Admin) + [HttpGet("ads")] + public async Task GetAllAds( + [FromQuery] string? q = null, + [FromQuery] int? statusId = null, + [FromQuery] int page = 1) + { + const int pageSize = 20; + var query = _context.Ads + .Include(a => a.Brand) + .Include(a => a.User) + .Include(a => a.Photos) // Para mostrar la miniatura + .AsNoTracking() // Optimización de lectura + .AsQueryable(); + + // Filtro por Texto (Marca, Modelo, Email Usuario, Nombre Usuario) + if (!string.IsNullOrEmpty(q)) + { + query = query.Where(a => + a.VersionName!.Contains(q) || + a.Brand.Name.Contains(q) || + a.User.Email.Contains(q) || + a.User.UserName.Contains(q) + ); + } + + // Filtro por Estado + if (statusId.HasValue) + { + query = query.Where(a => a.StatusID == statusId.Value); + } + + var total = await query.CountAsync(); + + var ads = await query + .OrderByDescending(a => a.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(a => new + { + a.AdID, + BrandName = a.Brand != null ? a.Brand.Name : null, + VersionName = a.VersionName, + StatusID = a.StatusID, + + CreatedAt = a.CreatedAt, + PublishedAt = a.PublishedAt, + ExpiresAt = a.ExpiresAt, + DeletedAt = a.DeletedAt, + Views = a.ViewsCounter, + LegacyID = a.LegacyAdID, + + // Datos de la transacción APROBADA más reciente + PaidAmount = _context.Transactions + .Where(t => t.AdID == a.AdID && t.Status == "APPROVED") + .OrderByDescending(t => t.CreatedAt) + .Select(t => (decimal?)t.Amount) // Cast a nullable para detectar si no hubo pago + .FirstOrDefault(), + + PaidDate = _context.Transactions + .Where(t => t.AdID == a.AdID && t.Status == "APPROVED") + .OrderByDescending(t => t.CreatedAt) + .Select(t => (DateTime?)t.CreatedAt) + .FirstOrDefault(), + + UserEmail = a.User.Email, + UserName = a.User.UserName, + Thumbnail = a.Photos.Where(p => p.IsCover).Select(p => p.FilePath).FirstOrDefault() + ?? a.Photos.Select(p => p.FilePath).FirstOrDefault() + }) + .ToListAsync(); + + return Ok(new { ads, total, page, pageSize }); + } + + [HttpGet("ads/pending")] + public async Task GetPendingAds() + { + var ads = await _context.Ads + .Include(a => a.User) + .Include(a => a.Photos) + .Where(a => a.StatusID == (int)AdStatusEnum.ModerationPending) + .OrderByDescending(a => a.CreatedAt) + .Select(a => new + { + a.AdID, + VersionName = a.Brand != null ? $"{a.Brand.Name} {a.VersionName}" : a.VersionName, + a.Price, + a.Currency, + a.CreatedAt, + UserID = a.UserID, + UserName = a.User.UserName, + Email = a.User.Email, + Thumbnail = a.Photos.Where(p => p.IsCover).Select(p => p.FilePath).FirstOrDefault() ?? a.Photos.Select(p => p.FilePath).FirstOrDefault() + }) + .ToListAsync(); + + return Ok(ads); + } + + [HttpPost("ads/{id}/approve")] + public async Task ApproveAd(int id) + { + var ad = await _context.Ads.Include(a => a.User).Include(a => a.Brand).FirstOrDefaultAsync(a => a.AdID == id); + if (ad == null) return NotFound(); + + ad.StatusID = (int)AdStatusEnum.Active; + ad.PublishedAt = DateTime.UtcNow; + ad.ExpiresAt = DateTime.UtcNow.AddDays(30); + + await _context.SaveChangesAsync(); + + // Audit Log + var adminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + _context.AuditLogs.Add(new AuditLog + { + Action = "AD_APPROVED", + Entity = "Ad", + EntityID = id, + UserID = adminId, + Details = $"Aviso '{ad.Brand?.Name} {ad.VersionName}' aprobado por administrador." + }); + await _context.SaveChangesAsync(); + + // Sincronizar a Legacy + try + { + await _syncService.SyncAdToLegacyAsync(id); + var adTitle = $"{ad.Brand?.Name} {ad.VersionName}"; + await _notificationService.SendAdStatusChangedEmailAsync(ad.User?.Email ?? string.Empty, adTitle, "APROBADO"); + } + catch (Exception) + { + // Logueamos pero no bloqueamos la aprobación en V2 + } + + return Ok(new { message = "Aviso aprobado y publicado." }); + } + + [HttpPost("ads/{id}/reject")] + public async Task RejectAd(int id, [FromBody] string reason) + { + var ad = await _context.Ads.Include(a => a.User).Include(a => a.Brand).FirstOrDefaultAsync(a => a.AdID == id); + if (ad == null) return NotFound(); + + ad.StatusID = (int)AdStatusEnum.Rejected; + + await _context.SaveChangesAsync(); + + // Audit Log + var adminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + _context.AuditLogs.Add(new AuditLog + { + Action = "AD_REJECTED", + Entity = "Ad", + EntityID = id, + UserID = adminId, + Details = $"Aviso '{ad.Brand?.Name} {ad.VersionName}' rechazado. Motivo: {reason}" + }); + await _context.SaveChangesAsync(); + + // Notificar rechazo + var adTitle = $"{ad.Brand?.Name} {ad.VersionName}"; + await _notificationService.SendAdStatusChangedEmailAsync(ad.User?.Email ?? string.Empty, adTitle, "RECHAZADO", reason); + + return Ok(new { message = "Aviso rechazado." }); + } + + // --- TRANSACCIONES --- + + [HttpGet("transactions")] + public async Task GetTransactions( + [FromQuery] string? status = null, + [FromQuery] string? userSearch = null, + [FromQuery] DateTime? fromDate = null, + [FromQuery] DateTime? toDate = null, + [FromQuery] int page = 1) + { + const int pageSize = 20; + + var query = from t in _context.Transactions + join a in _context.Ads on t.AdID equals a.AdID into adGroup + from ad in adGroup.DefaultIfEmpty() // Left Join con Ads + join u in _context.Users on ad.UserID equals u.UserID into userGroup + from user in userGroup.DefaultIfEmpty() // Left Join con Users + select new { t, ad, user }; + + // 1. Filtro por Estado + if (!string.IsNullOrEmpty(status)) + { + query = query.Where(x => x.t.Status == status); + } + + // 2. Filtro por Usuario (Búsqueda) + if (!string.IsNullOrEmpty(userSearch)) + { + // Solo buscamos si el usuario existe (no es nulo) + query = query.Where(x => + x.user != null && + (x.user.Email.Contains(userSearch) || x.user.UserName.Contains(userSearch)) + ); + } + + // 3. Filtro por Fechas + if (fromDate.HasValue) + { + query = query.Where(x => x.t.CreatedAt >= fromDate.Value); + } + + if (toDate.HasValue) + { + var toDateEndOfDay = toDate.Value.Date.AddHours(23).AddMinutes(59).AddSeconds(59); + query = query.Where(x => x.t.CreatedAt <= toDateEndOfDay); + } + + // 4. Paginación y Ejecución + var total = await query.CountAsync(); + + var txs = await query + .OrderByDescending(x => x.t.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(x => new + { + x.t.TransactionID, + x.t.OperationCode, + x.t.Amount, + x.t.Status, + x.t.CreatedAt, + x.t.UpdatedAt, + AdID = x.t.AdID, + + // LÓGICA DE PRIORIDAD: Snapshot > Relación Viva > Default + UserID = (x.ad != null) ? x.ad.UserID : 0, + + UserName = !string.IsNullOrEmpty(x.t.SnapshotUserName) + ? x.t.SnapshotUserName + : ((x.user != null) ? x.user.UserName : "ELIMINADO"), + + UserEmail = !string.IsNullOrEmpty(x.t.SnapshotUserEmail) + ? x.t.SnapshotUserEmail + : ((x.user != null) ? x.user.Email : "-"), + + AdTitle = !string.IsNullOrEmpty(x.t.SnapshotAdTitle) + ? x.t.SnapshotAdTitle + : ((x.ad != null && x.ad.Brand != null) ? $"{x.ad.Brand.Name} {x.ad.VersionName}" : "Vehículo Desconocido/Eliminado") + }) + .ToListAsync(); + + return Ok(new { transactions = txs, total, page, pageSize }); + } + + // --- USUARIOS --- + [HttpGet("users")] + public async Task GetUsers([FromQuery] string? q = null, [FromQuery] int page = 1) + { + const int pageSize = 20; + var query = _context.Users.AsQueryable(); + + if (!string.IsNullOrEmpty(q)) + { + query = query.Where(u => (u.Email ?? "").Contains(q) || + (u.UserName ?? "").Contains(q) || + (u.FirstName ?? "").Contains(q) || + (u.LastName ?? "").Contains(q)); + } + + var total = await query.CountAsync(); + var users = await query + .OrderByDescending(u => u.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(u => new + { + u.UserID, + u.UserName, + u.Email, + u.FirstName, + u.LastName, + u.UserType, + u.MigrationStatus, + u.IsBlocked, + u.CreatedAt + }) + .ToListAsync(); + + return Ok(new { users, total, page, pageSize }); + } + + [HttpGet("users/{id}")] + public async Task GetUserDetail(int id) + { + var user = await _context.Users + .Where(u => u.UserID == id) + .Select(u => new UserDetailDto + { + UserID = u.UserID, + UserName = u.UserName, + Email = u.Email, + FirstName = u.FirstName, + LastName = u.LastName, + PhoneNumber = u.PhoneNumber, + UserType = u.UserType, + IsBlocked = u.IsBlocked, + MigrationStatus = u.MigrationStatus, + IsEmailVerified = u.IsEmailVerified, + CreatedAt = u.CreatedAt + }) + .FirstOrDefaultAsync(); + + if (user == null) return NotFound(); + return Ok(user); + } + + [HttpPut("users/{id}")] + public async Task UpdateUser(int id, [FromBody] UserUpdateDto dto) + { + var user = await _context.Users.FindAsync(id); + if (user == null) return NotFound(); + + user.UserName = dto.UserName; + user.FirstName = dto.FirstName; + user.LastName = dto.LastName; + user.PhoneNumber = dto.PhoneNumber; + user.UserType = dto.UserType; + + await _context.SaveChangesAsync(); + + // Audit Log + var adminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + _context.AuditLogs.Add(new AuditLog + { + Action = "USER_UPDATED", + Entity = "User", + EntityID = id, + UserID = adminId, + Details = $"Usuario '{user.UserName}' actualizado por administrador." + }); + await _context.SaveChangesAsync(); + + return Ok(new { message = "Usuario actualizado correctamente." }); + } + + [HttpGet("stats")] + public async Task GetStats() + { + var stats = new + { + // 1. Usamos AuditLogs para contar TODAS las creaciones históricas, + // incluso si el aviso ya fue borrado físicamente de la tabla Ads. + TotalAds = await _context.AuditLogs.CountAsync(l => l.Action == "AD_CREATED"), + + // 2. Avisos actualmente activos (Status = 4) + ActiveAds = await _context.Ads.CountAsync(a => a.StatusID == (int)AdStatusEnum.Active), + + // 3. Pendientes de moderación (Status = 3) + PendingAds = await _context.Ads.CountAsync(a => a.StatusID == (int)AdStatusEnum.ModerationPending), + + // 4. Total de Usuarios + TotalUsers = await _context.Users.CountAsync(), + + // 5. Mensajes NO LEÍDOS (IsRead = false) + UnreadMessages = await _context.ChatMessages.CountAsync(m => !m.IsRead) + }; + return Ok(stats); + } + + [HttpGet("audit")] + public async Task GetAuditLogs( + [FromQuery] string? actionType = null, + [FromQuery] string? entity = null, + [FromQuery] int? userId = null, + [FromQuery] DateTime? fromDate = null, + [FromQuery] DateTime? toDate = null, + [FromQuery] int page = 1) + { + const int pageSize = 50; + var query = _context.AuditLogs.AsQueryable(); + + if (!string.IsNullOrEmpty(actionType)) + query = query.Where(l => l.Action == actionType); + + if (!string.IsNullOrEmpty(entity)) + query = query.Where(l => l.Entity == entity); + + if (userId.HasValue) + query = query.Where(l => l.UserID == userId.Value); + + if (fromDate.HasValue) + query = query.Where(l => l.CreatedAt >= fromDate.Value); + + if (toDate.HasValue) + query = query.Where(l => l.CreatedAt <= toDate.Value); + + var total = await query.CountAsync(); + var logs = await query + .OrderByDescending(l => l.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(l => new + { + l.AuditLogID, + l.Action, + l.Entity, + l.EntityID, + l.UserID, + l.Details, + l.CreatedAt, + UserName = l.UserID == 0 ? "Sistema" : _context.Users.Where(u => u.UserID == l.UserID).Select(u => u.UserName).FirstOrDefault() ?? "Desconocido" + }) + .ToListAsync(); + + return Ok(new { logs, total, page, pageSize }); + } + + // BÚSQUEDA DE USUARIOS PARA ASIGNAR AVISO + [HttpGet("users/search")] + public async Task SearchUsers([FromQuery] string q) + { + if (string.IsNullOrEmpty(q)) return Ok(new List()); + + var users = await _context.Users + .Where(u => (u.Email ?? "").Contains(q) || (u.UserName ?? "").Contains(q) || (u.FirstName ?? "").Contains(q) || (u.LastName ?? "").Contains(q)) + .Take(10) + .Select(u => new { u.UserID, u.UserName, u.Email, u.FirstName, u.LastName, u.PhoneNumber, u.IsBlocked }) + .ToListAsync(); + + return Ok(users); + } + + // GESTIÓN DE BLOQUEO + [HttpPost("users/{id}/toggle-block")] + public async Task ToggleBlockUser(int id) + { + var user = await _context.Users.FindAsync(id); + if (user == null) return NotFound(); + + // Evitar autobloqueo + var currentAdminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + if (user.UserID == currentAdminId) return BadRequest("No puedes bloquearte a ti mismo."); + + user.IsBlocked = !user.IsBlocked; + + // 📝 AUDITORÍA + _context.AuditLogs.Add(new AuditLog + { + Action = user.IsBlocked ? "USER_BLOCKED" : "USER_UNBLOCKED", + Entity = "User", + EntityID = user.UserID, + UserID = currentAdminId, + Details = $"Usuario '{user.UserName}' {(user.IsBlocked ? "bloqueado" : "desbloqueado")} por administrador." + }); + + await _context.SaveChangesAsync(); + + return Ok(new { message = user.IsBlocked ? "Usuario bloqueado" : "Usuario desbloqueado", isBlocked = user.IsBlocked }); + } + + // REPUBLICAR AVISO VENCIDO + [HttpPost("ads/{id}/republish")] + public async Task RepublishAd(int id) + { + var ad = await _context.Ads.Include(a => a.Brand).FirstOrDefaultAsync(a => a.AdID == id); + if (ad == null) return NotFound(); + + // Verificación de seguridad: solo se pueden republicar avisos vencidos. + if (ad.StatusID != (int)AdStatusEnum.Expired) + { + return BadRequest("Solo se pueden republicar avisos que se encuentren vencidos."); + } + + // Actualizamos el estado y las fechas para un nuevo ciclo de 30 días. + ad.StatusID = (int)AdStatusEnum.Active; + ad.PublishedAt = DateTime.UtcNow; // La nueva fecha de publicación es ahora. + ad.ExpiresAt = DateTime.UtcNow.AddDays(30); + ad.ExpirationWarningSent = false; // Reseteamos la advertencia de expiración. + + // Audit Log + var adminId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + _context.AuditLogs.Add(new AuditLog + { + Action = "AD_REPUBLISHED_BY_ADMIN", + Entity = "Ad", + EntityID = id, + UserID = adminId, + Details = $"Aviso ID {id} ('{ad.Brand?.Name} {ad.VersionName}') republicado por administrador." + }); + + await _context.SaveChangesAsync(); + + // Aquí podrías agregar una sincronización con el sistema legacy si fuera necesario. + // await _syncService.SyncAdToLegacyAsync(id); + + return Ok(new { message = "Aviso republicado y activado por 30 días." }); + } +} diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs b/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs new file mode 100644 index 0000000..3e2cb5c --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/AdsV2Controller.cs @@ -0,0 +1,816 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MotoresArgentinosV2.Infrastructure.Data; +using MotoresArgentinosV2.Core.Entities; +using MotoresArgentinosV2.Infrastructure.Services; +using MotoresArgentinosV2.Core.Interfaces; +using MotoresArgentinosV2.Core.DTOs; +using Microsoft.AspNetCore.Authorization; + +namespace MotoresArgentinosV2.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AdsV2Controller : ControllerBase +{ + private readonly MotoresV2DbContext _context; + private readonly IImageStorageService _imageService; + private readonly IIdentityService _identityService; + + public AdsV2Controller(MotoresV2DbContext context, IImageStorageService imageService, IIdentityService identityService) + { + _context = context; + _imageService = imageService; + _identityService = identityService; + } + + private int GetCurrentUserId() + { + return int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + } + + private bool IsUserAdmin() + { + return User.IsInRole("Admin"); + } + + [HttpGet] + public async Task GetAll( + [FromQuery] string? q, + [FromQuery] string? c, + [FromQuery] int? minPrice, + [FromQuery] int? maxPrice, + [FromQuery] string? currency, + [FromQuery] int? minYear, + [FromQuery] int? maxYear, + [FromQuery] int? userId, + [FromQuery] int? brandId, + [FromQuery] int? modelId, + [FromQuery] string? fuel, + [FromQuery] string? transmission, + [FromQuery] string? color, + [FromQuery] bool? isFeatured) + { + var query = _context.Ads + .Include(a => a.Photos) + .Include(a => a.Features) + .Include(a => a.Brand) + .Include(a => a.Model) + .AsQueryable(); + + if (isFeatured.HasValue) query = query.Where(a => a.IsFeatured == isFeatured.Value); + + if (brandId.HasValue) query = query.Where(a => a.BrandID == brandId.Value); + if (modelId.HasValue) query = query.Where(a => a.ModelID == modelId.Value); + + if (!string.IsNullOrEmpty(currency)) + { + query = query.Where(a => a.Currency == currency); + } + + if (minPrice.HasValue) query = query.Where(a => a.Price >= minPrice.Value); + if (maxPrice.HasValue) query = query.Where(a => a.Price <= maxPrice.Value); + + if (!string.IsNullOrEmpty(fuel)) + query = query.Where(a => a.FuelType == fuel || a.Features.Any(f => f.FeatureKey == "Combustible" && f.FeatureValue == fuel)); + + if (!string.IsNullOrEmpty(transmission)) + query = query.Where(a => a.Transmission == transmission || a.Features.Any(f => f.FeatureKey == "Transmision" && f.FeatureValue == transmission)); + + if (!string.IsNullOrEmpty(color)) + query = query.Where(a => a.Color == color || a.Features.Any(f => f.FeatureKey == "Color" && f.FeatureValue == color)); + + if (!string.IsNullOrEmpty(Request.Query["segment"])) + { + var segment = Request.Query["segment"].ToString(); + query = query.Where(a => a.Segment == segment); + } + if (!string.IsNullOrEmpty(Request.Query["location"])) + { + var loc = Request.Query["location"].ToString(); + query = query.Where(a => a.Location != null && a.Location.Contains(loc)); + } + if (!string.IsNullOrEmpty(Request.Query["condition"])) + { + var cond = Request.Query["condition"].ToString(); + query = query.Where(a => a.Condition == cond); + } + + if (!string.IsNullOrEmpty(Request.Query["steering"])) + { + var st = Request.Query["steering"].ToString(); + query = query.Where(a => a.Steering == st); + } + + if (int.TryParse(Request.Query["doorCount"], out int dc)) + { + query = query.Where(a => a.DoorCount == dc); + } + + if (!userId.HasValue) + { + var publicStatuses = new[] { + (int)AdStatusEnum.Active, + (int)AdStatusEnum.Sold, + (int)AdStatusEnum.Reserved + }; + query = query.Where(a => publicStatuses.Contains(a.StatusID)); + } + else + { + query = query.Where(a => a.UserID == userId.Value); + } + + // --- LÓGICA DE BÚSQUEDA POR PALABRAS --- + if (!string.IsNullOrEmpty(q)) + { + // 1. Dividimos el término de búsqueda en palabras individuales. + var keywords = q.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + // 2. Por cada palabra, agregamos una condición 'Where'. + // Esto crea un AND implícito: el aviso debe coincidir con TODAS las palabras. + foreach (var keyword in keywords) + { + var lowerKeyword = keyword.ToLower(); + query = query.Where(a => + (a.Brand != null && a.Brand.Name.ToLower().Contains(lowerKeyword)) || + (a.Model != null && a.Model.Name.ToLower().Contains(lowerKeyword)) || + (a.VersionName != null && a.VersionName.ToLower().Contains(lowerKeyword)) + ); + } + } + + if (!string.IsNullOrEmpty(c)) + { + int typeId = c == "EAUTOS" ? 1 : (c == "EMOTOS" ? 2 : 0); + if (typeId > 0) query = query.Where(a => a.VehicleTypeID == typeId); + } + + if (minPrice.HasValue) query = query.Where(a => a.Price >= minPrice.Value); + if (maxPrice.HasValue) query = query.Where(a => a.Price <= maxPrice.Value); + if (minYear.HasValue) query = query.Where(a => a.Year >= minYear.Value); + if (maxYear.HasValue) query = query.Where(a => a.Year <= maxYear.Value); + + var results = await query.OrderByDescending(a => a.IsFeatured) + .ThenByDescending(a => a.CreatedAt) + .Select(a => new + { + id = a.AdID, + brandName = a.Brand != null ? a.Brand.Name : null, + versionName = a.VersionName, + price = a.Price, + currency = a.Currency, + year = a.Year, + km = a.KM, + image = a.Photos.OrderBy(p => p.SortOrder).Select(p => p.FilePath).FirstOrDefault(), + isFeatured = a.IsFeatured, + statusId = a.StatusID, + viewsCounter = a.ViewsCounter, + + createdAt = a.CreatedAt, + location = a.Location, + fuelType = a.FuelType, + color = a.Color, + segment = a.Segment, + condition = a.Condition, + doorCount = a.DoorCount, + transmission = a.Transmission, + steering = a.Steering + }) + .ToListAsync(); + + return Ok(results); + } + + [HttpGet("search-suggestions")] + public async Task GetSearchSuggestions([FromQuery] string term) + { + if (string.IsNullOrEmpty(term) || term.Length < 2) + { + return Ok(new List()); + } + + // Buscar en Marcas + var brands = await _context.Brands + .Where(b => b.Name.Contains(term)) + .Select(b => b.Name) + .ToListAsync(); + + // Buscar en Modelos + var models = await _context.Models + .Where(m => m.Name.Contains(term)) + .Select(m => m.Name) + .ToListAsync(); + + // Combinar, eliminar duplicados y tomar los primeros 5 + var suggestions = brands.Concat(models) + .Distinct() + .OrderBy(s => s) + .Take(5) + .ToList(); + + return Ok(suggestions); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var ad = await _context.Ads + .Include(a => a.Photos) + .Include(a => a.Features) + .Include(a => a.Brand) + .Include(a => a.Model) + .Include(a => a.User) + .FirstOrDefaultAsync(a => a.AdID == id); + + if (ad == null) return NotFound(); + + // 1. SEGURIDAD Y CONTROL DE ACCESO + // Obtenemos datos del usuario actual + var userIdStr = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + int currentUserId = int.TryParse(userIdStr, out int uid) ? uid : 0; + bool isAdmin = User.IsInRole("Admin"); + bool isOwner = ad.UserID == currentUserId; + + // Definimos qué estados son visibles para todo el mundo + var publicStatuses = new[] { + (int)AdStatusEnum.Active, + (int)AdStatusEnum.Reserved, + (int)AdStatusEnum.Sold + }; + + bool isPublic = publicStatuses.Contains(ad.StatusID); + + // REGLA A: Si está ELIMINADO (9), nadie lo ve por URL pública (solo admin podría en dashboard) + if (ad.StatusID == (int)AdStatusEnum.Deleted && !isAdmin) + { + return NotFound(); + } + + // REGLA B: Si NO es público (ej: Vencido, Borrador, Pausado) y NO es dueño ni admin -> 404 + if (!isPublic && !isOwner && !isAdmin) + { + return NotFound(); + } + + // 2. LÓGICA DE CONTEO + try + { + // REGLA: Solo contamos si NO es el dueño (isOwner ya calculado arriba) + if (!isOwner) + { + var userIp = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0"; + var cutoffDate = DateTime.UtcNow.AddHours(-24); + + // Verificamos si esta IP ya vio este aviso en las últimas 24hs + var alreadyViewed = await _context.AdViewLogs + .AnyAsync(v => v.AdID == id && v.IPAddress == userIp && v.ViewDate > cutoffDate); + + if (!alreadyViewed) + { + // A. Registrar Log + _context.AdViewLogs.Add(new AdViewLog + { + AdID = id, + IPAddress = userIp, + ViewDate = DateTime.UtcNow + }); + + // B. Incrementar Contador + await _context.Ads + .Where(a => a.AdID == id) + .ExecuteUpdateAsync(s => s.SetProperty(a => a.ViewsCounter, a => a.ViewsCounter + 1)); + + await _context.SaveChangesAsync(); + } + } + } + catch (Exception) + { + // Ignoramos errores de conteo para no bloquear la visualización + } + + // 3. RESPUESTA + return Ok(new + { + adID = ad.AdID, + userID = ad.UserID, + ownerUserType = ad.User?.UserType, + vehicleTypeID = ad.VehicleTypeID, + brandID = ad.BrandID, + modelID = ad.ModelID, + versionName = ad.VersionName, + brandName = ad.Brand?.Name, + + year = ad.Year, + km = ad.KM, + price = ad.Price, + currency = ad.Currency, + description = ad.Description, + isFeatured = ad.IsFeatured, + statusID = ad.StatusID, + createdAt = ad.CreatedAt, + publishedAt = ad.PublishedAt, + expiresAt = ad.ExpiresAt, + legacyAdID = ad.LegacyAdID, + viewsCounter = ad.ViewsCounter, + fuelType = ad.FuelType, + color = ad.Color, + segment = ad.Segment, + location = ad.Location, + condition = ad.Condition, + doorCount = ad.DoorCount, + transmission = ad.Transmission, + steering = ad.Steering, + contactPhone = ad.ContactPhone, + contactEmail = ad.ContactEmail, + displayContactInfo = ad.DisplayContactInfo, + photos = ad.Photos.Select(p => new { p.PhotoID, p.FilePath, p.IsCover, p.SortOrder }), + features = ad.Features.Select(f => new { f.FeatureKey, f.FeatureValue }), + brand = ad.Brand != null ? new { id = ad.Brand.BrandID, name = ad.Brand.Name } : null, + model = ad.Model != null ? new { id = ad.Model.ModelID, name = ad.Model.Name } : null + }); + } + + [HttpPost] + public async Task Create([FromBody] CreateAdRequestDto request) + { + var currentUserId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + var userRole = User.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value; + bool isAdmin = userRole == "Admin"; + + int finalUserId = currentUserId; + + if (isAdmin) + { + if (request.TargetUserID.HasValue) + { + finalUserId = request.TargetUserID.Value; + } + else if (!string.IsNullOrEmpty(request.GhostUserEmail)) + { + // Pasamos nombre y apellido por separado + var ghost = await _identityService.CreateGhostUserAsync( + request.GhostUserEmail, + request.GhostFirstName ?? "Usuario", + request.GhostLastName ?? "", + request.GhostUserPhone ?? "" + ); + finalUserId = ghost.UserID; + } + } + + // 🟢 LÓGICA DE MODELO DINÁMICO + int finalModelId = request.ModelID; + + // Si el front manda 0 (modelo nuevo escrito a mano) + if (finalModelId == 0 && !string.IsNullOrEmpty(request.VersionName)) + { + // Intentamos extraer el nombre del modelo desde el VersionName + var brand = await _context.Brands.FindAsync(request.BrandID); + string modelNameCandidate = request.VersionName; + + // Si el nombre de versión empieza con la marca (ej: "Fiat 600"), quitamos la marca + if (brand != null && modelNameCandidate.StartsWith(brand.Name, StringComparison.OrdinalIgnoreCase)) + { + modelNameCandidate = modelNameCandidate.Substring(brand.Name.Length).Trim(); + } + + // Si quedó vacío o es muy genérico, usar un default + if (string.IsNullOrWhiteSpace(modelNameCandidate)) modelNameCandidate = "Modelo Desconocido"; + + // Opcional: Tomar solo la primera palabra como nombre del modelo para agrupar mejor + // var firstWord = modelNameCandidate.Split(' ')[0]; + // if (firstWord.Length > 2) modelNameCandidate = firstWord; + + // Buscar si ya existe este modelo para esta marca + var existingModel = await _context.Models + .FirstOrDefaultAsync(m => m.BrandID == request.BrandID && m.Name == modelNameCandidate); + + if (existingModel != null) + { + finalModelId = existingModel.ModelID; + } + else + { + // Crear Nuevo Modelo + var newModel = new Model + { + BrandID = request.BrandID, + Name = modelNameCandidate + }; + _context.Models.Add(newModel); + await _context.SaveChangesAsync(); // Guardar para obtener ID + finalModelId = newModel.ModelID; + } + } + + var ad = new Ad + { + UserID = finalUserId, + VehicleTypeID = request.VehicleTypeID, + BrandID = request.BrandID, + ModelID = finalModelId, // Usamos el ID resuelto + VersionName = request.VersionName, + Year = request.Year, + KM = request.KM, + Price = request.Price, + Currency = request.Currency, + Description = request.Description, + IsFeatured = request.IsFeatured, + + FuelType = request.FuelType, + Color = request.Color, + Segment = request.Segment, + Location = request.Location, + Condition = request.Condition, + DoorCount = request.DoorCount, + Transmission = request.Transmission, + Steering = request.Steering, + + ContactPhone = request.ContactPhone, + ContactEmail = request.ContactEmail, + DisplayContactInfo = request.DisplayContactInfo, + + CreatedAt = DateTime.UtcNow + }; + + if (isAdmin) + { + ad.StatusID = (int)AdStatusEnum.Active; + ad.PublishedAt = DateTime.UtcNow; + ad.ExpiresAt = DateTime.UtcNow.AddDays(30); + } + else + { + ad.StatusID = (int)AdStatusEnum.Draft; + } + + _context.Ads.Add(ad); + await _context.SaveChangesAsync(); + + // 📝 AUDITORÍA + var brandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? ""; + _context.AuditLogs.Add(new AuditLog + { + Action = "AD_CREATED", + Entity = "Ad", + EntityID = ad.AdID, + UserID = currentUserId, + Details = $"Aviso '{brandName} {ad.VersionName}' creado. Estado inicial: {ad.StatusID}" + }); + await _context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetById), new { id = ad.AdID }, ad); + } + + [HttpGet("brands/{vehicleTypeId}")] + public async Task GetBrands(int vehicleTypeId) + { + var brands = await _context.Set() + .Where(b => b.VehicleTypeID == vehicleTypeId) + .Select(b => new { id = b.BrandID, name = b.Name }) + .ToListAsync(); + return Ok(brands); + } + + [HttpGet("models/{brandId}")] + public async Task GetModels(int brandId) + { + var models = await _context.Set() + .Where(m => m.BrandID == brandId) + .Select(m => new { id = m.ModelID, name = m.Name }) + .ToListAsync(); + return Ok(models); + } + + [HttpGet("models/search")] + public async Task SearchModels([FromQuery] int brandId, [FromQuery] string query) + { + if (string.IsNullOrEmpty(query) || query.Length < 2) return Ok(new List()); + + var models = await _context.Models + .Where(m => m.BrandID == brandId && m.Name.Contains(query)) + .OrderBy(m => m.Name) + .Take(20) + .Select(m => new { id = m.ModelID, name = m.Name }) + .ToListAsync(); + + return Ok(models); + } + + [HttpPost("{id}/upload-photos")] + public async Task UploadPhotos(int id, [FromForm] IFormFileCollection files) + { + if (files == null || files.Count == 0) return BadRequest("No se recibieron archivos en la petición."); + + var ad = await _context.Ads.Include(a => a.Photos).FirstOrDefaultAsync(a => a.AdID == id); + if (ad == null) return NotFound("Aviso no encontrado."); + + // 🛡️ SEGURIDAD: Verificar propiedad o ser admin + var currentUserId = GetCurrentUserId(); + if (ad.UserID != currentUserId && !IsUserAdmin()) return Forbid(); + + int currentCount = ad.Photos.Count; + int availableSlots = 5 - currentCount; + + if (availableSlots <= 0) + { + return BadRequest($"Límite de 5 fotos alcanzado. Espacios disponibles: 0. Fotos actuales: {currentCount}"); + } + + var filesToProcess = files.Take(availableSlots).ToList(); + var uploadedCount = 0; + var errors = new List(); + + foreach (var file in filesToProcess) + { + if (file.Length > 0) + { + try + { + var relativePath = await _imageService.SaveAdImageAsync(id, file); + + _context.AdPhotos.Add(new AdPhoto + { + AdID = id, + FilePath = relativePath, + IsCover = !_context.AdPhotos.Any(p => p.AdID == id) && uploadedCount == 0, + SortOrder = currentCount + uploadedCount + }); + + uploadedCount++; + } + catch (Exception ex) + { + errors.Add($"{file.FileName}: {ex.Message}"); + } + } + } + + if (uploadedCount > 0) + { + await _context.SaveChangesAsync(); + return Ok(new + { + message = $"{uploadedCount} fotos procesadas.", + errors = errors.Any() ? errors : null + }); + } + + return BadRequest(new + { + message = "No se pudieron procesar las imágenes.", + details = errors + }); + } + + [HttpDelete("photos/{photoId}")] + public async Task DeletePhoto(int photoId) + { + var photo = await _context.AdPhotos.Include(p => p.Ad).FirstOrDefaultAsync(p => p.PhotoID == photoId); + if (photo == null) return NotFound(); + + // 🛡️ SEGURIDAD: Verificar propiedad o ser admin + var currentUserId = GetCurrentUserId(); + if (photo.Ad.UserID != currentUserId && !IsUserAdmin()) return Forbid(); + + // Eliminar del disco + _imageService.DeleteAdImage(photo.FilePath); + + // Eliminar de la base de datos + _context.AdPhotos.Remove(photo); + await _context.SaveChangesAsync(); + + return NoContent(); + } + + [HttpPut("{id}")] + [Authorize] + public async Task Update(int id, [FromBody] CreateAdRequestDto updatedAdDto) + { + var ad = await _context.Ads.FindAsync(id); + if (ad == null) return NotFound(); + + var userId = GetCurrentUserId(); + + if (ad.UserID != userId && !IsUserAdmin()) + { + return Forbid(); + } + + // Si está en moderación, no se puede editar (salvo Admin) + if (!IsUserAdmin() && ad.StatusID == (int)AdStatusEnum.ModerationPending) + { + return BadRequest("No puedes editar un aviso que está en proceso de revisión."); + } + + // --- Lógica de Modelo Dinámico (Si edita a un modelo nuevo) --- + int finalModelId = updatedAdDto.ModelID; + if (finalModelId == 0 && !string.IsNullOrEmpty(updatedAdDto.VersionName)) + { + var brand = await _context.Brands.FindAsync(updatedAdDto.BrandID); + string modelNameCandidate = updatedAdDto.VersionName; + if (brand != null && modelNameCandidate.StartsWith(brand.Name, StringComparison.OrdinalIgnoreCase)) + { + modelNameCandidate = modelNameCandidate.Substring(brand.Name.Length).Trim(); + } + if (string.IsNullOrWhiteSpace(modelNameCandidate)) modelNameCandidate = "Modelo Desconocido"; + + var existingModel = await _context.Models + .FirstOrDefaultAsync(m => m.BrandID == updatedAdDto.BrandID && m.Name == modelNameCandidate); + + if (existingModel != null) + { + finalModelId = existingModel.ModelID; + } + else + { + var newModel = new Model { BrandID = updatedAdDto.BrandID, Name = modelNameCandidate }; + _context.Models.Add(newModel); + await _context.SaveChangesAsync(); + finalModelId = newModel.ModelID; + } + } + + // --- Mapeo manual desde el DTO a la Entidad --- + ad.BrandID = updatedAdDto.BrandID; + ad.ModelID = finalModelId; // Usar el ID resuelto + ad.VersionName = updatedAdDto.VersionName; + ad.Year = updatedAdDto.Year; + ad.KM = updatedAdDto.KM; + ad.Price = updatedAdDto.Price; + ad.Currency = updatedAdDto.Currency; + ad.Description = updatedAdDto.Description; + + ad.FuelType = updatedAdDto.FuelType; + ad.Color = updatedAdDto.Color; + ad.Segment = updatedAdDto.Segment; + ad.Location = updatedAdDto.Location; + ad.Condition = updatedAdDto.Condition; + ad.DoorCount = updatedAdDto.DoorCount; + ad.Transmission = updatedAdDto.Transmission; + ad.Steering = updatedAdDto.Steering; + ad.ContactPhone = updatedAdDto.ContactPhone; + ad.ContactEmail = updatedAdDto.ContactEmail; + ad.DisplayContactInfo = updatedAdDto.DisplayContactInfo; + // Nota: IsFeatured y otros campos sensibles se manejan por separado (pago/admin) + + // 📝 AUDITORÍA + var adBrandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? ""; + _context.AuditLogs.Add(new AuditLog + { + Action = "AD_UPDATED", + Entity = "Ad", + EntityID = ad.AdID, + UserID = userId, + Details = $"Aviso ID {ad.AdID} ({adBrandName} {ad.VersionName}) actualizado por el propietario/admin." + }); + + await _context.SaveChangesAsync(); + return Ok(ad); + } + + [HttpDelete("{id}")] + [Authorize] + public async Task Delete(int id) + { + var ad = await _context.Ads.FindAsync(id); + if (ad == null) return NotFound(); + + var userId = GetCurrentUserId(); + + if (ad.UserID != userId && !IsUserAdmin()) + { + return Forbid(); + } + + ad.StatusID = (int)AdStatusEnum.Deleted; + ad.DeletedAt = DateTime.UtcNow; + + // 📝 AUDITORÍA + var delBrandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? ""; + _context.AuditLogs.Add(new AuditLog + { + Action = "AD_DELETED_SOFT", + Entity = "Ad", + EntityID = ad.AdID, + UserID = userId, + Details = $"Aviso ID {ad.AdID} ({delBrandName} {ad.VersionName}) marcado como eliminado (borrado lógico)." + }); + + await _context.SaveChangesAsync(); + return NoContent(); + } + + [HttpPost("favorites")] + public async Task AddFavorite([FromBody] Favorite fav) + { + if (await _context.Favorites.AnyAsync(f => f.UserID == fav.UserID && f.AdID == fav.AdID)) + return BadRequest("Ya es favorito"); + + _context.Favorites.Add(fav); + await _context.SaveChangesAsync(); + return Ok(); + } + + [HttpDelete("favorites")] + public async Task RemoveFavorite([FromQuery] int userId, [FromQuery] int adId) + { + var fav = await _context.Favorites.FindAsync(userId, adId); + if (fav == null) return NotFound(); + + _context.Favorites.Remove(fav); + await _context.SaveChangesAsync(); + return NoContent(); + } + + [HttpGet("favorites/{userId}")] + public async Task GetFavorites(int userId) + { + var ads = await _context.Favorites + .Where(f => f.UserID == userId) + .Join(_context.Ads, f => f.AdID, a => a.AdID, (f, a) => a) + .Include(a => a.Photos) + .Select(a => new + { + id = a.AdID, + BrandName = a.Brand != null ? a.Brand.Name : null, + VersionName = a.VersionName, + price = a.Price, + currency = a.Currency, + year = a.Year, + km = a.KM, + image = a.Photos.OrderBy(p => p.SortOrder).Select(p => p.FilePath).FirstOrDefault() + }) + .ToListAsync(); + + return Ok(ads); + } + + [HttpPatch("{id}/status")] + public async Task ChangeStatus(int id, [FromBody] int newStatus) + { + var ad = await _context.Ads.FindAsync(id); + if (ad == null) return NotFound(); + + var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + var userRole = User.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value; + bool isAdmin = userRole == "Admin"; + + if (ad.UserID != userId && !isAdmin) return Forbid(); + + // 🛡️ VALIDACIONES DE ESTADO + if (!isAdmin) + { + // 1. No activar borradores sin pagar + if (ad.StatusID == (int)AdStatusEnum.Draft || ad.StatusID == (int)AdStatusEnum.PaymentPending) + { + return BadRequest("Debes completar el pago para activar este aviso."); + } + + // 2. NUEVO: No tocar si está en moderación + if (ad.StatusID == (int)AdStatusEnum.ModerationPending) + { + return BadRequest("El aviso está en revisión. Espera la aprobación del administrador."); + } + } + + // Validar estados destino permitidos para el usuario + var allowedUserStatuses = new[] { 4, 6, 7, 9, 10 }; + if (!isAdmin && !allowedUserStatuses.Contains(newStatus)) + { + return BadRequest("Estado destino no permitido."); + } + + int oldStatus = ad.StatusID; + ad.StatusID = newStatus; + + // 📝 AUDITORÍA + var statusBrandName = (await _context.Brands.FindAsync(ad.BrandID))?.Name ?? ""; + _context.AuditLogs.Add(new AuditLog + { + Action = "AD_STATUS_CHANGED", + Entity = "Ad", + EntityID = ad.AdID, + UserID = userId, + Details = $"Estado de aviso ({statusBrandName} {ad.VersionName}) cambiado de {oldStatus} a {newStatus}." + }); + + await _context.SaveChangesAsync(); + + return Ok(new { message = "Estado actualizado" }); + } + + [HttpGet("payment-methods")] + public async Task GetPaymentMethods() + { + var methods = await _context.PaymentMethods + .OrderBy(p => p.PaymentMethodID) + .Select(p => new { id = p.PaymentMethodID, mediodepago = p.Name }) + .ToListAsync(); + + return Ok(methods); + } +} \ No newline at end of file diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs b/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs new file mode 100644 index 0000000..f4b34f7 --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/AuthController.cs @@ -0,0 +1,485 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MotoresArgentinosV2.Core.Interfaces; +using MotoresArgentinosV2.Infrastructure.Data; +using MotoresArgentinosV2.Core.Entities; +using MotoresArgentinosV2.Core.DTOs; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.RateLimiting; + +namespace MotoresArgentinosV2.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +// CORRECCIÓN: Se quitó [EnableRateLimiting("AuthPolicy")] de aquí para no bloquear /me ni /logout +public class AuthController : ControllerBase +{ + private readonly IIdentityService _identityService; + private readonly ITokenService _tokenService; + private readonly INotificationService _notificationService; + private readonly MotoresV2DbContext _context; + + public AuthController(IIdentityService identityService, ITokenService tokenService, INotificationService notificationService, MotoresV2DbContext context) + { + _identityService = identityService; + _tokenService = tokenService; + _notificationService = notificationService; + _context = context; + } + + // Helper privado para cookies + private void SetTokenCookie(string token, string cookieName) + { + var cookieOptions = new CookieOptions + { + HttpOnly = true, // Seguridad: JS no puede leer esto + Expires = DateTime.UtcNow.AddDays(7), + Secure = true, // Solo HTTPS (localhost con https cuenta) + SameSite = SameSiteMode.Strict, + IsEssential = true + }; + Response.Cookies.Append(cookieName, token, cookieOptions); + } + + [HttpPost("login")] + [EnableRateLimiting("AuthPolicy")] // PROTEGIDO (5 intentos/min) + public async Task Login([FromBody] LoginRequest request) + { + var (user, message) = await _identityService.AuthenticateAsync(request.Username, request.Password); + + if (user == null) + { + if (message == "EMAIL_NOT_VERIFIED") + return Unauthorized(new { message = "Debes verificar tu email antes de ingresar." }); + + if (message == "USER_BLOCKED") + return Unauthorized(new { message = "Tu usuario ha sido bloqueado por un administrador." }); + + return Unauthorized(new { message = "Credenciales inválidas" }); + } + + if (message == "FORCE_PASSWORD_CHANGE") + { + return Ok(new { status = "MIGRATION_REQUIRED", username = user.UserName }); + } + + // Lógica MFA (Si aplica) + if (user.IsMFAEnabled) + { + return Ok(new { status = "TOTP_REQUIRED", username = user.UserName }); + } + if (user.UserType == 3 && string.IsNullOrEmpty(user.MFASecret)) // Admin forzar setup + { + var secret = _tokenService.GenerateBase32Secret(); + user.MFASecret = secret; + await _context.SaveChangesAsync(); + var qrUri = _tokenService.GetQrCodeUri(user.Email, secret); + return Ok(new { status = "MFA_SETUP_REQUIRED", username = user.UserName, qrUri, secret }); + } + + // --- LOGIN EXITOSO --- + + // 1. Generar Tokens + var jwtToken = _tokenService.GenerateJwtToken(user); + var refreshToken = _tokenService.GenerateRefreshToken(HttpContext.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0"); + + // 2. Guardar RefreshToken en DB + // Asegúrate de que la propiedad RefreshTokens exista en User (Paso 2 abajo) + user.RefreshTokens.Add(refreshToken); + await _context.SaveChangesAsync(); + + // 3. Setear Cookies + SetTokenCookie(jwtToken, "accessToken"); + SetTokenCookie(refreshToken.Token, "refreshToken"); + + // 4. Audit Log + _context.AuditLogs.Add(new AuditLog + { + Action = "LOGIN_SUCCESS", + Entity = "User", + EntityID = user.UserID, + UserID = user.UserID, + Details = "Login con Cookies exitoso" + }); + await _context.SaveChangesAsync(); + + // 5. Retornar User (Sin el token, porque va en cookie) + return Ok(new + { + status = "SUCCESS", + recommendMfa = !user.IsMFAEnabled, + user = new + { + id = user.UserID, + username = user.UserName, + email = user.Email, + firstName = user.FirstName, + lastName = user.LastName, + userType = user.UserType, + isMFAEnabled = user.IsMFAEnabled + } + }); + } + + [HttpPost("refresh-token")] + // NO PROTEGIDO ESTRICTAMENTE (Usa límite global) + public async Task RefreshToken() + { + var refreshToken = Request.Cookies["refreshToken"]; + if (string.IsNullOrEmpty(refreshToken)) + return Unauthorized(new { message = "Token no proporcionado" }); + + var user = await _context.Users + .Include(u => u.RefreshTokens) + .FirstOrDefaultAsync(u => u.RefreshTokens.Any(t => t.Token == refreshToken)); + + if (user == null) return Unauthorized(new { message = "Usuario no encontrado" }); + + var storedToken = user.RefreshTokens.SingleOrDefault(x => x.Token == refreshToken); + + if (storedToken == null || !storedToken.IsActive) + return Unauthorized(new { message = "Token inválido o expirado" }); + + // Rotar Refresh Token + var newRefreshToken = _tokenService.GenerateRefreshToken(HttpContext.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0"); + + storedToken.Revoked = DateTime.UtcNow; + storedToken.RevokedByIp = HttpContext.Connection.RemoteIpAddress?.ToString(); + storedToken.ReplacedByToken = newRefreshToken.Token; + + user.RefreshTokens.Add(newRefreshToken); + await _context.SaveChangesAsync(); + + // Nuevo JWT + var newJwtToken = _tokenService.GenerateJwtToken(user); + + // Actualizar Cookies + SetTokenCookie(newJwtToken, "accessToken"); + SetTokenCookie(newRefreshToken.Token, "refreshToken"); + + return Ok(new { message = "Token renovado" }); + } + + [HttpPost("logout")] + // NO PROTEGIDO ESTRICTAMENTE + public IActionResult Logout() + { + Response.Cookies.Delete("accessToken"); + Response.Cookies.Delete("refreshToken"); + return Ok(new { message = "Sesión cerrada" }); + } + + [HttpGet("me")] + // 1. Sin [Authorize] para controlar nosotros la respuesta + public async Task GetMe() + { + // 2. Verificamos si existe la cookie de acceso + var hasToken = Request.Cookies.ContainsKey("accessToken"); + + // 3. Si NO tiene cookie, es un visitante anónimo. + // Devolvemos 200 OK con null para no ensuciar la consola con 401. + if (!hasToken) + { + return Ok(null); + } + + // 4. Si TIENE cookie, verificamos si el Middleware pudo leer el usuario. + var userIdStr = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + + // Si tiene cookie pero userIdStr es null, significa que el token expiró o es inválido. + // Aquí SI devolvemos 401 para disparar el "Auto Refresh" del frontend. + if (string.IsNullOrEmpty(userIdStr)) + { + return Unauthorized(); + } + + // 5. Todo válido, buscamos datos + var userId = int.Parse(userIdStr); + var user = await _context.Users.FindAsync(userId); + + if (user == null) return Unauthorized(); // Usuario borrado de DB + + return Ok(new + { + id = user.UserID, + username = user.UserName, + email = user.Email, + firstName = user.FirstName, + lastName = user.LastName, + userType = user.UserType, + phoneNumber = user.PhoneNumber, + isMFAEnabled = user.IsMFAEnabled + }); + } + + // Permite a un usuario logueado iniciar el proceso de MFA voluntariamente + [Authorize] + [HttpPost("init-mfa")] + public async Task InitMFA() + { + var userIdStr = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userIdStr)) return Unauthorized(); + + var user = await _context.Users.FindAsync(int.Parse(userIdStr)); + if (user == null) return NotFound(); + + // Generar secreto si no tiene + if (string.IsNullOrEmpty(user.MFASecret)) + { + user.MFASecret = _tokenService.GenerateBase32Secret(); + await _context.SaveChangesAsync(); + } + + // 📝 AUDITORÍA + _context.AuditLogs.Add(new AuditLog + { + Action = "MFA_INITIATED", + Entity = "User", + EntityID = user.UserID, + UserID = user.UserID, + Details = "Proceso de configuración de MFA (TOTP) iniciado." + }); + await _context.SaveChangesAsync(); + + var qrUri = _tokenService.GetQrCodeUri(user.Email, user.MFASecret); + + return Ok(new + { + qrUri = qrUri, + secret = user.MFASecret + }); + } + + [HttpPost("verify-mfa")] + [EnableRateLimiting("AuthPolicy")] + public async Task VerifyMFA([FromBody] MFARequest request) + { + var user = await _context.Users.FirstOrDefaultAsync(u => u.UserName == request.Username); + if (user == null || string.IsNullOrEmpty(user.MFASecret)) + { + return Unauthorized(new { message = "Sesión de autenticación inválida" }); + } + + bool isValid = _tokenService.ValidateTOTP(user.MFASecret, request.Code); + if (!isValid) + { + return Unauthorized(new { message = "Código de verificación inválido" }); + } + + if (!user.IsMFAEnabled) + { + user.IsMFAEnabled = true; + await _context.SaveChangesAsync(); + } + + var token = _tokenService.GenerateJwtToken(user); + var refreshToken = _tokenService.GenerateRefreshToken(HttpContext.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0"); + + // Cargar colección explícitamente para evitar errores de EF + await _context.Entry(user).Collection(u => u.RefreshTokens).LoadAsync(); + + user.RefreshTokens.Add(refreshToken); + await _context.SaveChangesAsync(); + + // Setear Cookies Seguras + SetTokenCookie(token, "accessToken"); + SetTokenCookie(refreshToken.Token, "refreshToken"); + + _context.AuditLogs.Add(new AuditLog + { + Action = "LOGIN_TOTP_SUCCESS", + Entity = "User", + EntityID = user.UserID, + UserID = user.UserID, + Details = "Login con TOTP (Authenticator) exitoso" + }); + await _context.SaveChangesAsync(); + + return Ok(new + { + status = "SUCCESS", + // user: retornamos datos para el frontend, pero NO el token + user = new + { + id = user.UserID, + username = user.UserName, + email = user.Email, + firstName = user.FirstName, + lastName = user.LastName, + userType = user.UserType, + isMFAEnabled = user.IsMFAEnabled + } + }); + } + + [Authorize] + [HttpPost("disable-mfa")] + public async Task DisableMFA() + { + var userIdStr = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userIdStr)) return Unauthorized(); + + var user = await _context.Users.FindAsync(int.Parse(userIdStr)); + if (user == null) return NotFound(); + + // Desactivamos MFA y limpiamos el secreto por seguridad + user.IsMFAEnabled = false; + user.MFASecret = null; + + // 📝 AUDITORÍA + _context.AuditLogs.Add(new AuditLog + { + Action = "MFA_DISABLED", + Entity = "User", + EntityID = user.UserID, + UserID = user.UserID, + Details = "Autenticación de dos factores desactivada por el usuario." + }); + + await _context.SaveChangesAsync(); + + return Ok(new { message = "MFA Desactivado correctamente." }); + } + + [HttpPost("migrate-password")] + [EnableRateLimiting("AuthPolicy")] + public async Task MigratePassword([FromBody] MigrateRequest request) + { + var success = await _identityService.MigratePasswordAsync(request.Username, request.NewPassword); + + if (!success) + { + return BadRequest(new { message = "No se pudo actualizar la contraseña" }); + } + + return Ok(new { message = "Contraseña actualizada con éxito. Ya puede iniciar sesión." }); + } + + [HttpPost("register")] + [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + public async Task Register([FromBody] RegisterRequest request) + { + var (success, message) = await _identityService.RegisterUserAsync(request); + if (!success) return BadRequest(new { message }); + + // 📝 AUDITORÍA + _context.AuditLogs.Add(new AuditLog + { + Action = "USER_REGISTERED", + Entity = "User", + EntityID = 0, + UserID = 0, + Details = $"Nuevo registro de usuario: {request.Email}" + }); + await _context.SaveChangesAsync(); + + return Ok(new { message }); + } + + [HttpPost("verify-email")] + [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + public async Task VerifyEmail([FromBody] VerifyEmailRequest request) + { + var (success, message) = await _identityService.VerifyEmailAsync(request.Token); + if (!success) return BadRequest(new { message }); + + // 📝 AUDITORÍA + _context.AuditLogs.Add(new AuditLog + { + Action = "EMAIL_VERIFIED", + Entity = "User", + EntityID = 0, + UserID = 0, + Details = "Correo electrónico verificado con éxito vía token." + }); + await _context.SaveChangesAsync(); + + return Ok(new { message }); + } + + [HttpPost("resend-verification")] + [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + public async Task ResendVerification([FromBody] ResendVerificationRequest request) + { + var (success, message) = await _identityService.ResendVerificationEmailAsync(request.Email); + if (!success) return BadRequest(new { message }); + return Ok(new { message }); + } + + [HttpPost("forgot-password")] + [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + public async Task ForgotPassword([FromBody] ForgotPasswordRequest request) + { + var (success, message) = await _identityService.ForgotPasswordAsync(request.Email); + + if (!success) + { + // Si falló por Rate Limit, devolvemos BadRequest o 429 Too Many Requests + return BadRequest(new { message }); + } + + return Ok(new { message }); + } + + [HttpPost("reset-password")] + [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + public async Task ResetPassword([FromBody] ResetPasswordRequest request) + { + var (success, message) = await _identityService.ResetPasswordAsync(request.Token, request.NewPassword); + if (!success) return BadRequest(new { message }); + + // 📝 AUDITORÍA + _context.AuditLogs.Add(new AuditLog + { + Action = "PASSWORD_RESET_SUCCESS", + Entity = "User", + EntityID = 0, // No tenemos el ID directo aquí sin buscarlo + UserID = 0, + Details = "Contraseña restablecida vía token de recuperación." + }); + await _context.SaveChangesAsync(); + + return Ok(new { message }); + } + + [Authorize] + [HttpPost("change-password")] + [EnableRateLimiting("AuthPolicy")] // PROTEGIDO + public async Task ChangePassword([FromBody] ChangePasswordRequest request) + { + var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + var (success, message) = await _identityService.ChangePasswordAsync(userId, request.CurrentPassword, request.NewPassword); + + if (!success) return BadRequest(new { message }); + + // Enviar notificación por mail + try + { + var user = await _context.Users.FindAsync(userId); + if (user != null) + { + await _notificationService.SendSecurityAlertEmailAsync(user.Email, "Cambio de contraseña"); + } + } + catch (Exception) { /* No bloqueamos el éxito si falla el mail */ } + + // 📝 AUDITORÍA + _context.AuditLogs.Add(new AuditLog + { + Action = "PASSWORD_CHANGED", + Entity = "User", + EntityID = userId, + UserID = userId, + Details = "Contraseña cambiada por el usuario desde la configuración." + }); + await _context.SaveChangesAsync(); + + return Ok(new { message }); + } +} + +public class ChangePasswordRequest +{ + public string CurrentPassword { get; set; } = string.Empty; + public string NewPassword { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Backend/MotoresArgentinosV2.API/Controllers/AvisosLegacyController.cs b/Backend/MotoresArgentinosV2.API/Controllers/AvisosLegacyController.cs new file mode 100644 index 0000000..eaafd27 --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/AvisosLegacyController.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Mvc; +using MotoresArgentinosV2.Core.DTOs; +using MotoresArgentinosV2.Core.Interfaces; + +namespace MotoresArgentinosV2.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AvisosLegacyController : ControllerBase +{ + private readonly IAvisosLegacyService _avisosService; + private readonly ILogger _logger; + + public AvisosLegacyController(IAvisosLegacyService avisosService, ILogger logger) + { + _avisosService = avisosService; + _logger = logger; + } + + /// + /// Obtiene las tarifas y configuración de avisos según la tarea y paquete + /// + /// Tipo de tarea (ej: EMOTORES, EAUTOS) + /// ID del paquete (opcional, default 0) + [HttpGet("configuracion")] + public async Task>> GetConfiguracion([FromQuery] string tarea, [FromQuery] int paquete = 0) + { + try + { + if (string.IsNullOrEmpty(tarea)) + { + return BadRequest("El parámetro 'tarea' es obligatorio."); + } + + var resultados = await _avisosService.ObtenerDatosAvisosAsync(tarea, paquete); + return Ok(resultados); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener configuración de avisos"); + return StatusCode(500, "Ocurrió un error interno al procesar la solicitud."); + } + } + + /// + /// Crea un nuevo aviso en el sistema legacy + /// + [HttpPost] + public async Task> CrearAviso([FromBody] InsertarAvisoDto dto) + { + try + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var resultado = await _avisosService.InsertarAvisoAsync(dto); + + if (resultado) + { + return CreatedAtAction(nameof(CrearAviso), true); + } + + return BadRequest("No se pudo crear el aviso."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear aviso para cliente {IdCliente}", dto.IdCliente); + return StatusCode(500, "Ocurrió un error interno al crear el aviso."); + } + } + [HttpGet("cliente/{nroDoc}")] + public async Task>> GetAvisosPorCliente(string nroDoc) + { + var avisos = await _avisosService.ObtenerAvisosPorClienteAsync(nroDoc); + return Ok(avisos); + } +} diff --git a/Backend/MotoresArgentinosV2.API/Controllers/ChatController.cs b/Backend/MotoresArgentinosV2.API/Controllers/ChatController.cs new file mode 100644 index 0000000..f0100de --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/ChatController.cs @@ -0,0 +1,126 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MotoresArgentinosV2.Infrastructure.Data; +using MotoresArgentinosV2.Core.Entities; +using MotoresArgentinosV2.Core.Interfaces; +using Microsoft.AspNetCore.Authorization; + +namespace MotoresArgentinosV2.API.Controllers; + +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class ChatController : ControllerBase +{ + private readonly MotoresV2DbContext _context; + private readonly INotificationService _notificationService; + + public ChatController(MotoresV2DbContext context, INotificationService notificationService) + { + _context = context; + _notificationService = notificationService; + } + + [HttpPost("send")] + public async Task SendMessage([FromBody] ChatMessage msg) + { + // 1. Guardar Mensaje + msg.SentAt = DateTime.UtcNow; + msg.IsRead = false; + + _context.ChatMessages.Add(msg); + await _context.SaveChangesAsync(); + + // 2. Notificar por Email (Con manejo de errores silencioso) + try + { + var receiver = await _context.Users.FindAsync(msg.ReceiverID); + var sender = await _context.Users.FindAsync(msg.SenderID); + + if (receiver != null && !string.IsNullOrEmpty(receiver.Email)) + { + // LÓGICA DE NOMBRE DE REMITENTE + string senderDisplayName; + + if (sender != null && sender.UserType == 3) // 3 = ADMIN + { + // Caso: Moderador escribe a Usuario + senderDisplayName = "Un moderador de Motores Argentinos"; + } + else + { + // Caso: Usuario responde a Moderador + string name = sender?.UserName ?? "Un usuario"; + senderDisplayName = $"El usuario {name}"; + } + + await _notificationService.SendChatNotificationEmailAsync( + receiver.Email, + senderDisplayName, // Pasamos el nombre formateado + msg.MessageText, + msg.AdID); + } + } + catch (Exception ex) + { + // Loguear error sin romper el flujo del chat + Console.WriteLine($"Error enviando notificación de chat: {ex.Message}"); + } + + return Ok(msg); + } + + [HttpGet("inbox/{userId}")] + public async Task GetInbox(int userId) + { + // Obtener todas las conversaciones donde el usuario es remitente o destinatario + var messages = await _context.ChatMessages + .Where(m => m.SenderID == userId || m.ReceiverID == userId) + .OrderByDescending(m => m.SentAt) + .ToListAsync(); + + return Ok(messages); + } + + [HttpGet("conversation/{adId}/{user1Id}/{user2Id}")] + public async Task GetConversation(int adId, int user1Id, int user2Id) + { + var messages = await _context.ChatMessages + .Where(m => m.AdID == adId && + ((m.SenderID == user1Id && m.ReceiverID == user2Id) || + (m.SenderID == user2Id && m.ReceiverID == user1Id))) + .OrderBy(m => m.SentAt) + .ToListAsync(); + + return Ok(messages); + } + + [HttpPost("mark-read/{messageId}")] + public async Task MarkRead(int messageId) + { + var msg = await _context.ChatMessages.FindAsync(messageId); + if (msg == null) return NotFound(); + + msg.IsRead = true; + await _context.SaveChangesAsync(); + return Ok(); + } + + [HttpGet("unread-count/{userId}")] + public async Task GetUnreadCount(int userId) + { + // Seguridad: Asegurarse de que el usuario que consulta es el dueño de los mensajes o un admin. + var currentUserId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + var isAdmin = User.IsInRole("Admin"); + + if (currentUserId != userId && !isAdmin) + { + return Forbid(); + } + + var count = await _context.ChatMessages + .CountAsync(m => m.ReceiverID == userId && !m.IsRead); + + return Ok(new { count }); + } +} diff --git a/Backend/MotoresArgentinosV2.API/Controllers/OperacionesLegacyController.cs b/Backend/MotoresArgentinosV2.API/Controllers/OperacionesLegacyController.cs new file mode 100644 index 0000000..f0eddce --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/OperacionesLegacyController.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.Mvc; +using MotoresArgentinosV2.Core.Entities; +using MotoresArgentinosV2.Core.Interfaces; + +namespace MotoresArgentinosV2.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class OperacionesLegacyController : ControllerBase +{ + private readonly IOperacionesLegacyService _operacionesService; + private readonly ILogger _logger; + + public OperacionesLegacyController(IOperacionesLegacyService operacionesService, ILogger logger) + { + _operacionesService = operacionesService; + _logger = logger; + } + + /// + /// Obtiene los medios de pago disponibles + /// + [HttpGet("medios-pago")] + public async Task>> GetMediosDePago() + { + try + { + var medios = await _operacionesService.ObtenerMediosDePagoAsync(); + return Ok(medios); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener medios de pago"); + return StatusCode(500, "Ocurrió un error interno al obtener medios de pago"); + } + } + + /// + /// Busca una operación por su número de operación + /// + [HttpGet("{noperacion}")] + public async Task>> GetOperacion(string noperacion) + { + try + { + var operaciones = await _operacionesService.ObtenerOperacionesPorNumeroAsync(noperacion); + + if (operaciones == null || !operaciones.Any()) + { + return NotFound($"No se encontraron operaciones con el número {noperacion}"); + } + + return Ok(operaciones); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al obtener operación {Noperacion}", noperacion); + return StatusCode(500, "Ocurrió un error interno al buscar la operación"); + } + } + + /// + /// Obtiene operaciones realizadas en un rango de fechas + /// + [HttpGet("buscar")] + public async Task>> GetOperacionesPorFecha([FromQuery] DateTime fechaInicio, [FromQuery] DateTime fechaFin) + { + try + { + if (fechaInicio > fechaFin) + { + return BadRequest("La fecha de inicio no puede ser mayor a la fecha de fin."); + } + + var operaciones = await _operacionesService.ObtenerOperacionesPorFechasAsync(fechaInicio, fechaFin); + return Ok(operaciones); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al buscar operaciones por fecha"); + return StatusCode(500, "Ocurrió un error interno al buscar operaciones."); + } + } + + /// + /// Registra una nueva operación de pago + /// + [HttpPost] + public async Task CrearOperacion([FromBody] Operacion operacion) + { + try + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + // Validar campos mínimos necesarios si es necesario + if (string.IsNullOrEmpty(operacion.Noperacion)) + { + return BadRequest("El número de operación es obligatorio."); + } + + var resultado = await _operacionesService.InsertarOperacionAsync(operacion); + + if (resultado) + { + return CreatedAtAction(nameof(GetOperacion), new { noperacion = operacion.Noperacion }, operacion); + } + + return BadRequest("No se pudo registrar la operación."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error al crear operación {Noperacion}", operacion.Noperacion); + return StatusCode(500, "Ocurrió un error interno al registrar la operación."); + } + } +} diff --git a/Backend/MotoresArgentinosV2.API/Controllers/PaymentsController.cs b/Backend/MotoresArgentinosV2.API/Controllers/PaymentsController.cs new file mode 100644 index 0000000..2a62472 --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/PaymentsController.cs @@ -0,0 +1,145 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MotoresArgentinosV2.Core.DTOs; +using MotoresArgentinosV2.Core.Interfaces; +using Microsoft.AspNetCore.RateLimiting; +using System.Security.Cryptography; +using System.Text; + +namespace MotoresArgentinosV2.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +[Authorize] +public class PaymentsController : ControllerBase +{ + private readonly IPaymentService _paymentService; + private readonly IConfiguration _config; + + public PaymentsController(IPaymentService paymentService, IConfiguration config) + { + _paymentService = paymentService; + _config = config; + } + + [HttpPost("process")] + [EnableRateLimiting("AuthPolicy")] + public async Task ProcessPayment([FromBody] CreatePaymentRequestDto request) + { + try + { + var userId = int.Parse(User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "0"); + + var result = await _paymentService.ProcessPaymentAsync(request, userId); + + if (result.Status == "approved") + { + return Ok(new { status = "approved", id = result.PaymentId }); + } + else if (result.Status == "in_process") + { + return Ok(new { status = "in_process", message = "El pago está siendo revisado." }); + } + else + { + return BadRequest(new { status = "rejected", detail = result.StatusDetail }); + } + } + catch (Exception ex) + { + return StatusCode(500, new { message = ex.Message }); + } + } + + // --- MÉTODO WEBHOOK ACTUALIZADO CON VALIDACIÓN DE FIRMA --- + [HttpPost("webhook")] + [AllowAnonymous] + public async Task MercadoPagoWebhook([FromQuery] string? topic, [FromQuery] string? id) + { + // 1. EXTRACCIÓN DE DATOS DE LA PETICIÓN + Request.Headers.TryGetValue("x-request-id", out var xRequestId); + Request.Headers.TryGetValue("x-signature", out var xSignature); + + var resourceId = id; + var resourceTopic = topic; + + if (string.IsNullOrEmpty(resourceId)) + { + try + { + Request.EnableBuffering(); + using var reader = new System.IO.StreamReader(Request.Body, leaveOpen: true); + var body = await reader.ReadToEndAsync(); + Request.Body.Position = 0; + + var json = System.Text.Json.JsonDocument.Parse(body); + if (json.RootElement.TryGetProperty("type", out var typeProp)) + { + resourceTopic = typeProp.GetString(); + } + if (json.RootElement.TryGetProperty("data", out var dataProp) && dataProp.TryGetProperty("id", out var idProp)) + { + resourceId = idProp.GetString(); + } + } + catch { /* Ignorar errores de lectura */ } + } + + if (string.IsNullOrEmpty(resourceId) || resourceTopic != "payment") + { + return Ok(); + } + + // 2. VALIDACIÓN DE LA FIRMA + var secret = _config["MercadoPago:WebhookSecret"]; + if (!string.IsNullOrEmpty(secret)) + { + var signatureParts = xSignature.ToString().Split(',') + .Select(part => part.Trim().Split('=')) + .ToDictionary(split => split[0], split => split.Length > 1 ? split[1] : ""); + + if (!signatureParts.TryGetValue("ts", out var ts) || !signatureParts.TryGetValue("v1", out var hash)) + { + return Unauthorized("Invalid signature header."); + } + + // Según la documentación de MP, el data.id debe estar en minúsculas para la firma. + var manifest = $"id:{resourceId.ToLower()};request-id:{xRequestId};ts:{ts};"; + + using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); + var computedHashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(manifest)); + var computedHash = Convert.ToHexString(computedHashBytes).ToLower(); + + if (computedHash != hash) + { + return Unauthorized("Signature mismatch."); + } + } + + // 3. PROCESAMIENTO (SOLO SI LA FIRMA ES VÁLIDA) + try + { + await _paymentService.ProcessWebhookAsync(resourceTopic, resourceId); + return Ok(); + } + catch (Exception ex) + { + Console.WriteLine($"Error procesando webhook validado: {ex.Message}"); + return StatusCode(500, "Internal server error processing webhook."); + } + } + + [HttpPost("check-status/{adId}")] + public async Task CheckStatus(int adId) + { + try + { + var result = await _paymentService.CheckPaymentStatusAsync(adId); + return Ok(result); + } + catch (Exception ex) + { + return BadRequest(new { message = ex.Message }); + } + } +} \ No newline at end of file diff --git a/Backend/MotoresArgentinosV2.API/Controllers/ProfileController.cs b/Backend/MotoresArgentinosV2.API/Controllers/ProfileController.cs new file mode 100644 index 0000000..c3ed282 --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/ProfileController.cs @@ -0,0 +1,74 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MotoresArgentinosV2.Core.DTOs; +using MotoresArgentinosV2.Core.Entities; +using MotoresArgentinosV2.Infrastructure.Data; +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; + +namespace MotoresArgentinosV2.API.Controllers; + +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class ProfileController : ControllerBase +{ + private readonly MotoresV2DbContext _context; + + public ProfileController(MotoresV2DbContext context) + { + _context = context; + } + + [HttpGet] + public async Task GetProfile() + { + var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "0"); + var user = await _context.Users + .Where(u => u.UserID == userId) + .Select(u => new + { + u.UserID, + u.UserName, + u.Email, + u.FirstName, + u.LastName, + u.PhoneNumber, + u.UserType, + u.CreatedAt, + u.IsEmailVerified + }) + .FirstOrDefaultAsync(); + + if (user == null) return NotFound(); + return Ok(user); + } + + [HttpPut] + public async Task UpdateProfile([FromBody] ProfileUpdateDto dto) + { + var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "0"); + var user = await _context.Users.FindAsync(userId); + + if (user == null) return NotFound(); + + user.FirstName = dto.FirstName; + user.LastName = dto.LastName; + user.PhoneNumber = dto.PhoneNumber; + + await _context.SaveChangesAsync(); + + // Audit Log + _context.AuditLogs.Add(new AuditLog + { + Action = "PROFILE_UPDATED", + Entity = "User", + EntityID = userId, + UserID = userId, + Details = "Usuario actualizó su perfil personal." + }); + await _context.SaveChangesAsync(); + + return Ok(new { message = "Perfil actualizado con éxito." }); + } +} diff --git a/Backend/MotoresArgentinosV2.API/Controllers/SeedController.cs b/Backend/MotoresArgentinosV2.API/Controllers/SeedController.cs new file mode 100644 index 0000000..b8ee48b --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/SeedController.cs @@ -0,0 +1,180 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MotoresArgentinosV2.Infrastructure.Data; +using MotoresArgentinosV2.Core.Entities; +using MotoresArgentinosV2.Core.Interfaces; + +namespace MotoresArgentinosV2.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class SeedController : ControllerBase +{ + private readonly MotoresV2DbContext _context; + private readonly IPasswordService _passwordService; + + public SeedController(MotoresV2DbContext context, IPasswordService passwordService) + { + _context = context; + _passwordService = passwordService; + } + + [HttpPost("database")] + public async Task SeedDatabase() + { + // 1. Asegurar Marcas y Modelos + if (!await _context.Brands.AnyAsync()) + { + var toyota = new Brand { VehicleTypeID = 1, Name = "Toyota" }; + var ford = new Brand { VehicleTypeID = 1, Name = "Ford" }; + var vw = new Brand { VehicleTypeID = 1, Name = "Volkswagen" }; + var honda = new Brand { VehicleTypeID = 2, Name = "Honda" }; + var yamaha = new Brand { VehicleTypeID = 2, Name = "Yamaha" }; + + _context.Brands.AddRange(toyota, ford, vw, honda, yamaha); + await _context.SaveChangesAsync(); + + _context.Models.AddRange( + new Model { BrandID = toyota.BrandID, Name = "Corolla" }, + new Model { BrandID = toyota.BrandID, Name = "Hilux" }, + new Model { BrandID = ford.BrandID, Name = "Ranger" }, + new Model { BrandID = vw.BrandID, Name = "Amarok" }, + new Model { BrandID = honda.BrandID, Name = "Wave 110" }, + new Model { BrandID = yamaha.BrandID, Name = "FZ FI" } + ); + await _context.SaveChangesAsync(); + } + + // 2. Crear Usuarios de Prueba + var testUser = await _context.Users.FirstOrDefaultAsync(u => u.UserName == "testuser"); + if (testUser == null) + { + testUser = new User + { + UserName = "testuser", + Email = "test@motores.com.ar", + PasswordHash = _passwordService.HashPassword("test123"), + FirstName = "Usuario", + LastName = "Prueba", + MigrationStatus = 1, + UserType = 1 + }; + _context.Users.Add(testUser); + } + + var adminUser = await _context.Users.FirstOrDefaultAsync(u => u.UserName == "admin"); + if (adminUser == null) + { + adminUser = new User + { + UserName = "admin", + Email = "admin@motoresargentinos.com.ar", + PasswordHash = _passwordService.HashPassword("admin123"), + FirstName = "Admin", + LastName = "Motores", + MigrationStatus = 1, + UserType = 3 // ADMIN + }; + _context.Users.Add(adminUser); + } + + await _context.SaveChangesAsync(); + + // 3. Crear Avisos de Prueba + if (!await _context.Ads.AnyAsync()) + { + var brands = await _context.Brands.ToListAsync(); + var models = await _context.Models.ToListAsync(); + + var ad1 = new Ad + { + UserID = testUser.UserID, + VehicleTypeID = 1, + BrandID = brands.First(b => b.Name == "Toyota").BrandID, + ModelID = models.First(m => m.Name == "Corolla").ModelID, + VersionName = "Toyota Corolla 1.8 XLI", + Year = 2022, + KM = 15000, + Price = 25000, + Currency = "USD", + Description = "Excelente estado, único dueño. Service al día.", + StatusID = 4, // Activo + IsFeatured = true, + CreatedAt = DateTime.UtcNow, + PublishedAt = DateTime.UtcNow + }; + + var ad2 = new Ad + { + UserID = testUser.UserID, + VehicleTypeID = 2, + BrandID = brands.First(b => b.Name == "Honda").BrandID, + ModelID = models.First(m => m.Name == "Wave 110").ModelID, + VersionName = "Honda Wave 110 S", + Year = 2023, + KM = 2500, + Price = 1800, + Currency = "USD", + Description = "Impecable, como nueva. Muy económica.", + StatusID = 4, // Activo + CreatedAt = DateTime.UtcNow, + PublishedAt = DateTime.UtcNow + }; + + var ad3 = new Ad + { + UserID = testUser.UserID, + VehicleTypeID = 1, + BrandID = brands.First(b => b.Name == "Ford").BrandID, + ModelID = models.First(m => m.Name == "Ranger").ModelID, + VersionName = "Ford Ranger Limited 4x4", + Year = 2021, + KM = 35000, + Price = 42000, + Currency = "USD", + Description = "Camioneta impecable, lista para transferir.", + StatusID = 3, // Moderacion Pendiente + CreatedAt = DateTime.UtcNow + }; + + _context.Ads.AddRange(ad1, ad2, ad3); + await _context.SaveChangesAsync(); + + // Agregar fotos + _context.AdPhotos.AddRange( + new AdPhoto { AdID = ad1.AdID, FilePath = "https://images.unsplash.com/photo-1621007947382-bb3c3994e3fb?auto=format&fit=crop&q=80&w=1200", IsCover = true }, + new AdPhoto { AdID = ad1.AdID, FilePath = "https://images.unsplash.com/photo-1590362891991-f776e933a68e?auto=format&fit=crop&q=80&w=1200" }, + new AdPhoto { AdID = ad1.AdID, FilePath = "https://images.unsplash.com/photo-1549317661-bd32c8ce0db2?auto=format&fit=crop&q=80&w=1200" }, + new AdPhoto { AdID = ad2.AdID, FilePath = "https://images.unsplash.com/photo-1558981403-c5f91cbba527?auto=format&fit=crop&q=80&w=800", IsCover = true }, + new AdPhoto { AdID = ad3.AdID, FilePath = "https://images.unsplash.com/photo-1533473359331-0135ef1b58bf?auto=format&fit=crop&q=80&w=1200", IsCover = true } + ); + + // Agregar Características Técnicas + _context.AdFeatures.AddRange( + new AdFeature { AdID = ad1.AdID, FeatureKey = "Combustible", FeatureValue = "Nafta" }, + new AdFeature { AdID = ad1.AdID, FeatureKey = "Transmision", FeatureValue = "Automática" }, + new AdFeature { AdID = ad1.AdID, FeatureKey = "Color", FeatureValue = "Blanco" }, + new AdFeature { AdID = ad2.AdID, FeatureKey = "Combustible", FeatureValue = "Nafta" }, + new AdFeature { AdID = ad2.AdID, FeatureKey = "Color", FeatureValue = "Rojo" } + ); + + await _context.SaveChangesAsync(); + } + + return Ok("Database seeded successfully with Features and Multiple Photos"); + } + + [HttpPost("reset")] + public async Task ResetDatabase() + { + _context.ChangeTracker.Clear(); + await _context.Database.ExecuteSqlRawAsync("DELETE FROM Transactions"); + await _context.Database.ExecuteSqlRawAsync("DELETE FROM AdPhotos"); + await _context.Database.ExecuteSqlRawAsync("DELETE FROM AdFeatures"); + await _context.Database.ExecuteSqlRawAsync("DELETE FROM Ads"); + await _context.Database.ExecuteSqlRawAsync("DELETE FROM Models"); + await _context.Database.ExecuteSqlRawAsync("DELETE FROM Brands"); + + return await SeedDatabase(); + } +} diff --git a/Backend/MotoresArgentinosV2.API/Controllers/UsuariosLegacyController.cs b/Backend/MotoresArgentinosV2.API/Controllers/UsuariosLegacyController.cs new file mode 100644 index 0000000..c52cd0e --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Controllers/UsuariosLegacyController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; +using MotoresArgentinosV2.Core.DTOs; +using MotoresArgentinosV2.Core.Interfaces; + +namespace MotoresArgentinosV2.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class UsuariosLegacyController : ControllerBase +{ + private readonly IUsuariosLegacyService _usuariosService; + + public UsuariosLegacyController(IUsuariosLegacyService usuariosService) + { + _usuariosService = usuariosService; + } + + [HttpGet("particular/{usuario}")] + public async Task> GetParticular(string usuario) + { + var datos = await _usuariosService.ObtenerParticularPorUsuarioAsync(usuario); + if (datos == null) return NotFound("Usuario particular no encontrado en legacy."); + return Ok(datos); + } + + [HttpGet("agencia/{usuario}")] + public async Task> GetAgencia(string usuario) + { + var datos = await _usuariosService.ObtenerAgenciaPorUsuarioAsync(usuario); + if (datos == null) return NotFound("Agencia no encontrada en legacy."); + return Ok(datos); + } +} diff --git a/Backend/MotoresArgentinosV2.API/MotoresArgentinosV2.API.csproj b/Backend/MotoresArgentinosV2.API/MotoresArgentinosV2.API.csproj new file mode 100644 index 0000000..8d3dc43 --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/MotoresArgentinosV2.API.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + enable + enable + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Backend/MotoresArgentinosV2.API/MotoresArgentinosV2.API.http b/Backend/MotoresArgentinosV2.API/MotoresArgentinosV2.API.http new file mode 100644 index 0000000..6e87d3d --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/MotoresArgentinosV2.API.http @@ -0,0 +1,6 @@ +@MotoresArgentinosV2.API_HostAddress = http://localhost:5262 + +GET {{MotoresArgentinosV2.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Backend/MotoresArgentinosV2.API/Program.cs b/Backend/MotoresArgentinosV2.API/Program.cs new file mode 100644 index 0000000..53f508c --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Program.cs @@ -0,0 +1,216 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.RateLimiting; // Rate Limit +using System.Threading.RateLimiting; // Rate Limit +using System.Text; +using MotoresArgentinosV2.Core.Interfaces; +using MotoresArgentinosV2.Infrastructure.Data; +using MotoresArgentinosV2.Infrastructure.Services; +using MotoresArgentinosV2.Core.Models; +using DotNetEnv; +using Microsoft.AspNetCore.HttpOverrides; + +// 🔒 ENV VARS +Env.Load(); + +var builder = WebApplication.CreateBuilder(args); + +// Forzar a la configuración a leer las variables +builder.Configuration.AddEnvironmentVariables(); + +// 🔒 KESTREL HARDENING +builder.WebHost.ConfigureKestrel(options => options.AddServerHeader = false); + +// LOGGING +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); +builder.Logging.AddDebug(); + +// 🔒 CORS POLICY +var frontendUrls = (builder.Configuration["AppSettings:FrontendUrl"] ?? "http://localhost:5173").Split(','); +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowSpecificOrigin", + policy => policy.WithOrigins(frontendUrls) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); +}); + +// FORWARDED HEADERS (CRÍTICO PARA DOCKER/NGINX) +// Por defecto, .NET solo confía en localhost. En Docker, Nginx tiene otra IP. +// Debemos limpiar las redes conocidas para que confíe en el proxy interno de Docker. +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.KnownIPNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +// 🔒 RATE LIMITING +builder.Services.AddRateLimiter(options => +{ + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + + options.GlobalLimiter = PartitionedRateLimiter.Create(context => + { + // En producción detrás de Nginx, RemoteIpAddress será la IP real del usuario. + // Si por alguna razón falla (ej: conexión directa local), usamos "unknown". + var remoteIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + + // Si es Loopback (localhost), sin límites (útil para dev) + if (System.Net.IPAddress.IsLoopback(context.Connection.RemoteIpAddress!)) + { + return RateLimitPartition.GetNoLimiter("loopback"); + } + + return RateLimitPartition.GetFixedWindowLimiter( + partitionKey: remoteIp, // Clave correcta: IP del usuario + factory: _ => new FixedWindowRateLimiterOptions + { + AutoReplenishment = true, + PermitLimit = 100, + QueueLimit = 2, + Window = TimeSpan.FromMinutes(1) + }); + }); + + options.AddPolicy("AuthPolicy", context => + { + // 🟢 FIX: Si es localhost, SIN LÍMITES + var remoteIp = context.Connection.RemoteIpAddress; + if (System.Net.IPAddress.IsLoopback(remoteIp!)) + { + return RateLimitPartition.GetNoLimiter("loopback_auth"); + } + + return RateLimitPartition.GetFixedWindowLimiter( + partitionKey: remoteIp?.ToString() ?? "unknown", + factory: _ => new FixedWindowRateLimiterOptions + { + AutoReplenishment = true, + PermitLimit = 5, // 5 intentos por minuto para IPs externas + QueueLimit = 0, + Window = TimeSpan.FromMinutes(1) + }); + }); +}); + +// DB CONTEXTS +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("Internet"))); +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("Autos"))); +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("MotoresV2"), + sqlOptions => sqlOptions.EnableRetryOnFailure())); + +// SERVICIOS +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.Configure(builder.Configuration.GetSection("SmtpSettings")); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddHostedService(); + +// 🔒 JWT AUTH +var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key Missing"); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)) + }; + // 🟢 LEER TOKEN DESDE COOKIE + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + // Buscar el token en la cookie llamada "accessToken" + var accessToken = context.Request.Cookies["accessToken"]; + if (!string.IsNullOrEmpty(accessToken)) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; + }); + +builder.Services.AddAuthorization(); +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// USAR EL MIDDLEWARE AL PRINCIPIO +// Debe ser lo primero para que el RateLimiter y los Logs vean la IP real +app.UseForwardedHeaders(); + +// 🔒 HEADERS DE SEGURIDAD MIDDLEWARE +app.Use(async (context, next) => +{ + context.Response.Headers.Append("X-Frame-Options", "DENY"); + context.Response.Headers.Append("X-Content-Type-Options", "nosniff"); + context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin"); + context.Response.Headers.Append("X-XSS-Protection", "1; mode=block"); + + // CSP adaptada para permitir pagos en Payway y WebSockets de Vite + string csp = "default-src 'self'; " + + "img-src 'self' data: https: blob:; " + + "script-src 'self' 'unsafe-inline'; " + + "style-src 'self' 'unsafe-inline'; " + + "connect-src 'self' https: ws: wss:; " + + "object-src 'none'; " + + "base-uri 'self'; " + + "form-action 'self' https://developers-ventasonline.payway.com.ar; " + + "frame-ancestors 'none';"; + context.Response.Headers.Append("Content-Security-Policy", csp); + + context.Response.Headers.Remove("Server"); + context.Response.Headers.Remove("X-Powered-By"); + await next(); +}); + +// PIPELINE +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} +else +{ + // 🔒 HSTS en Producción + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +// 🔒 APLICAR CORS & RATE LIMIT +app.UseCors("AllowSpecificOrigin"); +app.UseRateLimiter(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/Backend/MotoresArgentinosV2.API/Properties/launchSettings.json b/Backend/MotoresArgentinosV2.API/Properties/launchSettings.json new file mode 100644 index 0000000..c717a13 --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5262", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7251;http://localhost:5262", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Backend/MotoresArgentinosV2.API/appsettings.json b/Backend/MotoresArgentinosV2.API/appsettings.json new file mode 100644 index 0000000..ec04bc1 --- /dev/null +++ b/Backend/MotoresArgentinosV2.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/Backend/MotoresArgentinosV2.Core/DTOs/AdDtos.cs b/Backend/MotoresArgentinosV2.Core/DTOs/AdDtos.cs new file mode 100644 index 0000000..8603c53 --- /dev/null +++ b/Backend/MotoresArgentinosV2.Core/DTOs/AdDtos.cs @@ -0,0 +1,120 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace MotoresArgentinosV2.Core.DTOs; + +public class CreateAdRequestDto +{ + [Required] + [Range(1, 3, ErrorMessage = "Tipo de vehículo inválido")] + [JsonPropertyName("vehicleTypeID")] + public int VehicleTypeID { get; set; } + + [Required] + [JsonPropertyName("brandID")] + public int BrandID { get; set; } + + [JsonPropertyName("modelID")] + public int ModelID { get; set; } + + [Required] + [StringLength(100, MinimumLength = 1, ErrorMessage = "La versión debe tener entre 1 y 100 caracteres.")] + [RegularExpression(@"^[a-zA-Z0-9\s\-\.\(\),/áéíóúÁÉÍÓÚñÑ]+$", ErrorMessage = "Caracteres no permitidos en el nombre de versión.")] + [JsonPropertyName("versionName")] + public string VersionName { get; set; } = string.Empty; + + [Range(1900, 2100)] + [JsonPropertyName("year")] + public int Year { get; set; } + + [Range(0, 2000000)] + [JsonPropertyName("km")] + public int KM { get; set; } + + [Range(0, 999999999)] + [JsonPropertyName("price")] + public decimal Price { get; set; } + + [Required] + [StringLength(3, MinimumLength = 3)] + [RegularExpression(@"^(ARS|USD)$", ErrorMessage = "Moneda inválida.")] + [JsonPropertyName("currency")] + public string Currency { get; set; } = "ARS"; + + [StringLength(1000, ErrorMessage = "La descripción no puede superar los 1000 caracteres.")] + [RegularExpression(@"^(?!.* + + +
+ + + diff --git a/Frontend/nginx.conf b/Frontend/nginx.conf new file mode 100644 index 0000000..4744cab --- /dev/null +++ b/Frontend/nginx.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + # Proxy interno al Backend (Nombre del servicio en docker-compose) + location /api/ { + proxy_pass http://motores-backend:8080/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + 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; + } + + # Proxy para las imágenes servidas por el backend (fotos de vehículos) + location /uploads/ { + proxy_pass http://motores-backend:8080/uploads/; + proxy_set_header Host $host; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json new file mode 100644 index 0000000..4cf841a --- /dev/null +++ b/Frontend/package-lock.json @@ -0,0 +1,4200 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@mercadopago/sdk-react": "^1.0.7", + "@tailwindcss/vite": "^4.1.18", + "axios": "^1.13.2", + "qrcode.react": "^4.2.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-icons": "^5.5.0", + "react-router-dom": "^7.12.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mercadopago/sdk-js": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@mercadopago/sdk-js/-/sdk-js-0.0.3.tgz", + "integrity": "sha512-kO48DNLHdfFAp3on12nuKdqNlEmw1x3+nM6wLd04BdWOXoFcAhkNMQV3AyUIanXdO/bB/dENakdacLT29297EQ==", + "license": "Apache-2.0" + }, + "node_modules/@mercadopago/sdk-react": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@mercadopago/sdk-react/-/sdk-react-1.0.7.tgz", + "integrity": "sha512-babuAgsi1pQU+jfIQX1Sl0efp2no64OU/9yYK+RDz0NvSyARe+/D9ih9GyDNwVJJZuAA2lS4GF06oSxDFS5HYg==", + "license": "Apache-2.0", + "dependencies": { + "@mercadopago/sdk-js": "^0.0.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.7.tgz", + "integrity": "sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", + "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.52.0.tgz", + "integrity": "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/type-utils": "8.52.0", + "@typescript-eslint/utils": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.52.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.52.0.tgz", + "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.52.0.tgz", + "integrity": "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.52.0", + "@typescript-eslint/types": "^8.52.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.52.0.tgz", + "integrity": "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.52.0.tgz", + "integrity": "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.52.0.tgz", + "integrity": "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/utils": "8.52.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.52.0.tgz", + "integrity": "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.52.0.tgz", + "integrity": "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.52.0", + "@typescript-eslint/tsconfig-utils": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/visitor-keys": "8.52.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.52.0.tgz", + "integrity": "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.52.0", + "@typescript-eslint/types": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.52.0.tgz", + "integrity": "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.52.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "license": "MIT", + "dependencies": { + "react-router": "7.12.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.52.0.tgz", + "integrity": "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.52.0", + "@typescript-eslint/parser": "8.52.0", + "@typescript-eslint/typescript-estree": "8.52.0", + "@typescript-eslint/utils": "8.52.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/Frontend/package.json b/Frontend/package.json new file mode 100644 index 0000000..85e32c5 --- /dev/null +++ b/Frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@mercadopago/sdk-react": "^1.0.7", + "@tailwindcss/vite": "^4.1.18", + "axios": "^1.13.2", + "qrcode.react": "^4.2.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-icons": "^5.5.0", + "react-router-dom": "^7.12.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/Frontend/public/bg-car.jpg b/Frontend/public/bg-car.jpg new file mode 100644 index 0000000..582846c Binary files /dev/null and b/Frontend/public/bg-car.jpg differ diff --git a/Frontend/public/logo-ma.svg b/Frontend/public/logo-ma.svg new file mode 100644 index 0000000..dcdf615 --- /dev/null +++ b/Frontend/public/logo-ma.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Frontend/public/placeholder-car.png b/Frontend/public/placeholder-car.png new file mode 100644 index 0000000..1649377 Binary files /dev/null and b/Frontend/public/placeholder-car.png differ diff --git a/Frontend/src/App.tsx b/Frontend/src/App.tsx new file mode 100644 index 0000000..8dbc045 --- /dev/null +++ b/Frontend/src/App.tsx @@ -0,0 +1,336 @@ +import { useState, useEffect } from 'react'; +import { BrowserRouter as Router, Routes, Route, Link, Navigate, useNavigate, useLocation } from 'react-router-dom'; +import HomePage from './pages/HomePage'; +import ExplorarPage from './pages/ExplorarPage'; +import VehiculoDetailPage from './pages/VehiculoDetailPage'; +import PublicarAvisoPage from './pages/PublicarAvisoPage'; +import MisAvisosPage from './pages/MisAvisosPage'; +import SuccessPage from './pages/SuccessPage'; +import AdminPage from './pages/AdminPage'; +import LoginModal from './components/LoginModal'; +import { type UserSession } from './services/auth.service'; +import VerifyEmailPage from './pages/VerifyEmailPage'; +import ResetPasswordPage from './pages/ResetPasswordPage'; +import PerfilPage from './pages/PerfilPage'; +import SeguridadPage from './pages/SeguridadPage'; +import { FaHome, FaSearch, FaCar, FaUser, FaShieldAlt } from 'react-icons/fa'; +import { initMercadoPago } from '@mercadopago/sdk-react'; +import { AuthProvider, useAuth } from './context/AuthContext'; + +function AdminGuard({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth(); + if (loading) return
; + if (!user || user.userType !== 3) return ; + return <>{children}; +} + +// COMPONENTE NAVBAR CON DROPDOWN +function Navbar() { + const { user, logout, login, unreadCount } = useAuth(); + const [showLoginModal, setShowLoginModal] = useState(false); + const [showUserMenu, setShowUserMenu] = useState(false); + const [showMobileMenu, setShowMobileMenu] = useState(false); + const isAdmin = user?.userType === 3; + const navigate = useNavigate(); + const location = useLocation(); + + const handleLogout = () => { + logout(); + setShowUserMenu(false); + setShowMobileMenu(false); + navigate('/'); + }; + + const handleMobileNavClick = (path: string) => { + setShowMobileMenu(false); + navigate(path); + }; + + const handleLoginSuccess = (userSession: UserSession) => { + login(userSession); + setShowLoginModal(false); + }; + + const getLinkClass = (path: string) => { + const isActive = location.pathname === path; + return `transition-all duration-300 font-bold tracking-widest text-s hover:text-white ${isActive ? 'text-blue-400 drop-shadow-[0_0_8px_rgba(59,130,246,0.5)]' : 'text-gray-300'}`; + }; + + return ( + <> + + + {/* Menú móvil overlay MODERNIZADO */} + {showMobileMenu && ( +
+
+ + + + + + + + + {isAdmin && ( + + )} +
+ +
+

Motores Argentinos v2.0

+
+
+ )} + + {showLoginModal && ( +
+
+ setShowLoginModal(false)} + /> +
+
+ )} + + ); +} + +function FooterLegal() { + const currentYear = new Date().getFullYear(); + const baseEdition = 5858; + const baseDate = new Date('2026-01-21T00:00:00'); + const today = new Date(); + today.setHours(0, 0, 0, 0); + baseDate.setHours(0, 0, 0, 0); + const diffTime = today.getTime() - baseDate.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + const currentEdition = baseEdition + (diffDays > 0 ? diffDays : 0); + + return ( +
+
+
+

© {currentYear} MotoresArgentinos. Todos los derechos reservados. Edición número: {currentEdition}.

+

Registro DNDA Nº: RL-2024-70042723-APN-DNDA#MJ - Propietario: Publiéxito S.A.

+

Director: Leonardo Mario Forclaz - 46 N 423 - La Plata - Pcia. de Bs. As.

+
+
+
+ ); +} + +function MainLayout() { + const { loading } = useAuth(); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+ +
+
+
+
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } /> + +
+
+ +
+ ); +} + +function App() { + useEffect(() => { + const mpPublicKey = import.meta.env.VITE_MP_PUBLIC_KEY; + if (mpPublicKey) initMercadoPago(mpPublicKey); + }, []); + + return ( + + + + + + ); +} + +export default App; \ No newline at end of file diff --git a/Frontend/src/components/AdDetailsModal.tsx b/Frontend/src/components/AdDetailsModal.tsx new file mode 100644 index 0000000..8a7d016 --- /dev/null +++ b/Frontend/src/components/AdDetailsModal.tsx @@ -0,0 +1,124 @@ +import { parseUTCDate, formatCurrency } from '../utils/app.utils'; +import { STATUS_CONFIG } from '../constants/adStatuses'; + +interface Props { + ad: any; + onClose: () => void; +} + +export default function AdDetailsModal({ ad, onClose }: Props) { + if (!ad) return null; + + const status = STATUS_CONFIG[ad.statusID] || { label: 'Desconocido', color: 'text-gray-400', bg: 'bg-gray-500/10', border: 'border-white/10' }; + + // Helper para fecha de pago + const paymentDate = ad.paidDate + ? parseUTCDate(ad.paidDate).toLocaleDateString('es-AR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) + : '-'; + + return ( +
+
+ +
+ + {/* Header */} +
+
+
+ + {status.label} + + ID #{ad.adID} +
+

{ad.title}

+
+ +
+ + {/* Contenido con Scroll Interno */} +
+
+ {/* Columna 1: Datos Financieros */} +
+
+
+

💰 Transacción

+
+
+ Monto Abonado + + {ad.paidAmount ? formatCurrency(ad.paidAmount, 'ARS') : Sin pago registrado} + +
+
+ Fecha de Pago + {paymentDate} +
+
+
+ + {/* Métricas */} +
+

Métricas

+
+
+ {ad.views} + Visitas +
+
+
+ {ad.legacyID || '-'} + ID Operación +
+
+
+
+ + {/* Columna 2: Línea de Tiempo */} +
+

Ciclo de Vida

+
+ + + new Date()} /> + {ad.deletedAt && } +
+
+
+
+ + {/* Footer */} +
+
+
+ 👤 +
+
+ {ad.userName} + {ad.userEmail} +
+
+ + + Ver en Web ↗ + +
+
+
+ ); +} + +function TimelineItem({ date, label, active, highlight, future, color = 'text-gray-400' }: any) { + if (!date && !future) return null; + + return ( +
+
+

+ {date ? parseUTCDate(date).toLocaleDateString('es-AR', { day: '2-digit', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'} +

+

{label}

+
+ ); +} \ No newline at end of file diff --git a/Frontend/src/components/AdStatusBadge.tsx b/Frontend/src/components/AdStatusBadge.tsx new file mode 100644 index 0000000..e202b3c --- /dev/null +++ b/Frontend/src/components/AdStatusBadge.tsx @@ -0,0 +1,26 @@ +import { AD_STATUSES, STATUS_CONFIG } from '../constants/adStatuses'; + +interface Props { + statusId: number; + className?: string; +} + +export default function AdStatusBadge({ statusId, className = '' }: Props) { + // Si está activo, no mostramos badge en las tarjetas públicas (Home/Explorar) para mantener limpieza. + if (statusId === AD_STATUSES.ACTIVE) return null; + + const config = STATUS_CONFIG[statusId]; + if (!config) return null; + + return ( +
+ {config.icon} + {config.label} +
+ ); +} \ No newline at end of file diff --git a/Frontend/src/components/ChangePasswordModal.tsx b/Frontend/src/components/ChangePasswordModal.tsx new file mode 100644 index 0000000..74cdda7 --- /dev/null +++ b/Frontend/src/components/ChangePasswordModal.tsx @@ -0,0 +1,141 @@ +import { useState } from 'react'; +import { AuthService } from '../services/auth.service'; + +interface Props { + onClose: () => void; +} + +// --- ICONOS SVG (Reutilizados) --- +const EyeIcon = () => (); +const EyeSlashIcon = () => (); +const CheckCircleIcon = () => (); +const XCircleIcon = () => (); +const NeutralCircleIcon = () => (); + +export default function ChangePasswordModal({ onClose }: Props) { + const [currentPass, setCurrentPass] = useState(''); + const [newPass, setNewPass] = useState(''); + const [confirmPass, setConfirmPass] = useState(''); + + // Visibilidad + const [showCurrent, setShowCurrent] = useState(false); + const [showNew, setShowNew] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + + // Validaciones + const validations = { + length: newPass.length >= 8, + upper: /[A-Z]/.test(newPass), + number: /\d/.test(newPass), + special: /[\W_]/.test(newPass), + match: newPass.length > 0 && newPass === confirmPass + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (!validations.length || !validations.upper || !validations.number || !validations.special || !validations.match) { + setError('La nueva contraseña no cumple con los requisitos.'); + return; + } + + setLoading(true); + try { + await AuthService.changePassword(currentPass, newPass); + setSuccess(true); + setTimeout(() => onClose(), 2000); // Cerrar automáticamente tras éxito + } catch (err: any) { + setError(err.response?.data?.message || 'Error al cambiar contraseña'); + } finally { + setLoading(false); + } + }; + + const RequirementItem = ({ isValid, text }: { isValid: boolean, text: string }) => { + const isNeutral = newPass.length === 0; + return ( +
  • + {isNeutral ? : isValid ? : } + {text} +
  • + ); + }; + + return ( +
    +
    + + +

    Cambiar Contraseña

    +

    Seguridad de la cuenta

    + + {error && ( +
    + {error} +
    + )} + + {success ? ( +
    +
    +

    ¡Contraseña Actualizada!

    +
    + ) : ( +
    + {/* Contraseña Actual */} +
    + setCurrentPass(e.target.value)} + className="w-full bg-white/5 border border-white/10 rounded-xl pl-4 pr-10 py-3 text-sm text-white outline-none focus:border-blue-500 placeholder:text-gray-600" + placeholder="Contraseña Actual" /> + +
    + +
    + + {/* Nueva Contraseña */} +
    + setNewPass(e.target.value)} + className="w-full bg-white/5 border border-white/10 rounded-xl pl-4 pr-10 py-3 text-sm text-white outline-none focus:border-green-500 placeholder:text-gray-600" + placeholder="Nueva Contraseña" /> + +
    + + {/* Confirmar Nueva */} +
    + setConfirmPass(e.target.value)} + className="w-full bg-white/5 border border-white/10 rounded-xl pl-4 pr-10 py-3 text-sm text-white outline-none focus:border-green-500 placeholder:text-gray-600" + placeholder="Repetir Nueva Contraseña" /> + +
    + + {/* Validaciones Visuales */} +
    +
      + + + + + +
    +
    + + +
    + )} +
    +
    + ); +} \ No newline at end of file diff --git a/Frontend/src/components/ChatModal.tsx b/Frontend/src/components/ChatModal.tsx new file mode 100644 index 0000000..4b07c76 --- /dev/null +++ b/Frontend/src/components/ChatModal.tsx @@ -0,0 +1,132 @@ +import { useState, useEffect, useRef } from 'react'; +import { ChatService, type ChatMessage } from '../services/chat.service'; +import { parseUTCDate } from '../utils/app.utils'; + +interface ChatModalProps { + isOpen: boolean; + onClose: () => void; + adId: number; + adTitle: string; + sellerId: number; + currentUserId: number; +} + +export default function ChatModal({ isOpen, onClose, adId, adTitle, sellerId, currentUserId }: ChatModalProps) { + const [messages, setMessages] = useState([]); + const [newMessage, setNewMessage] = useState(''); + const [loading, setLoading] = useState(false); + const scrollRef = useRef(null); + + useEffect(() => { + if (isOpen) { + loadMessages(); + const interval = setInterval(loadMessages, 5000); + return () => clearInterval(interval); + } + }, [isOpen]); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages]); + + const loadMessages = async () => { + try { + const data = await ChatService.getConversation(adId, currentUserId, sellerId); + setMessages(data); + } catch (err) { + console.error(err); + } + }; + + const handleSend = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newMessage.trim()) return; + + setLoading(true); + try { + await ChatService.sendMessage({ + adID: adId, + senderID: currentUserId, + receiverID: sellerId, + messageText: newMessage + }); + setNewMessage(''); + loadMessages(); + } catch (err) { + alert('Error al enviar mensaje'); + } finally { + setLoading(false); + } + }; + + if (!isOpen) return null; + + return ( +
    +
    + + {/* Header Neutro */} +
    +
    + {/* Ícono Genérico de Mensaje */} +
    + 💬 +
    +
    +

    Mensajes del Aviso

    +

    {adTitle}

    +
    +
    + +
    + + {/* Cuerpo de Mensajes */} +
    + {messages.length === 0 ? ( +
    + 📝 +

    No hay mensajes previos

    +
    + ) : ( + messages.map((m, idx) => { + const isMine = m.senderID === currentUserId; + return ( +
    +
    +

    {m.messageText}

    + + {parseUTCDate(m.sentAt!).toLocaleTimeString('es-AR', { timeZone: 'America/Argentina/Buenos_Aires', hour: '2-digit', minute: '2-digit', hour12: false })} + +
    +
    + ); + }) + )} +
    + + {/* Input area */} +
    + setNewMessage(e.target.value)} + placeholder="Escribe aquí..." + className="flex-1 bg-black/30 border border-white/10 rounded-xl px-4 py-3 text-sm text-white focus:outline-none focus:border-blue-500 transition-all placeholder:text-gray-600" + /> + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/Frontend/src/components/ConfigPanel.tsx b/Frontend/src/components/ConfigPanel.tsx new file mode 100644 index 0000000..8ecd643 --- /dev/null +++ b/Frontend/src/components/ConfigPanel.tsx @@ -0,0 +1,196 @@ +import { useState } from 'react'; +import { AuthService, type UserSession } from '../services/auth.service'; +import { QRCodeSVG } from 'qrcode.react'; +import ChangePasswordModal from './ChangePasswordModal'; +import { useAuth } from '../context/AuthContext'; + +// Iconos +const CopyIcon = () => (); +const CheckIcon = () => (); + +export default function ConfigPanel({ user }: { user: UserSession }) { + const { refreshSession } = useAuth(); // Para actualizar si isMFAEnabled cambia en el contexto + const [showPasswordModal, setShowPasswordModal] = useState(false); + + // Estados MFA + const [mfaStep, setMfaStep] = useState<'IDLE' | 'QR'>('IDLE'); + const [qrUri, setQrUri] = useState(''); + const [secretKey, setSecretKey] = useState(''); // El código manual + const [mfaCode, setMfaCode] = useState(''); + const [msgMfa, setMsgMfa] = useState({ text: '', type: '' }); // type: 'success' | 'error' + const [copied, setCopied] = useState(false); + const [loading, setLoading] = useState(false); + const isMfaActive = (user as any).isMFAEnabled; + + const handleInitMfa = async () => { + if (isMfaActive) { + if (!window.confirm("Al reconfigurar, el código anterior dejará de funcionar en tu otro dispositivo. ¿Continuar?")) return; + } + + setLoading(true); + setMsgMfa({ text: '', type: '' }); + try { + const data = await AuthService.initMFA(); + setQrUri(data.qrUri); + setSecretKey(data.secret); + setMfaStep('QR'); + } catch { + setMsgMfa({ text: "Error iniciando configuración.", type: 'error' }); + } finally { + setLoading(false); + } + }; + + const handleVerifyMfa = async () => { + setLoading(true); + try { + await AuthService.verifyMFA(user.username, mfaCode); + setMsgMfa({ text: "¡MFA Activado correctamente!", type: 'success' }); + setMfaStep('IDLE'); + setMfaCode(''); + await refreshSession(); // Actualizar estado global + } catch { + setMsgMfa({ text: "Código incorrecto. Intenta nuevamente.", type: 'error' }); + } finally { + setLoading(false); + } + }; + + const handleDisableMfa = async () => { + if (!window.confirm("¿Seguro que deseas desactivar la protección de dos factores? Tu cuenta será menos segura.")) return; + + setLoading(true); + try { + await AuthService.disableMFA(); + setMsgMfa({ text: "MFA Desactivado.", type: 'success' }); + await refreshSession(); + } catch { + setMsgMfa({ text: "Error al desactivar MFA.", type: 'error' }); + } finally { + setLoading(false); + } + }; + + const copyToClipboard = () => { + navigator.clipboard.writeText(secretKey); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + <> +
    + + {/* SECCIÓN CONTRASEÑA */} +
    +
    +
    + 🔑 +
    +

    Contraseña

    +

    + Mantén tu cuenta segura actualizando tu contraseña periódicamente. +

    + +
    + + {/* SECCIÓN MFA */} +
    + + {/* Indicador de Estado */} +
    + {isMfaActive ? 'Protegido' : 'No Activo'} +
    + +
    + 🛡️ +
    +

    Doble Factor (2FA)

    + + {mfaStep === 'IDLE' ? ( +
    +

    + {isMfaActive + ? "Tu cuenta está protegida. Se solicita un código cada vez que inicias sesión en un dispositivo nuevo." + : "Añade una capa extra de seguridad. Requerirá un código de tu celular al iniciar sesión."} +

    + +
    + {isMfaActive ? ( +
    + + +
    + ) : ( + + )} + + {msgMfa.text && ( +

    + {msgMfa.text} +

    + )} +
    +
    + ) : ( +
    +
    + +
    + +

    1. Escanea el código

    +

    Usa Google Authenticator o Authy en tu celular.

    + + {/* CÓDIGO MANUAL */} +
    +

    O ingresa el código manual

    +
    + {secretKey} + +
    +
    + +

    2. Ingresa el token

    + setMfaCode(e.target.value.replace(/\D/g, ''))} + className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-center text-2xl font-black text-white mb-4 tracking-[0.4em] outline-none focus:border-blue-500 transition-colors placeholder:opacity-20" + /> + +
    + + +
    +
    + )} +
    +
    + + {showPasswordModal && ( + setShowPasswordModal(false)} /> + )} + + ); +} \ No newline at end of file diff --git a/Frontend/src/components/ConfirmationModal.tsx b/Frontend/src/components/ConfirmationModal.tsx new file mode 100644 index 0000000..3c36b03 --- /dev/null +++ b/Frontend/src/components/ConfirmationModal.tsx @@ -0,0 +1,85 @@ +import { useEffect } from 'react'; + +interface Props { + isOpen: boolean; + title: string; + message: React.ReactNode; + onConfirm: () => void; + onCancel: () => void; + confirmText?: string; + cancelText?: string; + isDanger?: boolean; // Para pintar el botón de rojo si es eliminar +} + +export default function ConfirmationModal({ + isOpen, + title, + message, + onConfirm, + onCancel, + confirmText = "Confirmar", + cancelText = "Cancelar", + isDanger = false +}: Props) { + + // Cerrar con tecla ESC + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') onCancel(); + }; + if (isOpen) window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [isOpen, onCancel]); + + if (!isOpen) return null; + + return ( +
    + {/* Backdrop */} +
    + + {/* Modal Content */} +
    + + {/* Icono decorativo según tipo */} +
    + {isDanger ? '⚠️' : 'ℹ️'} +
    + +

    + {title} +

    + +
    + {message} +
    + +
    + + + +
    + +
    +
    + ); +} \ No newline at end of file diff --git a/Frontend/src/components/CreditCardForm.tsx b/Frontend/src/components/CreditCardForm.tsx new file mode 100644 index 0000000..14f34f8 --- /dev/null +++ b/Frontend/src/components/CreditCardForm.tsx @@ -0,0 +1,378 @@ +import { useState, useEffect } from 'react'; +import VisualCreditCard from './VisualCreditCard'; + +declare global { + interface Window { + MercadoPago: any; + } +} + +interface Props { + amount: number; + onPaymentSuccess: (details: any) => Promise; + onPaymentError: (error: string) => void; + onCancel: () => void; +} + +interface IdentificationType { + id: string; + name: string; +} + +const FALLBACK_DOC_TYPES = [ + { id: 'DNI', name: 'DNI' }, { id: 'CUIT', name: 'CUIT' }, { id: 'CUIL', name: 'CUIL' } +]; + +export default function CreditCardForm({ amount, onPaymentSuccess, onPaymentError, onCancel }: Props) { + const [loading, setLoading] = useState(false); + const [docTypes, setDocTypes] = useState(FALLBACK_DOC_TYPES); + const [mpInstance, setMpInstance] = useState(null); + const [paymentMethod, setPaymentMethod] = useState<{ id: string, name: string, thumbnail: string } | null>(null); + const [isCvvFocused, setIsCvvFocused] = useState(false); + + const [formData, setFormData] = useState({ + cardNumber: '', + cardholderName: '', + cardExpiration: '', + securityCode: '', + identificationType: 'DNI', + identificationNumber: '', + email: '' + }); + + // --- PATRÓN SINGLETON PARA MERCADOPAGO --- + // El script de seguridad de MP (Armor) lanza errores de IndexedDB si se reinicializa múltiples veces. + useEffect(() => { + const publicKey = import.meta.env.VITE_MP_PUBLIC_KEY; + if (window.MercadoPago) { + if (!(window as any).mpInstanceGlobal) { + try { + (window as any).mpInstanceGlobal = new window.MercadoPago(publicKey); + } catch (e) { + console.error("Error al crear instancia de MP:", e); + } + } + setMpInstance((window as any).mpInstanceGlobal); + } + }, []); + + useEffect(() => { + if (mpInstance) { + mpInstance.getIdentificationTypes() + .then((types: IdentificationType[]) => { + if (types?.length > 0) setDocTypes(types); + }) + .catch(() => console.warn("Usando fallback de tipos de documento.")); + } + }, [mpInstance]); + + const handleBinLookup = async (bin: string) => { + if (bin.length < 6 || !mpInstance) { + setPaymentMethod(null); + return; + } + try { + const response = await mpInstance.getPaymentMethods({ bin }); + if (response && response.results && response.results.length > 0) { + setPaymentMethod(response.results[0]); + } + } catch (error) { console.error(error); } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + let finalValue = value; + + if (name === 'cardNumber') { + const cleanValue = value.replace(/[^\d]/g, ''); + finalValue = cleanValue.replace(/(.{4})/g, '$1 ').trim(); + + if (cleanValue.length >= 6) { + handleBinLookup(cleanValue.substring(0, 6)); + } else { + setPaymentMethod(null); + } + } else if (name === 'cardExpiration') { + const cleanValue = value.replace(/[^\d]/g, ''); + if (cleanValue.length <= 2) { + finalValue = cleanValue; + } else { + finalValue = `${cleanValue.slice(0, 2)}/${cleanValue.slice(2, 4)}`; + } + } + + setFormData(prev => ({ ...prev, [name]: finalValue })); + }; + + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + alert("Por seguridad, ingresa los datos manualmente."); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!mpInstance || !paymentMethod) { + onPaymentError("Verifica los datos de la tarjeta."); + return; + } + setLoading(true); + + const [expMonth, expYear] = formData.cardExpiration.split('/'); + + try { + const tokenResponse = await mpInstance.createCardToken({ + cardNumber: formData.cardNumber.replace(/\s/g, ''), + cardholderName: formData.cardholderName, + cardExpirationMonth: expMonth, + cardExpirationYear: `20${expYear}`, + securityCode: formData.securityCode, + identificationType: formData.identificationType, + identificationNumber: formData.identificationNumber, + }); + + if (!tokenResponse?.id) throw new Error("Datos de tarjeta inválidos."); + + // 1. Intentamos obtener el issuer desde la respuesta del token + let finalIssuerId = tokenResponse.issuer_id; + + // 2. Si no viene en el token, intentamos buscarlo explícitamente + if (!finalIssuerId && mpInstance) { + try { + const bin = formData.cardNumber.replace(/\s/g, '').substring(0, 6); + const issuers = await mpInstance.getIssuers({ paymentMethodId: paymentMethod.id, bin }); + + if (issuers && issuers.length > 0) { + finalIssuerId = issuers[0].id; // Tomamos el primero encontrado + } + } catch (issuerErr) { + console.warn("No se pudo obtener issuer explícito", issuerErr); + } + } + + const issuerIdToSend = finalIssuerId ? String(finalIssuerId) : ""; + + await onPaymentSuccess({ + token: tokenResponse.id, + transactionAmount: amount, + paymentMethodId: paymentMethod.id, + installments: 1, + issuerId: issuerIdToSend, + payerEmail: formData.email + }); + + } catch (err: any) { + let msg = "Error procesando el pago."; + if (err.cause?.[0]?.code) { + const code = err.cause[0].code; + if (code === "205") msg = "Revisá el número de tarjeta."; + else if (['208', '209'].includes(code)) msg = "Revisá la fecha de vencimiento."; + else if (['212', '213', '214'].includes(code)) msg = "Documento inválido."; + else if (code === "221") msg = "Nombre y apellido requeridos."; + else if (code === "224") msg = "Código de seguridad (CVV) inválido."; + } + onPaymentError(msg); + } finally { + setLoading(false); + } + }; + + const [expMonth, expYear] = formData.cardExpiration.split('/'); + + // 🟢 VALIDACIÓN EN TIEMPO REAL + // Verifica que todos los campos tengan contenido válido antes de habilitar el botón + const isFormValid = + formData.cardNumber.replace(/\s/g, '').length >= 15 && // Tarjetas suelen ser 15 o 16 dígitos + formData.cardholderName.trim().length > 2 && + formData.cardExpiration.length === 5 && // Formato MM/AA completo + formData.securityCode.length >= 3 && // CVV mínimo + formData.identificationNumber.trim().length >= 6 && // DNI razonable + /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email) && // Regex básico de email + paymentMethod !== null; // Tarjeta reconocida + + return ( +
    + + {/* --- COLUMNA IZQUIERDA: FORMULARIO --- */} +
    + +
    +

    + 💳 Datos de Facturación +

    + +
    + {/* Número */} +
    +
    + + {/* Logo en móvil: al lado del label para no tapar el número */} + {paymentMethod && ( +
    + card brand +
    + )} +
    +
    + + {/* Logo en desktop: dentro del input */} +
    + {paymentMethod ? ( + card brand + ) : ( +
    + )} +
    +
    +
    + + {/* Nombre */} +
    + + +
    + + {/* Grid Vencimiento + CVV */} +
    +
    + + +
    +
    + +
    + setIsCvvFocused(true)} + onBlur={() => setIsCvvFocused(false)} + /> +
    +
    +
    + + {/* DNI - FILA COMPLETA */} +
    + +
    +
    + + +
    + +
    +
    + + {/* EMAIL - FILA COMPLETA */} +
    + + +
    +
    +
    +
    + + {/* --- COLUMNA DERECHA: VISUAL + BOTONES --- */} +
    + + {/* Tarjeta Visual */} +
    +
    +
    + +
    + +
    +
    + +
    +

    + Revisa que los datos coincidan exactamente con tu tarjeta física para evitar rechazos. +

    +
    +
    + + {/* BOTONES DE ACCIÓN */} +
    + + + +
    + +
    + +
    + ); +} diff --git a/Frontend/src/components/FormularioAviso.tsx b/Frontend/src/components/FormularioAviso.tsx new file mode 100644 index 0000000..6dbf692 --- /dev/null +++ b/Frontend/src/components/FormularioAviso.tsx @@ -0,0 +1,1043 @@ +import { useState, useEffect, useRef } from 'react'; +import { AdsV2Service } from '../services/ads.v2.service'; +import { AuthService } from '../services/auth.service'; +import type { DatosAvisoDto } from '../types/aviso.types'; +import SearchableSelect from './SearchableSelect'; +import { AdminService } from '../services/admin.service'; +import CreditCardForm from './CreditCardForm'; +import MercadoPagoLogo from './MercadoPagoLogo'; +import ConfirmationModal from './ConfirmationModal'; +import { + VEHICLE_TYPES, + AUTO_SEGMENTS, + MOTO_SEGMENTS, + AUTO_TRANSMISSIONS, + MOTO_TRANSMISSIONS, + FUEL_TYPES, + VEHICLE_CONDITIONS, + STEERING_TYPES +} from '../constants/vehicleOptions'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; + +interface Props { + plan: DatosAvisoDto; + onCancel: () => void; + onSuccess: (adId: number, isAdminAction?: boolean) => void; + editId?: number | null; +} + +type PhotoSource = File | { id: number; path: string }; + +export default function FormularioAviso({ plan, onCancel, onSuccess, editId }: Props) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [photos, setPhotos] = useState([]); + const navigate = useNavigate(); + + // Estados Maestros + const [brands, setBrands] = useState<{ id: number, name: string }[]>([]); + + // Estados Autocomplete Modelos + const [modelSearch, setModelSearch] = useState(''); + const [modelSuggestions, setModelSuggestions] = useState<{ id: number, name: string }[]>([]); + const [showSuggestions, setShowSuggestions] = useState(false); + const modelWrapperRef = useRef(null); + + // Estados de Pago (Mercado Pago) + const [showPaymentForm, setShowPaymentForm] = useState(false); + const [currentAdId, setCurrentAdId] = useState(editId || null); + + // ADMIN STATE + const { user } = useAuth(); + const isAdmin = user?.userType === 3; + + // Buscador de Usuarios para Admin + const [userSearch, setUserSearch] = useState(''); + const [userSuggestions, setUserSuggestions] = useState<{ id: number, name: string, email: string }[]>([]); + const [targetUser, setTargetUser] = useState<{ id: number, name: string, email: string } | null>(null); + const [showUserSuggestions, setShowUserSuggestions] = useState(false); + const userWrapperRef = useRef(null); + + const [ghostUser, setGhostUser] = useState({ email: '', firstName: '', lastName: '', phone: '' }); + + // Estado para el modal de coincidencia de usuario + const [userMatchModal, setUserMatchModal] = useState<{ isOpen: boolean; user: any | null }>({ + isOpen: false, + user: null + }); + + // Estado del vehículo + const [vehicleData, setVehicleData] = useState({ + brandId: '', + modelId: '', + modelName: '', + year: new Date().getFullYear(), + km: 0, + price: 0, + currency: 'ARS', + description: '', + fuelType: '', + color: '', + segment: '', + location: '', + condition: '', + doorCount: undefined as number | undefined, + transmission: '', + steering: '' + }); + + // Contacto + const [contactData, setContactData] = useState({ + phone: (user as any)?.phoneNumber || (user as any)?.phone || '', + email: user?.email || '', + displayInfo: true + }); + + // Sincronizar contacto cuando el usuario cargue + useEffect(() => { + if (user && !editId && !targetUser && !ghostUser.email) { + setContactData(prev => ({ + ...prev, + email: user.email || prev.email, + phone: (user as any).phone || (user as any).phoneNumber || prev.phone + })); + } + }, [user, editId]); + + // --- EFECTOS DE CARGA --- + + // 1. Cargar Marcas + useEffect(() => { + const vehicleTypeId = plan.idRubro; + AdsV2Service.getBrands(vehicleTypeId).then(setBrands).catch(console.error); + }, [plan.idRubro]); + + // 2. Autocomplete Modelos + useEffect(() => { + if (vehicleData.brandId && modelSearch.length >= 2) { + const timer = setTimeout(() => { + AdsV2Service.searchModels(Number(vehicleData.brandId), modelSearch) + .then(setModelSuggestions) + .catch(console.error); + }, 300); + return () => clearTimeout(timer); + } else { + setModelSuggestions([]); + } + }, [modelSearch, vehicleData.brandId]); + + // 3. Autocomplete Usuarios (Admin) + useEffect(() => { + if (isAdmin && userSearch.length >= 3) { + const timer = setTimeout(async () => { + try { + const data = await AdminService.searchUsers(userSearch); + const users = data.map((u: any) => ({ + id: u.userID, + name: `${u.firstName || ''} ${u.lastName || ''} (${u.userName})`.trim(), + email: u.email, + phone: u.phoneNumber + })); + setUserSuggestions(users); + } catch (e) { console.error(e); } + }, 300); + return () => clearTimeout(timer); + } else { + setUserSuggestions([]); + } + }, [userSearch, isAdmin]); + + // 4. Cargar datos si es edición + useEffect(() => { + if (editId) { + cargarDatosAviso(editId); + } + }, [editId]); + + // 5. CERRAR SUGERENCIAS AL CLICKEAR FUERA + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (modelWrapperRef.current && !modelWrapperRef.current.contains(event.target as Node)) { + setShowSuggestions(false); + } + if (userWrapperRef.current && !userWrapperRef.current.contains(event.target as Node)) { + setShowUserSuggestions(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const cargarDatosAviso = async (id: number) => { + try { + const ad = await AdsV2Service.getById(id); + setVehicleData({ + brandId: ad.brandID?.toString() || '', + modelId: ad.modelID?.toString() || '', + modelName: ad.versionName || '', + year: ad.year, + km: ad.km, + price: ad.price, + currency: ad.currency, + description: ad.description || '', + fuelType: (ad.fuelType || ad.FuelType) || '', + color: (ad.color || ad.Color) || '', + segment: (ad.segment || ad.Segment) || '', + location: (ad.location || ad.Location) || '', + condition: (ad.condition || ad.Condition) || '', + doorCount: ad.doorCount || ad.DoorCount, + transmission: (ad.transmission || ad.Transmission) || '', + steering: (ad.steering || ad.Steering) || '' + }); + setModelSearch(ad.versionName || ''); + setContactData({ + phone: ad.contactPhone || '', + email: ad.contactEmail || '', + displayInfo: ad.displayContactInfo + }); + if (ad.photos && ad.photos.length > 0) { + const existingPhotos = ad.photos.map((p: any) => ({ + id: p.photoID, + path: p.filePath + })); + setPhotos(existingPhotos); + } + } catch (err) { + console.error("Error al cargar datos del aviso", err); + } + }; + + // --- HANDLERS --- + + const handleNumberInput = (field: string, value: string) => { + if (value === '') { + setVehicleData(prev => ({ ...prev, [field]: '' })); + return; + } + const num = parseInt(value); + setVehicleData(prev => ({ ...prev, [field]: isNaN(num) ? '' : num })); + }; + + const handleFloatInput = (field: string, value: string) => { + if (value === '') { + setVehicleData(prev => ({ ...prev, [field]: '' })); + return; + } + const num = parseFloat(value); + setVehicleData(prev => ({ ...prev, [field]: isNaN(num) ? '' : num })); + }; + + const handlePhotoChange = (e: React.ChangeEvent) => { + if (e.target.files) { + const newFiles = Array.from(e.target.files); + + if (photos.length + newFiles.length > 5) { + alert("Máximo 5 fotos permitidas."); + return; + } + + const MAX_SIZE = 3 * 1024 * 1024; + const oversizedFiles = newFiles.filter(file => file.size > MAX_SIZE); + + if (oversizedFiles.length > 0) { + alert(`Algunas imágenes superan el límite de 3MB:\n${oversizedFiles.map(f => f.name).join('\n')}`); + return; + } + + const invalidTypes = newFiles.filter(file => !['image/jpeg', 'image/png', 'image/webp'].includes(file.type)); + if (invalidTypes.length > 0) { + alert("Solo se permiten archivos JPG, PNG o WEBP."); + return; + } + + setPhotos([...photos, ...newFiles]); + } + }; + + const selectModel = (id: number, name: string) => { + setVehicleData({ ...vehicleData, modelId: id.toString(), modelName: name }); + setModelSearch(name); + setShowSuggestions(false); + }; + + const selectUser = (u: { id: number, name: string, email: string, phone?: string }) => { + setTargetUser(u); + setUserSearch(u.name); + setShowUserSuggestions(false); + + // Actualizamos los datos de contacto con los del usuario seleccionado + setContactData(prev => ({ + ...prev, + email: u.email, + // Si u.phone viene undefined (porque el backend no lo manda o es null), usamos string vacío + phone: u.phone || '' + })); + }; + + // Sincronizar datos de usuario fantasma con datos de contacto + useEffect(() => { + if (!targetUser && (ghostUser.email || ghostUser.phone)) { + setContactData(prev => ({ + ...prev, + email: ghostUser.email, + phone: ghostUser.phone + })); + } + }, [ghostUser, targetUser]); + + // Detección automática de usuario existente al escribir en "Crear Nuevo" + useEffect(() => { + if (!isAdmin || targetUser || !ghostUser.email || ghostUser.email.length < 5) return; + + const timer = setTimeout(async () => { + if (ghostUser.email.includes('@')) { + try { + const results = await AdminService.searchUsers(ghostUser.email); + const exactMatch = results.find((u: any) => u.email.toLowerCase() === ghostUser.email.toLowerCase()); + + if (exactMatch) { + setUserMatchModal({ + isOpen: true, + user: exactMatch + }); + } + } catch (err) { + console.error("Error verificando existencia de usuario", err); + } + } + }, 800); + + return () => clearTimeout(timer); + }, [ghostUser.email, isAdmin, targetUser]); + + const handleConfirmUserMatch = () => { + if (userMatchModal.user) { + const u = userMatchModal.user; + // Mapeamos al formato que espera selectUser + const userToSelect = { + id: u.userID, + name: `${u.firstName || ''} ${u.lastName || ''} (${u.userName})`.trim(), + email: u.email, + phone: u.phoneNumber + }; + + // 1. Asignamos el usuario existente + selectUser(userToSelect); + + // 2. Limpiamos los campos de "Crear Nuevo" + setGhostUser({ email: '', firstName: '', lastName: '', phone: '' }); + } + // Cerramos modal + setUserMatchModal({ isOpen: false, user: null }); + }; + + const handleCancelUserMatch = () => { + // Si cancela, limpiamos el email (y resto de campos) para evitar el error de duplicado al intentar guardar + setGhostUser({ email: '', firstName: '', lastName: '', phone: '' }); + setUserMatchModal({ isOpen: false, user: null }); + }; + + // --- SUBMIT INICIAL --- + const handleFormSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + const userSession = AuthService.getCurrentUser(); + if (!userSession) throw new Error("Usuario no autenticado"); + + if (!vehicleData.brandId || (!vehicleData.modelId && !vehicleData.modelName)) { + throw new Error("Seleccione una marca y escriba un modelo válido."); + } + + if (vehicleData.year < 1900 || vehicleData.year > new Date().getFullYear() + 1) { + throw new Error("El año ingresado no es válido."); + } + if (vehicleData.price <= 0) { + throw new Error("El precio debe ser mayor a 0."); + } + + // Si oculta datos, debe escribir descripción + if (!contactData.displayInfo && vehicleData.description.length < 10) { + throw new Error("Si ocultas tus datos de contacto, debes detallar un medio de comunicación en la descripción."); + } + + const finalVersionName = modelSearch; + + const adPayload = { + userID: userSession.id, + vehicleTypeID: plan.idRubro, + brandID: Number(vehicleData.brandId), + modelID: vehicleData.modelId ? Number(vehicleData.modelId) : 0, + versionName: finalVersionName, + year: Number(vehicleData.year) || 0, + km: Number(vehicleData.km) || 0, + price: Number(vehicleData.price) || 0, + currency: vehicleData.currency, + description: vehicleData.description, + + isFeatured: plan.paquete === 1, + + contactPhone: contactData.phone, + contactEmail: contactData.email, + displayContactInfo: contactData.displayInfo, + + fuelType: vehicleData.fuelType, + color: vehicleData.color, + segment: vehicleData.segment, + location: vehicleData.location, + condition: vehicleData.condition, + doorCount: vehicleData.doorCount, + transmission: vehicleData.transmission, + steering: vehicleData.steering, + + targetUserID: targetUser?.id, + ghostUserEmail: !targetUser && ghostUser.email ? ghostUser.email : undefined, + ghostFirstName: !targetUser && ghostUser.firstName ? ghostUser.firstName : undefined, + ghostLastName: !targetUser && ghostUser.lastName ? ghostUser.lastName : undefined, + ghostUserPhone: !targetUser && ghostUser.phone ? ghostUser.phone : undefined + }; + + let adId = currentAdId; + + const newPhotos = photos.filter(p => p instanceof File) as File[]; + + if (adId) { + await AdsV2Service.update(adId, adPayload); + if (newPhotos.length > 0) { + await AdsV2Service.uploadPhotos(adId, newPhotos); + } + + const currentAd = await AdsV2Service.getById(adId); + + // Si es Admin O el aviso ya estaba pago/aprobado + if (isAdmin || currentAd.statusID >= 3) { + alert("Cambios guardados."); + // Si es Admin, pasamos TRUE. Si es usuario editando un aviso ya activo, pasamos FALSE (para que diga "Guardado") + onSuccess(adId!, isAdmin); + return; + } + } else { + const response = await AdsV2Service.createDraft(adPayload); + adId = response.adID; + setCurrentAdId(adId); + + if (photos.length > 0 && adId) { + const allFiles = photos as File[]; + await AdsV2Service.uploadPhotos(adId, allFiles); + } + } + + if (isAdmin) { + onSuccess(adId!, true); + return; + } + + // Validación específica para Admin creando usuario fantasma + if (isAdmin && !targetUser && !editId) { + if (!ghostUser.email || !ghostUser.firstName || !ghostUser.lastName || !ghostUser.phone) { + setError("Para crear un nuevo cliente, debes completar Nombre, Apellido, Email y Teléfono."); + setLoading(false); + return; + } + // Validar formato de email básico + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(ghostUser.email)) { + setError("El email del nuevo cliente no es válido."); + setLoading(false); + return; + } + } + + setLoading(false); + setShowPaymentForm(true); + + } catch (err: any) { + console.error("Error al guardar aviso:", err.response?.data || err); + const backendError = err.response?.data; + let msg = "Error al procesar el aviso."; + + if (backendError?.errors) { + // Extraer el primer error de validación de .NET + const firstKey = Object.keys(backendError.errors)[0]; + msg = backendError.errors[firstKey][0]; + } else if (backendError?.message) { + msg = backendError.message; + } else if (err.message) { + msg = err.message; + } + + setError(msg); + setLoading(false); + } + }; + + const handlePaymentSuccess = async (paymentData: any) => { + try { + const finalPayload = { ...paymentData, adId: currentAdId }; + const response = await AdsV2Service.processPayment(finalPayload); + + if (response.status === 'approved') { + onSuccess(currentAdId!); + } else { + navigate(`/pago-confirmado?status=${response.status}&adId=${currentAdId}`); + } + + } catch (err: any) { + console.error("Error completo:", err.response?.data); + const detail = err.response?.data?.detail; + const backendMessage = err.response?.data?.message; + let userMessage = "El pago no pudo ser procesado. Intenta nuevamente."; + + const MP_ERRORS: Record = { + 'cc_rejected_bad_filled_card_number': 'Revisá el número de tarjeta.', + 'cc_rejected_bad_filled_date': 'Revisá la fecha de vencimiento.', + 'cc_rejected_bad_filled_other': 'Revisá los datos de la tarjeta.', + 'cc_rejected_bad_filled_security_code': 'Revisá el código de seguridad (CVV).', + 'cc_rejected_blacklist': 'No pudimos procesar tu pago.', + 'cc_rejected_call_for_authorize': 'Debes autorizar el pago llamando a tu banco.', + 'cc_rejected_card_disabled': 'Llamá a tu banco para activar tu tarjeta.', + 'cc_rejected_card_error': 'No pudimos procesar tu pago.', + 'cc_rejected_duplicated_payment': 'Ya hiciste un pago por ese valor. Revisa tus movimientos.', + 'cc_rejected_high_risk': 'Tu pago fue rechazado por motivos de seguridad.', + 'cc_rejected_insufficient_amount': 'Tu tarjeta no tiene fondos suficientes.', + 'cc_rejected_invalid_installments': 'La tarjeta no procesa pagos en la cantidad de cuotas seleccionada.', + 'cc_rejected_max_attempts': 'Llegaste al límite de intentos permitidos.', + 'cc_rejected_other_reason': 'La tarjeta rechazó el pago.' + }; + + if (detail && MP_ERRORS[detail]) { + userMessage = MP_ERRORS[detail]; + } else if (backendMessage) { + userMessage = backendMessage; + } else if (err.response?.data?.errors) { + const firstKey = Object.keys(err.response.data.errors)[0]; + userMessage = `${err.response.data.errors[firstKey][0]}`; + } + + setError(userMessage); + } + }; + + if (showPaymentForm && currentAdId) { + const rawMonto = plan.importeTotsiniva > 0 + ? plan.importeTotsiniva * 1.105 + : plan.importeSiniva * 1.105; + + const monto = Math.round(rawMonto); + + return ( +
    + {/* HEADER DEL PAGO */} +
    + + {/* Tarjeta contenedora con gradiente sutil hacia abajo */} +
    + + {/* Fondo decorativo "Atmósfera MP" (Brillo azul en la parte superior) */} +
    + +

    + Finalizar Compra +

    +

    + Completa los datos para publicar tu aviso. +

    + +
    +
    + +
    + Total a pagar + + ${monto.toLocaleString('es-AR')} + +
    + + {/* Separador vertical (solo desktop) */} +
    + {/* Separador horizontal (solo mobile) */} +
    + + {/* Contenedor del Logo con Fondo Sólido */} +
    + +
    +
    + + + Procesado de forma segura por Mercado Pago + +
    +
    +
    + + {error && ( +
    + {error} +
    + )} + + setError(msg)} + onCancel={() => setShowPaymentForm(false)} + /> +
    + ); + } + + const buttonText = loading + ? 'Procesando...' + : (isAdmin + ? (editId ? 'Guardar Cambios' : 'Publicar Directamente') + : (editId ? 'Guardar Cambios' : 'Ir a Pagar') + ); + + return ( +
    + + {/* --- SECCIÓN ADMIN --- */} + {isAdmin && !editId && ( +
    +

    Panel Administrativo

    +
    +
    + + { + setUserSearch(e.target.value); + setShowUserSuggestions(true); + if (e.target.value === '') setTargetUser(null); + }} + onFocus={() => setShowUserSuggestions(true)} + /> + {showUserSuggestions && userSuggestions.length > 0 && ( +
      + {userSuggestions.map(u => ( +
    • selectUser(u)} + className="px-4 py-3 hover:bg-amber-600/20 cursor-pointer text-sm transition-colors border-b border-white/5 last:border-0 flex justify-between" + > + {u.name} + {u.email} +
    • + ))} +
    + )} + {targetUser && ( +
    + Asignado a: {targetUser.name} (ID: {targetUser.id}) + +
    + )} +
    +
    +
    + O crear nuevo +
    +
    +
    +
    + setGhostUser({ ...ghostUser, email: e.target.value })} + className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:border-amber-500/50 outline-none" + /> + setGhostUser({ ...ghostUser, firstName: e.target.value })} + className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:border-amber-500/50 outline-none" + /> + setGhostUser({ ...ghostUser, lastName: e.target.value })} + className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:border-amber-500/50 outline-none" + /> + setGhostUser({ ...ghostUser, phone: e.target.value })} + className="bg-black/20 border border-white/10 rounded-xl px-4 py-2 text-sm text-white focus:border-amber-500/50 outline-none" + /> +
    +
    +
    +
    + )} + +
    +
    +

    + 1 + Datos del Vehículo +

    +
    +
    + + { + setVehicleData({ ...vehicleData, brandId: val, modelId: '', modelName: '' }); + setModelSearch(''); + setModelSuggestions([]); + }} + placeholder="Buscar Marca..." + /> +
    +
    + + { + const val = e.target.value; + setModelSearch(val); + setShowSuggestions(true); + setVehicleData(prev => ({ + ...prev, + modelId: '', + modelName: val + })); + }} + onFocus={() => setShowSuggestions(true)} + className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white outline-none focus:border-blue-500 disabled:opacity-50" + /> + {showSuggestions && modelSuggestions.length > 0 && ( +
      + {modelSuggestions.map(m => ( +
    • selectModel(m.id, m.name)} + className="px-4 py-3 hover:bg-blue-600 cursor-pointer text-sm transition-colors border-b border-white/5 last:border-0" + > + {m.name} +
    • + ))} +
    + )} +
    + {/* INPUTS NUMÉRICOS SEGUROS */} +
    + + handleNumberInput('year', e.target.value)} /> +
    +
    + + handleNumberInput('km', e.target.value)} /> +
    +
    + +
    + + handleFloatInput('price', e.target.value)} /> +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + setVehicleData({ ...vehicleData, color: e.target.value })} /> +
    + {plan.idRubro !== VEHICLE_TYPES.MOTOS && ( +
    + + handleNumberInput('doorCount', e.target.value)} /> +
    + )} +
    + + +
    +
    + + setVehicleData({ ...vehicleData, location: e.target.value })} /> +
    +
    + + +
    + {plan.idRubro !== VEHICLE_TYPES.MOTOS && ( +
    + + +
    + )} +
    +
    +
    + +
    + {/* Fondo decorativo sutil */} +
    + +

    + 2 + Multimedia y Detalles +

    + +
    + + {/* 1. DESCRIPCIÓN (Ancho completo) */} +
    +
    + + 900 ? 'text-amber-500' : 'text-gray-600'}`}> + {vehicleData.description.length}/1000 + +
    + +
    +