This commit is contained in:
2026-01-11 13:04:33 +01:00
parent 6f2ab9fc56
commit 971aa7288b
44 changed files with 2168 additions and 1240 deletions

View File

@@ -0,0 +1,206 @@
Delivery Adapters vs App Code (Strict Clean Architecture)
This document clarifies where Controllers live, why they are adapters, and how to structure them cleanly in large systems.
It resolves the common confusion between:
• architectural role (what something is)
• physical placement (where something lives in the repo)
This document is framework-agnostic in its principles and feature-based in its structure.
1. Clean Architecture Layers (Authoritative)
Clean Architecture defines roles, not folders.
ENTITIES
USE CASES
INTERFACE ADAPTERS
FRAMEWORKS & DRIVERS
Everything that follows maps code to these roles.
2. What a Controller Is (Architecturally)
A Controller is an Interface Adapter.
Why:
• It receives external input (HTTP)
• It translates that input into Use Case input
• It invokes a Use Case
• It performs no business logic
By definition:
Anything that translates between external input and the application boundary is an adapter.
So:
✅ Controllers are Interface Adapters
3. Why Controllers Do NOT Live in adapters/
Although Controllers are adapters by role, they are also:
• Framework-specific
• Delivery-specific
• Not reusable outside their app
They:
• use routing decorators
• depend on HTTP concepts
• depend on a specific framework
Therefore:
Controllers belong to the delivery application, not to shared adapters.
4. Adapter vs App — The Key Distinction
Concept Meaning
Adapter Architectural role (translator)
App Delivery mechanism (HTTP, UI, CLI)
“Adapter” answers what it does.
“App” answers where it runs.
Both are correct at the same time.
5. Feature-Based Structure (Mandatory for Scale)
Flat technical folders do not scale.
Everything is organized by feature / bounded context.
6. Canonical Project Structure (Strict)
Root
core/ # Application + Domain (pure)
adapters/ # Reusable infrastructure adapters
apps/ # Delivery applications
Core (Feature-Based)
core/
└── racing/
├── domain/
│ ├── entities/
│ ├── value-objects/
│ └── services/
└── application/
├── use-cases/
├── inputs/
├── results/
└── ports/
├── gateways/
└── output/
• No framework imports
• No DTOs
• No controllers
Reusable Adapters (Framework-Agnostic Implementations)
adapters/
└── racing/
├── persistence/
│ ├── TypeOrmRaceRepository.ts
│ └── TypeOrmDriverRepository.ts
├── presentation/
│ └── presenters/
│ └── GetDashboardOverviewPresenter.ts
├── messaging/
│ └── EventPublisher.ts
└── logging/
└── StructuredLogger.ts
These adapters:
• implement Core ports
• are reusable across apps
• do not depend on routing or UI
API App (Delivery Application)
apps/api/
└── racing/
├── controllers/
│ └── DashboardController.ts
├── services/
│ └── DashboardApplicationService.ts
├── dto/
│ └── DashboardOverviewResponseDto.ts
└── module.ts
Responsibilities:
• Controllers translate HTTP → Input
• Application Services orchestrate Use Case + Presenter
• DTOs represent HTTP contracts
• Module wires dependencies
7. Responsibilities by Layer (No Overlap)
Controllers
• HTTP only
• No business logic
• No mapping logic
• Call Application Services only
Application Services (API)
• Instantiate Output Adapters
• Invoke Use Cases
• Return response DTOs
Presenters
• Implement Output Ports
• Map Result → DTO/ViewModel
• Hold state per execution
8. Forbidden Patterns
❌ Controllers inside adapters/
❌ Use Cases inside apps/api
❌ DTOs inside core
❌ Controllers calling Use Cases directly
❌ Business logic in Controllers or Services
9. Final Mental Model
Controllers are adapters by responsibility.
Apps define where adapters live.
This separation allows:
• strict Clean Architecture
• multiple delivery mechanisms
• feature-level scalability
10. One-Line Summary
Controller = Adapter (role), App = Delivery (location).
This document is the authoritative reference for controller placement and adapter roles.

View File

