This commit is contained in:
2026-01-11 14:42:54 +01:00
parent 2f0b83f030
commit 90b6e73a22
27 changed files with 980 additions and 2513 deletions

View File

@@ -0,0 +1,51 @@
# Authentication and Authorization (Shared Contract)
This document defines the shared, cross-app contract for authentication and authorization.
It does not define Next.js routing details or NestJS guard wiring.
App-specific documents:
- API enforcement: [`docs/architecture/api/AUTH_FLOW.md`](docs/architecture/api/AUTH_FLOW.md:1)
- Website UX flow: [`docs/architecture/website/WEBSITE_AUTH_FLOW.md`](docs/architecture/website/WEBSITE_AUTH_FLOW.md:1)
## 1) Core principle (non-negotiable)
The API is the single source of truth for:
- who the actor is
- what the actor is allowed to do
The website may improve UX. It does not enforce security.
## 2) Authentication (strict)
Authentication answers:
- who is this actor
Rules:
- the actor identity is derived from the authenticated session
- the client must never be allowed to claim an identity
## 3) Authorization (strict)
Authorization answers:
- is this actor allowed to perform this action
Rules:
- authorization is enforced in the API
- the website may hide or disable UI, but cannot enforce correctness
See: [`docs/architecture/api/AUTHORIZATION.md`](docs/architecture/api/AUTHORIZATION.md:1)
## 4) Shared terminology (hard)
- Guard: API enforcement mechanism
- Blocker: website UX prevention mechanism
Shared contract: [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1)

View File

@@ -0,0 +1,60 @@
# Blockers and Guards (Shared Contract)
This document defines the **shared contract** for Blockers (website UX) and Guards (API enforcement).
If a more specific document conflicts with this one, this shared contract wins.
Related:
- API authorization: [`docs/architecture/api/AUTHORIZATION.md`](docs/architecture/api/AUTHORIZATION.md:1)
- Website delivery-layer contract: [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1)
## 1) Core principle (non-negotiable)
Guards enforce. Blockers prevent.
- Guards protect the system.
- Blockers protect the UX.
There are no exceptions.
## 2) Definitions (strict)
### 2.1 Guard (API)
A Guard is an **API mechanism** that enforces access or execution rules.
If a Guard denies execution, the request does not reach application logic.
### 2.2 Blocker (Website)
A Blocker is a **website mechanism** that prevents an action from being executed.
Blockers exist solely to improve UX and reduce unnecessary requests.
Blockers are not security.
## 3) Responsibility split (hard boundary)
| Aspect | Blocker (Website) | Guard (API) |
|---|---|---|
| Purpose | Prevent execution | Enforce rules |
| Security | ❌ No | ✅ Yes |
| Authority | ❌ Best-effort | ✅ Final |
| Reversible | ✅ Yes | ❌ No |
| Failure effect | UX feedback | HTTP error |
## 4) Naming rules (hard)
- Website uses `*Blocker`.
- API uses `*Guard`.
- Never mix the terms.
- Never implement Guards in the website.
- Never implement Blockers in the API.
## 5) Final rule
If it must be enforced, it is a Guard.
If it only prevents UX mistakes, it is a Blocker.

View File

