Initial commit
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
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;
|
||||
|
||||
/// <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 ApplicationDbContext _db;
|
||||
private readonly IInAppNotificationService _inApp;
|
||||
|
||||
public ReleaseNotesController(ApplicationDbContext db, IInAppNotificationService inApp)
|
||||
{
|
||||
_db = db;
|
||||
_inApp = inApp;
|
||||
}
|
||||
|
||||
// ── Public: Changelog ────────────────────────────────────────────────────
|
||||
// Visible to all authenticated users
|
||||
|
||||
/// <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 _db.ReleaseNotes
|
||||
.AsNoTracking()
|
||||
.Where(r => r.IsPublished)
|
||||
.OrderByDescending(r => r.ReleasedAt)
|
||||
.ThenByDescending(r => r.Id)
|
||||
.ToListAsync();
|
||||
|
||||
return View(notes);
|
||||
}
|
||||
|
||||
// ── SuperAdmin: Manage ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Returns the SuperAdmin management list of all release notes (published and
|
||||
/// draft alike), ordered newest-first. Unlike <see cref="Index"/> there is no
|
||||
/// <c>IsPublished</c> filter here so admins can see and edit drafts.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
||||
public async Task<IActionResult> Manage()
|
||||
{
|
||||
var notes = await _db.ReleaseNotes
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(r => r.ReleasedAt)
|
||||
.ThenByDescending(r => r.Id)
|
||||
.ToListAsync();
|
||||
|
||||
return View(notes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View(new ReleaseNote
|
||||
{
|
||||
ReleasedAt = DateTime.UtcNow,
|
||||
Tag = "Feature"
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists a new release note and captures the creating SuperAdmin's identity
|
||||
/// (<c>CreatedByUserId</c> / <c>CreatedByUserName</c>) for audit purposes.
|
||||
/// New notes start unpublished by default unless the form explicitly sets
|
||||
/// <c>IsPublished = true</c>, giving authors a chance to review before going live.
|
||||
/// </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;
|
||||
|
||||
_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));
|
||||
}
|
||||
|
||||
/// <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 _db.ReleaseNotes.FindAsync(id);
|
||||
if (note == null) return NotFound();
|
||||
return View(note);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the edited values to the tracked entity. Uses explicit field
|
||||
/// mapping (rather than <c>_db.Entry(model).State = Modified</c>) to prevent
|
||||
/// over-posting attacks and to ensure audit fields like <c>CreatedAt</c> and
|
||||
/// <c>CreatedByUserId</c> 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 _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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the published state of a release note. Publishing makes the note
|
||||
/// immediately visible to all authenticated users via <see cref="Index"/>;
|
||||
/// un-publishing hides it without permanently deleting it so it can be revised
|
||||
/// and re-published later.
|
||||
/// </summary>
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
||||
public async Task<IActionResult> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="TogglePublish"/> to hide a note without permanent removal.
|
||||
/// </summary>
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
||||
public async Task<IActionResult> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user