1cb7a8ca4a
Phase 3 — eliminated ApplicationDbContext from all non-exempt controllers, routing all data access through IUnitOfWork. Added IPlainRepository<T> for the four platform entities (Announcement, BannedIp, DashboardTip, ReleaseNote) that intentionally don't extend BaseEntity and therefore can't use the constrained IRepository<T>. Added permanent-exception comments to the 18 controllers that legitimately retain direct DbContext access (Identity infra, cross-tenant platform ops, bulk streaming exports). Phase 4 — added EnforceDataAccessArchitecture() to Program.cs, a startup gate that reflects over every Controller subclass and throws at boot if any non-exempt controller injects ApplicationDbContext. The app cannot start with a violation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
144 lines
5.4 KiB
C#
144 lines
5.4 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using PowderCoating.Application.DTOs.Common;
|
|
using PowderCoating.Application.DTOs.Notification;
|
|
using PowderCoating.Core.Enums;
|
|
using PowderCoating.Core.Interfaces;
|
|
using PowderCoating.Shared.Constants;
|
|
|
|
namespace PowderCoating.Web.Controllers;
|
|
|
|
/// <summary>
|
|
/// Company-scoped notification log viewer accessible to users with the
|
|
/// <c>CanManageJobs</c> policy (Managers and above within a company).
|
|
/// The platform-wide equivalent for SuperAdmins lives in
|
|
/// <see cref="PlatformNotificationsController"/>.
|
|
/// </summary>
|
|
[Authorize(Policy = AppConstants.Policies.CanManageJobs)]
|
|
public class NotificationLogsController : Controller
|
|
{
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly ILogger<NotificationLogsController> _logger;
|
|
|
|
public NotificationLogsController(IUnitOfWork unitOfWork, ILogger<NotificationLogsController> logger)
|
|
{
|
|
_unitOfWork = unitOfWork;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Displays a paginated, filterable list of notification log entries for the current company.
|
|
/// Supports filtering by free-text search, channel, delivery status, notification type, and
|
|
/// an optional job context. Page size is validated against an allowlist (10/25/50/100).
|
|
/// </summary>
|
|
// GET: /NotificationLogs
|
|
public async Task<IActionResult> Index(
|
|
string? searchTerm,
|
|
string? channelFilter,
|
|
string? statusFilter,
|
|
string? typeFilter,
|
|
int? jobId,
|
|
string? sortColumn,
|
|
string sortDirection = "desc",
|
|
int pageNumber = 1,
|
|
int pageSize = 25)
|
|
{
|
|
pageNumber = Math.Max(1, pageNumber);
|
|
pageSize = pageSize is 10 or 25 or 50 or 100 ? pageSize : 25;
|
|
|
|
NotificationChannel? channel = Enum.TryParse<NotificationChannel>(channelFilter, out var ch) ? ch : null;
|
|
NotificationStatus? status = Enum.TryParse<NotificationStatus>(statusFilter, out var st) ? st : null;
|
|
NotificationType? type = Enum.TryParse<NotificationType>(typeFilter, out var ty) ? ty : null;
|
|
|
|
var (logs, totalCount) = await _unitOfWork.NotificationLogs.GetPagedFilteredAsync(
|
|
pageNumber, pageSize, searchTerm, channel, status, type, jobId,
|
|
sortColumn ?? "SentAt", sortDirection);
|
|
|
|
var items = logs.Select(n => new NotificationLogDto
|
|
{
|
|
Id = n.Id,
|
|
Channel = n.Channel,
|
|
NotificationType = n.NotificationType,
|
|
Status = n.Status,
|
|
RecipientName = n.RecipientName,
|
|
Recipient = n.Recipient,
|
|
Subject = n.Subject,
|
|
Message = n.Message,
|
|
ErrorMessage = n.ErrorMessage,
|
|
SentAt = n.SentAt,
|
|
CustomerId = n.CustomerId,
|
|
JobId = n.JobId,
|
|
QuoteId = n.QuoteId,
|
|
JobNumber = n.Job?.JobNumber,
|
|
QuoteNumber = n.Quote?.QuoteNumber,
|
|
CustomerName = n.Customer != null
|
|
? (n.Customer.CompanyName ?? $"{n.Customer.ContactFirstName} {n.Customer.ContactLastName}".Trim())
|
|
: null
|
|
}).ToList();
|
|
|
|
var pagedResult = new PagedResult<NotificationLogDto>
|
|
{
|
|
Items = items,
|
|
TotalCount = totalCount,
|
|
PageNumber = pageNumber,
|
|
PageSize = pageSize
|
|
};
|
|
|
|
ViewBag.SearchTerm = searchTerm;
|
|
ViewBag.ChannelFilter = channelFilter;
|
|
ViewBag.StatusFilter = statusFilter;
|
|
ViewBag.TypeFilter = typeFilter;
|
|
ViewBag.JobId = jobId;
|
|
ViewBag.SortColumn = sortColumn ?? "SentAt";
|
|
ViewBag.SortDirection = sortDirection;
|
|
|
|
return View(pagedResult);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Displays the full details of a single notification log entry including the complete message
|
|
/// body and any error message, which are omitted from the list view. Loads related Customer,
|
|
/// Job, and Quote navigation properties to resolve display names.
|
|
/// </summary>
|
|
// GET: /NotificationLogs/Details/5
|
|
public async Task<IActionResult> Details(int id)
|
|
{
|
|
try
|
|
{
|
|
var log = await _unitOfWork.NotificationLogs.GetByIdAsync(
|
|
id, false, n => n.Customer, n => n.Job, n => n.Quote);
|
|
|
|
if (log == null) return NotFound();
|
|
|
|
var dto = new NotificationLogDto
|
|
{
|
|
Id = log.Id,
|
|
Channel = log.Channel,
|
|
NotificationType = log.NotificationType,
|
|
Status = log.Status,
|
|
RecipientName = log.RecipientName,
|
|
Recipient = log.Recipient,
|
|
Subject = log.Subject,
|
|
Message = log.Message,
|
|
ErrorMessage = log.ErrorMessage,
|
|
SentAt = log.SentAt,
|
|
CustomerId = log.CustomerId,
|
|
JobId = log.JobId,
|
|
QuoteId = log.QuoteId,
|
|
JobNumber = log.Job?.JobNumber,
|
|
QuoteNumber = log.Quote?.QuoteNumber,
|
|
CustomerName = log.Customer != null
|
|
? (log.Customer.CompanyName ?? $"{log.Customer.ContactFirstName} {log.Customer.ContactLastName}".Trim())
|
|
: null
|
|
};
|
|
|
|
return View(dto);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error loading notification log {Id}", id);
|
|
return RedirectToAction(nameof(Index));
|
|
}
|
|
}
|
|
}
|