The cropper upload POSTs with the session token and no OTP query auth, and the panel shows the profile's domain and picture target. Field pass was verified earlier (upload 0b90075b, saved with no OTP). Merged the image-cropper delta into the main spec and archived the change.
105 lines
4.7 KiB
Markdown
105 lines
4.7 KiB
Markdown
# image-cropper Specification
|
|
|
|
## Purpose
|
|
|
|
The Image Cropper at `/cropper`: operators review, crop, clean and save
|
|
identity photos submitted for YOUreka ID domain profiles, with every proxied
|
|
action authorized by the crop_image relation.
|
|
|
|
## Requirements
|
|
|
|
### Requirement: Crop permission gate
|
|
Access to the cropper SHALL require the `crop_image` relation on `domain:idid`
|
|
(subsystem `idid`) for the signed-in operator's phone, verified against the
|
|
portal OpenFGA endpoint and failing closed on denial or error. Read paths
|
|
(page load, picture loading, background cleanup, dequeue) MAY serve a cached
|
|
result for up to 5 minutes; saving SHALL always verify live.
|
|
|
|
#### Scenario: Permitted operator opens the cropper
|
|
- **WHEN** an operator with the crop_image relation requests `/cropper`
|
|
- **THEN** the editor page loads
|
|
|
|
#### Scenario: Non-permitted operator is kept out
|
|
- **WHEN** an operator without the crop_image relation requests the cropper
|
|
page
|
|
- **THEN** the page renders a not-permitted explanation with no editor UI
|
|
|
|
#### Scenario: Cached gate on reads
|
|
- **WHEN** a read action occurs within 5 minutes of the operator's last
|
|
permission check
|
|
- **THEN** the cached result is used without calling the portal
|
|
|
|
### Requirement: Picture loading is gated and namespaced
|
|
The cropper SHALL load review pictures only through its namespaced routes
|
|
(`/cropper/picture/next`, `/cropper/picture/load/:id`,
|
|
`/cropper/picture/:file_name`) behind the session and crop gates. A denied
|
|
route SHALL return 403 without contacting the queue, storage, or review
|
|
services.
|
|
|
|
#### Scenario: Next queued picture
|
|
- **WHEN** the operator requests the next picture
|
|
- **THEN** the route proxies the Azure queue and returns the queue message
|
|
|
|
#### Scenario: Denied picture load does not reach services
|
|
- **WHEN** a request without the crop_image relation loads a picture route
|
|
- **THEN** the route answers 403 and makes no upstream calls
|
|
|
|
### Requirement: Background cleanup is gated
|
|
The background-cleanup proxy (`/cropper/api/removebg`) SHALL require the
|
|
session and crop gate, forwarding the cropped image to the cleanup service
|
|
only for permitted operators.
|
|
|
|
#### Scenario: Denied cleanup does not reach the cleanup service
|
|
- **WHEN** a request without the crop_image relation calls the cleanup proxy
|
|
- **THEN** it answers 403 without contacting the cleanup service
|
|
|
|
#### Scenario: Cleanup proxies the cleaned image
|
|
- **WHEN** a permitted operator submits a cropped image for cleanup
|
|
- **THEN** the route forwards it to the cleanup service and returns the
|
|
cleaned image
|
|
|
|
### Requirement: Saving requires a live gate and an OTP
|
|
Saving a cropped picture (`/cropper/profile/picture`) SHALL verify the crop
|
|
permission live (bypassing the cache) and SHALL authenticate the operator with
|
|
the session's PocketBase token, sent to a2loft as a bearer credential; no OTP
|
|
SHALL be collected or forwarded. The picture's domain SHALL come from a2loft's
|
|
own record for the upload (`dims/picture/{uploadId}` → `domainCode`), falling
|
|
back to the queue item's label, and SHALL match a path-safe pattern before it
|
|
is used in the request. A denied save SHALL not reach a2loft.
|
|
|
|
#### Scenario: Successful save
|
|
- **WHEN** a permitted operator saves
|
|
- **THEN** the route verifies the live gate and forwards the picture to
|
|
a2loft with the session token as a bearer credential and the profile's
|
|
resolved domain
|
|
|
|
#### Scenario: Denied save does not reach a2loft
|
|
- **WHEN** the live crop check denies, or the request carries no message body
|
|
- **THEN** the route answers 403/400 and makes no a2loft call
|
|
|
|
#### Scenario: Unsafe domain values are refused
|
|
- **WHEN** neither a2loft's record nor the queue label yields a path-safe
|
|
domain
|
|
- **THEN** the save is refused with 400 and a2loft is not contacted
|
|
|
|
### Requirement: Unified login — no separate artist login
|
|
The cropper SHALL NOT implement its own login. The operator's identity comes
|
|
from the YOUreka session and is presented to a2loft as the session's PocketBase
|
|
token; the picture's own metadata carries the domain. The cropper collects no
|
|
other credential — there is no save-time OTP.
|
|
|
|
#### Scenario: Phone prefilled from the session
|
|
- **WHEN** a signed-in operator opens the cropper
|
|
- **THEN** their phone is already known to the page without any login form
|
|
|
|
### Requirement: Cropper credentials come from the environment
|
|
The cropper's service credentials (Minio keys, Azure storage connection
|
|
string, nodered credentials, cleanup-service key) SHALL be read from
|
|
environment variables. When they are absent, the cropper routes SHALL answer
|
|
503 without attempting upstream calls; the application itself SHALL still
|
|
boot.
|
|
|
|
#### Scenario: Unconfigured deployment degrades gracefully
|
|
- **WHEN** the app starts without the cropper credentials
|
|
- **THEN** the other tools work and cropper routes answer 503
|