[AZ-488] UAV tile batch upload + 5-rule quality gate

Replaces the 501 stub at POST /api/satellite/upload with a multipart
batch endpoint that ingests UAV-captured tiles, runs each item through
a 5-rule quality gate, and persists accepted tiles via the AZ-484
multi-source storage path with source='uav'.

Quality gate (in fixed order, first failure wins): JPEG format
(content-type + magic), size band 5 KiB-5 MiB, exact 256x256
dimensions, captured-at age (no future >30 s skew, no older than
7 days), luminance variance on 32x32 downsample. Closed reject-reason
enumeration in v1.0.0 contract.

Authorization: custom PermissionsRequirement / PermissionsAuthorization
Handler that reads the JWT `permissions` claim (tolerates both
repeated-string and JSON-array shapes). Endpoint protected by
RequiresGpsPermission policy; 401 without token, 403 without GPS perm.

Persistence: file-first to ./tiles/uav/{z}/{x}/{y}.jpg, then
ITileRepository.InsertAsync UPSERT (per-source UPSERT contract from
AZ-484). Per-item failures reported in response without aborting the
batch. Kestrel MaxRequestBodySize and FormOptions limits set to
MaxBatchSize x MaxBytes (default 100 x 5 MiB = 500 MiB).

New frozen contract: _docs/02_document/contracts/api/uav-tile-upload.md
v1.0.0. PT-08 NFR added to performance-tests.md as Deferred (harness
work tracked in PT-07 leftover, per AZ-488 § Risk 4).

