Fix local auth errors
This commit is contained in:
3
api/.gitignore
vendored
3
api/.gitignore
vendored
@@ -480,3 +480,6 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
# Vim temporary swap files
|
# Vim temporary swap files
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
|
# SQLite database file
|
||||||
|
klapi.db
|
||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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.
17
start.sh
17
start.sh
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/, ""),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user