Files
Oleksandr Bezdieniezhnykh 7025f4d075 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.
2026-05-14 19:48:25 +03:00

5.6 KiB

Module: Azaion.Missions.Services.MissionService

File: Services/MissionService.cs

NOTE (forward-looking): post-rename + post-GPS-Denied-removal. Today's source is Services/FlightService.cs and its cascade still reaches into orthophotos + gps_corrections. Renames tracked under Jira AZ-EPIC child B6; cascade shrink under B7.

Purpose

CRUD over the missions aggregate plus a manual cascade-delete chain that walks mission -> waypoints -> media -> annotations -> detection and clears the map_objects side table.

Public Interface

public class MissionService(AppDataConnection db) {
    Task<Mission>                    CreateMission(CreateMissionRequest request);
    Task<Mission>                    UpdateMission(Guid id, UpdateMissionRequest request);
    Task<Mission>                    GetMission(Guid id);
    Task<PaginatedResponse<Mission>> GetMissions(GetMissionsQuery query);
    Task                             DeleteMission(Guid id);
}

Internal Logic

CreateMission

  1. Existence check: db.Vehicles.AnyAsync(v => v.Id == request.VehicleId). On miss -> throw ArgumentException (mapped to 400 by middleware).
  2. Build a fresh Mission:
    • Id = Guid.NewGuid()
    • CreatedDate = request.CreatedDate ?? DateTime.UtcNow
    • Name, VehicleId copied from request.
  3. db.InsertAsync(mission) and return.

UpdateMission

  1. Load by id (404 on miss).
  2. If request.Name != null -> assign.
  3. If request.VehicleId.HasValue:
    • Existence check on the new VehicleId (400 on miss).
    • Assign.
  4. db.UpdateAsync(mission).

GetMission

  • FirstOrDefaultAsync(m => m.Id == id) -> 404 on miss.

GetMissions -- paginated

  1. Build query on db.Missions.
  2. Filters:
    • Name: case-insensitive Contains (LOWER(name) LIKE %lower%).
    • FromDate: created_date >= FromDate.
    • ToDate: created_date <= ToDate.
  3. Compute totalCount via a CountAsync() call on the filtered query.
  4. Fetch the page: OrderByDescending(CreatedDate).Skip((Page-1)*PageSize).Take(PageSize).ToListAsync().
  5. Return PaginatedResponse<Mission> { Items, TotalCount, Page, PageSize }.

DeleteMission -- manual cascade

  1. Load the mission (404 on miss).
  2. Top-level scrub:
    • map_objects WHERE mission_id = id -> DeleteAsync.
  3. Resolve dependents through waypoints:
    • waypointIds = SELECT id FROM waypoints WHERE mission_id = id.
    • If any waypoints exist:
      • mediaIds = SELECT id FROM media WHERE waypoint_id IN (waypointIds).
      • If any media exist:
        • annotationIds = SELECT id FROM annotations WHERE media_id IN (mediaIds).
        • If any annotations exist: DELETE FROM detection WHERE annotation_id IN (annotationIds).
        • DELETE FROM annotations WHERE media_id IN (mediaIds).
      • DELETE FROM media WHERE waypoint_id IN (waypointIds).
  4. DELETE FROM waypoints WHERE mission_id = id.
  5. DELETE FROM missions WHERE id = id.

No transaction wraps the cascade -- partial failure leaves orphan rows.

Dependencies

  • LinqToDB (extension methods like AnyAsync, DeleteAsync)
  • AppDataConnection, Database.Entities.Mission + 6 other entities used in the cascade
  • DTOs.{CreateMissionRequest, UpdateMissionRequest, GetMissionsQuery, PaginatedResponse}

Consumers

  • Controllers.MissionsController -- wraps each method 1:1.

Data Models

Reads vehicles (existence check). Writes missions. On delete, also writes map_objects, media, annotations, detection, waypoints.

Cross-service contract

media / annotations / detection are owned schema-wise by other edge components (annotations, detection pipeline) on the shared local PostgreSQL. MissionService.DeleteMission is the canonical place that knows the full mission ownership graph: when a mission is deleted, its derived rows in those tables are scrubbed by THIS service. If those tables are missing on the deployment target, the cascade throws relation does not exist; in normal edge deployment they always exist (annotations migrates them at startup, same DB).

Removed from cascade in B7: orthophotos, gps_corrections. Those tables are no longer in this DB after B9; if a fielded device still has them they'll just be dropped (cascade no longer touches them).

Configuration / External Integrations

None.

Security

  • Behind [Authorize(Policy = "FL")] (controller-level).
  • Manual cascade respects FK direction; no SQL injection surface.

Tests

None present.

Notes / Smells

  1. Manual cascade vs. database ON DELETE CASCADE: the DB declares plain REFERENCES (no ON DELETE clause), so the service does the work. Deliberate (likely to permit partial denial / business rules), but it means schema and code are coupled -- any new mission-owned table must be added BOTH to the schema and to this method.
  2. No transaction around the cascade -- failure mid-cascade orphans rows. For PostgreSQL, wrapping in db.BeginTransactionAsync() is one extra line and would make the cascade atomic. Opportunistic improvement candidate.
  3. GetMissions does a count + page query -- two round trips. Standard. The count is unfiltered by pagination but filtered by the same predicates.
  4. Name filter and date filter are independent ANDs -- no support for OR or full-text search.
  5. No soft-delete -- deletes are physical. Audit/recovery would need DB-level WAL or external backup.
  6. UpdateMission allows changing VehicleId but does NOT validate that any in-mission state (waypoints, etc.) is compatible with the new vehicle. Probably fine if vehicle-specific behavior is downstream (e.g., autopilot recomputes route on next read).