Consumes the a2loft cropper-token-auth change: the save now sends the
session's PocketBase token as a bearer credential and no phone/otp. The
backend authorizes with OpenFGA crop_image (domain:<code> -> domain:idid),
so an operator no longer needs super-user rights in the profile's domain —
which is what made saves fail with a blanket 'Invalid OTP specified.'.
- the save's domain comes from a2loft's own record of the picture
(dims/picture/{uploadId} -> domainCode), falling back to the queue label,
guarded by a path-safety pattern instead of the catalog check
- the 'core' -> 'uc' alias is deleted: 'core' is a2loft's own pseudo-domain
for a profile picture (special-cased there), so rewriting it changed the
attribution and the ID-card notification
- queue loads are enriched with the profile's domainCode; the panel shows
the profile's domain and, when it differs, the picture target
- the cropper's OTP field, state and parameters are gone; the fingerprint
wizard is untouched (its own OTP flow on its own endpoints)
- tests updated: bearer + no query auth, resolved domain wins, unsafe values
refused, no OTP field served (46 pass)
Requires the a2loft change deployed first.
52 lines
2.1 KiB
Markdown
52 lines
2.1 KiB
Markdown
# Design
|
|
|
|
## Context
|
|
|
|
See proposal.md. The a2loft `cropper-token-auth` change adds token mode to the
|
|
profile-picture endpoints: `Authorization: Bearer <pb token>`, identity from
|
|
the token's phone, authorized with OpenFGA `crop_image` on `domain:<code>` then
|
|
`domain:idid`. This change consumes it.
|
|
|
|
## Goals / Non-Goals
|
|
|
|
**Goals:** the cropper saves with the session token and no OTP; the save's
|
|
domain is a2loft's own record of the picture; the panel is truthful about the
|
|
profile's domain.
|
|
|
|
**Non-Goals:** the fingerprint wizard's OTP flow; the cropper's lookup (there
|
|
is none — pictures come from the queue); removing OTP support in a2loft.
|
|
|
|
## Decisions
|
|
|
|
1. **Token sent from the server**, never exposed to the browser beyond the
|
|
page's existing injected session: the proxy attaches
|
|
`Authorization: Bearer ${req.session.token}`.
|
|
2. **Domain from a2loft, not the client**: `dims/picture/{uploadId}` is the
|
|
record of the picture; its `domainCode` is the profile's identity domain and
|
|
its `domain` is the picture target (`core`). Sent as `domainCode`, displayed
|
|
as both. Falls back to the queue item's label when a2loft has no record
|
|
(custom ids), guarded by a path-safety pattern.
|
|
3. **The `core → uc` alias is deleted**: `core` is a2loft's own pseudo-domain
|
|
(special-cased in `getUserByPhoneAsync`/`updatePicture`), so rewriting it —
|
|
as the old bot did for its own calls — changed attribution and the
|
|
notification. Authorization no longer depends on the domain, so no alias is
|
|
needed.
|
|
4. **Queue loads are enriched** with `domainCode` for display (one extra
|
|
read-only a2loft call per picture).
|
|
|
|
## Risks / Trade-offs
|
|
|
|
- [a2loft record unavailable for an upload] → falls back to the queue label
|
|
under the path-safety check; the save still carries the token for authz.
|
|
- [Operators outside the domain's super-users] → irrelevant now: the OpenFGA
|
|
check with the `domain:idid` fallback authorizes them.
|
|
|
|
## Migration Plan
|
|
|
|
Deploy a2loft first (dual-mode, no client change), then this change; the
|
|
cropper saves without an OTP. Rollback = revert the proxy and re-add the field.
|
|
|
|
## Open Questions
|
|
|
|
None.
|