@@ -0,0 +1,468 @@
Clean Architecture Application Services, Use Cases, Ports, and Data Flow (Strict, Final)
This document defines the final, non-ambiguous Clean Architecture setup for the project.
It explicitly covers:
• Use Cases vs Application Services
• Input & Output Ports (and what does not exist)
• API responsibilities
• Frontend responsibilities
• Naming, placement, and dependency rules
• End-to-end flow with concrete paths and code examples
There are no hybrid concepts, no overloaded terms, and no optional interpretations.
1. Architectural Layers (Final)
Domain → Business truth
Application → Use Cases + Application Services
Adapters → API, Persistence, External Systems
Frontend → UI, View Models, UX logic
Only dependency-inward is allowed.
2. Domain Layer (Core / Domain)
What lives here
• Entities (classes)
• Value Objects (classes)
• Domain Services (stateless business logic)
• Domain Events
• Domain Errors / Invariants
What NEVER lives here
• DTOs
• Models
• Ports
• Use Cases
• Application Services
• Framework code
3. Application Layer (Core / Application)
The Application Layer has two distinct responsibilities:
1. Use Cases business decisions
2. Application Services orchestration of multiple use cases
4. Use Cases (Application / Use Cases)
Definition
A Use Case represents one business intent.
Examples:
• CreateLeague
• ApproveSponsorship
• CompleteDriverOnboarding
Rules
• A Use Case:
• contains business logic
• enforces invariants
• operates on domain entities
• communicates ONLY via ports
• A Use Case:
• does NOT orchestrate multiple workflows
• does NOT know HTTP, UI, DB, queues
Structure
core/racing/application/use-cases/
└─ CreateLeagueUseCase.ts
Example
export class CreateLeagueUseCase {
constructor(
private readonly leagueRepository: LeagueRepositoryPort,
private readonly output: CreateLeagueOutputPort
) {}
execute(input: CreateLeagueInputPort): void {
// business rules & invariants
const league = League.create(input.name, input.maxMembers);
this.leagueRepository.save(league);
this.output.presentSuccess(league.id);
}
}
5. Ports (Application / Ports)
The Only Two Kinds of Ports
Everything crossing the Application boundary is a Port.
Input Ports
Input Ports describe what a use case needs.
export interface CreateLeagueInputPort {
readonly name: string;
readonly maxMembers: number;
}
Rules:
• Interfaces only
• No behavior
• No validation logic
Output Ports
Output Ports describe how a use case emits outcomes.
export interface CreateLeagueOutputPort {
presentSuccess(leagueId: string): void;
presentFailure(reason: string): void;
}
Rules:
• No return values
• No getters
• No state
• Use methods, not result objects
6. Application Services (Application / Services)
Definition
An Application Service orchestrates multiple Use Cases.
It exists because:
• No single Use Case should know the whole workflow
• Orchestration is not business logic
Rules
• Application Services:
• call multiple Use Cases
• define execution order
• handle partial failure & compensation
• Application Services:
• do NOT contain business rules
• do NOT modify entities directly
Structure
core/racing/application/services/
└─ LeagueSetupService.ts
Example (with Edge Cases)
export class LeagueSetupService {
constructor(
private readonly createLeague: CreateLeagueUseCase,
private readonly createSeason: CreateSeasonUseCase,
private readonly assignOwner: AssignLeagueOwnerUseCase,
private readonly notify: SendLeagueWelcomeNotificationUseCase
) {}
execute(input: LeagueSetupInputPort): void {
const leagueId = this.createLeague.execute(input);
try {
this.createSeason.execute({ leagueId });
this.assignOwner.execute({ leagueId, ownerId: input.ownerId });
this.notify.execute({ leagueId, ownerId: input.ownerId });
} catch (error) {
// compensation / rollback logic
throw error;
}
}
}
Edge cases that belong ONLY here:
• Partial failure handling
• Workflow order
• Optional steps
• Retry / idempotency logic
7. API Layer (apps/api)
Responsibilities
• Transport (HTTP)
• Validation (request shape)
• Mapping to Input Ports
• Calling Application Services
• Adapting Output Ports
Structure
apps/api/leagues/
├─ LeagueController.ts
├─ presenters/
│ └─ CreateLeaguePresenter.ts
└─ dto/
├─ CreateLeagueRequestDto.ts
└─ CreateLeagueResponseDto.ts
API Presenter (Adapter)
export class CreateLeaguePresenter implements CreateLeagueOutputPort {
private response!: CreateLeagueResponseDto;
presentSuccess(leagueId: string): void {
this.response = { success: true, leagueId };
}
presentFailure(reason: string): void {
this.response = { success: false, errorMessage: reason };
}
getResponse(): CreateLeagueResponseDto {
return this.response;
}
}
8. Frontend Layer (apps/website)
The frontend layer contains UI-specific data shapes. None of these cross into the Core.
Important: `apps/website` is a Next.js delivery app with SSR/RSC. This introduces one additional presentation concept to keep server/client boundaries correct.
There are four distinct frontend data concepts:
1. API DTOs (transport)
2. Command Models (user input / form state)
3. View Models (client-only presentation classes)
4. ViewData (template input, serializable)
8.1 API DTOs (Transport Contracts)
API DTOs represent exact HTTP contracts exposed by the backend.
They are usually generated from OpenAPI or manually mirrored.
apps/website/lib/dtos/
└─ CreateLeagueResponseDto.ts
Rules:
• Exact mirror of backend response
• No UI logic
• No derived values
• Never used directly by components
8.2 Command Models (User Input / Form State)
Command Models represent user intent before submission.
They are frontend-only and exist to manage:
• form state
• validation feedback
• step-based wizards
They are NOT:
• domain objects
• API DTOs
• View Models
apps/website/lib/commands/
└─ CreateLeagueCommandModel.ts
Rules:
• Classes (stateful)
• May contain client-side validation
• May contain UX-specific helpers (step validation, dirty flags)
• Must expose a method to convert to an API Request DTO
Example responsibility:
• hold incomplete or invalid user input
• guide the user through multi-step flows
• prepare data for submission
Command Models:
• are consumed by components
• are passed into services
• are never sent directly over HTTP
8.3 View Models (UI Display State)
View Models represent fully prepared UI state after data is loaded.
apps/website/lib/view-models/
└─ CreateLeagueViewModel.ts
Rules:
• Classes only
• UI logic allowed (formatting, labels, derived flags)
• No domain logic
• No mutation after construction
SSR/RSC rule (website-only):
• View Models are client-only and MUST NOT cross server-to-client boundaries.
• Templates MUST NOT accept View Models.
8.4 Website Presenters (DTO → ViewModel)
Website Presenters are pure mappers.
export class CreateLeaguePresenter {
present(dto: CreateLeagueResponseDto): CreateLeagueViewModel {
return new CreateLeagueViewModel(dto);
}
}
Rules:
• Input: API DTOs
• Output: View Models
• No side effects
• No API calls
8.5 Website Services (Orchestration)
Website Services orchestrate:
• Command Models
• API Client calls
• Presenter mappings
export class LeagueService {
async createLeague(command: CreateLeagueCommandModel): Promise<CreateLeagueViewModel> {
const dto = await this.api.createLeague(command.toRequestDto());
return this.presenter.present(dto);
}
}
Rules:
• Services accept Command Models
• Services return View Models
• Components never call API clients directly
View Models (UI State)
apps/website/lib/view-models/
└─ CreateLeagueViewModel.ts
export class CreateLeagueViewModel {
constructor(private readonly dto: CreateLeagueResponseDto) {}
get message(): string {
return this.dto.success
? 'League created successfully'
: this.dto.errorMessage ?? 'Creation failed';
}
}
Rules:
• Classes only
• UI logic allowed
• No domain logic
Website Presenter (DTO → ViewModel)
export class CreateLeaguePresenter {
present(dto: CreateLeagueResponseDto): CreateLeagueViewModel {
return new CreateLeagueViewModel(dto);
}
}
Website Service (Orchestration)
export class LeagueService {
constructor(
private readonly api: LeaguesApiClient,
private readonly presenter: CreateLeaguePresenter
) {}
async createLeague(input: unknown): Promise<CreateLeagueViewModel> {
const dto = await this.api.createLeague(input);
return this.presenter.present(dto);
}
}
9. Full End-to-End Flow (Final)
UI Component
→ Website Service
→ API Client
→ HTTP Request DTO
→ API Controller
→ Application Service
→ Use Case(s)
→ Domain
→ Output Port
→ API Presenter
→ HTTP Response DTO
→ Website Presenter
→ View Model
→ UI
10. Final Non-Negotiable Rules
• Core knows ONLY Ports + Domain
• Core has NO Models, DTOs, or ViewModels
• API talks ONLY to Application Services
• Controllers NEVER call Use Cases directly
• Frontend Components see ONLY ViewData (Templates) or ViewModels (Client orchestrators)
• API DTOs never cross into Templates
• View Models never cross into Templates
11. Final Merksatz
Use Cases decide.
Application Services orchestrate.
Adapters translate.
UI presents.
If a class violates more than one of these roles, it is incorrectly placed.
8.3.1 ViewData (Template Input)
ViewData is the only allowed input for Templates in `apps/website`.
Definition:
• JSON-serializable data structure
• Contains only primitives/arrays/plain objects
• Ready to render: Templates perform no formatting and no derived computation
Rules:
• ViewData is built in client code from:
1) Page DTO (initial SSR-safe render)
2) ViewModel (post-hydration enhancement)
• ViewData MUST NOT contain ViewModel instances or Display Object instances.
Authoritative details:
• [docs/architecture/website/VIEW_DATA.md](docs/architecture/website/VIEW_DATA.md:1)
• [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1)

