Files
admin/_docs/02_tasks/done/AZ-513_classes_crud_routes.md
Oleksandr Bezdieniezhnykh 5e90512987 [AZ-197] Remove hardware ID binding from resource flow
Sealed-Jetson + SaaS architecture eliminates the credential-reuse-across-
machines threat that motivated hardware fingerprint binding. The binding's
only remaining effect was a real production failure mode on legitimate
hardware events.

Production:
- Drop PUT /users/hardware/set and POST /resources/check.
- Simplify POST /resources/get/{dataFolder?} (no Hardware field).
- Remove CheckHardwareHash, UpdateHardware, Security.GetHWHash.
- GetApiEncryptionKey signature: (email, password) — no hardwareHash.
- Drop SetHWRequest DTO and Hardware property from GetResourceRequest.
- Remove HardwareIdMismatch (40) and BadHardware (45) ExceptionEnum
  entries; numeric codes left as a gap, not for reuse.

Wire-compat policy: drop entirely (no Loader; no in-flight legacy
clients). Stale callers will see 404s, which is the right loud failure.

Tombstones:
- User.Hardware DB column kept (nullable, unused) — separate cleanup
  ticket for the migration per workspace "no rename without confirmation".
- User.LastLogin is now never written by app code (only writer was inside
  the deleted CheckHardwareHash); flagged in batch_06_review for a future
  ticket.

Tests:
- Delete e2e HardwareBindingTests (165 lines) and Azaion.Test
  UserServiceTest (sole test was CheckHardwareHashTest).
- Drop Hardware payloads + /resources/check preconditions from e2e
  ResourceTests, SecurityTests, ResilienceTests; drop hardwareId arg
  from Azaion.Test SecurityTest.
- Add SecurityTests.Hardware_endpoints_are_removed_AZ_197 (AC-2 regression
  asserting both removed routes return 404).

Docs:
- architecture.md: System Context note, ADR-003 new key formula, ADR-004
  retired with rationale.
- diagrams/flows/flow_hardware_check.md: tombstoned.

Also archives the four batch-1+batch-2 task files into _docs/02_tasks/done/
(file moves were missed by the batch_05 commit).

Code review: PASS — see _docs/03_implementation/reviews/batch_06_review.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 04:46:39 +03:00

8.6 KiB

Detection Classes CRUD Routes

Task: AZ-513_classes_crud_routes Name: POST + PATCH + DELETE /classes routes for detection-class CRUD Description: Add the three missing /classes endpoints to the admin API. The UI already calls POST and DELETE today (broken end-to-end — pre-existing bug); PATCH is required by AZ-512 (UI workspace) for the in-place edit affordance. Complexity: 3 points Dependencies: None on the admin/ side. Unblocks: AZ-512 in the UI workspace. Component: Admin API Tracker: AZ-513 Epic: AZ-509 Filed by: cross-workspace prerequisite from ui/ autodev cycle 3 batch 15 (BLOCKING gate). See ui/_docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md.

Problem

The UI workspace (ui/src/features/admin/AdminPage.tsx) calls three /classes endpoints today, but the admin/ service does not expose any of them — verified 2026-05-13 by grepping Azaion.AdminApi/Program.cs for MapPost|MapPatch|MapDelete against classes (zero matches):

  • POST /api/admin/classes — UI calls this to add a new detection class (handleAddClass). Today: 404. Pre-existing bug — the "Add" button on the Detection Classes table has been broken end-to-end against the live admin/ service.
  • DELETE /api/admin/classes/{id} — UI calls this to delete a class (handleDeleteClass). Today: 404. Pre-existing bug — the row delete affordance has been broken end-to-end too.
  • PATCH /api/admin/classes/{id} — UI does NOT call this today; AZ-512 (UI workspace) needs to call it to deliver the in-place edit affordance promised by Architecture Vision principle P12 ("the Admin Detection Classes table supports the full CRUD surface, not add+delete only"). Currently the route does not exist.

The UI's endpoints.admin.classes() and endpoints.admin.class(id) builders pin the URLs (ui/src/api/endpoints.ts) and are wire-contract tested in ui/src/api/endpoints.test.ts. nginx routes /api/admin/http://admin:8080/, so inside this service the paths are /classes and /classes/{id}.

Outcome

  • POST /classes creates a new detection class.
  • PATCH /classes/{id} updates an existing detection class (full or partial body — both accepted).
  • DELETE /classes/{id} deletes a detection class.
  • All three guarded by the same auth middleware as /users (apiAdminPolicy).
  • After this lands, the UI's existing add/delete affordances start working end-to-end, and AZ-512 (UI workspace) un-blocks for its in-place edit form.

Scope

Included

  • IDetectionClassService interface + DetectionClassService implementation in Azaion.Services/, injected via DI alongside IUserService (the existing pattern in Program.cs).
  • DTOs in Azaion.Common/Requests/: CreateDetectionClassRequest, UpdateDetectionClassRequest. Field set: name, shortName, color, maxSizeM (and photoMode if the live backend already supports it for ADD — verify via the existing read-path service before adding).
  • 3 minimal-API handlers in Program.cs mounted under the existing /classes prefix, each calling RequireAuthorization(apiAdminPolicy) per the /users precedent.
  • DB schema work: verify detection_classes table exists on the admin/ DB. If it does not, add a migration following the existing Azaion.Test/docker.test schema-init pattern.
  • Tests in Azaion.Test mirroring the existing user-CRUD test shape.

