feat(fpagent): authenticate the wizard's a2loft calls with the session token

The fingerprint wizard's staff-lookup and fingerprint-save proxies forwarded the page's OTP with the session phone. a2loft resolves that phone against the domain being addressed, so an operator who holds only capture_biometric was rejected unless they were a domain super-user. The cropper already saves with the session token.

- tools/server.js: both a2loft proxies send Authorization: Bearer <session token>, no phone/otp; validateProxy drops the OTP and phone checks.
- tools/public/assets/fp: both OTP fields, otpLookup/otpSave state, request params and resets removed; canLookup/canSave no longer gate on OTP.
- Tests assert the Bearer header, no query auth, no OTP field in the served page, and both gates without a credential (66/66 pass).
- README and Help page updated.

Requires the a2loft fingerprint-token-auth change deployed first. OpenSpec change: fingerprint-token-auth.
This commit is contained in:
2026-10-02 20:41:43 +01:00
parent 548e33390c
commit 14f6fd6a8e
13 changed files with 262 additions and 67 deletions
+4 -11
View File
@@ -25,8 +25,6 @@ Vue.config.ignoredElements = [/^cds-/];
agent: { connected: false, version: '', captureAgentId: '', available: null, device: '' },
domain: '',
reference: '',
otpLookup: '',
otpSave: '',
staff: null,
busy: { lookup: false, save: false },
refreshing: false,
@@ -49,10 +47,10 @@ Vue.config.ignoredElements = [/^cds-/];
this.fingers['right-index-finger'].status === 'done';
},
canLookup: function () {
return !!(this.domain && this.reference && this.otpLookup) && !this.busy.lookup;
return !!(this.domain && this.reference) && !this.busy.lookup;
},
canSave: function () {
return this.bothCaptured && !!this.otpSave && !this.busy.save;
return this.bothCaptured && !this.busy.save;
},
refreshUnlocksAt: function () {
var ttl = (this.session && this.session.domainCacheTtlMs) || 5 * 60 * 1000;
@@ -296,9 +294,7 @@ Vue.config.ignoredElements = [/^cds-/];
if (!this.canLookup) return;
this.busy.lookup = true;
axios.post(
'/api/' + encodeURIComponent(this.domain) + '/staff/' + encodeURIComponent(this.reference),
null,
{ params: { otp: this.otpLookup } }
'/api/' + encodeURIComponent(this.domain) + '/staff/' + encodeURIComponent(this.reference)
).then(function (r) {
if (!r.data.ok) {
self.showNotif('error', 'Not found', 'Operation failed. ' + (r.data.message || '') + ' (reference ' + self.reference + ')');
@@ -322,8 +318,7 @@ Vue.config.ignoredElements = [/^cds-/];
});
axios.post(
'/api/' + encodeURIComponent(this.domain) + '/staff/' + encodeURIComponent(this.reference) + '/fingerprints',
{ fingerprints: fingerprints },
{ params: { otp: this.otpSave } }
{ fingerprints: fingerprints }
).then(function (r) {
if (r.data.ok) {
self.showNotif('success', 'Enrollment saved',
@@ -343,8 +338,6 @@ Vue.config.ignoredElements = [/^cds-/];
newEnrollment: function () {
this.staff = null;
this.reference = '';
this.otpLookup = '';
this.otpSave = '';
this.fingers = { 'right-thumb': newFinger(), 'right-index-finger': newFinger() };
this.capturing = null;
this.cancelling = false;