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>
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
IDetectionClassServiceinterface +DetectionClassServiceimplementation inAzaion.Services/, injected via DI alongsideIUserService(the existing pattern inProgram.cs).- DTOs in
Azaion.Common/Requests/:CreateDetectionClassRequest,UpdateDetectionClassRequest. Field set:name,shortName,color,maxSizeM(andphotoModeif the live backend already supports it for ADD — verify via the existing read-path service before adding). - 3 minimal-API handlers in
Program.csmounted under the existing/classesprefix, each callingRequireAuthorization(apiAdminPolicy)per the/usersprecedent. - DB schema work: verify
detection_classestable exists on the admin/ DB. If it does not, add a migration following the existingAzaion.Test/docker.testschema-init pattern. - Tests in
Azaion.Testmirroring 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 (likelyannotations/); if it is NOTadmin/, do NOT migrate the read path toadmin/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
/usersdoes 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:
- Re-run the verification grep against
Azaion.AdminApi/Program.cs. - If routes exist → move
ui/_docs/02_tasks/backlog/AZ-512_*.mdback totodo/and re-attempt batch 15. - If routes still missing → leave the UI leftover as-is, surface to the user.