Adds 2 units and docs for unit 3. nfr-requirements plan
This commit is contained in:
+152
@@ -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.
|
||||
+103
@@ -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.
|
||||
+72
@@ -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`.
|
||||
Reference in New Issue
Block a user