Skip to content

Instantly share code, notes, and snippets.

@andiradulescu
Last active June 16, 2023 15:23
Show Gist options
  • Save andiradulescu/ea738c5e5536328dbdd531562b3d6950 to your computer and use it in GitHub Desktop.
Save andiradulescu/ea738c5e5536328dbdd531562b3d6950 to your computer and use it in GitHub Desktop.
/**
* 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