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>
184 lines
7.1 KiB
C#
184 lines
7.1 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using PowderCoating.Application.Interfaces;
|
|
using PowderCoating.Core.Entities;
|
|
using PowderCoating.Core.Interfaces;
|
|
using PowderCoating.Shared.Constants;
|
|
|
|
namespace PowderCoating.Web.Controllers;
|
|
|
|
/// <summary>
|
|
/// Manages the platform changelog / release notes feed.
|
|
/// <para>
|
|
/// Has two access tiers: the public changelog (<see cref="Index"/>) is available
|
|
/// to any authenticated tenant user so they can review what has changed; the
|
|
/// management actions (Create, Edit, TogglePublish, Delete) are restricted to
|
|
/// SuperAdmins because only platform staff should author release content.
|
|
/// </para>
|
|
/// </summary>
|
|
public class ReleaseNotesController : Controller
|
|
{
|
|
private readonly IUnitOfWork _unitOfWork;
|
|
private readonly IInAppNotificationService _inApp;
|
|
|
|
public ReleaseNotesController(IUnitOfWork unitOfWork, IInAppNotificationService inApp)
|
|
{
|
|
_unitOfWork = unitOfWork;
|
|
_inApp = inApp;
|
|
}
|
|
|
|
// ── Public: Changelog ────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Renders the public changelog — shows only published release notes ordered
|
|
/// newest-first. Drafts are invisible to ordinary users so SuperAdmins can
|
|
/// prepare notes in advance without surfacing them prematurely.
|
|
/// </summary>
|
|
[Authorize]
|
|
public async Task<IActionResult> Index()
|
|
{
|
|
var notes = (await _unitOfWork.ReleaseNotes.FindAsync(r => r.IsPublished))
|
|
.OrderByDescending(r => r.ReleasedAt)
|
|
.ThenByDescending(r => r.Id)
|
|
.ToList();
|
|
return View(notes);
|
|
}
|
|
|
|
// ── SuperAdmin: Manage ───────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Returns the SuperAdmin management list of all release notes (published and draft alike), ordered newest-first.
|
|
/// </summary>
|
|
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
|
public async Task<IActionResult> Manage()
|
|
{
|
|
var notes = (await _unitOfWork.ReleaseNotes.GetAllAsync())
|
|
.OrderByDescending(r => r.ReleasedAt)
|
|
.ThenByDescending(r => r.Id)
|
|
.ToList();
|
|
return View(notes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the Create form pre-populated with today's UTC date and the "Feature" tag as sensible defaults.
|
|
/// </summary>
|
|
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
|
public IActionResult Create()
|
|
{
|
|
return View(new ReleaseNote { ReleasedAt = DateTime.UtcNow, Tag = "Feature" });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Persists a new release note. New notes start unpublished unless the form explicitly sets IsPublished = true.
|
|
/// </summary>
|
|
[HttpPost, ValidateAntiForgeryToken]
|
|
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
|
public async Task<IActionResult> Create(ReleaseNote model)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
return View(model);
|
|
|
|
model.CreatedAt = DateTime.UtcNow;
|
|
model.CreatedByUserId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
|
model.CreatedByUserName = User.Identity?.Name;
|
|
|
|
await _unitOfWork.ReleaseNotes.AddAsync(model);
|
|
await _unitOfWork.CompleteAsync();
|
|
|
|
if (model.IsPublished)
|
|
await NotifyAllTenantsAsync(model);
|
|
|
|
TempData["Success"] = $"Release note v{model.Version} created.";
|
|
return RedirectToAction(nameof(Manage));
|
|
}
|
|
|
|
/// <summary>Returns the Edit form loaded from the database by primary key.</summary>
|
|
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
|
public async Task<IActionResult> Edit(int id)
|
|
{
|
|
var note = await _unitOfWork.ReleaseNotes.GetByIdAsync(id);
|
|
if (note == null) return NotFound();
|
|
return View(note);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the edited values to the tracked entity using explicit field mapping to prevent
|
|
/// over-posting attacks and ensure audit fields are never overwritten.
|
|
/// </summary>
|
|
[HttpPost, ValidateAntiForgeryToken]
|
|
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
|
public async Task<IActionResult> Edit(int id, ReleaseNote model)
|
|
{
|
|
if (id != model.Id) return BadRequest();
|
|
if (!ModelState.IsValid)
|
|
return View(model);
|
|
|
|
var note = await _unitOfWork.ReleaseNotes.GetByIdAsync(id);
|
|
if (note == null) return NotFound();
|
|
|
|
note.Version = model.Version;
|
|
note.Title = model.Title;
|
|
note.Body = model.Body;
|
|
note.Tag = model.Tag;
|
|
note.IsPublished = model.IsPublished;
|
|
note.ReleasedAt = model.ReleasedAt;
|
|
note.UpdatedAt = DateTime.UtcNow;
|
|
|
|
await _unitOfWork.CompleteAsync();
|
|
|
|
TempData["Success"] = $"Release note v{note.Version} updated.";
|
|
return RedirectToAction(nameof(Manage));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toggles the published state of a release note. Publishing fires an in-app notification to all tenants.
|
|
/// </summary>
|
|
[HttpPost, ValidateAntiForgeryToken]
|
|
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
|
public async Task<IActionResult> TogglePublish(int id)
|
|
{
|
|
var note = await _unitOfWork.ReleaseNotes.GetByIdAsync(id);
|
|
if (note == null) return NotFound();
|
|
|
|
var wasPublished = note.IsPublished;
|
|
note.IsPublished = !note.IsPublished;
|
|
note.UpdatedAt = DateTime.UtcNow;
|
|
await _unitOfWork.CompleteAsync();
|
|
|
|
if (note.IsPublished && !wasPublished)
|
|
await NotifyAllTenantsAsync(note);
|
|
|
|
TempData["Success"] = note.IsPublished
|
|
? $"v{note.Version} published — now visible to all users."
|
|
: $"v{note.Version} unpublished — hidden from users.";
|
|
return RedirectToAction(nameof(Manage));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Permanently (hard) deletes a release note. Release notes are platform metadata and do not use soft delete.
|
|
/// </summary>
|
|
[HttpPost, ValidateAntiForgeryToken]
|
|
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
|
public async Task<IActionResult> Delete(int id)
|
|
{
|
|
var note = await _unitOfWork.ReleaseNotes.GetByIdAsync(id);
|
|
if (note == null) return NotFound();
|
|
|
|
await _unitOfWork.ReleaseNotes.DeleteAsync(note);
|
|
await _unitOfWork.CompleteAsync();
|
|
|
|
TempData["Success"] = $"Release note v{note.Version} deleted.";
|
|
return RedirectToAction(nameof(Manage));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fans out a "What's New" in-app notification to every active tenant company when a release note is published.
|
|
/// </summary>
|
|
private Task NotifyAllTenantsAsync(ReleaseNote note)
|
|
{
|
|
var title = $"What's New — {note.Title}";
|
|
var message = note.Tag != null ? $"[{note.Tag}] See the latest updates in What's New." : "See the latest updates in What's New.";
|
|
return _inApp.CreateForAllCompaniesAsync(title, message, "ReleaseNote", "/ReleaseNotes");
|
|
}
|
|
}
|