Update OpenWiki documentation and claims
Some checks failed
Build / build (pull_request) Has been cancelled

Reference AGENTS.md in CLAUDE.md and record verification claims for
the architecture overview and operations runbook.

Signed-off-by: Tim Krampitz <Tim.Krampitz@live.com>
This commit is contained in:
Tim Krampitz
2026-09-07 17:53:25 +02:00
parent 118660a207
commit de852aada7
8 changed files with 669 additions and 44 deletions

View File

@@ -1,14 +1,17 @@
---
type: Architecture
title: System Architecture & Security Overview
description: Technical architecture of SlipItIn, covering .NET Aspire orchestration, ASP.NET Core SignalR hub, .NET MAUI MVVM client layer, WeakReferenceMessenger event routing, and dual-layer local storage.
tags: [architecture, spire, signalr, jwt, efcore, security, maui, mvvm]
description: Technical architecture of SlipItIn, covering .NET Aspire orchestration, ASP.NET Core SignalR hub, .NET MAUI MVVM client layer, WeakReferenceMessenger event routing, dual-layer local storage, and concurrency safety via IDbContextFactory.
tags: [architecture, aspire, signalr, jwt, efcore, security, maui, mvvm, concurrency, privacy]
openwiki:
roles: [architecture, domain]
change_kinds: [lifecycle, public-api]
source_paths: [SlipItIn/MauiProgram.cs, SlipItIn/Services/SignalRService.cs, SlipItIn/Services/GameStateService.cs, SlipItIn.Server/Program.cs, SlipItIn.Server/Hubs/GameHub.cs]
symbols: [SignalRService, GameStateService, LocalStorageService, GameHub, GameService, IDbContextFactory]
validation_commands: ["dotnet build SlipItIn.slnx"]
verified:
- by: openwiki/0.5.0
at: 2026-09-07T15:04:46.288Z
---
# System Architecture & Security Overview
@@ -25,11 +28,11 @@ The solution uses **.NET Aspire** to orchestrate application resources, services
- **AppHost (`SlipItIn.AppHost/AppHost.cs`)**:
- Provisions a PostgreSQL database container (`AddPostgres("pgsql").AddDatabase("postgresdb")`).
- Registers the backend project `SlipItIn.Server` with a direct reference to the PostgreSQL resource.
- Registers the client project `SlipItIn` with service discovery references to `SlipItIn.Server`.
- Registers the backend project `SlipItIn.Server` with a direct reference to the PostgreSQL resource, plus `WaitFor(db)` to ensure the database is ready before the server starts.
- Registers the client project `SlipItIn` with service discovery references to `SlipItIn.Server` via `WithReference(server)`.
- **Service Defaults (`SlipItIn.ServiceDefaults/Extensions.cs`)**:
- Configures **OpenTelemetry** logging, metrics (AspNetCore, HttpClient, Runtime), and tracing.
- Exposes standardized health check endpoints (`/health` and `/alive`).
- Configures **OpenTelemetry** logging (with `IncludeFormattedMessage` and `IncludeScopes`), metrics (AspNetCore, HttpClient, Runtime), and tracing.
- Exposes standardized health check endpoints (`/health` and `/alive`), filtering health paths from tracing.
- Enforces automatic HTTP resilience (`AddStandardResilienceHandler()`) and service discovery.
---
@@ -42,16 +45,19 @@ The backend is an ASP.NET Core Web API & SignalR application providing REST endp
1. **`Program.cs`**:
- Registers `AddServiceDefaults()`, `AddNpgsqlDataSource("postgresdb")`, and `AddDbContextFactory<SlipItInDbContext>()`.
- Configures JWT Bearer authentication with custom `OnMessageReceived` token resolution for SignalR.
- Automatically executes database migrations (`db.Database.Migrate()`) at startup.
- Configures JWT Bearer authentication with custom `OnMessageReceived` token resolution for SignalR and `OnTokenValidated` for user existence checks.
- Adds SignalR, CORS (`AllowAll`), controllers, and OpenAPI.
- Automatically executes database migrations (`db.Database.Migrate()`) and seeds test data (`DbSeeder.SeedAsync()`) at startup.
2. **`AuthController.cs`**:
- Manages user registration (`/api/auth/register`), authentication (`/api/auth/login`), and profile retrieval (`/api/auth/me`).
- Uses `BCrypt.Net` for secure password hashing.
- Issues JWT tokens signed with `Jwt:Key`, containing `ClaimTypes.NameIdentifier`, `ClaimTypes.Name`, and `ClaimTypes.Email`.
- Uses `IDbContextFactory` for thread-safe database access per request.
3. **`GameHub.cs`**:
- Real-time SignalR hub mapped to `/hubs/game`.
- Annotated with `[Authorize]` to reject unauthenticated WebSocket connections.
- Extracts and verifies user claims, delegating state mutations to `IGameService`.
- Manages `ConnectionId` lifecycle: stores it on `OnConnectedAsync` (auto-rejoining the lobby group), and clears it on `OnDisconnectedAsync`.
4. **`GameService.cs`**:
- Implements core business logic: game creation, player joins, card dealing, slip submission, challenge creation, and resolution.
- Adds `hostPlayer` directly to `game.Players` before `context.Games.Add(game)` to ensure EF Core navigation tracking consistency.
@@ -81,12 +87,16 @@ options.Events = new JwtBearerEvents
};
```
### Token Validation
`OnTokenValidated` extracts the `NameIdentifier` claim, creates a short-lived `DbContext` via `IDbContextFactory`, and verifies the user still exists and has `IsActive == true`. If the user is missing or inactive, the token is rejected before any hub method runs.
### Claims Validation & Access Control in `GameHub.cs`
Inside `GameHub.cs`, claims are extracted directly from the authenticated SignalR context:
* `GetAuthenticatedUserId()`: Extracts `Context.User?.FindFirst(ClaimTypes.NameIdentifier)`. Throws `UnauthorizedAccessException` if missing or invalid.
* `ValidatePlayerAccessAsync(playerId, authUserId)`: Queries the database to verify that `Player.UserId` matches the authenticated user ID.
* `ValidateHostAccessAsync(gameId, authUserId)`: Verifies that `Game.HostId` matches the authenticated user ID before allowing lobby configuration or game start.
* `ValidateUserInGameAsync(gameId, authUserId)`: Verifies the authenticated user is a participant in the specified game, used by `RequestGameState`.
* `ValidateChallengeTargetAccessAsync(challengeId, authUserId)`: Ensures only the accused player can resolve a challenge against them.
### Real-Time Auth Flow Diagram
@@ -101,14 +111,14 @@ sequenceDiagram
participant DB as SlipItInDbContext
Client->>Auth: POST /api/auth/login
Auth->>DB: Query User & Verify BCrypt Hash
Auth-->>Client: 200 OK (JWT Access Token)
Auth->>DB: Query User and Verify BCrypt Hash
Auth-->>Client: 200 OK with JWT Access Token
Client->>Hub: Connect WebSocket /hubs/game?access_token=JWT
Hub->>Hub: Validate JWT Signature & Claims
Client->>Hub: JoinLobby(lobbyCode)
Hub->>Hub: GetAuthenticatedUserId()
Hub->>Service: JoinGameAsync(lobbyCode, userId, connectionId)
Hub-->>Client: Broadcast PlayerJoined (GameStateDto)
Hub->>Hub: Validate JWT Signature, Claims, and User Active Status
Client->>Hub: JoinLobby with lobbyCode
Hub->>Hub: GetAuthenticatedUserId
Hub->>Service: JoinGameAsync with lobbyCode, userId, connectionId
Hub-->>Client: Broadcast PlayerJoined with GameStateDto
```
*Sequence diagram showing user authentication via REST and subsequent authenticated SignalR WebSocket connection.*
@@ -120,7 +130,7 @@ The cross-platform client app uses the **MVVM Pattern** built on `CommunityToolk
```mermaid
graph TD
subgraph UI_Layer["UI Layer (Views)"]
subgraph UI_Layer["UI Layer - Views"]
LoginPage["LoginPage"]
RegisterPage["RegisterPage"]
LobbyPage["LobbyPage"]
@@ -136,7 +146,7 @@ graph TD
subgraph Messaging_Layer["Messaging Layer"]
Messenger["WeakReferenceMessenger"]
Messages["Messages (GameStateChanged, PlayerHandChanged, ChallengeReceived)"]
Messages["Messages including GameStateChanged, PlayerHandChanged, ChallengeReceived, SyncStateChanged"]
end
subgraph Service_Layer["Service Layer"]
@@ -145,6 +155,7 @@ graph TD
AuthSessionSvc["AuthSessionService"]
ApiSvc["ApiService"]
LocalStorageSvc["LocalStorageService"]
AppConfigSvc["AppConfigurationService"]
end
LoginPage --> LoginVM
@@ -167,7 +178,7 @@ graph TD
### Dependency Injection & Service Registration (`MauiProgram.cs`)
- **Singletons**: `IAppConfigurationService` (`AppConfigurationService`), `ILocalStorageService` (`LocalStorageService`), `IAuthSessionService` (`AuthSessionService`), `IApiService` (`ApiService`), `ISignalRService` (`SignalRService`), `IGameStateService` (`GameStateService`), `LobbyViewModel`, `GameBoardViewModel`.
- **Transients**: `LoginViewModel`, `RegisterViewModel`.
- **Service Locator Helper**: `ServiceHelper.Services` captures `app.Services` at startup to allow static access where constructor DI is unavailable.
- **Service Locator Helper**: `ServiceHelper.Services` captures `app.Services` at startup to allow static access where constructor DI is unavailable. Provides `GetRequiredService<T>()` for use in contexts like `WeakReferenceMessenger` handlers.
### Decoupled Messaging Infrastructure (`WeakReferenceMessenger`)
To avoid memory leaks caused by long-lived event subscriptions in ViewModels, `GameStateService` translates raw `ISignalRService` hub events into strongly-typed messages dispatched via `WeakReferenceMessenger.Default`:
@@ -179,6 +190,7 @@ To avoid memory leaks caused by long-lived event subscriptions in ViewModels, `G
- `ChallengeReceivedMessage`: Dispatched when a player is directly targeted by a slip challenge.
- `ErrorOccurredMessage`: Dispatched when a hub or network error occurs.
- `ConnectionStateChangedMessage`: Dispatched when SignalR connection drops or reconnects.
- `SyncStateChangedMessage`: Dispatched when the sync status changes (syncing, idle, queue count). Carries a `SyncStateInfo` record with `IsSyncing`, `StatusText`, and `PendingActions` fields.
ViewModels implement `IRecipient<TMessage>` and register via `WeakReferenceMessenger.Default.RegisterAll(this)` to receive automated UI updates.
@@ -189,13 +201,20 @@ ViewModels implement `IRecipient<TMessage>` and register via `WeakReferenceMesse
_hubConnection = new HubConnectionBuilder()
.WithUrl(_configuration.HubUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult<string?>(token);
options.AccessTokenProvider = () => Task.FromResult(_authSession.AccessToken);
})
.WithAutomaticReconnect([TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)])
.WithAutomaticReconnect(new[] {TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)})
.Build();
```
When connection drops, `SignalRService` emits `ConnectionStateChanged`. Upon reconnection, `GameStateService` automatically triggers `ResyncAsync()` to fetch current state via `RequestGameStateAsync()` and flush locally queued offline actions.
When connection drops, `SignalRService` emits `ConnectionStateChanged` with `false`. Upon reconnection, it emits `ConnectionStateChanged` with `true`, and `GameStateService` automatically triggers `ResyncAsync()` to fetch current state via `RequestGameStateAsync()` and flush locally queued offline actions.
The `AccessTokenProvider` reads `_authSession.AccessToken` at invocation time (not cached at connection build time), ensuring the latest token is used on reconnects. The `SemaphoreSlim _connectLock` prevents concurrent connect/disconnect operations.
On the server side, `GameHub.OnConnectedAsync` automatically detects if the reconnecting user was already in a game (Lobby or InProgress), stores the new `ConnectionId`, and re-joins the SignalR lobby group. `OnDisconnectedAsync` clears the `ConnectionId` for all affected players to prevent messages from being sent to dead connections.
### Offline Action Queue with Retry
When the SignalR connection is unavailable, `GameStateService` provides `EnqueueActionAsync(method, arguments)` to persist actions locally. Each action stores the hub method name and serialized arguments as JSON. On reconnect, `FlushQueuedActionsCoreAsync` replays actions sequentially through `SignalRService.SendQueuedActionAsync`. Failed actions retry up to `MaxQueueRetries` (3) times; actions exceeding the retry limit are dropped and an error message is dispatched. The queue is persisted to `LocalStorageService` after each modification.
### Dual-Layer Storage Security Model (`LocalStorageService`)
To satisfy security and privacy constraints:
@@ -212,6 +231,7 @@ SignalR hubs process concurrent requests from multiple clients over persistent c
SlipItIn solves this by registering EF Core with `AddDbContextFactory<SlipItInDbContext>`:
* Every method call inside `GameHub.cs`, `AuthController.cs`, and `GameService.cs` creates a short-lived, isolated `DbContext` session using `using var context = _contextFactory.CreateDbContext()`.
* `OnTokenValidated` in Program.cs uses `CreateDbContextAsync` to verify user existence during JWT validation.
* **Benefit**: Thread-safe database operations across simultaneous challenges, card transfers, and lobby updates without race conditions.
---
@@ -220,8 +240,8 @@ SlipItIn solves this by registering EF Core with `AddDbContextFactory<SlipItInDb
To prevent players from inspecting opponent cards via network trace analysis, SlipItIn strictly separates public and private data DTOs:
* **`GameStateDto` (Public)**: Broadcast to all players in a lobby. Contains public game information (`GameId`, `LobbyCode`, `Status`, `CurrentRound`, `RoundTimeRemaining`) and player summaries (`PlayerId`, `Username`, `Score`, `IsReady`, `CardCount`). **Crucially, no card text is included.**
* **`PlayerHandDto` (Private)**: Direct unicast message sent *only* to the specific player's SignalR `ConnectionId`. Contains card text (`CardId`, `PhraseId`, `Text`, `IsUsed`).
* **`GameStateDto` (Public)**: Broadcast to all players in a lobby. Contains public game information (`GameId`, `LobbyCode`, `Status`, `CurrentRound`, `RoundTimeRemaining`) and player summaries (`PlayerId`, `Username`, `Score`, `IsReady`, `CardCount`). **Crucially, no card text is included.** The `CardCount` field represents only active (unused) cards.
* **`PlayerHandDto` (Private)**: Direct unicast message sent *only* to the specific player's SignalR `ConnectionId` via `Clients.Client(connectionId).SendAsync("PlayerHandUpdated", hand)`. Contains card text (`CardId`, `PhraseId`, `Text`, `IsUsed`). Only cards from the active round are included.
This architecture guarantees that players cannot see opponent phrase cards, even if they inspect raw WebSocket packets.
@@ -233,19 +253,24 @@ When extending or modifying the system architecture:
- **When to Consult**: Consult this document before adding new real-time SignalR methods, introducing client ViewModels, modifying storage mechanisms, or altering server authentication logic.
- **Runtime Invariants**:
- `GameHub` methods must validate caller claims via `GetAuthenticatedUserId()` and `ValidatePlayerAccessAsync()`.
- `GameHub` methods must validate caller claims via `GetAuthenticatedUserId()` and the appropriate `Validate*Async` method before every state mutation.
- Client ViewModels must consume `WeakReferenceMessenger` messages rather than subscribing directly to `ISignalRService` C# events.
- JWT tokens must never be persisted in `Preferences`; always use `SecureStorage`.
- `GameService` must use `IDbContextFactory` and create short-lived contexts — never inject a shared `DbContext`.
- New `GameHub` methods must handle `UnauthorizedAccessException`, `InvalidOperationException`, and general `Exception` with appropriate error messages sent to the caller.
- **Extension Points**:
- New real-time server messages: Add method to `GameHub`, add event to `ISignalRService`, add message class in `SlipItIn/Messages/`, update `GameStateService` listener.
- New ViewModels: Register as Transient (dialogs/auth) or Singleton (main navigation screens) in `MauiProgram.cs`.
- New queued offline actions: Use `GameStateService.EnqueueActionAsync` with the hub method name and arguments.
- **Primary Source Files**:
- `SlipItIn/MauiProgram.cs`
- `SlipItIn/Services/SignalRService.cs`
- `SlipItIn/Services/GameStateService.cs`
- `SlipItIn/Services/LocalStorageService.cs`
- `SlipItIn.Server/Program.cs`
- `SlipItIn.Server/Hubs/GameHub.cs`
- `SlipItIn.Server/Services/GameService.cs`
- `SlipItIn.Server/Controllers/AuthController.cs`
- **Focused Checks**: Build and verify project references across client, server, and shared libraries.
- **Minimal Validation Command**:
```bash