From 3e3b4d5f47ca7b11173c83fe17a17cdb505fad70 Mon Sep 17 00:00:00 2001 From: Oleksii Serdiukov Date: Fri, 30 Oct 2020 20:12:02 +0200 Subject: [PATCH] updated ZoomAPI class for v9 --- src/server/server.config.js | 10 +- src/server/verification/api/ZoomAPI.js | 222 ++++++++++++++----------- 2 files changed, 130 insertions(+), 102 deletions(-) diff --git a/src/server/server.config.js b/src/server/server.config.js index 78bd7699..53101d33 100644 --- a/src/server/server.config.js +++ b/src/server/server.config.js @@ -7,7 +7,9 @@ import dotenv from 'dotenv' import getNetworks from './networks' import ContractsAddress from '@gooddollar/goodcontracts/releases/deployment.json' -import { version } from '../../package.json' +import { version, description } from '../../package.json' + +export const appName = description.replace(/\s*server\s*/i, '') let dotenvPath = '.env' @@ -276,6 +278,12 @@ const conf = convict({ env: 'ZOOM_MINIMAL_MATCHLEVEL', default: 1 }, + zoomSearchIndexName: { + doc: 'FaceTec 3d DB search index name', + format: '*', + env: 'ZOOM_SEARCH_INDEX_NAME', + default: appName + }, zoomServerBaseUrl: { doc: 'FaceTec Managed Testing API URL', format: '*', diff --git a/src/server/verification/api/ZoomAPI.js b/src/server/verification/api/ZoomAPI.js index 2d750663..7b56760d 100644 --- a/src/server/verification/api/ZoomAPI.js +++ b/src/server/verification/api/ZoomAPI.js @@ -7,114 +7,138 @@ import { assign, merge, get, pick, omit, isPlainObject, isArray, mapValues, once import Config from '../../server.config' import logger from '../../../imports/logger' -const LIVENESS_PASSED = 0 +export const ZoomAPIError = { + FacemapNotFound: 'facemapNotFound', + LivenessCheckFailed: 'livenessCheckFailed' +} class ZoomAPI { http = null defaultMinimalMatchLevel = null + defaultSearchIndexName = null constructor(Config, httpFactory, logger) { - const { zoomMinimalMatchLevel } = Config + const { zoomMinimalMatchLevel, zoomSearchIndexName } = Config const httpClientOptions = this._configureClient(Config, logger) this.logger = logger this.http = httpFactory(httpClientOptions) this.defaultMinimalMatchLevel = Number(zoomMinimalMatchLevel) + this.defaultSearchIndexName = zoomSearchIndexName this._configureRequests() this._configureResponses() } async getSessionToken(customLogger = null) { - let [response, exception] = await this._sendRequest('get', '/session-token', { customLogger }) + const response = await this.http.get('/session-token', { customLogger }) + + if (!get(response, 'sessionToken')) { + const exception = new Error('No sessionToken in the FaceTec API response') - if (!exception && !get(response, 'sessionToken')) { - exception = new Error('No sessionToken in the FaceTec API response') assign(exception, { response }) + throw exception } - if (exception) { + return response + } + + async readEnrollment(enrollmentIdentifier, customLogger = null) { + let response + + try { + response = await this.http.get('/enrollment-3d/:enrollmentIdentifier', { + customLogger, + params: { enrollmentIdentifier } + }) + } catch (exception) { + const { message } = exception + + if (/no\s+entry\s+found/i.test(message)) { + exception.name = ZoomAPIError.FacemapNotFound + } + throw exception } return response } - async submitEnrollment(payload, customLogger = null) { - let [response, exception] = await this._sendRequest('post', '/enrollment', payload, { customLogger }) - const { message, isEnrolled } = response - const [isLivenessPassed, reasonOfFailure] = this._checkLivenessStatus(response) + async submitEnrollment(enrollmentIdentifier, faceSnapshot, customLogger = null) { + const payload = { + ...pick(faceSnapshot, 'sessionId', 'faceScan', 'auditTrailImage', 'lowQualityAuditTrailImage'), + externalDatabaseRefID: enrollmentIdentifier + } - if (exception || !isLivenessPassed || !isEnrolled) { - if (/enrollment\s+already\s+exists/i.test(message)) { - response.subCode = 'nameCollision' + const response = await this.http.post('post', '/enrollment-3d', payload, { customLogger }) + const isLivenessPassed = this.isLivenessCheckPassed(response) + const { success, faceScanSecurityChecks } = response + + const { + auditTrailVerificationCheckSucceeded, + replayCheckSucceeded, + sessionTokenCheckSucceeded + } = faceScanSecurityChecks + + if (!success) { + let message = 'FaceMap was not enrolled' + + if (!sessionTokenCheckSucceeded) { + message += ' because the session token is missing or was failed to be checked' + } else if (!replayCheckSucceeded) { + message += ' because the replay check was failed' + } else if (!isLivenessPassed) { + message = 'Liveness could not be determined' + + if (!auditTrailVerificationCheckSucceeded) { + message += ' because the photoshoots evaluated to be of poor quality' + } } - if (!exception) { - exception = new Error(reasonOfFailure) + const exception = new Error(message + '.') + + if (!isLivenessPassed) { + exception.name = ZoomAPIError.LivenessCheckFailed } - assign(exception, { response, message: reasonOfFailure }) + assign(exception, { response }) throw exception } return response } + // eslint-disable-line require-await + async indexEnrollment(enrollmentIdentifier, indexName = null, customLogger = null) { + return this._3dDbRequest('enroll', enrollmentIdentifier, indexName, null, customLogger) + } + // eslint-disable-next-line require-await - async readEnrollment(enrollmentIdentifier, customLogger = null) { - return this._faceMapRequest('get', enrollmentIdentifier, customLogger) + async readEnrollmentIndex(enrollmentIdentifier, indexName = null, customLogger = null) { + return this._3dDbIndexRequest('get', enrollmentIdentifier, indexName, customLogger) } // eslint-disable-next-line require-await - async disposeEnrollment(enrollmentIdentifier, customLogger = null) { - return this._faceMapRequest('delete', enrollmentIdentifier, customLogger) + async removeEnrollmentFromIndex(enrollmentIdentifier, indexName = null, customLogger = null) { + return this._3dDbIndexRequest('delete', enrollmentIdentifier, indexName, customLogger) } - async faceSearch(payload, minimalMatchLevel: number = null, customLogger = null) { + // eslint-disable-next-line require-await + async faceSearch(enrollmentIdentifier, minimalMatchLevel: number = null, indexName = null, customLogger = null) { let minMatchLevel = minimalMatchLevel - let [response, exception] = await this._sendRequest('post', '/search', payload, { customLogger }) - - if (exception) { - let livenessCheckFailed = false - - if ('livenessStatus' in response) { - const [isLivenessPassed, reasonOfFailure] = this._checkLivenessStatus(response) - - livenessCheckFailed = !isLivenessPassed - - if (livenessCheckFailed) { - exception.message = reasonOfFailure - } - } else { - livenessCheckFailed = /must\s+have.+?liveness\s+proven/i.test(response.message) - } - - if (livenessCheckFailed) { - response.subCode = 'livenessCheckFailed' - } - - throw exception - } if (null === minMatchLevel) { minMatchLevel = this.defaultMinimalMatchLevel } - if (minMatchLevel) { - const { results = [] } = response - minMatchLevel = Number(minMatchLevel) - - response.results = results.filter(({ matchLevel }) => Number(matchLevel) >= minMatchLevel) - } - - return response + return this._3dDbRequest('search', enrollmentIdentifier, indexName, { minMatchLevel }, customLogger) } isLivenessCheckPassed(response) { - const { livenessStatus } = response + const { faceScanSecurityChecks } = response || {} + const { faceScanLivenessCheckSucceeded } = faceScanSecurityChecks || {} - return LIVENESS_PASSED === livenessStatus + return true === faceScanLivenessCheckSucceeded } _configureClient(Config, logger) { @@ -187,11 +211,11 @@ class ZoomAPI { async _responseInterceptor(response) { const zoomResponse = this._transformResponse(response) - const { success, errorMessage } = zoomResponse + const { error, errorMessage } = zoomResponse this._logResponse('Received response from Zoom API:', response) - if (false === success) { + if (true === error) { const exception = new Error(errorMessage || 'FaceTec API response is empty') exception.response = zoomResponse @@ -255,78 +279,74 @@ class ZoomAPI { } } - _createLoggingSafeCopy(payload) { - if (isArray(payload)) { - return payload.map(item => this._createLoggingSafeCopy(item)) - } + _getDatabaseIndex(indexName = null) { + let databaseIndex = indexName - if (!isPlainObject(payload)) { - return payload + if (null === indexName) { + databaseIndex = this.defaultSearchIndexName } - return mapValues(omit(payload, 'faceMap', 'auditTrailImage', 'lowQualityAuditTrailImage'), payloadField => - this._createLoggingSafeCopy(payloadField) - ) + return databaseIndex } - async _sendRequest(method, endpoint, payloadOrOptions = null, options = null) { + async _3dDbRequest(operation, enrollmentIdentifier, indexName = null, additionalData = null, customLogger = null) { let response - let exception + const databaseIndex = this._getDatabaseIndex(indexName) + + const payload = { + externalDatabaseRefID: enrollmentIdentifier, + groupName: databaseIndex, + ...(additionalData || {}) + } try { - response = await this.http[method](...filter([endpoint, payloadOrOptions, options])) - } catch (apiException) { - exception = apiException - response = apiException.response + response = await this.http.post(`/3d-db/${operation}`, payload, { customLogger }) + } catch (exception) { + const { message } = exception - if (!response) { - throw apiException + if (/enrollment\s+does\s+not\s+exist/i.test(message)) { + exception.name = ZoomAPIError.FacemapNotFound } + + throw exception } - return [response, exception] + return response } - async _faceMapRequest(method, enrollmentIdentifier, customLogger = null) { - let [response, exception] = await this._sendRequest(method, '/enrollment/:enrollmentIdentifier', { - customLogger, - params: { enrollmentIdentifier } - }) + async _3dDbIndexRequest(method, enrollmentIdentifier, indexName = null, customLogger = null) { + const databaseIndex = this._getDatabaseIndex(indexName) - const { message } = response + const payload = { + identifier: enrollmentIdentifier, + groupName: databaseIndex + } - if (/no\s+entry\s+found/i.test(message)) { - if (!exception) { - exception = new Error(message) - exception.response = response - } + const response = await this.http.post(`/3d-db/${method}`, payload, { customLogger }) + const { success } = response - response.subCode = 'facemapNotFound' - } + if (false === success) { + const exception = new Error('An enrollment does not exist for this externalDatabaseRefID.') - if (exception) { + assign(exception, { response, name: ZoomAPIError.FacemapNotFound }) throw exception } return response } - _checkLivenessStatus(response) { - const { glasses, message, isLowQuality } = response - const isLivenessPassed = this.isLivenessCheckPassed(response) - - let errorMessage = null - - if (!isLivenessPassed) { - errorMessage = message + _createLoggingSafeCopy(payload) { + if (isArray(payload)) { + return payload.map(item => this._createLoggingSafeCopy(item)) + } - if (isLowQuality) { - errorMessage = 'Liveness could not be determined because ' - errorMessage += 'the photoshoots evaluated to be of poor quality.' - } + if (!isPlainObject(payload)) { + return payload } - return [isLivenessPassed, errorMessage] + return mapValues(omit(payload, 'faceMapBase64', 'auditTrailBase64'), payloadField => + this._createLoggingSafeCopy(payloadField) + ) } }