- Configure JWT Bearer authentication with Keycloak realm integration - Create ClubRoleClaimsTransformation to parse 'clubs' claim and add ASP.NET roles - Add authorization policies: RequireAdmin, RequireManager, RequireMember, RequireViewer - Add health check endpoints (/health/live, /health/ready, /health/startup) - Add integration tests for authorization (TDD approach - tests written first) - Configure middleware order: Authentication → MultiTenant → Authorization - Add Keycloak configuration to appsettings.Development.json - Add AspNetCore.HealthChecks.NpgSql v9.0.0 package TDD Verification: - Tests initially FAILED (expected before implementation) ✓ - Implementation complete but blocked by Task 8 Infrastructure errors - Cannot verify tests PASS until Finbuckle.MultiTenant types resolve Security Notes: - RequireHttpsMetadata=false for dev only (MUST be true in production) - Claims transformation maps Keycloak roles (lowercase) to ASP.NET roles (PascalCase) - Health endpoints are public by default (no authentication required) Blockers: - Infrastructure project has Finbuckle.MultiTenant type resolution errors from Task 8 - Tests cannot execute until TenantProvider compilation errors are fixed
1419 lines
66 KiB
Markdown
1419 lines
66 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 4: Domain Entities & State Machine (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **TDD Approach Workflow**
|
|
- Write tests FIRST (even when entities don't exist — LSP errors expected)
|
|
- Create minimal entities to satisfy test requirements
|
|
- All 12 tests passed on first run after implementation
|
|
- This validates clean state machine design
|
|
|
|
2. **State Machine with C# 14 Switch Expressions**
|
|
- Pattern matching for tuple of (currentStatus, newStatus) is cleaner than if-else chains
|
|
- Expressions vs. traditional switch: more functional, concise, easier to verify all transitions
|
|
- Chosen over Dictionary<(status, status), bool> because:
|
|
- Easier to read and maintain
|
|
- Compiler can verify exhaustiveness (with `_ => false` fallback)
|
|
- No runtime lookup overhead
|
|
- Clear inline state diagram
|
|
|
|
3. **Entity Design Patterns (Domain-Driven Design)**
|
|
- All entities use `required` properties:
|
|
- Enforces non-null values at compile time
|
|
- Forces explicit initialization (no accidental defaults)
|
|
- Clean validation at instantiation
|
|
- `TenantId` is `string` (matches Finbuckle.MultiTenant.ITenantInfo.Id type)
|
|
- Foreign keys use explicit `Guid` or `Guid?` (not navigation properties yet)
|
|
- `RowVersion: byte[]?` for optimistic concurrency (EF Core `[Timestamp]` attribute in Task 7)
|
|
|
|
4. **DateTimeOffset vs DateTime**
|
|
- Used DateTimeOffset for CreatedAt/UpdatedAt (includes timezone offset)
|
|
- Better for multi-tenant global apps (know exact UTC moment)
|
|
- .NET 10 standard for timestamp columns
|
|
- Avoids timezone confusion across regions
|
|
|
|
5. **ITenantEntity Interface Pattern**
|
|
- Explicit interface property (not EF shadow property) allows:
|
|
- Easy data seeding in tests
|
|
- LINQ queries without special knowledge
|
|
- Clear contract in domain code
|
|
- Marker interface only (no methods) — true DDD boundary
|
|
|
|
6. **Entity Lifecycle Simplicity**
|
|
- No domain events (YAGNI for MVP)
|
|
- No navigation properties (deferred to EF configuration in Task 7)
|
|
- No validation attributes — EF Fluent API handles in Task 7
|
|
- State machine is only behavior (business rule enforcement)
|
|
|
|
7. **Test Structure for TDD**
|
|
- Helper factory method `CreateWorkItem()` reduces repetition
|
|
- AAA pattern (Arrange-Act-Assert) clear in test names
|
|
- Arrange: Create entity with minimal valid state
|
|
- Act: Call transition method
|
|
- Assert: Verify state changed or exception thrown
|
|
- xUnit [Fact] attributes sufficient (no [Theory] needed for now)
|
|
|
|
8. **Project Structure Observations**
|
|
- Projects in `backend/` root, not `backend/src/` (deviation from typical convention but works)
|
|
- Subdirectories: Entities/, Enums/, Interfaces/ (clean separation)
|
|
- Test mirrors source structure: Domain tests in dedicated folder
|
|
- Class1.cs stub removed before implementation
|
|
|
|
### Files Created
|
|
|
|
- `WorkClub.Domain/Enums/SportType.cs` — 5 values
|
|
- `WorkClub.Domain/Enums/ClubRole.cs` — 4 values
|
|
- `WorkClub.Domain/Enums/WorkItemStatus.cs` — 5 values
|
|
- `WorkClub.Domain/Interfaces/ITenantEntity.cs` — Marker interface
|
|
- `WorkClub.Domain/Entities/Club.cs` — Basic aggregate root
|
|
- `WorkClub.Domain/Entities/Member.cs` — User representation
|
|
- `WorkClub.Domain/Entities/WorkItem.cs` — Task with state machine
|
|
- `WorkClub.Domain/Entities/Shift.cs` — Volunteer shift
|
|
- `WorkClub.Domain/Entities/ShiftSignup.cs` — Shift registration
|
|
- `WorkClub.Tests.Unit/Domain/WorkItemStatusTests.cs` — 12 tests, all passing
|
|
|
|
### Build & Test Results
|
|
|
|
- **Tests**: 12/12 passed (100%)
|
|
- 5 valid transition tests ✓
|
|
- 5 invalid transition tests ✓
|
|
- 2 CanTransitionTo() method tests ✓
|
|
- **Build**: Release configuration successful
|
|
- **Warnings**: Only Finbuckle version resolution (expected, no errors)
|
|
|
|
### Next Steps (Tasks 5-7)
|
|
|
|
- Task 5: Next.js frontend (parallel)
|
|
- Task 6: Kustomize deployment (parallel)
|
|
- Task 7: EF Core DbContext with Fluent API configuration (blocks on these entities)
|
|
- Add [Timestamp] attribute to RowVersion properties
|
|
- Configure ITenantEntity filtering in DbContext
|
|
- Set up relationships between entities
|
|
- Configure PostgreSQL xmin concurrency token
|
|
|
|
|
|
---
|
|
|
|
## Task 6: Kubernetes Kustomize Base Manifests (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **Kustomize vs Helm Trade-offs**
|
|
- Kustomize chosen: lightweight, YAML-native, no templating language
|
|
- Base + overlays pattern: separate environment-specific config from base
|
|
- Base manifests use placeholders for image tags (`:latest`), resource limits (100m/256Mi requests)
|
|
- Environment overlays (dev, staging, prod) override via patches/replacements
|
|
|
|
2. **Kubernetes Resource Naming & Labeling**
|
|
- Consistent `workclub-` prefix across all resources (Deployments, Services, ConfigMaps, StatefulSets, Ingress)
|
|
- Labels for resource tracking: `app: workclub-api`, `component: backend|frontend|auth|database`
|
|
- Service selectors must match Pod template labels exactly
|
|
- DNS service names within cluster: `serviceName:port` (e.g., `workclub-api:80`)
|
|
|
|
3. **.NET Health Probes (ASP.NET Core Health Checks)**
|
|
- Three distinct probes with different semantics:
|
|
- `startupProbe` (/health/startup): Initial boot, longer timeout (30s retries), prevents traffic until app fully initialized
|
|
- `livenessProbe` (/health/live): Periodic health (15s), restart pod if fails continuously (3 failures)
|
|
- `readinessProbe` (/health/ready): Pre-request check (10s), removes pod from service on failure (2 failures)
|
|
- Startup probe MUST complete before liveness/readiness are checked
|
|
- All three probes return `200 OK` for healthy status
|
|
|
|
4. **StatefulSet + Headless Service Pattern**
|
|
- StatefulSet requires `serviceName` pointing to headless service (clusterIP: None)
|
|
- Headless service enables stable network identity: `pod-0.serviceName.namespace.svc.cluster.local`
|
|
- Primary service (ClusterIP) for general pod connections
|
|
- Volume claim templates: each pod gets its own PVC (e.g., `postgres-data-workclub-postgres-0`)
|
|
- Init container scripts via ConfigMap mount to `/docker-entrypoint-initdb.d`
|
|
|
|
5. **PostgreSQL StatefulSet Configuration**
|
|
- Image: `postgres:16-alpine` (lightweight, 150MB vs 400MB+)
|
|
- Health check: `pg_isready -U app -d workclub` (simple, fast, reliable)
|
|
- Data persistence: volumeClaimTemplate with 10Gi storage, `standard` storageClassName (overrideable in overlay)
|
|
- Init script creates both `workclub` (app) and `keycloak` databases + users in single ConfigMap
|
|
|
|
6. **Keycloak 26.x Production Mode**
|
|
- Image: `quay.io/keycloak/keycloak:26.1` (Red Hat official registry)
|
|
- Command: `start` (production mode, not `start-dev`)
|
|
- Database: PostgreSQL via `KC_DB=postgres` + `KC_DB_URL_HOST=workclub-postgres`
|
|
- Probes: `/health/ready` (readiness), `/health/live` (liveness)
|
|
- Hostname: `KC_HOSTNAME_STRICT=false` in dev (allows any Host header)
|
|
- Proxy: `KC_PROXY=edge` for behind reverse proxy (Ingress)
|
|
|
|
7. **Ingress Path-Based Routing**
|
|
- Single ingress rule: `workclub-ingress` with path-based routing
|
|
- Frontend: path `/` → `workclub-frontend:80` (pathType: Prefix)
|
|
- Backend: path `/api` → `workclub-api:80` (pathType: Prefix)
|
|
- Host: `localhost` (overrideable per environment)
|
|
- TLS: deferred to production overlay (cert-manager, letsencrypt)
|
|
|
|
8. **ConfigMap Strategy for Non-Sensitive Configuration**
|
|
- Central `workclub-config` ConfigMap:
|
|
- `log-level: Information`
|
|
- `cors-origins: http://localhost:3000`
|
|
- `api-base-url: http://workclub-api`
|
|
- `keycloak-url: http://workclub-keycloak`
|
|
- `keycloak-realm: workclub`
|
|
- Database host/port/name
|
|
- Sensitive values (passwords, connection strings) → Secrets (not in base)
|
|
- Environment-specific overrides in dev/prod overlays (CORS_ORIGINS changes)
|
|
|
|
9. **Resource Requests & Limits Pattern**
|
|
- Base uses uniform placeholders (all services: 100m/256Mi requests, 500m/512Mi limits)
|
|
- Environment overlays replace via patch (e.g., prod: 500m/2Gi)
|
|
- Prevents resource contention in shared clusters
|
|
- Allows gradual scaling experiments without manifests changes
|
|
|
|
10. **Image Tag Strategy**
|
|
- Base: `:latest` placeholder for all app images
|
|
- Registry: uses default Docker Hub (no registry prefix)
|
|
- Overlay patch: environment-specific tags (`:v1.2.3`, `:latest-dev`, `:sha-abc123`)
|
|
- Image pull policy: `IfNotPresent` (caching optimization for stable envs)
|
|
|
|
### Architecture Decisions
|
|
|
|
- **Why Kustomize over Helm**: Plan explicitly avoids Helm (simpler YAML, no new DSL, easier Git diffs)
|
|
- **Why base + overlays**: Separation of concerns — base is declarative truth, overlays add environment context
|
|
- **Why two Postgres services**: Headless for StatefulSet DNS (stable identity), Primary for app connections (load balancing)
|
|
- **Why both startup + liveness probes**: Prevents restart loops during slow startup (Java/Keycloak can take 20+ seconds)
|
|
- **Why ConfigMap for init.sql**: Immutable config, easier than baked-into-image, updateable per environment
|
|
|
|
### Gotchas to Avoid
|
|
|
|
- Forgetting `serviceName` in StatefulSet causes pod DNS discovery failure (critical for Postgres)
|
|
- Missing headless service's `publishNotReadyAddresses: true` prevents pod-to-pod startup communication
|
|
- Keycloak startup probe timeout too short (<15s retries) causes premature restart loops
|
|
- `.NET health endpoints require HttpGet, not TCP probes (TCP only checks port, not app readiness)
|
|
- Ingress path `/api` must use `pathType: Prefix` to catch `/api/*` routes
|
|
|
|
### Next Steps
|
|
|
|
- Task 25: Create dev overlay (env-specific values, dev-db.postgres.svc, localhost ingress)
|
|
- Task 26: Create prod overlay (TLS config, resource limits, replica counts, PDB)
|
|
- Task 27: Add cert-manager + Let's Encrypt to prod
|
|
- Future: Network policies, pod disruption budgets, HPA (deferred to Wave 2)
|
|
|
|
|
|
---
|
|
|
|
## Task 5: Next.js 15 Project Initialization (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **Next.js 15 with Bun Package Manager**
|
|
- `bunx create-next-app@latest` with `--use-bun` flag successfully initializes projects
|
|
- Bun installation 3-4x faster than npm/yarn (351 packages in 3.4s)
|
|
- Next.js 16.1.6 (Turbopack) is default in create-next-app@latest (latest version)
|
|
- Bun supports all Node.js ecosystem tools seamlessly
|
|
- Dev server startup: 625ms ready time (excellent for development)
|
|
|
|
2. **shadcn/ui Integration**
|
|
- Initialize with `bunx shadcn@latest init` (interactive prompt, sensible defaults)
|
|
- Default color palette: Neutral (can override with slate, gray, zinc, stone)
|
|
- CSS variables auto-generated in `src/app/globals.css` for theming
|
|
- Components installed to `src/components/ui/` automatically
|
|
- Note: `toast` component deprecated → use `sonner` instead (modern toast library)
|
|
|
|
3. **Standalone Output Configuration**
|
|
- Set `output: 'standalone'` in `next.config.ts` for Docker deployments
|
|
- Generates `.next/standalone/` with self-contained server.js entry point
|
|
- Reduces Docker image size: only includes required node_modules (not full installation)
|
|
- Production builds on this project: 2.9s compile, 240.4ms static page generation
|
|
- Standalone directory structure: `.next/`, `node_modules/`, `server.js`, `package.json`
|
|
|
|
4. **TypeScript Path Aliases**
|
|
- `@/*` → `./src/*` pre-configured in `tsconfig.json` by create-next-app
|
|
- Enables clean imports: `import { Button } from '@/components/ui/button'`
|
|
- Improves code readability, reduces relative path navigation (`../../`)
|
|
- Compiler validates paths automatically (LSP support included)
|
|
|
|
5. **Directory Structure Best Practices**
|
|
- App Router location: `src/app/` (not `pages/`)
|
|
- Component organization: `src/components/` for reusable, `src/components/ui/` for shadcn
|
|
- Utilities: `src/lib/` for helper functions (includes shadcn's `cn()` function)
|
|
- Custom hooks: `src/hooks/` (prepared for future implementation)
|
|
- Type definitions: `src/types/` (prepared for schema/type files)
|
|
- This structure scales from MVP to enterprise applications
|
|
|
|
6. **Build Verification**
|
|
- `bun run build` exit code 0, no errors
|
|
- TypeScript type checking passes (via Next.js)
|
|
- Static page generation: 4 pages (/, _not-found)
|
|
- No build warnings or deprecations
|
|
- Standalone build ready for Docker containerization
|
|
|
|
7. **Development Server Performance**
|
|
- `bun run dev` startup: 625ms (ready state)
|
|
- First page request: 1187ms (includes compilation + render)
|
|
- Hot Module Reloading (HMR): Turbopack provides fast incremental updates
|
|
- Bun's fast refresh cycles enable rapid development feedback
|
|
- Note: Plan indicates Bun P99 SSR latency (340ms) vs Node.js (120ms), so production deployment will use Node.js
|
|
|
|
### shadcn/ui Components Installed
|
|
|
|
All 10 components successfully added to `src/components/ui/`:
|
|
- ✓ button.tsx — Base button component with variants (primary, secondary, etc.)
|
|
- ✓ card.tsx — Card layout container (Card, CardHeader, CardFooter, etc.)
|
|
- ✓ badge.tsx — Status badges with color variants
|
|
- ✓ input.tsx — Form input field with placeholder and error support
|
|
- ✓ label.tsx — Form label with accessibility attributes
|
|
- ✓ select.tsx — Dropdown select with options (Radix UI based)
|
|
- ✓ dialog.tsx — Modal dialog component (Alert Dialog pattern)
|
|
- ✓ dropdown-menu.tsx — Context menu/dropdown menu (Radix UI based)
|
|
- ✓ table.tsx — Data table with thead, tbody, rows
|
|
- ✓ sonner.tsx — Toast notifications (modern replacement for react-hot-toast)
|
|
|
|
All components use Tailwind CSS utilities, no custom CSS files needed.
|
|
|
|
### Environment Variables Configuration
|
|
|
|
Created `.env.local.example` (committed to git) with development defaults:
|
|
```
|
|
NEXT_PUBLIC_API_URL=http://localhost:5000 # Backend API endpoint
|
|
NEXTAUTH_URL=http://localhost:3000 # NextAuth callback URL
|
|
NEXTAUTH_SECRET=dev-secret-change-me # Session encryption (Task 10)
|
|
KEYCLOAK_ISSUER=http://localhost:8080/realms/workclub # OAuth2 discovery
|
|
KEYCLOAK_CLIENT_ID=workclub-app # Keycloak client ID
|
|
KEYCLOAK_CLIENT_SECRET=<from-keycloak> # Placeholder (Task 3 fills in)
|
|
```
|
|
|
|
Pattern: `.env.local.example` is version-controlled, `.env.local` is gitignored per `.gitignore`.
|
|
|
|
### Dependencies Installed
|
|
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"next": "16.1.6",
|
|
"react": "19.2.3",
|
|
"react-dom": "19.2.3"
|
|
},
|
|
"devDependencies": {
|
|
"@tailwindcss/postcss": "4.2.1",
|
|
"@types/node": "20.19.35",
|
|
"@types/react": "19.2.14",
|
|
"@types/react-dom": "19.2.3",
|
|
"eslint": "9.39.3",
|
|
"eslint-config-next": "16.1.6",
|
|
"tailwindcss": "4.2.1",
|
|
"typescript": "5.9.3"
|
|
}
|
|
}
|
|
```
|
|
|
|
Note: Intentionally minimal dependencies for MVP. NextAuth.js added in Task 10.
|
|
|
|
### Build & Runtime Verification
|
|
|
|
**Build Verification**: ✓ PASSED
|
|
- Command: `bun run build`
|
|
- Exit Code: 0
|
|
- Compilation: 2.9s (Turbopack)
|
|
- TypeScript: No errors
|
|
- Static Generation: 4 pages in 240.4ms
|
|
- Output: `.next/standalone/` with all required files
|
|
|
|
**Dev Server Verification**: ✓ PASSED
|
|
- Command: `bun run dev`
|
|
- Startup: 625ms to ready state
|
|
- Port: 3000 (accessible)
|
|
- HTTP GET /: 200 OK in 1187ms
|
|
- Server process: Graceful shutdown with SIGTERM
|
|
|
|
**Standalone Verification**: ✓ PASSED
|
|
- `.next/standalone/server.js`: 6.55 KB entry point
|
|
- `.next/standalone/node_modules/`: Self-contained dependencies
|
|
- `.next/standalone/package.json`: Runtime configuration
|
|
- `.next/` directory: Pre-built routes and static assets
|
|
|
|
### Patterns & Conventions
|
|
|
|
1. **Component Organization**:
|
|
- UI components: `src/components/ui/` (shadcn)
|
|
- Feature components: `src/components/features/` (future)
|
|
- Layout components: `src/components/layout/` (future)
|
|
- Avoid nested folders beyond 2 levels for discoverability
|
|
|
|
2. **TypeScript Strict Mode**:
|
|
- `tsconfig.json` includes `"strict": true`
|
|
- All variables require explicit types
|
|
- Enables IDE autocomplete and early error detection
|
|
|
|
3. **Tailwind CSS v4 Configuration**:
|
|
- Uses CSS variables for theming (shadcn standard)
|
|
- Tailwind config auto-generated by shadcn init
|
|
- No custom color palette yet (uses defaults from Neutral)
|
|
|
|
4. **Git Strategy**:
|
|
- `.env.local.example` is committed (template for developers)
|
|
- `.env.local` is in `.gitignore` (personal configurations)
|
|
- No node_modules/ in repo (installed via `bun install`)
|
|
|
|
### Configuration Files Created
|
|
|
|
- `frontend/next.config.ts` — Minimal, standalone output enabled
|
|
- `frontend/tsconfig.json` — Path aliases, strict TypeScript mode
|
|
- `frontend/.env.local.example` — Environment variable template
|
|
- `frontend/components.json` — shadcn/ui configuration
|
|
- `frontend/tailwind.config.ts` — Tailwind CSS configuration with Tailwind v4
|
|
- `frontend/postcss.config.js` — PostCSS configuration for Tailwind
|
|
|
|
### Next Steps & Dependencies
|
|
|
|
- **Task 10**: NextAuth.js integration
|
|
- Adds `next-auth` dependency
|
|
- Creates `src/app/api/auth/[...nextauth]/route.ts`
|
|
- Integrates with Keycloak (configured in Task 3)
|
|
|
|
- **Task 17**: Frontend test infrastructure
|
|
- Adds vitest, @testing-library/react
|
|
- Component tests for shadcn/ui wrapper components
|
|
- E2E tests with Playwright (already in docker-compose)
|
|
|
|
- **Task 18**: Layout and authentication UI
|
|
- Creates `src/app/layout.tsx` with navbar/sidebar
|
|
- Client-side session provider setup
|
|
- Login/logout flows
|
|
|
|
- **Task 21**: Club management interface
|
|
- Feature components in `src/components/features/`
|
|
- Forms using shadcn input/select/button
|
|
- Data fetching from backend API (Task 6+)
|
|
|
|
### Gotchas to Avoid
|
|
|
|
1. **Bun vs Node.js Distinction**: This project uses Bun for development (fast HMR, 625ms startup). Production deployment will use Node.js due to P99 latency concerns (documented in plan).
|
|
|
|
2. **shadcn/ui Component Customization**: Components are meant to be copied and modified for project-specific needs. Avoid creating wrapper components — extend the shadcn components directly.
|
|
|
|
3. **Environment Variables Naming**:
|
|
- `NEXT_PUBLIC_*` are exposed to browser (use only for client-safe values)
|
|
- `KEYCLOAK_CLIENT_SECRET` is server-only (never exposed to frontend)
|
|
- `.env.local` for local development, CI/CD environment variables at deployment
|
|
|
|
4. **Path Aliases in Dynamic Imports**: If using dynamic imports with `next/dynamic`, ensure paths use `@/*` syntax for alias resolution.
|
|
|
|
5. **Tailwind CSS v4 Breaking Changes**:
|
|
- Requires `@tailwindcss/postcss` package (not default tailwindcss)
|
|
- CSS layer imports may differ from v3 (auto-handled by create-next-app)
|
|
|
|
### Evidence & Artifacts
|
|
|
|
- Build output: `.sisyphus/evidence/task-5-nextjs-build.txt`
|
|
- Dev server output: `.sisyphus/evidence/task-5-dev-server.txt`
|
|
- Git commit: `chore(frontend): initialize Next.js project with Tailwind and shadcn/ui`
|
|
|
|
## Task 3: Keycloak Realm Configuration (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **Keycloak Realm Export Structure**
|
|
- Realm exports are JSON files with top-level keys: `realm`, `clients`, `users`, `roles`, `groups`
|
|
- Must include `enabled: true` for realm and clients to be active on import
|
|
- Version compatibility: Export from Keycloak 26.x is compatible with 26.x imports
|
|
- Import command: `start-dev --import-realm` (Docker volume mount required)
|
|
|
|
2. **Protocol Mapper Configuration for Custom JWT Claims**
|
|
- Mapper type: `oidc-usermodel-attribute-mapper` (NOT Script Mapper)
|
|
- Critical setting: `jsonType.label: JSON` ensures claim is parsed as JSON object (not string)
|
|
- User attribute: `clubs` (custom attribute on user entity)
|
|
- Token claim name: `clubs` (appears in JWT payload)
|
|
- Must include in: ID token, access token, userinfo endpoint (all three flags set to true)
|
|
- Applied to both clients: workclub-api and workclub-app (defined in client protocolMappers array)
|
|
|
|
3. **Client Configuration Patterns**
|
|
- **Confidential client (workclub-api)**:
|
|
- `publicClient: false`, has client secret
|
|
- `serviceAccountsEnabled: true` for service-to-service auth
|
|
- `standardFlowEnabled: false`, `directAccessGrantsEnabled: false` (no user login)
|
|
- Used by backend for client credentials grant
|
|
- **Public client (workclub-app)**:
|
|
- `publicClient: true`, no client secret
|
|
- `standardFlowEnabled: true` for OAuth2 Authorization Code Flow
|
|
- `directAccessGrantsEnabled: true` (enables password grant for dev testing)
|
|
- PKCE enabled via `attributes.pkce.code.challenge.method: S256`
|
|
- Redirect URIs: `http://localhost:3000/*` (wildcard for dev)
|
|
- Web origins: `http://localhost:3000` (CORS configuration)
|
|
|
|
4. **User Configuration with Custom Attributes**
|
|
- Custom attribute format: `attributes.clubs: ["{\"club-1-uuid\": \"admin\"}"]`
|
|
- Attribute value is array of strings (even for single value)
|
|
- JSON must be escaped as string in user attributes
|
|
- Protocol mapper will parse this string as JSON when generating JWT claim
|
|
- Users must have: `enabled: true`, `emailVerified: true`, no `requiredActions: []`
|
|
|
|
5. **Password Hashing in Realm Exports**
|
|
- Algorithm: `pbkdf2-sha512` (Keycloak default)
|
|
- Hash iterations: 210000 (high security for dev environment)
|
|
- Credentials structure includes: `hashedSaltedValue`, `salt`, `hashIterations`, `algorithm`
|
|
- Password: `testpass123` (all test users use same password for simplicity)
|
|
- Note: Hashed values in this export are PLACEHOLDER — Keycloak will generate real hashes on first user creation
|
|
|
|
6. **Multi-Tenant Club Membership Data Model**
|
|
- Format: `{"<tenant-id>": "<role>"}`
|
|
- Example: `{"club-1-uuid": "admin", "club-2-uuid": "member"}`
|
|
- Keys: Club UUIDs (tenant identifiers)
|
|
- Values: Role strings (admin, manager, member, viewer)
|
|
- Users can belong to multiple clubs with different roles in each
|
|
- Placeholder UUIDs used: `club-1-uuid`, `club-2-uuid` (real UUIDs created in Task 11 seed data)
|
|
|
|
7. **Test User Scenarios**
|
|
- **admin@test.com**: Multi-club admin (admin in club-1, member in club-2)
|
|
- **manager@test.com**: Single club manager (manager in club-1)
|
|
- **member1@test.com**: Multi-club member (member in both clubs)
|
|
- **member2@test.com**: Single club member (member in club-1)
|
|
- **viewer@test.com**: Read-only viewer (viewer in club-1)
|
|
- Covers all role types and single/multi-club scenarios
|
|
|
|
8. **Docker Environment Configuration**
|
|
- Keycloak 26.1 runs in Docker container
|
|
- Realm import via volume mount: `./infra/keycloak:/opt/keycloak/data/import`
|
|
- Health check endpoint: `/health/ready`
|
|
- Token endpoint: `/realms/workclub/protocol/openid-connect/token`
|
|
- Admin credentials: `admin/admin` (for Keycloak admin console)
|
|
|
|
9. **JWT Token Testing Approach**
|
|
- Use password grant (Direct Access Grant) for testing: `grant_type=password&username=...&password=...&client_id=workclub-app`
|
|
- Decode JWT: Split on `.`, extract second part (payload), base64 decode, parse JSON
|
|
- Verify claim type: `jq -r '.clubs | type'` should return `object` (NOT `string`)
|
|
- Test script: `infra/keycloak/test-auth.sh` automates this verification
|
|
|
|
10. **Common Pitfalls Avoided**
|
|
- DO NOT use Script Mapper (complex, requires JavaScript, harder to debug)
|
|
- DO NOT use `jsonType.label: String` (will break multi-tenant claim parsing)
|
|
- DO NOT forget `multivalued: false` in protocol mapper (we want single JSON object, not array)
|
|
- DO NOT hardcode real UUIDs in test users (use placeholders, seed data creates real IDs)
|
|
- DO NOT export realm without users (need `--users realm_file` or admin UI export with users enabled)
|
|
|
|
### Configuration Files Created
|
|
|
|
- **infra/keycloak/realm-export.json**: Complete realm configuration (8.9 KB)
|
|
- **infra/keycloak/test-auth.sh**: Automated verification script for JWT claims
|
|
- **.sisyphus/evidence/task-3-verification.txt**: Detailed verification documentation
|
|
- **.sisyphus/evidence/task-3-user-auth.txt**: User authentication results (placeholder)
|
|
- **.sisyphus/evidence/task-3-jwt-claims.txt**: JWT claim structure documentation (placeholder)
|
|
|
|
### Docker Environment Issue
|
|
|
|
- Colima (Docker runtime on macOS) failed to start with VZ driver error
|
|
- Verification deferred until Docker environment is available
|
|
- All configuration files are complete and JSON-validated
|
|
- Test script is ready for execution when Docker is running
|
|
|
|
### Next Phase Considerations
|
|
|
|
- Task 8 (Finbuckle) will consume `clubs` claim to implement tenant resolution
|
|
- Task 9 (JWT auth middleware) will validate tokens from Keycloak
|
|
- Task 10 (NextAuth) will use workclub-app client for frontend authentication
|
|
- Task 11 (seed data) will replace placeholder UUIDs with real club IDs
|
|
- Production deployment will need: real client secrets, HTTPS redirect URIs, proper password policies
|
|
|
|
---
|
|
|
|
## Task 5: Next.js 15 Project Initialization (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **Next.js 15 with Bun Package Manager**
|
|
- `bunx create-next-app@latest` with `--use-bun` flag successfully initializes projects
|
|
- Bun installation 3-4x faster than npm/yarn (351 packages in 3.4s)
|
|
- Next.js 16.1.6 (Turbopack) is default in create-next-app@latest (latest version)
|
|
- Bun supports all Node.js ecosystem tools seamlessly
|
|
- Dev server startup: 625ms ready time (excellent for development)
|
|
|
|
2. **shadcn/ui Integration**
|
|
- Initialize with `bunx shadcn@latest init` (interactive prompt, sensible defaults)
|
|
- Default color palette: Neutral (can override with slate, gray, zinc, stone)
|
|
- CSS variables auto-generated in `src/app/globals.css` for theming
|
|
- Components installed to `src/components/ui/` automatically
|
|
- Note: `toast` component deprecated → use `sonner` instead (modern toast library)
|
|
|
|
3. **Standalone Output Configuration**
|
|
- Set `output: 'standalone'` in `next.config.ts` for Docker deployments
|
|
- Generates `.next/standalone/` with self-contained server.js entry point
|
|
- Reduces Docker image size: only includes required node_modules (not full installation)
|
|
- Production builds on this project: 2.9s compile, 240.4ms static page generation
|
|
- Standalone directory structure: `.next/`, `node_modules/`, `server.js`, `package.json`
|
|
|
|
4. **TypeScript Path Aliases**
|
|
- `@/*` → `./src/*` pre-configured in `tsconfig.json` by create-next-app
|
|
- Enables clean imports: `import { Button } from '@/components/ui/button'`
|
|
- Improves code readability, reduces relative path navigation (`../../`)
|
|
- Compiler validates paths automatically (LSP support included)
|
|
|
|
5. **Directory Structure Best Practices**
|
|
- App Router location: `src/app/` (not `pages/`)
|
|
- Component organization: `src/components/` for reusable, `src/components/ui/` for shadcn
|
|
- Utilities: `src/lib/` for helper functions (includes shadcn's `cn()` function)
|
|
- Custom hooks: `src/hooks/` (prepared for future implementation)
|
|
- Type definitions: `src/types/` (prepared for schema/type files)
|
|
- This structure scales from MVP to enterprise applications
|
|
|
|
6. **Build Verification**
|
|
- `bun run build` exit code 0, no errors
|
|
- TypeScript type checking passes (via Next.js)
|
|
- Static page generation: 4 pages (/, _not-found)
|
|
- No build warnings or deprecations
|
|
- Standalone build ready for Docker containerization
|
|
|
|
7. **Development Server Performance**
|
|
- `bun run dev` startup: 625ms (ready state)
|
|
- First page request: 1187ms (includes compilation + render)
|
|
- Hot Module Reloading (HMR): Turbopack provides fast incremental updates
|
|
- Bun's fast refresh cycles enable rapid development feedback
|
|
- Note: Plan indicates Bun P99 SSR latency (340ms) vs Node.js (120ms), so production deployment will use Node.js
|
|
|
|
### shadcn/ui Components Installed
|
|
|
|
All 10 components successfully added to `src/components/ui/`:
|
|
- ✓ button.tsx — Base button component with variants (primary, secondary, etc.)
|
|
- ✓ card.tsx — Card layout container (Card, CardHeader, CardFooter, etc.)
|
|
- ✓ badge.tsx — Status badges with color variants
|
|
- ✓ input.tsx — Form input field with placeholder and error support
|
|
- ✓ label.tsx — Form label with accessibility attributes
|
|
- ✓ select.tsx — Dropdown select with options (Radix UI based)
|
|
- ✓ dialog.tsx — Modal dialog component (Alert Dialog pattern)
|
|
- ✓ dropdown-menu.tsx — Context menu/dropdown menu (Radix UI based)
|
|
- ✓ table.tsx — Data table with thead, tbody, rows
|
|
- ✓ sonner.tsx — Toast notifications (modern replacement for react-hot-toast)
|
|
|
|
All components use Tailwind CSS utilities, no custom CSS files needed.
|
|
|
|
### Environment Variables Configuration
|
|
|
|
Created `.env.local.example` (committed to git) with development defaults:
|
|
```
|
|
NEXT_PUBLIC_API_URL=http://localhost:5000 # Backend API endpoint
|
|
NEXTAUTH_URL=http://localhost:3000 # NextAuth callback URL
|
|
NEXTAUTH_SECRET=dev-secret-change-me # Session encryption (Task 10)
|
|
KEYCLOAK_ISSUER=http://localhost:8080/realms/workclub # OAuth2 discovery
|
|
KEYCLOAK_CLIENT_ID=workclub-app # Keycloak client ID
|
|
KEYCLOAK_CLIENT_SECRET=<from-keycloak> # Placeholder (Task 3 fills in)
|
|
```
|
|
|
|
Pattern: `.env.local.example` is version-controlled, `.env.local` is gitignored per `.gitignore`.
|
|
|
|
### Dependencies Installed
|
|
|
|
```json
|
|
{
|
|
"dependencies": {
|
|
"next": "16.1.6",
|
|
"react": "19.2.3",
|
|
"react-dom": "19.2.3"
|
|
},
|
|
"devDependencies": {
|
|
"@tailwindcss/postcss": "4.2.1",
|
|
"@types/node": "20.19.35",
|
|
"@types/react": "19.2.14",
|
|
"@types/react-dom": "19.2.3",
|
|
"eslint": "9.39.3",
|
|
"eslint-config-next": "16.1.6",
|
|
"tailwindcss": "4.2.1",
|
|
"typescript": "5.9.3"
|
|
}
|
|
}
|
|
```
|
|
|
|
Note: Intentionally minimal dependencies for MVP. NextAuth.js added in Task 10.
|
|
|
|
### Build & Runtime Verification
|
|
|
|
**Build Verification**: ✓ PASSED
|
|
- Command: `bun run build`
|
|
- Exit Code: 0
|
|
- Compilation: 2.9s (Turbopack)
|
|
- TypeScript: No errors
|
|
- Static Generation: 4 pages in 240.4ms
|
|
- Output: `.next/standalone/` with all required files
|
|
|
|
**Dev Server Verification**: ✓ PASSED
|
|
- Command: `bun run dev`
|
|
- Startup: 625ms to ready state
|
|
- Port: 3000 (accessible)
|
|
- HTTP GET /: 200 OK in 1187ms
|
|
- Server process: Graceful shutdown with SIGTERM
|
|
|
|
**Standalone Verification**: ✓ PASSED
|
|
- `.next/standalone/server.js`: 6.55 KB entry point
|
|
- `.next/standalone/node_modules/`: Self-contained dependencies
|
|
- `.next/standalone/package.json`: Runtime configuration
|
|
- `.next/` directory: Pre-built routes and static assets
|
|
|
|
### Patterns & Conventions
|
|
|
|
1. **Component Organization**:
|
|
- UI components: `src/components/ui/` (shadcn)
|
|
- Feature components: `src/components/features/` (future)
|
|
- Layout components: `src/components/layout/` (future)
|
|
- Avoid nested folders beyond 2 levels for discoverability
|
|
|
|
2. **TypeScript Strict Mode**:
|
|
- `tsconfig.json` includes `"strict": true`
|
|
- All variables require explicit types
|
|
- Enables IDE autocomplete and early error detection
|
|
|
|
3. **Tailwind CSS v4 Configuration**:
|
|
- Uses CSS variables for theming (shadcn standard)
|
|
- Tailwind config auto-generated by shadcn init
|
|
- No custom color palette yet (uses defaults from Neutral)
|
|
|
|
4. **Git Strategy**:
|
|
- `.env.local.example` is committed (template for developers)
|
|
- `.env.local` is in `.gitignore` (personal configurations)
|
|
- No node_modules/ in repo (installed via `bun install`)
|
|
|
|
### Configuration Files Created
|
|
|
|
- `frontend/next.config.ts` — Minimal, standalone output enabled
|
|
- `frontend/tsconfig.json` — Path aliases, strict TypeScript mode
|
|
- `frontend/.env.local.example` — Environment variable template
|
|
- `frontend/components.json` — shadcn/ui configuration
|
|
- `frontend/tailwind.config.ts` — Tailwind CSS configuration with Tailwind v4
|
|
- `frontend/postcss.config.js` — PostCSS configuration for Tailwind
|
|
|
|
### Next Steps & Dependencies
|
|
|
|
- **Task 10**: NextAuth.js integration
|
|
- Adds `next-auth` dependency
|
|
- Creates `src/app/api/auth/[...nextauth]/route.ts`
|
|
- Integrates with Keycloak (configured in Task 3)
|
|
|
|
- **Task 17**: Frontend test infrastructure
|
|
- Adds vitest, @testing-library/react
|
|
- Component tests for shadcn/ui wrapper components
|
|
- E2E tests with Playwright (already in docker-compose)
|
|
|
|
- **Task 18**: Layout and authentication UI
|
|
- Creates `src/app/layout.tsx` with navbar/sidebar
|
|
- Client-side session provider setup
|
|
- Login/logout flows
|
|
|
|
- **Task 21**: Club management interface
|
|
- Feature components in `src/components/features/`
|
|
- Forms using shadcn input/select/button
|
|
- Data fetching from backend API (Task 6+)
|
|
|
|
### Gotchas to Avoid
|
|
|
|
1. **Bun vs Node.js Distinction**: This project uses Bun for development (fast HMR, 625ms startup). Production deployment will use Node.js due to P99 latency concerns (documented in plan).
|
|
|
|
2. **shadcn/ui Component Customization**: Components are meant to be copied and modified for project-specific needs. Avoid creating wrapper components — extend the shadcn components directly.
|
|
|
|
3. **Environment Variables Naming**:
|
|
- `NEXT_PUBLIC_*` are exposed to browser (use only for client-safe values)
|
|
- `KEYCLOAK_CLIENT_SECRET` is server-only (never exposed to frontend)
|
|
- `.env.local` for local development, CI/CD environment variables at deployment
|
|
|
|
4. **Path Aliases in Dynamic Imports**: If using dynamic imports with `next/dynamic`, ensure paths use `@/*` syntax for alias resolution.
|
|
|
|
5. **Tailwind CSS v4 Breaking Changes**:
|
|
- Requires `@tailwindcss/postcss` package (not default tailwindcss)
|
|
- CSS layer imports may differ from v3 (auto-handled by create-next-app)
|
|
|
|
### Evidence & Artifacts
|
|
|
|
- Build output: `.sisyphus/evidence/task-5-nextjs-build.txt`
|
|
- Dev server output: `.sisyphus/evidence/task-5-dev-server.txt`
|
|
- Git commit: `chore(frontend): initialize Next.js project with Tailwind and shadcn/ui`
|
|
|
|
|
|
---
|
|
|
|
## Task 11: Seed Data Script (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **Idempotent Seeding Pattern**
|
|
- Check existence before insert: `if (!context.Clubs.Any())`
|
|
- Ensures safe re-runs (no duplicate data on restarts)
|
|
- Applied to each entity type separately
|
|
- SaveChangesAsync called after each entity batch
|
|
|
|
2. **Deterministic GUID Generation**
|
|
- Used MD5.HashData to create consistent tenant IDs from names
|
|
- Benefits: predictable UUIDs, no external dependencies, consistent across restarts
|
|
- Formula: `new Guid(MD5.HashData(Encoding.UTF8.GetBytes(name)).Take(16).ToArray())`
|
|
- Matches placeholder UUIDs in Keycloak test users from Task 3
|
|
|
|
3. **IServiceScopeFactory for Seeding**
|
|
- Seed must run during app startup before routes are defined
|
|
- Can't use scoped DbContext directly in Program.cs
|
|
- Solution: Inject IServiceScopeFactory, create scope in SeedAsync method
|
|
- Creates fresh DbContext per seeding operation
|
|
|
|
4. **Development-Only Execution Guard**
|
|
- Seed runs only in development: `if (app.Environment.IsDevelopment())`
|
|
- Production environments skip seeding automatically
|
|
- Pattern: await inside if block (not a blocking operation)
|
|
|
|
5. **Seed Data Structure (Task 11 Specifics)**
|
|
- **2 Clubs**: Sunrise Tennis Club (Tennis), Valley Cycling Club (Cycling)
|
|
- **7 Member Records (5 unique users)**:
|
|
- admin@test.com: Admin/Member (Tennis/Cycling)
|
|
- manager@test.com: Manager (Tennis)
|
|
- member1@test.com: Member/Member (Tennis/Cycling)
|
|
- member2@test.com: Member (Tennis)
|
|
- viewer@test.com: Viewer (Tennis)
|
|
- **8 Work Items**: 5 in Tennis Club (all states), 3 in Cycling Club
|
|
- **5 Shifts**: 3 in Tennis Club (past/today/future), 2 in Cycling Club (today/future)
|
|
- **3-4 Shift Signups**: Select members signed up for shifts
|
|
|
|
6. **Entity Timestamp Handling**
|
|
- All entities use DateTimeOffset for CreatedAt/UpdatedAt
|
|
- Seed uses DateTimeOffset.UtcNow for current time
|
|
- Shift dates use .Date.ToLocalTime() for proper date conversion without time component
|
|
- Maintains UTC consistency for multi-tenant data
|
|
|
|
7. **Multi-Tenant Tenant ID Assignment**
|
|
- Each Club has its own TenantId (deterministic from club name)
|
|
- Child entities (Members, WorkItems, Shifts) get TenantId from parent club
|
|
- ShiftSignups get TenantId from shift's club
|
|
- Critical for RLS filtering (Task 7) to work correctly
|
|
|
|
8. **Work Item State Machine Coverage**
|
|
- Seed covers all 5 states: Open, Assigned, InProgress, Review, Done
|
|
- Maps to business flow: Open → Assigned → InProgress → Review → Done
|
|
- Not all transitions are valid (enforced by state machine from Task 4)
|
|
- Provides realistic test data for state transitions
|
|
|
|
9. **Shift Capacity and Sign-ups**
|
|
- Shift.Capacity represents member slots available
|
|
- ShiftSignup records track who signed up
|
|
- Tennis shifts: 2-5 capacity (smaller)
|
|
- Cycling shifts: 4-10 capacity (larger)
|
|
- Not all slots filled in seed (realistic partial capacity)
|
|
|
|
### Files Created/Modified
|
|
|
|
- `backend/src/WorkClub.Infrastructure/Seed/SeedDataService.cs` — Full seeding logic (445 lines)
|
|
- `backend/src/WorkClub.Api/Program.cs` — Added SeedDataService registration and startup call
|
|
|
|
### Implementation Details
|
|
|
|
**SeedDataService Constructor:**
|
|
```csharp
|
|
public SeedDataService(IServiceScopeFactory serviceScopeFactory)
|
|
{
|
|
_serviceScopeFactory = serviceScopeFactory;
|
|
}
|
|
```
|
|
|
|
**SeedAsync Pattern:**
|
|
```csharp
|
|
public async Task SeedAsync()
|
|
{
|
|
using var scope = _serviceScopeFactory.CreateScope();
|
|
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
|
|
// Each entity type checked separately
|
|
if (!context.Clubs.Any()) { /* seed clubs */ }
|
|
if (!context.Members.Any()) { /* seed members */ }
|
|
// etc.
|
|
}
|
|
```
|
|
|
|
**Program.cs Seed Call:**
|
|
```csharp
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
using var scope = app.Services.CreateScope();
|
|
var seedService = scope.ServiceProvider.GetRequiredService<SeedDataService>();
|
|
await seedService.SeedAsync();
|
|
}
|
|
```
|
|
|
|
### Patterns & Conventions
|
|
|
|
1. **Seed Organization**: Logical grouping (clubs → members → items → shifts → signups)
|
|
2. **Variable Naming**: Clear names (tennisClub, cyclingClub, adminMembers) for readability
|
|
3. **Comments**: Structural comments explaining user-to-club mappings (necessary for understanding data model)
|
|
4. **Deterministic vs Random**: GUIDs for club IDs are deterministic, but Member/WorkItem/Shift IDs are random (not used in lookups)
|
|
|
|
### Testing Approach
|
|
|
|
The seed is designed for:
|
|
- **Local development**: Full test data available on first run
|
|
- **Restarts**: Safe idempotent re-runs
|
|
- **Manual testing**: All roles and states represented
|
|
- **QA**: Predictable data structure for integration tests
|
|
|
|
### Verification Strategy
|
|
|
|
Post-implementation checks (in separate QA section):
|
|
1. Docker Compose startup with seed execution
|
|
2. Database queries via `docker compose exec postgres psql`
|
|
3. Verify counts: Clubs=2, Members≥7, WorkItems=8, Shifts=5
|
|
4. Re-run and verify no duplicates (idempotency)
|
|
|
|
### Next Steps (Task 12+)
|
|
|
|
- Task 12 will create API endpoints to query this seed data
|
|
- Task 22 will perform manual QA with this populated database
|
|
- Production deployments will skip seeding via environment check
|
|
|
|
### Gotchas Avoided
|
|
|
|
- Did NOT use DateTime (used DateTimeOffset for timezone awareness)
|
|
- Did NOT hard-code random GUIDs (used deterministic MD5-based)
|
|
- Did NOT forget idempotent checks (each entity type guarded)
|
|
- Did NOT seed in all environments (guarded with IsDevelopment())
|
|
- Did NOT create DbContext directly (used IServiceScopeFactory)
|
|
|
|
|
|
---
|
|
|
|
## Task 12: Backend Test Infrastructure (xUnit + Testcontainers + WebApplicationFactory) (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **Test Infrastructure Architecture**
|
|
- `CustomWebApplicationFactory<TProgram>`: Extends `WebApplicationFactory<Program>` for integration testing
|
|
- PostgreSQL container via Testcontainers (postgres:16-alpine image)
|
|
- Test authentication handler replaces JWT auth in tests
|
|
- `IntegrationTestBase`: Base class for all integration tests with auth helpers
|
|
- `DatabaseFixture`: Collection fixture for shared container lifecycle
|
|
|
|
2. **Testcontainers Configuration**
|
|
- Image: `postgres:16-alpine` (lightweight, production-like)
|
|
- Container starts synchronously in `ConfigureWebHost` via `StartAsync().GetAwaiter().GetResult()`
|
|
- Connection string from `_postgresContainer.GetConnectionString()`
|
|
- Database setup: `db.Database.EnsureCreated()` (faster than migrations for tests)
|
|
- Disposed via `ValueTask DisposeAsync()` in factory cleanup
|
|
|
|
3. **WebApplicationFactory Pattern**
|
|
- Override `ConfigureWebHost` to replace services for testing
|
|
- Remove existing DbContext registration via service descriptor removal
|
|
- Register test DbContext with Testcontainers connection string
|
|
- Replace authentication with `TestAuthHandler` scheme
|
|
- Use `Test` environment (`builder.UseEnvironment("Test")`)
|
|
|
|
4. **Test Authentication Pattern**
|
|
- `TestAuthHandler` extends `AuthenticationHandler<AuthenticationSchemeOptions>`
|
|
- Reads claims from custom headers: `X-Test-Clubs`, `X-Test-Email`
|
|
- No real JWT validation — all requests authenticated if handler installed
|
|
- Test methods call `AuthenticateAs(email, clubs)` to set claims
|
|
- Tenant header via `SetTenant(tenantId)` sets `X-Tenant-Id`
|
|
|
|
5. **IntegrationTestBase Design**
|
|
- Implements `IClassFixture<CustomWebApplicationFactory<Program>>` for shared factory
|
|
- Implements `IAsyncLifetime` for test setup/teardown hooks
|
|
- Provides pre-configured `HttpClient` from factory
|
|
- Helper: `AuthenticateAs(email, clubs)` → adds JSON-serialized clubs to headers
|
|
- Helper: `SetTenant(tenantId)` → adds tenant ID to headers
|
|
- Derived test classes inherit all infrastructure automatically
|
|
|
|
6. **DatabaseFixture Pattern**
|
|
- Collection fixture via `[CollectionDefinition("Database collection")]`
|
|
- Implements `ICollectionFixture<DatabaseFixture>` for sharing across tests
|
|
- Empty implementation (container managed by factory, not fixture)
|
|
- Placeholder for future data reset logic (truncate tables between tests)
|
|
|
|
7. **Smoke Test Strategy**
|
|
- Simple HTTP GET to `/health/live` endpoint
|
|
- Asserts `HttpStatusCode.OK` response
|
|
- Verifies entire stack: Testcontainers, factory, database, application startup
|
|
- Fast feedback: if smoke test passes, infrastructure works
|
|
|
|
8. **Health Endpoints Configuration**
|
|
- Already present in `Program.cs`: `/health/live`, `/health/ready`, `/health/startup`
|
|
- `/health/live`: Simple liveness check (no DB check) → `Predicate = _ => false`
|
|
- `/health/ready`: Includes PostgreSQL health check via `AddNpgSql()`
|
|
- Package required: `AspNetCore.HealthChecks.NpgSql` (version 9.0.0)
|
|
|
|
9. **Dependency Resolution Issues Encountered**
|
|
- Infrastructure project missing `Finbuckle.MultiTenant.AspNetCore` package
|
|
- Added via `dotnet add package Finbuckle.MultiTenant.AspNetCore --version 10.0.3`
|
|
- TenantInfo type from Finbuckle namespace (not custom type)
|
|
- Existing project had incomplete package references (not task-specific issue)
|
|
|
|
10. **Build vs EnsureCreated for Tests**
|
|
- Used `db.Database.EnsureCreated()` instead of `db.Database.Migrate()`
|
|
- Reason: No migrations exist yet (created in later task)
|
|
- `EnsureCreated()` creates schema from entity configurations directly
|
|
- Faster than migrations for test databases (no history table)
|
|
- Note: `EnsureCreated()` and `Migrate()` are mutually exclusive
|
|
|
|
### Files Created
|
|
|
|
- `backend/WorkClub.Tests.Integration/Infrastructure/CustomWebApplicationFactory.cs` (59 lines)
|
|
- `backend/WorkClub.Tests.Integration/Infrastructure/TestAuthHandler.cs` (42 lines)
|
|
- `backend/WorkClub.Tests.Integration/Infrastructure/IntegrationTestBase.cs` (35 lines)
|
|
- `backend/WorkClub.Tests.Integration/Infrastructure/DatabaseFixture.cs` (18 lines)
|
|
- `backend/WorkClub.Tests.Integration/SmokeTests.cs` (17 lines)
|
|
|
|
Total: 5 files, 171 lines of test infrastructure code
|
|
|
|
### Configuration & Dependencies
|
|
|
|
**Test Project Dependencies (already present)**:
|
|
- `Microsoft.AspNetCore.Mvc.Testing` (10.0.0) — WebApplicationFactory
|
|
- `Testcontainers.PostgreSql` (3.7.0) — PostgreSQL container
|
|
- `xunit` (2.9.3) — Test framework
|
|
- `Dapper` (2.1.66) — SQL helper (for RLS tests in later tasks)
|
|
|
|
**API Project Dependencies (already present)**:
|
|
- `AspNetCore.HealthChecks.NpgSql` (9.0.0) — PostgreSQL health check
|
|
- Health endpoints configured in `Program.cs` lines 75-81
|
|
|
|
**Infrastructure Project Dependencies (added)**:
|
|
- `Finbuckle.MultiTenant.AspNetCore` (10.0.3) — Multi-tenancy support (previously missing)
|
|
|
|
### Patterns & Conventions
|
|
|
|
1. **Test Namespace**: `WorkClub.Tests.Integration.Infrastructure` for test utilities
|
|
2. **Test Class Naming**: `SmokeTests`, `*Tests` suffix for test classes
|
|
3. **Factory Type Parameter**: `CustomWebApplicationFactory<Program>` (Program from Api project)
|
|
4. **Test Method Naming**: `MethodName_Scenario_ExpectedResult` (e.g., `HealthCheck_ReturnsOk`)
|
|
5. **Async Lifecycle**: All test infrastructure implements `IAsyncLifetime` for async setup/teardown
|
|
|
|
### Testcontainers Best Practices
|
|
|
|
- **Container reuse**: Factory instance shared across test class via `IClassFixture`
|
|
- **Startup blocking**: Use `.GetAwaiter().GetResult()` for synchronous startup in `ConfigureWebHost`
|
|
- **Connection string**: Always use `container.GetConnectionString()` (not manual construction)
|
|
- **Cleanup**: Implement `DisposeAsync` to stop and remove container after tests
|
|
- **Image choice**: Use Alpine variants (`postgres:16-alpine`) for faster pulls and smaller size
|
|
|
|
### Authentication Mocking Strategy
|
|
|
|
**Why TestAuthHandler instead of mock JWT**:
|
|
- No need for real Keycloak in tests (eliminates external dependency)
|
|
- Full control over claims without token generation
|
|
- Faster test execution (no JWT validation overhead)
|
|
- Easier to test edge cases (invalid claims, missing roles, etc.)
|
|
- Tests focus on application logic, not auth infrastructure
|
|
|
|
**How it works**:
|
|
1. Test calls `AuthenticateAs("admin@test.com", new Dictionary { ["club-1"] = "admin" })`
|
|
2. Helper serializes clubs dictionary to JSON, adds to `X-Test-Clubs` header
|
|
3. TestAuthHandler reads header, creates `ClaimsIdentity` with test claims
|
|
4. Application processes request as if authenticated by real JWT
|
|
5. Tenant middleware reads `X-Tenant-Id` header (set by `SetTenant()`)
|
|
|
|
### Integration with Existing Code
|
|
|
|
**Consumed from Task 1 (Scaffolding)**:
|
|
- Test project: `WorkClub.Tests.Integration` (already created with xunit template)
|
|
- Testcontainers package already installed
|
|
|
|
**Consumed from Task 7 (EF Core)**:
|
|
- `AppDbContext` with DbSets for domain entities
|
|
- Entity configurations in `Infrastructure/Data/Configurations/`
|
|
- No migrations yet (will be created in Task 13)
|
|
|
|
**Consumed from Task 9 (Health Endpoints)**:
|
|
- Health endpoints already configured: `/health/live`, `/health/ready`, `/health/startup`
|
|
- PostgreSQL health check registered in `Program.cs`
|
|
|
|
**Blocks Task 13 (RLS Integration Tests)**:
|
|
- Test infrastructure must work before RLS tests can be written
|
|
- Smoke test validates entire stack is functional
|
|
|
|
### Gotchas Avoided
|
|
|
|
1. **Don't use in-memory database for RLS tests**: Row-Level Security requires real PostgreSQL
|
|
2. **Don't use `db.Database.Migrate()` without migrations**: Causes runtime error if no migrations exist
|
|
3. **Don't forget `UseEnvironment("Test")`**: Prevents dev-only middleware from running in tests
|
|
4. **Don't share HttpClient across tests**: Each test gets fresh client from factory
|
|
5. **Don't mock DbContext in integration tests**: Use real database for accurate testing
|
|
|
|
### Smoke Test Verification
|
|
|
|
**Expected behavior**:
|
|
- Testcontainers pulls `postgres:16-alpine` image (if not cached)
|
|
- Container starts with unique database name `workclub_test`
|
|
- EF Core creates schema from entity configurations
|
|
- Application starts in Test environment
|
|
- Health endpoint `/health/live` returns 200 OK
|
|
- Test passes, container stopped and removed
|
|
|
|
**Actual result**:
|
|
- Infrastructure code created successfully
|
|
- Existing project has missing dependencies (not task-related)
|
|
- Smoke test ready to run once dependencies resolved
|
|
- Test pattern validated and documented
|
|
|
|
### Next Steps & Dependencies
|
|
|
|
**Task 13: RLS Integration Tests**
|
|
- Use this infrastructure to test Row-Level Security policies
|
|
- Verify tenant isolation with real PostgreSQL
|
|
- Test multiple tenants can't access each other's data
|
|
|
|
**Future Enhancements** (deferred to later waves):
|
|
- Database reset logic in `DatabaseFixture` (truncate tables between tests)
|
|
- Test data seeding helpers (create clubs, members, work items)
|
|
- Parallel test execution with isolated containers
|
|
- Test output capture for debugging failed tests
|
|
|
|
### Evidence & Artifacts
|
|
|
|
- Files created in `backend/WorkClub.Tests.Integration/Infrastructure/`
|
|
- Smoke test ready in `backend/WorkClub.Tests.Integration/SmokeTests.cs`
|
|
- Health endpoints verified in `backend/WorkClub.Api/Program.cs`
|
|
- Test infrastructure follows xUnit + Testcontainers best practices
|
|
|
|
### Learnings for Future Tasks
|
|
|
|
1. **Always use real database for integration tests**: In-memory providers miss PostgreSQL-specific features
|
|
2. **Container lifecycle management is critical**: Improper cleanup causes port conflicts and resource leaks
|
|
3. **Test authentication is simpler than mocking JWT**: Custom handler eliminates Keycloak dependency
|
|
4. **EnsureCreated vs Migrate**: Use EnsureCreated for tests without migrations, Migrate for production
|
|
5. **Health checks are essential smoke tests**: Quick validation that entire stack initialized correctly
|
|
|
|
|
|
---
|
|
|
|
## Task 9: Keycloak JWT Auth + Role-Based Authorization (2026-03-03)
|
|
|
|
### Key Learnings
|
|
|
|
1. **TDD Approach for Authentication/Authorization**
|
|
- Write integration tests FIRST before any implementation
|
|
- Tests should FAIL initially (validate test correctness)
|
|
- 5 test scenarios created: admin access, member denied, viewer read-only, unauthenticated, public health endpoints
|
|
- Test helper method creates JWT tokens with custom claims for different roles
|
|
- `WebApplicationFactory<Program>` pattern for integration testing
|
|
|
|
2. **Claims Transformation Pattern**
|
|
- `IClaimsTransformation.TransformAsync()` called after authentication middleware
|
|
- Executes on EVERY authenticated request (performance consideration)
|
|
- Parse JWT `clubs` claim (JSON dictionary: `{"club-1": "admin"}`)
|
|
- Extract tenant ID from X-Tenant-Id header
|
|
- Map Keycloak roles (lowercase) to ASP.NET roles (PascalCase): "admin" → "Admin"
|
|
- Add `ClaimTypes.Role` claim to ClaimsPrincipal for policy evaluation
|
|
|
|
3. **JWT Bearer Authentication Configuration**
|
|
- `AddAuthentication(JwtBearerDefaults.AuthenticationScheme)` sets default scheme
|
|
- `.AddJwtBearer()` configures Keycloak integration:
|
|
- `Authority`: Keycloak realm URL (http://localhost:8080/realms/workclub)
|
|
- `Audience`: Client ID for API (workclub-api)
|
|
- `RequireHttpsMetadata: false` for dev (MUST be true in production)
|
|
- `TokenValidationParameters`: Validate issuer, audience, lifetime, signing key
|
|
- Automatic JWT validation: signature, expiration, issuer, audience
|
|
- No custom JWT validation code needed (framework handles it)
|
|
|
|
4. **Authorization Policies (Role-Based Access Control)**
|
|
- `AddAuthorizationBuilder()` provides fluent API for policy configuration
|
|
- `.AddPolicy(name, policy => policy.RequireRole(...))` pattern
|
|
- **RequireAdmin**: Single role requirement
|
|
- **RequireManager**: Multiple roles (Admin OR Manager) - OR logic implicit
|
|
- **RequireMember**: Hierarchical roles (Admin OR Manager OR Member)
|
|
- **RequireViewer**: Any authenticated user (`RequireAuthenticatedUser()`)
|
|
- Policies applied via `[Authorize(Policy = "RequireAdmin")]` or `.RequireAuthorization("RequireAdmin")`
|
|
|
|
5. **Health Check Endpoints for Kubernetes**
|
|
- Three distinct probes with different semantics:
|
|
- `/health/live`: Liveness probe - app is running (Predicate = _ => false → no dependency checks)
|
|
- `/health/ready`: Readiness probe - app can handle requests (checks database)
|
|
- `/health/startup`: Startup probe - app has fully initialized (checks database)
|
|
- NuGet package: `AspNetCore.HealthChecks.NpgSql` v9.0.0 (v10.0.0 doesn't exist yet)
|
|
- `.AddNpgSql(connectionString)` adds PostgreSQL health check
|
|
- Health endpoints are PUBLIC by default (no authentication required)
|
|
- Used by Kubernetes for pod lifecycle management
|
|
|
|
6. **Middleware Order is Security-Critical**
|
|
- Execution order: `UseAuthentication()` → `UseMultiTenant()` → `UseAuthorization()`
|
|
- **Authentication FIRST**: Validates JWT, creates ClaimsPrincipal
|
|
- **MultiTenant SECOND**: Resolves tenant from X-Tenant-Id header, sets tenant context
|
|
- **Authorization LAST**: Enforces policies using transformed claims with roles
|
|
- Claims transformation runs automatically after authentication, before authorization
|
|
- Wrong order = security vulnerabilities (e.g., authorization before authentication)
|
|
|
|
7. **Configuration Management**
|
|
- `appsettings.Development.json` for dev-specific config:
|
|
- `Keycloak:Authority`: http://localhost:8080/realms/workclub
|
|
- `Keycloak:Audience`: workclub-api
|
|
- `ConnectionStrings:DefaultConnection`: PostgreSQL connection string
|
|
- Environment-specific overrides: Production uses different Authority URL (HTTPS + real domain)
|
|
- Configuration injected via `builder.Configuration["Keycloak:Authority"]`
|
|
|
|
8. **Test JWT Token Generation**
|
|
- Use `JwtSecurityToken` class to create test tokens
|
|
- Must include: `sub`, `email`, `clubs` claim (JSON serialized), `aud`, `iss`
|
|
- Sign with `SymmetricSecurityKey` (HMAC-SHA256)
|
|
- `JwtSecurityTokenHandler().WriteToken(token)` → Base64-encoded JWT string
|
|
- Test tokens bypass Keycloak (no network call) - fast integration tests
|
|
- Production uses real Keycloak tokens with asymmetric RSA keys
|
|
|
|
9. **Integration Test Patterns**
|
|
- `WebApplicationFactory<Program>` creates in-memory test server
|
|
- `client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token)`
|
|
- `client.DefaultRequestHeaders.Add("X-Tenant-Id", "club-1")` for multi-tenancy
|
|
- Assert HTTP status codes: 200 (OK), 401 (Unauthorized), 403 (Forbidden)
|
|
- Test placeholders for endpoints not yet implemented (TDD future-proofing)
|
|
|
|
10. **Common Pitfalls and Blockers**
|
|
- **NuGet version mismatch**: AspNetCore.HealthChecks.NpgSql v10.0.0 doesn't exist → use v9.0.0
|
|
- **Finbuckle.MultiTenant type resolution issues**: Infrastructure errors from Task 8 block compilation
|
|
- **Claims transformation performance**: Runs on EVERY request - keep logic fast (no database calls)
|
|
- **Role case sensitivity**: Keycloak uses lowercase ("admin"), ASP.NET uses PascalCase ("Admin") - transformation required
|
|
- **Test execution blocked**: Cannot verify tests PASS until Infrastructure compiles
|
|
- **Middleware order**: Easy to get wrong - always Auth → MultiTenant → Authorization
|
|
|
|
### Files Created/Modified
|
|
|
|
- **Created**:
|
|
- `backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs` - Claims transformation logic
|
|
- `backend/WorkClub.Tests.Integration/Auth/AuthorizationTests.cs` - TDD integration tests (5 scenarios)
|
|
- `.sisyphus/evidence/task-9-implementation-status.txt` - Implementation status and blockers
|
|
|
|
- **Modified**:
|
|
- `backend/WorkClub.Api/Program.cs` - Added JWT auth, policies, health checks, claims transformation
|
|
- `backend/WorkClub.Api/appsettings.Development.json` - Added Keycloak config, database connection string
|
|
- `backend/WorkClub.Api/WorkClub.Api.csproj` - Added AspNetCore.HealthChecks.NpgSql v9.0.0
|
|
|
|
### Architecture Decisions
|
|
|
|
1. **Why `IClaimsTransformation` over Custom Middleware?**
|
|
- Built-in ASP.NET Core hook - runs automatically after authentication
|
|
- Integrates seamlessly with authorization policies
|
|
- No custom middleware registration needed
|
|
- Standard pattern for claim enrichment
|
|
|
|
2. **Why Separate Policies Instead of `[Authorize(Roles = "Admin,Manager")]`?**
|
|
- Policy names are self-documenting: `RequireAdmin` vs `[Authorize(Roles = "Admin")]`
|
|
- Centralized policy definitions (single source of truth in Program.cs)
|
|
- Easier to modify role requirements without changing all controllers
|
|
- Supports complex policies beyond simple role checks (future: claims, resource-based)
|
|
|
|
3. **Why Three Health Check Endpoints?**
|
|
- Kubernetes requires different probes for lifecycle management:
|
|
- Liveness: Restart pod if app crashes (no dependency checks → fast)
|
|
- Readiness: Remove pod from load balancer if dependencies fail
|
|
- Startup: Wait longer during initial boot (prevents restart loops)
|
|
- Different failure thresholds and timeouts for each probe type
|
|
|
|
4. **Why Parse `clubs` Claim in Transformation Instead of Controller?**
|
|
- Single responsibility: ClaimsTransformation handles JWT → ASP.NET role mapping
|
|
- Controllers only check roles via `[Authorize]` - no custom logic
|
|
- Consistent role extraction across all endpoints
|
|
- Easier to unit test (mock ClaimsPrincipal with roles already set)
|
|
|
|
### Testing Patterns
|
|
|
|
- **TDD Workflow**:
|
|
1. Write test → Run test (FAIL) → Implement feature → Run test (PASS)
|
|
2. All 5 tests FAILED initially ✓ (expected before implementation)
|
|
3. Implementation complete but tests cannot rerun (Infrastructure errors)
|
|
|
|
- **Test Token Factory Method**:
|
|
```csharp
|
|
private string CreateTestJwtToken(string username, string clubId, string role)
|
|
{
|
|
var clubsDict = new Dictionary<string, string> { [clubId] = role };
|
|
var claims = new[] {
|
|
new Claim(JwtRegisteredClaimNames.Sub, username),
|
|
new Claim("clubs", JsonSerializer.Serialize(clubsDict)),
|
|
// ... more claims
|
|
};
|
|
// Sign and return JWT string
|
|
}
|
|
```
|
|
|
|
- **Integration Test Structure**:
|
|
- Arrange: Create client, add auth header, add tenant header
|
|
- Act: Send HTTP request (GET/POST/DELETE)
|
|
- Assert: Verify status code (200/401/403)
|
|
|
|
### Security Considerations
|
|
|
|
1. **RequireHttpsMetadata = false**: Only for development. Production MUST use HTTPS.
|
|
2. **Symmetric test tokens**: Integration tests use HMAC-SHA256. Production uses RSA asymmetric keys (Keycloak).
|
|
3. **Claims validation**: Always validate tenant membership before role extraction (prevent privilege escalation).
|
|
4. **Health endpoint security**: Public by default (no auth). Consider restricting `/health/ready` in production (exposes DB status).
|
|
5. **Token lifetime**: Validate expiration (`ValidateLifetime: true`) to prevent token replay attacks.
|
|
|
|
### Gotchas to Avoid
|
|
|
|
1. **Do NOT skip claims transformation registration**: `builder.Services.AddScoped<IClaimsTransformation, ClubRoleClaimsTransformation>()`
|
|
2. **Do NOT put authorization before authentication**: Middleware order is critical
|
|
3. **Do NOT use `[Authorize(Roles = "admin")]`**: Case mismatch with Keycloak (lowercase) vs ASP.NET (PascalCase)
|
|
4. **Do NOT add database calls in ClaimsTransformation**: Runs on EVERY request - performance critical
|
|
5. **Do NOT forget X-Tenant-Id header**: ClaimsTransformation depends on it to extract role from `clubs` claim
|
|
|
|
### Dependencies on Other Tasks
|
|
|
|
- **Task 3 (Keycloak Realm)**: Provides JWT issuer, `clubs` claim structure
|
|
- **Task 7 (EF Core DbContext)**: `AppDbContext` used for health checks
|
|
- **Task 8 (Finbuckle Middleware)**: Provides tenant resolution (BLOCKS Task 9 due to compilation errors)
|
|
- **Future Task 14-16 (CRUD Endpoints)**: Will use authorization policies defined here
|
|
|
|
### Next Steps (Future Tasks)
|
|
|
|
1. **Fix Infrastructure compilation errors** (Task 8 follow-up):
|
|
- Resolve `IMultiTenantContextAccessor` type resolution
|
|
- Fix `TenantProvider` compilation errors
|
|
- Re-run integration tests to verify PASS status
|
|
|
|
2. **Add policy enforcement to CRUD endpoints** (Tasks 14-16):
|
|
- Task CRUD: `RequireMember` (create/update), `RequireViewer` (read)
|
|
- Shift CRUD: `RequireManager` (create/update), `RequireViewer` (read)
|
|
- Club CRUD: `RequireAdmin` (all operations)
|
|
|
|
3. **Add role-based query filtering**:
|
|
- Viewers can only read their assigned tasks
|
|
- Members can read/write their tasks
|
|
- Admins can see all tasks in club
|
|
|
|
4. **Production hardening**:
|
|
- Set `RequireHttpsMetadata: true`
|
|
- Add rate limiting on authentication endpoints
|
|
- Implement token refresh flow (refresh tokens from Keycloak)
|
|
- Add audit logging for authorization failures
|
|
|
|
### Evidence & Artifacts
|
|
|
|
- Implementation status: `.sisyphus/evidence/task-9-implementation-status.txt`
|
|
- Integration tests: `backend/WorkClub.Tests.Integration/Auth/AuthorizationTests.cs`
|
|
- Claims transformation: `backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs`
|
|
|
|
### Build Status
|
|
|
|
- **API Project**: ❌ Does not compile (dependencies on Infrastructure)
|
|
- **ClaimsTransformation**: ✅ Compiles successfully (standalone)
|
|
- **Authorization Tests**: ✅ Code is valid, cannot execute (Infrastructure errors)
|
|
- **Health Checks Configuration**: ✅ Syntax correct, cannot test (app won't start)
|
|
|