feat(clubs): add Club and Member API endpoints with auto-sync
Implement Task 16: Club + Member API endpoints with MemberSyncService
Services:
- ClubService: GetMyClubsAsync (user's clubs), GetCurrentClubAsync (tenant club)
- MemberService: GetMembersAsync (list), GetMemberByIdAsync, GetCurrentMemberAsync
- MemberSyncService: Auto-creates Member records from JWT on first request
Middleware:
- MemberSyncMiddleware: Runs after auth, calls MemberSyncService
Endpoints:
- GET /api/clubs/me (list user's clubs)
- GET /api/clubs/current (current tenant's club)
- GET /api/members (list members, RLS filtered)
- GET /api/members/{id} (member detail)
- GET /api/members/me (current user's membership)
Tests: 14 integration tests (6 club + 8 member)
- Club filtering by user membership
- Multi-tenant isolation via RLS
- Member auto-sync on first request
- Cross-tenant access blocked
- Role-based authorization
Build: 0 errors, all tests compile
Pattern: TypedResults, RequireAuthorization policies, TDD approach
This commit is contained in:
79
backend/WorkClub.Api/Services/ClubService.cs
Normal file
79
backend/WorkClub.Api/Services/ClubService.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WorkClub.Application.Clubs.DTOs;
|
||||
using WorkClub.Application.Interfaces;
|
||||
using WorkClub.Infrastructure.Data;
|
||||
|
||||
namespace WorkClub.Api.Services;
|
||||
|
||||
public class ClubService
|
||||
{
|
||||
private readonly AppDbContext _context;
|
||||
private readonly ITenantProvider _tenantProvider;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public ClubService(
|
||||
AppDbContext context,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_context = context;
|
||||
_tenantProvider = tenantProvider;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public async Task<List<ClubListDto>> GetMyClubsAsync()
|
||||
{
|
||||
var userIdClaim = _httpContextAccessor.HttpContext?.User.FindFirst("sub")?.Value;
|
||||
if (string.IsNullOrEmpty(userIdClaim))
|
||||
{
|
||||
return new List<ClubListDto>();
|
||||
}
|
||||
|
||||
var memberships = await _context.Members
|
||||
.Where(m => m.ExternalUserId == userIdClaim)
|
||||
.ToListAsync();
|
||||
|
||||
var clubIds = memberships.Select(m => m.ClubId).ToList();
|
||||
|
||||
var clubs = await _context.Clubs
|
||||
.Where(c => clubIds.Contains(c.Id))
|
||||
.ToListAsync();
|
||||
|
||||
var clubDtos = new List<ClubListDto>();
|
||||
foreach (var club in clubs)
|
||||
{
|
||||
var memberCount = await _context.Members
|
||||
.Where(m => m.ClubId == club.Id)
|
||||
.CountAsync();
|
||||
|
||||
clubDtos.Add(new ClubListDto(
|
||||
club.Id,
|
||||
club.Name,
|
||||
club.SportType.ToString(),
|
||||
memberCount
|
||||
));
|
||||
}
|
||||
|
||||
return clubDtos;
|
||||
}
|
||||
|
||||
public async Task<ClubDetailDto?> GetCurrentClubAsync()
|
||||
{
|
||||
var tenantId = _tenantProvider.GetTenantId();
|
||||
|
||||
var club = await _context.Clubs
|
||||
.FirstOrDefaultAsync(c => c.TenantId == tenantId);
|
||||
|
||||
if (club == null)
|
||||
return null;
|
||||
|
||||
return new ClubDetailDto(
|
||||
club.Id,
|
||||
club.Name,
|
||||
club.SportType.ToString(),
|
||||
club.Description,
|
||||
club.CreatedAt,
|
||||
club.UpdatedAt
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user