Files
work-club-manager/backend/WorkClub.Api/Endpoints/Shifts/ShiftEndpoints.cs
WorkClub Automation a8730245b2
All checks were successful
CI Pipeline / Backend Build & Test (pull_request) Successful in 52s
CI Pipeline / Frontend Lint, Test & Build (pull_request) Successful in 29s
CI Pipeline / Infrastructure Validation (pull_request) Successful in 5s
fix(backend): resolve shift signup by looking up Member via ExternalUserId
The signup/cancel endpoints were passing the Keycloak sub claim (external UUID)
directly as MemberId, but ShiftSignup.MemberId references the internal Member.Id.
Now ShiftService resolves ExternalUserId to the internal Member.Id before creating
the signup record. Integration tests updated to seed proper Member entities.
2026-03-09 13:24:50 +01:00

168 lines
5.2 KiB
C#

using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using WorkClub.Api.Services;
using WorkClub.Application.Shifts.DTOs;
namespace WorkClub.Api.Endpoints.Shifts;
public static class ShiftEndpoints
{
public static void MapShiftEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/shifts");
group.MapGet("", GetShifts)
.RequireAuthorization("RequireMember")
.WithName("GetShifts");
group.MapGet("{id:guid}", GetShift)
.RequireAuthorization("RequireMember")
.WithName("GetShift");
group.MapPost("", CreateShift)
.RequireAuthorization("RequireManager")
.WithName("CreateShift");
group.MapPut("{id:guid}", UpdateShift)
.RequireAuthorization("RequireManager")
.WithName("UpdateShift");
group.MapDelete("{id:guid}", DeleteShift)
.RequireAuthorization("RequireAdmin")
.WithName("DeleteShift");
group.MapPost("{id:guid}/signup", SignUpForShift)
.RequireAuthorization("RequireMember")
.WithName("SignUpForShift");
group.MapDelete("{id:guid}/signup", CancelSignup)
.RequireAuthorization("RequireMember")
.WithName("CancelSignup");
}
private static async Task<Ok<ShiftListDto>> GetShifts(
ShiftService shiftService,
[FromQuery] DateTimeOffset? from = null,
[FromQuery] DateTimeOffset? to = null,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var result = await shiftService.GetShiftsAsync(from, to, page, pageSize);
return TypedResults.Ok(result);
}
private static async Task<Results<Ok<ShiftDetailDto>, NotFound>> GetShift(
Guid id,
ShiftService shiftService)
{
var result = await shiftService.GetShiftByIdAsync(id);
if (result == null)
return TypedResults.NotFound();
return TypedResults.Ok(result);
}
private static async Task<Results<Created<ShiftDetailDto>, BadRequest<string>>> CreateShift(
CreateShiftRequest request,
ShiftService shiftService,
HttpContext httpContext)
{
var userIdClaim = httpContext.User.FindFirst("sub")?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var createdById))
{
return TypedResults.BadRequest("Invalid user ID");
}
var (shift, error) = await shiftService.CreateShiftAsync(request, createdById);
if (error != null || shift == null)
return TypedResults.BadRequest(error ?? "Failed to create shift");
return TypedResults.Created($"/api/shifts/{shift.Id}", shift);
}
private static async Task<Results<Ok<ShiftDetailDto>, NotFound, Conflict<string>>> UpdateShift(
Guid id,
UpdateShiftRequest request,
ShiftService shiftService)
{
var (shift, error, isConflict) = await shiftService.UpdateShiftAsync(id, request);
if (error != null)
{
if (error == "Shift not found")
return TypedResults.NotFound();
if (isConflict)
return TypedResults.Conflict(error);
}
return TypedResults.Ok(shift!);
}
private static async Task<Results<NoContent, NotFound>> DeleteShift(
Guid id,
ShiftService shiftService)
{
var deleted = await shiftService.DeleteShiftAsync(id);
if (!deleted)
return TypedResults.NotFound();
return TypedResults.NoContent();
}
private static async Task<Results<Ok, NotFound, UnprocessableEntity<string>, Conflict<string>>> SignUpForShift(
Guid id,
ShiftService shiftService,
HttpContext httpContext)
{
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
if (string.IsNullOrEmpty(externalUserId))
{
return TypedResults.UnprocessableEntity("Invalid user ID");
}
var (success, error, isConflict) = await shiftService.SignUpForShiftAsync(id, externalUserId);
if (!success)
{
if (error == "Shift not found" || error == "Member not found")
return TypedResults.NotFound();
if (error == "Cannot sign up for past shifts")
return TypedResults.UnprocessableEntity(error);
if (isConflict)
return TypedResults.Conflict(error!);
}
return TypedResults.Ok();
}
private static async Task<Results<Ok, NotFound, UnprocessableEntity<string>>> CancelSignup(
Guid id,
ShiftService shiftService,
HttpContext httpContext)
{
var externalUserId = httpContext.User.FindFirst("sub")?.Value;
if (string.IsNullOrEmpty(externalUserId))
{
return TypedResults.UnprocessableEntity("Invalid user ID");
}
var (success, error) = await shiftService.CancelSignupAsync(id, externalUserId);
if (!success)
{
if (error == "Sign-up not found" || error == "Member not found")
return TypedResults.NotFound();
return TypedResults.UnprocessableEntity(error!);
}
return TypedResults.Ok();
}
}