Adds 2 units and docs for unit 3. nfr-requirements plan

This commit is contained in:
2026-06-29 22:18:37 +02:00
parent 0e01ca1e1c
commit c156107cb1
126 changed files with 15204 additions and 80199 deletions
@@ -0,0 +1,152 @@
# Business Logic Model — Unit 2: slave-availability-extension
## Flow 1: Register Master (POST /api/v1/master/register)
```mermaid
sequenceDiagram
box rgba(33,150,243,0.15) Master CMS
participant MC as MasterCms
end
box rgba(76,175,80,0.15) Slave CMS
participant Ctrl as MasterController
participant Svc as MasterAvailabilityService
participant DB as AvailabilityDbContext
end
MC->>Ctrl: POST register with X-Master-Api-Key header and MasterUrl body
Ctrl->>Svc: RegisterAsync(masterUrl, apiKey)
Svc->>DB: GetRegistrationAsync()
alt No registration exists
DB-->>Svc: null
Svc->>DB: Insert MasterRegistration with masterUrl apiKey RegisteredAt LastContactedAt=now
DB-->>Svc: saved
Svc-->>Ctrl: success
Ctrl-->>MC: 200 OK
else Registration exists and ApiKey matches
DB-->>Svc: existing record
Svc->>DB: Update MasterUrl and LastContactedAt=now
DB-->>Svc: saved
Svc-->>Ctrl: success
Ctrl-->>MC: 200 OK
else Registration exists and ApiKey does not match
DB-->>Svc: existing record
Svc-->>Ctrl: unauthorized
Ctrl-->>MC: 401 Unauthorized
end
```
Text alternative: Master posts to slave register endpoint. Service checks DB for existing registration. If none: insert new row. If exists and key matches: update MasterUrl. If exists but key mismatch: return 401.
---
## Flow 2: Status Push (POST /api/v1/master/status)
```mermaid
sequenceDiagram
box rgba(33,150,243,0.15) Master CMS
participant MC as MasterCms
end
box rgba(76,175,80,0.15) Slave CMS
participant Ctrl as MasterController
participant Svc as MasterAvailabilityService
participant DB as AvailabilityDbContext
participant Cache as StaticCache
end
MC->>Ctrl: POST status with X-Master-Api-Key header isAvailable and disableMessage
Ctrl->>Svc: PushStatusAsync(apiKey, isAvailable, disableMessage)
Svc->>DB: GetRegistrationAsync()
alt No registration
DB-->>Svc: null
Svc-->>Ctrl: unauthorized
Ctrl-->>MC: 401 Unauthorized
else ApiKey mismatch
DB-->>Svc: record with different key
Svc-->>Ctrl: unauthorized
Ctrl-->>MC: 401 Unauthorized
else ApiKey valid
DB-->>Svc: valid registration
Svc->>Cache: set _masterIsAvailable and _masterDisableMessage
Svc->>DB: Update LastContactedAt=now
DB-->>Svc: saved
Svc-->>Ctrl: success
Ctrl-->>MC: 200 OK
end
```
Text alternative: Master posts status. Service validates API key. If valid: update static cache and LastContactedAt. On any validation failure: 401.
---
## Flow 3: Get Registered URL (GET /api/v1/master/registered-url)
```mermaid
sequenceDiagram
box rgba(33,150,243,0.15) Master CMS
participant MC as MasterCms
end
box rgba(76,175,80,0.15) Slave CMS
participant Ctrl as MasterController
participant Svc as MasterAvailabilityService
participant DB as AvailabilityDbContext
end
MC->>Ctrl: GET registered-url with X-Master-Api-Key header
Ctrl->>Svc: GetRegisteredUrlAsync(apiKey)
Svc->>DB: GetRegistrationAsync()
alt No registration
DB-->>Svc: null
Svc-->>Ctrl: unauthorized
Ctrl-->>MC: 401 Unauthorized
else ApiKey mismatch
DB-->>Svc: record
Svc-->>Ctrl: unauthorized
Ctrl-->>MC: 401 Unauthorized
else ApiKey valid
DB-->>Svc: valid registration
Svc->>DB: Update LastContactedAt=now
DB-->>Svc: saved
Svc-->>Ctrl: return MasterUrl
Ctrl-->>MC: 200 OK with MasterUrl payload
end
```
Text alternative: Master requests the URL it registered on this slave. Service validates key, updates LastContactedAt, returns stored MasterUrl. 401 on any validation failure.
---
## Flow 4: Middleware Gate Evaluation (every request)
```mermaid
sequenceDiagram
box rgba(33,150,243,0.15) Incoming Request
participant Req as HttpRequest
end
box rgba(76,175,80,0.15) Slave CMS Pipeline
participant MW as AvailabilityMiddleware
participant Cache as StaticCache
participant Local as IAvailabilityService
participant Next as NextMiddleware
end
Req->>MW: any request
alt Path in bypass list
MW-->>Next: pass through unconditionally
else Admin JWT bearer token present
MW-->>Next: pass through unconditionally
else Check master gate
MW->>Cache: read _masterIsAvailable
alt Master is available or no status received yet
MW->>Local: IsAvailableAsync()
alt Locally available
MW-->>Next: pass through
else Locally unavailable
MW-->>Req: 503 with local disable message
end
else Master says unavailable
MW-->>Req: 503 with master disable message
end
end
```
Text alternative: Middleware first checks bypass paths, then admin JWT. If neither applies: checks static master cache; if master unavailable return 503. If master available: checks local availability service; if locally unavailable return 503. Otherwise pass through.
@@ -0,0 +1,103 @@
# Business Rules — Unit 2: slave-availability-extension
## Rule Set 1: API Key Validation
```mermaid
graph TD
A{Registration\nexists in DB?} -->|No| B{Is this a\nregister call?}
B -->|Yes| C[Accept and create\nnew registration]
B -->|No| D[Return 401\nUnauthorized]
A -->|Yes| E{X-Master-Api-Key\nmatches stored key?}
E -->|Yes| F[Proceed with\nbusiness logic]
E -->|No| G[Return 401\nUnauthorized]
classDef decision fill:#FFC107,stroke:#F57F17,color:#000
classDef pass fill:#9ae6b4,stroke:#2f855a,color:#000
classDef fail fill:#FC8181,stroke:#C53030,color:#000
class A,B,E decision
class C,F pass
class D,G fail
```
Text alternative: If no registration exists and this is a register call, create it. If no registration and not a register call, 401. If registration exists, validate key; match = proceed, mismatch = 401.
**Validation rules**:
| # | Rule | Applies to |
|---|------|-----------|
| BR-SLAVE-01 | First `POST /register` with no existing registration: accept unconditionally, store `ApiKey` from `X-Master-Api-Key` header | Register endpoint |
| BR-SLAVE-02 | Subsequent `POST /register`: validate header against stored `ApiKey`. Match → update `MasterUrl` + `LastContactedAt`. Mismatch → 401. | Register endpoint |
| BR-SLAVE-03 | `POST /status` without existing registration → 401 | Status push endpoint |
| BR-SLAVE-04 | `POST /status` with key mismatch → 401 | Status push endpoint |
| BR-SLAVE-05 | `GET /registered-url` without existing registration → 401 | Get-URL endpoint |
| BR-SLAVE-06 | `GET /registered-url` with key mismatch → 401 | Get-URL endpoint |
| BR-SLAVE-07 | Missing or empty `X-Master-Api-Key` header → 401 on all endpoints | All master endpoints |
---
## Rule Set 2: Master Gate Bypass
```mermaid
graph TD
A{Path starts with\nbypass prefix?} -->|Yes| B[Pass through\nunconditionally]
A -->|No| C{Valid admin\nJWT bearer?}
C -->|Yes| B
C -->|No| D{Master gate\nenabled?}
D -->|_masterIsAvailable = true\nor default| E[Proceed to\nlocal gate]
D -->|_masterIsAvailable = false| F[Return 503\nwith master message]
E --> G{Local availability\ncheck}
G -->|Available| H[Pass to next\nmiddleware]
G -->|Unavailable| I[Return 503\nwith local message]
classDef decision fill:#FFC107,stroke:#F57F17,color:#000
classDef pass fill:#9ae6b4,stroke:#2f855a,color:#000
classDef fail fill:#FC8181,stroke:#C53030,color:#000
class A,C,D,G decision
class B,E,H pass
class F,I fail
```
Text alternative: Bypass path check first. Admin JWT next (bypasses both gates). Then master gate (static field). If master blocks: 503. If master passes: local gate. If local blocks: 503. Otherwise pass through.
**Bypass prefix list** (extended from existing):
| Path prefix | Reason |
|-------------|--------|
| `/api/v1/Availability/status` | Already bypassed — public status endpoint |
| `/api/v1/Auth/` | Already bypassed — login must always work |
| `/api/v1/Setup/status` | Already bypassed — frontend init check |
| `/api/v1/master/` | **NEW** — master management endpoints must bypass gate so master can always push status or re-register |
**Cache behavior rules**:
| # | Rule |
|---|------|
| BR-SLAVE-08 | `_masterIsAvailable` defaults to `true` (fail-open) on process startup |
| BR-SLAVE-09 | `_masterDisableMessage` defaults to `null` on process startup |
| BR-SLAVE-10 | Cache has no expiry (Q4=A); only updated on `POST /status` with valid API key |
| BR-SLAVE-11 | 503 response from master gate includes `_masterDisableMessage` in `ProblemDetails.Detail` |
---
## Rule Set 3: MasterRegistration Singleton
| # | Rule |
|---|------|
| BR-SLAVE-12 | `MasterRegistration` is a singleton: `Id` is always `Guid.Parse("00000000-0000-0000-0000-000000000001")` |
| BR-SLAVE-13 | On `RegisterAsync`: if row with that Id exists → update. If not → insert. Never delete. |
| BR-SLAVE-14 | `RegisteredAt` is set once at creation and never updated |
| BR-SLAVE-15 | `LastContactedAt` is updated on every successful master call (register, status push, get-url) |
---
## Rule Set 4: Controller Routing
| Endpoint | Method | Route | Auth |
|----------|--------|-------|------|
| Register master | `POST` | `/api/v1/master/register` | None (API key in header) |
| Receive status push | `POST` | `/api/v1/master/status` | None (API key in header) |
| Get registered URL | `GET` | `/api/v1/master/registered-url` | None (API key in header) |
All three endpoints are unauthenticated from ASP.NET Core's perspective — they use the custom `X-Master-Api-Key` header validation implemented in `MasterAvailabilityService`. They are also in the middleware bypass list so the gate cannot block master management calls.
@@ -0,0 +1,72 @@
# Domain Entities — Unit 2: slave-availability-extension
## Entity Relationship Diagram
```mermaid
graph TD
subgraph AvailabilityModule["Availability Module (slave side)"]
DbCtx["AvailabilityDbContext"]
MR["MasterRegistration\n(singleton row)"]
Cache["MasterStatusCache\n(static fields)"]
MAS["IMasterAvailabilityService\n/MasterAvailabilityService"]
end
DbCtx -->|owns| MR
MAS -->|reads/writes| DbCtx
MAS -->|updates| Cache
MW["AvailabilityMiddleware\n(extended)"] -->|reads synchronously| Cache
MC["MasterController\n(new)"] -->|delegates to| MAS
classDef entity fill:#FFC107,stroke:#F57F17,color:#000
classDef service fill:#9ae6b4,stroke:#2f855a,color:#000
classDef infra fill:#63b3ed,stroke:#2b6cb0,color:#000
classDef cache fill:#CE93D8,stroke:#6A1B9A,color:#000
class MR entity
class MAS service
class DbCtx,MC infra
class Cache,MW cache
```
Text alternative: `AvailabilityDbContext` owns the singleton `MasterRegistration` row. `MasterAvailabilityService` reads/writes the DbContext and updates the in-process `MasterStatusCache` (static fields). `AvailabilityMiddleware` reads the cache synchronously. `MasterController` delegates all logic to `MasterAvailabilityService`.
---
## Entity: MasterRegistration
Singleton row — at most one record exists per slave instance. Upserted on each successful registration.
| Field | Type | Constraints | Notes |
|-------|------|-------------|-------|
| `Id` | `Guid` | PK | Fixed value (e.g. `Guid.Empty`) enforces singleton |
| `MasterUrl` | `string` | Required, max 500 | URL of the master CMS that registered this slave |
| `ApiKey` | `string` | Required, max 1000 | Plain-text API key sent in first registration; used for subsequent validation |
| `RegisteredAt` | `DateTimeOffset` | Required | Timestamp of first registration |
| `LastContactedAt` | `DateTimeOffset?` | Optional | Updated on every successful master call (register, status push, get-url) |
> **Singleton enforcement**: The `Id` is a fixed known value (`Guid.Parse("00000000-0000-0000-0000-000000000001")`). On first `POST /api/v1/master/register` the row is created; on re-registration the same row is updated in-place. This avoids a composite unique constraint and makes EF upsert trivial.
---
## In-Process Cache: MasterStatusCache (static fields)
Not a DB entity — lives in memory on the slave process.
| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `_masterIsAvailable` | `bool` | `true` | Set by status push; `true` = pass gate |
| `_masterDisableMessage` | `string?` | `null` | Message forwarded from master to 503 response |
**No expiry** (Q4=A): cache is valid indefinitely until the master pushes again. If the master goes offline permanently, the last known status is used. Default = `true` (Available) = fail-open.
---
## DbContext: AvailabilityDbContext
New per-module DbContext in `SlpModularCms.Modules.Availability`. Separate from `ApplicationDbContext` (Core).
| DbSet | Entity | Table Name |
|-------|--------|-----------|
| `MasterRegistrations` | `MasterRegistration` | `AvailabilityMasterRegistrations` |
Migration assembly: `SlpModularCms.Modules.Availability`. Applied at startup in `AvailabilityModule.UseModule`.