From 64ca9e0bdfb53782be989777677062b5c09ae023 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Mon, 13 Jul 2026 13:22:53 -0300 Subject: [PATCH] feat(work-orders): add legacy deprecation middleware and Blazor WO sunset guard --- .../Middleware/LegacyDeprecationMiddleware.cs | 57 +++++++++++++++++++ Api.SeaHavenIndustries/Program.cs | 5 +- .../Data/Services/WorkorderService.cs | 12 +++- .../Helper/BlazorWorkOrderSunsetGuard.cs | 26 +++++++++ .../Options/BlazorWorkOrderSunsetOptions.cs | 11 ++++ SeaHavenIndustries/Program.cs | 3 + 6 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 Api.SeaHavenIndustries/Middleware/LegacyDeprecationMiddleware.cs create mode 100644 SeaHavenIndustries/Helper/BlazorWorkOrderSunsetGuard.cs create mode 100644 SeaHavenIndustries/Options/BlazorWorkOrderSunsetOptions.cs diff --git a/Api.SeaHavenIndustries/Middleware/LegacyDeprecationMiddleware.cs b/Api.SeaHavenIndustries/Middleware/LegacyDeprecationMiddleware.cs new file mode 100644 index 0000000..0e2ccb6 --- /dev/null +++ b/Api.SeaHavenIndustries/Middleware/LegacyDeprecationMiddleware.cs @@ -0,0 +1,57 @@ +using Api.SeaHavenIndustries.Options; +using Microsoft.Extensions.Options; + +namespace Api.SeaHavenIndustries.Middleware +{ + /// Adds Sunset/Deprecation headers to legacy WorkOrder REST routes. + public class LegacyDeprecationMiddleware + { + private static readonly HashSet LegacyPathSegments = new(StringComparer.OrdinalIgnoreCase) + { + "GetWorkOrderList", + "AddWorkorder", + "EditWorkorder", + "ChangeStatus", + "ChangeAssignment", + "DeleteWorkorder", + "GetWorkorderById" + }; + + private readonly RequestDelegate _next; + private readonly LegacyEndpointOptions _options; + + public LegacyDeprecationMiddleware(RequestDelegate next, IOptions options) + { + _next = next; + _options = options.Value; + } + + public async Task InvokeAsync(HttpContext context) + { + if (_options.DeprecationEnabled) + { + var path = context.Request.Path.Value ?? ""; + var isLegacyWo = (path.Contains("/WorkOrder", StringComparison.OrdinalIgnoreCase) + || path.Contains("/workorders", StringComparison.OrdinalIgnoreCase)) + && LegacyPathSegments.Any(seg => path.Contains(seg, StringComparison.OrdinalIgnoreCase)); + + if (isLegacyWo) + { + context.Response.OnStarting(() => + { + context.Response.Headers["Deprecation"] = "true"; + if (!string.IsNullOrWhiteSpace(_options.SunsetDate) + && DateTimeOffset.TryParse(_options.SunsetDate, out var sunset)) + { + context.Response.Headers["Sunset"] = sunset.UtcDateTime.ToString("r"); + } + context.Response.Headers["Link"] = "; rel=\"successor-version\""; + return Task.CompletedTask; + }); + } + } + + await _next(context); + } + } +} diff --git a/Api.SeaHavenIndustries/Program.cs b/Api.SeaHavenIndustries/Program.cs index e897f24..c85271b 100644 --- a/Api.SeaHavenIndustries/Program.cs +++ b/Api.SeaHavenIndustries/Program.cs @@ -1,5 +1,6 @@ -using Api.SeaHavenIndustries.Helper; +using Api.SeaHavenIndustries.Helper; using Api.SeaHavenIndustries.HostedServices; +using Api.SeaHavenIndustries.Middleware; using Api.SeaHavenIndustries.Options; using Data.SeaHavenIndustries; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -151,6 +152,7 @@ if (!app.Environment.IsDevelopment()) app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCors(); +app.UseMiddleware(); app.UseAuthentication(); app.UseAuthorization(); @@ -213,4 +215,3 @@ app.MapControllers(); //} app.Run(); - diff --git a/SeaHavenIndustries/Data/Services/WorkorderService.cs b/SeaHavenIndustries/Data/Services/WorkorderService.cs index f616377..2e18cf2 100644 --- a/SeaHavenIndustries/Data/Services/WorkorderService.cs +++ b/SeaHavenIndustries/Data/Services/WorkorderService.cs @@ -22,14 +22,22 @@ namespace SeaHavenIndustries.Data.Services private UserManager _userManager; private UploadFileHp _uploadFileHelper; private readonly SendMessage _sendemail; + private readonly BlazorWorkOrderSunsetGuard _sunsetGuard; - public WorkorderService(SendMessage sendemail, ApplicationDbContext context, IHttpContextAccessor httpContextAccessor, UserManager userManager, UploadFileHp uploadFileHelper) + public WorkorderService( + SendMessage sendemail, + ApplicationDbContext context, + IHttpContextAccessor httpContextAccessor, + UserManager userManager, + UploadFileHp uploadFileHelper, + BlazorWorkOrderSunsetGuard sunsetGuard) { _context = context; _httpContextAccessor = httpContextAccessor; _userManager = userManager; _uploadFileHelper = uploadFileHelper; _sendemail = sendemail; + _sunsetGuard = sunsetGuard; } public async Task> Getworkorder() { @@ -178,6 +186,7 @@ namespace SeaHavenIndustries.Data.Services } public async Task Addworkorder(WorkOrder model, List files) { + _sunsetGuard.ThrowIfMutationsBlocked(); try { _context.ChangeTracker.Clear(); @@ -425,6 +434,7 @@ namespace SeaHavenIndustries.Data.Services } public async Task Deleteworkorder(int id) { + _sunsetGuard.ThrowIfMutationsBlocked(); _context.ChangeTracker.Clear(); try diff --git a/SeaHavenIndustries/Helper/BlazorWorkOrderSunsetGuard.cs b/SeaHavenIndustries/Helper/BlazorWorkOrderSunsetGuard.cs new file mode 100644 index 0000000..5c7bbf8 --- /dev/null +++ b/SeaHavenIndustries/Helper/BlazorWorkOrderSunsetGuard.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Options; +using SeaHavenIndustries.Options; + +namespace SeaHavenIndustries.Helper +{ + public class BlazorWorkOrderSunsetGuard + { + private readonly BlazorWorkOrderSunsetOptions _options; + + public BlazorWorkOrderSunsetGuard(IOptions options) + { + _options = options.Value; + } + + public bool IsSunsetEnabled => _options.Enabled; + + public string ShocBaseUrl => _options.ShocBaseUrl.TrimEnd('/'); + + public void ThrowIfMutationsBlocked() + { + if (_options.Enabled) + throw new InvalidOperationException( + "Work Orders module has moved to SHOC. Editing via Blazor is disabled."); + } + } +} diff --git a/SeaHavenIndustries/Options/BlazorWorkOrderSunsetOptions.cs b/SeaHavenIndustries/Options/BlazorWorkOrderSunsetOptions.cs new file mode 100644 index 0000000..e368d9f --- /dev/null +++ b/SeaHavenIndustries/Options/BlazorWorkOrderSunsetOptions.cs @@ -0,0 +1,11 @@ +namespace SeaHavenIndustries.Options +{ + public class BlazorWorkOrderSunsetOptions + { + public const string SectionName = "BlazorWorkOrderSunset"; + + public bool Enabled { get; set; } + + public string ShocBaseUrl { get; set; } = "https://shoc.seahaven.com"; + } +} diff --git a/SeaHavenIndustries/Program.cs b/SeaHavenIndustries/Program.cs index 5450abf..48b76c3 100644 --- a/SeaHavenIndustries/Program.cs +++ b/SeaHavenIndustries/Program.cs @@ -77,6 +77,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.Configure( + builder.Configuration.GetSection(SeaHavenIndustries.Options.BlazorWorkOrderSunsetOptions.SectionName)); +builder.Services.AddSingleton(); builder.Services.AddSession(options => {