Excluded

  • UI-side changes — handled by AZ-512 in the UI workspace once this lands.
  • Read path GET /classes — UI today already reads classes successfully. Verify which service serves the read (likely annotations/); if it is NOT admin/, do NOT migrate the read path to admin/ as part of this ticket. Leave the read wire shape alone — the UI will keep calling whichever service answers today.
  • Bulk operations (no batch POST/PATCH/DELETE) — out of scope.
  • Soft-delete vs hard-delete policy — match whatever /users does today (presumably hard-delete).

Implementation Details

Endpoints (illustrative — match the /users pattern in Program.cs)

app.MapPost("/classes",
    async (IDetectionClassService svc, CreateDetectionClassRequest req, CancellationToken ct)
        => await svc.Create(req, ct))
    .RequireAuthorization(apiAdminPolicy)
    .WithSummary("Creates a new detection class");

app.MapPatch("/classes/{id}",
    async (IDetectionClassService svc, int id, UpdateDetectionClassRequest req, CancellationToken ct)
        => await svc.Update(id, req, ct))
    .RequireAuthorization(apiAdminPolicy)
    .WithSummary("Updates an existing detection class (partial-merge accepted)");

app.MapDelete("/classes/{id}",
    async (IDetectionClassService svc, int id, CancellationToken ct)
        => await svc.Delete(id, ct))
    .RequireAuthorization(apiAdminPolicy)
    .WithSummary("Deletes a detection class");

PATCH semantics

The UI sends the complete body on edit (per AZ-512 spec Risk 2 mitigation — "send all fields on PATCH so the implementer doesn't have to choose between full-replace and partial-merge"). Implementing PATCH as a partial-merge over the loaded entity is therefore safe: missing fields keep their existing value, present fields overwrite. Either full-replace or partial-merge works for the UI; partial-merge is the more conservative choice.

Acceptance Criteria

AC-1: POST /classes creates a class Given a valid ApiAdmin JWT and a body { "name": "Tank", "shortName": "T", "color": "#FF0000", "maxSizeM": 5.0 } When POST /classes is called Then 200 or 201 is returned with the created class object including its assigned id

AC-2: POST /classes requires ApiAdmin authorization Given a request without a JWT or with a non-ApiAdmin JWT When POST /classes is called Then 401 or 403 is returned

AC-3: PATCH /classes/{id} updates an existing class Given a class with id 7 exists with name: "Tank" When PATCH /classes/7 is called with body { "name": "Heavy Tank", "shortName": "T", "color": "#FF0000", "maxSizeM": 5.0 } Then 200 is returned with the updated class object reflecting name: "Heavy Tank"

AC-4: PATCH /classes/{id} accepts partial body Given a class with id 7 exists with name: "Tank", color: "#FF0000", maxSizeM: 5.0 When PATCH /classes/7 is called with body { "color": "#00FF00" } (only the changed field) Then 200 is returned with color: "#00FF00" and the other fields unchanged

AC-5: PATCH /classes/{id} returns 404 for unknown id Given no class with id 9999 exists When PATCH /classes/9999 is called Then 404 is returned

AC-6: PATCH /classes/{id} requires ApiAdmin authorization Given a request without a JWT or with a non-ApiAdmin JWT When PATCH /classes/{id} is called Then 401 or 403 is returned

AC-7: DELETE /classes/{id} removes a class Given a class with id 7 exists When DELETE /classes/7 is called Then 200 or 204 is returned and the class is gone from the DB

AC-8: DELETE /classes/{id} returns 404 for unknown id Given no class with id 9999 exists When DELETE /classes/9999 is called Then 404 is returned (or 200/204 if idempotent-delete is the established pattern in this service — match /users behavior)

AC-9: DELETE /classes/{id} requires ApiAdmin authorization Given a request without a JWT or with a non-ApiAdmin JWT When DELETE /classes/{id} is called Then 401 or 403 is returned

AC-10: After this ticket lands, UI add/delete affordances work end-to-end Given ui/src/features/admin/AdminPage.tsx is deployed against this admin/ build When the user clicks the existing "Add Class" button or the row delete button on the Detection Classes table Then the operation succeeds against the live admin/ service (no 404) (This AC is verified by the UI workspace's e2e harness in ui/e2e/, not by tests in this workspace. Cross-workspace coordination point: notify the UI team via the AZ-509 epic when this ships so they can re-attempt AZ-512 batch 15.)

Cross-workspace notes

This ticket exists because of a BLOCKING gate hit during the UI workspace's autodev cycle 3. Full context:

  • UI leftover record: ui/_docs/_process_leftovers/2026-05-13_az-512-admin-classes-prereq.md
  • UI task spec (parked in backlog until this ships): ui/_docs/02_tasks/backlog/AZ-512_admin_edit_detection_class.md
  • UI epic linkage: AZ-509 (Cycle 3 — Auth bootstrap fix + classColors carve-out + admin class edit). AZ-513 is linked under AZ-509 because that is the epic the prerequisite unblocks.

When this ticket ships, the UI workspace's leftovers replay step (next /autodev invocation) will:

  1. Re-run the verification grep against Azaion.AdminApi/Program.cs.
  2. If routes exist → move ui/_docs/02_tasks/backlog/AZ-512_*.md back to todo/ and re-attempt batch 15.
  3. If routes still missing → leave the UI leftover as-is, surface to the user.