using BCards.Web.Models; using BCards.Web.Services; using BCards.Web.Utils; using BCards.Web.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Configuration; using System.Security.Claims; using System.Text; using System.Text.Json; using System.Linq; using MongoDB.Bson; using System.Collections.Generic; namespace BCards.Web.Controllers; [Authorize] [Route("Admin")] public class AdminController : Controller { private readonly IAuthService _authService; private readonly IUserPageService _userPageService; private readonly ICategoryService _categoryService; private readonly IThemeService _themeService; private readonly IModerationService _moderationService; private readonly IEmailService _emailService; private readonly ILivePageService _livePageService; private readonly IImageStorageService _imageStorage; private readonly IDocumentStorageService _documentStorage; private readonly IPaymentService _paymentService; private readonly IDowngradeService _downgradeService; private readonly ILogger _logger; private readonly IConfiguration _configuration; public AdminController( IAuthService authService, IUserPageService userPageService, ICategoryService categoryService, IThemeService themeService, IModerationService moderationService, IEmailService emailService, ILivePageService livePageService, IImageStorageService imageStorage, IDocumentStorageService documentStorage, IPaymentService paymentService, IDowngradeService downgradeService, ILogger logger, IConfiguration configuration) { _authService = authService; _userPageService = userPageService; _categoryService = categoryService; _themeService = themeService; _moderationService = moderationService; _emailService = emailService; _livePageService = livePageService; _imageStorage = imageStorage; _documentStorage = documentStorage; _paymentService = paymentService; _downgradeService = downgradeService; _logger = logger; _configuration = configuration; } [HttpGet] [Route("Dashboard")] public async Task Dashboard() { ViewBag.IsHomePage = false; // Menu normal do dashboard var user = await _authService.GetCurrentUserAsync(User); if (user == null) return RedirectToAction("Login", "Auth"); _logger.LogInformation("[DASHBOARD DEBUG] User {UserId} ({Email}) - CurrentPlan: '{CurrentPlan}'", user.Id, user.Email, user.CurrentPlan ?? "NULL"); var userPlanType = Enum.TryParse(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial; _logger.LogInformation("[DASHBOARD DEBUG] Parsed PlanType: {PlanType} (from '{CurrentPlan}')", userPlanType, user.CurrentPlan ?? "NULL"); var userPages = await _userPageService.GetUserPagesAsync(user.Id); var listCounts = new Dictionary(); // Atualizar status das baseado nas livepasges foreach (var page in userPages) { if (page.Status == ViewModels.PageStatus.Active) { var livePage = await _livePageService.GetLivePageFromUserPageId(page.Id); if (livePage != null) { listCounts.Add(page.Id, new { TotalViews = livePage.Analytics?.TotalViews ?? 0, TotalClicks = livePage.Analytics?.TotalClicks ?? 0 }); } } else { listCounts.Add(page.Id, new { TotalViews = (long)(page.Analytics?.TotalViews ?? 0), TotalClicks = (long)(page.Analytics?.TotalClicks ?? 0) }); } } var dashboardModel = new DashboardViewModel { CurrentUser = user, UserPages = userPages.Select(p => new UserPageSummary { Id = p.Id, DisplayName = p.DisplayName, Slug = p.Slug, Category = p.Category, Status = p.Status, TotalClicks = listCounts[p.Id].TotalClicks ?? 0, TotalViews = listCounts[p.Id].TotalViews ?? 0, PreviewToken = p.PreviewToken, CreatedAt = p.CreatedAt, LastModerationStatus = p.ModerationHistory == null || p.ModerationHistory.Count == 0 || p.ModerationHistory.Last().Status == "rejected" ? null : Enum.Parse(p.ModerationHistory.Last().Status, true), Motive = p.ModerationHistory == null || p.ModerationHistory.Count == 0 || p.ModerationHistory.Last().Status != "rejected" ? "" : p.ModerationHistory.Last().Reason }).ToList(), CurrentPlan = new PlanInfo { Type = userPlanType, Name = userPlanType.GetDisplayName(), MaxPages = userPlanType.GetMaxPages(), MaxLinksPerPage = userPlanType.GetMaxLinksPerPage(), DurationDays = userPlanType.GetTrialDays(), Price = userPlanType.GetPrice(), AllowsAnalytics = userPlanType.AllowsAnalytics(), AllowsCustomThemes = userPlanType.AllowsCustomThemes() }, CanCreateNewPage = userPages.Count < userPlanType.GetMaxPages(), DaysRemaining = userPlanType == PlanType.Trial ? CalculateTrialDaysRemaining(user) : 0 }; return View(dashboardModel); } private int CalculateTrialDaysRemaining(User user) { // This would be calculated based on subscription data // For now, return a default value return 7; } [HttpGet] [Route("ManagePage")] public async Task ManagePage(string id = null) { try { ViewBag.IsHomePage = false; var user = await _authService.GetCurrentUserAsync(User); if (user == null) return RedirectToAction("Login", "Auth"); var userPlanType = Enum.TryParse(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial; var categories = await _categoryService.GetAllCategoriesAsync(); var themes = await _themeService.GetAvailableThemesAsync(); if (string.IsNullOrEmpty(id) || id == "new") { // Check if user can create new page var existingPages = await _userPageService.GetUserPagesAsync(user.Id); var maxPages = userPlanType.GetMaxPages(); if (existingPages.Count >= maxPages) { TempData["Error"] = $"Você já atingiu o limite de {maxPages} página(s) do seu plano atual. Faça upgrade para criar mais páginas."; return RedirectToAction("Dashboard"); } // CRIAR NOVA PÁGINA var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString()); var model = new ManagePageViewModel { IsNewPage = true, AvailableCategories = categories, AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(), MaxLinksAllowed = userPlanType.GetMaxLinksPerPage(), AllowProductLinks = planLimitations.AllowProductLinks, AllowDocumentUpload = planLimitations.AllowDocumentUpload, MaxDocumentsAllowed = planLimitations.MaxDocuments, Documents = new List(), DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay() }; return View(model); } else { // EDITAR PÁGINA EXISTENTE var page = await _userPageService.GetPageByIdAsync(id); if (page == null || page.UserId != user.Id) return NotFound(); var model = await MapToManageViewModel(page, categories, themes, userPlanType); return View(model); } } catch (Exception ex) { _logger.LogError(ex, "Error in ManagePage GET"); TempData["Error"] = "Ocorreu um erro ao carregar a página. Tente novamente."; throw new Exception("Erro ao salvar o bcard", ex); } } [HttpPost] [Route("ManagePage")] [RequestSizeLimit(5 * 1024 * 1024)] // Allow 5MB uploads [RequestFormLimits(MultipartBodyLengthLimit = 5 * 1024 * 1024)] public async Task ManagePage(ManagePageViewModel model) { string userId = ""; try { ViewBag.IsHomePage = false; var user = await _authService.GetCurrentUserAsync(User); if (user == null) return RedirectToAction("Login", "Auth"); userId = user.Id; var userPlanType = Enum.TryParse(user.CurrentPlan, true, out var parsedPlan) ? parsedPlan : PlanType.Trial; var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString()); model.AllowProductLinks = planLimitations.AllowProductLinks; model.AllowDocumentUpload = planLimitations.AllowDocumentUpload; model.MaxDocumentsAllowed = planLimitations.MaxDocuments; model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay(); // Limpar campos de redes sociais que são apenas espaços (tratados como vazios) CleanSocialMediaFields(model); AdjustModelState(ModelState, model); _logger.LogInformation($"ManagePage POST: IsNewPage={model.IsNewPage}, DisplayName={model.DisplayName}, Category={model.Category}, Links={model.Links?.Count ?? 0}"); //Logar modelstate em information _logger.LogInformation($"ModelState: {JsonSerializer.Serialize(ModelState)}"); // Processar upload de imagem se fornecida if (model.ProfileImageFile != null && model.ProfileImageFile.Length > 0) { try { using var memoryStream = new MemoryStream(); await model.ProfileImageFile.CopyToAsync(memoryStream); var imageBytes = memoryStream.ToArray(); var imageId = await _imageStorage.SaveImageAsync( imageBytes, model.ProfileImageFile.FileName, model.ProfileImageFile.ContentType ); model.ProfileImageId = imageId; _logger.LogInformation("Profile image uploaded successfully: {ImageId}", imageId); } catch (Exception ex) { _logger.LogError(ex, "Userid: {UserId} - Error uploading profile image. FileName: {FileName}, ContentType: {ContentType}, Size: {Size}KB, ExceptionType: {ExceptionType}", userId, model.ProfileImageFile?.FileName ?? "Unknown", model.ProfileImageFile?.ContentType ?? "Unknown", model.ProfileImageFile?.Length / 1024 ?? 0, ex.GetType().Name); // Mensagem específica baseada no tipo de erro var errorMessage = ex is ArgumentException argEx ? argEx.Message : "Erro ao processar a imagem. Verifique o formato e tamanho."; ModelState.AddModelError("ProfileImageFile", errorMessage); TempData["ImageError"] = errorMessage; // Preservar dados do form e repopular dropdowns model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); model.MaxLinksAllowed = userPlanType.GetMaxLinksPerPage(); model.AllowProductLinks = planLimitations.AllowProductLinks; model.AllowDocumentUpload = planLimitations.AllowDocumentUpload; model.MaxDocumentsAllowed = planLimitations.MaxDocuments; model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay(); // Preservar ProfileImageId existente se estava editando if (!model.IsNewPage && !string.IsNullOrEmpty(model.Id)) { var existingPage = await _userPageService.GetPageByIdAsync(model.Id); if (existingPage != null) { model.ProfileImageId = existingPage.ProfileImageId; } } return View(model); } } var (processedDocuments, removedFileIds, newFileIds) = await BuildDocumentsAsync(model, planLimitations); if (!ModelState.IsValid) { var sbError = new StringBuilder(); sbError.AppendLine("ModelState is invalid!"); foreach (var error in ModelState) { var erroMsg = string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage)); if (!string.IsNullOrEmpty(erroMsg)) { sbError.AppendLine($"Key: {error.Key}, Errors: {erroMsg}"); } } _logger.LogWarning(sbError.ToString()); // Repopulate dropdowns var slug = await _userPageService.GenerateSlugAsync(model.Category, model.DisplayName); model.Slug = slug; model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); model.AllowProductLinks = planLimitations.AllowProductLinks; model.AllowDocumentUpload = planLimitations.AllowDocumentUpload; model.MaxDocumentsAllowed = planLimitations.MaxDocuments; model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay(); return View(model); } if (model.IsNewPage) { // CRITICAL: Check if user can create new page (validate MaxPages limit) var existingPages = await _userPageService.GetUserPagesAsync(user.Id); var maxPages = userPlanType.GetMaxPages(); if (existingPages.Count >= maxPages) { TempData["Error"] = $"Você já atingiu o limite de {maxPages} página(s) do seu plano atual. Faça upgrade para criar mais páginas."; model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay(); return View(model); } // Generate slug if not provided if (string.IsNullOrEmpty(model.Slug)) { model.Slug = await _userPageService.GenerateSlugAsync(model.Category, model.DisplayName); } // Check if slug is available if (!await _userPageService.ValidateSlugAsync(model.Category, model.Slug)) { ModelState.AddModelError("Slug", "Esta URL já está em uso. Tente outra."); model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay(); return View(model); } // Check if user can create the requested number of links var activeLinksCount = model.Links?.Count ?? 0; if (activeLinksCount > model.MaxLinksAllowed) { ModelState.AddModelError("", $"Você excedeu o limite de {model.MaxLinksAllowed} links do seu plano atual."); model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay(); return View(model); } try { // Create new page var userPage = await MapToUserPage(model, user.Id); userPage.Documents = processedDocuments; _logger.LogInformation($"Mapped to UserPage: {userPage.DisplayName}, Category: {userPage.Category}, Slug: {userPage.Slug}"); // Set status to Creating for new pages userPage.Status = ViewModels.PageStatus.Creating; await _userPageService.CreatePageAsync(userPage); _logger.LogInformation("Page created successfully!"); // Token será gerado apenas quando usuário clicar "Testar Página" TempData["Success"] = "Página criada com sucesso! Use o botão 'Enviar para Moderação' quando estiver pronta."; } catch (Exception ex) { _logger.LogError(ex, $"Userid: {userId} - Error creating page"); ModelState.AddModelError("", "Erro ao criar página. Tente novamente."); model.AvailableCategories = await _categoryService.GetAllCategoriesAsync(); model.AvailableThemes = await _themeService.GetAvailableThemesAsync(); model.DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay(); TempData["Error"] = $"Erro ao criar página. Tente novamente. TechMsg: {ex.Message}"; return View(model); } } else { // Update existing page var existingPage = await _userPageService.GetPageByIdAsync(model.Id); if (existingPage == null || existingPage.UserId != user.Id) { await CleanupNewDocumentsAsync(newFileIds); return NotFound(); } if (!planLimitations.AllowDocumentUpload && existingPage.Documents?.Any() == true) { removedFileIds.AddRange(existingPage.Documents.Select(d => d.FileId)); } // Check if user can create pages (for users with rejected pages) var canCreatePage = await _moderationService.CanUserCreatePageAsync(user.Id); if (!canCreatePage) { TempData["Error"] = "Você não pode editar páginas devido a muitas rejeições. Entre em contato com o suporte."; return RedirectToAction("Dashboard"); } // IMPORTANTE: Tratar remoção de imagem ou preservar existente se não houver novo upload if (model.ProfileImageFile == null || model.ProfileImageFile.Length == 0) { if (model.ProfileImageId == "REMOVE_IMAGE") { // Usuário quer remover a imagem existente model.ProfileImageId = null; _logger.LogInformation("Profile image removed by user request"); } else { // Preservar imagem existente model.ProfileImageId = existingPage.ProfileImageId; } } await UpdateUserPageFromModel(existingPage, model, processedDocuments); // Set status to PendingModeration for updates existingPage.Status = ViewModels.PageStatus.Creating; existingPage.ModerationAttempts = existingPage.ModerationAttempts; await _userPageService.UpdatePageAsync(existingPage); if (removedFileIds.Count > 0) { foreach (var fileId in removedFileIds) { try { await _documentStorage.DeleteDocumentAsync(fileId); } catch (Exception cleanupEx) { _logger.LogWarning(cleanupEx, "Erro ao remover documento antigo {FileId}", fileId); } } } // Token será gerado apenas quando usuário clicar "Testar Página" // Send email to user await _emailService.SendModerationStatusAsync( user.Email, user.Name, existingPage.DisplayName, "pending", null, null); // previewUrl não é mais necessário - token será gerado no clique TempData["Success"] = "Página atualizada! Teste e envie para moderação."; } return RedirectToAction("Dashboard"); } catch (Exception ex) { _logger.LogError(ex, $"Userid: {userId} - Error in ManagePage GET"); TempData["Error"] = "Ocorreu um erro ao carregar a página. Tente novamente."; throw new Exception("Erro ao salvar o bcard", ex); } } [HttpPost] [Route("CreatePage")] public async Task CreatePage(CreatePageViewModel model) { var user = await _authService.GetCurrentUserAsync(User); if (user == null) return RedirectToAction("Login", "Auth"); // Check if user already has a page var existingPage = await _userPageService.GetUserPageAsync(user.Id); if (existingPage != null) return RedirectToAction("EditPage"); if (!ModelState.IsValid) { var categories = await _categoryService.GetAllCategoriesAsync(); var themes = await _themeService.GetAvailableThemesAsync(); ViewBag.Categories = categories; ViewBag.Themes = themes; return View(model); } // Generate slug if not provided if (string.IsNullOrEmpty(model.Slug)) { model.Slug = await _userPageService.GenerateSlugAsync(model.Category, model.DisplayName); } // Check if slug is available if (!await _userPageService.ValidateSlugAsync(model.Category, model.Slug)) { ModelState.AddModelError("Slug", "Esta URL já está em uso. Tente outra."); var categories = await _categoryService.GetAllCategoriesAsync(); var themes = await _themeService.GetAvailableThemesAsync(); ViewBag.Categories = categories; ViewBag.Themes = themes; return View(model); } // Check if user can create the requested number of links var activeLinksCount = model.Links?.Count ?? 0; if (!await _userPageService.CanCreateLinksAsync(user.Id, activeLinksCount)) { ModelState.AddModelError("", "Você excedeu o limite de links do seu plano atual."); var categories = await _categoryService.GetAllCategoriesAsync(); var themes = await _themeService.GetAvailableThemesAsync(); ViewBag.Categories = categories; ViewBag.Themes = themes; return View(model); } // Convert ViewModel to UserPage var userPage = new UserPage { UserId = user.Id, DisplayName = model.DisplayName, Category = model.Category, BusinessType = model.BusinessType, Bio = model.Bio, Slug = model.Slug, Theme = await _themeService.GetThemeByNameAsync(model.SelectedTheme) ?? _themeService.GetDefaultTheme(), Links = model.Links?.Select(l => new LinkItem { Title = l.Title, Url = l.Url, Description = l.Description, Icon = l.Icon, IsActive = true, Order = model.Links.IndexOf(l) }).ToList() ?? new List() }; // Add social media links var socialLinks = new List(); if (!string.IsNullOrEmpty(model.WhatsAppNumber)) { socialLinks.Add(new LinkItem { Title = "WhatsApp", Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}", Icon = "fab fa-whatsapp", IsActive = true, Order = userPage.Links.Count + socialLinks.Count }); } if (!string.IsNullOrEmpty(model.FacebookUrl)) { socialLinks.Add(new LinkItem { Title = "Facebook", Url = model.FacebookUrl, Icon = "fab fa-facebook", IsActive = true, Order = userPage.Links.Count + socialLinks.Count }); } if (!string.IsNullOrEmpty(model.TwitterUrl)) { socialLinks.Add(new LinkItem { Title = "X / Twitter", Url = model.TwitterUrl, Icon = "fab fa-x-twitter", IsActive = true, Order = userPage.Links.Count + socialLinks.Count }); } if (!string.IsNullOrEmpty(model.InstagramUrl)) { socialLinks.Add(new LinkItem { Title = "Instagram", Url = model.InstagramUrl, Icon = "fab fa-instagram", IsActive = true, Order = userPage.Links.Count + socialLinks.Count }); } if (!string.IsNullOrEmpty(model.TiktokUrl)) { socialLinks.Add(new LinkItem { Title = "TikTok", Url = model.TiktokUrl, Icon = "fab fa-tiktok", IsActive = true, Order = userPage.Links.Count + socialLinks.Count }); } if (!string.IsNullOrEmpty(model.PinterestUrl)) { socialLinks.Add(new LinkItem { Title = "Pinterest", Url = model.PinterestUrl, Icon = "fab fa-pinterest", IsActive = true, Order = userPage.Links.Count + socialLinks.Count }); } if (!string.IsNullOrEmpty(model.DiscordUrl)) { socialLinks.Add(new LinkItem { Title = "Discord", Url = model.DiscordUrl, Icon = "fab fa-discord", IsActive = true, Order = userPage.Links.Count + socialLinks.Count }); } if (!string.IsNullOrEmpty(model.KawaiUrl)) { socialLinks.Add(new LinkItem { Title = "Kawai", Url = model.KawaiUrl, Icon = "fas fa-heart", IsActive = true, Order = userPage.Links.Count + socialLinks.Count }); } userPage.Links.AddRange(socialLinks); await _userPageService.CreatePageAsync(userPage); TempData["Success"] = "Página criada com sucesso!"; return RedirectToAction("Dashboard"); } [HttpGet] [Route("EditPage")] public async Task EditPage() { var user = await _authService.GetCurrentUserAsync(User); if (user == null) return RedirectToAction("Login", "Auth"); var userPage = await _userPageService.GetUserPageAsync(user.Id); var categories = await _categoryService.GetAllCategoriesAsync(); var themes = await _themeService.GetAvailableThemesAsync(); ViewBag.Categories = categories; ViewBag.Themes = themes; ViewBag.IsNew = userPage == null; return View(userPage ?? new UserPage { UserId = user.Id }); } [HttpPost] [Route("EditPage")] public async Task EditPage(UserPage model) { var user = await _authService.GetCurrentUserAsync(User); if (user == null) return RedirectToAction("Login", "Auth"); if (!ModelState.IsValid) { var categories = await _categoryService.GetAllCategoriesAsync(); var themes = await _themeService.GetAvailableThemesAsync(); ViewBag.Categories = categories; ViewBag.Themes = themes; return View(model); } // Check if user can create the requested number of links var activeLinksCount = model.Links?.Count(l => l.IsActive) ?? 0; if (!await _userPageService.CanCreateLinksAsync(user.Id, activeLinksCount)) { ModelState.AddModelError("", "Você excedeu o limite de links do seu plano atual."); var categories = await _categoryService.GetAllCategoriesAsync(); var themes = await _themeService.GetAvailableThemesAsync(); ViewBag.Categories = categories; ViewBag.Themes = themes; return View(model); } model.UserId = user.Id; // Check if slug is available if (!await _userPageService.ValidateSlugAsync(model.Category, model.Slug, model.Id)) { ModelState.AddModelError("Slug", "Esta URL já está em uso. Tente outra."); var categories = await _categoryService.GetAllCategoriesAsync(); var themes = await _themeService.GetAvailableThemesAsync(); ViewBag.Categories = categories; ViewBag.Themes = themes; return View(model); } if (string.IsNullOrEmpty(model.Id)) { await _userPageService.CreatePageAsync(model); } else { await _userPageService.UpdatePageAsync(model); } TempData["Success"] = "Página salva com sucesso!"; return RedirectToAction("Dashboard"); } [HttpPost] [Route("CheckSlugAvailability")] public async Task CheckSlugAvailability(string category, string slug, string? excludeId = null) { if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(slug)) return Json(new { available = false, message = "Categoria e slug são obrigatórios." }); var isValid = await _userPageService.ValidateSlugAsync(category, slug, excludeId); return Json(new { available = isValid, message = isValid ? "URL disponível!" : "Esta URL já está em uso." }); } [HttpPost] [Route("GenerateSlug")] public async Task GenerateSlug(string category, string name) { if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(name)) return Json(new { slug = "", category = "" }); var slug = await _userPageService.GenerateSlugAsync(category, name); var categorySlug = SlugHelper.CreateCategorySlug(category).ToLower(); return Json(new { slug = slug, category = categorySlug }); } [HttpGet] [Route("Analytics")] public async Task Analytics() { ViewBag.IsHomePage = false; var user = await _authService.GetCurrentUserAsync(User); if (user == null) return RedirectToAction("Login", "Auth"); var userPage = await _userPageService.GetUserPageAsync(user.Id); if (userPage == null || !userPage.PlanLimitations.AllowAnalytics) return RedirectToAction("Dashboard"); return View(userPage); } [HttpPost] [Route("DeletePage/{id}")] public async Task DeletePage(string id) { var user = await _authService.GetCurrentUserAsync(User); if (user == null) return RedirectToAction("Login", "Auth"); var userPage = await _userPageService.GetPageByIdAsync(id); if (userPage == null || userPage.UserId != user.Id) { TempData["Error"] = "Página não encontrada!"; return RedirectToAction("Dashboard"); } await _userPageService.DeletePageAsync(userPage.Id); TempData["Success"] = "Página excluída com sucesso!"; return RedirectToAction("Dashboard"); } private async Task MapToManageViewModel(UserPage page, List categories, List themes, PlanType userPlanType) { var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString()); return new ManagePageViewModel { Id = page.Id, IsNewPage = false, DisplayName = page.DisplayName, Category = page.Category, BusinessType = page.BusinessType, Bio = page.Bio, Slug = page.Slug, SelectedTheme = page.Theme?.Name ?? "minimalist", ProfileImageId = page.ProfileImageId, Links = page.Links?.Select((l, index) => new ManageLinkViewModel { Id = $"link_{index}", Title = l.Title, Url = l.Url, Description = l.Description, Icon = l.Icon, Order = l.Order, IsActive = l.IsActive, Type = l.Type, ProductTitle = l.ProductTitle, ProductImage = l.ProductImage, ProductPrice = l.ProductPrice, ProductDescription = l.ProductDescription, ProductDataCachedAt = l.ProductDataCachedAt }).ToList() ?? new List(), Documents = page.Documents?.Select((d, index) => new ManageDocumentViewModel { Id = string.IsNullOrEmpty(d.Id) ? $"doc_{index}" : d.Id, DocumentId = d.FileId, Title = d.Title, Description = d.Description, FileName = d.FileName, FileSize = d.FileSize, UploadedAt = d.UploadedAt }).ToList() ?? new List(), AvailableCategories = categories, AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(), MaxLinksAllowed = userPlanType.GetMaxLinksPerPage(), AllowProductLinks = planLimitations.AllowProductLinks, AllowDocumentUpload = planLimitations.AllowDocumentUpload, MaxDocumentsAllowed = planLimitations.MaxDocuments, DocumentUploadPlansDisplay = GetDocumentUploadPlansDisplay() }; } private async Task MapToUserPage(ManagePageViewModel model, string userId) { var theme = await _themeService.GetThemeByNameAsync(model.SelectedTheme) ?? _themeService.GetDefaultTheme(); var userPage = new UserPage { UserId = userId, DisplayName = model.DisplayName, Category = SlugHelper.ConvertCategory(model.Category.ToLower()), BusinessType = model.BusinessType, Bio = model.Bio, Slug = SlugHelper.CreateSlug(model.Slug.ToLower()), Theme = theme, Status = ViewModels.PageStatus.Active, ProfileImageId = model.ProfileImageId, Links = new List() }; // Add regular links if (model.Links?.Any() == true) { userPage.Links.AddRange(model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url)) .Select((l, index) => new LinkItem { Title = l.Title, Url = l.Url.ToLower(), Description = l.Description, Icon = l.Icon, IsActive = l.IsActive, Order = index, Type = l.Type, ProductTitle = l.ProductTitle, ProductImage = l.ProductImage, ProductPrice = l.ProductPrice, ProductDescription = l.ProductDescription, ProductDataCachedAt = l.ProductDataCachedAt })); } // Add social media links var socialLinks = new List(); var currentOrder = userPage.Links.Count; if (!string.IsNullOrEmpty(model.WhatsAppNumber)) { socialLinks.Add(new LinkItem { Title = "WhatsApp", Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}", Icon = "fab fa-whatsapp", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.FacebookUrl)) { socialLinks.Add(new LinkItem { Title = "Facebook", Url = model.FacebookUrl, Icon = "fab fa-facebook", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.TwitterUrl)) { socialLinks.Add(new LinkItem { Title = "X / Twitter", Url = model.TwitterUrl, Icon = "fab fa-x-twitter", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.InstagramUrl)) { socialLinks.Add(new LinkItem { Title = "Instagram", Url = model.InstagramUrl, Icon = "fab fa-instagram", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.TiktokUrl)) { socialLinks.Add(new LinkItem { Title = "TikTok", Url = model.TiktokUrl, Icon = "fab fa-tiktok", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.PinterestUrl)) { socialLinks.Add(new LinkItem { Title = "Pinterest", Url = model.PinterestUrl, Icon = "fab fa-pinterest", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.DiscordUrl)) { socialLinks.Add(new LinkItem { Title = "Discord", Url = model.DiscordUrl, Icon = "fab fa-discord", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.KawaiUrl)) { socialLinks.Add(new LinkItem { Title = "Kawai", Url = model.KawaiUrl, Icon = "fas fa-heart", IsActive = true, Order = currentOrder++ }); } userPage.Links.AddRange(socialLinks); return userPage; } private string GetDocumentUploadPlansDisplay() { var sections = _configuration.GetSection("Plans").GetChildren(); var friendlyNames = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var section in sections) { if (!bool.TryParse(section["AllowDocumentUpload"], out var allow) || !allow) continue; var key = section.Key ?? string.Empty; var normalizedKey = key.EndsWith("Yearly", StringComparison.OrdinalIgnoreCase) ? key[..^6] : key; if (Enum.TryParse(normalizedKey, true, out var planType)) { friendlyNames.Add(planType.GetDisplayName()); } else { var display = section["Name"] ?? key; if (!string.IsNullOrWhiteSpace(display)) { friendlyNames.Add(display); } } } if (friendlyNames.Count == 0) return "planos com suporte a documentos"; var orderedNames = friendlyNames.OrderBy(name => name).ToList(); return orderedNames.Count switch { 1 => orderedNames[0], 2 => string.Join(" e ", orderedNames), _ => $"{string.Join(", ", orderedNames.Take(orderedNames.Count - 1))} e {orderedNames.Last()}" }; } private async Task<(List Documents, List RemovedFileIds, List NewlyUploadedFileIds)> BuildDocumentsAsync(ManagePageViewModel model, PlanLimitations planLimitations) { var documents = new List(); var removedFileIds = new List(); var newFileIds = new List(); if (!planLimitations.AllowDocumentUpload) { return (documents, removedFileIds, newFileIds); } if (model.Documents == null || model.Documents.Count == 0) return (documents, removedFileIds, newFileIds); for (int index = 0; index < model.Documents.Count; index++) { var docVm = model.Documents[index]; if (docVm == null) continue; var hasExisting = !string.IsNullOrEmpty(docVm.DocumentId); if (docVm.MarkForRemoval) { if (hasExisting && docVm.DocumentId != null) removedFileIds.Add(docVm.DocumentId); continue; } if (string.IsNullOrWhiteSpace(docVm.Title)) { ModelState.AddModelError($"Documents[{index}].Title", "Título é obrigatório"); continue; } string fileId = docVm.DocumentId?.Trim() ?? string.Empty; string fileName = docVm.FileName?.Trim() ?? string.Empty; long fileSize = docVm.FileSize; var uploadedAt = docVm.UploadedAt ?? DateTime.UtcNow; if (hasExisting && string.IsNullOrEmpty(docVm.Id)) { docVm.Id = ObjectId.GenerateNewId().ToString(); } if (docVm.DocumentFile != null && docVm.DocumentFile.Length > 0) { if (!docVm.DocumentFile.ContentType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) { ModelState.AddModelError($"Documents[{index}].DocumentFile", "Envie um arquivo em PDF."); continue; } if (docVm.DocumentFile.Length > 10 * 1024 * 1024) { ModelState.AddModelError($"Documents[{index}].DocumentFile", "Arquivo muito grande. Tamanho máximo: 10MB."); continue; } using var memoryStream = new MemoryStream(); await docVm.DocumentFile.CopyToAsync(memoryStream); var documentBytes = memoryStream.ToArray(); try { fileId = await _documentStorage.SaveDocumentAsync(documentBytes, docVm.DocumentFile.FileName, docVm.DocumentFile.ContentType); fileName = docVm.DocumentFile.FileName; fileSize = docVm.DocumentFile.Length; uploadedAt = DateTime.UtcNow; newFileIds.Add(fileId); if (hasExisting && docVm.DocumentId != null) removedFileIds.Add(docVm.DocumentId); } catch (ArgumentException ex) { ModelState.AddModelError($"Documents[{index}].DocumentFile", ex.Message); continue; } catch (Exception ex) { _logger.LogError(ex, "Erro ao salvar PDF do usuário"); ModelState.AddModelError($"Documents[{index}].DocumentFile", "Erro ao salvar o PDF. Tente novamente."); continue; } } else if (!hasExisting) { // Nenhum arquivo associado: ignorar continue; } if (string.IsNullOrEmpty(fileId)) { ModelState.AddModelError($"Documents[{index}].DocumentFile", "Falha ao processar o documento."); continue; } documents.Add(new PageDocument { Id = string.IsNullOrEmpty(docVm.Id) || !ObjectId.TryParse(docVm.Id, out _) ? ObjectId.GenerateNewId().ToString() : docVm.Id, FileId = fileId, Title = docVm.Title, Description = docVm.Description, FileName = fileName, FileSize = fileSize, UploadedAt = uploadedAt }); docVm.DocumentId = fileId; docVm.FileName = fileName; docVm.FileSize = fileSize; docVm.UploadedAt = uploadedAt; if (string.IsNullOrEmpty(docVm.Id) || !ObjectId.TryParse(docVm.Id, out _)) { docVm.Id = documents.Last().Id; } } if (planLimitations.MaxDocuments > 0 && documents.Count > planLimitations.MaxDocuments) { ModelState.AddModelError("Documents", $"Você pode enviar no máximo {planLimitations.MaxDocuments} documento(s) com seu plano."); } return (documents, removedFileIds, newFileIds); } private async Task CleanupNewDocumentsAsync(IEnumerable documentIds) { if (documentIds == null) return; foreach (var docId in documentIds) { if (string.IsNullOrEmpty(docId)) continue; try { await _documentStorage.DeleteDocumentAsync(docId); } catch (Exception ex) { _logger.LogWarning(ex, "Erro ao limpar documento temporário {DocumentId}", docId); } } } private async Task UpdateUserPageFromModel(UserPage page, ManagePageViewModel model, List documents) { page.DisplayName = model.DisplayName; page.Category = model.Category; page.BusinessType = model.BusinessType; page.Bio = model.Bio; page.Slug = model.Slug; page.ProfileImageId = model.ProfileImageId; // CRUCIAL: Atualizar ProfileImageId // CRUCIAL: Atualizar tema selecionado var selectedTheme = await _themeService.GetThemeByNameAsync(model.SelectedTheme) ?? _themeService.GetDefaultTheme(); page.Theme = selectedTheme; page.UpdatedAt = DateTime.UtcNow; // Update links page.Links = new List(); // Add regular links if (model.Links?.Any() == true) { // Validar links de produto baseado no plano do usuário var user = await _authService.GetCurrentUserAsync(User); var userPlanType = Enum.TryParse(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial; var planLimitations = await _paymentService.GetPlanLimitationsAsync(userPlanType.ToString()); var filteredLinks = model.Links.Where(l => !string.IsNullOrEmpty(l.Title) && !string.IsNullOrEmpty(l.Url)); foreach (var link in filteredLinks) { // Verificar se usuário pode criar links de produto if (link.Type == LinkType.Product && !planLimitations.AllowProductLinks) { throw new InvalidOperationException("Links de produto disponíveis apenas no plano Premium + Afiliados"); } } page.Links.AddRange(filteredLinks.Select((l, index) => new LinkItem { Title = l.Title, Url = l.Url, Description = l.Description, Icon = l.Icon, IsActive = l.IsActive, Order = index, Type = l.Type, ProductTitle = l.ProductTitle, ProductImage = l.ProductImage, ProductPrice = l.ProductPrice, ProductDescription = l.ProductDescription, ProductDataCachedAt = l.ProductDataCachedAt })); } // Add social media links (same logic as create) var socialLinks = new List(); var currentOrder = page.Links.Count; if (!string.IsNullOrEmpty(model.WhatsAppNumber)) { socialLinks.Add(new LinkItem { Title = "WhatsApp", Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}", Icon = "fab fa-whatsapp", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.FacebookUrl)) { socialLinks.Add(new LinkItem { Title = "Facebook", Url = model.FacebookUrl, Icon = "fab fa-facebook", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.TwitterUrl)) { socialLinks.Add(new LinkItem { Title = "X / Twitter", Url = model.TwitterUrl, Icon = "fab fa-x-twitter", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.InstagramUrl)) { socialLinks.Add(new LinkItem { Title = "Instagram", Url = model.InstagramUrl, Icon = "fab fa-instagram", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.TiktokUrl)) { socialLinks.Add(new LinkItem { Title = "TikTok", Url = model.TiktokUrl, Icon = "fab fa-tiktok", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.PinterestUrl)) { socialLinks.Add(new LinkItem { Title = "Pinterest", Url = model.PinterestUrl, Icon = "fab fa-pinterest", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.DiscordUrl)) { socialLinks.Add(new LinkItem { Title = "Discord", Url = model.DiscordUrl, Icon = "fab fa-discord", IsActive = true, Order = currentOrder++ }); } if (!string.IsNullOrEmpty(model.KawaiUrl)) { socialLinks.Add(new LinkItem { Title = "Kawai", Url = model.KawaiUrl, Icon = "fas fa-heart", IsActive = true, Order = currentOrder++ }); } page.Links.AddRange(socialLinks); page.Documents = documents ?? new List(); } [HttpPost] [Route("SubmitForModeration/{id}")] public async Task SubmitForModeration(string id) { var user = await _authService.GetCurrentUserAsync(User); if (user == null) return Json(new { success = false, message = "Usuário não autenticado" }); var pageItem = await _userPageService.GetPageByIdAsync(id); if (pageItem == null || pageItem.UserId != user.Id) return Json(new { success = false, message = "Página não encontrada" }); // Validar status atual if (pageItem.Status != ViewModels.PageStatus.Creating && pageItem.Status != ViewModels.PageStatus.Rejected) return Json(new { success = false, message = "Página não pode ser enviada para moderação neste momento" }); // Validar se tem pelo menos 1 link ativo var activeLinksCount = pageItem.Links?.Count(l => l.IsActive) ?? 0; if (activeLinksCount < 1) return Json(new { success = false, message = "Página deve ter pelo menos 1 link ativo para ser enviada" }); try { // Mudar status para PendingModeration pageItem.Status = ViewModels.PageStatus.PendingModeration; pageItem.ModerationAttempts++; pageItem.UpdatedAt = DateTime.UtcNow; await _userPageService.UpdatePageAsync(pageItem); // Enviar email de notificação ao usuário await _emailService.SendModerationStatusAsync( user.Email, user.Name, pageItem.DisplayName, "pending", null, $"{Request.Scheme}://{Request.Host}/page/{pageItem.Category}/{pageItem.Slug}?preview={pageItem.PreviewToken}"); _logger.LogInformation($"Page {pageItem.Id} submitted for moderation by user {user.Id}"); return Json(new { success = true, message = "Página enviada para moderação com sucesso! Você receberá um email quando for processada.", newStatus = "PendingModeration" }); } catch (Exception ex) { _logger.LogError(ex, $"Error submitting page {id} for moderation"); return Json(new { success = false, message = "Erro interno. Tente novamente." }); } } [HttpPost] [Route("RefreshPreviewToken/{id}")] public async Task RefreshPreviewToken(string id) { var user = await _authService.GetCurrentUserAsync(User); if (user == null) return Json(new { success = false, message = "Não autorizado" }); var pageItem = await _userPageService.GetPageByIdAsync(id); if (pageItem == null || pageItem.UserId != user.Id) return Json(new { success = false, message = "Página não encontrada" }); // Só renovar token para páginas "Creating" e "Rejected" if (pageItem.Status != ViewModels.PageStatus.Creating && pageItem.Status != ViewModels.PageStatus.Rejected) return Json(new { success = false, message = "Token só pode ser renovado para páginas em desenvolvimento ou rejeitadas" }); try { // Gerar novo token com 4 horas de validade var newToken = await _moderationService.GeneratePreviewTokenAsync(pageItem.Id); var previewUrl = $"{Request.Scheme}://{Request.Host}/page/{pageItem.Category}/{pageItem.Slug}?preview={newToken}"; return Json(new { success = true, previewToken = newToken, previewUrl = previewUrl, expiresAt = DateTime.UtcNow.AddMinutes(5).ToString("yyyy-MM-dd HH:mm:ss") }); } catch (Exception ex) { _logger.LogError(ex, $"Error refreshing preview token for page {id}"); return Json(new { success = false, message = "Erro ao renovar token" }); } } [HttpPost] [Route("GeneratePreviewToken/{id}")] public async Task GeneratePreviewToken(string id) { var user = await _authService.GetCurrentUserAsync(User); if (user == null) return Json(new { success = false, message = "Usuário não autenticado" }); _logger.LogInformation($"Generating preview token for page {id} by user {user.Id}"); var pageItem = await _userPageService.GetPageByIdAsync(id); if (pageItem == null || pageItem.UserId != user.Id) return Json(new { success = false, message = "Página não encontrada" }); _logger.LogInformation($"Generating preview token for page {id} preview token {pageItem.PreviewToken} by user {user.Id}"); // Verificar se página pode ter preview if (pageItem.Status != ViewModels.PageStatus.Creating && pageItem.Status != ViewModels.PageStatus.PendingModeration && pageItem.Status != ViewModels.PageStatus.Rejected) { return Json(new { success = false, message = "Preview não disponível para este status" }); } try { // Gerar novo token com 4 horas de validade var newToken = await _moderationService.GeneratePreviewTokenAsync(pageItem.Id); _logger.LogInformation($"Generating preview token for page {id} preview token {pageItem.PreviewToken} by user {user.Id}"); return Json(new { success = true, previewToken = newToken, message = "Preview gerado com sucesso!", expiresAt = DateTime.UtcNow.AddMinutes(5).ToString("yyyy-MM-dd HH:mm:ss") }); } catch (Exception ex) { _logger.LogError(ex, $"Error generating preview token for page {id}"); return Json(new { success = false, message = "Erro interno. Tente novamente." }); } } [HttpPost] [Route("MigrateToLivePages")] public async Task MigrateToLivePages() { var user = await _authService.GetCurrentUserAsync(User); if (user == null) return Json(new { success = false, message = "Usuário não autenticado" }); try { // Buscar todas as páginas ativas do usuário atual var activePages = await _userPageService.GetUserPagesAsync(user.Id); var eligiblePages = activePages.Where(p => p.Status == ViewModels.PageStatus.Active).ToList(); if (!eligiblePages.Any()) { return Json(new { success = false, message = "Nenhuma página ativa encontrada para migração" }); } int successCount = 0; int errorCount = 0; var errors = new List(); foreach (var page in eligiblePages) { try { await _livePageService.SyncFromUserPageAsync(page.Id); successCount++; _logger.LogInformation($"Successfully migrated page {page.Id} ({page.DisplayName}) to LivePages"); } catch (Exception ex) { errorCount++; var errorMsg = $"Erro ao migrar '{page.DisplayName}': {ex.Message}"; errors.Add(errorMsg); _logger.LogError(ex, $"Failed to migrate page {page.Id} to LivePages"); } } var message = $"Migração concluída: {successCount} páginas migradas com sucesso"; if (errorCount > 0) { message += $", {errorCount} erros encontrados"; } return Json(new { success = errorCount == 0, message = message, details = new { totalPages = eligiblePages.Count, successCount = successCount, errorCount = errorCount, errors = errors } }); } catch (Exception ex) { _logger.LogError(ex, "Error during LivePages migration"); return Json(new { success = false, message = $"Erro durante migração: {ex.Message}" }); } } // Método auxiliar para critérios de downgrade (usado na API) private DowngradeCriteria GetDowngradeCriteria(PlanType plan) { return new DowngradeCriteria { MaxPages = plan.GetMaxPages(), MaxLinksPerPage = plan.GetMaxLinksPerPage(), SelectionCriteria = "Páginas mais antigas têm prioridade", LinksCriteria = "Páginas com muitos links são automaticamente suspensas" }; } private void CleanSocialMediaFields(ManagePageViewModel model) { // Tratar espaço em branco como campo vazio para redes sociais if (string.IsNullOrWhiteSpace(model.WhatsAppNumber) || model.WhatsAppNumber.Trim().Length <= 1) model.WhatsAppNumber = string.Empty; if (string.IsNullOrWhiteSpace(model.FacebookUrl) || model.FacebookUrl.Trim().Length <= 1) model.FacebookUrl = string.Empty; if (string.IsNullOrWhiteSpace(model.InstagramUrl) || model.InstagramUrl.Trim().Length <= 1) model.InstagramUrl = string.Empty; if (string.IsNullOrWhiteSpace(model.TwitterUrl) || model.TwitterUrl.Trim().Length <= 1) model.TwitterUrl = string.Empty; if (string.IsNullOrWhiteSpace(model.TiktokUrl) || model.TiktokUrl.Trim().Length <= 1) model.TiktokUrl = string.Empty; if (string.IsNullOrWhiteSpace(model.PinterestUrl) || model.PinterestUrl.Trim().Length <= 1) model.PinterestUrl = string.Empty; if (string.IsNullOrWhiteSpace(model.DiscordUrl) || model.DiscordUrl.Trim().Length <= 1) model.DiscordUrl = string.Empty; if (string.IsNullOrWhiteSpace(model.KawaiUrl) || model.KawaiUrl.Trim().Length <= 1) model.KawaiUrl = string.Empty; } // Endpoint para validar impacto de downgrade [HttpPost] [Route("ValidateDowngrade")] public async Task ValidateDowngrade(string targetPlan) { try { var user = await _authService.GetCurrentUserAsync(User); if (user == null) return Json(new { error = "Usuário não encontrado" }); if (!Enum.TryParse(targetPlan, true, out var newPlan)) return Json(new { error = "Plano inválido" }); var analysis = await _downgradeService.AnalyzeDowngradeImpact(user.Id, newPlan); // CASO CRÍTICO: Nenhuma página atende os critérios if (analysis.IsCritical) { return Json(new { canDowngrade = false, critical = true, title = "⚠️ Downgrade não recomendado", message = "Nenhuma de suas páginas atende aos limites do novo plano. Todas seriam suspensas.", details = analysis.Issues, suggestion = "Considere editar suas páginas para reduzir o número de links antes do downgrade.", criteria = GetDowngradeCriteria(newPlan) }); } // CASO NORMAL: Algumas páginas serão afetadas return Json(new { canDowngrade = true, critical = false, title = "Confirmação de Downgrade", summary = analysis.Summary, eligiblePages = analysis.EligiblePages.Select(p => new { name = p.DisplayName, linkCount = p.LinkCount, reason = "✅ Dentro dos limites" }), suspendedPages = analysis.AffectedPages.Select(p => new { name = p.DisplayName, linkCount = p.LinkCount, reason = p.SuspensionReason }), criteria = GetDowngradeCriteria(newPlan) }); } catch (Exception ex) { _logger.LogError(ex, "Erro ao validar downgrade para usuário {User}", User.Identity?.Name); return Json(new { error = "Erro interno do servidor" }); } } // 🔥 OTIMIZAÇÃO: Endpoint para manter a sessão do usuário ativa [HttpPost] [Route("KeepAlive")] public IActionResult KeepAlive() { _logger.LogInformation("KeepAlive endpoint triggered for user {User}", User.Identity?.Name ?? "Anonymous"); return Json(new { status = "session_extended" }); } private void AdjustModelState(ModelStateDictionary modelState, ManagePageViewModel model) { modelState.Remove(x => x.InstagramUrl); modelState.Remove(x => x.FacebookUrl); modelState.Remove(x => x.TwitterUrl); modelState.Remove(x => x.WhatsAppNumber); modelState.Remove(x => x.TiktokUrl); modelState.Remove(x => x.PinterestUrl); modelState.Remove(x => x.DiscordUrl); modelState.Remove(x => x.KawaiUrl); // Remover validação de 'Description' para links do tipo 'Normal' if (model.Links != null) { for (int i = 0; i < model.Links.Count; i++) { if (model.Links[i].Type == LinkType.Normal) { string key = $"Links[{i}].Description"; if (ModelState.ContainsKey(key)) { ModelState.Remove(key); ModelState.MarkFieldValid(key); } key = $"Links[{i}].Url"; if (ModelState.ContainsKey(key)) { ModelState.Remove(key); ModelState.MarkFieldValid(key); } } } } } }