View File

@@ -0,0 +1,315 @@
# Feature Availability (Modes + Feature Flags)
This document defines a clean, consistent system for enabling/disabling functionality across:
- API endpoints
- Website links/navigation
- Website components
It is designed to support:
- test mode
- maintenance mode
- disabling features due to risk/issues
- coming soon features
- future super admin flag management
It is aligned with the hard separation of responsibilities in `Blockers & Guards`:
- Frontend uses Blockers (UX best-effort)
- Backend uses Guards (authoritative enforcement)
See: docs/architecture/BLOCKER_GUARDS.md
---
## 1) Core Principle
Availability is decided once, then applied in multiple places.
- Backend Guards enforce availability for correctness and security.
- Frontend Blockers reflect availability for UX, but must never be relied on for enforcement.
If it must be enforced, it is a Guard.
If it only improves UX, it is a Blocker.
---
## 2) Definitions (Canonical Vocabulary)
### 2.1 Operational Mode (system-level)
A small, global state representing operational posture.
Recommended enum:
- normal
- maintenance
- test
Operational Mode is:
- authoritative in backend
- typically environment-scoped
- required for rapid response (maintenance must be runtime-changeable)
### 2.2 Feature State (capability-level)
A per-feature state machine (not a boolean).
Recommended enum:
- enabled
- disabled
- coming_soon
- hidden
Semantics:
- enabled: feature is available and advertised
- disabled: feature exists but must not be used (safety kill switch)
- coming_soon: may be visible in UI as teaser, but actions are blocked
- hidden: not visible/advertised; actions are blocked (safest default)
### 2.3 Capability
A named unit of functionality (stable key) used consistently across API + website.
Examples:
- races.create
- payments.checkout
- sponsor.portal
- stewarding.protests
A capability key is a contract.
### 2.4 Action Type
Availability decisions vary by the type of action:
- view: read-only operations (pages, GET endpoints)
- mutate: state-changing operations (POST/PUT/PATCH/DELETE)
---
## 3) Policy Model (What Exists)
### 3.1 FeatureAvailabilityPolicy (single evaluation model)
One evaluation function produces a decision.
Inputs:
- environment (dev/test/prod)
- operationalMode (normal/maintenance/test)
- capabilityKey (string)
- actionType (view/mutate)
- actorContext (anonymous/authenticated; roles later)
Outputs:
- allow: boolean
- publicReason: one of maintenance | disabled | coming_soon | hidden | not_configured
- uxHint: optional { messageKey, redirectPath, showTeaser }
The same decision model is reused by:
- API Guard enforcement
- Website navigation visibility
- Website component rendering/disablement
### 3.2 Precedence (where values come from)
To avoid “mystery behavior”, use strict precedence:
1. runtime overrides (highest priority)
2. build-time environment configuration
3. code defaults (lowest priority, should be safe: hidden/disabled)
Rationale:
- runtime overrides enable emergency response without rebuild
- env config enables environment-specific defaults
- code defaults keep behavior deterministic if config is missing
---
## 4) Evaluation Rules (Deterministic, Explicit)
### 4.1 Maintenance mode rules
Maintenance must be able to block the platform fast and consistently.
Default behavior:
- mutate actions: denied unless explicitly allowlisted
- view actions: allowed only for a small allowlist (status page, login, health, static public routes)
This creates a safe “fail closed” posture.
Optional refinement:
- define a maintenance allowlist for critical reads (e.g., dashboards for operators)
### 4.2 Test mode rules
Test mode should primarily exist in non-prod, and should be explicit in prod.
Recommended behavior:
- In prod, test mode should not be enabled accidentally.
- In test environments, test mode may:
- enable test-only endpoints
- bypass external integrations (through adapters)
- relax rate limits
- expose test banners in UI (Blocker-level display)
### 4.3 Feature state rules (per capability)
Given a capability state:
- enabled:
- allow view + mutate (subject to auth/roles)
- visible in UI
- coming_soon:
- allow view of teaser pages/components
- deny mutate and deny sensitive reads
- visible in UI with Coming Soon affordances
- disabled:
- deny view + mutate
- hidden in nav by default
- hidden:
- deny view + mutate
- never visible in UI
Note:
- “disabled” and “hidden” are both blocked; the difference is UI and information disclosure.
### 4.4 Missing configuration
If a capability is not configured:
- treat as hidden (fail closed)
- optionally log a warning (server-side)
---
## 5) Enforcement Mapping (Where Each Requirement Lives)
This section is the “wiring contract” across layers.
### 5.1 API endpoints (authoritative)
- Enforce via Backend Guards (NestJS CanActivate).
- Endpoints must declare the capability they require.
Mapping to HTTP:
- maintenance: 503 Service Unavailable (preferred for global maintenance)
- disabled/hidden: 404 Not Found (avoid advertising unavailable capabilities)
- coming_soon: 404 Not Found publicly, or 409 Conflict internally if you want explicit semantics for trusted clients later
Guideline:
- External clients should not get detailed feature availability information unless explicitly intended.
### 5.2 Website links / navigation (UX)
- Enforce via Frontend Blockers.
- Hide links when state is disabled/hidden.
- For coming_soon, show link but route to teaser page or disable with explanation.
Rules:
- Never assume hidden in UI equals enforced on server.
- UI should degrade gracefully (API may still block).
### 5.3 Website components (UX)
- Use Blockers to:
- hide components for hidden/disabled
- show teaser content for coming_soon
- disable buttons or flows for coming_soon/disabled, with consistent messaging
Recommendation:
- Provide a single reusable component (FeatureBlocker) that consumes policy decisions and renders:
- children when allowed
- teaser when coming_soon
- null or fallback when disabled/hidden
---
## 6) Build-Time vs Runtime (Clean, Predictable)
### 6.1 Build-time flags (require rebuild/redeploy)
What they are good for:
- preventing unfinished UI code from shipping in a bundle
- cutting entire routes/components from builds for deterministic releases
Limitations:
- NEXT_PUBLIC_* values are compiled into the client bundle; changing them does not update clients without rebuild.
Use build-time flags for:
- experimental UI
- “not yet shipped” components/routes
- simplifying deployments (pre-launch vs alpha style gating)
### 6.2 Runtime flags (no rebuild)
What they are for:
- maintenance mode
- emergency disable for broken endpoints
- quickly hiding risky features
Runtime flags must be available to:
- API Guards (always)
- Website SSR/middleware optionally
- Website client optionally (for UX only)
Key tradeoff:
- runtime access introduces caching and latency concerns
- treat runtime policy reads as cached, fast, and resilient
Recommended approach:
- API is authoritative source of runtime policy
- website can optionally consume a cached policy snapshot endpoint
---
## 7) Storage and Distribution (Now + Future Super Admin)
### 7.1 Now (no super admin UI)
Use a single “policy snapshot” stored in one place and read by the API, with caching.
Options (in priority order):
1. Remote KV/DB-backed policy snapshot (preferred for true runtime changes)
2. Environment variable JSON (simpler, but changes require restart/redeploy)
3. Static config file in repo (requires rebuild/redeploy)
### 7.2 Future (super admin UI)
Super admin becomes a writer to the same store.
Non-negotiable:
- The storage schema must be stable and versioned.
Recommended schema (conceptual):
- policyVersion
- operationalMode
- capabilities: map of capabilityKey -> featureState
- allowlists: maintenance view/mutate allowlists
- optional targeting rules later (by role/user)
---
## 8) Data Flow (Conceptual)
```mermaid
flowchart LR
UI[Website UI] --> FB[Frontend Blockers]
FB --> PC[Policy Client]
UI --> API[API Request]
API --> FG[Feature Guard]
FG --> AS[API Application Service]
AS --> UC[Core Use Case]
PC --> PS[Policy Snapshot]
FG --> PS
```
Interpretation:
- Website reads policy for UX (best-effort).
- API enforces policy (authoritative) before any application logic.
---
## 9) Implementation Checklist (For Code Mode)
Backend (apps/api):
- Define capability keys and feature states as shared types in a local module.
- Create FeaturePolicyService that resolves the current policy snapshot (cached).
- Add FeatureFlagGuard (or FeatureAvailabilityGuard) that:
- reads required capability metadata for an endpoint
- evaluates allow/deny with actionType
- maps denial to the chosen HTTP status codes
Frontend (apps/website):
- Add a small PolicyClient that fetches policy snapshot from API (optional for phase 1).
- Add FeatureBlocker component for consistent UI behavior.
- Centralize navigation link definitions and filter them via policy.
Ops/Config:
- Define how maintenance mode is toggled (KV/DB entry or config endpoint restricted to operators later).
- Ensure defaults are safe (fail closed).
---
## 10) Non-Goals (Explicit)
- This system is not an authorization system.
- Roles/permissions are separate (but can be added as actorContext inputs later).
- Blockers never replace Guards.

