Fix local auth errors

This commit is contained in:
2026-03-03 22:32:32 +02:00
parent 2beeadd42c
commit 667fa25525
6 changed files with 131 additions and 40 deletions

3
api/.gitignore vendored
View File

@@ -480,3 +480,6 @@ $RECYCLE.BIN/
# Vim temporary swap files # Vim temporary swap files
*.swp *.swp
# SQLite database file
klapi.db

View File

@@ -3,12 +3,13 @@ using System.Net.Http.Headers;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace App.Tests; namespace App.Tests;
public class ApiEndpointsTests(ApiTestFactory factory) : IClassFixture<ApiTestFactory> public class ApiEndpointsTests(DevelopmentApiTestFactory factory) : IClassFixture<DevelopmentApiTestFactory>
{ {
private readonly HttpClient _client = factory.CreateClient(); private readonly HttpClient _client = factory.CreateClient();
@@ -36,34 +37,8 @@ public class ApiEndpointsTests(ApiTestFactory factory) : IClassFixture<ApiTestFa
} }
[Fact] [Fact]
public async Task OpenHours_Crud_Works() public async Task OpenHours_Crud_Works_WithoutAuthInDevelopment()
{ {
var unauthorizedCreateResponse = 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, unauthorizedCreateResponse.StatusCode);
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<AuthTokenDto>();
Assert.NotNull(auth);
Assert.False(string.IsNullOrWhiteSpace(auth.AccessToken));
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", auth.AccessToken);
var createPayload = new var createPayload = new
{ {
id = 0, id = 0,
@@ -159,17 +134,85 @@ public class ApiEndpointsTests(ApiTestFactory factory) : IClassFixture<ApiTestFa
} }
} }
public class ApiTestFactory : WebApplicationFactory<Program> public class ProductionAuthTests(ProductionApiTestFactory factory) : IClassFixture<ProductionApiTestFactory>
{ {
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<AuthTokenDto>();
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<Program>
{
private readonly string _environmentName = environmentName;
private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"klapi-tests-{Guid.NewGuid():N}.db"); private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"klapi-tests-{Guid.NewGuid():N}.db");
protected override void ConfigureWebHost(IWebHostBuilder builder) protected override void ConfigureWebHost(IWebHostBuilder builder)
{ {
builder.UseEnvironment(_environmentName);
builder.ConfigureAppConfiguration((_, configBuilder) => builder.ConfigureAppConfiguration((_, configBuilder) =>
{ {
configBuilder.AddInMemoryCollection(new Dictionary<string, string?> configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{ {
["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<Program>
} }
} }
public sealed class DevelopmentApiTestFactory : ApiTestFactoryBase
{
public DevelopmentApiTestFactory() : base(Environments.Development)
{
}
}
public sealed class ProductionApiTestFactory : ApiTestFactoryBase
{
public ProductionApiTestFactory() : base(Environments.Production)
{
}
}
public class LokOpenHoursDto public class LokOpenHoursDto
{ {
public long Id { get; set; } public long Id { get; set; }

View File

@@ -2,7 +2,7 @@ public static class LokEndpoints
{ {
public static void MapLokEndpoints(WebApplication app) 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<LokService>(); var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
var openHours = await httpContext.Request.ReadFromJsonAsync<LokOpenHours>(); var openHours = await httpContext.Request.ReadFromJsonAsync<LokOpenHours>();
@@ -32,10 +32,14 @@ public static class LokEndpoints
httpContext.Response.Headers.Location = "/lok/open-hours"; httpContext.Response.Headers.Location = "/lok/open-hours";
await httpContext.Response.WriteAsJsonAsync(createdOpenHours); await httpContext.Response.WriteAsJsonAsync(createdOpenHours);
}) })
.RequireAuthorization("OpenHoursWrite")
.RequireCors("FrontendWriteCors") .RequireCors("FrontendWriteCors")
.WithName("CreateLokOpenHours"); .WithName("CreateLokOpenHours");
if (!app.Environment.IsDevelopment())
{
createLokOpenHoursEndpoint.RequireAuthorization("OpenHoursWrite");
}
app.MapGet("/lok/open-hours", async (HttpContext httpContext) => app.MapGet("/lok/open-hours", async (HttpContext httpContext) =>
{ {
var lokService = httpContext.RequestServices.GetRequiredService<LokService>(); var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
@@ -56,7 +60,7 @@ public static class LokEndpoints
.RequireCors("PublicReadCors") .RequireCors("PublicReadCors")
.WithName("GetLokOpenHours"); .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<LokService>(); var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
var deleted = await lokService.DeleteOpenHours(id); var deleted = await lokService.DeleteOpenHours(id);
@@ -73,11 +77,15 @@ public static class LokEndpoints
httpContext.Response.StatusCode = StatusCodes.Status204NoContent; httpContext.Response.StatusCode = StatusCodes.Status204NoContent;
}) })
.RequireAuthorization("OpenHoursWrite")
.RequireCors("FrontendWriteCors") .RequireCors("FrontendWriteCors")
.WithName("DeleteLokOpenHours"); .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<LokService>(); var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
var openHours = await httpContext.Request.ReadFromJsonAsync<LokOpenHours>(); var openHours = await httpContext.Request.ReadFromJsonAsync<LokOpenHours>();
@@ -116,11 +124,15 @@ public static class LokEndpoints
await httpContext.Response.WriteAsJsonAsync(updatedOpenHours); await httpContext.Response.WriteAsJsonAsync(updatedOpenHours);
}) })
.RequireAuthorization("OpenHoursWrite")
.RequireCors("FrontendWriteCors") .RequireCors("FrontendWriteCors")
.WithName("UpdateLokOpenHours"); .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<LokService>(); var lokService = httpContext.RequestServices.GetRequiredService<LokService>();
var activated = await lokService.SetActiveOpenHours(id); var activated = await lokService.SetActiveOpenHours(id);
@@ -141,8 +153,12 @@ public static class LokEndpoints
IsActive = true IsActive = true
}); });
}) })
.RequireAuthorization("OpenHoursWrite")
.RequireCors("FrontendWriteCors") .RequireCors("FrontendWriteCors")
.WithName("SetActiveLokOpenHours"); .WithName("SetActiveLokOpenHours");
if (!app.Environment.IsDevelopment())
{
setActiveLokOpenHoursEndpoint.RequireAuthorization("OpenHoursWrite");
}
} }
} }

Binary file not shown.

View File

@@ -33,8 +33,23 @@ ensure_app_window() {
start_or_restart_services() { start_or_restart_services() {
ensure_app_window 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" 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-pane -t "$SESSION_NAME":app.0
tmux select-window -t "$SESSION_NAME":app tmux select-window -t "$SESSION_NAME":app
} }

View File

@@ -8,7 +8,7 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
"/api": { "/api": {
target: "http://localhost:5013", target: "http://127.0.0.1:5013",
changeOrigin: true, changeOrigin: true,
rewrite: (pathValue) => pathValue.replace(/^\/api/, ""), rewrite: (pathValue) => pathValue.replace(/^\/api/, ""),
}, },