mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 16:41:09 +00:00
[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>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Resources Table & Update Check API
|
||||
|
||||
**Task**: AZ-183_resources_table_update_api
|
||||
**Name**: Resources Table & Update Check API
|
||||
**Description**: Add Resources table to admin API PostgreSQL DB and implement POST /get-update endpoint for fleet OTA updates
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: None
|
||||
**Component**: Admin API
|
||||
**Tracker**: AZ-183
|
||||
**Epic**: AZ-181
|
||||
|
||||
## Problem
|
||||
|
||||
The fleet update system needs a server-side component that tracks published artifact versions and tells devices what needs updating. CI/CD publishes encrypted artifacts to CDN; the server must store metadata (version, URL, hash, encryption key) and serve it to devices on request.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Resources table stores per-artifact metadata populated by CI/CD
|
||||
- Devices call POST /get-update with their current versions and get back only what's newer
|
||||
- Server-side memory cache handles 2000+ devices polling every 5 minutes without DB pressure
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Resources table migration (resource_name, dev_stage, architecture, version, cdn_url, sha256, encryption_key, size_bytes, created_at)
|
||||
- POST /get-update endpoint: accepts device's current versions + architecture + dev_stage, returns only newer resources
|
||||
- Server-side memory cache invalidated on CI/CD publish
|
||||
- Internal endpoint or direct DB write for CI/CD to publish new resource versions
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Resources table created**
|
||||
Given the admin API database
|
||||
When the migration runs
|
||||
Then the Resources table exists with all required columns
|
||||
|
||||
**AC-2: Update check returns newer resources**
|
||||
Given Resources table has annotations version 2026-04-13
|
||||
When device sends POST /get-update with annotations version 2026-02-25
|
||||
Then response includes annotations with version, cdn_url, sha256, encryption_key, size_bytes
|
||||
|
||||
**AC-3: Current device gets empty response**
|
||||
Given device already has the latest version of all resources
|
||||
When POST /get-update is called
|
||||
Then response is an empty array
|
||||
|
||||
**AC-4: Memory cache avoids repeated DB queries**
|
||||
Given 2000 devices polling every 5 minutes
|
||||
When POST /get-update is called repeatedly
|
||||
Then the latest versions are served from memory cache, not from DB on every request
|
||||
|
||||
**AC-5: Cache invalidated on publish**
|
||||
Given a new resource version is published via CI/CD
|
||||
When the publish endpoint/function completes
|
||||
Then the next POST /get-update call returns the new version
|
||||
|
||||
## Constraints
|
||||
|
||||
- Must integrate with existing admin API (linq2db + PostgreSQL)
|
||||
- encryption_key column must be stored securely (encrypted at rest in DB or via application-level encryption)
|
||||
- Response must include encryption_key only over HTTPS with valid JWT
|
||||
@@ -0,0 +1,95 @@
|
||||
# Register Device Endpoint
|
||||
|
||||
**Task**: AZ-196_register_device_endpoint
|
||||
**Name**: POST /devices endpoint for auto device registration
|
||||
**Description**: Add POST /devices endpoint to admin API that auto-generates device serial, email, and password for CompanionPC users
|
||||
**Complexity**: 2 points
|
||||
**Dependencies**: None
|
||||
**Component**: Admin API
|
||||
**Tracker**: AZ-196
|
||||
**Epic**: AZ-181
|
||||
|
||||
## Problem
|
||||
|
||||
During Jetson manufacturing, each device needs a unique CompanionPC identity (serial, email, password). Currently the provisioning script generates the email client-side and calls POST /users. The serial/email format should be server-controlled so the admin API is the single source of truth for device numbering.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Single POST /devices endpoint that requires no request body
|
||||
- Server auto-assigns the next sequential serial (azj-0000, azj-0001, ...)
|
||||
- Returns plaintext credentials so the provisioning script can embed them in device.conf
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- `RegisterDeviceResponse` DTO in `Azaion.Common/Requests/` with `Serial`, `Email`, `Password` fields
|
||||
- `RegisterDevice` method on `IUserService` / `UserService`
|
||||
- `POST /devices` endpoint in `Program.cs` with `RequireAuthorization(apiAdminPolicy)`
|
||||
- Sequential serial assignment based on most recent CompanionPC user
|
||||
|
||||
### Excluded
|
||||
|
||||
- Changes to the provisioning shell script (handled in loader repo)
|
||||
- Removing old POST /users endpoint (still used for non-device users)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Serial number logic
|
||||
|
||||
Email format: `azj-NNNN@azaion.com` where NNNN is zero-padded to 4 digits.
|
||||
|
||||
Constants at the top of `UserService`:
|
||||
|
||||
```csharp
|
||||
private const int SerialNumberStart = 4;
|
||||
private const int SerialNumberLength = 4;
|
||||
```
|
||||
|
||||
`RegisterDevice` implementation:
|
||||
|
||||
1. Query the single most recent CompanionPC user: `WHERE role = 'CompanionPC' ORDER BY created_at DESC LIMIT 1`
|
||||
2. If none found, next number is 0000; otherwise extract via `Substring(SerialNumberStart, SerialNumberLength)`, parse, increment
|
||||
3. Generate email `azj-{number:D4}@azaion.com`
|
||||
4. Generate random 32-char hex password: `Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLower()`
|
||||
5. Insert User with `Role = CompanionPC`, `IsEnabled = true`, password hashed via `ToHash()`
|
||||
6. Return `RegisterDeviceResponse { Serial, Email, Password (plaintext) }`
|
||||
|
||||
### Endpoint
|
||||
|
||||
```csharp
|
||||
app.MapPost("/devices",
|
||||
async (IUserService userService, CancellationToken cancellationToken)
|
||||
=> await userService.RegisterDevice(cancellationToken))
|
||||
.RequireAuthorization(apiAdminPolicy)
|
||||
.WithSummary("Creates a new device");
|
||||
```
|
||||
|
||||
No request body required.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: First device gets serial azj-0000**
|
||||
Given no CompanionPC users exist in the database
|
||||
When POST /devices is called with a valid ApiAdmin JWT
|
||||
Then the response contains `serial: "azj-0000"`, `email: "azj-0000@azaion.com"`, and a 32-char hex password
|
||||
|
||||
**AC-2: Sequential numbering**
|
||||
Given azj-0000 already exists
|
||||
When POST /devices is called again
|
||||
Then the response contains `serial: "azj-0001"`
|
||||
|
||||
**AC-3: User persisted with correct role**
|
||||
Given POST /devices returned successfully
|
||||
When the users table is queried
|
||||
Then a user exists with the returned email, Role=CompanionPC, IsEnabled=true
|
||||
|
||||
**AC-4: Password is hashed in DB**
|
||||
Given POST /devices returned a plaintext password
|
||||
When the users table is inspected
|
||||
Then PasswordHash contains the SHA-384 hash of the plaintext password, not the plaintext itself
|
||||
|
||||
**AC-5: Requires ApiAdmin authorization**
|
||||
Given a request without a JWT or with a non-ApiAdmin JWT
|
||||
When POST /devices is called
|
||||
Then 401 or 403 is returned
|
||||
@@ -0,0 +1,126 @@
|
||||
# Remove Hardware ID Binding
|
||||
|
||||
**Task**: AZ-197_remove_hardware_id
|
||||
**Name**: Remove hardware ID binding from resource flow (admin-side cleanup)
|
||||
**Description**: Remove `CheckHardwareHash`, `UpdateHardware`, `HardwareService`, the `PUT /users/hardware/set` endpoint, and the hardware-hash component of API encryption-key derivation. The threat this protected against (credential reuse across machines via desktop installers) no longer exists in the target architecture.
|
||||
**Complexity**: 3 points
|
||||
**Dependencies**: None
|
||||
**Component**: Admin API
|
||||
**Tracker**: AZ-197
|
||||
**Epic**: AZ-181
|
||||
|
||||
## Problem
|
||||
|
||||
The `Hardware` field on `User` and the `CheckHardwareHash` flow were designed to bind a user account to a specific physical machine, preventing credential reuse across machines when users had desktop installers.
|
||||
|
||||
The target architecture has eliminated that threat:
|
||||
|
||||
- **Edge devices** ship as **secured Jetsons with fTPM** (secure boot, fTPM-protected key storage, no user filesystem access, no desktop installers distributed). Hardware identity is anchored in the fTPM, not in a SHA-384 of CPU/GPU/Memory/DriveSerial strings.
|
||||
- **Server / desktop access** uses the **SaaS** path (browser → admin API). There is no installer to copy and no hardware fingerprint to take.
|
||||
- The Loader component itself has been **architecturally retired** (Scenario X = Watchtower + rclone + flight-gate; see `suite/_docs/_repo-config.yaml` `unresolved:loader-retirement-arch-doc` and the assumptions log entries dated 2026-04-19). Provisioning was relocated from `loader/scripts/` to `suite/_infra/provisioning/`. There is no `loader/` workspace in the suite anymore, so this ticket is now **purely admin-side**.
|
||||
|
||||
The hardware binding therefore adds:
|
||||
- Unnecessary complexity in the encryption-key derivation chain.
|
||||
- A real production failure mode (`HardwareIdMismatch`, error code 40) on legitimate drive-replacement / fTPM-attested rotations.
|
||||
- A maintenance cost on every endpoint and DTO that still carries the `Hardware` field.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Resource download flow no longer requires a hardware fingerprint.
|
||||
- API encryption-key derivation simplified to email + password only.
|
||||
- All admin-API hardware-binding code paths removed.
|
||||
- Hardware-binding tests removed (unit + e2e); other tests updated to stop sending `Hardware`.
|
||||
- DB column `User.Hardware` left in place but nullable and unused — no migration in this ticket (separate cleanup ticket if/when desired).
|
||||
|
||||
## Scope
|
||||
|
||||
### Included — Admin API production code
|
||||
|
||||
- Remove `CheckHardwareHash` and `UpdateHardware` from `IUserService` / `UserService` (`Azaion.Services/UserService.cs`).
|
||||
- Remove `PUT /users/hardware/set` endpoint from `Azaion.AdminApi/Program.cs`.
|
||||
- Simplify `POST /resources/get/{dataFolder}` (`Program.cs` + `Azaion.Services/ResourcesService.cs`): remove `request.Hardware` parameter usage; derive encryption key without the hardware hash.
|
||||
- Simplify `POST /resources/check`: remove the hardware-binding side-effect entirely. If the endpoint becomes purely a "do I have any newer resources?" probe, keep it; if it becomes a no-op shell, remove it (decide based on what consumers still call today).
|
||||
- Update `Security.GetApiEncryptionKey` (`Azaion.Services/Security.cs`) to drop the `hardwareHash` parameter from its signature; derive the key from `email + password` only.
|
||||
- Remove (do not deprecate) `Security.GetHWHash` — the codebase has no Loader to coordinate with anymore.
|
||||
- Remove `SetHWRequest` DTO (`Azaion.Common/Requests/SetHWRequest.cs`).
|
||||
- Remove the `Hardware` property usage from `GetResourceRequest` (`Azaion.Common/Requests/GetResourceRequest.cs`). The wire field may still be **accepted** (deserialized and ignored) for one release cycle to keep any in-flight legacy clients from breaking on 400s — pick the simplest of (drop entirely / accept-and-ignore) and document the choice in the implementation report.
|
||||
- Remove `HardwareIdMismatch` and `BadHardware` from `Azaion.Common/BusinessException.cs` `ExceptionEnum`.
|
||||
- Leave `User.Hardware` column in DB (nullable, unused). No migration here.
|
||||
|
||||
### Included — Tests in this workspace
|
||||
|
||||
- Delete `e2e/Azaion.E2E/Tests/HardwareBindingTests.cs` entirely (every test in that file asserts behaviour that is being removed).
|
||||
- Update `e2e/Azaion.E2E/Tests/ResourceTests.cs`, `ResilienceTests.cs`, `SecurityTests.cs` to stop sending the `Hardware` field on resource calls (or to assert the field is ignored, whichever matches the chosen wire-compat policy above).
|
||||
- Update `Azaion.Test/UserServiceTest.cs` and `Azaion.Test/SecurityTest.cs` to remove tests asserting hardware-hash behaviour and to drop the `hardwareHash` argument from any retained `GetApiEncryptionKey` calls.
|
||||
- Trim test fixtures in `db-init/` and `Azaion.Test` if they seed a `User.Hardware` value purely to satisfy hardware-binding flows.
|
||||
|
||||
### Included — Workspace docs (pointer-only updates, no full rewrite)
|
||||
|
||||
- Mark `_docs/02_document/diagrams/flows/flow_hardware_check.md` as obsolete (header note + link to AZ-197 implementation report) — full deletion is fine if cleaner.
|
||||
- Mark `_docs/02_document/modules/common_requests_set_hw.md` as obsolete (the documented module no longer exists).
|
||||
- Note in `_docs/02_document/architecture.md` (Security & Encryption section) that API encryption-key derivation no longer includes a hardware hash, and that `User.Hardware` is a tombstoned column.
|
||||
|
||||
### Excluded
|
||||
|
||||
- Database migration to drop the `hardware` column from `users` (separate ticket if/when desired; harmless to leave nullable).
|
||||
- Changes to user registration or login flow (those don't touch the hardware path).
|
||||
- Any change to the suite-level `_docs/00_top_level_architecture.md` "Security & Encryption" / "Binary Split Security" sections — that's part of `unresolved:loader-retirement-arch-doc` and is owned at the suite level, not by this ticket.
|
||||
- Live-device decommissioning of fielded Loader containers (separate ops runway, tracked as a sibling of `loader-retirement-arch-doc`).
|
||||
- Anything in the `loader/` workspace — it does not exist in the suite anymore.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: Resource download works without hardware**
|
||||
Given a provisioned device user with valid email and password
|
||||
When `POST /resources/get/{dataFolder}` is called without a `Hardware` field
|
||||
Then the resource is returned and decrypts successfully using a key derived from email + password only
|
||||
|
||||
**AC-2: Hardware-set endpoint is gone**
|
||||
Given the updated admin API
|
||||
When `PUT /users/hardware/set` is called with any payload
|
||||
Then the response is 404
|
||||
|
||||
**AC-3: Encryption-key derivation is simplified**
|
||||
Given the updated `Security.GetApiEncryptionKey`
|
||||
When it is called with `(email, password)`
|
||||
Then it returns the key derived from `email + password` only — there is no `hardwareHash` parameter on the public signature
|
||||
|
||||
**AC-4: Hardware-binding tests are gone**
|
||||
Given the updated test projects
|
||||
When the test suite is built and listed
|
||||
Then `HardwareBindingTests` does not exist and no remaining test asserts `HardwareIdMismatch` / error code 40 / hardware-hash binding
|
||||
|
||||
**AC-5: Resource calls in remaining tests do not send `Hardware`**
|
||||
Given the updated `ResourceTests`, `ResilienceTests`, `SecurityTests`
|
||||
When the resource-download / resource-check requests are inspected
|
||||
Then no test sends a `Hardware` field on any resource request (or, if accept-and-ignore wire-compat was chosen, tests assert the response is unchanged whether `Hardware` is present or absent)
|
||||
|
||||
**AC-6: ExceptionEnum no longer has hardware codes**
|
||||
Given `Azaion.Common/BusinessException.cs`
|
||||
When the `ExceptionEnum` is read
|
||||
Then `HardwareIdMismatch` and `BadHardware` entries are gone, and no production code references them
|
||||
|
||||
**AC-7: Build is clean**
|
||||
Given the workspace after the changes
|
||||
When `dotnet build` runs across the solution
|
||||
Then it completes with no errors and no new warnings introduced by this ticket
|
||||
|
||||
**AC-8: Test suite passes**
|
||||
Given the workspace after the changes
|
||||
When the existing test suite (`Azaion.Test` + `e2e/Azaion.E2E`) is run via `docker-compose.test.yml`
|
||||
Then all tests pass (the deleted `HardwareBindingTests` are not counted)
|
||||
|
||||
## Constraints
|
||||
|
||||
- Wire-compat policy on the `Hardware` field of resource requests must be chosen explicitly (drop / accept-and-ignore) and recorded in the implementation report — this is the only consumer-facing contract change in the ticket.
|
||||
- Do not rename `User.Hardware` column or drop it from the entity in this ticket; only stop reading/writing it. Renaming/dropping requires a separate migration ticket per the workspace's "no rename without confirmation" rule.
|
||||
|
||||
## Cross-architecture context
|
||||
|
||||
This ticket is the admin-side half of an architectural transition that has already happened:
|
||||
|
||||
- Loader retirement (Scenario X) — `suite/_docs/_repo-config.yaml` → `unresolved:loader-retirement-arch-doc`
|
||||
- Suite-root restructure (2026-04-19) — see assumptions_log entries in the same file
|
||||
- Admin-side hardware-binding cleanup — **this ticket** (AZ-197)
|
||||
|
||||
The matching suite-doc refresh (top-level architecture, Binary Split Security section) is tracked separately under the unresolved item above and is intentionally NOT in this ticket's scope.
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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`)
|
||||
|
||||
```csharp
|
||||
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.
|
||||
Reference in New Issue
Block a user