View File

@@ -0,0 +1,115 @@
# File Structure
## Core
```
core/ # * Business- & Anwendungslogik (framework-frei)
├── shared/ # * Gemeinsame Core-Bausteine
│ ├── domain/ # * Domain-Basistypen
│ │ ├── Entity.ts # * Basisklasse für Entities
│ │ ├── ValueObject.ts # * Basisklasse für Value Objects
│ │ └── DomainError.ts # * Domain-spezifische Fehler
│ └── application/
│ └── ApplicationError.ts # * Use-Case-/Application-Fehler
├── racing/ # * Beispiel-Domain (Bounded Context)
│ ├── domain/ # * Fachliche Wahrheit
│ │ ├── entities/ # * Aggregate Roots & Entities
│ │ │ ├── League.ts # * Aggregate Root
│ │ │ └── Race.ts # * Entity
│ │ ├── value-objects/ # * Unveränderliche Fachwerte
│ │ │ └── LeagueName.ts # * Beispiel VO
│ │ ├── services/ # * Domain Services (Regeln, kein Ablauf)
│ │ │ └── ChampionshipScoringService.ts # * Regel über mehrere Entities
│ │ └── errors/ # * Domain-Invariantenfehler
│ │ └── RacingDomainError.ts
│ │
│ └── application/ # * Anwendungslogik
│ ├── ports/ # * EINZIGE Schnittstellen des Cores
│ │ ├── input/ # * Input Ports (Use-Case-Grenzen)
│ │ │ └── CreateLeagueInputPort.ts
│ │ └── output/ # * Output Ports (Use-Case-Ergebnisse)
│ │ └── CreateLeagueOutputPort.ts
│ │
│ ├── use-cases/ # * Einzelne Business-Intents
│ │ └── CreateLeagueUseCase.ts
│ │
│ └── services/ # * Application Services (Orchestrierung)
│ └── LeagueSetupService.ts # * Koordiniert mehrere Use Cases
```
## Adapters
```
adapters/ # * Alle äußeren Implementierungen
├── persistence/ # * Datenhaltung
│ ├── typeorm/ # Konkrete DB-Technologie
│ │ ├── entities/ # ORM-Entities (nicht Domain!)
│ │ └── repositories/ # * Implementieren Core-Ports
│ │ └── LeagueRepository.ts
│ └── inmemory/ # Test-/Dev-Implementierungen
│ └── LeagueRepository.ts
├── notifications/ # Externe Systeme
│ └── EmailNotificationAdapter.ts # Implementiert Notification-Port
├── logging/ # Logging / Telemetrie
│ └── ConsoleLoggerAdapter.ts # Adapter für Logger-Port
└── bootstrap/ # Initialisierung / Seeding
└── EnsureInitialData.ts # App-Start-Logik
```
## API
```
apps/api/ # * Delivery Layer (HTTP)
├── app.module.ts # * Framework-Zusammenbau
├── leagues/ # * Feature-Modul
│ ├── LeagueController.ts # * HTTP → Application Service
│ │
│ ├── dto/ # * Transport-DTOs (HTTP)
│ │ ├── CreateLeagueRequestDto.ts # * Request-Shape
│ │ └── CreateLeagueResponseDto.ts # * Response-Shape
│ │
│ └── presenters/ # * Output-Port-Adapter
│ └── CreateLeaguePresenter.ts # * Core Output → HTTP Response
└── shared/ # API-spezifisch
└── filters/ # Exception-Handling
```
## Frontend
```
apps/website/ # * Frontend (UI)
├── app/ # * Next.js Routen
│ └── leagues/ # * Page-Level
│ └── page.tsx
├── components/ # * Reine UI-Komponenten
│ └── LeagueForm.tsx
├── lib/
│ ├── api/ # * HTTP-Client
│ │ └── LeaguesApiClient.ts # * Gibt NUR API DTOs zurück
│ │
│ ├── dtos/ # * API-Vertrags-Typen
│ │ └── CreateLeagueResponseDto.ts
│ │
│ ├── commands/ # * Command Models (Form State)
│ │ └── CreateLeagueCommandModel.ts
│ │
│ ├── presenters/ # * DTO → ViewModel Mapper
│ │ └── CreateLeaguePresenter.ts
│ │
│ ├── view-models/ # * UI-State
│ │ └── CreateLeagueViewModel.ts
│ │
│ ├── services/ # * Frontend-Orchestrierung
│ │ └── LeagueService.ts
│ │
│ └── blockers/ # UX-Schutz (Throttle, Submit)
│ ├── SubmitBlocker.ts
│ └── ThrottleBlocker.ts
```

