Refactoring

This commit is contained in:
oaklake89 committed 2026-08-12 16:40:02 +00:00
1 parent e13a710fa6
commit 8835137e9b
7 files changed
+63 -81

No files matched your search

@@ -1,5 +1,4 @@
using System; using System;
using System.Globalization;
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -25,24 +24,22 @@ namespace PostNordLotteriet.WebAPI.Controllers
[HttpGet("trackandtrace")] [HttpGet("trackandtrace")]
public IActionResult Get() public IActionResult Get()
{ {
DateTime currentDate = DateTime.Now; if (_workDayService.IsNonWorkDay(DateTime.Now))
if (_workDayService.IsNonWorkDay(currentDate))
{ {
DateTime nextWorkDay = _workDayService.GetNextWorkDay(currentDate);
Response.Headers.RetryAfter = nextWorkDay
.ToUniversalTime()
.ToString("R", CultureInfo.InvariantCulture);
return StatusCode( return StatusCode(
StatusCodes.Status503ServiceUnavailable, StatusCodes.Status503ServiceUnavailable,
new { message = "Ingen postutdelning på arbetsfria dagar." }); new { message = "Ingen postutdelning när det är helg." });
} }
return Random.Shared.NextDouble() < 0.4 bool won = 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." }); return Ok(new
{
won,
message = won
? "Grattis! Försändelsen finns snart i en brevlåda nära dig."
: "Tyvärr, ingen post idag."
});
} }
} }
} }
@@ -1,5 +1,4 @@
using System; using System;
using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Threading.RateLimiting; using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@@ -12,77 +11,63 @@ namespace PostNordLotteriet.WebAPI.Extensions
public static class RateLimiterExtensions public static class RateLimiterExtensions
{ {
// Adds a rate limiter with a custom error message in JSON format. // 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 => 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<IWorkDayService>().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 = IWorkDayService workDayService =
httpContext.RequestServices.GetRequiredService<IWorkDayService>(); httpContext.RequestServices.GetRequiredService<IWorkDayService>();
DateTimeOffset now = DateTimeOffset.Now; DateTime now = DateTime.Now;
DateTime nextWorkDay = workDayService.GetNextWorkDay(now.DateTime); // Disable the rate limiter on non-work days.
if (workDayService.IsNonWorkDay(now))
// 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))
{ {
DateTimeOffset limiterReset = now.Add(retryAfter); return RateLimitPartition.GetNoLimiter("NoLimiter");
if (limiterReset > nextAttempt)
{
nextAttempt = limiterReset;
}
} }
string formattedNextAttempt = nextAttempt.ToString( string ipAddress =
"dddd d MMMM 'klockan' HH:mm", httpContext.Connection.RemoteIpAddress?.ToString()
CultureInfo.GetCultureInfo("sv-SE")); ?? "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 return RateLimitPartition.GetFixedWindowLimiter(
.ToUniversalTime() partitionKey,
.ToString("R", CultureInfo.InvariantCulture); _ => new FixedWindowRateLimiterOptions
await httpContext.Response.WriteAsync(
JsonSerializer.Serialize(new
{ {
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); cancellationToken);
}; };
}); });
+1 -1
View File
@@ -22,7 +22,7 @@ services:
build: build:
context: ./frontend/.devcontainer context: ./frontend/.devcontainer
dockerfile: Dockerfile dockerfile: Dockerfile
image: devcontainers/angular:21 image: devcontainers/typescript-node:24-bookworm
networks: networks:
- net - net
volumes: volumes:
+1 -1
View File
@@ -9,7 +9,7 @@
@if (result(); as result) { @if (result(); as result) {
<div class="absolute inset-x-0 top-full mt-8 flex justify-center"> <div class="absolute inset-x-0 top-full mt-8 flex justify-center">
<pn-toast <pn-toast
class="max-w-full" class="block w-full max-w-83.75"
[appearance]="result.appearance" [appearance]="result.appearance"
[text]="result.message" [text]="result.message"
closable closable
+5 -6
View File
@@ -6,9 +6,7 @@ import { ConfettiService } from '../confetti/confetti.service';
import { ShipmentService } from '../shipment/shipment.service'; import { ShipmentService } from '../shipment/shipment.service';
type DrawResult = type DrawResult =
| { appearance: 'success'; message: string } { appearance: 'success'; message: string } | { appearance: 'warning'; message: string } | { appearance: 'error'; message: string };
| { appearance: 'warning'; message: string }
| { appearance: 'error'; message: string };
@Component({ @Component({
selector: 'app-draw', selector: 'app-draw',
@@ -43,11 +41,13 @@ export class DrawComponent {
.subscribe({ .subscribe({
next: (response) => { next: (response) => {
this.result.set({ this.result.set({
appearance: 'success', appearance: response.won ? 'success' : 'warning',
message: response.message message: response.message
}); });
this.confettiService.celebrate(); if (response.won) {
this.confettiService.celebrate();
}
}, },
error: (error: HttpErrorResponse) => { error: (error: HttpErrorResponse) => {
this.handleError(error); this.handleError(error);
@@ -61,7 +61,6 @@ export class DrawComponent {
private handleError(error: HttpErrorResponse): void { private handleError(error: HttpErrorResponse): void {
switch (error.status) { switch (error.status) {
case 400:
case 429: case 429:
case 503: case 503:
this.result.set({ this.result.set({
@@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
export interface ShipmentResponse { export interface ShipmentResponse {
message: string; message: string;
won: boolean
} }
@Injectable({ @Injectable({
+1 -1
View File
@@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
</head> </head>
<body class="bg-linear-to-b/srgb from-white from-0% via-[#f2fcff] via-40% to-pn-blue-25 to-100%"> <body class="bg-linear-to-b/srgb from-white from-0% via-[#f2fcff] via-50% to-pn-blue-25 to-100%">
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>