Phases 3 & 4: Complete data access architecture migration
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>
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Shared.Constants;
|
||||
|
||||
namespace PowderCoating.Web.Controllers;
|
||||
@@ -15,28 +14,26 @@ namespace PowderCoating.Web.Controllers;
|
||||
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
||||
public class BannedIpsController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _db;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly ILogger<BannedIpsController> _logger;
|
||||
|
||||
public BannedIpsController(
|
||||
ApplicationDbContext db,
|
||||
IUnitOfWork unitOfWork,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ILogger<BannedIpsController> logger)
|
||||
{
|
||||
_db = db;
|
||||
_unitOfWork = unitOfWork;
|
||||
_userManager = userManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>Lists all banned IPs, showing active and expired separately.</summary>
|
||||
// GET: BannedIps
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var bans = await _db.BannedIps
|
||||
var bans = (await _unitOfWork.BannedIps.GetAllAsync())
|
||||
.OrderByDescending(b => b.BannedAt)
|
||||
.ToListAsync();
|
||||
|
||||
.ToList();
|
||||
return View(bans);
|
||||
}
|
||||
|
||||
@@ -44,9 +41,7 @@ public class BannedIpsController : Controller
|
||||
/// Adds a new IP ban. Rejects obviously invalid formats but doesn't require
|
||||
/// a perfect regex — admins are trusted to enter valid IPs.
|
||||
/// </summary>
|
||||
// POST: BannedIps/Add
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Add(string ipAddress, string? reason, DateTime? expiresAt)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
@@ -57,15 +52,13 @@ public class BannedIpsController : Controller
|
||||
|
||||
ipAddress = ipAddress.Trim();
|
||||
|
||||
// Basic sanity check — must look like an IPv4 or IPv6 address
|
||||
if (!System.Net.IPAddress.TryParse(ipAddress, out _))
|
||||
{
|
||||
TempData["Error"] = $"'{ipAddress}' is not a valid IP address.";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
// Don't duplicate an active ban for the same IP
|
||||
var existing = await _db.BannedIps.FirstOrDefaultAsync(b => b.IpAddress == ipAddress && b.IsActive);
|
||||
var existing = await _unitOfWork.BannedIps.FirstOrDefaultAsync(b => b.IpAddress == ipAddress && b.IsActive);
|
||||
if (existing != null)
|
||||
{
|
||||
TempData["Error"] = $"{ipAddress} already has an active ban (added {existing.BannedAt:MMM dd, yyyy}).";
|
||||
@@ -74,7 +67,7 @@ public class BannedIpsController : Controller
|
||||
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
|
||||
_db.BannedIps.Add(new BannedIp
|
||||
await _unitOfWork.BannedIps.AddAsync(new BannedIp
|
||||
{
|
||||
IpAddress = ipAddress,
|
||||
Reason = reason?.Trim(),
|
||||
@@ -83,8 +76,7 @@ public class BannedIpsController : Controller
|
||||
ExpiresAt = expiresAt,
|
||||
IsActive = true
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
_logger.LogWarning("IP {IP} banned by {Admin}. Reason: {Reason}", ipAddress, User.Identity?.Name, reason);
|
||||
TempData["Success"] = $"{ipAddress} has been banned.";
|
||||
@@ -92,12 +84,10 @@ public class BannedIpsController : Controller
|
||||
}
|
||||
|
||||
/// <summary>Lifts a ban immediately by marking IsActive = false.</summary>
|
||||
// POST: BannedIps/Lift/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Lift(int id)
|
||||
{
|
||||
var ban = await _db.BannedIps.FindAsync(id);
|
||||
var ban = await _unitOfWork.BannedIps.GetByIdAsync(id);
|
||||
if (ban == null)
|
||||
{
|
||||
TempData["Error"] = "Ban not found.";
|
||||
@@ -105,7 +95,7 @@ public class BannedIpsController : Controller
|
||||
}
|
||||
|
||||
ban.IsActive = false;
|
||||
await _db.SaveChangesAsync();
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
_logger.LogInformation("IP ban lifted for {IP} by {Admin}", ban.IpAddress, User.Identity?.Name);
|
||||
TempData["Success"] = $"Ban on {ban.IpAddress} has been lifted.";
|
||||
@@ -113,25 +103,21 @@ public class BannedIpsController : Controller
|
||||
}
|
||||
|
||||
/// <summary>Permanently deletes a ban record.</summary>
|
||||
// POST: BannedIps/Delete/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
var ban = await _db.BannedIps.FindAsync(id);
|
||||
var ban = await _unitOfWork.BannedIps.GetByIdAsync(id);
|
||||
if (ban != null)
|
||||
{
|
||||
_db.BannedIps.Remove(ban);
|
||||
await _db.SaveChangesAsync();
|
||||
await _unitOfWork.BannedIps.DeleteAsync(ban);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
_logger.LogInformation("IP ban record deleted for {IP} by {Admin}", ban.IpAddress, User.Identity?.Name);
|
||||
TempData["Success"] = $"Ban record for {ban.IpAddress} deleted.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
/// <summary>Returns the requesting client's IP so the admin can pre-fill it quickly.</summary>
|
||||
// GET: BannedIps/MyIp
|
||||
public IActionResult MyIp()
|
||||
{
|
||||
return Json(new { ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown" });
|
||||
|
||||
Reference in New Issue
Block a user