View File

@@ -0,0 +1,245 @@
Logging & Correlation ID Design Guide (Clean Architecture)
This document defines a clean, strict, and production-ready logging architecture with correlation IDs.
It removes ambiguity around:
• where logging belongs
• how context is attached
• how logs stay machine-readable
• how Clean Architecture boundaries are preserved
The rules below are non-negotiable.
Core Principles
Logs are data, not text.
Context is injected, never constructed in the Core.
Logging must:
• be machine-readable
• be framework-agnostic in the Core
• support correlation across requests
• be safe for parallel execution
Architectural Responsibilities
Core
• describes intent
• never knows about correlation IDs
• never knows about log destinations
App Layer (API)
• defines runtime context (request, user)
• binds correlation IDs
Adapters
• implement concrete loggers (console, file, structured)
• decide formatting and transport
1. Logger Port (Core)
Purpose
Defines what logging means, without defining how logging works.
Rules
• exactly one logging interface
• no framework imports
• no correlation or runtime context
Location
core/shared/application/LoggerPort.ts
Contract
• debug(message, meta?)
• info(message, meta?)
• warn(message, meta?)
• error(message, meta?)
Messages are semantic.
Metadata is optional and structured.
2. Request Context (App Layer)
Purpose
Represents runtime execution context.
Contains
• correlationId
• optional userId
Rules
• never visible to Core
• created per request
Location
apps/api/context/RequestContext.ts
3. Logger Implementations (Adapters)
Purpose
Provide concrete logging behavior.
Rules
• implement LoggerPort
• accept context via constructor
• produce structured logs
• no business logic
Examples
• ConsoleLogger
• FileLogger
• PinoLogger
• LokiLogger
Location
adapters/logging/
4. Logger Factory
Purpose
Creates context-bound logger instances.
Rules
• factory is injected
• logger instances are short-lived
• component name is bound here
Location
adapters/logging/LoggerFactory.ts
adapters/logging/LoggerFactoryImpl.ts
5. Correlation ID Handling
Where it lives
• API middleware
• message envelopes
• background job contexts
Rules
• generated once per request
• propagated across async boundaries
• never generated in the Core
6. Usage Rules by Layer
Layer Logging Allowed Notes
Domain ❌ No Throw domain errors instead
Use Cases ⚠️ Minimal Business milestones only
API Services ✅ Yes Main logging location
Adapters ✅ Yes IO, integration, failures
Frontend ⚠️ Limited Errors + analytics only
7. Forbidden Patterns
❌ Manual string prefixes ([ServiceName])
❌ Global/singleton loggers with mutable state
❌ any in logger abstractions
❌ Correlation IDs in Core
❌ Logging inside domain entities
8. File Structure (Final)
core/
└── shared/
└── application/
└── LoggerPort.ts # * required
apps/api/
├── context/
│ └── RequestContext.ts # * required
├── middleware/
│ └── CorrelationMiddleware.ts
└── modules/
└── */
└── *Service.ts
adapters/
└── logging/
├── LoggerFactory.ts # * required
├── LoggerFactoryImpl.ts # * required
├── ConsoleLogger.ts # optional
├── FileLogger.ts # optional
└── PinoLogger.ts # optional
Mental Model (Final)
The Core describes events.
The App provides context.
Adapters deliver telemetry.
If any layer violates this, the architecture is broken.
Summary
• one LoggerPort in the Core
• context bound outside the Core
• adapters implement logging destinations
• correlation IDs are runtime concerns
• logs are structured, searchable, and safe
This setup is:
• Clean Architecture compliant
• production-ready
• scalable
• refactor-safe

View File

