1695 lines
67 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|