using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; using PowderCoating.Infrastructure.Data; 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 ApplicationDbContext _db; private readonly IInAppNotificationService _inApp; public ReleaseNotesController(ApplicationDbContext db, IInAppNotificationService inApp) { _db = db; _inApp = inApp; } // ── Public: Changelog ──────────────────────────────────────────────────── // Visible to all authenticated users /// /// 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 _db.ReleaseNotes .AsNoTracking() .Where(r => r.IsPublished) .OrderByDescending(r => r.ReleasedAt) .ThenByDescending(r => r.Id) .ToListAsync(); return View(notes); } // ── SuperAdmin: Manage ─────────────────────────────────────────────────── /// /// Returns the SuperAdmin management list of all release notes (published and /// draft alike), ordered newest-first. Unlike there is no /// IsPublished filter here so admins can see and edit drafts. /// [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public async Task Manage() { var notes = await _db.ReleaseNotes .AsNoTracking() .OrderByDescending(r => r.ReleasedAt) .ThenByDescending(r => r.Id) .ToListAsync(); return View(notes); } /// /// Returns the Create form pre-populated with today's UTC date and the "Feature" /// tag as sensible defaults for new entries, reducing data-entry friction. /// [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public IActionResult Create() { return View(new ReleaseNote { ReleasedAt = DateTime.UtcNow, Tag = "Feature" }); } /// /// Persists a new release note and captures the creating SuperAdmin's identity /// (CreatedByUserId / CreatedByUserName) for audit purposes. /// New notes start unpublished by default unless the form explicitly sets /// IsPublished = true, giving authors a chance to review before going live. /// [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; _db.ReleaseNotes.Add(model); await _db.SaveChangesAsync(); 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 _db.ReleaseNotes.FindAsync(id); if (note == null) return NotFound(); return View(note); } /// /// Applies the edited values to the tracked entity. Uses explicit field /// mapping (rather than _db.Entry(model).State = Modified) to prevent /// over-posting attacks and to ensure audit fields like CreatedAt and /// CreatedByUserId 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 _db.ReleaseNotes.FindAsync(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 _db.SaveChangesAsync(); TempData["Success"] = $"Release note v{note.Version} updated."; return RedirectToAction(nameof(Manage)); } /// /// Toggles the published state of a release note. Publishing makes the note /// immediately visible to all authenticated users via ; /// un-publishing hides it without permanently deleting it so it can be revised /// and re-published later. /// [HttpPost, ValidateAntiForgeryToken] [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public async Task TogglePublish(int id) { var note = await _db.ReleaseNotes.FindAsync(id); if (note == null) return NotFound(); var wasPublished = note.IsPublished; note.IsPublished = !note.IsPublished; note.UpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); 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. This is intentional — release notes /// are platform metadata, not business data, so they do not use soft delete. /// Use to hide a note without permanent removal. /// [HttpPost, ValidateAntiForgeryToken] [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public async Task Delete(int id) { var note = await _db.ReleaseNotes.FindAsync(id); if (note == null) return NotFound(); _db.ReleaseNotes.Remove(note); await _db.SaveChangesAsync(); 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 transitions to published. Notification fires exactly once per /// publish event — re-publishing after unpublishing will send a second notification, /// which is intentional (the content may have changed). /// 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"); } }