@@ -0,0 +1,251 @@
Services Design Guide (Clean Architecture)
This document defines all service types used across the system and assigns clear, non-overlapping responsibilities.
It exists to remove ambiguity around the word “service”, which is heavily overloaded.
The rules below are strict.
Overview
The system contains four distinct service categories, each in a different layer:
1. Frontend Services
2. API Services
3. Core Application Services
4. Core Domain Services
They must never be mixed.
1. Frontend Services
Purpose
Frontend services orchestrate UI-driven workflows.
They answer the question:
“How does the UI obtain and submit data?”
Responsibilities
Frontend services MAY:
• call API clients
• apply client-side guards (blockers, throttles)
• create View Models
• orchestrate multiple API calls
• handle recoverable UI errors
Frontend services MUST NOT:
• contain business rules
• validate domain invariants
• modify domain state
• know about core domain objects
Placement
apps/website/lib/services/
Example
• LeagueService
• RaceService
• AuthService
Each service is UI-facing, not business-facing.
2. API Services (Application Services)
Purpose
API services adapt HTTP-level concerns to core use cases.
They answer the question:
“How does an external client interact with the core?”
Responsibilities
API services MAY:
• orchestrate multiple use cases
• perform authorization checks
• map transport input to use-case input
• coordinate transactions
API services MUST NOT:
• contain domain logic
• enforce business invariants
• build domain entities
• return domain objects
Placement
apps/api/**/ApplicationService.ts
Example
• LeagueApplicationService
API services are delivery-layer coordinators.
3. Core Application Services (Use Cases)
Purpose
Core application services implement business use cases.
They answer the question:
“What does the system do?”
Responsibilities
Use Cases MUST:
• accept primitive input only
• create Value Objects
• create or modify Entities
• enforce business rules
• call repositories via ports
• communicate results via output ports
Use Cases MUST NOT:
• know about HTTP, UI, or frameworks
• return DTOs
• perform persistence directly
Placement
core/<context>/application/commands/
core/<context>/application/queries/
Example
• CreateLeagueUseCase
• ApplyPenaltyUseCase
• GetLeagueStandingsQuery
Use Cases define system behavior.
4. Core Domain Services
Purpose
Domain services encapsulate domain logic that does not belong to a single entity.
They answer the question:
“What rules exist that span multiple domain objects?”
Responsibilities
Domain services MAY:
• coordinate multiple entities
• compute derived domain values
• enforce cross-aggregate rules
Domain services MUST:
• use only domain concepts
• return domain objects or primitives
Domain services MUST NOT:
• access repositories
• depend on application services
• perform IO
Placement
core/<context>/domain/services/
Example
• SeasonConfigurationFactory
• ChampionshipAggregator
• StrengthOfFieldCalculator
Domain services protect business integrity.
Dependency Rules (Non-Negotiable)
Frontend Service
→ API Client
→ API Service
→ Core Use Case
→ Domain Service / Entity
Reverse dependencies are forbidden.
Anti-Patterns (Forbidden)
❌ Frontend calling core directly
❌ API service constructing domain entities
❌ Use case returning DTOs
❌ Domain service accessing repositories
❌ Single class acting as multiple service types
Naming Conventions
Layer Naming
Frontend *Service
API *ApplicationService
Core Application *UseCase, *Query
Core Domain *Service, *Factory, *Calculator
Mental Model (Final)
Services coordinate.
Use Cases decide.
Domain enforces truth.
Adapters translate.
If a class violates this mental model, it is in the wrong layer.
Final Summary
• “Service” means different things in different layers
• Mixing service types causes architectural decay
• Clean Architecture remains simple when roles stay pure
This document defines the only allowed service roles in the system.

View File

