feat(cropper): save with the session token — no OTP, domain from a2loft

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.
This commit is contained in:
2026-10-01 12:37:25 +01:00
parent 88d71ffec0
commit 796094f61b
9 changed files with 229 additions and 67 deletions
+3 -4
View File
@@ -23,7 +23,6 @@ Vue.config.ignoredElements = [/^cds-/];
accepted: false,
cleanedImage: null,
requiresCleaning: '',
otp: '',
reviewOptions: [
'IMAGE: Not in the midline of the photo frame (aligned to the left or right)',
'IMAGE: Taken too close-up (Only head is visible, shoulders barely visible)',
@@ -59,7 +58,8 @@ Vue.config.ignoredElements = [/^cds-/];
return this.selectedReviewOptions.length > 0 || this.otherReviewOption.length > 0;
},
canSave: function () {
return !this.busy.save && this.otp.length > 0 && this.requiresCleaning !== '';
// the session token authenticates the save; no OTP is involved
return !this.busy.save && this.requiresCleaning !== '';
},
otherOptionSelected: function () {
return this.selectedReviewOptions.indexOf('Others not specified.') >= 0;
@@ -124,7 +124,6 @@ Vue.config.ignoredElements = [/^cds-/];
this.selectedReviewOptions = [];
this.otherReviewOption = '';
this.requiresCleaning = '';
this.otp = '';
},
loadPicture: async function (mode) {
@@ -268,7 +267,7 @@ Vue.config.ignoredElements = [/^cds-/];
this.busy.save = true;
try {
var { data } = await axios.post(
'/cropper/profile/picture?otp=' + encodeURIComponent(this.otp),
'/cropper/profile/picture',
{
pictureDataUrl: this.cleanedImage || this.croppaInstance.generateDataUrl('image/jpeg'),
phone: this.message.message.caption,