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.
5.2 KiB
Module: Azaion.Missions.Services.VehicleService
File: Services/VehicleService.cs
NOTE (forward-looking): post-rename. Today's source is
Services/AircraftService.csoperating overAircraftentities. Renames tracked under Jira AZ-EPIC child B6.
Purpose
Encapsulates vehicle-related domain operations: CRUD plus the "is_default" exclusivity rule. This is the only place the "exactly one vehicle is default" invariant is enforced.
Public Interface
public class VehicleService(AppDataConnection db) {
Task<Vehicle> CreateVehicle(CreateVehicleRequest request);
Task<Vehicle> UpdateVehicle(Guid id, UpdateVehicleRequest request);
Task<Vehicle> GetVehicle(Guid id);
Task<List<Vehicle>> GetVehicles(GetVehiclesQuery query);
Task DeleteVehicle(Guid id);
Task SetDefault(Guid id, SetDefaultRequest request);
}
Internal Logic
CreateVehicle
- If
request.IsDefaultistrue, clearis_defaulton every other vehicle first (UPDATE vehicles SET is_default = FALSE WHERE is_default = TRUE). - Build a fresh
VehiclewithId = Guid.NewGuid()and copy every request field 1:1. db.InsertAsync(vehicle)and return the persisted entity.
UpdateVehicle
- Load the row by id; throw
KeyNotFoundExceptionif not found (mapped to 404 by middleware). - For each property on
UpdateVehicleRequest, apply only the non-null fields (partial update). - If
IsDefault.Value == true, run the same exclusivity clear asCreateVehicleBEFORE setting the new default. db.UpdateAsync(vehicle).
GetVehicle
FirstOrDefaultAsync(v => v.Id == id)-> 404 on miss.
GetVehicles
- Builds a LINQ query on
db.Vehicles. - Filters:
query.Name-> case-insensitiveContains(usesstring.ToLower()on both sides -- runs asLOWER(name) LIKE %lower(input)%server-side via LinqToDB translation);query.IsDefault-> exact equality. - Orders by
Nameascending. - Returns the full list -- no pagination despite
GetVehiclesQuerybeing a query object. Spec accepts this (vehicles are a small dataset).
DeleteVehicle
- Guard:
db.Missions.AnyAsync(m => m.VehicleId == id)-> if any mission references the vehicle, throwInvalidOperationException("Vehicle is referenced by missions") -> 409 Conflict. - Load by id (404 on miss).
db.Vehicles.DeleteAsync(v => v.Id == id).
SetDefault
- Loads the row (404 on miss).
- If
request.IsDefault == true, clears the flag on every other vehicle, then sets it on the target. - If
request.IsDefault == false, simply clears the flag on the target -- no guarantee remains that exactly one vehicle is default. Behavior is "the user told me to clear the flag, so I clear it".
"Exactly one default" rule -- spec vs code
This is the canonical surviving spec-vs-code divergence in the rename Epic. See Jira B12 (decision-only ticket).
- Spec (
../../suite/_docs/02_missions.md):PATCH /vehicles/{id}/default"toggles the default flag on a vehicle." Only the target row changes. - Code: when setting
IsDefault = true, the service first clears the flag on every other vehicle in the same connection. Two operations, no transaction.
Two real consequences:
- Code is stricter than spec -- in code there can be 0 or 1 default vehicle, never 2+. Spec would allow N defaults if the UI sets multiple.
- Race window: two concurrent "set default = true" calls on different rows could both clear-then-set, leaving two defaults. linq2db default behavior is autocommit per statement; no
BeginTransactionAsyncis called.
B12 either lifts the rule into spec + wraps in a transaction (preferred), or drops the side-effect from code and lets UI handle exclusivity. The race fix is part of either resolution.
Dependencies
Azaion.Missions.Database.AppDataConnectionAzaion.Missions.Database.Entities.VehicleAzaion.Missions.Database.Entities.Mission(queried viadb.MissionsinDeleteVehicle)Azaion.Missions.DTOs(request/query DTOs)
Consumers
Controllers.VehiclesController-- wraps every method 1:1 with an HTTP route.
Data Models
Reads/writes only vehicles table; reads (existence check) missions table.
Configuration / External Integrations
None.
Security
- All endpoints in the controller carry
[Authorize(Policy = "FL")], so this service is unreachable without a JWT bearingpermissions=FL. - Exception messages include the supplied id (
"Vehicle {id} not found") -- no PII risk for GUIDs.
Tests
None present.
Notes / Smells
GetVehiclesignores pagination by design -- the dataset is small (a fleet, not a catalog of millions). Inconsistent listing contract withMissionService.GetMissionsbut intentional.- "Exactly one default" race -- see B12 above.
- Delete fails fast on referenced vehicle but does NOT cascade-delete or null-out the
vehicle_id-- strictly a 409 to the caller. Consistent with the schema:missions.vehicle_idisNOT NULL REFERENCES vehicles(id)with noON DELETEclause (PostgreSQL defaults toNO ACTION). - Case-insensitive search uses
string.ToLower()on both sides, which LinqToDB renders asLOWER(...)-- non-indexed; full-table scan on large datasets. Fine at fleet size.