@@ -0,0 +1,640 @@
# Unified Authentication & Authorization Architecture
## Executive Summary
This document defines a **clean, predictable, and secure** authentication and authorization architecture that eliminates the current "fucking unpredictable mess" by establishing clear boundaries between server-side and client-side responsibilities.
## Current State Analysis
### What's Wrong
1. **Confusing Layers**: Middleware, RouteGuards, AuthGuards, Blockers, Gateways - unclear hierarchy
2. **Mixed Responsibilities**: Server and client both doing similar checks inconsistently
3. **Inconsistent Patterns**: Some routes use middleware, some use guards, some use both
4. **Role Confusion**: Frontend has role logic that should be server-only
5. **Debugging Nightmare**: Multiple layers with unclear flow
### What's Actually Working
1. **API Guards**: Clean NestJS pattern with `@Public()`, `@RequireRoles()`
2. **Basic Middleware**: Route protection works at edge
3. **Auth Context**: Session management exists
4. **Permission Model**: Documented in AUTHORIZATION.md
## Core Principle: Server as Source of Truth
**Golden Rule**: The API server is the **single source of truth** for authentication and authorization. The client is a dumb terminal that displays what the server allows.
### Server-Side Responsibilities (API)
#### 1. Authentication
-**Session Validation**: Verify JWT/session cookies
-**Identity Resolution**: Who is this user?
-**Token Management**: Issue, refresh, revoke tokens
-**UI Redirects**: Never redirect, return 401/403
#### 2. Authorization
-**Role Verification**: Check user roles against requirements
-**Permission Evaluation**: Check capabilities (view/mutate)
-**Scope Resolution**: Determine league/sponsor/team context
-**Access Denial**: Return 401/403 with clear messages
-**Client State**: Never trust client-provided identity
#### 3. Data Filtering
-**Filter sensitive data**: Remove fields based on permissions
-**Scope-based queries**: Only return data user can access
-**Client-side filtering**: Never rely on frontend to hide data
### Client-Side Responsibilities (Website)
#### 1. UX Enhancement
-**Loading States**: Show "Verifying authentication..."
-**Redirects**: Send unauthenticated users to login
-**UI Hiding**: Hide buttons/links user can't access
-**Feedback**: Show "Access denied" messages
-**Security**: Never trust client checks for security
#### 2. Session Management
-**Session Cache**: Store session in context
-**Auto-refresh**: Fetch session on app load
-**Logout Flow**: Clear local state, call API logout
-**Role Logic**: Don't make decisions based on roles
#### 3. Route Protection
-**Middleware**: Basic auth check at edge
-**Layout Guards**: Verify session before rendering
-**Page Guards**: Additional verification (defense in depth)
-**Authorization**: Don't check permissions, let API fail
## Clean Architecture Layers
```
┌─────────────────────────────────────────────────────────────┐
│ USER REQUEST │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 1. EDGE MIDDLEWARE (Next.js) │
│ • Check for session cookie │
│ • Public routes: Allow through │
│ • Protected routes: Require auth cookie │
│ • Redirect to login if no cookie │
│ • NEVER check roles here │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. API REQUEST (with session cookie) │
│ • NestJS AuthenticationGuard extracts user from session │
│ • Attaches user identity to request │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. API AUTHORIZATION GUARD │
│ • Check route metadata: @Public(), @RequireRoles() │
│ • Evaluate permissions based on user identity │
│ • Return 401 (unauthenticated) or 403 (forbidden) │
│ • NEVER redirect, NEVER trust client identity │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. API CONTROLLER │
│ • Execute business logic │
│ • Filter data based on permissions │
│ • Return appropriate response │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. CLIENT RESPONSE HANDLING │
│ • 200: Render data │
│ • 401: Redirect to login with returnTo │
│ • 403: Show "Access denied" message │
│ • 404: Show "Not found" (for non-disclosure) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 6. COMPONENT RENDERING │
│ • Layout guards: Verify session exists │
│ • Route guards: Show loading → content or redirect │
│ • UI elements: Hide buttons user can't use │
└─────────────────────────────────────────────────────────────┘
```
## Implementation: Clean Route Protection
### Step 1: Simplify Middleware (Edge Layer)
**File**: `apps/website/middleware.ts`
```typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Edge Middleware - Simple and Predictable
*
* Responsibilities:
* 1. Allow public routes (static assets, auth pages, discovery)
* 2. Check for session cookie on protected routes
* 3. Redirect to login if no cookie
* 4. Let everything else through (API handles authorization)
*/
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 1. Always allow static assets and API routes
if (
pathname.startsWith('/_next/') ||
pathname.startsWith('/api/') ||
pathname.match(/\.(svg|png|jpg|jpeg|gif|webp|ico|css|js)$/)
) {
return NextResponse.next();
}
// 2. Define public routes (no auth required)
const publicRoutes = [
'/',
'/auth/login',
'/auth/signup',
'/auth/forgot-password',
'/auth/reset-password',
'/auth/iracing',
'/auth/iracing/start',
'/auth/iracing/callback',
'/leagues',
'/drivers',
'/teams',
'/leaderboards',
'/races',
'/sponsor/signup',
];
// 3. Check if current route is public
const isPublic = publicRoutes.includes(pathname) ||
publicRoutes.some(route => pathname.startsWith(route + '/'));
if (isPublic) {
// Special handling: redirect authenticated users away from auth pages
const hasAuthCookie = request.cookies.has('gp_session');
const authRoutes = ['/auth/login', '/auth/signup', '/auth/forgot-password', '/auth/reset-password'];
if (authRoutes.includes(pathname) && hasAuthCookie) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
// 4. Protected routes: require session cookie
const hasAuthCookie = request.cookies.has('gp_session');
if (!hasAuthCookie) {
const loginUrl = new URL('/auth/login', request.url);
loginUrl.searchParams.set('returnTo', pathname);
return NextResponse.redirect(loginUrl);
}
// 5. User has cookie, let them through
// API will handle actual authorization
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|_next/data|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|mp4|webm|mov|avi)$).*)',
],
};
```
### Step 2: Clean Layout Guards (Client Layer)
**File**: `apps/website/lib/guards/AuthLayout.tsx`
```typescript
'use client';
import { ReactNode, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth/AuthContext';
import { LoadingState } from '@/components/shared/LoadingState';
interface AuthLayoutProps {
children: ReactNode;
requireAuth?: boolean;
redirectTo?: string;
}
/**
* AuthLayout - Client-side session verification
*
* Responsibilities:
* 1. Verify user session exists
* 2. Show loading state while checking
* 3. Redirect to login if no session
* 4. Render children if authenticated
*
* Does NOT check permissions - that's the API's job
*/
export function AuthLayout({
children,
requireAuth = true,
redirectTo = '/auth/login'
}: AuthLayoutProps) {
const router = useRouter();
const { session, loading } = useAuth();
useEffect(() => {
if (!requireAuth) return;
// If done loading and no session, redirect
if (!loading && !session) {
const returnTo = window.location.pathname;
router.push(`${redirectTo}?returnTo=${encodeURIComponent(returnTo)}`);
}
}, [loading, session, router, requireAuth, redirectTo]);
// Show loading state
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-deep-graphite">
<LoadingState message="Verifying authentication..." />
</div>
);
}
// Show nothing while redirecting (or show error if not redirecting)
if (requireAuth && !session) {
return null;
}
// Render protected content
return <>{children}</>;
}
```
### Step 3: Role-Based Layout (Client Layer)
**File**: `apps/website/lib/guards/RoleLayout.tsx`
```typescript
'use client';
import { ReactNode, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth/AuthContext';
import { LoadingState } from '@/components/shared/LoadingState';
interface RoleLayoutProps {
children: ReactNode;
requiredRoles: string[];
redirectTo?: string;
}
/**
* RoleLayout - Client-side role verification
*
* Responsibilities:
* 1. Verify user session exists
* 2. Show loading state
* 3. Redirect if no session OR insufficient role
* 4. Render children if authorized
*
* Note: This is UX enhancement. API is still source of truth.
*/
export function RoleLayout({
children,
requiredRoles,
redirectTo = '/auth/login'
}: RoleLayoutProps) {
const router = useRouter();
const { session, loading } = useAuth();
useEffect(() => {
if (loading) return;
// No session? Redirect
if (!session) {
const returnTo = window.location.pathname;
router.push(`${redirectTo}?returnTo=${encodeURIComponent(returnTo)}`);
return;
}
// Has session but wrong role? Redirect
if (requiredRoles.length > 0 && !requiredRoles.includes(session.role || '')) {
// Could redirect to dashboard or show access denied
router.push('/dashboard');
return;
}
}, [loading, session, router, requiredRoles, redirectTo]);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-deep-graphite">
<LoadingState message="Verifying access..." />
</div>
);
}
if (!session || (requiredRoles.length > 0 && !requiredRoles.includes(session.role || ''))) {
return null;
}
return <>{children}</>;
}
```
### Step 4: Usage Examples
#### Public Route (No Protection)
```typescript
// app/leagues/page.tsx
export default function LeaguesPage() {
return <LeaguesList />;
}
```
#### Authenticated Route
```typescript
// app/dashboard/layout.tsx
import { AuthLayout } from '@/lib/guards/AuthLayout';
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<AuthLayout requireAuth={true}>
<div className="min-h-screen bg-deep-graphite">
{children}
</div>
</AuthLayout>
);
}
// app/dashboard/page.tsx
export default function DashboardPage() {
// No additional auth checks needed - layout handles it
return <DashboardContent />;
}
```
#### Role-Protected Route
```typescript
// app/admin/layout.tsx
import { RoleLayout } from '@/lib/guards/RoleLayout';
export default function AdminLayout({ children }: { children: ReactNode }) {
return (
<RoleLayout requiredRoles={['owner', 'admin']}>
<div className="min-h-screen bg-deep-graphite">
{children}
</div>
</RoleLayout>
);
}
// app/admin/page.tsx
export default function AdminPage() {
// No additional checks - layout handles role verification
return <AdminDashboard />;
}
```
#### Scoped Route (League Admin)
```typescript
// app/leagues/[id]/settings/layout.tsx
import { AuthLayout } from '@/lib/guards/AuthLayout';
import { LeagueAccessGuard } from '@/components/leagues/LeagueAccessGuard';
export default function LeagueSettingsLayout({
children,
params
}: {
children: ReactNode;
params: { id: string };
}) {
return (
<AuthLayout requireAuth={true}>
<LeagueAccessGuard leagueId={params.id}>
<div className="min-h-screen bg-deep-graphite">
{children}
</div>
</LeagueAccessGuard>
</AuthLayout>
);
}
```
### Step 5: API Guard Cleanup
**File**: `apps/api/src/domain/auth/AuthorizationGuard.ts`
```typescript
import { CanActivate, ExecutionContext, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthorizationService } from './AuthorizationService';
import { PUBLIC_ROUTE_METADATA_KEY } from './Public';
import { REQUIRE_ROLES_METADATA_KEY, RequireRolesMetadata } from './RequireRoles';
type AuthenticatedRequest = {
user?: { userId: string };
};
@Injectable()
export class AuthorizationGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly authorizationService: AuthorizationService,
) {}
canActivate(context: ExecutionContext): boolean {
const handler = context.getHandler();
const controllerClass = context.getClass();
// 1. Check if route is public
const isPublic = this.reflector.getAllAndOverride<{ public: true } | undefined>(
PUBLIC_ROUTE_METADATA_KEY,
[handler, controllerClass],
)?.public ?? false;
if (isPublic) {
return true;
}
// 2. Get required roles
const rolesMetadata = this.reflector.getAllAndOverride<RequireRolesMetadata | undefined>(
REQUIRE_ROLES_METADATA_KEY,
[handler, controllerClass],
) ?? null;
// 3. Get user identity from request (set by AuthenticationGuard)
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
const userId = request.user?.userId;
// 4. Deny if not authenticated
if (!userId) {
throw new UnauthorizedException('Authentication required');
}
// 5. If no roles required, allow
if (!rolesMetadata || rolesMetadata.anyOf.length === 0) {
return true;
}
// 6. Check if user has required role
const userRoles = this.authorizationService.getRolesForUser(userId);
const hasAnyRole = rolesMetadata.anyOf.some((r) => userRoles.includes(r));
if (!hasAnyRole) {
throw new ForbiddenException(`Access requires one of: ${rolesMetadata.anyOf.join(', ')}`);
}
return true;
}
}
```
### Step 6: Client Error Handling
**File**: `apps/website/lib/api/client.ts`
```typescript
/**
* API Client with unified error handling
*/
export async function apiFetch(url: string, options: RequestInit = {}) {
const response = await fetch(url, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
// Handle authentication errors
if (response.status === 401) {
// Session expired or invalid
window.location.href = '/auth/login?returnTo=' + encodeURIComponent(window.location.pathname);
throw new Error('Authentication required');
}
// Handle authorization errors
if (response.status === 403) {
const error = await response.json().catch(() => ({ message: 'Access denied' }));
throw new Error(error.message || 'You do not have permission to access this resource');
}
// Handle not found
if (response.status === 404) {
throw new Error('Resource not found');
}
// Handle server errors
if (response.status >= 500) {
throw new Error('Server error. Please try again later.');
}
return response;
}
```
## Benefits of This Architecture
### 1. **Clear Responsibilities**
- Server: Security and authorization
- Client: UX and user experience
### 2. **Predictable Flow**
```
User → Middleware → API → Guard → Controller → Response → Client
```
### 3. **Easy Debugging**
- Check middleware logs
- Check API guard logs
- Check client session state
### 4. **Secure by Default**
- API never trusts client
- Client never makes security decisions
- Defense in depth without confusion
### 5. **Scalable**
- Easy to add new routes
- Easy to add new roles
- Easy to add new scopes
## Migration Plan
### Phase 1: Clean Up Middleware (1 day)
- [ ] Simplify `middleware.ts` to only check session cookie
- [ ] Remove role logic from middleware
- [ ] Define clear public routes list
### Phase 2: Create Clean Guards (2 days)
- [ ] Create `AuthLayout` component
- [ ] Create `RoleLayout` component
- [ ] Create `ScopedLayout` component
- [ ] Remove old RouteGuard/AuthGuard complexity
### Phase 3: Update Route Layouts (2 days)
- [ ] Update all protected route layouts
- [ ] Remove redundant page-level checks
- [ ] Test all redirect flows
### Phase 4: API Guard Enhancement (1 day)
- [ ] Ensure all endpoints have proper decorators
- [ ] Add missing `@Public()` or `@RequireRoles()`
- [ ] Test 401/403 responses
### Phase 5: Documentation & Testing (1 day)
- [ ] Update all route protection docs
- [ ] Create testing checklist
- [ ] Verify all scenarios work
## Testing Checklist
### Unauthenticated User
- [ ] `/dashboard` → Redirects to `/auth/login?returnTo=/dashboard`
- [ ] `/profile` → Redirects to `/auth/login?returnTo=/profile`
- [ ] `/admin` → Redirects to `/auth/login?returnTo=/admin`
- [ ] `/leagues` → Works (public)
- [ ] `/auth/login` → Works (public)
### Authenticated User (Regular)
- [ ] `/dashboard` → Works
- [ ] `/profile` → Works
- [ ] `/admin` → Redirects to `/dashboard` (no role)
- [ ] `/leagues` → Works (public)
- [ ] `/auth/login` → Redirects to `/dashboard`
### Authenticated User (Admin)
- [ ] `/dashboard` → Works
- [ ] `/profile` → Works
- [ ] `/admin` → Works
- [ ] `/admin/users` → Works
- [ ] `/leagues` → Works (public)
### Session Expiry
- [ ] Navigate to protected route with expired session → Redirect to login
- [ ] Return to original route after login → Works
### API Direct Calls
- [ ] Call protected endpoint without auth → 401
- [ ] Call admin endpoint without role → 403
- [ ] Call public endpoint → 200
## Summary
This architecture eliminates the "fucking unpredictable mess" by:
1. **One Source of Truth**: API server handles all security
2. **Clear Layers**: Middleware → API → Guards → Controller
3. **Simple Client**: UX enhancement only, no security decisions
4. **Predictable Flow**: Always the same path for every request
5. **Easy to Debug**: Each layer has one job
The result: **Clean, predictable, secure authentication and authorization that just works.**