2026-03-03 14:23:50 +01:00
|
|
|
using Microsoft.AspNetCore.Authentication;
|
|
|
|
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using WorkClub.Api.Auth;
|
2026-03-03 19:41:01 +01:00
|
|
|
using WorkClub.Api.Endpoints.Clubs;
|
|
|
|
|
using WorkClub.Api.Endpoints.Members;
|
feat(shifts): add Shift CRUD API with sign-up/cancel and capacity management
- ShiftService with 7 methods: list, detail, create, update, delete, signup, cancel
- 5 DTOs: ShiftListDto, ShiftDetailDto, CreateShiftRequest, UpdateShiftRequest, ShiftSignupDto
- Minimal API endpoints: GET /api/shifts, GET /api/shifts/{id}, POST, PUT, DELETE, POST /signup, DELETE /signup
- Capacity validation: sign-up rejected when full → 409 Conflict
- Past shift blocking: cannot sign up for past shifts → 422 Unprocessable
- Duplicate signup prevention: check existing before create → 409 Conflict
- Concurrency: 2-attempt retry loop for last-slot race conditions
- Authorization: POST/PUT (Manager+), DELETE (Admin), signup/cancel (Member+)
- Test infrastructure: Added X-Test-UserId header support for member ID injection
- 13 TDD integration tests: CRUD, sign-up, capacity, past shift, concurrency
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 15 complete. Wave 3: 3/5 tasks done.
2026-03-03 19:30:23 +01:00
|
|
|
using WorkClub.Api.Endpoints.Shifts;
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using WorkClub.Api.Endpoints.Tasks;
|
2026-03-03 14:23:50 +01:00
|
|
|
using WorkClub.Api.Middleware;
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
using WorkClub.Api.Services;
|
2026-03-03 14:23:50 +01:00
|
|
|
using WorkClub.Application.Interfaces;
|
|
|
|
|
using WorkClub.Infrastructure.Data;
|
2026-03-03 18:52:35 +01:00
|
|
|
using WorkClub.Infrastructure.Data.Interceptors;
|
2026-03-03 14:23:50 +01:00
|
|
|
using WorkClub.Infrastructure.Services;
|
|
|
|
|
using WorkClub.Infrastructure.Seed;
|
|
|
|
|
|
2026-03-03 14:02:37 +01:00
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
|
|
|
|
|
|
builder.Services.AddOpenApi();
|
|
|
|
|
|
2026-03-03 14:23:50 +01:00
|
|
|
builder.Services.AddHttpContextAccessor();
|
|
|
|
|
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
|
|
|
|
|
builder.Services.AddScoped<SeedDataService>();
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
builder.Services.AddScoped<TaskService>();
|
feat(shifts): add Shift CRUD API with sign-up/cancel and capacity management
- ShiftService with 7 methods: list, detail, create, update, delete, signup, cancel
- 5 DTOs: ShiftListDto, ShiftDetailDto, CreateShiftRequest, UpdateShiftRequest, ShiftSignupDto
- Minimal API endpoints: GET /api/shifts, GET /api/shifts/{id}, POST, PUT, DELETE, POST /signup, DELETE /signup
- Capacity validation: sign-up rejected when full → 409 Conflict
- Past shift blocking: cannot sign up for past shifts → 422 Unprocessable
- Duplicate signup prevention: check existing before create → 409 Conflict
- Concurrency: 2-attempt retry loop for last-slot race conditions
- Authorization: POST/PUT (Manager+), DELETE (Admin), signup/cancel (Member+)
- Test infrastructure: Added X-Test-UserId header support for member ID injection
- 13 TDD integration tests: CRUD, sign-up, capacity, past shift, concurrency
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 15 complete. Wave 3: 3/5 tasks done.
2026-03-03 19:30:23 +01:00
|
|
|
builder.Services.AddScoped<ShiftService>();
|
2026-03-03 19:41:01 +01:00
|
|
|
builder.Services.AddScoped<ClubService>();
|
|
|
|
|
builder.Services.AddScoped<MemberService>();
|
|
|
|
|
builder.Services.AddScoped<MemberSyncService>();
|
2026-03-03 14:23:50 +01:00
|
|
|
|
2026-03-05 19:22:21 +01:00
|
|
|
builder.Services.AddScoped<TenantDbConnectionInterceptor>();
|
2026-03-03 18:52:35 +01:00
|
|
|
builder.Services.AddSingleton<SaveChangesTenantInterceptor>();
|
|
|
|
|
|
2026-03-03 14:23:50 +01:00
|
|
|
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
|
|
|
.AddJwtBearer(options =>
|
|
|
|
|
{
|
|
|
|
|
options.Authority = builder.Configuration["Keycloak:Authority"];
|
|
|
|
|
options.Audience = builder.Configuration["Keycloak:Audience"];
|
|
|
|
|
options.RequireHttpsMetadata = false;
|
|
|
|
|
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
|
|
|
|
|
{
|
2026-03-05 14:12:53 +01:00
|
|
|
ValidateIssuer = false, // Disabled for local dev - external clients use localhost:8080, internal use keycloak:8080
|
2026-03-03 14:23:50 +01:00
|
|
|
ValidateAudience = true,
|
|
|
|
|
ValidateLifetime = true,
|
|
|
|
|
ValidateIssuerSigningKey = true
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
builder.Services.AddScoped<IClaimsTransformation, ClubRoleClaimsTransformation>();
|
|
|
|
|
|
|
|
|
|
builder.Services.AddAuthorizationBuilder()
|
|
|
|
|
.AddPolicy("RequireAdmin", policy => policy.RequireRole("Admin"))
|
|
|
|
|
.AddPolicy("RequireManager", policy => policy.RequireRole("Admin", "Manager"))
|
|
|
|
|
.AddPolicy("RequireMember", policy => policy.RequireRole("Admin", "Manager", "Member"))
|
|
|
|
|
.AddPolicy("RequireViewer", policy => policy.RequireAuthenticatedUser());
|
|
|
|
|
|
2026-03-03 18:52:35 +01:00
|
|
|
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
|
|
|
|
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
|
|
|
|
|
.AddInterceptors(
|
|
|
|
|
sp.GetRequiredService<TenantDbConnectionInterceptor>(),
|
|
|
|
|
sp.GetRequiredService<SaveChangesTenantInterceptor>()));
|
2026-03-03 14:23:50 +01:00
|
|
|
|
feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string
Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00
|
|
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
|
|
|
|
|
if (!string.IsNullOrEmpty(connectionString))
|
|
|
|
|
{
|
|
|
|
|
builder.Services.AddHealthChecks()
|
|
|
|
|
.AddNpgSql(connectionString);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
builder.Services.AddHealthChecks();
|
|
|
|
|
}
|
2026-03-03 14:23:50 +01:00
|
|
|
|
2026-03-03 14:02:37 +01:00
|
|
|
var app = builder.Build();
|
|
|
|
|
|
|
|
|
|
if (app.Environment.IsDevelopment())
|
|
|
|
|
{
|
|
|
|
|
app.MapOpenApi();
|
2026-03-05 11:07:19 +01:00
|
|
|
|
2026-03-03 14:23:50 +01:00
|
|
|
using var scope = app.Services.CreateScope();
|
|
|
|
|
var seedService = scope.ServiceProvider.GetRequiredService<SeedDataService>();
|
|
|
|
|
await seedService.SeedAsync();
|
2026-03-03 14:02:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app.UseHttpsRedirection();
|
|
|
|
|
|
2026-03-03 14:23:50 +01:00
|
|
|
app.UseAuthentication();
|
|
|
|
|
app.UseMiddleware<TenantValidationMiddleware>();
|
|
|
|
|
app.UseAuthorization();
|
2026-03-03 19:41:01 +01:00
|
|
|
app.UseMiddleware<MemberSyncMiddleware>();
|
2026-03-03 14:23:50 +01:00
|
|
|
|
|
|
|
|
app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
|
|
|
|
|
{
|
|
|
|
|
Predicate = _ => false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.MapHealthChecks("/health/ready");
|
|
|
|
|
app.MapHealthChecks("/health/startup");
|
|
|
|
|
|
2026-03-03 14:02:37 +01:00
|
|
|
var summaries = new[]
|
|
|
|
|
{
|
|
|
|
|
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
app.MapGet("/weatherforecast", () =>
|
|
|
|
|
{
|
2026-03-05 11:07:19 +01:00
|
|
|
var forecast = Enumerable.Range(1, 5).Select(index =>
|
2026-03-03 14:02:37 +01:00
|
|
|
new WeatherForecast
|
|
|
|
|
(
|
|
|
|
|
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
|
|
|
|
Random.Shared.Next(-20, 55),
|
|
|
|
|
summaries[Random.Shared.Next(summaries.Length)]
|
|
|
|
|
))
|
|
|
|
|
.ToArray();
|
|
|
|
|
return forecast;
|
|
|
|
|
})
|
|
|
|
|
.WithName("GetWeatherForecast");
|
|
|
|
|
|
2026-03-03 14:23:50 +01:00
|
|
|
app.MapGet("/api/test", () => Results.Ok(new { message = "Test endpoint" }))
|
|
|
|
|
.RequireAuthorization();
|
|
|
|
|
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
app.MapTaskEndpoints();
|
feat(shifts): add Shift CRUD API with sign-up/cancel and capacity management
- ShiftService with 7 methods: list, detail, create, update, delete, signup, cancel
- 5 DTOs: ShiftListDto, ShiftDetailDto, CreateShiftRequest, UpdateShiftRequest, ShiftSignupDto
- Minimal API endpoints: GET /api/shifts, GET /api/shifts/{id}, POST, PUT, DELETE, POST /signup, DELETE /signup
- Capacity validation: sign-up rejected when full → 409 Conflict
- Past shift blocking: cannot sign up for past shifts → 422 Unprocessable
- Duplicate signup prevention: check existing before create → 409 Conflict
- Concurrency: 2-attempt retry loop for last-slot race conditions
- Authorization: POST/PUT (Manager+), DELETE (Admin), signup/cancel (Member+)
- Test infrastructure: Added X-Test-UserId header support for member ID injection
- 13 TDD integration tests: CRUD, sign-up, capacity, past shift, concurrency
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 15 complete. Wave 3: 3/5 tasks done.
2026-03-03 19:30:23 +01:00
|
|
|
app.MapShiftEndpoints();
|
2026-03-03 19:41:01 +01:00
|
|
|
app.MapClubEndpoints();
|
|
|
|
|
app.MapMemberEndpoints();
|
feat(tasks): add Task CRUD API with 5-state workflow
- TaskService with CRUD operations + state machine enforcement
- 5 DTOs: TaskListDto, TaskDetailDto, CreateTaskRequest, UpdateTaskRequest
- Minimal API endpoints: GET /api/tasks, GET /api/tasks/{id}, POST, PATCH, DELETE
- State machine: Open→Assigned→InProgress→Review→Done (invalid transitions → 422)
- Concurrency: DbUpdateConcurrencyException → 409 Conflict
- Authorization: POST (Manager+), DELETE (Admin), GET/PATCH (Member+)
- RLS: Automatic tenant filtering via TenantDbConnectionInterceptor
- 10 TDD integration tests: CRUD, state transitions, concurrency, role enforcement
- Build: 0 errors (6 BouncyCastle warnings expected)
Task 14 complete. Wave 3: 2/5 tasks done.
2026-03-03 19:19:21 +01:00
|
|
|
|
2026-03-03 14:02:37 +01:00
|
|
|
app.Run();
|
|
|
|
|
|
|
|
|
|
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
|
|
|
|
{
|
|
|
|
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
|
|
|
|
}
|
2026-03-03 14:23:50 +01:00
|
|
|
|
|
|
|
|
public partial class Program { }
|