BCards/src/BCards.Web/Services/ModerationService.cs
Ricardo Carneiro 3d2ce1f8cf
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 4s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 15m22s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m54s
BCards Deployment Pipeline / Deploy to Test (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
fix: Increase session timeout to 7 days and set SameSite=None for Cloudflare compatibility
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-18 12:32:42 -03:00

287 lines
11 KiB
C#

using BCards.Web.Models;
using BCards.Web.Repositories;
using BCards.Web.ViewModels;
using MongoDB.Driver;
namespace BCards.Web.Services;
public class ModerationService : IModerationService
{
private readonly IUserPageRepository _userPageRepository;
private readonly IUserRepository _userRepository;
private readonly ILivePageService _livePageService;
private readonly ILogger<ModerationService> _logger;
public ModerationService(
IUserPageRepository userPageRepository,
IUserRepository userRepository,
ILivePageService livePageService,
ILogger<ModerationService> logger)
{
_userPageRepository = userPageRepository;
_userRepository = userRepository;
_livePageService = livePageService;
_logger = logger;
}
public async Task<string> GeneratePreviewTokenAsync(string pageId)
{
var token = Guid.NewGuid().ToString("N")[..16];
var expiry = DateTime.UtcNow.AddMinutes(5); // Token válido por 5 minutos
// LOG ANTES da busca
_logger.LogInformation("Generating token for page {PageId} - searching page in DB", pageId);
var page = await _userPageRepository.GetByIdAsync(pageId);
// LOG TOKEN ANTERIOR
_logger.LogInformation("Page {PageId} - Old token: {OldToken}, New token: {NewToken}",
pageId, page.PreviewToken, token);
page.PreviewToken = token;
page.PreviewTokenExpiry = expiry;
page.PreviewViewCount = 0;
// LOG ANTES do update
_logger.LogInformation("Updating page {PageId} with new token {Token}", pageId, token);
await _userPageRepository.UpdateAsync(page);
// LOG APÓS update
_logger.LogInformation("Successfully updated page {PageId} with token {Token}", pageId, token);
return token;
}
public async Task<bool> ValidatePreviewTokenAsync(string pageId, string token)
{
var page = await _userPageRepository.GetByIdAsync(pageId);
if (page == null)
return false;
var isValid = page.PreviewToken == token &&
page.PreviewTokenExpiry > DateTime.UtcNow &&
page.PreviewViewCount < 50;
return isValid;
}
public async Task<List<UserPage>> GetPendingModerationAsync(int skip = 0, int take = 20, string? filter = null)
{
var filterBuilder = Builders<UserPage>.Filter;
var baseFilter = filterBuilder.Eq(p => p.Status, PageStatus.PendingModeration);
FilterDefinition<UserPage> finalFilter = baseFilter;
// Aplicar filtro de moderação especial
if (!string.IsNullOrEmpty(filter))
{
switch (filter.ToLower())
{
case "special":
// Filtrar apenas páginas com moderação especial (Premium + Afiliados)
var specialFilter = filterBuilder.Eq("planLimitations.specialModeration", true);
finalFilter = filterBuilder.And(baseFilter, specialFilter);
break;
case "normal":
// Filtrar apenas páginas sem moderação especial
var normalFilter = filterBuilder.Or(
filterBuilder.Eq("planLimitations.specialModeration", false),
filterBuilder.Exists("planLimitations.specialModeration", false)
);
finalFilter = filterBuilder.And(baseFilter, normalFilter);
break;
default:
// "all" ou qualquer outro valor: sem filtro adicional
break;
}
}
// Ordenar por moderação especial primeiro (SLA reduzido), depois por data
var sort = Builders<UserPage>.Sort
.Descending("planLimitations.specialModeration")
.Ascending(p => p.CreatedAt);
var pages = await _userPageRepository.GetManyAsync(finalFilter, sort, skip, take);
return pages.ToList();
}
public async Task<UserPage?> GetPageForModerationAsync(string pageId)
{
var page = await _userPageRepository.GetByIdAsync(pageId);
if (page?.Status != PageStatus.PendingModeration)
return null;
return page;
}
public async Task DeleteForModerationAsync(string pageId)
{
var page = await _userPageRepository.GetByIdAsync(pageId);
await _userPageRepository.DeleteAsync(pageId);
}
public async Task ApprovePageAsync(string pageId, string moderatorId, string? notes = null)
{
var page = await _userPageRepository.GetByIdAsync(pageId);
if (page == null)
throw new ArgumentException("Page not found");
var moderationEntry = new ModerationHistory
{
Attempt = page.ModerationAttempts + 1,
Status = "approved",
ModeratorId = moderatorId,
Date = DateTime.UtcNow,
Reason = notes
};
page.ModerationHistory.Add(moderationEntry);
var update = Builders<UserPage>.Update
.Set(p => p.Status, PageStatus.Active)
.Set(p => p.ApprovedAt, DateTime.UtcNow)
.Set(p => p.ModerationAttempts, page.ModerationAttempts + 1)
.Set(p => p.ModerationHistory, page.ModerationHistory)
.Set(p => p.PublishedAt, DateTime.UtcNow)
.Unset(p => p.PreviewToken)
.Unset(p => p.PreviewTokenExpiry);
await _userPageRepository.UpdateAsync(pageId, update);
_logger.LogInformation("Page {PageId} approved by moderator {ModeratorId}", pageId, moderatorId);
// 🔥 NOVA FUNCIONALIDADE: Sincronizar para LivePage
try
{
await _livePageService.SyncFromUserPageAsync(pageId);
_logger.LogInformation("Page {PageId} synced to LivePages successfully", pageId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to sync page {PageId} to LivePages. Approval completed but sync failed.", pageId);
// Não falhar a aprovação se sync falhar
}
}
public async Task RejectPageAsync(string pageId, string moderatorId, string reason, List<string> issues)
{
var page = await _userPageRepository.GetByIdAsync(pageId);
if (page == null)
throw new ArgumentException("Page not found");
var moderationEntry = new ModerationHistory
{
Attempt = page.ModerationAttempts + 1,
Status = "rejected",
ModeratorId = moderatorId,
Date = DateTime.UtcNow,
Reason = reason,
Issues = issues
};
page.ModerationHistory.Add(moderationEntry);
var newStatus = page.ModerationAttempts >= 2 ? PageStatus.Rejected : PageStatus.Inactive;
var userScoreDeduction = Math.Min(20, page.UserScore / 5);
var update = Builders<UserPage>.Update
.Set(p => p.Status, newStatus)
.Set(p => p.ModerationAttempts, page.ModerationAttempts + 1)
.Set(p => p.ModerationHistory, page.ModerationHistory)
.Set(p => p.UserScore, Math.Max(0, page.UserScore - userScoreDeduction))
.Unset(p => p.PreviewToken)
.Unset(p => p.PreviewTokenExpiry);
await _userPageRepository.UpdateAsync(pageId, update);
_logger.LogInformation("Page {PageId} rejected by moderator {ModeratorId}. Reason: {Reason}",
pageId, moderatorId, reason);
// Remover da LivePages se existir
try
{
await _livePageService.DeleteByOriginalPageIdAsync(pageId);
_logger.LogInformation("LivePage removed for rejected UserPage {PageId}", pageId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to remove LivePage for UserPage {PageId}", pageId);
}
}
public async Task<bool> CanUserCreatePageAsync(string userId)
{
var user = await _userRepository.GetByIdAsync(userId);
if (user == null)
return false;
//var rejectedPages = userPages.Count(p => p.Status == PageStatus.Rejected);
var filter = Builders<UserPage>.Filter.Eq(p => p.UserId, userId);
filter &= Builders<UserPage>.Filter.Eq(p => p.Status, PageStatus.Rejected);
var rejectedPages = await _userPageRepository.CountAsync(filter);
// Usuários com mais de 2 páginas rejeitadas não podem criar novas
return rejectedPages < 2;
}
public async Task<bool> IncrementPreviewViewAsync(string pageId)
{
var page = await _userPageRepository.GetByIdAsync(pageId);
if (page == null || page.PreviewViewCount >= 50)
return false;
var update = Builders<UserPage>.Update
.Inc(p => p.PreviewViewCount, 1);
await _userPageRepository.UpdateAsync(pageId, update);
return true;
}
public async Task<Dictionary<string, int>> GetModerationStatsAsync()
{
var stats = new Dictionary<string, int>();
var pendingCount = await _userPageRepository.CountAsync(
Builders<UserPage>.Filter.Eq(p => p.Status, PageStatus.PendingModeration));
var approvedToday = await _userPageRepository.CountAsync(
Builders<UserPage>.Filter.And(
Builders<UserPage>.Filter.Eq(p => p.Status, PageStatus.Active),
Builders<UserPage>.Filter.Gte(p => p.ApprovedAt, DateTime.UtcNow.Date)));
var rejectedToday = await _userPageRepository.CountAsync(
Builders<UserPage>.Filter.And(
Builders<UserPage>.Filter.Eq(p => p.Status, PageStatus.Rejected),
Builders<UserPage>.Filter.Gte(p => p.UpdatedAt, DateTime.UtcNow.Date)));
stats["pending"] = (int)pendingCount;
stats["approvedToday"] = (int)approvedToday;
stats["rejectedToday"] = (int)rejectedToday;
return stats;
}
public async Task<List<UserPage>> GetModerationHistoryAsync(int skip = 0, int take = 20)
{
var filter = Builders<UserPage>.Filter.Or(
Builders<UserPage>.Filter.Eq(p => p.Status, PageStatus.Active),
Builders<UserPage>.Filter.Eq(p => p.Status, PageStatus.Rejected));
var sort = Builders<UserPage>.Sort.Descending(p => p.UpdatedAt);
var pages = await _userPageRepository.GetManyAsync(filter, sort, skip, take);
return pages.ToList();
}
public async Task<UserPage?> GetPageByPreviewTokenAsync(string token)
{
var filter = Builders<UserPage>.Filter.And(
Builders<UserPage>.Filter.Eq(p => p.PreviewToken, token),
Builders<UserPage>.Filter.Gt(p => p.PreviewTokenExpiry, DateTime.UtcNow));
var pages = await _userPageRepository.GetManyAsync(filter);
return pages.FirstOrDefault();
}
}