Last active
June 16, 2023 15:23
-
-
Save andiradulescu/ea738c5e5536328dbdd531562b3d6950 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* AliceOnboarding.service.ts | |
* | |
* This file was created based on the Alice Biometrics documentation found at: | |
* https://docs.alicebiometrics.com/onboarding/ | |
* and the full API Reference from: | |
* https://apis.alicebiometrics.com/onboarding/ui/#/ | |
* and the Python SDK code from: | |
* https://github.com/alice-biometrics/onboarding-python | |
* | |
* Further guidance and improvements were made using OpenAI's ChatGPT (GPT-4): | |
* https://chat.openai.com/?model=gpt-4 | |
* | |
* The code in this file is provided under the MIT License. It grants permissions | |
* for you to use, copy, modify, merge, publish, distribute, sublicense, | |
* and/or sell copies of the software. For the full license, see: | |
* https://opensource.org/licenses/MIT | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
import axios, { AxiosInstance } from 'axios'; | |
import jwt from 'jsonwebtoken'; | |
import FormData from 'form-data'; | |
const ALICE_BASE_URL = 'https://apis.alicebiometrics.com/onboarding'; | |
const ALICE_API_KEY = 'Replace with API key'; // from https://dashboard.alicebiometrics.com/#/credentials | |
interface UserInfo { | |
first_name?: string; | |
last_name?: string; | |
email?: string; | |
} | |
interface DeviceInfo { | |
device_platform?: string; | |
device_platform_version?: string; | |
device_model?: string; | |
} | |
export enum AliceDocumentType { | |
ID_CARD = 'idcard', | |
DRIVER_LICENSE = 'driverlicense', | |
PASSPORT = 'passport', | |
RESIDENCE_PERMIT = 'residencepermit', | |
HEALTH_INSURANCE_CARD = 'healthinsurancecard' | |
} | |
export enum AliceDocumentSide { | |
Front = 'front', | |
Back = 'back', | |
} | |
enum DocumentSource { | |
Camera = 'camera', | |
File = 'file', | |
} | |
interface BoundingBox { | |
x: number; | |
y: number; | |
width: number; | |
height: number; | |
} | |
enum Version { | |
V0 = '0', | |
V1 = '1', | |
DEFAULT = 'default', | |
} | |
enum AliceResponseStatus { | |
// There is already an active and completed document of the same type when trying to unvoid a document | |
CompletedDocumentAlreadyExists = 409, | |
// The maximum number of active documents (5) has been reached and it's not possible to unvoid the document | |
TooManyActiveDocuments = 422, | |
} | |
export default class AliceOnboardingService { | |
private aliceClient: AxiosInstance; | |
private loginToken: string | null = null; | |
private backendToken: string | null = null; | |
private userTokenCache: Record<string, string> = {}; | |
private userBackendTokenCache: Record<string, string> = {}; | |
constructor() { | |
this.aliceClient = axios.create({ | |
baseURL: ALICE_BASE_URL, | |
}); | |
} | |
async createUser(userInfo: UserInfo = {}, deviceInfo: DeviceInfo = {}): Promise<string> { | |
const backendToken = await this.getBackendToken(); | |
const formData = new FormData(); | |
Object.entries({ ...userInfo, ...deviceInfo }).forEach(([key, value]) => { | |
formData.append(key, value); | |
}); | |
const response = await this.aliceClient.post('/user', formData, { | |
headers: { | |
...formData.getHeaders(), | |
Authorization: `Bearer ${backendToken}`, | |
}, | |
}); | |
return response.data.user_id; | |
} | |
async createDocument(userId: string, type: AliceDocumentType, issuingCountry: string): Promise<string> { | |
const userToken = await this.getUserToken(userId); | |
const formData = new FormData(); | |
formData.append('type', type); | |
formData.append('issuing_country', issuingCountry); | |
const response = await this.aliceClient.post('/user/document', formData, { | |
headers: { | |
...formData.getHeaders(), | |
Authorization: `Bearer ${userToken}`, | |
}, | |
}); | |
return response.data.document_id; | |
} | |
async addDocument( | |
userId: string, | |
documentId: string, | |
mediaData: Buffer, | |
side: AliceDocumentSide, | |
manual = true, | |
source: DocumentSource = DocumentSource.Camera, | |
boundingBox?: BoundingBox, | |
fields?: Record<string, any>, | |
): Promise<any> { | |
const userToken = await this.getUserToken(userId); | |
const formData = new FormData(); | |
formData.append('document_id', documentId); | |
formData.append('side', side); | |
formData.append('manual', manual.toString()); | |
formData.append('source', source); | |
formData.append('image', mediaData, { filename: 'image', contentType: 'image/jpeg' }); | |
if (fields) { | |
formData.append('fields', JSON.stringify(fields)); | |
} | |
if (boundingBox) { | |
formData.append('bounding_box', JSON.stringify(boundingBox)); | |
} | |
const response = await this.aliceClient.put('/user/document', formData, { | |
headers: { | |
...formData.getHeaders(), | |
Authorization: `Bearer ${userToken}`, | |
}, | |
}); | |
return response.data.message; | |
} | |
async addSelfie(userId: string, mediaData: Buffer): Promise<any> { | |
const userToken = await this.getUserToken(userId); | |
const formData = new FormData(); | |
formData.append('video', mediaData, { filename: 'video', contentType: 'video/mov' }); | |
const response = await this.aliceClient.post('/user/selfie', formData, { | |
headers: { | |
...formData.getHeaders(), | |
Authorization: `Bearer ${userToken}`, | |
}, | |
}); | |
return response.data.message; | |
} | |
async voidDocument(userId: string, documentId: string) { | |
const backendToken = await this.getUserBackendToken(userId); | |
const response = await this.aliceClient.patch(`/user/document/${documentId}`, null, { | |
headers: { | |
Authorization: `Bearer ${backendToken}`, | |
}, | |
}); | |
return response.data; | |
} | |
async deleteDocument(userId: string, documentId: string) { | |
const backendToken = await this.getUserBackendToken(userId); | |
const response = await this.aliceClient.delete(`/user/document/${documentId}`, { | |
headers: { | |
Authorization: `Bearer ${backendToken}`, | |
}, | |
}); | |
return response.data; | |
} | |
async getSupportedDocuments(userId?: string): Promise<any> { | |
let token; | |
if (userId) { | |
token = await this.getUserToken(userId); | |
} else { | |
token = await this.getBackendToken(); | |
} | |
const response = await this.aliceClient.get('/documents/supported', { | |
headers: { | |
Authorization: `Bearer ${token}`, | |
}, | |
}); | |
return response.data; | |
} | |
async getReport(userId: string, version: Version = Version.DEFAULT): Promise<any> { | |
const backendToken = await this.getUserBackendToken(userId); | |
const response = await this.aliceClient.get('/user/report', { | |
headers: { | |
Authorization: `Bearer ${backendToken}`, | |
'Alice-Report-Version': version, | |
}, | |
}); | |
return response.data.report; | |
} | |
async deleteUser(userId: string) { | |
const backendToken = await this.getUserBackendToken(userId); | |
const response = await this.aliceClient.delete(`/user`, { | |
headers: { | |
Authorization: `Bearer ${backendToken}`, | |
}, | |
}); | |
return response.data; | |
} | |
private async getUserToken(userId: string): Promise<string> { | |
if (this.userTokenCache[userId] && !this.isTokenExpired(this.userTokenCache[userId])) { | |
return this.userTokenCache[userId]; | |
} | |
// Get a new user token if it does not exist or is expired | |
const newToken = await this.createUserToken(userId); | |
this.userTokenCache[userId] = newToken; | |
return newToken; | |
} | |
private async createUserToken(userId: string): Promise<string> { | |
const loginToken = await this.getLoginToken(); | |
const response = await this.aliceClient.get(`/user_token/${userId}`, { | |
headers: { | |
Authorization: `Bearer ${loginToken}`, | |
}, | |
}); | |
return response.data.token; | |
} | |
private async getUserBackendToken(userId: string): Promise<string> { | |
if (this.userBackendTokenCache[userId] && !this.isTokenExpired(this.userBackendTokenCache[userId])) { | |
return this.userBackendTokenCache[userId]; | |
} | |
// Get a new user backend token if it does not exist or is expired | |
const newToken = await this.createUserBackendToken(userId); | |
this.userBackendTokenCache[userId] = newToken; | |
return newToken; | |
} | |
private async createUserBackendToken(userId: string): Promise<string> { | |
const loginToken = await this.getLoginToken(); | |
const response = await this.aliceClient.get(`/backend_token/${userId}`, { | |
headers: { | |
Authorization: `Bearer ${loginToken}`, | |
}, | |
}); | |
return response.data.token; | |
} | |
private async getLoginToken(): Promise<string> { | |
if (!this.loginToken || this.isTokenExpired(this.loginToken)) { | |
const response = await this.aliceClient.get('/login_token', { | |
headers: { | |
apikey: ALICE_API_KEY, | |
}, | |
}); | |
this.loginToken = response.data.token; | |
} | |
return this.loginToken; | |
} | |
private async getBackendToken(): Promise<string> { | |
if (!this.backendToken || this.isTokenExpired(this.backendToken)) { | |
const loginToken = await this.getLoginToken(); | |
const response = await this.aliceClient.get('/backend_token', { | |
headers: { | |
Authorization: `Bearer ${loginToken}`, | |
}, | |
}); | |
this.backendToken = response.data.token; | |
} | |
return this.backendToken; | |
} | |
private isTokenExpired(token: string): boolean { | |
const decodedToken: any = jwt.decode(token, { complete: true }); | |
const exp = decodedToken?.payload?.exp; | |
if (!exp) { | |
return true; | |
} | |
const currentTimestamp = Math.floor(Date.now() / 1000); | |
return currentTimestamp >= exp; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment