- Add middleware.ts for route protection (redirects unauthenticated users to /login) - Add useActiveClub() hook for managing active club context (localStorage + session) - Add apiClient() fetch wrapper with automatic Authorization + X-Tenant-Id headers - Configure vitest with jsdom environment and global test setup - Add comprehensive test coverage: 16/16 tests passing (hooks + API utility) - Install test dependencies: vitest, @testing-library/react, @vitejs/plugin-react, happy-dom Task 10 COMPLETE - all acceptance criteria met
550 lines
24 KiB
Markdown
550 lines
24 KiB
Markdown
# Learnings — Club Work Manager
|
|
|
|
_Conventions, patterns, and accumulated wisdom from task execution_
|
|
|
|
---
|
|
|
|
## Task 1: Monorepo Scaffolding (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **.NET 10 Solution Format Change**
|
|
- .NET 10 uses `.slnx` format (not `.sln`)
|
|
- Solution files are still named `WorkClub.slnx`, compatible with `dotnet sln add`
|
|
- Both formats work seamlessly with build system
|
|
|
|
2. **Clean Architecture Implementation**
|
|
- Successfully established layered architecture with proper dependencies
|
|
- Api → (Application + Infrastructure) → Domain
|
|
- Tests reference all layers for comprehensive coverage
|
|
- Project references added via `dotnet add reference`
|
|
|
|
3. **NuGet Package Versioning**
|
|
- Finbuckle.MultiTenant: Specified 8.2.0 but .NET 10 SDK resolved to 9.0.0
|
|
- This is expected behavior with `rollForward: latestFeature` in global.json
|
|
- No build failures - warnings only about version resolution
|
|
- Testcontainers brings in BouncyCastle which has known security advisories (expected in test dependencies)
|
|
|
|
4. **Git Configuration for Automation**
|
|
- Set `user.email` and `user.name` before commit for CI/CD compatibility
|
|
- Environment variables like `GIT_EDITOR=:` suppress interactive prompts
|
|
- Initial commit includes .sisyphus directory (plans, notepads, etc.)
|
|
|
|
5. **Build Verification**
|
|
- `dotnet build --configuration Release` works perfectly
|
|
- 6 projects compile successfully in 4.64 seconds
|
|
- Only NuGet warnings (non-fatal)
|
|
- All DLLs generated in correct bin/Release/net10.0 directories
|
|
|
|
### Configuration Files Created
|
|
|
|
- **.gitignore**: Comprehensive coverage for:
|
|
- .NET: bin/, obj/, *.user, .vs/
|
|
- Node: node_modules/, .next/, .cache/
|
|
- IDE: .idea/, .vscode/, *.swp
|
|
|
|
- **.editorconfig**: C# conventions with:
|
|
- 4-space indentation for .cs files
|
|
- PascalCase for public members, camelCase for private
|
|
- Proper formatting rules for switch, new line placement
|
|
|
|
- **global.json**: SDK pinning with latestFeature rollForward for flexibility
|
|
|
|
### Project Template Choices
|
|
|
|
- Api: `dotnet new webapi` (includes Program.cs, appsettings.json, Controllers template)
|
|
- Application/Domain/Infrastructure: `dotnet new classlib` (clean base)
|
|
- Tests: `dotnet new xunit` (modern testing framework, includes base dependencies)
|
|
|
|
### Next Phase Considerations
|
|
|
|
- Generated Program.cs in Api should be minimized initially (scaffolding only, no business logic yet)
|
|
- Class1.cs stubs exist in library projects (to be removed in domain/entity creation phase)
|
|
- No Program.cs modifications yet - pure scaffolding as required
|
|
|
|
---
|
|
|
|
## Task 2: Docker Compose with PostgreSQL 16 & Keycloak 26.x (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **Docker Compose v3.9 for Development**
|
|
- Uses explicit `app-network` bridge for service discovery
|
|
- Keycloak service depends on postgres with `condition: service_healthy` for ordered startup
|
|
- Health checks critical: PostgreSQL uses `pg_isready`, Keycloak uses `/health/ready` endpoint
|
|
|
|
2. **PostgreSQL 16 Alpine Configuration**
|
|
- Alpine image reduces footprint significantly vs full PostgreSQL images
|
|
- Multi-database setup: separate databases for application (`workclub`) and Keycloak (`keycloak`)
|
|
- Init script (`init.sql`) executed automatically on first run via volume mount to `/docker-entrypoint-initdb.d`
|
|
- Default PostgreSQL connection isolation: `read_committed` with max 200 connections configured
|
|
|
|
3. **Keycloak 26.x Setup**
|
|
- Image: `quay.io/keycloak/keycloak:26.1` from Red Hat's container registry
|
|
- Command: `start-dev --import-realm` (development mode with automatic realm import)
|
|
- Realm import directory: `/opt/keycloak/data/import` mounted from `./infra/keycloak`
|
|
- Database credentials: separate `keycloak` user with `keycloakpass` (not production-safe, dev only)
|
|
- Health check uses curl to `/health/ready` endpoint (startup probe: 30s initial wait, 30 retries)
|
|
|
|
4. **Volume Management**
|
|
- Named volume `postgres-data` for persistent PostgreSQL storage
|
|
- Bind mount `./infra/keycloak` to `/opt/keycloak/data/import` for realm configuration
|
|
- Bind mount `./infra/postgres` to `/docker-entrypoint-initdb.d` for database initialization
|
|
|
|
5. **Service Discovery & Networking**
|
|
- All services on `app-network` bridge network
|
|
- Service names act as hostnames: `postgres:5432` for PostgreSQL, `localhost:8080` for Keycloak UI
|
|
- JDBC connection string in Keycloak: `jdbc:postgresql://postgres:5432/keycloak`
|
|
|
|
6. **Development vs Production**
|
|
- This configuration is dev-only: hardcoded credentials, start-dev mode, default admin user
|
|
- Security note: Keycloak admin credentials (admin/admin) and PostgreSQL passwords visible in plain text
|
|
- No TLS/HTTPS, no resource limits, no restart policies beyond defaults
|
|
- Future: Task 22 will add backend/frontend services to this compose file
|
|
|
|
### Configuration Files Created
|
|
|
|
- **docker-compose.yml**: 68 lines, v3.9 format with postgres + keycloak services
|
|
- **infra/postgres/init.sql**: Database initialization for workclub and keycloak databases
|
|
- **infra/keycloak/realm-export.json**: Placeholder realm (will be populated by Task 3)
|
|
|
|
### Environment Constraints
|
|
|
|
- Docker Compose CLI plugin not available in development environment
|
|
- Configuration validated against v3.9 spec structure
|
|
- YAML syntax verified via grep pattern matching
|
|
- Full integration testing deferred to actual Docker deployment
|
|
|
|
### Patterns & Conventions
|
|
|
|
- Use Alpine Linux images for smaller container footprints
|
|
- Health checks with appropriate startup periods and retry counts
|
|
- Ordered service startup via `depends_on` with health conditions
|
|
- Named volumes for persistent state, bind mounts for configuration
|
|
- Separate database users and passwords even in development (easier to migrate to secure configs)
|
|
|
|
### Gotchas to Avoid
|
|
|
|
- Keycloak startup takes 20-30 seconds even in dev mode (don't reduce retries)
|
|
- `/health/ready` is not the same as `/health/live` (use ready for startup confirmation)
|
|
- PostgreSQL in Alpine doesn't include common extensions by default (not needed yet)
|
|
- Keycloak password encoding: stored hashed in PostgreSQL, admin creds only in environment
|
|
- Missing realm-export.json or empty directory causes Keycloak to start but import silently fails
|
|
|
|
### Next Dependencies
|
|
|
|
- Task 3: Populate `realm-export.json` with actual Keycloak realm configuration
|
|
- Task 7: PostgreSQL migrations for Entity Framework Core
|
|
- Task 22: Add backend (Api, Application, Infrastructure services) and frontend to compose file
|
|
|
|
---
|
|
|
|
## Task 7: PostgreSQL Schema + EF Core Migrations + RLS Policies (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **Finbuckle.MultiTenant v9 → v10 Breaking Changes**
|
|
- **v9 API**: `IMultiTenantContextAccessor<TenantInfo>`, access via `.TenantInfo.Id`
|
|
- **v10 API**: `IMultiTenantContextAccessor` (non-generic), access via `.TenantInfo.Identifier`
|
|
- **Required Namespaces**:
|
|
- `using Finbuckle.MultiTenant.Abstractions;` (for TenantInfo type)
|
|
- `using Finbuckle.MultiTenant.Extensions;` (for AddMultiTenant)
|
|
- `using Finbuckle.MultiTenant.AspNetCore.Extensions;` (for UseMultiTenant middleware)
|
|
- **Constructor Injection**: Changed from `IMultiTenantContextAccessor<TenantInfo>` to `IMultiTenantContextAccessor`
|
|
- **Impact**: TenantProvider and both interceptors required updates
|
|
- **Version Used**: Finbuckle.MultiTenant.AspNetCore 10.0.3
|
|
|
|
2. **PostgreSQL xmin Concurrency Token Configuration**
|
|
- **Issue**: Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0 does NOT have `.UseXminAsConcurrencyToken()` extension method
|
|
- **Solution**: Manual configuration via Fluent API:
|
|
```csharp
|
|
builder.Property(e => e.RowVersion)
|
|
.IsRowVersion()
|
|
.HasColumnName("xmin")
|
|
.HasColumnType("xid")
|
|
.ValueGeneratedOnAddOrUpdate();
|
|
```
|
|
- **Entity Property Type**: Changed from `byte[]?` to `uint` for PostgreSQL xmin compatibility
|
|
- **Migration Output**: Correctly generates `xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)`
|
|
- **Applied To**: WorkItem and Shift entities (concurrency-sensitive aggregates)
|
|
|
|
3. **EF Core 10.x Interceptor Registration Pattern**
|
|
- **Registration**: Interceptors must be singletons for connection pooling safety
|
|
```csharp
|
|
builder.Services.AddSingleton<TenantDbConnectionInterceptor>();
|
|
builder.Services.AddSingleton<SaveChangesTenantInterceptor>();
|
|
```
|
|
- **DbContext Integration**: Use service provider to inject interceptors
|
|
```csharp
|
|
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
|
|
options.UseNpgsql(connectionString)
|
|
.AddInterceptors(
|
|
sp.GetRequiredService<TenantDbConnectionInterceptor>(),
|
|
sp.GetRequiredService<SaveChangesTenantInterceptor>()));
|
|
```
|
|
- **Why Service Provider**: Allows DI resolution of interceptor dependencies (IMultiTenantContextAccessor)
|
|
|
|
4. **Row-Level Security (RLS) Implementation**
|
|
- **SET LOCAL vs SET**: CRITICAL - use `SET LOCAL` (transaction-scoped) NOT `SET` (session-scoped)
|
|
- `SET` persists for entire session (dangerous with connection pooling)
|
|
- `SET LOCAL` resets at transaction commit (safe with connection pooling)
|
|
- **Implementation Location**: TenantDbConnectionInterceptor overrides ConnectionOpeningAsync
|
|
- **SQL Pattern**:
|
|
```csharp
|
|
command.CommandText = $"SET LOCAL app.current_tenant_id = '{tenantId}'";
|
|
```
|
|
- **RLS Policy Pattern**:
|
|
```sql
|
|
CREATE POLICY tenant_isolation ON table_name
|
|
FOR ALL
|
|
USING ("TenantId" = current_setting('app.current_tenant_id', true)::text);
|
|
```
|
|
- **current_setting Second Parameter**: `true` returns NULL instead of error when unset (prevents crashes)
|
|
|
|
5. **ShiftSignups RLS Special Case**
|
|
- **Issue**: ShiftSignups has no direct TenantId column (relates via Shift)
|
|
- **Solution**: Subquery pattern in RLS policy
|
|
```sql
|
|
CREATE POLICY tenant_isolation ON shift_signups
|
|
FOR ALL
|
|
USING ("ShiftId" IN (SELECT "Id" FROM shifts WHERE "TenantId" = current_setting('app.current_tenant_id', true)::text));
|
|
```
|
|
- **Why**: Maintains referential integrity while enforcing tenant isolation
|
|
- **Performance**: PostgreSQL optimizes subquery execution, minimal overhead
|
|
|
|
6. **Admin Bypass Pattern for RLS**
|
|
- **Purpose**: Allow migrations and admin operations to bypass RLS
|
|
- **SQL Pattern**:
|
|
```sql
|
|
CREATE POLICY bypass_rls_policy ON table_name
|
|
FOR ALL TO app_admin
|
|
USING (true);
|
|
```
|
|
- **Applied To**: All 5 tenant-scoped tables (clubs, members, work_items, shifts, shift_signups)
|
|
- **Admin Connection**: Use `Username=app_admin;Password=adminpass` for migrations
|
|
- **App Connection**: Use `Username=app_user;Password=apppass` for application (RLS enforced)
|
|
|
|
7. **Entity Type Configuration Pattern (EF Core)**
|
|
- **Approach**: Separate `IEntityTypeConfiguration<T>` classes (NOT Fluent API in OnModelCreating)
|
|
- **Benefits**:
|
|
- Single Responsibility: Each entity has its own configuration class
|
|
- Testability: Configuration classes can be unit tested
|
|
- Readability: No massive OnModelCreating method
|
|
- Discovery: `modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly)`
|
|
- **File Structure**: `Data/Configurations/ClubConfiguration.cs`, `MemberConfiguration.cs`, etc.
|
|
|
|
8. **Index Strategy for Multi-Tenant Tables**
|
|
- **TenantId Index**: CRITICAL - index on TenantId column for ALL tenant-scoped tables
|
|
```csharp
|
|
builder.HasIndex(e => e.TenantId);
|
|
```
|
|
- **Composite Indexes**:
|
|
- Members: `HasIndex(m => new { m.TenantId, m.Email })` (tenant-scoped user lookup)
|
|
- **Additional Indexes**:
|
|
- WorkItem: Status index for filtering (Open, Assigned, etc.)
|
|
- Shift: StartTime index for date-based queries
|
|
- **Why**: RLS policies filter by TenantId on EVERY query - without index, full table scans
|
|
|
|
9. **TDD Approach for Database Work**
|
|
- **Order**: Write tests FIRST, watch them FAIL, implement, watch them PASS
|
|
- **Test Files Created**:
|
|
- `MigrationTests.cs`: Verifies migration creates tables, indexes, RLS policies
|
|
- `RlsTests.cs`: Verifies tenant isolation, cross-tenant blocking, admin bypass
|
|
- **Test Infrastructure**: Testcontainers PostgreSQL (real database, not in-memory)
|
|
- **Dapper Requirement**: Tests use raw SQL via Dapper to verify RLS (bypasses EF Core)
|
|
|
|
10. **EF Core Version Alignment**
|
|
- **Issue**: API project had transitive EF Core 10.0.0, Infrastructure had 10.0.3 (from Design package)
|
|
- **Solution**: Added explicit `Microsoft.EntityFrameworkCore 10.0.3` and `Microsoft.EntityFrameworkCore.Design 10.0.3` to API project
|
|
- **Why**: Prevents version mismatch issues, ensures consistent EF Core behavior across projects
|
|
- **Package Versions**:
|
|
- Microsoft.EntityFrameworkCore: 10.0.3
|
|
- Microsoft.EntityFrameworkCore.Design: 10.0.3
|
|
- Npgsql.EntityFrameworkCore.PostgreSQL: 10.0.0 (latest stable)
|
|
|
|
### Files Created
|
|
|
|
**Infrastructure Layer**:
|
|
- `Data/AppDbContext.cs` — DbContext with DbSets for 5 entities
|
|
- `Data/Configurations/ClubConfiguration.cs` — Club entity configuration
|
|
- `Data/Configurations/MemberConfiguration.cs` — Member entity configuration
|
|
- `Data/Configurations/WorkItemConfiguration.cs` — WorkItem with xmin concurrency token
|
|
- `Data/Configurations/ShiftConfiguration.cs` — Shift with xmin concurrency token
|
|
- `Data/Configurations/ShiftSignupConfiguration.cs` — ShiftSignup configuration
|
|
- `Data/Interceptors/TenantDbConnectionInterceptor.cs` — SET LOCAL for RLS
|
|
- `Data/Interceptors/SaveChangesTenantInterceptor.cs` — Auto-assign TenantId
|
|
- `Migrations/20260303132952_InitialCreate.cs` — EF Core migration
|
|
- `Migrations/add-rls-policies.sql` — RLS policies SQL script
|
|
|
|
**Test Layer**:
|
|
- `Tests.Integration/Data/MigrationTests.cs` — Migration verification tests
|
|
- `Tests.Integration/Data/RlsTests.cs` — RLS isolation tests
|
|
|
|
### Files Modified
|
|
|
|
- `Domain/Entities/WorkItem.cs` — RowVersion: byte[]? → uint
|
|
- `Domain/Entities/Shift.cs` — RowVersion: byte[]? → uint
|
|
- `Infrastructure/Services/TenantProvider.cs` — Finbuckle v9 → v10 API
|
|
- `Api/Program.cs` — Interceptor registration + DbContext configuration
|
|
|
|
### Build Verification
|
|
|
|
✅ **Build Status**: ALL PROJECTS BUILD SUCCESSFULLY
|
|
- Command: `dotnet build WorkClub.slnx`
|
|
- Errors: 0
|
|
- Warnings: 6 (BouncyCastle.Cryptography security vulnerabilities from Testcontainers - transitive dependency, non-blocking)
|
|
- Projects: 6 (Domain, Application, Infrastructure, Api, Tests.Unit, Tests.Integration)
|
|
|
|
### Pending Tasks (Docker Environment Issue)
|
|
|
|
⏳ **Database setup blocked by Colima VM failure**:
|
|
- Issue: `failed to run attach disk "colima", in use by instance "colima"`
|
|
- Impact: Cannot start PostgreSQL container
|
|
- Workaround: Manual PostgreSQL installation or fix Colima/Docker environment
|
|
|
|
**Manual steps required (when Docker available)**:
|
|
1. Start PostgreSQL: `docker compose up -d postgres`
|
|
2. Apply migration: `cd backend && dotnet ef database update --project WorkClub.Infrastructure --startup-project WorkClub.Api`
|
|
3. Apply RLS: `psql -h localhost -U app_admin -d workclub -f backend/WorkClub.Infrastructure/Migrations/add-rls-policies.sql`
|
|
4. Run tests: `dotnet test backend/WorkClub.Tests.Integration --filter "FullyQualifiedName~MigrationTests|RlsTests"`
|
|
|
|
### Patterns & Conventions
|
|
|
|
1. **Connection Strings**:
|
|
- App user: `Host=localhost;Port=5432;Database=workclub;Username=app_user;Password=apppass`
|
|
- Admin user: `Host=localhost;Port=5432;Database=workclub;Username=app_admin;Password=adminpass`
|
|
|
|
2. **Interceptor Lifecycle**: Singletons (shared across all DbContext instances)
|
|
|
|
3. **RLS Policy Naming**: `tenant_isolation` for tenant filtering, `bypass_rls_policy` for admin bypass
|
|
|
|
4. **Migration Naming**: `YYYYMMDDHHMMSS_Description` format (EF Core default)
|
|
|
|
5. **Test Organization**: `Tests.Integration/Data/` for database-related tests
|
|
|
|
### Gotchas Avoided
|
|
|
|
- ❌ **DO NOT** use `SET` (session-scoped) — MUST use `SET LOCAL` (transaction-scoped)
|
|
- ❌ **DO NOT** use `UseXminAsConcurrencyToken()` extension (doesn't exist in Npgsql 10.x)
|
|
- ❌ **DO NOT** use `byte[]` for xmin (PostgreSQL xmin is uint/xid type)
|
|
- ❌ **DO NOT** forget second parameter in `current_setting('key', true)` (prevents errors when unset)
|
|
- ❌ **DO NOT** register interceptors as scoped/transient (must be singleton for connection pooling)
|
|
- ❌ **DO NOT** apply RLS to non-tenant tables (global tables like system config)
|
|
- ❌ **DO NOT** use Fluent API in OnModelCreating (use IEntityTypeConfiguration<T> classes)
|
|
|
|
### Security Notes
|
|
|
|
✅ **Transaction-Scoped RLS**: Using `SET LOCAL` prevents tenant leakage across connections in connection pool
|
|
✅ **Admin Bypass**: Separate admin role with unrestricted RLS policies for migrations
|
|
✅ **Subquery Pattern**: ShiftSignups RLS enforces tenant isolation via related Shift entity
|
|
✅ **Index Coverage**: TenantId indexed on all tenant tables for query performance
|
|
|
|
### Next Dependencies
|
|
|
|
- **Task 8**: Repository pattern implementation (depends on AppDbContext)
|
|
- **Task 9**: JWT authentication middleware (depends on TenantProvider)
|
|
- **Task 12**: API endpoint implementation (depends on repositories)
|
|
- **DO NOT COMMIT YET**: Task 7 and Task 8 will be committed together per directive
|
|
|
|
### Evidence Files
|
|
|
|
- `.sisyphus/evidence/task-7-build-success.txt` — Build verification output
|
|
|
|
---
|
|
|
|
## Task 10: NextAuth.js Keycloak Integration - COMPLETED (2026-03-03)
|
|
|
|
### What Was Delivered
|
|
|
|
**Core Files Created**:
|
|
- `frontend/src/middleware.ts` - NextAuth-based route protection
|
|
- `frontend/src/hooks/useActiveClub.ts` - Active club context management
|
|
- `frontend/src/lib/api.ts` - Fetch wrapper with auto-injected auth headers
|
|
- `frontend/vitest.config.ts` - Vitest test configuration
|
|
- `frontend/src/test/setup.ts` - Global test setup with localStorage mock
|
|
- `frontend/src/hooks/__tests__/useActiveClub.test.ts` - 7 passing tests
|
|
- `frontend/src/lib/__tests__/api.test.ts` - 9 passing tests
|
|
|
|
**Testing Infrastructure**:
|
|
- Vitest v4.0.18 with happy-dom environment
|
|
- @testing-library/react for React hooks testing
|
|
- Global localStorage mock in setup file
|
|
- 16/16 tests passing
|
|
|
|
### Auth.js v5 Patterns Discovered
|
|
|
|
**Middleware in Next.js 16**:
|
|
- Next.js 16 deprecates `middleware.ts` in favor of `proxy.ts` (warning displayed)
|
|
- Still works as middleware for now but migration path exists
|
|
- Must use `auth()` function from auth config, NOT `useSession()` (server-side only)
|
|
- Matcher pattern excludes Next.js internals: `/((?!_next/static|_next/image|favicon.ico|.*\\..*|api/auth).*)`
|
|
|
|
**Client vs Server Patterns**:
|
|
- `useSession()` hook: client components only (requires SessionProvider wrapper)
|
|
- `getSession()` function: can be called anywhere, returns Promise<Session | null>
|
|
- `auth()` function: server-side only (middleware, server components, API routes)
|
|
|
|
**API Client Design**:
|
|
- Cannot use React hooks in utility functions
|
|
- Use `getSession()` from 'next-auth/react' for async session access
|
|
- Read localStorage directly with `typeof window !== 'undefined'` check
|
|
- Headers must be `Record<string, string>` not `HeadersInit` for type safety
|
|
|
|
### Vitest Testing with Next-Auth
|
|
|
|
**Mock Strategy**:
|
|
```typescript
|
|
const mockUseSession = vi.fn();
|
|
vi.mock('next-auth/react', () => ({
|
|
useSession: () => mockUseSession(),
|
|
}));
|
|
```
|
|
This allows per-test override with `mockUseSession.mockReturnValueOnce({...})`
|
|
|
|
**localStorage Mock**:
|
|
- Must be set up in global test setup file
|
|
- Use closure to track state: `let localStorageData: Record<string, string> = {}`
|
|
- Mock getItem/setItem to read/write from closure object
|
|
- Reset in beforeEach with proper mock implementation
|
|
|
|
**Vitest with Bun**:
|
|
- Run with `./node_modules/.bin/vitest` NOT `bun test`
|
|
- Bun's test runner doesn't load vitest config properly
|
|
- Add npm scripts: `"test": "vitest run"`, `"test:watch": "vitest"`
|
|
|
|
### TypeScript Strict Mode Issues
|
|
|
|
**HeadersInit Indexing**:
|
|
```typescript
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string>),
|
|
};
|
|
```
|
|
Cannot use `HeadersInit` type and index with string keys. Must cast to `Record<string, string>`.
|
|
|
|
**Type Augmentation Location**:
|
|
- Module augmentation for next-auth types must be in auth.ts file
|
|
- `declare module "next-auth"` block extends Session and JWT interfaces
|
|
- Custom claims like `clubs` must be added to both JWT and Session types
|
|
|
|
### Middleware Route Protection
|
|
|
|
**Public Routes Strategy**:
|
|
- Explicit allowlist: `['/', '/login']`
|
|
- Auth routes: paths starting with `/api/auth`
|
|
- All other routes require authentication
|
|
- Redirect to `/login?callbackUrl=<pathname>` for unauthenticated requests
|
|
|
|
**Performance Note**:
|
|
- Middleware runs on EVERY request (including static assets if not excluded)
|
|
- Matcher pattern critical for performance
|
|
- Exclude: `_next/static`, `_next/image`, `favicon.ico`, file extensions, `api/auth/*`
|
|
|
|
### Active Club Management
|
|
|
|
**localStorage Pattern**:
|
|
- Key: `'activeClubId'`
|
|
- Fallback to first club in session.user.clubs if localStorage empty
|
|
- Validate stored ID exists in session clubs (prevent stale data)
|
|
- Update localStorage on explicit `setActiveClub()` call
|
|
|
|
**Hook Implementation**:
|
|
- React hook with `useSession()` and `useState` + `useEffect`
|
|
- Returns: `{ activeClubId, role, clubs, setActiveClub }`
|
|
- Role derived from `clubs[activeClubId]` (Keycloak club roles)
|
|
- Null safety: returns null when no session or no clubs
|
|
|
|
### API Client Auto-Headers
|
|
|
|
**Authorization Header**:
|
|
- Format: `Bearer ${session.accessToken}`
|
|
- Only added if session exists and has accessToken
|
|
- Uses Auth.js HTTP-only cookie session by default
|
|
|
|
**X-Tenant-Id Header**:
|
|
- Reads from localStorage directly (not hook-based)
|
|
- Only added if activeClubId exists
|
|
- Backend expects this for RLS context
|
|
|
|
**Header Merging**:
|
|
- Default `Content-Type: application/json`
|
|
- Spread user-provided headers AFTER defaults (allows override)
|
|
- Cast to `Record<string, string>` for type safety
|
|
|
|
### Testing Discipline Applied
|
|
|
|
**TDD Flow**:
|
|
1. Write failing test first
|
|
2. Implement minimal code to pass
|
|
3. Refactor while keeping tests green
|
|
4. All 16 tests written before implementation
|
|
|
|
**Test Coverage**:
|
|
- useActiveClub: localStorage read, fallback, validation, switching, null cases
|
|
- apiClient: header injection, merging, overriding, conditional headers
|
|
- Both positive and negative test cases
|
|
|
|
### Build Verification
|
|
|
|
**Next.js Build**:
|
|
- ✅ TypeScript compilation successful
|
|
- ✅ No type errors in new files
|
|
- ✅ Static generation works (4 pages)
|
|
- ⚠️ Middleware deprecation warning (Next.js 16 prefers "proxy")
|
|
|
|
**Test Suite**:
|
|
- ✅ 16/16 tests passing
|
|
- ✅ Test duration: ~12ms (fast unit tests)
|
|
- ✅ No setup/teardown leaks
|
|
|
|
### Integration Points
|
|
|
|
**Auth Flow**:
|
|
1. User authenticates via Keycloak (Task 9)
|
|
2. Auth.js stores session with clubs claim
|
|
3. Middleware protects routes based on session
|
|
4. useActiveClub provides club context to components
|
|
5. apiClient auto-injects auth + tenant headers
|
|
|
|
**Multi-Tenancy**:
|
|
- Frontend: X-Tenant-Id header from active club
|
|
- Backend: TenantProvider reads header for RLS (Task 7)
|
|
- Session: Keycloak clubs claim maps to club roles
|
|
|
|
### Gotchas and Warnings
|
|
|
|
1. **Cannot use hooks in utility functions** - Use getSession() instead of useSession()
|
|
2. **localStorage only works client-side** - Check `typeof window !== 'undefined'`
|
|
3. **Vitest setup must be configured** - setupFiles in vitest.config.ts
|
|
4. **Mock localStorage properly** - Use closure to track state across tests
|
|
5. **HeadersInit is readonly** - Cast to Record<string, string> for indexing
|
|
6. **Middleware runs on every request** - Use matcher to exclude static assets
|
|
7. **Next.js 16 middleware deprecation** - Plan migration to proxy.ts
|
|
|
|
### Dependencies
|
|
|
|
**Installed Packages**:
|
|
- vitest@4.0.18 (test runner)
|
|
- @testing-library/react@16.3.2 (React hooks testing)
|
|
- @testing-library/jest-dom@6.9.1 (DOM matchers)
|
|
- @vitejs/plugin-react@5.1.4 (Vite React plugin)
|
|
- happy-dom@20.8.3 (DOM environment for tests)
|
|
|
|
**Already Present**:
|
|
- next-auth@5.0.0-beta.30 (Auth.js v5)
|
|
- @auth/core@0.34.3 (Auth.js core)
|
|
|
|
### Next Steps
|
|
|
|
- **Task 11**: shadcn/ui component setup (independent)
|
|
- **Task 12**: API endpoint implementation (depends on Task 8 repositories)
|
|
- **Task 13**: Dashboard page with club selector (depends on Task 10 hooks)
|
|
|
|
### Evidence Files
|
|
|
|
- `.sisyphus/evidence/task-10-tests.txt` — All 16 tests passing
|
|
- `.sisyphus/evidence/task-10-build.txt` — Successful Next.js build
|
|
|
|
---
|