From 8835137e9b258a059f417dc60de53705e0b3231a Mon Sep 17 00:00:00 2001 From: Gunnar Johansson Date: Wed, 12 Aug 2026 16:40:02 +0000 Subject: [PATCH] Refactoring --- .../Controllers/ShipmentController.cs | 25 ++--- .../Extensions/RateLimiterExtensions.cs | 101 ++++++++---------- docker-compose.yml | 2 +- frontend/src/app/draw/draw.component.html | 2 +- frontend/src/app/draw/draw.component.ts | 11 +- frontend/src/app/shipment/shipment.service.ts | 1 + frontend/src/index.html | 2 +- 7 files changed, 63 insertions(+), 81 deletions(-) diff --git a/backend/src/PostNordLotteriet.WebAPI/Controllers/ShipmentController.cs b/backend/src/PostNordLotteriet.WebAPI/Controllers/ShipmentController.cs index 7431ca6..a887716 100644 --- a/backend/src/PostNordLotteriet.WebAPI/Controllers/ShipmentController.cs +++ b/backend/src/PostNordLotteriet.WebAPI/Controllers/ShipmentController.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -25,24 +24,22 @@ namespace PostNordLotteriet.WebAPI.Controllers [HttpGet("trackandtrace")] public IActionResult Get() { - DateTime currentDate = DateTime.Now; - - if (_workDayService.IsNonWorkDay(currentDate)) + if (_workDayService.IsNonWorkDay(DateTime.Now)) { - DateTime nextWorkDay = _workDayService.GetNextWorkDay(currentDate); - - Response.Headers.RetryAfter = nextWorkDay - .ToUniversalTime() - .ToString("R", CultureInfo.InvariantCulture); - return StatusCode( StatusCodes.Status503ServiceUnavailable, - new { message = "Ingen postutdelning på arbetsfria dagar." }); + new { message = "Ingen postutdelning när det är helg." }); } - return Random.Shared.NextDouble() < 0.4 - ? Ok(new { message = "Grattis! Försändelsen finns snart i en brevlåda nära dig." }) - : BadRequest(new { message = "Tyvärr, ingen post idag." }); + bool won = Random.Shared.NextDouble() < 0.4; + + return Ok(new + { + won, + message = won + ? "Grattis! Försändelsen finns snart i en brevlåda nära dig." + : "Tyvärr, ingen post idag." + }); } } } \ No newline at end of file diff --git a/backend/src/PostNordLotteriet.WebAPI/Extensions/RateLimiterExtensions.cs b/backend/src/PostNordLotteriet.WebAPI/Extensions/RateLimiterExtensions.cs index 80e905a..d22994b 100644 --- a/backend/src/PostNordLotteriet.WebAPI/Extensions/RateLimiterExtensions.cs +++ b/backend/src/PostNordLotteriet.WebAPI/Extensions/RateLimiterExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using System.Text.Json; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Builder; @@ -12,77 +11,63 @@ namespace PostNordLotteriet.WebAPI.Extensions public static class RateLimiterExtensions { // Adds a rate limiter with a custom error message in JSON format. - public static IServiceCollection AddPostNordLotterietRateLimiter(this IServiceCollection services) + public static IServiceCollection AddPostNordLotterietRateLimiter( + this IServiceCollection services) { services.AddRateLimiter(options => { - options.AddPolicy(policyName: "fixed", httpContext => + options.AddPolicy("fixed", httpContext => { - // Disable rate limiter if today is a non-work day. - if (httpContext.RequestServices.GetRequiredService().IsNonWorkDay(DateTime.Now)) - { - return RateLimitPartition.GetNoLimiter("NoLimiter"); - } - else - { - return RateLimitPartition.GetFixedWindowLimiter(partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() - ?? "Unknown", - factory: _ => new FixedWindowRateLimiterOptions - { - Window = TimeSpan.FromDays(1), - PermitLimit = 1 - }); - } - }); - - options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; - - options.OnRejected = async (context, cancellationToken) => - { - HttpContext httpContext = context.HttpContext; - IWorkDayService workDayService = httpContext.RequestServices.GetRequiredService(); - DateTimeOffset now = DateTimeOffset.Now; + DateTime now = DateTime.Now; - DateTime nextWorkDay = workDayService.GetNextWorkDay(now.DateTime); - - // The next workday starts at midnight in the server's local time zone. - DateTimeOffset nextWorkDayStart = new( - nextWorkDay, - now.Offset); - - DateTimeOffset nextAttempt = nextWorkDayStart; - - // Get the rate limiter's actual remaining lockout duration. - if (context.Lease.TryGetMetadata( - MetadataName.RetryAfter, - out TimeSpan retryAfter)) + // Disable the rate limiter on non-work days. + if (workDayService.IsNonWorkDay(now)) { - DateTimeOffset limiterReset = now.Add(retryAfter); - - if (limiterReset > nextAttempt) - { - nextAttempt = limiterReset; - } + return RateLimitPartition.GetNoLimiter("NoLimiter"); } - string formattedNextAttempt = nextAttempt.ToString( - "dddd d MMMM 'klockan' HH:mm", - CultureInfo.GetCultureInfo("sv-SE")); + string ipAddress = + httpContext.Connection.RemoteIpAddress?.ToString() + ?? "Unknown"; - httpContext.Response.ContentType = "application/json"; + // Include today's date in the partition key so a new + // partition is created automatically at midnight. + string partitionKey = $"{ipAddress}:{now:yyyy-MM-dd}"; - httpContext.Response.Headers.RetryAfter = nextAttempt - .ToUniversalTime() - .ToString("R", CultureInfo.InvariantCulture); - - await httpContext.Response.WriteAsync( - JsonSerializer.Serialize(new + return RateLimitPartition.GetFixedWindowLimiter( + partitionKey, + _ => new FixedWindowRateLimiterOptions { - message = $"Posten delas ut en gång per arbetsdag. Gör ett nytt försök {formattedNextAttempt}." - }), + PermitLimit = 1, + Window = TimeSpan.FromDays(1) + }); + }); + + options.RejectionStatusCode = + StatusCodes.Status429TooManyRequests; + + options.OnRejected = async (context, cancellationToken) => + { + DateTime now = DateTime.Now; + DateTime nextMidnight = now.Date.AddDays(1); + + int retryAfterSeconds = (int)Math.Ceiling( + (nextMidnight - now).TotalSeconds); + + context.HttpContext.Response.ContentType = "application/json"; + + context.HttpContext.Response.Headers.RetryAfter = retryAfterSeconds.ToString(); + + string response = JsonSerializer.Serialize(new + { + message = "Posten delas ut en gång om dagen. Gör ett nytt försök imorgon." + }); + + await context.HttpContext.Response.WriteAsync( + response, cancellationToken); }; }); diff --git a/docker-compose.yml b/docker-compose.yml index c2a427b..01bdbe0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: build: context: ./frontend/.devcontainer dockerfile: Dockerfile - image: devcontainers/angular:21 + image: devcontainers/typescript-node:24-bookworm networks: - net volumes: diff --git a/frontend/src/app/draw/draw.component.html b/frontend/src/app/draw/draw.component.html index 2bba3e0..024fa11 100644 --- a/frontend/src/app/draw/draw.component.html +++ b/frontend/src/app/draw/draw.component.html @@ -9,7 +9,7 @@ @if (result(); as result) {
{ this.result.set({ - appearance: 'success', + appearance: response.won ? 'success' : 'warning', message: response.message }); - this.confettiService.celebrate(); + if (response.won) { + this.confettiService.celebrate(); + } }, error: (error: HttpErrorResponse) => { this.handleError(error); @@ -61,7 +61,6 @@ export class DrawComponent { private handleError(error: HttpErrorResponse): void { switch (error.status) { - case 400: case 429: case 503: this.result.set({ diff --git a/frontend/src/app/shipment/shipment.service.ts b/frontend/src/app/shipment/shipment.service.ts index 9b38924..14b5e62 100644 --- a/frontend/src/app/shipment/shipment.service.ts +++ b/frontend/src/app/shipment/shipment.service.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs'; export interface ShipmentResponse { message: string; + won: boolean } @Injectable({ diff --git a/frontend/src/index.html b/frontend/src/index.html index 0d3e1ec..a3548b4 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -7,7 +7,7 @@ - +