@@ -1,468 +1,47 @@
Clean Architecture Application Services, Use Cases, Ports, and Data Flow (Strict, Final)
# Clean Architecture Data Flow (Shared Contract)
This document defines the final, non-ambiguous Clean Architecture setup for the project.
This document defines the **shared** data-flow rules that apply across all delivery applications.
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
It does not contain app-specific rules.
There are no hybrid concepts, no overloaded terms, and no optional interpretations.
App-specific contracts:
- Core: [`docs/architecture/core/CORE_DATA_FLOW.md`](docs/architecture/core/CORE_DATA_FLOW.md:1)
- API: [`docs/architecture/api/API_DATA_FLOW.md`](docs/architecture/api/API_DATA_FLOW.md:1)
- Website: [`docs/architecture/website/WEBSITE_DATA_FLOW.md`](docs/architecture/website/WEBSITE_DATA_FLOW.md:1)
1. Architectural Layers (Final)
## 1) Dependency rule (non-negotiable)
Domain → Business truth
Application → Use Cases + Application Services
Adapters → API, Persistence, External Systems
Frontend → UI, View Models, UX logic
Dependencies point inward.
Only dependency-inward is allowed.
```text
Delivery apps → adapters → core
```
Core never depends on delivery apps.
2. Domain Layer (Core / Domain)
## 2) Cross-boundary mapping rule
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.
If data crosses a boundary, it is mapped.
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
- HTTP Request DTO is mapped to Core input.
- Core result is mapped to HTTP Response DTO.
Structure
## 3) Ownership rule
core/racing/application/use-cases/
└─ CreateLeagueUseCase.ts
Each layer owns its data shapes.
Example
- Core owns domain and application models.
- API owns HTTP DTOs.
- Website owns ViewData and ViewModels.
export class CreateLeagueUseCase {
constructor(
private readonly leagueRepository: LeagueRepositoryPort,
private readonly output: CreateLeagueOutputPort
) {}
No layer re-exports another layers models as-is across a boundary.
execute(input: CreateLeagueInputPort): void {
// business rules & invariants
## 4) Non-negotiable rules
const league = League.create(input.name, input.maxMembers);
this.leagueRepository.save(league);
1. Core contains business truth.
2. Delivery apps translate and enforce.
3. Adapters implement ports.
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,84 @@
# Enums (Shared Contract)
This document defines how enums are modeled, placed, and used across the system.
Enums are frequently a source of architectural leakage. This contract removes ambiguity.
## 1) Core principle (non-negotiable)
Enums represent knowledge.
Knowledge must live where it is true.
## 2) Enum categories (strict)
There are four and only four valid enum categories:
1. Domain enums
2. Application workflow enums
3. Transport enums (HTTP contracts)
4. UI enums (website-only)
## 3) Domain enums
Definition:
- business meaning
- affects rules or invariants
Placement:
- `core/**`
Rule:
- domain enums MUST NOT cross a delivery boundary
## 4) Application workflow enums
Definition:
- internal workflow coordination
- not business truth
Placement:
- `core/**`
Rule:
- workflow enums MUST remain internal
## 5) Transport enums
Definition:
- constrain HTTP contracts
Placement:
- `apps/api/**` and `apps/website/**` (as transport representations)
Rules:
- transport enums are copies, not reexports of domain enums
- transport enums MUST NOT be used inside Core
## 6) UI enums
Definition:
- website presentation or interaction state
Placement:
- `apps/website/**`
Rule:
- UI enums MUST NOT leak into API or Core
## 7) Final rule
If an enum crosses a boundary, it is in the wrong place.

View File

@@ -1,315 +1,37 @@
# Feature Availability (Modes + Feature Flags)
# Feature Availability (Shared Contract)
This document defines a clean, consistent system for enabling/disabling functionality across:
- API endpoints
- Website links/navigation
- Website components
This document defines the shared, cross-app system for enabling and disabling capabilities.
It is designed to support:
- test mode
- maintenance mode
- disabling features due to risk/issues
- coming soon features
- future super admin flag management
Feature availability is not authorization.
It is aligned with the hard separation of responsibilities in `Blockers & Guards`:
- Frontend uses Blockers (UX best-effort)
- Backend uses Guards (authoritative enforcement)
Shared contract:
See: docs/architecture/BLOCKER_GUARDS.md
- Blockers and Guards: [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1)
---
## 1) Core Principle
## 1) Core principle (non-negotiable)
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.
- API Guards enforce availability.
- Website Blockers reflect availability for UX.
If it must be enforced, it is a Guard.
If it only improves UX, it is a Blocker.
## 2) Capability model (strict)
---
Inputs to evaluation:
## 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)
- operational mode (normal, maintenance, test)
- capability key (stable string)
- action type (view, mutate)
- actor context (anonymous, authenticated)
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
- allow or deny
- a public reason (maintenance, disabled, coming_soon, hidden, not_configured)
### 3.2 Precedence (where values come from)
To avoid “mystery behavior”, use strict precedence:
## 3) Non-negotiable rules
1. runtime overrides (highest priority)
2. build-time environment configuration
3. code defaults (lowest priority, should be safe: hidden/disabled)
1. Default is deny unless explicitly enabled.
2. The API is authoritative.
3. The website is UX-only.
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

@@ -1,115 +0,0 @@
# 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,56 @@
# Repository Structure (Shared Contract)
This document defines the **physical repository structure**.
It describes **where** code lives, not what responsibilities are.
## 1) Top-level layout (strict)
```text
core/  business logic (framework-free)
adapters/  reusable infrastructure implementations
apps/  delivery applications (API, website)
docs/  documentation
tests/  cross-app tests
```
## 2) Meaning of each top-level folder
### 2.1 `core/`
The Core contains domain and application logic.
See [`docs/architecture/core/CORE_FILE_STRUCTURE.md`](docs/architecture/core/CORE_FILE_STRUCTURE.md:1).
### 2.2 `adapters/`
Adapters are **reusable outer-layer implementations**.
Rules:
- adapters implement Core ports
- adapters contain technical details (DB, external systems)
- adapters do not define HTTP routes
See [`docs/architecture/shared/ADAPTERS.md`](docs/architecture/shared/ADAPTERS.md:1).
### 2.3 `apps/`
Apps are **delivery mechanisms**.
This repo has (at minimum):
- `apps/api` (HTTP API)
- `apps/website` (Next.js website)
See:
- [`docs/architecture/api/API_FILE_STRUCTURE.md`](docs/architecture/api/API_FILE_STRUCTURE.md:1)
- [`docs/architecture/website/WEBSITE_FILE_STRUCTURE.md`](docs/architecture/website/WEBSITE_FILE_STRUCTURE.md:1)
## 3) Non-negotiable rules
1. Business truth lives in `core/`.
2. `apps/*` are delivery apps; they translate and enforce.
3. `adapters/*` implement ports and contain technical details.

View File

@@ -1,640 +0,0 @@
# 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.**