Add helper functions for OneRep API

This commit is contained in:
Vincent 2023-04-14 16:06:21 +02:00 коммит произвёл Vincent
Родитель 5ed89e9dba
Коммит 56300ad1e0
6 изменённых файлов: 304 добавлений и 0 удалений

Просмотреть файл

@ -32,6 +32,7 @@ const requiredEnvVars = [
'OAUTH_CLIENT_SECRET',
'OAUTH_PROFILE_URI',
'OAUTH_TOKEN_URI',
'ONEREP_API_KEY',
'REDIS_URL',
'SENTRY_DSN',
'SERVER_URL',

13
src/custom-types.d.ts поставляемый
Просмотреть файл

@ -46,3 +46,16 @@ declare namespace Express {
};
}
}
declare module 'mozlog' {
type LogFunction = (_op: string, _details?: object) => void
type Options = {
app: string;
level: string;
fmt: string;
};
const defaultFunction: (_options: Options) => (_scope: string) => ({ debug: LogFunction, info: LogFunction, warn: LogFunction, error: LogFunction })
export default defaultFunction
}

233
src/external/onerep.js поставляемый Normal file
Просмотреть файл

@ -0,0 +1,233 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import AppConstants from '../appConstants.js'
import mozlog from '../utils/log.js'
const log = mozlog('external.onerep')
/**
* @param {string} path
* @param {Parameters<typeof fetch>[1]} [options]
*/
async function onerepFetch (path, options = {}) {
const url = 'https://api.onerep.com' + path
const headers = new Headers(options.headers)
headers.set('Authorization', `Basic ${Buffer.from(`${AppConstants.ONEREP_API_KEY}:`).toString('base64')}`)
return fetch(url, { ...options, headers })
}
/**
* @typedef {object} OneRepProfile
* @property {string} first_name
* @property {string} last_name
* @property {string} city
* @property {import('../utils/states').StateAbbr} state
* @property {import('../utils/parse.js').ISO8601DateString} [birth_date]
* @property {import('../utils/parse.js').E164PhoneNumberString} [phone_number]
*/
/**
* @param {OneRepProfile} profileData
* @returns {Promise<number>} Profile ID
*/
export async function createProfile (profileData) {
/**
* See https://docs.onerep.com/#operation/createProfile
*
* @type {any}
*/
const requestBody = {
first_name: profileData.first_name,
last_name: profileData.last_name,
addresses: [
{
state: profileData.state,
city: profileData.city
}
]
}
if (profileData.birth_date) {
requestBody.birth_date = profileData.birth_date
}
if (profileData.phone_number) {
requestBody.phone_numbers = [
{ number: profileData.phone_number }
]
}
const response = await onerepFetch('/profiles', {
method: 'POST',
body: JSON.stringify(requestBody)
})
if (!response.ok) {
log.info(`Failed to create OneRep profile: [${response.status}] [${response.statusText}]`)
throw new Error(`Failed to create OneRep profile: [${response.status}] [${response.statusText}]`)
}
/**
* See https://docs.onerep.com/#operation/createProfile
*
* @type {{
* id: number,
* status: 'active' | 'inactive',
* created_at: import('../utils/parse.js').ISO8601DateString,
* updated_at: import('../utils/parse.js').ISO8601DateString,
* url: string,
* }}
*/
const savedProfile = await response.json()
return savedProfile.id
}
/**
* @param {number} profileId
* @returns {Promise<void>}
*/
export async function activateProfile (profileId) {
/**
* See https://docs.onerep.com/#operation/activateProfile
*
* @type {any}
*/
const response = await onerepFetch(`/profiles/${profileId}/activate`, {
method: 'PUT'
})
if (!response.ok) {
log.info(`Failed to activate OneRep profile: [${response.status}] [${response.statusText}]`)
throw new Error(`Failed to activate OneRep profile: [${response.status}] [${response.statusText}]`)
}
}
/**
* @param {number} profileId
* @returns {Promise<void>}
*/
export async function optoutProfile (profileId) {
/**
* See https://docs.onerep.com/#operation/optoutProfile
*/
const response = await onerepFetch(`/profiles/${profileId}/optout`, {
method: 'POST'
})
if (!response.ok) {
log.info(`Failed to opt-out OneRep profile: [${response.status}] [${response.statusText}]`)
throw new Error(`Failed to opt-out OneRep profile: [${response.status}] [${response.statusText}]`)
}
}
/**
* @typedef {object} CreateScanResponse
* @property {number} id
* @property {number} profile_id
* @property {'in_progress'} status
* @property {'manual'} reason
* @property {import('../utils/parse.js').ISO8601DateString} created_at
* @property {import('../utils/parse.js').ISO8601DateString} updated_at
*/
/**
* @param {number} profileId
* @returns {Promise<CreateScanResponse>}
*/
export async function createScan (profileId) {
/**
* See https://docs.onerep.com/#operation/createScan
*/
const response = await onerepFetch(`/profiles/${profileId}/scans`, {
method: 'POST'
})
if (!response.ok) {
log.info(`Failed to create a scan: [${response.status}] [${response.statusText}]`)
throw new Error(`Failed to create a scan: [${response.status}] [${response.statusText}]`)
}
return response.json()
}
/**
* @typedef {{ current_page: number; from: number; last_page: number; per_page: number; to: number; total: number; }} OneRepMeta
* @typedef {object} Scan
* @property {number} id
* @property {number} profile_id
* @property {'in_progress' | 'finished'} status
* @property {'initial' | 'monitoring' | 'manual'} reason
* @typedef {{ meta: OneRepMeta, data: Scan[] }} ListScansResponse
*/
/**
* @param {number} profileId
* @param {Partial<{ page: number; per_page: number }>} [options]
* @returns {Promise<ListScansResponse>}
*/
export async function listScans (profileId, options = {}) {
const queryParams = new URLSearchParams()
if (options.page) {
queryParams.set('page', options.page.toString())
}
if (options.per_page) {
queryParams.set('per_page', options.per_page.toString())
}
/**
* See https://docs.onerep.com/#operation/getScans
*
* @type {any}
*/
const response = await onerepFetch(`/profiles/${profileId}/scans?` + queryParams.toString(), {
method: 'GET'
})
if (!response.ok) {
log.info(`Failed to fetch scans: [${response.status}] [${response.statusText}]`)
throw new Error(`Failed to fetch scans: [${response.status}] [${response.statusText}]`)
}
return response.json()
}
/**
* @typedef {object} ScanResult
* @property {number} id
* @property {number} profile_id
* @property {string} first_name
* @property {string} last_name
* @property {string} middle_name
* @property {`${number}`} age
* @property {Array<{ city: string; state: string; street: string; zip: string; }>} addresses
* @property {string[]} phones
* @property {string[]} emails
* @property {string} data_broker
* @property {import('../utils/parse.js').ISO8601DateString} created_at
* @property {import('../utils/parse.js').ISO8601DateString} updated_at
* @typedef {{ meta: OneRepMeta, data: ScanResult[] }} ListScanResultsResponse
*/
/**
* @typedef {'new' | 'optout_in_progress' | 'waiting_for_verification' | 'removed'} RemovalStatus
* @param {number} profileId
* @param {Partial<{ page: number; per_page: number; status: RemovalStatus }>} [options]
* @returns {Promise<ListScanResultsResponse>}
*/
export async function listScanResults (profileId, options = {}) {
const queryParams = new URLSearchParams({ 'profile_id[]': profileId.toString() })
if (options.page) {
queryParams.set('page', options.page.toString())
}
if (options.per_page) {
queryParams.set('per_page', options.per_page.toString())
}
if (options.status) {
const statuses = Array.isArray(options.status) ? options.status : [options.status]
statuses.forEach(status => {
queryParams.append('status[]', status)
})
}
/**
* See https://docs.onerep.com/#operation/getScanResults
*
* @type {any}
*/
const response = await onerepFetch('/scan-results/?' + queryParams.toString(), {
method: 'GET'
})
if (!response.ok) {
log.info(`Failed to fetch scan results: [${response.status}] [${response.statusText}]`)
throw new Error(`Failed to fetch scan results: [${response.status}] [${response.statusText}]`)
}
return response.json()
}