Tests: 11 quality-gate unit tests, 5 handler unit tests, 3 file-path
unit tests, 12 permission-handler unit tests, 7 integration tests
(AC-1..AC-6, AC-8). All 253 unit tests + smoke integration suite
green.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 23:50:49 +03:00
parent 11b7074485
commit 1802d32107
35 changed files with 2280 additions and 107 deletions
+23 -11
View File
@@ -11,7 +11,7 @@ Application entry point. Configures DI container, sets up middleware, defines mi
| GET | `/tiles/{z}/{x}/{y}` | `ServeTile` | Slippy map tile server with in-memory caching |
| GET | `/api/satellite/tiles/latlon` | `GetTileByLatLon` | Download single tile by lat/lon/zoom |
| GET | `/api/satellite/tiles/mgrs` | `GetSatelliteTilesByMgrs` | MGRS stub (returns empty) |
| POST | `/api/satellite/upload` | `UploadImage` | Image upload stub (returns `Success: false`) |
| POST | `/api/satellite/upload` | `UploadUavTileBatch` | UAV tile batch upload (AZ-488) — multipart envelope, 5-rule quality gate, per-source UPSERT with `source='uav'`. Requires the `RequiresGpsPermission` policy. |
| POST | `/api/satellite/request` | `RequestRegion` | Queue region for async tile processing |
| GET | `/api/satellite/region/{id}` | `GetRegionStatus` | Get region processing status |
| POST | `/api/satellite/route` | `CreateRoute` | Create route with intermediate points |
@@ -19,24 +19,31 @@ Application entry point. Configures DI container, sets up middleware, defines mi
### Local Records (defined in Program.cs)
- `GetSatelliteTilesResponse`, `SatelliteTile` — MGRS response stubs
- `UploadImageRequest` — multipart form data request
- `SaveResult` — upload response stub
- `DownloadTileResponse` — tile download response
- `RequestRegionRequest` — region request body
- `ParameterDescriptionFilter` — Swagger operation filter
### Api/DTOs (AZ-488)
- `UavTileBatchUploadRequest` — multipart envelope with `metadata` (JSON string) and `files` (`IFormFileCollection`)
### Common/DTO (AZ-488)
- `UavTileMetadata`, `UavTileBatchMetadataPayload` — per-item metadata + envelope shape
- `UavTileBatchUploadResponse`, `UavTileUploadResultItem` — per-item response shape
- `UavTileUploadStatus`, `UavTileRejectReasons` — string-constant enumerations exposed in the v1.0.0 contract
## Internal Logic
### DI Registration
1. Serilog configured from `appsettings.json`
2. Connection string extracted from `ConnectionStrings:DefaultConnection`
3. Config bindings: `MapConfig`, `StorageConfig`, `ProcessingConfig`
4. Singletons: repositories (`TileRepository`, `RegionRepository`, `RouteRepository`), `GoogleMapsDownloaderV2`, `ITileService`, `IRegionService`, `IRouteService`
5. `IRegionRequestQueue` with configurable capacity
6. Hosted services: `RegionProcessingService`, `RouteProcessingService`
7. CORS policy: `TilesCors` — configured origins from `CorsConfig:AllowedOrigins`, falls back to allow-any
8. JSON options: camelCase, case-insensitive
9. **JWT authentication (AZ-487)**: `AddSatelliteJwt(builder.Configuration)` (extension in `SatelliteProvider.Api.Authentication`) registers `JwtBearer` with `TokenValidationParameters` set per the suite auth contract (signature + lifetime, no issuer/audience validation, 30 s clock skew, ≥ 32-byte HMAC key). Followed by `AddAuthorization()`.
3. Config bindings: `MapConfig`, `StorageConfig`, `ProcessingConfig`, `UavQualityConfig` (AZ-488)
4. **Request size limits (AZ-488)**: `KestrelServerOptions.Limits.MaxRequestBodySize` and `FormOptions.MultipartBodyLengthLimit` are set to `UavQualityConfig.MaxBatchSize × UavQualityConfig.MaxBytes` (default 100 × 5 MiB = 500 MiB) so an oversized UAV batch is rejected at the framework layer before reaching the handler.
5. Singletons: repositories (`TileRepository`, `RegionRepository`, `RouteRepository`), `GoogleMapsDownloaderV2`, `ITileService`, `IRegionService`, `IRouteService`, `IUavTileQualityGate`, `IUavTileUploadHandler` (AZ-488)
6. `IRegionRequestQueue` with configurable capacity
7. Hosted services: `RegionProcessingService`, `RouteProcessingService`
8. CORS policy: `TilesCors` — configured origins from `CorsConfig:AllowedOrigins`, falls back to allow-any
9. JSON options: camelCase, case-insensitive
10. **JWT authentication (AZ-487)**: `AddSatelliteJwt(builder.Configuration)` (extension in `SatelliteProvider.Api.Authentication`) registers `JwtBearer` with `TokenValidationParameters` set per the suite auth contract (signature + lifetime, no issuer/audience validation, 30 s clock skew, ≥ 32-byte HMAC key). Followed by `AddAuthorization` with the `RequiresGpsPermission` policy (AZ-488). The `PermissionsAuthorizationHandler` singleton supports both repeated-string and JSON-array shapes for the `permissions` claim.
### Startup
1. Database migration via `DatabaseMigrator.RunMigrations()` — throws on failure
@@ -57,6 +64,9 @@ Downloads a tile, persists it, returns metadata as `DownloadTileResponse`.
### RequestRegion Handler
Validates size (10010000m), delegates to `IRegionService.RequestRegionAsync`.
### UploadUavTileBatch Handler (AZ-488)
Buffers each `IFormFile` into memory, packages them as `UavUploadFile` records (filename, content-type, bytes), and delegates to `IUavTileUploadHandler.HandleAsync`. Envelope-level errors (mismatched batch, oversized batch, malformed metadata) are surfaced as HTTP 400 ProblemDetails; per-item rejects are returned in the HTTP 200 response payload. The endpoint is protected by `.RequireAuthorization(SatellitePermissions.UavUploadPolicy)` so 401 (no token) and 403 (no `GPS` permission) are returned before the handler runs.
## Dependencies
All project references: Common, DataAccess, Services.
NuGet: `Serilog.AspNetCore`, `Swashbuckle.AspNetCore`, `Microsoft.AspNetCore.OpenApi`, `Microsoft.AspNetCore.Authentication.JwtBearer` (8.0.21, AZ-487), `SixLabors.ImageSharp`, `Newtonsoft.Json`.
@@ -72,6 +82,7 @@ Defines several local request/response records that are not shared with other pr
All configuration sections are consumed here:
- `ConnectionStrings:DefaultConnection`
- `MapConfig`, `StorageConfig`, `ProcessingConfig`
- `UavQuality` (AZ-488) — `MinBytes`, `MaxBytes`, `MaxAgeDays`, `CapturedAtFutureSkewSeconds`, `MinLuminanceVariance`, `MaxBatchSize`, `LuminanceSampleSize`. Drives the 5-rule quality gate AND the per-request body-size limits.
- `CorsConfig:AllowedOrigins`
- `Jwt:Secret` — HMAC-SHA256 signing key for JWT validation (AZ-487). Resolution: `JWT_SECRET` env var (preferred, opaque production secret) → `Jwt:Secret` configuration key (`appsettings.Development.json` placeholder only). Startup fails fast if the resolved value is unset, empty, or shorter than 32 bytes.
- `Serilog` section
@@ -85,7 +96,8 @@ All configuration sections are consumed here:
- CORS configured (permissive by default when no origins specified)
- Swagger only in Development; Bearer token "Authorize" button registered via `AddSecurityDefinition`/`AddSecurityRequirement` (AZ-487)
- HTTPS redirection enabled
- JWT bearer authentication (AZ-487) — every endpoint requires a valid HS256-signed token. Anonymous, expired, or signature-tampered requests return 401 before the handler runs. Per-endpoint permission claims are layered on top in subsequent PBIs (e.g. AZ-488 requires `permissions: ["GPS"]` on the upload endpoint).
- JWT bearer authentication (AZ-487) — every endpoint requires a valid HS256-signed token. Anonymous, expired, or signature-tampered requests return 401 before the handler runs.
- Permission-claim policies (AZ-488) — `POST /api/satellite/upload` is wrapped in `.RequireAuthorization(SatellitePermissions.UavUploadPolicy)`. The `PermissionsAuthorizationHandler` reads the `permissions` claim (repeated-string OR JSON-array shape) and returns 403 if `GPS` is not present.
## Tests
Integration tests exercise all endpoints. Unit test project has only a dummy test.