From 667fa25525239543b8a7246382a6ecabe462499c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veikko=20Lintuj=C3=A4rvi?= Date: Tue, 3 Mar 2026 22:32:32 +0200 Subject: [PATCH] Fix local auth errors --- api/.gitignore | 3 + api/App.Tests/ApiEndpointsTests.cs | 117 +++++++++++++++++++++-------- api/App/Endpoints/LokEndpoints.cs | 32 ++++++-- api/Database/klapi.db | Bin 16384 -> 16384 bytes start.sh | 17 ++++- ui/vite.config.ts | 2 +- 6 files changed, 131 insertions(+), 40 deletions(-) diff --git a/api/.gitignore b/api/.gitignore index 0808c4a..94f4e9c 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -480,3 +480,6 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp + +# SQLite database file +klapi.db \ No newline at end of file diff --git a/api/App.Tests/ApiEndpointsTests.cs b/api/App.Tests/ApiEndpointsTests.cs index f6915df..5d399ae 100644 --- a/api/App.Tests/ApiEndpointsTests.cs +++ b/api/App.Tests/ApiEndpointsTests.cs @@ -3,12 +3,13 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; namespace App.Tests; -public class ApiEndpointsTests(ApiTestFactory factory) : IClassFixture +public class ApiEndpointsTests(DevelopmentApiTestFactory factory) : IClassFixture { private readonly HttpClient _client = factory.CreateClient(); @@ -36,34 +37,8 @@ public class ApiEndpointsTests(ApiTestFactory factory) : IClassFixture(); - Assert.NotNull(auth); - Assert.False(string.IsNullOrWhiteSpace(auth.AccessToken)); - - _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", auth.AccessToken); - var createPayload = new { id = 0, @@ -159,17 +134,85 @@ public class ApiEndpointsTests(ApiTestFactory factory) : IClassFixture +public class ProductionAuthTests(ProductionApiTestFactory factory) : IClassFixture { + private readonly HttpClient _client = factory.CreateClient(); + + [Fact] + public async Task WriteEndpoints_RequireAuthInProduction() + { + var createWithoutAuthResponse = await _client.PostAsJsonAsync("/lok/open-hours", new + { + id = 0, + name = "unauthorized", + version = DateTime.UtcNow.ToString("O"), + paragraph1 = "p1", + paragraph2 = "p2", + paragraph3 = "p3", + paragraph4 = "p4", + kitchenNotice = "k1" + }); + + Assert.Equal(HttpStatusCode.Unauthorized, createWithoutAuthResponse.StatusCode); + + var activateWithoutAuthResponse = await _client.PutAsync("/lok/open-hours/1/active", null); + Assert.Equal(HttpStatusCode.Unauthorized, activateWithoutAuthResponse.StatusCode); + } + + [Fact] + public async Task CreateOpenHours_WorksWithAuthInProduction() + { + var tokenResponse = await _client.PostAsJsonAsync("/auth/token", new + { + email = "admin@klapi.local", + password = "changeme" + }); + + Assert.Equal(HttpStatusCode.OK, tokenResponse.StatusCode); + var auth = await tokenResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(auth); + Assert.False(string.IsNullOrWhiteSpace(auth.AccessToken)); + + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", auth.AccessToken); + + var createPayload = new + { + id = 0, + name = "authorized", + version = DateTime.UtcNow.ToString("O"), + paragraph1 = "p1", + paragraph2 = "p2", + paragraph3 = "p3", + paragraph4 = "p4", + kitchenNotice = "k1" + }; + + var createResponse = await _client.PostAsJsonAsync("/lok/open-hours", createPayload); + + Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode); + } +} + +public abstract class ApiTestFactoryBase(string environmentName) : WebApplicationFactory +{ + private readonly string _environmentName = environmentName; private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"klapi-tests-{Guid.NewGuid():N}.db"); protected override void ConfigureWebHost(IWebHostBuilder builder) { + builder.UseEnvironment(_environmentName); + builder.ConfigureAppConfiguration((_, configBuilder) => { configBuilder.AddInMemoryCollection(new Dictionary { - ["ConnectionStrings:DefaultConnection"] = $"Data Source={_dbPath}" + ["ConnectionStrings:DefaultConnection"] = $"Data Source={_dbPath}", + ["Auth:Issuer"] = "klapi-api-tests", + ["Auth:Audience"] = "klapi-ui-tests", + ["Auth:SigningKey"] = "test-signing-key-which-is-at-least-32-characters-long", + ["Auth:AllowedOrigins:0"] = "http://localhost:5173", + ["Auth:Users:0:Email"] = "admin@klapi.local", + ["Auth:Users:0:Password"] = "changeme" }); }); } @@ -196,6 +239,20 @@ public class ApiTestFactory : WebApplicationFactory } } +public sealed class DevelopmentApiTestFactory : ApiTestFactoryBase +{ + public DevelopmentApiTestFactory() : base(Environments.Development) + { + } +} + +public sealed class ProductionApiTestFactory : ApiTestFactoryBase +{ + public ProductionApiTestFactory() : base(Environments.Production) + { + } +} + public class LokOpenHoursDto { public long Id { get; set; } diff --git a/api/App/Endpoints/LokEndpoints.cs b/api/App/Endpoints/LokEndpoints.cs index 32a57a4..313785f 100644 --- a/api/App/Endpoints/LokEndpoints.cs +++ b/api/App/Endpoints/LokEndpoints.cs @@ -2,7 +2,7 @@ public static class LokEndpoints { public static void MapLokEndpoints(WebApplication app) { - app.MapPost("/lok/open-hours", async (HttpContext httpContext) => + var createLokOpenHoursEndpoint = app.MapPost("/lok/open-hours", async (HttpContext httpContext) => { var lokService = httpContext.RequestServices.GetRequiredService(); var openHours = await httpContext.Request.ReadFromJsonAsync(); @@ -32,10 +32,14 @@ public static class LokEndpoints httpContext.Response.Headers.Location = "/lok/open-hours"; await httpContext.Response.WriteAsJsonAsync(createdOpenHours); }) - .RequireAuthorization("OpenHoursWrite") .RequireCors("FrontendWriteCors") .WithName("CreateLokOpenHours"); + if (!app.Environment.IsDevelopment()) + { + createLokOpenHoursEndpoint.RequireAuthorization("OpenHoursWrite"); + } + app.MapGet("/lok/open-hours", async (HttpContext httpContext) => { var lokService = httpContext.RequestServices.GetRequiredService(); @@ -56,7 +60,7 @@ public static class LokEndpoints .RequireCors("PublicReadCors") .WithName("GetLokOpenHours"); - app.MapDelete("/lok/open-hours/{id:long}", async (HttpContext httpContext, long id) => + var deleteLokOpenHoursEndpoint = app.MapDelete("/lok/open-hours/{id:long}", async (HttpContext httpContext, long id) => { var lokService = httpContext.RequestServices.GetRequiredService(); var deleted = await lokService.DeleteOpenHours(id); @@ -73,11 +77,15 @@ public static class LokEndpoints httpContext.Response.StatusCode = StatusCodes.Status204NoContent; }) - .RequireAuthorization("OpenHoursWrite") .RequireCors("FrontendWriteCors") .WithName("DeleteLokOpenHours"); - app.MapPut("/lok/open-hours/{id:long}", async (HttpContext httpContext, long id) => + if (!app.Environment.IsDevelopment()) + { + deleteLokOpenHoursEndpoint.RequireAuthorization("OpenHoursWrite"); + } + + var updateLokOpenHoursEndpoint = app.MapPut("/lok/open-hours/{id:long}", async (HttpContext httpContext, long id) => { var lokService = httpContext.RequestServices.GetRequiredService(); var openHours = await httpContext.Request.ReadFromJsonAsync(); @@ -116,11 +124,15 @@ public static class LokEndpoints await httpContext.Response.WriteAsJsonAsync(updatedOpenHours); }) - .RequireAuthorization("OpenHoursWrite") .RequireCors("FrontendWriteCors") .WithName("UpdateLokOpenHours"); - app.MapPut("/lok/open-hours/{id:long}/active", async (HttpContext httpContext, long id) => + if (!app.Environment.IsDevelopment()) + { + updateLokOpenHoursEndpoint.RequireAuthorization("OpenHoursWrite"); + } + + var setActiveLokOpenHoursEndpoint = app.MapPut("/lok/open-hours/{id:long}/active", async (HttpContext httpContext, long id) => { var lokService = httpContext.RequestServices.GetRequiredService(); var activated = await lokService.SetActiveOpenHours(id); @@ -141,8 +153,12 @@ public static class LokEndpoints IsActive = true }); }) - .RequireAuthorization("OpenHoursWrite") .RequireCors("FrontendWriteCors") .WithName("SetActiveLokOpenHours"); + + if (!app.Environment.IsDevelopment()) + { + setActiveLokOpenHoursEndpoint.RequireAuthorization("OpenHoursWrite"); + } } } \ No newline at end of file diff --git a/api/Database/klapi.db b/api/Database/klapi.db index 515e77791e2d3ce34deb6a6d39ba10c3e9b6f4fc..4b0b0e60b4eaa9f4e9662f1037f1a8349e618a01 100644 GIT binary patch delta 131 zcmZo@U~Fh$oFFa8#K6G70>m)DG*QP`n2ABJO_LWW#LVl$z?aJ>&FivRP~j5KWKI6- zlb!g2xS9nR*~L{=8C#Pl&*N*IEXSuj*@pK4J6I2cz-B>%SNszv2(W>SK;m+tZ~-9k B8q)v( literal 16384 zcmeI2Ur!rH5Wx3hl)&M$(^7;eLb|lIfEr(J|J+~Eq{g`xRL2BrRPYP3@fj= z00e*l5C8%|00;m9AOHk_K%Bt0De2B!CL?`+5LIhW{o?7#R^YcbPR}}Fe0;00hbUZb7 zquNO`Iz0AUrPHW+czt^Es@e?mhf(vnKY(?Gg1++P*6q1$R@(2CBWyqA$H{}xZ=d{B=2TXYOG}`ewV?s{1cC_YUGsb+Fa)FHptb zow&?*x3syv>v4#Wygl_=OWrj9E?cFcW?t@BXk{$xuQt3L?{!r_QL&;-pm(RwQR=7k zgY+M1l=?mOP&rR2N;mzhvYz}+k&_$AACvRbe@>rIH{%--#y|iF00AHX1b_e#00KbZ z|3~0RlTp^YCuHg@s8=Jup1VBL5uKPho#u2>Cc2B6s}s$zOv|98(pL@eU_=HZIvBD3 z=mZz;%V@=$5i%M1Vbu4MBX9I$V1{j032-eSwSd+FRy)Bp{$>3^#(PJ|ROD;Aj21mb z$W%3~Q^E)xUW{Ou63wzqW*D@R&Bo@S| z+FV^{nyF(#FsnTJSa5tpdIlTuLR@BO7#pT#;nDo=XJs_+&4`R9kHR{kSB)vtY?~UE z&MJJmurvl$?6}Mn!P<_c6LN)WNiu-@ohg%8Jz8&j7#8nJC=GBQf5ppLhQ z?zqIz3~CbFxUxv+_coSZ97qK<0+k`QP7p30r*0b}kw>ftj zzKE0PMa67h3Su~hUQu{7Qu;5%FAyLA1b_e#00KY&2mk>f00e*l5C8%|;H@BVS4w20 v_&*f*FNos&|2N_n2oL}QKmZ5;0U!VbfB+Bx0zd!=00AKI_7Rv!OiB7b8HPL} diff --git a/start.sh b/start.sh index b2a3e2b..7ada83d 100755 --- a/start.sh +++ b/start.sh @@ -33,8 +33,23 @@ ensure_app_window() { start_or_restart_services() { ensure_app_window - tmux respawn-pane -k -t "$SESSION_NAME":app.0 -c "$UI_DIR" "bun dev" tmux respawn-pane -k -t "$SESSION_NAME":app.1 -c "$API_DIR" "dotnet run --project $API_DIR/App/App.csproj" + + local health_url="http://127.0.0.1:5013/health/db" + local max_attempts=60 + local attempt=1 + + until curl --silent --fail "$health_url" >/dev/null 2>&1; do + if [[ "$attempt" -ge "$max_attempts" ]]; then + echo "API did not become healthy in time; starting UI anyway." >&2 + break + fi + + sleep 0.5 + attempt=$((attempt + 1)) + done + + tmux respawn-pane -k -t "$SESSION_NAME":app.0 -c "$UI_DIR" "bun dev" tmux select-pane -t "$SESSION_NAME":app.0 tmux select-window -t "$SESSION_NAME":app } diff --git a/ui/vite.config.ts b/ui/vite.config.ts index b7a4c32..6d28e0e 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ server: { proxy: { "/api": { - target: "http://localhost:5013", + target: "http://127.0.0.1:5013", changeOrigin: true, rewrite: (pathValue) => pathValue.replace(/^\/api/, ""), },