45
src/utils/parse.js Normal file
Просмотреть файл

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* @typedef {string} ISO8601DateString See https://en.wikipedia.org/wiki/ISO_8601
* @typedef {`+${string}`} E164PhoneNumberString See https://en.wikipedia.org/wiki/E.164
*/
/**
* @param {string} phoneNumber
* @returns {E164PhoneNumberString | null}
*/
export function parseE164PhoneNumber (phoneNumber) {
if (typeof phoneNumber !== 'string' || phoneNumber.length > 16 || !phoneNumber.startsWith('+')) {
return null
}
const parsedNumber = /** @type {E164PhoneNumberString} */ ('+' + Number.parseInt(phoneNumber.substring(1), 10).toString())
if (parsedNumber !== phoneNumber) {
return null
}
return parsedNumber
}
/**
* @param {ISO8601DateString} datetime
* @returns {Date | null}
*/
export function parseIso8601Datetime (datetime) {
if (typeof datetime !== 'string') {
return null
}
// Important caveat to keep in mind:
// > Support for ISO 8601 formats differs in that date-only strings
// > (e.g. "1970-01-01") are treated as UTC, not local.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#parameters
try {
return new Date(datetime)
} catch (_e) {
return null
}
}

9
src/utils/states.js Normal file
Просмотреть файл

@ -0,0 +1,9 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* @typedef {typeof usStates[keyof typeof usStates]} StateAbbr
*/
export const usStates = /** @type {const} */ (['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY', 'AS', 'GU', 'MP', 'PR', 'VI', 'UM', 'MH', 'FM', 'PW'])

Просмотреть файл

@ -9,7 +9,10 @@
"src/views/partials/exposures-list.js",
"src/controllers/exposure-scan.js",
"src/controllers/exposures.js",
"src/external/onerep.js",
"src/utils/emailAddress.js",
"src/utils/states.js",
"src/utils/parse.js",
// Replace the above with the following when our entire codebase has type annotations:
// "src/**/*",
],