From 90b6e73a22b30bfa39bb96e2e41dfc521fac0bf2 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sun, 11 Jan 2026 14:42:54 +0100 Subject: [PATCH] docs --- docs/architecture/api/API_DATA_FLOW.md | 78 +++ docs/architecture/api/API_FILE_STRUCTURE.md | 38 ++ docs/architecture/api/AUTHORIZATION.md | 6 +- docs/architecture/api/AUTH_FLOW.md | 47 ++ docs/architecture/api/GUARDS.md | 47 ++ docs/architecture/api/USE_CASE_WIRING.md | 38 ++ docs/architecture/core/CORE_DATA_FLOW.md | 92 +++ docs/architecture/core/CORE_FILE_STRUCTURE.md | 57 ++ docs/architecture/core/CQRS.md | 17 +- docs/architecture/core/ENUMS.md | 339 ---------- docs/architecture/core/USECASES.md | 519 +------------- docs/architecture/shared/AUTH_CONTRACT.md | 51 ++ .../shared/BLOCKERS_AND_GUARDS.md | 60 ++ docs/architecture/shared/DATA_FLOW.md | 475 +------------ docs/architecture/shared/ENUMS.md | 84 +++ .../shared/FEATURE_AVAILABILITY.md | 318 +-------- docs/architecture/shared/FILE_STRUCTURE.md | 115 ---- .../shared/REPOSITORY_STRUCTURE.md | 56 ++ .../shared/UNIFIED_AUTH_CONCEPT.md | 640 ------------------ docs/architecture/website/BLOCKERS.md | 51 ++ docs/architecture/website/BLOCKER_GUARDS.md | 154 ----- docs/architecture/website/CLIENT_STATE.md | 3 +- .../website/LOGIN_FLOW_STATE_MACHINE.md | 45 +- .../architecture/website/WEBSITE_AUTH_FLOW.md | 46 ++ docs/architecture/website/WEBSITE_CONTRACT.md | 2 +- .../architecture/website/WEBSITE_DATA_FLOW.md | 65 ++ .../website/WEBSITE_FILE_STRUCTURE.md | 50 ++ 27 files changed, 980 insertions(+), 2513 deletions(-) create mode 100644 docs/architecture/api/API_DATA_FLOW.md create mode 100644 docs/architecture/api/API_FILE_STRUCTURE.md create mode 100644 docs/architecture/api/AUTH_FLOW.md create mode 100644 docs/architecture/api/GUARDS.md create mode 100644 docs/architecture/api/USE_CASE_WIRING.md create mode 100644 docs/architecture/core/CORE_DATA_FLOW.md create mode 100644 docs/architecture/core/CORE_FILE_STRUCTURE.md delete mode 100644 docs/architecture/core/ENUMS.md create mode 100644 docs/architecture/shared/AUTH_CONTRACT.md create mode 100644 docs/architecture/shared/BLOCKERS_AND_GUARDS.md create mode 100644 docs/architecture/shared/ENUMS.md delete mode 100644 docs/architecture/shared/FILE_STRUCTURE.md create mode 100644 docs/architecture/shared/REPOSITORY_STRUCTURE.md delete mode 100644 docs/architecture/shared/UNIFIED_AUTH_CONCEPT.md create mode 100644 docs/architecture/website/BLOCKERS.md delete mode 100644 docs/architecture/website/BLOCKER_GUARDS.md create mode 100644 docs/architecture/website/WEBSITE_AUTH_FLOW.md create mode 100644 docs/architecture/website/WEBSITE_DATA_FLOW.md create mode 100644 docs/architecture/website/WEBSITE_FILE_STRUCTURE.md diff --git a/docs/architecture/api/API_DATA_FLOW.md b/docs/architecture/api/API_DATA_FLOW.md new file mode 100644 index 000000000..4e235f0f0 --- /dev/null +++ b/docs/architecture/api/API_DATA_FLOW.md @@ -0,0 +1,78 @@ +# API Data Flow (Strict) + +This document defines the **apps/api** data flow and responsibilities. + +API scope: + +- `apps/api/**` + +## 1) API role + +The API is a **delivery application**. + +Responsibilities: + +- HTTP transport boundary +- authentication and authorization enforcement +- request validation (transport shape) +- mapping between HTTP DTOs and Core inputs +- calling Core use cases +- mapping Core results into HTTP responses + +## 2) API data types (strict) + +### 2.1 Request DTO + +Definition: HTTP request contract shape. + +Rules: + +- lives in the API layer +- validated at the API boundary +- never enters Core unchanged + +### 2.2 Response DTO + +Definition: HTTP response contract shape. + +Rules: + +- lives in the API layer +- never contains domain objects + +### 2.3 API Presenter + +Definition: mapping logic from Core results to HTTP response DTOs. + +Rules: + +- pure transformation +- no business rules +- may hold state per request + +## 3) Canonical flow + +```text +HTTP Request + ↓ +Guards (auth, authorization, feature availability) + ↓ +Controller (transport-only) + ↓ +Mapping: Request DTO → Core input + ↓ +Core Use Case + ↓ +Mapping: Core result → Response DTO (Presenter) + ↓ +HTTP Response +``` + +## 4) Non-negotiable rules + +1. Controllers contain no business rules. +2. Controllers do not construct domain objects. +3. Core results never leave the API without mapping. + +See authorization model: [`docs/architecture/api/AUTHORIZATION.md`](docs/architecture/api/AUTHORIZATION.md:1). + diff --git a/docs/architecture/api/API_FILE_STRUCTURE.md b/docs/architecture/api/API_FILE_STRUCTURE.md new file mode 100644 index 000000000..50b5b0799 --- /dev/null +++ b/docs/architecture/api/API_FILE_STRUCTURE.md @@ -0,0 +1,38 @@ +# API File Structure (Strict) + +This document defines the canonical **physical** structure for `apps/api/**`. + +It describes where code lives, not the full behavioral rules. + +## 1) API is feature-based + +The API is organized by feature modules. + +```text +apps/api/ + src/ + domain/ + shared/ +``` + +Within feature modules: + +```text +apps/api/src/domain// + Controller.ts + Service.ts + Module.ts + dto/ + presenters/ +``` + +## 2) What belongs where (strict) + +- Controllers: HTTP boundary only +- DTOs: HTTP contracts only +- Presenters: map Core results  response DTOs + +API flow rules: + +- [`docs/architecture/api/API_DATA_FLOW.md`](docs/architecture/api/API_DATA_FLOW.md:1) + diff --git a/docs/architecture/api/AUTHORIZATION.md b/docs/architecture/api/AUTHORIZATION.md index fa4942c87..5734569ca 100644 --- a/docs/architecture/api/AUTHORIZATION.md +++ b/docs/architecture/api/AUTHORIZATION.md @@ -12,7 +12,7 @@ It complements (but does not replace) feature availability: - Authorization answers: “Is this actor allowed to do it?” Related: -- Feature gating concept: docs/architecture/FEATURE_AVAILABILITY.md +- Feature gating concept: [`docs/architecture/shared/FEATURE_AVAILABILITY.md`](docs/architecture/shared/FEATURE_AVAILABILITY.md:1) --- @@ -154,7 +154,7 @@ Return **404** when: Use this sparingly and intentionally. ### 6.3 Feature availability interaction -Feature availability failures (disabled/hidden/coming soon) should behave as “not found” for public callers, while maintenance mode should return 503. See docs/architecture/FEATURE_AVAILABILITY.md. +Feature availability failures (disabled/hidden/coming soon) should behave as “not found” for public callers, while maintenance mode should return 503. See [`docs/architecture/shared/FEATURE_AVAILABILITY.md`](docs/architecture/shared/FEATURE_AVAILABILITY.md:1). --- @@ -253,4 +253,4 @@ Rules: - A super-admin UI can manage: - global roles (owner/admin) - scoped roles (league_owner/admin/steward, sponsor_owner/admin, team_owner/admin) -- Feature availability remains a separate control plane (maintenance mode, coming soon, kill switches), documented in docs/architecture/FEATURE_AVAILABILITY.md. \ No newline at end of file +- Feature availability remains a separate control plane (maintenance mode, coming soon, kill switches), documented in [`docs/architecture/shared/FEATURE_AVAILABILITY.md`](docs/architecture/shared/FEATURE_AVAILABILITY.md:1). diff --git a/docs/architecture/api/AUTH_FLOW.md b/docs/architecture/api/AUTH_FLOW.md new file mode 100644 index 000000000..ca1d17efd --- /dev/null +++ b/docs/architecture/api/AUTH_FLOW.md @@ -0,0 +1,47 @@ +# Authentication and Authorization Flow (API) + +This document defines how authentication and authorization are enforced in the API. + +Shared contract: + +- [`docs/architecture/shared/AUTH_CONTRACT.md`](docs/architecture/shared/AUTH_CONTRACT.md:1) + +## 1) Enforcement location (strict) + +All enforcement happens in the API. + +The API must: + +- authenticate the actor from the session +- authorize the actor for the requested capability +- deny requests deterministically with appropriate HTTP status + +## 2) Canonical request flow + +```text +HTTP Request + ↓ +Authentication (resolve actor) + ↓ +Authorization (roles, permissions, scope) + ↓ +Controller (transport-only) + ↓ +Core Use Case + ↓ +Presenter mapping + ↓ +HTTP Response +``` + +## 3) Non-negotiable rules + +1. Deny by default unless explicitly public. +2. The actor identity is derived from the session. +3. Controllers do not contain business rules. + +Related: + +- Authorization model: [`docs/architecture/api/AUTHORIZATION.md`](docs/architecture/api/AUTHORIZATION.md:1) +- Guards definition: [`docs/architecture/api/GUARDS.md`](docs/architecture/api/GUARDS.md:1) + diff --git a/docs/architecture/api/GUARDS.md b/docs/architecture/api/GUARDS.md new file mode 100644 index 000000000..2002af8fe --- /dev/null +++ b/docs/architecture/api/GUARDS.md @@ -0,0 +1,47 @@ +# Guards (API Enforcement) + +This document defines **Guards** as API enforcement mechanisms. + +Shared contract: [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1) + +## 1) Definition + +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) Responsibilities + +Guards MAY: + +- block requests entirely +- return HTTP errors (401, 403, 429) +- enforce authentication and authorization +- enforce rate limits +- enforce feature availability +- protect against abuse and attacks + +Guards MUST: + +- be deterministic +- be authoritative +- be security-relevant + +## 3) Restrictions + +Guards MUST NOT: + +- depend on website/client state +- contain UI logic +- attempt to improve UX +- assume the client behaved correctly + +## 4) Common Guards + +- AuthGuard +- RolesGuard +- PermissionsGuard +- Throttler/RateLimit guards +- CSRF guards +- Feature availability guards + diff --git a/docs/architecture/api/USE_CASE_WIRING.md b/docs/architecture/api/USE_CASE_WIRING.md new file mode 100644 index 000000000..876f95c34 --- /dev/null +++ b/docs/architecture/api/USE_CASE_WIRING.md @@ -0,0 +1,38 @@ +# Use Case Wiring (API) (Strict) + +This document defines how the API wires HTTP requests to Core Use Cases. + +Core contract: + +- [`docs/architecture/core/USECASES.md`](docs/architecture/core/USECASES.md:1) + +## 1) Non-negotiable rules + +1. Controllers are transport boundaries. +2. Controllers validate request DTOs. +3. Controllers map request DTOs to Core inputs. +4. Controllers execute Core Use Cases. +5. Controllers map Core results to response DTOs. + +## 2) Presenter meaning in the API + +In the API, a Presenter is an output adapter that maps Core results to HTTP response DTOs. + +Rule: + +- API presenters are request-scoped. They must not be shared across concurrent requests. + +## 3) Canonical flow + +```text +HTTP Request DTO + ↓ +Controller mapping + ↓ +Core Use Case + ↓ +Presenter mapping + ↓ +HTTP Response DTO +``` + diff --git a/docs/architecture/core/CORE_DATA_FLOW.md b/docs/architecture/core/CORE_DATA_FLOW.md new file mode 100644 index 000000000..833358aa3 --- /dev/null +++ b/docs/architecture/core/CORE_DATA_FLOW.md @@ -0,0 +1,92 @@ +# Core Data Flow (Strict) + +This document defines the **Core** data flow rules and boundaries. + +Core scope: + +- `core/**` + +Core does not know: + +- HTTP +- Next.js +- databases +- DTOs +- UI models + +## 1) Layers inside Core + +Core contains two inner layers: + +- Domain +- Application + +### 1.1 Domain + +Domain is business truth. + +Allowed: + +- Entities +- Value Objects +- Domain Services + +Forbidden: + +- DTOs +- frameworks +- IO + +See [`docs/architecture/core/DOMAIN_OBJECTS.md`](docs/architecture/core/DOMAIN_OBJECTS.md:1). + +### 1.2 Application + +Application coordinates business intents. + +Allowed: + +- Use Cases (commands and queries) +- Application-level ports (repository ports, gateways) + +Forbidden: + +- HTTP +- persistence implementations +- frontend models + +## 2) Core I/O boundary + +All communication across the Core boundary occurs through **ports**. + +Rules: + +- Port interfaces live in Core. +- Implementations live outside Core. + +## 3) Core data types (strict) + +- Use Case inputs are plain data and/or domain types. +- Use Case outputs are plain data and/or domain types. + +Core MUST NOT define HTTP DTOs. + +## 4) Canonical flow + +```text +Delivery App (HTTP or Website) + ↓ +Core Application (Use Case) + ↓ +Core Domain (Entities, Value Objects) + ↓ +Ports (repository, gateway) + ↓ +Adapter implementation (outside Core) +``` + +## 5) Non-negotiable rules + +1. Core is framework-agnostic. +2. DTOs do not enter Core. +3. Core defines ports; outer layers implement them. + diff --git a/docs/architecture/core/CORE_FILE_STRUCTURE.md b/docs/architecture/core/CORE_FILE_STRUCTURE.md new file mode 100644 index 000000000..64b79a7c1 --- /dev/null +++ b/docs/architecture/core/CORE_FILE_STRUCTURE.md @@ -0,0 +1,57 @@ +# Core File Structure (Strict) + +This document defines the canonical **physical** structure for `core/`. + +It describes where code lives, not the full behavioral rules. + +Core rules and responsibilities are defined elsewhere. + +## 1) Core is feature-based + +Core is organized by bounded context / feature. + +```text +core/ + shared/ + / + domain/ + application/ +``` + +## 2) `core//domain/` + +Domain contains business truth. + +Canonical folders: + +```text +core//domain/ + entities/ + value-objects/ + services/ + events/ + errors/ +``` + +See [`docs/architecture/core/DOMAIN_OBJECTS.md`](docs/architecture/core/DOMAIN_OBJECTS.md:1). + +## 3) `core//application/` + +Application coordinates business intents. + +Canonical folders: + +```text +core//application/ + commands/ + queries/ + use-cases/ + services/ + ports/ +``` + +See: + +- [`docs/architecture/core/USECASES.md`](docs/architecture/core/USECASES.md:1) +- [`docs/architecture/core/CQRS.md`](docs/architecture/core/CQRS.md:1) + diff --git a/docs/architecture/core/CQRS.md b/docs/architecture/core/CQRS.md index a50caef5f..0dd666a8e 100644 --- a/docs/architecture/core/CQRS.md +++ b/docs/architecture/core/CQRS.md @@ -196,15 +196,16 @@ Avoid CQRS Light when: ⸻ -12. Migration Path +12. Adoption Rule (Strict) -CQRS Light allows incremental adoption: - 1. Start with classic Clean Architecture - 2. Separate commands and queries logically - 3. Optimize read paths as needed - 4. Introduce events or projections later (optional) +CQRS Light is a structural rule inside Core. -No rewrites required. +If CQRS Light is used: + +- commands and queries MUST be separated by responsibility +- queries MUST remain read-only and must not enforce invariants + +This document does not define a migration plan. ⸻ @@ -241,4 +242,4 @@ Without: • Event sourcing complexity • Premature optimization -It is the safest way to gain CQRS benefits while staying true to Clean Architecture. \ No newline at end of file +It is the safest way to gain CQRS benefits while staying true to Clean Architecture. diff --git a/docs/architecture/core/ENUMS.md b/docs/architecture/core/ENUMS.md deleted file mode 100644 index 4d55b52ec..000000000 --- a/docs/architecture/core/ENUMS.md +++ /dev/null @@ -1,339 +0,0 @@ -Enums in Clean Architecture (Strict & Final) - -This document defines how enums are modeled, placed, and used in a strict Clean Architecture setup. - -Enums are one of the most common sources of architectural leakage. This guide removes all ambiguity. - -⸻ - -1. Core Principle - -Enums represent knowledge. -Knowledge must live where it is true. - -Therefore: - • Not every enum is a domain enum - • Enums must never cross architectural boundaries blindly - • Ports must remain neutral - -⸻ - -2. Enum Categories (Authoritative) - -There are four and only four valid enum categories: - 1. Domain Enums - 2. Application (Workflow) Enums - 3. Transport Enums (API) - 4. UI Enums (Frontend) - -Each category has strict placement and usage rules. - -⸻ - -3. Domain Enums - -Definition - -A Domain Enum represents a business concept that: - • has meaning in the domain - • affects rules or invariants - • is part of the ubiquitous language - -Examples: - • LeagueVisibility - • MembershipRole - • RaceStatus - • SponsorshipTier - • PenaltyType - -⸻ - -Placement - -core//domain/ -├── value-objects/ -│ └── LeagueVisibility.ts -└── entities/ - -Preferred: model domain enums as Value Objects instead of enum keywords. - -⸻ - -Example (Value Object) - -export class LeagueVisibility { - private constructor(private readonly value: 'public' | 'private') {} - - static from(value: string): LeagueVisibility { - if (value !== 'public' && value !== 'private') { - throw new DomainError('Invalid LeagueVisibility'); - } - return new LeagueVisibility(value); - } - - isPublic(): boolean { - return this.value === 'public'; - } -} - - -⸻ - -Usage Rules - -Allowed: - • Domain - • Use Cases - -Forbidden: - • Ports - • Adapters - • API DTOs - • Frontend - -Domain enums must never cross a Port boundary. - -⸻ - -4. Application Enums (Workflow Enums) - -Definition - -Application Enums represent internal workflow or state coordination. - -They are not business truth and must not leak. - -Examples: - • LeagueSetupStep - • ImportPhase - • ProcessingState - -⸻ - -Placement - -core//application/internal/ -└── LeagueSetupStep.ts - - -⸻ - -Example - -export enum LeagueSetupStep { - CreateLeague, - CreateSeason, - AssignOwner, - Notify -} - - -⸻ - -Usage Rules - -Allowed: - • Application Services - • Use Cases - -Forbidden: - • Domain - • Ports - • Adapters - • Frontend - -These enums must remain strictly internal. - -⸻ - -5. Transport Enums (API DTOs) - -Definition - -Transport Enums describe allowed values in HTTP contracts. -They exist purely to constrain transport data, not to encode business rules. - -Naming rule: - -Transport enums MUST end with Enum. - -This makes enums immediately recognizable in code reviews and prevents silent leakage. - -Examples: - • LeagueVisibilityEnum - • SponsorshipStatusEnum - • PenaltyTypeEnum - -⸻ - -Placement - -apps/api//dto/ -└── LeagueVisibilityEnum.ts - -Website mirrors the same naming: - -apps/website/lib/dtos/ -└── LeagueVisibilityEnum.ts - - -⸻ - -Example - -export enum LeagueVisibilityEnum { - Public = 'public', - Private = 'private' -} - - -⸻ - -Usage Rules - -Allowed: - • API Controllers - • API Presenters - • Website API DTOs - -Forbidden: - • Core Domain - • Use Cases - -Transport enums are copies, never reexports of domain enums. - -⸻ - -Placement - -apps/api//dto/ -└── LeagueVisibilityDto.ts - -or inline as union types in DTOs. - -⸻ - -Example - -export type LeagueVisibilityDto = 'public' | 'private'; - - -⸻ - -Usage Rules - -Allowed: - • API Controllers - • API Presenters - • Website API DTOs - -Forbidden: - • Core Domain - • Use Cases - -Transport enums are copies, never reexports of domain enums. - -⸻ - -6. UI Enums (Frontend) - -Definition - -UI Enums describe presentation or interaction state. - -They have no business meaning. - -Examples: - • WizardStep - • SortOrder - • ViewMode - • TabKey - -⸻ - -Placement - -apps/website/lib/ui/ -└── LeagueWizardStep.ts - - -⸻ - -Example - -export enum LeagueWizardStep { - Basics, - Structure, - Scoring, - Review -} - - -⸻ - -Usage Rules - -Allowed: - • Frontend only - -Forbidden: - • Core - • API - -⸻ - -7. Absolute Prohibitions - -❌ Enums in Ports - -// ❌ forbidden -export interface CreateLeagueInputPort { - visibility: LeagueVisibility; -} - -✅ Correct - -export interface CreateLeagueInputPort { - visibility: 'public' | 'private'; -} - -Mapping happens inside the Use Case: - -const visibility = LeagueVisibility.from(input.visibility); - - -⸻ - -8. Decision Checklist - -Ask these questions: - 1. Does changing this enum change business rules? - • Yes → Domain Enum - • No → continue - 2. Is it only needed for internal workflow coordination? - • Yes → Application Enum - • No → continue - 3. Is it part of an HTTP contract? - • Yes → Transport Enum - • No → continue - 4. Is it purely for UI state? - • Yes → UI Enum - -⸻ - -9. Summary Table - -Enum Type Location May Cross Ports Scope -Domain Enum core/domain ❌ No Business rules -Application Enum core/application ❌ No Workflow only -Transport Enum apps/api + website ❌ No HTTP contracts -UI Enum apps/website ❌ No Presentation only - - -⸻ - -10. Final Rule (Non-Negotiable) - -If an enum crosses a boundary, it is in the wrong place. - -This rule alone prevents most long-term architectural decay. \ No newline at end of file diff --git a/docs/architecture/core/USECASES.md b/docs/architecture/core/USECASES.md index b4501d3c9..0d5cb8af8 100644 --- a/docs/architecture/core/USECASES.md +++ b/docs/architecture/core/USECASES.md @@ -1,513 +1,62 @@ -Use Case Architecture Guide +# Use Cases (Core Application Boundary) (Strict) -This document defines the correct structure and responsibilities of Application Use Cases -according to Clean Architecture, in a NestJS-based system. +This document defines the strict rules for Core Use Cases. -The goal is: - • strict separation of concerns - • correct terminology (no fake "ports") - • minimal abstractions - • long-term consistency +Scope: -This is the canonical reference for all use cases in this codebase. +- `core/**` -~ +Non-scope: -1. Core Concepts (Authoritative Definitions) +- HTTP controllers +- DTOs +- Next.js pages -Use Case - • Encapsulates application-level business logic - • Is the Input Port - • Is injected via DI - • Knows no API, no DTOs, no transport - • Coordinates domain objects and infrastructure +## 1) Definition -The public execute() method is the input port. +A Use Case represents one business intent. -~ +It answers: -Input - • Pure data - • Not a port - • Not an interface - • May be omitted if the use case has no parameters +- what the system does -type GetSponsorsInput = { - leagueId: LeagueId -} +## 2) Non-negotiable rules +1. Use Cases contain business logic. +2. Use Cases enforce invariants. +3. Use Cases do not know about HTTP. +4. Use Cases do not know about UI. +5. Use Cases do not depend on delivery-layer presenters. +6. Use Cases do not accept or return HTTP DTOs. -~ +## 3) Inputs and outputs -Result - • The business outcome of a use case - • May contain Entities and Value Objects - • Not a DTO - • Never leaves the core directly +Inputs: -type GetSponsorsResult = { - sponsors: Sponsor[] -} +- plain data and/or domain types +Outputs: -~ +- a `Result` containing plain data and/or domain types -Output Port - • A behavioral boundary - • Defines how the core communicates outward - • Never a data structure - • Lives in the Application Layer +Rule: -export interface UseCaseOutputPort { - present(data: T): void -} +- mapping to and from HTTP DTOs happens in the API, not in the Core. +See API wiring: [`docs/architecture/api/USE_CASE_WIRING.md`](docs/architecture/api/USE_CASE_WIRING.md:1) -~ +## 4) Ports -Presenter - • Implements UseCaseOutputPort - • Lives in the API / UI layer - • Translates Result → ViewModel / DTO - • Holds internal state - • Is pulled by the controller after execution - -~ - -2. Canonical Use Case Structure - -Application Layer - -Use Case - -@Injectable() -export class GetSponsorsUseCase { - constructor( - private readonly sponsorRepository: ISponsorRepository, - private readonly output: UseCaseOutputPort, - ) {} - - async execute(): Promise> { - const sponsors = await this.sponsorRepository.findAll() - - this.output.present({ sponsors }) - - return Result.ok(undefined) - } -} +Use Cases depend on ports for IO. Rules: - • execute() is the Input Port - • The use case does not return result data - • All output flows through the OutputPort - • The return value signals success or failure only -### ⚠️ ARCHITECTURAL VIOLATION ALERT +- port interfaces live in Core +- implementations live in adapters or delivery apps -**The pattern shown above is INCORRECT and violates Clean Architecture.** +## 5) CQRS -#### ❌ WRONG PATTERN (What NOT to do) +If CQRS-light is used, commands and queries are separated by responsibility. -```typescript -@Injectable() -export class GetSponsorsUseCase { - constructor( - private readonly sponsorRepository: ISponsorRepository, - private readonly output: UseCaseOutputPort, - ) {} +See [`docs/architecture/core/CQRS.md`](docs/architecture/core/CQRS.md:1). - async execute(): Promise> { - const sponsors = await this.sponsorRepository.findAll() - - this.output.present({ sponsors }) // ❌ WRONG: Use case calling presenter - return Result.ok(undefined) - } -} -``` - -**Why this violates Clean Architecture:** -- Use cases **know about presenters** and how to call them -- Creates **tight coupling** between application logic and presentation -- Makes use cases **untestable** without mocking presenters -- Violates the **Dependency Rule** (inner layer depending on outer layer behavior) - -#### ✅ CORRECT PATTERN (Clean Architecture) - -```typescript -@Injectable() -export class GetSponsorsUseCase { - constructor( - private readonly sponsorRepository: ISponsorRepository, - // NO output port needed in constructor - ) {} - - async execute(): Promise> { - const sponsors = await this.sponsorRepository.findAll() - - return Result.ok({ sponsors }) - // ✅ Returns Result, period. No .present() call. - } -} -``` - -**The Controller (in API layer) handles the wiring:** - -```typescript -@Controller('/sponsors') -export class SponsorsController { - constructor( - private readonly useCase: GetSponsorsUseCase, - private readonly presenter: GetSponsorsPresenter, - ) {} - - @Get() - async getSponsors() { - // 1. Execute use case - const result = await this.useCase.execute() - - if (result.isErr()) { - throw mapApplicationError(result.unwrapErr()) - } - - // 2. Wire to presenter - this.presenter.present(result.value) - - // 3. Return ViewModel - return this.presenter.getViewModel() - } -} -``` - -**This is the ONLY pattern that respects Clean Architecture.** - -~ - -Result Model - -type GetSponsorsResult = { - sponsors: Sponsor[] -} - -Rules: - • Domain objects are allowed - • No DTOs - • No interfaces - • No transport concerns - -~ - -3. API Layer - -API Services / Controllers (Thin Orchestration) - -The API layer is a transport boundary. It MUST delegate business logic to `./core`: - - • orchestrate auth + authorization checks (actor/session/roles) - • collect/validate transport input (DTOs at the boundary) - • execute a Core use case (entities/value objects live here) - • map Result → DTO / ViewModel via a Presenter (presenter owns mapping) - -Rules: - • Controllers stay thin: no business rules, no domain validation, no decision-making - • API services orchestrate: auth + use case execution + presenter mapping - • Domain objects never cross the API boundary un-mapped - -Presenter - -@Injectable() -export class GetSponsorsPresenter - implements UseCaseOutputPort -{ - private viewModel!: GetSponsorsViewModel - - present(result: GetSponsorsResult): void { - this.viewModel = { - sponsors: result.sponsors.map(s => ({ - id: s.id.value, - name: s.name, - websiteUrl: s.websiteUrl, - })), - } - } - - getViewModel(): GetSponsorsViewModel { - return this.viewModel - } -} - - -~ - -Controller - -@Controller('/sponsors') -export class SponsorsController { - constructor( - private readonly useCase: GetSponsorsUseCase, - private readonly presenter: GetSponsorsPresenter, - ) {} - - @Get() - async getSponsors() { - const result = await this.useCase.execute() - - if (result.isErr()) { - throw mapApplicationError(result.unwrapErr()) - } - - return this.presenter.getViewModel() - } -} - - -~ - -Payments Example - -Application Layer - -Use Case - -@Injectable() -export class CreatePaymentUseCase { - constructor( - private readonly paymentRepository: IPaymentRepository, - private readonly output: UseCaseOutputPort, - ) {} - - async execute(input: CreatePaymentInput): Promise>> { - // business logic - const payment = await this.paymentRepository.create(payment); - - this.output.present({ payment }); - - return Result.ok(undefined); - } -} - -Result Model - -type CreatePaymentResult = { - payment: Payment; -}; - -API Layer - -Presenter - -@Injectable() -export class CreatePaymentPresenter - implements UseCaseOutputPort -{ - private viewModel: CreatePaymentViewModel | null = null; - - present(result: CreatePaymentResult): void { - this.viewModel = { - payment: this.mapPaymentToDto(result.payment), - }; - } - - getViewModel(): CreatePaymentViewModel | null { - return this.viewModel; - } - - reset(): void { - this.viewModel = null; - } - - private mapPaymentToDto(payment: Payment): PaymentDto { - return { - id: payment.id, - // ... other fields - }; - } -} - -Controller - -@Controller('/payments') -export class PaymentsController { - constructor( - private readonly useCase: CreatePaymentUseCase, - private readonly presenter: CreatePaymentPresenter, - ) {} - - @Post() - async createPayment(@Body() input: CreatePaymentInput) { - const result = await this.useCase.execute(input); - - if (result.isErr()) { - throw mapApplicationError(result.unwrapErr()); - } - - return this.presenter.getViewModel(); - } -} - -~ - -4. Module Wiring (Composition Root) - -@Module({ - providers: [ - GetSponsorsUseCase, - GetSponsorsPresenter, - { - provide: USE_CASE_OUTPUT_PORT, - useExisting: GetSponsorsPresenter, - }, - ], -}) -export class SponsorsModule {} - -Rules: - • The use case depends only on the OutputPort interface - • The presenter is bound as the OutputPort implementation - • process.env is not used inside the use case - -~ - -5. Explicitly Forbidden - -❌ DTOs in use cases -❌ Domain objects returned directly to the API -❌ Output ports used as data structures -❌ present() returning a value -❌ Input data named InputPort -❌ Mapping logic inside use cases -❌ Environment access inside the core - -~ - -Do / Don’t (Boundary Examples) - - ✅ DO: Keep pages/components consuming ViewModels returned by website services (DTOs stop at the service boundary), e.g. [LeagueAdminSchedulePage()](apps/website/app/leagues/[id]/schedule/admin/page.tsx:12). - ✅ DO: Keep controllers/services thin and delegating, e.g. [LeagueController.createLeagueSeasonScheduleRace()](apps/api/src/domain/league/LeagueController.ts:291). - ❌ DON’T: Put business rules in the API layer; rules belong in `./core` use cases/entities/value objects, e.g. [CreateLeagueSeasonScheduleRaceUseCase.execute()](core/racing/application/use-cases/CreateLeagueSeasonScheduleRaceUseCase.ts:38). - -~ - -6. Optional Extensions - -Custom Output Ports - -Only introduce a dedicated OutputPort interface if: - • multiple presentation paths exist - • streaming or progress updates are required - • more than one output method is needed - -interface ComplexOutputPort { - presentSuccess(...) - presentFailure(...) -} - - -~ - -Input Port Interfaces - -Only introduce an explicit InputPort interface if: - • multiple implementations of the same use case exist - • feature flags or A/B variants are required - • the use case itself must be substituted - -Otherwise: - -The use case class itself is the input port. - -~ - -7. Key Rules (Memorize These) - -Use cases answer what. -Presenters answer how. - -Ports have behavior. -Data does not. - -The core produces truth. -The API interprets it. - -~ - -TL;DR - • Use cases are injected via DI - • execute() is the Input Port - • Outputs flow only through Output Ports - • Results are business models, not DTOs - • Interfaces exist only for behavior variability - -### 🚨 CRITICAL CLEAN ARCHITECTURE CORRECTION - -**The examples in this document (sections 2, 3, and the Payments Example) demonstrate the WRONG pattern that violates Clean Architecture.** - -#### The Fundamental Problem - -The current architecture shows use cases **calling presenters directly**: - -```typescript -// ❌ WRONG - This violates Clean Architecture -this.output.present({ sponsors }) -``` - -**This is architecturally incorrect.** Use cases must **never** know about presenters or call `.present()`. - -#### The Correct Clean Architecture Pattern - -**Use cases return Results. Controllers wire them to presenters.** - -```typescript -// ✅ CORRECT - Use case returns data -@Injectable() -export class GetSponsorsUseCase { - constructor(private readonly sponsorRepository: ISponsorRepository) {} - - async execute(): Promise> { - const sponsors = await this.sponsorRepository.findAll() - return Result.ok({ sponsors }) - // NO .present() call! - } -} - -// ✅ CORRECT - Controller handles wiring -@Controller('/sponsors') -export class SponsorsController { - constructor( - private readonly useCase: GetSponsorsUseCase, - private readonly presenter: GetSponsorsPresenter, - ) {} - - @Get() - async getSponsors() { - const result = await this.useCase.execute() - - if (result.isErr()) { - throw mapApplicationError(result.unwrapErr()) - } - - this.presenter.present(result.value) - return this.presenter.getViewModel() - } -} -``` - -#### Why This Matters - -1. **Dependency Rule**: Inner layers (use cases) cannot depend on outer layers (presenters) -2. **Testability**: Use cases can be tested without mocking presenters -3. **Flexibility**: Same use case can work with different presenters -4. **Separation of Concerns**: Use cases do business logic, presenters do transformation - -#### What Must Be Fixed - -**All use cases in the codebase must be updated to:** -1. **Remove** the `output: UseCaseOutputPort` constructor parameter -2. **Return** `Result` directly from `execute()` -3. **Remove** all `this.output.present()` calls - -**All controllers must be updated to:** -1. **Call** the use case and get the Result -2. **Pass** `result.value` to the presenter's `.present()` method -3. **Return** the presenter's `.getViewModel()` - -This is the **single source of truth** for correct Clean Architecture in this project. \ No newline at end of file diff --git a/docs/architecture/shared/AUTH_CONTRACT.md b/docs/architecture/shared/AUTH_CONTRACT.md new file mode 100644 index 000000000..2047d8782 --- /dev/null +++ b/docs/architecture/shared/AUTH_CONTRACT.md @@ -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) + diff --git a/docs/architecture/shared/BLOCKERS_AND_GUARDS.md b/docs/architecture/shared/BLOCKERS_AND_GUARDS.md new file mode 100644 index 000000000..ae129854e --- /dev/null +++ b/docs/architecture/shared/BLOCKERS_AND_GUARDS.md @@ -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. + diff --git a/docs/architecture/shared/DATA_FLOW.md b/docs/architecture/shared/DATA_FLOW.md index 030286ef2..4f49c6364 100644 --- a/docs/architecture/shared/DATA_FLOW.md +++ b/docs/architecture/shared/DATA_FLOW.md @@ -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 layer’s 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 { - 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 { - 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) diff --git a/docs/architecture/shared/ENUMS.md b/docs/architecture/shared/ENUMS.md new file mode 100644 index 000000000..22f92ca73 --- /dev/null +++ b/docs/architecture/shared/ENUMS.md @@ -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. + diff --git a/docs/architecture/shared/FEATURE_AVAILABILITY.md b/docs/architecture/shared/FEATURE_AVAILABILITY.md index caeb1247b..f5fe51cd0 100644 --- a/docs/architecture/shared/FEATURE_AVAILABILITY.md +++ b/docs/architecture/shared/FEATURE_AVAILABILITY.md @@ -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. \ No newline at end of file diff --git a/docs/architecture/shared/FILE_STRUCTURE.md b/docs/architecture/shared/FILE_STRUCTURE.md deleted file mode 100644 index ca4d06b13..000000000 --- a/docs/architecture/shared/FILE_STRUCTURE.md +++ /dev/null @@ -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 -``` \ No newline at end of file diff --git a/docs/architecture/shared/REPOSITORY_STRUCTURE.md b/docs/architecture/shared/REPOSITORY_STRUCTURE.md new file mode 100644 index 000000000..c9de50bea --- /dev/null +++ b/docs/architecture/shared/REPOSITORY_STRUCTURE.md @@ -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. + diff --git a/docs/architecture/shared/UNIFIED_AUTH_CONCEPT.md b/docs/architecture/shared/UNIFIED_AUTH_CONCEPT.md deleted file mode 100644 index e315c9d23..000000000 --- a/docs/architecture/shared/UNIFIED_AUTH_CONCEPT.md +++ /dev/null @@ -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 ( -
- -
- ); - } - - // 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 ( -
- -
- ); - } - - 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 ; -} -``` - -#### Authenticated Route -```typescript -// app/dashboard/layout.tsx -import { AuthLayout } from '@/lib/guards/AuthLayout'; - -export default function DashboardLayout({ children }: { children: ReactNode }) { - return ( - -
- {children} -
-
- ); -} - -// app/dashboard/page.tsx -export default function DashboardPage() { - // No additional auth checks needed - layout handles it - return ; -} -``` - -#### Role-Protected Route -```typescript -// app/admin/layout.tsx -import { RoleLayout } from '@/lib/guards/RoleLayout'; - -export default function AdminLayout({ children }: { children: ReactNode }) { - return ( - -
- {children} -
-
- ); -} - -// app/admin/page.tsx -export default function AdminPage() { - // No additional checks - layout handles role verification - return ; -} -``` - -#### 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 ( - - -
- {children} -
-
-
- ); -} -``` - -### 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( - REQUIRE_ROLES_METADATA_KEY, - [handler, controllerClass], - ) ?? null; - - // 3. Get user identity from request (set by AuthenticationGuard) - const request = context.switchToHttp().getRequest(); - 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.** \ No newline at end of file diff --git a/docs/architecture/website/BLOCKERS.md b/docs/architecture/website/BLOCKERS.md new file mode 100644 index 000000000..a6291191a --- /dev/null +++ b/docs/architecture/website/BLOCKERS.md @@ -0,0 +1,51 @@ +# Blockers (Website UX) + +This document defines **Blockers** as UX-only prevention mechanisms in the website. + +Shared contract: [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1) + +## 1) Definition + +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. + +## 2) Responsibilities + +Blockers MAY: + +- prevent multiple submissions +- disable actions temporarily +- debounce or throttle interactions +- hide or disable UI elements +- prevent navigation under certain conditions + +Blockers MUST: + +- be reversible +- be local to the website +- be treated as best-effort helpers + +## 3) Restrictions + +Blockers MUST NOT: + +- enforce security +- claim authorization +- block access permanently +- replace API Guards +- make assumptions about backend state + +## 4) Common Blockers + +- SubmitBlocker +- ThrottleBlocker +- NavigationBlocker +- FeatureBlocker + +## 5) Canonical placement + +- `apps/website/lib/blockers/**` + diff --git a/docs/architecture/website/BLOCKER_GUARDS.md b/docs/architecture/website/BLOCKER_GUARDS.md deleted file mode 100644 index 93472ab41..000000000 --- a/docs/architecture/website/BLOCKER_GUARDS.md +++ /dev/null @@ -1,154 +0,0 @@ -Blockers & Guards - -This document defines clear, non-overlapping responsibilities for Blockers (frontend) and Guards (backend). -The goal is to prevent semantic drift, security confusion, and inconsistent implementations. - -⸻ - -Core Principle - -Guards enforce. Blockers prevent. - • Guards protect the system. - • Blockers protect the UX. - -There are no exceptions to this rule. - -⸻ - -Backend — Guards (NestJS) - -Definition - -A Guard is a backend mechanism that enforces access or execution rules. -If a Guard denies execution, the request does not reach the application logic. - -In NestJS, Guards implement CanActivate. - -⸻ - -Responsibilities - -Guards MAY: - • block requests entirely - • return HTTP errors (401, 403, 429) - • enforce authentication and authorization - • enforce rate limits - • enforce feature availability - • protect against abuse and attacks - -Guards MUST: - • be deterministic - • be authoritative - • be security-relevant - -⸻ - -Restrictions - -Guards MUST NOT: - • depend on frontend state - • contain UI logic - • attempt to improve UX - • assume the client behaved correctly - -⸻ - -Common Backend Guards - • AuthGuard - • RolesGuard - • PermissionsGuard - • ThrottlerGuard (NestJS) - • RateLimitGuard - • CsrfGuard - • FeatureFlagGuard - -⸻ - -Summary (Backend) - • Guards decide - • Guards enforce - • Guards secure the system - -⸻ - -Frontend — Blockers - -Definition - -A Blocker is a frontend mechanism that prevents an action from being executed. -Blockers exist solely to improve UX and reduce unnecessary requests. - -Blockers are not security mechanisms. - -⸻ - -Responsibilities - -Blockers MAY: - • prevent multiple submissions - • disable actions temporarily - • debounce or throttle interactions - • hide or disable UI elements - • prevent navigation under certain conditions - -Blockers MUST: - • be reversible - • be local to the frontend - • be treated as best-effort helpers - -⸻ - -Restrictions - -Blockers MUST NOT: - • enforce security - • claim authorization - • block access permanently - • replace backend Guards - • make assumptions about backend state - -⸻ - -Common Frontend Blockers - • SubmitBlocker - • AuthBlocker - • RoleBlocker - • ThrottleBlocker - • NavigationBlocker - • FeatureBlocker - -⸻ - -Summary (Frontend) - • Blockers prevent execution - • Blockers improve UX - • Blockers reduce mistakes and load - -⸻ - -Clear Separation - -Aspect Blocker (Frontend) Guard (Backend) -Purpose Prevent execution Enforce rules -Security ❌ No ✅ Yes -Authority ❌ Best-effort ✅ Final -Reversible ✅ Yes ❌ No -Failure effect UI feedback HTTP error - - -⸻ - -Naming Rules (Hard) - • Frontend uses *Blocker - • Backend uses *Guard - • Never mix the terms - • Never implement Guards in the frontend - • Never implement Blockers in the backend - -⸻ - -Final Rule - -If it must be enforced, it is a Guard. - -If it only prevents UX mistakes, it is a Blocker. \ No newline at end of file diff --git a/docs/architecture/website/CLIENT_STATE.md b/docs/architecture/website/CLIENT_STATE.md index 5b2ccbc38..d0bd7f5e3 100644 --- a/docs/architecture/website/CLIENT_STATE.md +++ b/docs/architecture/website/CLIENT_STATE.md @@ -50,11 +50,10 @@ Blockers exist to prevent UX mistakes. - Blockers may reduce unnecessary requests. - The API still enforces rules. -See [`BLOCKER_GUARDS.md`](docs/architecture/website/BLOCKER_GUARDS.md:1). +See [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1) and [`docs/architecture/website/BLOCKERS.md`](docs/architecture/website/BLOCKERS.md:1). ## 6) Canonical placement in this repo - `apps/website/lib/blockers/**` - `apps/website/lib/hooks/**` - `apps/website/lib/command-models/**` - diff --git a/docs/architecture/website/LOGIN_FLOW_STATE_MACHINE.md b/docs/architecture/website/LOGIN_FLOW_STATE_MACHINE.md index cad094d3b..8606de3bf 100644 --- a/docs/architecture/website/LOGIN_FLOW_STATE_MACHINE.md +++ b/docs/architecture/website/LOGIN_FLOW_STATE_MACHINE.md @@ -1,14 +1,20 @@ -# Login Flow State Machine Architecture +# Login Flow State Machine (Strict) -## Problem -The current login page has unpredictable behavior due to: -- Multiple useEffect runs with different session states -- Race conditions between session loading and redirect logic -- Client-side redirects that interfere with test expectations +This document defines the canonical, deterministic login flow controller for the website. -## Solution: State Machine Pattern +Authoritative website contract: -### State Definitions +- [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1) + +## 1) Core rule + +Login flow logic MUST be deterministic. + +The same inputs MUST produce the same state and the same next action. + +## 2) State machine definition (strict) + +### 2.1 State definitions ```typescript enum LoginState { @@ -19,7 +25,7 @@ enum LoginState { } ``` -### State Transition Table +### 2.2 State transition table | Current State | Session | ReturnTo | Next State | Action | |---------------|---------|----------|------------|--------| @@ -29,7 +35,7 @@ enum LoginState { | UNAUTHENTICATED | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo | | AUTHENTICATED_WITHOUT_PERMISSIONS | exists | any | POST_AUTH_REDIRECT | Redirect to returnTo | -### Class-Based Controller +### 2.3 Class-based controller ```typescript class LoginFlowController { @@ -57,7 +63,7 @@ class LoginFlowController { return this.state; } - // Pure function - returns action, doesn't execute + // Pure function - returns action, does not execute getNextAction(): LoginAction { switch (this.state) { case LoginState.UNAUTHENTICATED: @@ -71,7 +77,7 @@ class LoginFlowController { } } - // Called after authentication + // Transition called after authentication transitionToPostAuth(): void { if (this.session) { this.state = LoginState.POST_AUTH_REDIRECT; @@ -80,15 +86,14 @@ class LoginFlowController { } ``` -### Benefits +## 3) Non-negotiable rules -1. **Predictable**: Same inputs always produce same outputs -2. **Testable**: Can test each state transition independently -3. **No Race Conditions**: State determined once at construction -4. **Clear Intent**: Each state has a single purpose -5. **Maintainable**: Easy to add new states or modify transitions +1. The controller MUST be constructed from explicit inputs only. +2. The controller MUST NOT perform side effects. +3. Side effects (routing) MUST be executed outside the controller. +4. The controller MUST be unit-tested per transition. -### Usage in Login Page +## 4) Usage in login page (example) ```typescript export default function LoginPage() { @@ -129,4 +134,4 @@ export default function LoginPage() { } ``` -This eliminates all the unpredictable behavior and makes the flow testable and maintainable. \ No newline at end of file +This pattern ensures deterministic behavior and makes the flow testable. diff --git a/docs/architecture/website/WEBSITE_AUTH_FLOW.md b/docs/architecture/website/WEBSITE_AUTH_FLOW.md new file mode 100644 index 000000000..d1d91fe95 --- /dev/null +++ b/docs/architecture/website/WEBSITE_AUTH_FLOW.md @@ -0,0 +1,46 @@ +# Authentication UX Flow (Website) + +This document defines how the website handles authentication from a UX perspective. + +Shared contract: + +- [`docs/architecture/shared/AUTH_CONTRACT.md`](docs/architecture/shared/AUTH_CONTRACT.md:1) + +Authoritative website contract: + +- [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1) + +## 1) Website role (strict) + +The website: + +- redirects unauthenticated users to login +- hides or disables UI based on best-effort session knowledge + +The website does not enforce security. + +## 2) Canonical website flow + +```text +Request + ↓ +Website routing + ↓ +API requests with credentials + ↓ +API enforces authentication and authorization + ↓ +Website renders result or redirects +``` + +## 3) Non-negotiable rules + +1. The website MUST NOT claim authorization. +2. The website MUST NOT trust client state for enforcement. +3. Every write still relies on the API to accept or reject. + +Related: + +- Website blockers: [`docs/architecture/website/BLOCKERS.md`](docs/architecture/website/BLOCKERS.md:1) +- Client state rules: [`docs/architecture/website/CLIENT_STATE.md`](docs/architecture/website/CLIENT_STATE.md:1) + diff --git a/docs/architecture/website/WEBSITE_CONTRACT.md b/docs/architecture/website/WEBSITE_CONTRACT.md index d67af84fa..29a785bc2 100644 --- a/docs/architecture/website/WEBSITE_CONTRACT.md +++ b/docs/architecture/website/WEBSITE_CONTRACT.md @@ -189,7 +189,7 @@ See [`FORM_SUBMISSION.md`](docs/architecture/website/FORM_SUBMISSION.md:1). - The website MUST NOT enforce security. - The API enforces authentication and authorization. -See [`BLOCKER_GUARDS.md`](docs/architecture/website/BLOCKER_GUARDS.md:1). +See [`docs/architecture/shared/BLOCKERS_AND_GUARDS.md`](docs/architecture/shared/BLOCKERS_AND_GUARDS.md:1) and [`docs/architecture/website/BLOCKERS.md`](docs/architecture/website/BLOCKERS.md:1). ## 7.1) Client state (strict) diff --git a/docs/architecture/website/WEBSITE_DATA_FLOW.md b/docs/architecture/website/WEBSITE_DATA_FLOW.md new file mode 100644 index 000000000..89c18bf6e --- /dev/null +++ b/docs/architecture/website/WEBSITE_DATA_FLOW.md @@ -0,0 +1,65 @@ +# Website Data Flow (Strict) + +This document defines the **apps/website** data flow. + +Authoritative contract: [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1). + +Website scope: + +- `apps/website/**` + +## 1) Website role + +The website is a **delivery layer**. + +It renders truth from the API and forwards user intent to the API. + +## 2) Read flow + +```text +RSC page.tsx + ↓ +PageQuery + ↓ +API client (infra) + ↓ +API Transport DTO + ↓ +Page DTO + ↓ +Presenter (client) + ↓ +ViewModel (optional) + ↓ +Presenter (client) + ↓ +ViewData + ↓ +Template +``` + +## 3) Write flow + +All writes enter through **Server Actions**. + +```text +User intent + ↓ +Server Action + ↓ +Command Model / Request DTO + ↓ +API + ↓ +Revalidation + ↓ +RSC reload +``` + +## 4) Non-negotiable rules + +1. Templates accept ViewData only. +2. Page Queries do not format. +3. Presenters do not call the API. +4. Client state is UI-only. + diff --git a/docs/architecture/website/WEBSITE_FILE_STRUCTURE.md b/docs/architecture/website/WEBSITE_FILE_STRUCTURE.md new file mode 100644 index 000000000..c83aaa3a4 --- /dev/null +++ b/docs/architecture/website/WEBSITE_FILE_STRUCTURE.md @@ -0,0 +1,50 @@ +# Website File Structure (Strict) + +This document defines the canonical **physical** structure for `apps/website/**`. + +It describes where code lives, not the full behavioral rules. + +Authoritative contract: + +- [`docs/architecture/website/WEBSITE_CONTRACT.md`](docs/architecture/website/WEBSITE_CONTRACT.md:1) + +## 1) High-level layout + +```text +apps/website/ + app/  Next.js routes (RSC pages, layouts, server actions) + templates/  template components (ViewData only) + lib/  website code (clients, services, view-models, etc.) +``` + +## 2) `apps/website/app/` (routing) + +Routes are implemented via Next.js App Router. + +Rules: + +- server `page.tsx` does composition only +- templates are pure +- writes enter via server actions + +See [`docs/architecture/website/WEBSITE_RSC_PRESENTATION.md`](docs/architecture/website/WEBSITE_RSC_PRESENTATION.md:1). + +## 3) `apps/website/lib/` (website internals) + +Canonical folders (existing in this repo): + +```text +apps/website/lib/ + api/  API clients + infrastructure/  technical concerns + services/  UI orchestration (read-only and write orchestration) + page-queries/  server composition + types/  API transport DTOs + view-models/  client-only classes + display-objects/  deterministic formatting helpers + command-models/  transient form models + blockers/  UX-only prevention + hooks/  React-only helpers + di/  client-first DI integration +``` +