BCards/src/BCards.Web/Controllers/AdminController.cs

1695 lines
67 KiB
C#

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<AdminController> _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<AdminController> 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<IActionResult> 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<PlanType>(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<string, dynamic>();
// 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<PageStatus>(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<IActionResult> 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<PlanType>(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<ManageDocumentViewModel>(),
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<IActionResult> 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<PlanType>(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<IActionResult> 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<LinkItem>()
};
// Add social media links
var socialLinks = new List<LinkItem>();
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<ManagePageViewModel> MapToManageViewModel(UserPage page, List<Category> categories, List<PageTheme> 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<ManageLinkViewModel>(),
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<ManageDocumentViewModel>(),
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<UserPage> 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<LinkItem>()
};
// 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<LinkItem>();
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<string>(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<PlanType>(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<PageDocument> Documents, List<string> RemovedFileIds, List<string> NewlyUploadedFileIds)> BuildDocumentsAsync(ManagePageViewModel model, PlanLimitations planLimitations)
{
var documents = new List<PageDocument>();
var removedFileIds = new List<string>();
var newFileIds = new List<string>();
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<string> 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<PageDocument> 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<LinkItem>();
// 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<PlanType>(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<LinkItem>();
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<PageDocument>();
}
[HttpPost]
[Route("SubmitForModeration/{id}")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<string>();
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<IActionResult> 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<PlanType>(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<ManagePageViewModel>(x => x.InstagramUrl);
modelState.Remove<ManagePageViewModel>(x => x.FacebookUrl);
modelState.Remove<ManagePageViewModel>(x => x.TwitterUrl);
modelState.Remove<ManagePageViewModel>(x => x.WhatsAppNumber);
modelState.Remove<ManagePageViewModel>(x => x.TiktokUrl);
modelState.Remove<ManagePageViewModel>(x => x.PinterestUrl);
modelState.Remove<ManagePageViewModel>(x => x.DiscordUrl);
modelState.Remove<ManagePageViewModel>(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);
}
}
}
}
}
}