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; /// /// Manages the platform changelog / release notes feed. /// /// Has two access tiers: the public changelog () 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. /// /// public class ReleaseNotesController : Controller { private readonly IUnitOfWork _unitOfWork; private readonly IInAppNotificationService _inApp; public ReleaseNotesController(IUnitOfWork unitOfWork, IInAppNotificationService inApp) { _unitOfWork = unitOfWork; _inApp = inApp; } // ── Public: Changelog ──────────────────────────────────────────────────── /// /// 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. /// [Authorize] public async Task Index() { var notes = (await _unitOfWork.ReleaseNotes.FindAsync(r => r.IsPublished)) .OrderByDescending(r => r.ReleasedAt) .ThenByDescending(r => r.Id) .ToList(); return View(notes); } // ── SuperAdmin: Manage ─────────────────────────────────────────────────── /// /// Returns the SuperAdmin management list of all release notes (published and draft alike), ordered newest-first. /// [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public async Task Manage() { var notes = (await _unitOfWork.ReleaseNotes.GetAllAsync()) .OrderByDescending(r => r.ReleasedAt) .ThenByDescending(r => r.Id) .ToList(); return View(notes); } /// /// Returns the Create form pre-populated with today's UTC date and the "Feature" tag as sensible defaults. /// [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public IActionResult Create() { return View(new ReleaseNote { ReleasedAt = DateTime.UtcNow, Tag = "Feature" }); } /// /// Persists a new release note. New notes start unpublished unless the form explicitly sets IsPublished = true. /// [HttpPost, ValidateAntiForgeryToken] [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public async Task 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)); } /// Returns the Edit form loaded from the database by primary key. [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public async Task Edit(int id) { var note = await _unitOfWork.ReleaseNotes.GetByIdAsync(id); if (note == null) return NotFound(); return View(note); } /// /// Applies the edited values to the tracked entity using explicit field mapping to prevent /// over-posting attacks and ensure audit fields are never overwritten. /// [HttpPost, ValidateAntiForgeryToken] [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public async Task 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)); } /// /// Toggles the published state of a release note. Publishing fires an in-app notification to all tenants. /// [HttpPost, ValidateAntiForgeryToken] [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public async Task 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)); } /// /// Permanently (hard) deletes a release note. Release notes are platform metadata and do not use soft delete. /// [HttpPost, ValidateAntiForgeryToken] [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public async Task 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)); } /// /// Fans out a "What's New" in-app notification to every active tenant company when a release note is published. /// 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"); } }