refactor: enhance JWT authentication and CORS configuration

Updated JWT authentication to use configuration values instead of hardcoded secrets, improving security and flexibility. Enhanced CORS policy to conditionally allow origins based on configuration settings, with logging for permissive defaults. Updated README to reflect project renaming and clarify service context.
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-14 19:48:25 +03:00
parent 2fe394d732
commit 7025f4d075
74 changed files with 8494 additions and 19 deletions
@@ -0,0 +1,109 @@
# 01 — Vehicle Catalog
**Spec source**: `../../../suite/_docs/02_missions.md` § "Vehicles" (items 10-15) and `../../../suite/_docs/00_database_schema.md` § `Vehicles` table.
**Required permission**: `FL` (Operator, Operator+, Validator, CompanionPC, Admin, ApiAdmin per `../../../suite/_docs/00_roles_permissions.md`).
**Implementation status**: ✅ implemented (with one stricter-than-spec rule -- see Caveats #1).
> **NOTE (forward-looking)**: file paths and identifiers below reflect the post-rename state. Today's source still uses `Aircraft*` filenames + `[Route("aircrafts")]`. The renames are tracked under Jira AZ-EPIC children B6 (domain rename) and B8 (HTTP routes). The doc IS the spec for that work.
**Files** (post-rename):
- HTTP: `Controllers/VehiclesController.cs`
- Service: `Services/VehicleService.cs`
- DTOs: `DTOs/CreateVehicleRequest.cs`, `DTOs/UpdateVehicleRequest.cs`, `DTOs/GetVehiclesQuery.cs`, `DTOs/SetDefaultRequest.cs`
- Resource enums: `Enums/VehicleType.cs`, `Enums/FuelType.cs`
(The `Vehicle` entity itself lives in `04_persistence` because the table is part of the shared edge-PostgreSQL schema this service migrates.)
## 1. High-Level Overview
**Purpose**: Maintain the inventory of physical vehicles available to operators on this edge device. Vehicles are not just UAVs -- the catalog covers four classes today:
| `VehicleType` | Description |
|---------------|-------------|
| `Plane = 0` | Fixed-wing UAV |
| `Copter = 1` | Multirotor UAV |
| `UGV = 2` | Unmanned Ground Vehicle (per `../../../hardware/_standalone/target_acquisition/target_acquisition.md`) |
| `GuidedMissile = 3` | Single-use loitering munition |
Fields capture vehicle type, model / display name, fuel/battery characteristics, and an `is_default` flag used by the UI when starting a new mission.
**Architectural pattern**: Controller -> Service -> linq2db `ITable<Vehicle>` (active-record style; no repository abstraction).
**Upstream dependencies**: `05_identity` (`[Authorize FL]`), `04_persistence` (`AppDataConnection`, `Vehicle` entity), `06_http_conventions` (error mapping).
**Downstream consumers**: `02_mission_planning` reads vehicles (existence check on `mission.vehicle_id` in `MissionService.CreateMission` / `UpdateMission`).
## 2. Internal Interface
```csharp
public class VehicleService(AppDataConnection db) {
Task<Vehicle> CreateVehicle(CreateVehicleRequest);
Task<Vehicle> UpdateVehicle(Guid id, UpdateVehicleRequest);
Task<Vehicle> GetVehicle(Guid id);
Task<List<Vehicle>> GetVehicles(GetVehiclesQuery); // unpaginated by spec
Task DeleteVehicle(Guid id); // 409 if referenced by any mission
Task SetDefault(Guid id, SetDefaultRequest);
}
```
Throws `KeyNotFoundException` (-> 404), `InvalidOperationException` (-> 409, on delete-with-references).
## 3. External API
| Spec # | Endpoint | Method | Auth | Description |
|--------|----------|--------|------|-------------|
| 10 | `/vehicles` | POST | `FL` | Create. If `IsDefault=true`, code clears the flag on every other vehicle first (see Caveats #1). |
| 11 | `/vehicles/{id:guid}` | PUT | `FL` | Partial update -- every nullable field is applied only if non-null. |
| 12 | `/vehicles/{id:guid}` | DELETE | `FL` | 204 on success; 409 if any mission references the vehicle. |
| 13 | `/vehicles` | GET | `FL` | List, optionally filtered by `Name` (case-insensitive contains) and `IsDefault`. **Unpaginated** (matches spec). |
| 14 | `/vehicles/{id:guid}` | GET | `FL` | Single by id. 404 if missing. |
| 15 | `/vehicles/{id:guid}/default` | PATCH | `FL` | Set/clear default flag (see Caveats #1 for the exclusivity divergence). |
Wire shape: `Vehicle` entity serialized PascalCase via System.Text.Json defaults -- see `06_http_conventions` Caveats for the suite-wide divergence (spec is camelCase).
## 4. Data Access Patterns
| Query | Frequency | Hot Path | Index |
|-------|-----------|----------|-------|
| `vehicles WHERE id = ?` | Every read/update/delete | Yes | PK ✓ |
| `vehicles ORDER BY name` | List endpoint | Medium | None -- table is small in practice |
| `vehicles WHERE LOWER(name) LIKE %?%` | List with name filter | Low | None -- full scan |
| `vehicles WHERE is_default = TRUE -> UPDATE FALSE` | On every default-setting create/update/SetDefault | Medium | A partial index `WHERE is_default` would help if catalog grows |
### Storage Estimates
Not specified in spec. Vehicle tables in field deployments are typically tens to low hundreds of rows.
## 5. Implementation Details
**State Management**: Stateless service.
**Code-only business rule -- "exactly one default" exclusivity**: enforced by clearing the `is_default` flag on every other row BEFORE setting it on the target. **This is stricter than the spec** (`../../../suite/_docs/02_missions.md` §11 + §15 just `Set(IsDefault, request.IsDefault).Update()`). The exclusivity is also race-prone without a transaction (concurrent default-set ops can both clear and set, producing two defaults). Resolution tracked under Jira AZ-EPIC child B12.
**Error Handling**: Service throws domain exceptions; `06_http_conventions`' middleware maps them.
## 6. Extensions and Helpers
None.
## 7. Caveats & Edge Cases
1. **`IsDefault` exclusivity divergence from spec** -- code is stricter than spec; concurrent ops are race-prone (no transaction). Tracked as B12.
2. **`SetDefault(false)` does not preserve "at least one default exists"** -- caller can leave the system with zero defaults.
3. **No validation on request DTOs** (no `[Required]`, no range checks): empty `Name`, negative `BatteryCapacity`, invalid enum int values, etc., are accepted.
4. **Entity returned on the wire** with no DTO mapping -- couples DB column shape to HTTP response shape. Today benign because `Vehicle` has no associations.
5. **Case-insensitive search via `LOWER(...)`** -- full-table scan; fine while the catalog is small.
6. **`FuelType` may not fit `GuidedMissile`** -- the existing `{ Electric, Gasoline, Diesel }` set assumes a powered, reusable vehicle. Carry forward as Phase C decision (see plan); may spawn a follow-up ticket to allow a `None` value or make `FuelType` nullable for missiles.
## 8. Dependency Graph
**Must be implemented after**: `05_identity`, `06_http_conventions`, `04_persistence`.
**Can be implemented in parallel with**: `02_mission_planning` (modulo the existence-check coupling).
**Blocks**: `02_mission_planning` (existence check), `07_host`.
## 9. Logging Strategy
No app-level logs in this component. Errors surface via `06_http_conventions`' middleware only.
@@ -0,0 +1,143 @@
# 02 — Mission Planning (Missions + Waypoints + Cross-Service Cascade)
**Spec source**: `../../../suite/_docs/02_missions.md` § "Missions" (items 1-9), `../../../suite/_docs/00_database_schema.md` § `Missions` + `Waypoints`.
**Required permission**: `FL`.
**Implementation status**: ✅ implemented (with two divergences -- see Caveats).
> **NOTE (forward-looking)**: file paths, route prefixes, and identifiers below reflect the post-rename state. Today's source still uses `Flight*` filenames + `[Route("flights")]` and the cascade still touches `orthophotos` + `gps_corrections`. Renames + cascade shrink tracked under Jira AZ-EPIC children B6 (rename), B7 (GPS-Denied removal), B8 (HTTP routes).
**Files** (post-rename):
- HTTP: `Controllers/MissionsController.cs` (parent + nested waypoint routes)
- Services: `Services/MissionService.cs`, `Services/WaypointService.cs`
- DTOs: `DTOs/CreateMissionRequest.cs`, `DTOs/UpdateMissionRequest.cs`, `DTOs/GetMissionsQuery.cs`, `DTOs/CreateWaypointRequest.cs`, `DTOs/UpdateWaypointRequest.cs`, `DTOs/GeoPoint.cs`
- Resource enums: `Enums/WaypointSource.cs`, `Enums/WaypointObjective.cs`
(Entity row maps live in `04_persistence`.)
## 1. High-Level Overview
**Purpose**: Own the **mission lifecycle** for an edge deployment. A "mission" is a planned record with a name, creation timestamp, and assigned vehicle (Plane / Copter / UGV / GuidedMissile); "waypoints" are the ordered geo-points (with altitude, source, and objective) that define the mission's route. This component is consumed by `autopilot` (reads the mission and waypoints to drive the vehicle) and by the `ui` (map view + planning UI).
**Architectural pattern**: Aggregate root (`Mission`) with a sub-aggregate (`Waypoint`). Manual cascade-delete -- schema declares plain `REFERENCES` (no `ON DELETE CASCADE`); this service walks the dependency graph by hand.
**Cross-service contract -- the cascade**: when a mission or waypoint is deleted, this service **also** tears down rows in tables it does NOT own the schema for: `media` + `annotations` (owned by the `annotations` service) and `detection` (owned by the detection pipeline), plus its own `map_objects`. Per `../../../suite/_docs/02_missions.md` §5 + §9 this is the canonical, spec-defined behavior -- this service is the only place that knows the full mission ownership graph and is contractually responsible for this cleanup. The shared local PostgreSQL on the edge device makes the multi-table cascade physically possible in one connection.
**Removed from cascade in B7**: `orthophotos` and `gps_corrections`. Those tables now live in the separate `gps-denied` service per `../../../suite/_docs/11_gps_denied.md`. `MissionService.DeleteMission` and `WaypointService.DeleteWaypoint` no longer reference them.
**Upstream dependencies**: `05_identity` (`[Authorize FL]`), `04_persistence`, `06_http_conventions`, `01_vehicle_catalog` (existence check on `vehicle_id`).
**Downstream consumers** (live runtime): `autopilot` (reads missions + waypoints), `ui` (planning + map). The new `gps-denied` service references `mission_id` and `waypoint_id` from its own tables but does NOT depend on this service at runtime; cleanup of its rows is its own concern.
## 2. Internal Interface
```csharp
public class MissionService(AppDataConnection db) {
Task<Mission> CreateMission(CreateMissionRequest);
Task<Mission> UpdateMission(Guid id, UpdateMissionRequest);
Task<Mission> GetMission(Guid id);
Task<PaginatedResponse<Mission>> GetMissions(GetMissionsQuery);
Task DeleteMission(Guid id); // cross-service cascade
}
public class WaypointService(AppDataConnection db) {
Task<Waypoint> CreateWaypoint(Guid missionId, CreateWaypointRequest);
Task<Waypoint> UpdateWaypoint(Guid missionId, Guid waypointId, UpdateWaypointRequest);
Task<List<Waypoint>> GetWaypoints(Guid missionId); // unpaginated by spec
Task DeleteWaypoint(Guid missionId, Guid waypointId); // cross-service cascade
}
```
Throws `KeyNotFoundException` (-> 404), `ArgumentException` (-> 400, when referenced `vehicle_id` doesn't exist).
## 3. External API
### Missions
| Spec # | Endpoint | Method | Auth | Description |
|--------|----------|--------|------|-------------|
| 1 | `/missions` | POST | `FL` | Create. Body `CreateMissionRequest`. Code throws `ArgumentException -> 400` if `VehicleId` doesn't exist; spec says 404 -- minor divergence. |
| 2 | `/missions/{id:guid}` | PUT | `FL` | Partial update (Name and/or VehicleId). |
| 7 | `/missions/{id:guid}` | GET | `FL` | Single by id. |
| 8 | `/missions` | GET | `FL` | Paginated list. Query: `Name?`, `FromDate?`, `ToDate?`, `Page=1`, `PageSize=20`. Returns `PaginatedResponse<Mission>` (envelope from `06_http_conventions`). |
| 9 | `/missions/{id:guid}` | DELETE | `FL` | Cascade-deletes waypoints, media, annotations, detection, map_objects (in dependency order). |
### Waypoints (nested under mission)
| Spec # | Endpoint | Method | Auth | Description |
|--------|----------|--------|------|-------------|
| 3 | `/missions/{id:guid}/waypoints` | POST | `FL` | Create. Body `CreateWaypointRequest`. 404 if mission missing. |
| 4 | `/missions/{id:guid}/waypoints/{wpId:guid}` | PUT | `FL` | **Full overwrite** of all waypoint fields (Caveats #2 -- diverges from partial-update intent). |
| 5 | `/missions/{id:guid}/waypoints/{wpId:guid}` | DELETE | `FL` | Cascade-deletes related media, annotations, detection. |
| 6 | `/missions/{id:guid}/waypoints` | GET | `FL` | Ordered by `OrderNum`. Unpaginated (matches spec). |
Wire shape: PascalCase (suite-wide divergence -- see `06_http_conventions`).
## 4. Data Access Patterns
### Read queries
| Query | Frequency | Hot Path | Index |
|-------|-----------|----------|-------|
| `missions WHERE id = ?` | Every read/update/delete | Yes | PK ✓ |
| `missions WHERE ... ORDER BY created_date DESC LIMIT N OFFSET M` (+ count) | Listing | Yes | None on `created_date` -- could be added |
| `vehicles WHERE id = ?` (existence) | Every mission create / update with vehicle change | Yes | PK ✓ (cross-component) |
| `waypoints WHERE mission_id = ? AND id = ?` | Per-waypoint read/update/delete | Yes | PK + `ix_waypoints_mission_id` ✓ |
| `waypoints WHERE mission_id = ? ORDER BY order_num` | Nested list | Medium | `ix_waypoints_mission_id` (sort still in-memory) |
### Cascade-delete writes (`MissionService.DeleteMission`)
In strict dependency order:
1. `DELETE FROM map_objects WHERE mission_id = ?` (autopilot-written, owned-here schema)
2. Resolve `waypointIds = SELECT id FROM waypoints WHERE mission_id = ?`
3. If any: resolve `mediaIds`, `annotationIds`, then `DELETE FROM detection`, `DELETE FROM annotations`, `DELETE FROM media` *(cross-service tables -- schema owned by annotations + detection pipeline)*
4. `DELETE FROM waypoints WHERE mission_id = ?`
5. `DELETE FROM missions WHERE id = ?`
`WaypointService.DeleteWaypoint` does the equivalent steps 2-4 scoped to one waypoint.
**No transaction wraps either cascade** -- partial failure leaves orphan rows. Tracked as Caveat #1; would be a one-line fix (`db.BeginTransactionAsync()`).
### Caching
None.
### Storage Estimates
Not specified.
## 5. Implementation Details
**State Management**: Stateless services.
**Key business rules**:
- `mission.vehicle_id` must reference an existing vehicle (validated on create + on update if changed).
- `waypoint.mission_id` must reference an existing mission (validated on create).
- Cascade tables must be deleted in child-before-parent order due to FK constraints.
**Error Handling**: Services throw; `06_http_conventions` middleware maps.
## 6. Extensions and Helpers
`PaginatedResponse<T>` (defined in `06_http_conventions`) is consumed only by this component.
## 7. Caveats & Edge Cases
1. **No transaction around cascade delete** -- partial failure orphans rows in `media`, `annotations`, `detection`, `map_objects`, or `waypoints`. Wrapping in `db.BeginTransactionAsync()` is one extra line and would make the cascade atomic.
2. **`UpdateWaypoint` overwrites all fields** even though the request looks "partial-shaped" -- sending `{}` zeroes out coordinates and resets enums. Spec §4 also overwrites all fields, but spec uses the auto-converting `Geopoint` type so a missing `Geopoint` would be `null` not zero. With code's 3-flat-fields shape, this is more error-prone.
3. **Geopoint shape divergence from spec**: spec defines a single `string GPS` with auto-conversion (`Lat <-> MGRS`). Code uses 3 separate columns with no conversion. Carries through `Waypoint`, `MapObject`, and the request DTOs.
4. **Vehicle existence check + mission insert is non-transactional** -- TOCTOU window for vehicle delete is mitigated by the FK (which would reject the insert), but the error UX would surface as a 500 instead of a 400 in that race.
5. **No reorder endpoint** -- N waypoints reordered = N PUTs, racy.
6. **Cascade depends on cross-service tables existing in the same DB.** In standard edge deployment this is guaranteed (annotations/detection migrate them in the same compose stack, same `postgres-local`). In any deployment where those services are absent, the cascade will throw `relation does not exist`.
7. **Entity returned on the wire** with `[Association]` properties (`Mission.Vehicle`, `Mission.Waypoints`, `Waypoint.Mission`); LinqToDB does NOT eager-load by default on `FirstOrDefaultAsync(predicate)`, so they serialize as `null` / `[]`. Verify in Step 4 against actual responses.
8. **Spec §1 says 404 on missing VehicleId**; code throws `ArgumentException` which maps to **400**. Minor divergence.
## 8. Dependency Graph
**Must be implemented after**: `05_identity`, `06_http_conventions`, `04_persistence`, `01_vehicle_catalog`.
**Blocks**: `07_host`.
## 9. Logging Strategy
No app-level logs.
@@ -0,0 +1,122 @@
# 04 — Persistence (Edge PostgreSQL)
**Spec source**: `../../../suite/_docs/00_database_schema.md` (authoritative ER diagram), `../../../suite/_docs/00_top_level_architecture.md` § Database Topology (per-edge-device PostgreSQL pattern).
**Implementation status**: ✅ implemented -- the 4 owned tables migrate cleanly; the 3 borrowed tables read/delete cleanly under standard edge deployment.
> **NOTE (forward-looking)**: post-rename + post-GPS-Denied-removal. Today's source still has 6 owned tables (incl. `orthophotos`, `gps_corrections`) and 9 entity files (incl. `Aircraft.cs`, `Flight.cs`, `Orthophoto.cs`, `GpsCorrection.cs`). Renames + table drops tracked under Jira AZ-EPIC children B5 (namespace), B6 (rename), B7 (GPS-Denied removal), B9 (DB migration).
**Files** (post-rename):
- Connection / migrator: `Database/AppDataConnection.cs`, `Database/DatabaseMigrator.cs`
- Owned-table entities (4): `Database/Entities/{Vehicle, Mission, Waypoint, MapObject}.cs`
- Borrowed-table entities (3): `Database/Entities/{Media, Annotation, Detection}.cs`
- Cross-cutting enum: `Enums/ObjectStatus.cs`
## 1. High-Level Overview
**Purpose**: All PostgreSQL access for this service against the **shared local edge PostgreSQL**. Owns the LinqToDB `DataConnection` (one per HTTP request), the entity row maps, and the in-process schema bootstrap. The shared-DB pattern is documented in `../../../suite/_docs/00_top_level_architecture.md` § Database Topology -- every edge service connects to the same `postgres-local` and migrates only its own tables.
**Architectural pattern**: linq2db `DataConnection` + attribute-mapped entities. No repository abstraction.
**Upstream dependencies**: None.
**Downstream consumers**: `01_vehicle_catalog` (`VehicleService`), `02_mission_planning` (`MissionService`, `WaypointService`), `07_host` (registers the connection + runs the migrator).
## 2. Internal Interface
```csharp
public class AppDataConnection(DataOptions options) : DataConnection(options) {
// Owned tables -- schema migrated by this service
ITable<Vehicle> Vehicles; // owned + written
ITable<Mission> Missions; // owned + written
ITable<Waypoint> Waypoints; // owned + written
ITable<MapObject> MapObjects; // owned schema; written by autopilot
// Borrowed tables -- schema migrated by other suite services
ITable<Media> Media; // owned by `annotations` service
ITable<Annotation> Annotations; // owned by `annotations` service
ITable<Detection> Detections; // owned by detection pipeline
}
public static class DatabaseMigrator { static void Migrate(AppDataConnection db); }
```
Entity surface -- see `modules/entities.md` for column-level shape.
## 3. External API
Not applicable.
## 4. Data Access Patterns
### Tables this service migrates (owned)
| Table | Schema source | Writers | Indexes |
|-------|---------------|---------|---------|
| `vehicles` | this migrator | `01_vehicle_catalog` | PK only |
| `missions` | this migrator | `02_mission_planning` | PK + `ix_missions_vehicle_id` |
| `waypoints` | this migrator | `02_mission_planning` | PK + `ix_waypoints_mission_id` |
| `map_objects` | this migrator | `autopilot` (per `../../../suite/_docs/06_autopilot_design.md`) | PK + `ix_map_objects_mission_id` |
**Removed in B7 + B9**: `orthophotos` and `gps_corrections`. Those tables now live in the separate `gps-denied` service. `DatabaseMigrator` includes a one-shot `DROP TABLE IF EXISTS orthophotos; DROP TABLE IF EXISTS gps_corrections;` for fielded edge devices that previously ran the legacy schema (B9).
### Tables this service borrows (NOT migrated here, intentional cross-service ownership)
| Table | Schema source | Writers | This service's interaction |
|-------|---------------|---------|----------------------------|
| `media` | `annotations` migrator | `annotations` (Media CRUD) | Read `id`, `waypoint_id`; cascade-delete only (during mission/waypoint delete) |
| `annotations` | `annotations` migrator | `annotations` (Annotations CRUD) | Read `id`, `media_id`; cascade-delete only |
| `detection` (singular) | detection pipeline migrator | `detections` / `ai-training` | Read `id`, `annotation_id`; cascade-delete only |
This split matches the suite-wide pattern in `../../../suite/_docs/00_top_level_architecture.md` § Database Topology and `../../../suite/_docs/01_annotations.md` § Database. Each edge service migrates its own tables; all services see the full shared schema through their own `DataConnection`.
### Caching Strategy
None.
### Storage Estimates
Not specified in spec.
### Data Management
- **Seed data**: none. The migrator only creates schema.
- **Rollback**: not built-in. Forward-only-additive (`IF NOT EXISTS`); the B9 DROPs are the one explicit destructive step.
## 5. Implementation Details
**State Management**: `DataConnection` is per-HTTP-request scoped (registered via `AddScoped` in `07_host`). Each request gets its own physical Npgsql connection from the pool. All services within one request share that connection.
**Algorithmic Complexity**: Trivial -- direct column selects, FK joins via `[Association]`, single-table inserts/updates/deletes.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| `linq2db` | 6.2.0 | LINQ -> SQL provider, attribute mapping, async extensions |
| `Npgsql` | 10.0.2 | PostgreSQL driver |
**Error Handling**: linq2db / Npgsql exceptions propagate up; if they happen to be `KeyNotFoundException` / `Argument...` (rare), the global middleware in `06_http_conventions` maps them. Otherwise -> 500.
## 6. Extensions and Helpers
None.
## 7. Caveats & Edge Cases
1. **No schema versioning** -- additive `IF NOT EXISTS` only. Column drops, type changes, constraint changes require manual SQL or a migration tool. Acceptable today; will become a problem when the schema evolves under a deployed fleet. The B9 `DROP` is the one explicit exception.
2. **No transaction wrapping in `Migrate`** -- multi-statement `Execute` runs as autocommit-per-statement. All statements are individually idempotent so partial failure is recoverable on next startup.
3. **Mixed PK types**: `Guid` for in-house tables, `string` for `media`, `annotations` (XxHash64-based per `../../../suite/_docs/00_database_schema.md`). The TEXT-PK entities are the ones whose IDs are computed from file content, allowing dedup across services.
4. **Geopoint columns split into 3 fields** (`lat`, `lon`, `mgrs`) -- diverges from the spec's single `string GPS` representation. Carry the divergence to verification log.
5. **`detection` table singularity** -- owned by another service; not this service's call to rename.
6. **`LOWER(...)` indexes absent** -- case-insensitive name search is full-table scan. Fine while tables are small.
7. **No SQL logging configured** -- debug LinqToDB issues by enabling `DataConnection.WriteTraceLine` or wrapping the provider; not done today.
## 8. Dependency Graph
**Must be implemented after**: nothing internal.
**Can be implemented in parallel with**: `05_identity`, `06_http_conventions`.
**Blocks**: `01_vehicle_catalog`, `02_mission_planning`, `07_host`.
## 9. Logging Strategy
LinqToDB defaults -- no SQL logging configured.
@@ -0,0 +1,102 @@
# 05 — Identity & Authorization
**Spec source**: `../../../suite/_docs/10_auth.md` (suite-wide JWT model), `../../../suite/_docs/00_roles_permissions.md` (the `FL` permission code).
**Implementation status**: ✅ implemented. Single policy `FL` is declared and consumed by every controller.
> **NOTE (forward-looking)**: post-rename + post-GPS-Denied-removal. Today's `JwtExtensions.cs` also declares a `"GPS"` policy reserved for the (now-removed-from-this-repo) GPS-Denied endpoints. After Jira AZ-EPIC child B7 lands, only `"FL"` remains.
**Files**: `Auth/JwtExtensions.cs`
## 1. High-Level Overview
**Purpose**: Validate JWT bearer tokens issued by the remote `admin` service and expose the named authorization policy (`FL`) used by controllers in the feature components. This service does not issue tokens -- it consumes them.
**Architectural pattern**: ASP.NET Core extension method (`AddJwtAuth`) configuring `IServiceCollection` at DI time.
**Upstream dependencies**: None internally.
**Downstream consumers**: `07_host` (calls `AddJwtAuth(jwtSecret)` once); `01_vehicle_catalog`, `02_mission_planning` (controllers carry `[Authorize(Policy = "FL")]`).
## 2. Internal Interface
```csharp
public static IServiceCollection AddJwtAuth(this IServiceCollection services, string jwtSecret);
```
Side effects: registers `JwtBearerDefaults.AuthenticationScheme` and one named authorization policy in DI:
| Policy | Requirement |
|--------|-------------|
| `"FL"` | JWT contains a `permissions` claim with value `"FL"` |
## 3. Suite-wide JWT pattern
This is the canonical "every backend service" identity model in the Azaion suite. Per `../../../suite/_docs/00_top_level_architecture.md` and `../../../suite/_docs/10_auth.md`:
```
┌─────────────────────┐ ┌──────────────────────┐
│ Operator UI │ POST /login │ admin (.NET, remote) │
│ (React, edge) │ ──────────────► │ central user DB │
│ │ ◄────────────── │ mints HS256 JWT │
│ │ Bearer JWT │ (claim: permissions)│
└──────────┬──────────┘ └──────────────────────┘
│ Bearer JWT (the SAME token reused for every service)
├──────────────────► annotations (.NET, edge) -- ANN claim
├──────────────────► missions (.NET, edge) -- FL claim ◄── this service
├──────────────────► satellite-provider (.NET, remote) -- ADM claim
└──────────────────► (any future .NET service)
```
Every service (admin, annotations, missions, satellite-provider, ...) shares one HMAC secret (`JWT_SECRET`) and validates tokens locally with no network round-trip. The user logs in once at the UI; the resulting bearer token is reusable across every service. **This service neither issues tokens nor talks to the central user DB** -- it only validates.
The `permissions` claim drives per-service `[Authorize(Policy = "...")]` checks. The role -> permission matrix lives in `../../../suite/_docs/00_roles_permissions.md`. All routes here require `FL`.
## 4. External API
None directly. Auth contract is observable only via `401 Unauthorized` / `403 Forbidden` on protected routes.
## 5. Data Access Patterns
None.
## 6. Implementation Details
**Algorithm**: HMAC-SHA256 signature validation via `SymmetricSecurityKey(UTF-8(jwtSecret))`. Matches the suite-wide shared-secret model.
**Token validation flags**:
- `ValidateIssuerSigningKey = true`
- `ValidateLifetime = true` (with `ClockSkew = 1 minute` -- tighter than .NET's 5-minute default)
- `ValidateIssuer = false`, `ValidateAudience = false` -- `iss` / `aud` NOT enforced (consistent with shared-secret intra-suite model). Per the CMMC L2 scorecard (`../../../suite/_docs/05_security/cmmc_l2_scorecard.md` row 3), this is a known finding tracked at the suite level under AZ-487/AZ-494; the remediation will copy the `satellite-provider` pattern across `annotations` and `missions`.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| `Microsoft.AspNetCore.Authentication.JwtBearer` | 10.0.5 | JWT bearer middleware + handler |
| `Microsoft.IdentityModel.Tokens` | (transitive) | `SymmetricSecurityKey`, `TokenValidationParameters` |
## 7. Extensions and Helpers
None.
## 8. Caveats & Edge Cases
1. **Shared-secret trust model** -- any service that knows `JWT_SECRET` can mint tokens this API will accept. Not safe for multi-tenant or third-party token issuance. Consistent with the rest of the suite; tightening this is suite-wide work, not a per-service decision.
2. **No claim type for "user id" is consumed** -- only the `permissions` claim is checked. Services don't know who is calling them; per-user audit trails / business rules cannot be enforced at the service layer today. When a future feature needs an "applied by" attribution this gap will need to close.
3. **No offline-grace-window logic in this service** -- `../../../suite/_docs/10_auth.md` describes an offline JWT cache; that lives in the UI / `admin` consumption pattern, not here.
4. **Hardcoded fallback secret** in `Program.cs` (`"development-secret-key-min-32-chars!!"`) is dev-only. Production deployments MUST set `JWT_SECRET`.
5. **`FL` permission code carries the legacy "Flight" name even after the service rename to `missions`.** The plan documents this explicitly: changing the permission code is a fleet-wide auth change (would break every issued token until new ones are minted) and is **NOT** in this Epic's scope. Tracked as a TODO in `../../../suite/_docs/00_roles_permissions.md`.
## 9. Dependency Graph
**Must be implemented after**: nothing.
**Can be implemented in parallel with**: `04_persistence`, `06_http_conventions`.
**Blocks**: `07_host`, `01_vehicle_catalog`, `02_mission_planning`.
## 10. Logging Strategy
ASP.NET Core's JwtBearer handler logs token validation outcomes at default levels (Information / Debug). Not customized.
@@ -0,0 +1,117 @@
# 06 — HTTP Conventions (Suite-Standard Wire Layer)
**Spec source**: `../../../suite/_docs/00_top_level_architecture.md` § "Error Response Format" + § "Pagination". These are **suite-wide** — all .NET services (`missions`, `annotations`, `admin`, `satellite-provider`) are expected to emit the same shapes.
**Implementation status**: ⚠ **DIVERGES from suite spec on entity bodies (PascalCase) and on the error envelope's missing `errors` field**. The error envelope IS already camelCase on case (an accidental match — the anonymous object literal uses lowercase property names). See Caveats.
**Files**: `Middleware/ErrorHandlingMiddleware.cs`, `DTOs/ErrorResponse.cs`, `DTOs/PaginatedResponse.cs`
## 1. High-Level Overview
**Purpose**: Implement the suite's two cross-cutting wire conventions:
1. **Error envelope**`{ statusCode, message, errors? }` (camelCase, `errors` is `object?` keyed by field name) — emitted by the global exception → JSON middleware.
2. **Paginated response envelope**`{ items, totalCount, page, pageSize }` (camelCase) — wrapped around list endpoints.
**Architectural pattern**: ASP.NET Core middleware (pipeline interceptor) + plain DTO types.
**Upstream dependencies**: None internally.
**Downstream consumers**: `07_host` (registers middleware first in pipeline); `02_mission_planning` (consumes `PaginatedResponse<Mission>` from `MissionService.GetMissions`); every component benefits indirectly from the global error handler.
## 2. Internal Interface
```csharp
public class ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger) {
Task Invoke(HttpContext context);
}
public class ErrorResponse { // currently unused on the wire (Caveats)
int StatusCode;
string Message;
List<string>?Errors; // wrong shape per spec — see Caveats
}
public class PaginatedResponse<T> {
List<T> Items;
int TotalCount;
int Page;
int PageSize;
}
```
## 3. External API
Not an endpoint owner — defines the **error response wire shape** (which deviates from spec today).
### Spec-mandated shape (`../../../suite/_docs/00_top_level_architecture.md` § Error Response Format)
```json
{
"statusCode": 400,
"message": "Missing required fields",
"errors": {
"Name": ["Name is required"]
}
}
```
### Code's actual shape (anonymous object via `JsonSerializer.Serialize(new { statusCode, message })` with no naming policy override)
```json
{
"statusCode": 404,
"message": "Vehicle <guid> not found"
}
```
Property names are camelCase (the anonymous-type property names `statusCode` / `message` are written lowercase-first in code, and `System.Text.Json` preserves them as-is when no `JsonNamingPolicy` is configured). Two divergences from spec remain: no `errors` field, and the `ErrorResponse` DTO is unused (middleware writes the anonymous object instead, and the DTO's `Errors: List<string>?` is the wrong shape per spec — should be `object?` keyed by field name).
### Status code mapping (consistent with spec where there's overlap)
| Exception type | HTTP status |
|----------------|-------------|
| `KeyNotFoundException` | 404 Not Found |
| `ArgumentException` (base — covers `ArgumentNullException`, etc.) | 400 Bad Request |
| `InvalidOperationException` | 409 Conflict |
| anything else | 500 Internal Server Error (message generic, exception logged) |
## 4. Data Access Patterns
None.
## 5. Implementation Details
**State Management**: Stateless.
**Key Dependencies**: `System.Text.Json` (response serialization), `Microsoft.Extensions.Logging.ILogger<T>`.
**Error Handling Strategy**: This component IS the error handler. Recoverable domain failures (`KeyNotFound`, `Argument`, `InvalidOperation`) are mapped to specific status codes with the exception's message; everything else is a 500 with a sanitized message and a logged stack trace.
## 6. Extensions and Helpers
`ErrorResponse` and `PaginatedResponse<T>` could move to a shared helpers folder if a future component spawns more wire-shape concerns; today only `PaginatedResponse<T>` is consumed (by `MissionService.GetMissions`).
## 7. Caveats & Edge Cases
1. **PascalCase wire shape for entity bodies** vs. suite-spec camelCase. Controller responses that return entities (`Ok(vehicle)`, `Ok(mission)`, `PaginatedResponse<Mission>`) serialize PascalCase property names because the entity / DTO types are declared PascalCase and no `JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase` is configured. **Exception**: the global error envelope IS already camelCase (the anonymous object literal uses lowercase property names directly).
2. **`ErrorResponse` DTO is dead on the wire** — middleware writes an anonymous object instead. AND the DTO's `Errors` is `List<string>?` while spec says `object?` (per-field validation arrays keyed by field name). Two divergences in one DTO. Note: even if the DTO were used, its property names would serialize as PascalCase (`StatusCode`, `Message`, `Errors`) and would diverge from spec on case — additional reason the anonymous-object workaround happens to align with spec on case.
3. **No `errors` field emitted** — even when it would be relevant (validation failures). Today the codebase has no validation attributes anyway, so 400s come from `ArgumentException` with a single `Message`. When validation is added, the spec's per-field shape will need to be implemented.
4. **`InvalidOperationException → 409`** is non-standard; any third-party library throwing `InvalidOperationException` for an unrelated reason becomes a 409, masking the real cause. In this codebase the only intentional use is `VehicleService.DeleteVehicle` ("vehicle is referenced by missions" — a true 409).
5. **No correlation ID / request ID** in the error body — production support has to grep logs by timestamp.
6. **`PaginatedResponse<T>` is used by exactly one endpoint** (missions list). `vehicles` and `waypoints` listings are unpaginated by spec, so this is correct.
## 8. Dependency Graph
**Must be implemented after**: nothing.
**Can be implemented in parallel with**: `04_persistence`, `05_identity`.
**Blocks**: `07_host` (pipeline order matters — must be in DI by the time `app.UseMiddleware<ErrorHandlingMiddleware>` runs); `02_mission_planning` (uses `PaginatedResponse<T>`).
## 9. Logging Strategy
| Log Level | When | Example |
|-----------|------|---------|
| ERROR | Unhandled exception caught by the catch-all branch | `Unhandled exception` (stack trace attached via `LogError(ex, ...)`) |
(No INFO/DEBUG/WARN emitted by this component.)
@@ -0,0 +1,78 @@
# 07 — Host (Composition Root)
**Spec source**: `../../../suite/_docs/00_top_level_architecture.md` § Edge compose excerpt -- confirms the env vars (`DATABASE_URL`, `JWT_SECRET`), port (`5002:8080`), and DB target (`postgres-local`).
**Implementation status**: ✅ implemented.
> **NOTE (forward-looking)**: post-rename. Today's source has `Azaion.Flights` namespace + `dotnet Azaion.Flights.dll` entrypoint + container image `azaion/flights:*-arm`. Renames + DLL/image/compose changes tracked under Jira AZ-EPIC children B5 (namespace), B10 (Dockerfile + Woodpecker + suite compose).
**Files**: `Program.cs`, `GlobalUsings.cs`
## 1. High-Level Overview
**Purpose**: Build the ASP.NET Core web host: read environment, register all DI services, configure the request pipeline, run the schema migrator at startup, and serve the API on port 8080 (mapped to host 5002 in edge compose).
**Architectural pattern**: Composition root + ASP.NET Core minimal-host bootstrap (top-level statements).
**Upstream dependencies**: Every other component in this service.
**Downstream consumers**: The container runtime (`ENTRYPOINT ["dotnet", "Azaion.Missions.dll"]` in `Dockerfile` after B10) and any local `dotnet run`.
## 2. Internal Interface
None. The host has no exported types -- its surface is the running HTTP server.
## 3. External API
| Endpoint | Method | Auth | Description |
|----------|--------|------|-------------|
| `/health` | GET | Public | Returns `{ "status": "healthy" }` |
| `/swagger/*` | GET | Public | Swagger UI + JSON spec, served unconditionally in all environments |
| (mapped controllers from feature components) | various | Per-controller `[Authorize]` | See components 01 (vehicles) and 02 (missions). |
## 4. Data Access Patterns
- Opens a single scope at startup to call `DatabaseMigrator.Migrate(db)` -- populates the 4 owned tables in the shared local PostgreSQL.
- Registers `AppDataConnection` as **scoped** so each HTTP request gets a fresh `DataConnection` (one Npgsql connection per request from the pool).
## 5. Implementation Details
**State Management**: Stateless (request pipeline). The only run-once side effect is the migrator call.
**Key Dependencies**:
| Library | Version | Purpose |
|---------|---------|---------|
| `Microsoft.AspNetCore` (in `Microsoft.NET.Sdk.Web`) | net10.0 | Web host + middleware pipeline |
| `linq2db` | 6.2.0 | DB access via `AppDataConnection` registration |
| `Npgsql` | 10.0.2 | PostgreSQL driver (used through linq2db) |
| `Swashbuckle.AspNetCore` | 10.1.5 | Swagger UI + JSON spec generation |
**Error Handling**: Delegated to `06_http_conventions`' middleware, placed FIRST in the pipeline so it wraps everything else.
**Configuration**: Reads `DATABASE_URL` and `JWT_SECRET` from `IConfiguration` -> `Environment.GetEnvironmentVariable` -> hardcoded dev fallback. Both fallbacks are dev-only and MUST be overridden in production.
**`ConvertPostgresUrl` helper**: ad-hoc parser converting `postgresql://user[:pass]@host[:port]/db` to Npgsql key=value form. Does not URL-decode user/password -- caveat for credentials with `@`, `:`, `/`, `%`.
## 6. Extensions and Helpers
- `GlobalUsings.cs` -- three project-wide `global using` directives for LinqToDB.
## 7. Caveats & Edge Cases
- **No environment guards**: Swagger and the dev fallbacks for secrets are NOT gated on `IsDevelopment()`. If `JWT_SECRET` is unset in production, the service silently runs with the well-known development secret.
- **CORS open by default**: `AllowAnyOrigin/Method/Header` applied unconditionally. Spec doesn't mandate a CORS policy -- likely safe behind suite's reverse proxy on edge, but worth confirming.
- **Migrator failure crashes the process** at startup. Container orchestrator (Watchtower-restarted Docker) is expected to bring it back; `flight-gate` (per `../../../suite/_docs/00_top_level_architecture.md`) ensures this doesn't happen mid-mission.
- **No HTTPS redirection** middleware; assumes a TLS-terminating reverse proxy upstream (Caddy fronting Gitea is documented but in-deployment TLS termination is environment-specific).
- **Port 8080** matches the Dockerfile `EXPOSE 8080` and edge compose `5002:8080` mapping per `../../../suite/_docs/00_top_level_architecture.md` excerpt.
- **No GPS-Denied service registration** here. Earlier drafts of this doc reserved a slot for a GPS-Denied feature component; per Jira AZ-EPIC child B7, GPS-Denied lives in a separate (out-of-this-repo) service, so this host registers only `VehicleService`, `MissionService`, `WaypointService`.
## 8. Dependency Graph
**Must be implemented after**: every other component (01-06).
**Blocks**: nothing internal (it is the runtime root).
## 9. Logging Strategy
ASP.NET Core defaults (Console / Debug providers, no Serilog/structured logging configured). The only structured log emitted by app code is `06_http_conventions`' middleware `LogError(ex, "Unhandled exception")`. No correlation ID, no request tracing.