Refactoring
This commit is contained in:
1 parent
e13a710fa6
commit
8835137e9b
7 files changed
+62
-80
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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.won) {
|
||||||
this.confettiService.celebrate();
|
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({
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in new issue
Block a user