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.
18 KiB
Codebase Discovery — Azaion.Missions
NOTE (forward-looking): this discovery doc reflects the post-rename, post-GPS-Denied-removal target for this repo. Today the source still uses
Azaion.Flightsnamespace,Aircraft*/Flight*/Orthophoto*/GpsCorrection*filenames,[Route("aircrafts"|"flights")], and migrates 6 tables. The renames + drops are tracked under Jira AZ-EPIC + child tickets B5–B12 (see plan and_autodev_state.md). The doc IS the spec for that work.
Suite Context (read first)
This workspace is one submodule of the azaion-suite meta-repo (an aggregate of 11 component submodules orchestrated by the parent at ../../). The canonical, human-confirmed documentation for what this service is supposed to do lives at:
| Suite doc | Relevance to missions |
|---|---|
../../suite/_docs/02_missions.md |
Primary spec — Missions / Waypoints / Vehicles CRUD, all 15 endpoints (post-rename) |
../../suite/_docs/00_top_level_architecture.md |
Service topology, deployment tiers, error/pagination wire conventions |
../../suite/_docs/00_database_schema.md |
Authoritative ER diagram for the shared edge PostgreSQL |
../../suite/_docs/00_roles_permissions.md |
FL permission code consumed by this service |
../../suite/_docs/04_system_design_clarifications.md |
SSE conventions (relevant for downstream consumers) |
../../suite/_docs/glossary.md |
Suite-wide terminology |
../../suite/_docs/11_gps_denied.md |
NOT this service — GPS-Denied is a separate (out-of-this-repo) service |
../../suite/_docs/05_security/cmmc_l2_scorecard.md |
CMMC L2 row 3 finding on JWT iss/aud validation, tracked at suite level (AZ-487/AZ-494) |
../../suite/_docs/_repo-config.yaml post-rename: name: missions, stack: .NET, deployment_tier: edge, primary_doc: _docs/02_missions.md.
What missions IS, in suite terms
missions is the edge-tier .NET service that owns the "mission" domain of an Azaion deployment. It runs on each Jetson / OrangePI / operator-PC alongside annotations, detections, autopilot, gps-denied, and the React ui. All edge services share one local PostgreSQL on the device; each migrates only its own tables. JWTs are minted by the remote admin service and validated locally with the shared HMAC secret. Per the spec, this service has TWO feature slots, both gated by ONE permission:
| Spec feature slot | Permission | Status in code today |
|---|---|---|
| Vehicle Catalog (Plane / Copter / UGV / GuidedMissile inventory + default selection) | FL |
Implemented (today as "aircraft catalog" with Plane/Copter only; VehicleType expansion is B6) |
| Mission Planning (Missions + Waypoints CRUD + cross-service cascade-delete) | FL |
Implemented (today as "flight planning"; rename + cascade shrink is B6 + B7) |
GPS-Denied is not a feature of this service. The orthophoto / live-GPS / GPS-correction endpoints listed in ../../suite/_docs/11_gps_denied.md live in the new (separate) gps-denied service.
Why "missions" not "flights"
The previous service name flights was too narrow. The fleet covered by VehicleType includes:
Plane = 0— fixed-wing UAVCopter = 1— multirotor UAVUGV = 2— Unmanned Ground Vehicle (per../../hardware/_standalone/target_acquisition/target_acquisition.md)GuidedMissile = 3— single-use loitering munition
A "flight" only describes air vehicles. A "mission" is the right abstraction for any of the above.
Cross-service contracts owned here
- JWT validation — trusts admin-issued tokens via shared HMAC secret (
JWT_SECRET). This service never talks toadmin; it only validates. - Mission cascade-delete — when a mission or waypoint is deleted, this service tears down rows in
media,annotations,detection(owned schema-wise byannotations+ the detection pipeline) ANDmap_objects(written byautopilotfrom H3-indexed detections per../../suite/_docs/06_autopilot_design.md). It is the only place in the system that knows the full mission ownership graph. - Suite-standard wire shapes — error envelope and
PaginatedResponse<T>are shared withannotations,admin,satellite-provider(defined in../../suite/_docs/00_top_level_architecture.md).
Repository Layout
flights/ ← post-rename: missions/
├── Auth/ (1 file) — JWT bearer auth setup + permission policy
├── Controllers/ (2 files) — REST API surface
├── DTOs/ (15 files) — Request/response/query/shared payloads
├── Database/
│ ├── AppDataConnection.cs — LinqToDB DataConnection with ITable<T> properties
│ ├── DatabaseMigrator.cs — Bootstraps the 4 tables this service OWNS the schema for (post-B7+B9)
│ └── Entities/ (7 files) — LinqToDB-mapped row types (4 owned + 3 borrowed read-only stubs) (post-B7)
├── Enums/ (5 files) — Domain enumerations stored as INTEGER columns
├── Middleware/ (1 file) — Global exception → JSON error mapper
├── Entities/ (empty dir) — likely scaffolding leftover; delete in B5
├── Infrastructure/ (empty dir) — likely scaffolding leftover; delete in B5
├── Program.cs — Web host composition root + URL→connection-string adapter
├── GlobalUsings.cs — `global using LinqToDB[.Async|.Data]`
├── Azaion.Missions.csproj — `Microsoft.NET.Sdk.Web`, TargetFramework `net10.0` (post-B5)
├── Dockerfile — Multi-arch SDK build → `dotnet/aspnet:10.0` runtime, EXPOSE 8080 (entrypoint Azaion.Missions.dll post-B10)
├── README.md — One-liner; today says ".NET 8" — STALE (csproj targets net10.0)
├── .woodpecker/build-arm.yml — Single CI job: docker build + push on `[dev, stage, main]`
└── .gitignore
Total source files post-B7: ~33 .cs files across 7 logical directories (down from 37 today; loses Aircraft.cs renamed → Vehicle.cs, Flight.cs renamed → Mission.cs, Orthophoto.cs deleted, GpsCorrection.cs deleted, OrthophotoRequest.cs does not exist anyway).
Tech Stack
| Concern | Technology | Source of evidence |
|---|---|---|
| Language / runtime | C# on .NET 10 (net10.0) |
csproj |
| Web framework | ASP.NET Core (Microsoft.NET.Sdk.Web) |
csproj, Program.cs |
| Data access | linq2db 6.2.0 |
csproj, AppDataConnection.cs |
| Database driver | Npgsql 10.0.2 (PostgreSQL — shared with all other edge services) |
csproj, Program.cs (UsePostgreSQL) |
| Migrations | None (raw CREATE TABLE IF NOT EXISTS for the 4 owned tables, plus a one-shot DROP TABLE IF EXISTS block in B9 for legacy GPS-Denied tables) |
Database/DatabaseMigrator.cs |
| Auth | JWT bearer, HS256 shared-secret with admin; single claim-based permission FL (post-B7) |
Auth/JwtExtensions.cs |
| API docs | Swashbuckle 10.1.5 (Swagger UI mounted unconditionally) |
csproj, Program.cs |
| CORS | Open default policy (AllowAnyOrigin/Method/Header) |
Program.cs |
| Error handling | Custom middleware mapping KeyNotFoundException/ArgumentException/InvalidOperationException → 404/400/409, fallthrough → 500 |
Middleware/ErrorHandlingMiddleware.cs |
| Health endpoint | GET /health returns { status: "healthy" } |
Program.cs |
| Container build | Dockerfile multi-arch (--platform=$BUILDPLATFORM, dotnet publish --os linux --arch $arch) |
Dockerfile |
| CI | Woodpecker single ARM-tagged build-and-push job; tag pattern ${REGISTRY_HOST}/azaion/missions:${BRANCH}-arm (post-B10) |
.woodpecker/build-arm.yml |
| Tests | None present — tracked in ../../suite/_docs/_process_leftovers/2026-04-22_ci-unit-test-lane-missing-projects.md |
full file scan |
Entry Points
Program.cs— Web host (top-level statements). Builds DI graph, runsDatabaseMigrator.Migrateonce at startup, thenapp.Run().- HTTP routes (registered via
MapControllers+ one minimalMapGet) (post-B6 + B8):[Authorize FL]vehicles/*→VehiclesController(matches spec items 10–15 in../../suite/_docs/02_missions.md)[Authorize FL]missions/*→MissionsController(matches spec items 1–9, includes nestedmissions/{id}/waypoints/*)- Anonymous
GET /health - No GPS-Denied endpoints — those live in the separate
gps-deniedservice.
Configuration / Secrets
Resolved at startup in Program.cs:
| Key | Source order | Default (development fallback) |
|---|---|---|
DATABASE_URL |
IConfiguration → Environment.GetEnvironmentVariable → fallback |
Host=localhost;Database=azaion;Username=postgres;Password=changeme |
JWT_SECRET |
IConfiguration → Environment.GetEnvironmentVariable → fallback |
development-secret-key-min-32-chars!! |
DATABASE_URL accepts either a postgresql://user:pass@host:port/db URL (converted via local helper ConvertPostgresUrl) or a raw Npgsql connection string. Edge compose passes DATABASE_URL: postgresql://postgres:${PG_LOCAL_PASSWORD}@postgres-local/azaion per ../../suite/_docs/00_top_level_architecture.md.
Test Layout
No tests detected. Tracked in ../../suite/_docs/_process_leftovers/2026-04-22_ci-unit-test-lane-missing-projects.md. The autodev BUILD pipeline will fill this gap (Steps 3 → 6 of existing-code flow). Sibling project would be Azaion.Missions.Tests (post-B5).
Existing Documentation
README.md— one line, says ".NET 8" butcsprojtargetsnet10.0. STALE — fixed by Phase A1 of the rename plan.- No inline XML doc comments on any public type observed.
- No
docs/directory in this submodule. The canonical docs live at../../suite/_docs/(suite level). - This
_docs/02_document/tree is the local autodev artifact set.
Dependency Graph (file-level, internal-only) — post-rename
Modules grouped by topological layer (each module imports only from layers above it). Internal-only edges shown; external NuGet edges (LinqToDB, ASP.NET Core, Npgsql) omitted.
Layer 0 — Leaves (no internal deps)
Enums.VehicleType, Enums.FuelType, Enums.WaypointSource,
Enums.WaypointObjective, Enums.ObjectStatus
DTOs.GeoPoint, DTOs.SetDefaultRequest, DTOs.UpdateMissionRequest,
DTOs.CreateMissionRequest, DTOs.GetMissionsQuery, DTOs.PaginatedResponse,
DTOs.ErrorResponse, DTOs.GetVehiclesQuery
Database.Entities.Media, Database.Entities.Annotation,
Database.Entities.Detection
Middleware.ErrorHandlingMiddleware
Auth.JwtExtensions
GlobalUsings
Layer 1 — Depend on Enums (or GeoPoint)
DTOs.CreateVehicleRequest → Enums
DTOs.UpdateVehicleRequest → Enums
DTOs.CreateWaypointRequest → Enums, DTOs.GeoPoint
DTOs.UpdateWaypointRequest → Enums, DTOs.GeoPoint
Database.Entities.Vehicle → Enums
Database.Entities.MapObject → Enums
Database.Entities.Waypoint → Enums (Association → Mission)
Database.Entities.Mission → Enums (Associations → Vehicle, Waypoint)
Layer 2 — Database wiring
Database.AppDataConnection → all Database.Entities (7)
Database.DatabaseMigrator → Database.AppDataConnection
Layer 3 — Services
Services.VehicleService → AppDataConnection, Entities, DTOs
Services.WaypointService → AppDataConnection, Entities, DTOs, Enums
Services.MissionService → AppDataConnection, Entities, DTOs
(cascade also touches MapObjects,
Media, Annotations, Detections)
Layer 4 — HTTP surface
Controllers.VehiclesController → Services.VehicleService, DTOs
Controllers.MissionsController → Services.MissionService,
Services.WaypointService, DTOs
Layer 5 — Composition root
Program.cs → Auth.JwtExtensions, Database (AppDataConnection,
DatabaseMigrator), Middleware.ErrorHandlingMiddleware,
Services.{Vehicle,Waypoint,Mission}Service
Topological Processing Order (used for module-level documentation)
- Enums (5)
- Simple DTOs (8)
- Simple Entities (3 cross-service stubs)
- Cross-cutting leaves (3):
JwtExtensions,ErrorHandlingMiddleware,GlobalUsings - DTOs depending on Enums (4)
- Entities depending on Enums (4)
- DB wiring (2)
- Services (3)
- Controllers (2)
- Composition root (1)
No import cycles detected.
Cross-Repo Schema Ownership (the key insight)
The shared edge PostgreSQL hosts tables owned by multiple services. This service's AppDataConnection exposes ALL of them, but only migrates the 4 it owns:
| Table | Schema owner | Written by | Read by missions |
missions writes? |
|---|---|---|---|---|
vehicles |
missions | missions (VehicleService) |
yes | yes (full CRUD) |
missions |
missions | missions (MissionService) |
yes | yes (full CRUD) |
waypoints |
missions | missions (WaypointService) |
yes | yes (full CRUD) |
map_objects |
missions | autopilot (per ../../suite/_docs/06_autopilot_design.md) |
no | cascade-delete only |
media |
annotations (per ../../suite/_docs/01_annotations.md) |
annotations |
yes (id + waypoint_id resolution) | cascade-delete only |
annotations |
annotations |
annotations |
yes (id + media_id resolution) | cascade-delete only |
detection |
detection pipeline | detections / ai-training | yes (id resolution) | cascade-delete only |
orthophotos |
gps-denied (separate service, post-B7) | gps-denied | no | no (was: cascade-delete only) |
gps_corrections |
gps-denied (separate service, post-B7) | gps-denied | no | no (was: cascade-delete only) |
This pattern explains the shape of AppDataConnection and DatabaseMigrator — what looks like "incompleteness" in the migrator is the deliberate per-service ownership pattern.
Cycles / Anomalies (post-rename)
- Entity ↔ Entity navigation (
Mission.Waypoints↔Waypoint.Mission,Mission.Vehicle) — LinqToDB[Association]pair, not an import cycle. - Empty directories:
Entities/andInfrastructure/at the root are empty. Delete in B5 as part of the rename pass. detectiontable singularity:Detection.csmaps to[Table("detection")](singular) while every other table is plural. Owned by another service — naming is THEIR call to make consistent.- README ".NET 8" claim: contradicts
net10.0incsproj. Fix in Phase A1.
Spec ↔ Code Divergences (carried into the verification log)
| # | Concern | Spec source | Code reality | Resolution |
|---|---|---|---|---|
| 1 | Service / domain naming | ../../suite/_docs/02_missions.md (post-rename) |
Azaion.Flights.*, [Route("flights")], [Route("aircrafts")] today |
B5 + B6 + B8 |
| 2 | GPS-Denied feature slot | ../../suite/_docs/11_gps_denied.md (separate service) |
Schema (Orthophoto, GpsCorrection) + cascade branches still in this repo |
B7 + B9 (drop entirely) |
| 3 | VehicleType membership |
../../suite/_docs/02_missions.md (Plane / Copter / UGV / GuidedMissile) |
AircraftType { Plane, Copter } only |
B6 |
| 4 | Geopoint representation |
../../suite/_docs/02_missions.md § "GPS (Geopoint)" + ../../suite/_docs/00_database_schema.md (Waypoints.GPS: string) |
3 separate columns (lat NUMERIC, lon NUMERIC, mgrs TEXT); no auto-conversion |
Out of this Epic — carry forward |
| 5 | Error wire shape | ../../suite/_docs/00_top_level_architecture.md § Error Response Format (camelCase, errors: object? keyed by field) |
PascalCase { "StatusCode", "Message" }; no errors; ErrorResponse DTO has List<string>? (wrong shape) and is unused |
Out of this Epic — carry forward |
| 6 | Pagination wire shape | ../../suite/_docs/00_top_level_architecture.md § Pagination (camelCase) |
PascalCase via System.Text.Json defaults | Out of this Epic — carry forward |
| 7 | Vehicle IsDefault exclusivity |
../../suite/_docs/02_missions.md § 11 + 15 (just toggles) |
Code clears the flag on every other row (stricter than spec) + race-prone | B12 (decision-only ticket) |
| 8 | Vehicle listing pagination | Spec endpoint 13 says "unpaginated" | Matches spec ✓ | — |
| 9 | Waypoint listing pagination | Spec endpoint 6 says "unpaginated" | Matches spec ✓ | — |
| 10 | Cascade-delete chain | Spec § 9 covers mission delete cascade; § 5 covers waypoint delete | Code matches the spec's cascade order; missing transaction wrapping | B7 shrinks the chain; transaction wrap is opportunistic improvement carried forward |
| 11 | Swagger gating | Not specified | Unconditional, no IsDevelopment guard |
Out of this Epic |
| 12 | CORS policy | Not specified | Open in all environments | Out of this Epic |
| 13 | JWT issuer/audience validation | CMMC L2 finding (suite-tracked, AZ-487/AZ-494) | Disabled, consistent with shared-secret model; suite-level remediation pending | Suite-level (not this Epic) |
| 14 | FL permission code name |
../../suite/_docs/00_roles_permissions.md |
Code carries "FL" (legacy "Flight" name) even after rename to missions |
TODO in 00_roles_permissions.md; not this Epic (fleet-wide auth change) |
| 15 | FuelType for GuidedMissile |
Not specified | Existing { Electric, Gasoline, Diesel } may not fit single-use missiles |
Phase C decision — may spawn follow-up ticket |
Cross-cutting Observations
- No automated tests in the repository.
- CORS allows any origin/method/header in all environments — production exposure if not overridden upstream.
- JWT secret default (
development-secret-key-min-32-chars!!) is hardcoded; production deploys MUST setJWT_SECRET. - Swagger UI is enabled unconditionally (no
IsDevelopmentguard). - Database password default (
changeme) is hardcoded. - CI pipeline (
.woodpecker/build-arm.yml) has only a build/push step — no test, no security scan, no migration check. - Schema bootstrap runs every startup (
DatabaseMigrator.Migrate); idempotent (IF NOT EXISTS) but no schema versioning. The B9DROP TABLE IF EXISTSis the one explicit destructive step in the migrator's history.