From c77fda262355652e9c18aa2733e0645d42ad2ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Thu, 7 Jul 2022 19:28:33 +0100 Subject: [PATCH 1/4] feat: add button to check against OFAC sanction list --- lib/customers.js | 1 + lib/new-admin/admin-server.js | 3 +- lib/new-admin/graphql/resolvers/index.js | 2 ++ .../graphql/resolvers/sanctions.resolver.js | 14 ++++++++++ lib/new-admin/graphql/types/index.js | 2 ++ lib/new-admin/graphql/types/sanctions.type.js | 13 +++++++++ lib/new-admin/middlewares/index.js | 2 ++ .../middlewares/loadSanctionLists.js | 28 +++++++++++++++++++ .../src/pages/Customers/CustomerData.js | 15 ++++++++-- .../src/pages/Customers/CustomerProfile.js | 26 ++++++++++++++++- .../Customers/components/EditableCard.js | 13 ++++++++- 11 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 lib/new-admin/graphql/resolvers/sanctions.resolver.js create mode 100644 lib/new-admin/graphql/types/sanctions.type.js create mode 100644 lib/new-admin/middlewares/loadSanctionLists.js diff --git a/lib/customers.js b/lib/customers.js index d7975972..23542928 100644 --- a/lib/customers.js +++ b/lib/customers.js @@ -114,6 +114,7 @@ function update (id, data, userToken) { async function updateCustomer (id, data, userToken) { const formattedData = _.pick( [ + 'sanctions', 'authorized_override', 'id_card_photo_override', 'id_card_data_override', diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index 3b13daf8..e2a7ab70 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -20,7 +20,7 @@ const { typeDefs, resolvers } = require('./graphql/schema') const findOperatorId = require('../middlewares/operatorId') const computeSchema = require('../compute-schema') const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants') -const { session, cleanUserSessions, buildApolloContext } = require('./middlewares') +const { session, cleanUserSessions, buildApolloContext, loadSanctionLists } = require('./middlewares') const devMode = require('minimist')(process.argv.slice(2)).dev @@ -48,6 +48,7 @@ app.use(express.static(path.resolve(__dirname, '..', '..', 'public'))) app.use(cleanUserSessions(USER_SESSIONS_CLEAR_INTERVAL)) app.use(computeSchema) app.use(findOperatorId) +app.use(loadSanctionLists) app.use(session) app.use(graphqlUploadExpress()) diff --git a/lib/new-admin/graphql/resolvers/index.js b/lib/new-admin/graphql/resolvers/index.js index 81e79614..a20d9216 100644 --- a/lib/new-admin/graphql/resolvers/index.js +++ b/lib/new-admin/graphql/resolvers/index.js @@ -14,6 +14,7 @@ const machine = require('./machine.resolver') const notification = require('./notification.resolver') const pairing = require('./pairing.resolver') const rates = require('./rates.resolver') +const sanctions = require('./sanctions.resolver') const scalar = require('./scalar.resolver') const settings = require('./settings.resolver') const sms = require('./sms.resolver') @@ -37,6 +38,7 @@ const resolvers = [ notification, pairing, rates, + sanctions, scalar, settings, sms, diff --git a/lib/new-admin/graphql/resolvers/sanctions.resolver.js b/lib/new-admin/graphql/resolvers/sanctions.resolver.js new file mode 100644 index 00000000..9bd211dd --- /dev/null +++ b/lib/new-admin/graphql/resolvers/sanctions.resolver.js @@ -0,0 +1,14 @@ +const _ = require('lodash/fp') +const ofac = require('../../../ofac') + +const resolvers = { + Query: { + checkAgainstSanctions: (...[, { firstName, lastName, birthdate }]) => { + const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false }) + + return { ofacSanctioned: _.size(ofacMatches) > 0 } + } + } +} + +module.exports = resolvers diff --git a/lib/new-admin/graphql/types/index.js b/lib/new-admin/graphql/types/index.js index a1886a28..f4794b67 100644 --- a/lib/new-admin/graphql/types/index.js +++ b/lib/new-admin/graphql/types/index.js @@ -14,6 +14,7 @@ const machine = require('./machine.type') const notification = require('./notification.type') const pairing = require('./pairing.type') const rates = require('./rates.type') +const sanctions = require('./sanctions.type') const scalar = require('./scalar.type') const settings = require('./settings.type') const sms = require('./sms.type') @@ -37,6 +38,7 @@ const types = [ notification, pairing, rates, + sanctions, scalar, settings, sms, diff --git a/lib/new-admin/graphql/types/sanctions.type.js b/lib/new-admin/graphql/types/sanctions.type.js new file mode 100644 index 00000000..76a5e572 --- /dev/null +++ b/lib/new-admin/graphql/types/sanctions.type.js @@ -0,0 +1,13 @@ +const { gql } = require('apollo-server-express') + +const typeDef = gql` + type SanctionMatches { + ofacSanctioned: Boolean + } + + type Query { + checkAgainstSanctions(firstName: String, lastName: String, birthdate: String): SanctionMatches @auth + } +` + +module.exports = typeDef diff --git a/lib/new-admin/middlewares/index.js b/lib/new-admin/middlewares/index.js index cedba31c..21cd4751 100644 --- a/lib/new-admin/middlewares/index.js +++ b/lib/new-admin/middlewares/index.js @@ -1,9 +1,11 @@ const cleanUserSessions = require('./cleanUserSessions') const buildApolloContext = require('./context') +const loadSanctionLists = require('./loadSanctionLists') const session = require('./session') module.exports = { cleanUserSessions, buildApolloContext, + loadSanctionLists, session } diff --git a/lib/new-admin/middlewares/loadSanctionLists.js b/lib/new-admin/middlewares/loadSanctionLists.js new file mode 100644 index 00000000..48d2c0b8 --- /dev/null +++ b/lib/new-admin/middlewares/loadSanctionLists.js @@ -0,0 +1,28 @@ +const logger = require('../../logger') +const sanctions = require('../../ofac') + +const sanctionStatus = { + loaded: false, + timestamp: null +} + +const loadSanctionLists = (req, res, next) => { + if (!sanctionStatus.loaded) { + logger.info('No sanction lists loaded. Loading sanctions...') + return sanctions.load() + .then(() => { + logger.info('OFAC sanction list loaded!') + sanctionStatus.loaded = true + sanctionStatus.timestamp = Date.now() + return next() + }) + .catch(e => { + logger.error('Couldn\'t load OFAC sanction list!') + return next(e) + }) + } + + return next() +} + +module.exports = loadSanctionLists diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js index b605eb07..affe5159 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js @@ -73,7 +73,8 @@ const CustomerData = ({ authorizeCustomRequest, updateCustomEntry, retrieveAdditionalDataDialog, - setRetrieve + setRetrieve, + checkAgainstSanctions }) => { const classes = useStyles() const [listView, setListView] = useState(false) @@ -172,6 +173,14 @@ const CustomerData = ({ idCardData: R.merge(idData, formatDates(values)) }), validationSchema: customerDataSchemas.idCardData, + checkAgainstSanctions: () => + checkAgainstSanctions({ + variables: { + firstName: initialValues.idCardData.firstName, + lastName: initialValues.idCardData.lastName, + birthdate: R.replace(/-/g, '')(initialValues.idCardData.dateOfBirth) + } + }), initialValues: initialValues.idCardData, isAvailable: !R.isNil(idData), editable: true @@ -434,7 +443,8 @@ const CustomerData = ({ initialValues, hasImage, hasAdditionalData, - editable + editable, + checkAgainstSanctions }, idx ) => { @@ -455,6 +465,7 @@ const CustomerData = ({ save={save} deleteEditedData={deleteEditedData} retrieveAdditionalData={retrieveAdditionalData} + checkAgainstSanctions={checkAgainstSanctions} editable={editable}> ) } diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index d9256f3b..3c03cb4f 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -1,4 +1,4 @@ -import { useQuery, useMutation } from '@apollo/react-hooks' +import { useQuery, useMutation, useLazyQuery } from '@apollo/react-hooks' import { makeStyles, Breadcrumbs, @@ -292,6 +292,22 @@ const GET_ACTIVE_CUSTOM_REQUESTS = gql` } ` +const CHECK_AGAINST_SANCTIONS = gql` + query checkAgainstSanctions( + $firstName: String + $lastName: String + $birthdate: String + ) { + checkAgainstSanctions( + firstName: $firstName + lastName: $lastName + birthdate: $birthdate + ) { + ofacSanctioned + } + } +` + const CustomerProfile = memo(() => { const history = useHistory() @@ -400,6 +416,13 @@ const CustomerProfile = memo(() => { onCompleted: () => getCustomer() }) + const [checkAgainstSanctions] = useLazyQuery(CHECK_AGAINST_SANCTIONS, { + onCompleted: ({ checkAgainstSanctions: { ofacSanctioned } }) => + updateCustomer({ + sanctions: !ofacSanctioned + }) + }) + const updateCustomer = it => setCustomer({ variables: { @@ -662,6 +685,7 @@ const CustomerProfile = memo(() => { authorizeCustomRequest={authorizeCustomRequest} updateCustomEntry={updateCustomEntry} setRetrieve={setRetrieve} + checkAgainstSanctions={checkAgainstSanctions} retrieveAdditionalDataDialog={ { diff --git a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js index 1dc71165..ce5c85bd 100644 --- a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js +++ b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js @@ -145,7 +145,8 @@ const EditableCard = ({ deleteEditedData, retrieveAdditionalData, hasAdditionalData = true, - editable + editable, + checkAgainstSanctions }) => { const classes = useStyles() @@ -273,6 +274,16 @@ const EditableCard = ({ Retrieve API data )} + {checkAgainstSanctions && ( + checkAgainstSanctions()}> + Check against OFAC sanction list + + )} {editable && ( Date: Fri, 8 Jul 2022 18:17:30 +0100 Subject: [PATCH 2/4] fix: remove sanction loader middleware --- lib/new-admin/admin-server.js | 3 +- .../graphql/resolvers/sanctions.resolver.js | 34 ++++++++++++++++--- lib/new-admin/middlewares/index.js | 2 -- .../middlewares/loadSanctionLists.js | 28 --------------- 4 files changed, 30 insertions(+), 37 deletions(-) delete mode 100644 lib/new-admin/middlewares/loadSanctionLists.js diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index e2a7ab70..3b13daf8 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -20,7 +20,7 @@ const { typeDefs, resolvers } = require('./graphql/schema') const findOperatorId = require('../middlewares/operatorId') const computeSchema = require('../compute-schema') const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants') -const { session, cleanUserSessions, buildApolloContext, loadSanctionLists } = require('./middlewares') +const { session, cleanUserSessions, buildApolloContext } = require('./middlewares') const devMode = require('minimist')(process.argv.slice(2)).dev @@ -48,7 +48,6 @@ app.use(express.static(path.resolve(__dirname, '..', '..', 'public'))) app.use(cleanUserSessions(USER_SESSIONS_CLEAR_INTERVAL)) app.use(computeSchema) app.use(findOperatorId) -app.use(loadSanctionLists) app.use(session) app.use(graphqlUploadExpress()) diff --git a/lib/new-admin/graphql/resolvers/sanctions.resolver.js b/lib/new-admin/graphql/resolvers/sanctions.resolver.js index 9bd211dd..3945516a 100644 --- a/lib/new-admin/graphql/resolvers/sanctions.resolver.js +++ b/lib/new-admin/graphql/resolvers/sanctions.resolver.js @@ -1,13 +1,37 @@ const _ = require('lodash/fp') +const logger = require('../../../logger') const ofac = require('../../../ofac') +const T = require('../../../time') + +const sanctionStatus = { + loaded: false, + timestamp: null +} + +const loadOrUpdateSanctions = () => { + if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.minute)) { + logger.info('No sanction lists loaded. Loading sanctions...') + return ofac.load() + .then(() => { + logger.info('OFAC sanction list loaded!') + sanctionStatus.loaded = true + sanctionStatus.timestamp = Date.now() + }) + .catch(e => { + logger.error('Couldn\'t load OFAC sanction list!') + }) + } + + return Promise.resolve() +} const resolvers = { Query: { - checkAgainstSanctions: (...[, { firstName, lastName, birthdate }]) => { - const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false }) - - return { ofacSanctioned: _.size(ofacMatches) > 0 } - } + checkAgainstSanctions: (...[, { firstName, lastName, birthdate }]) => loadOrUpdateSanctions() + .then(() => { + const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false }) + return { ofacSanctioned: _.size(ofacMatches) > 0 } + }) } } diff --git a/lib/new-admin/middlewares/index.js b/lib/new-admin/middlewares/index.js index 21cd4751..cedba31c 100644 --- a/lib/new-admin/middlewares/index.js +++ b/lib/new-admin/middlewares/index.js @@ -1,11 +1,9 @@ const cleanUserSessions = require('./cleanUserSessions') const buildApolloContext = require('./context') -const loadSanctionLists = require('./loadSanctionLists') const session = require('./session') module.exports = { cleanUserSessions, buildApolloContext, - loadSanctionLists, session } diff --git a/lib/new-admin/middlewares/loadSanctionLists.js b/lib/new-admin/middlewares/loadSanctionLists.js deleted file mode 100644 index 48d2c0b8..00000000 --- a/lib/new-admin/middlewares/loadSanctionLists.js +++ /dev/null @@ -1,28 +0,0 @@ -const logger = require('../../logger') -const sanctions = require('../../ofac') - -const sanctionStatus = { - loaded: false, - timestamp: null -} - -const loadSanctionLists = (req, res, next) => { - if (!sanctionStatus.loaded) { - logger.info('No sanction lists loaded. Loading sanctions...') - return sanctions.load() - .then(() => { - logger.info('OFAC sanction list loaded!') - sanctionStatus.loaded = true - sanctionStatus.timestamp = Date.now() - return next() - }) - .catch(e => { - logger.error('Couldn\'t load OFAC sanction list!') - return next(e) - }) - } - - return next() -} - -module.exports = loadSanctionLists From 30a43869ec860f9232b9183d2a40faf28803db13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Mon, 11 Jul 2022 02:06:14 +0100 Subject: [PATCH 3/4] fix: increase OFAC list update timer --- lib/new-admin/graphql/resolvers/sanctions.resolver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/new-admin/graphql/resolvers/sanctions.resolver.js b/lib/new-admin/graphql/resolvers/sanctions.resolver.js index 3945516a..95cf2d88 100644 --- a/lib/new-admin/graphql/resolvers/sanctions.resolver.js +++ b/lib/new-admin/graphql/resolvers/sanctions.resolver.js @@ -9,7 +9,7 @@ const sanctionStatus = { } const loadOrUpdateSanctions = () => { - if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.minute)) { + if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.day)) { logger.info('No sanction lists loaded. Loading sanctions...') return ofac.load() .then(() => { From cec02c52ba68118195c39c9ea2e47d2e9730deef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Sat, 8 Oct 2022 01:08:29 +0100 Subject: [PATCH 4/4] fix: resolve full sanction checking flow in the backend --- .../graphql/resolvers/sanctions.resolver.js | 37 +++------------- lib/new-admin/graphql/types/sanctions.type.js | 2 +- lib/sanctions.js | 44 +++++++++++++++++++ .../src/pages/Customers/CustomerData.js | 4 +- .../src/pages/Customers/CustomerProfile.js | 17 ++----- 5 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 lib/sanctions.js diff --git a/lib/new-admin/graphql/resolvers/sanctions.resolver.js b/lib/new-admin/graphql/resolvers/sanctions.resolver.js index 95cf2d88..bb1a8862 100644 --- a/lib/new-admin/graphql/resolvers/sanctions.resolver.js +++ b/lib/new-admin/graphql/resolvers/sanctions.resolver.js @@ -1,37 +1,12 @@ -const _ = require('lodash/fp') -const logger = require('../../../logger') -const ofac = require('../../../ofac') -const T = require('../../../time') - -const sanctionStatus = { - loaded: false, - timestamp: null -} - -const loadOrUpdateSanctions = () => { - if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.day)) { - logger.info('No sanction lists loaded. Loading sanctions...') - return ofac.load() - .then(() => { - logger.info('OFAC sanction list loaded!') - sanctionStatus.loaded = true - sanctionStatus.timestamp = Date.now() - }) - .catch(e => { - logger.error('Couldn\'t load OFAC sanction list!') - }) - } - - return Promise.resolve() -} +const sanctions = require('../../../sanctions') +const authentication = require('../modules/userManagement') const resolvers = { Query: { - checkAgainstSanctions: (...[, { firstName, lastName, birthdate }]) => loadOrUpdateSanctions() - .then(() => { - const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false }) - return { ofacSanctioned: _.size(ofacMatches) > 0 } - }) + checkAgainstSanctions: (...[, { customerId }, context]) => { + const token = authentication.getToken(context) + return sanctions.checkByUser(customerId, token) + } } } diff --git a/lib/new-admin/graphql/types/sanctions.type.js b/lib/new-admin/graphql/types/sanctions.type.js index 76a5e572..b7899da6 100644 --- a/lib/new-admin/graphql/types/sanctions.type.js +++ b/lib/new-admin/graphql/types/sanctions.type.js @@ -6,7 +6,7 @@ const typeDef = gql` } type Query { - checkAgainstSanctions(firstName: String, lastName: String, birthdate: String): SanctionMatches @auth + checkAgainstSanctions(customerId: ID): SanctionMatches @auth } ` diff --git a/lib/sanctions.js b/lib/sanctions.js new file mode 100644 index 00000000..34b1b7fd --- /dev/null +++ b/lib/sanctions.js @@ -0,0 +1,44 @@ +const _ = require('lodash/fp') +const ofac = require('./ofac') +const T = require('./time') +const logger = require('./logger') +const customers = require('./customers') + +const sanctionStatus = { + loaded: false, + timestamp: null +} + +const loadOrUpdateSanctions = () => { + if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.day)) { + logger.info('No sanction lists loaded. Loading sanctions...') + return ofac.load() + .then(() => { + logger.info('OFAC sanction list loaded!') + sanctionStatus.loaded = true + sanctionStatus.timestamp = Date.now() + }) + .catch(e => { + logger.error('Couldn\'t load OFAC sanction list!') + }) + } + + return Promise.resolve() +} + +const checkByUser = (customerId, userToken) => { + return Promise.all([loadOrUpdateSanctions(), customers.getCustomerById(customerId)]) + .then(([, customer]) => { + const { firstName, lastName, dateOfBirth } = customer?.idCardData + const birthdate = _.replace(/-/g, '')(dateOfBirth) + const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false }) + const isOfacSanctioned = _.size(ofacMatches) > 0 + customers.updateCustomer(customerId, { sanctions: !isOfacSanctioned }, userToken) + + return { ofacSanctioned: isOfacSanctioned } + }) +} + +module.exports = { + checkByUser +} diff --git a/new-lamassu-admin/src/pages/Customers/CustomerData.js b/new-lamassu-admin/src/pages/Customers/CustomerData.js index affe5159..5edbe829 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerData.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerData.js @@ -176,9 +176,7 @@ const CustomerData = ({ checkAgainstSanctions: () => checkAgainstSanctions({ variables: { - firstName: initialValues.idCardData.firstName, - lastName: initialValues.idCardData.lastName, - birthdate: R.replace(/-/g, '')(initialValues.idCardData.dateOfBirth) + customerId: R.path(['id'])(customer) } }), initialValues: initialValues.idCardData, diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js index 3c03cb4f..44478022 100644 --- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js +++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js @@ -293,16 +293,8 @@ const GET_ACTIVE_CUSTOM_REQUESTS = gql` ` const CHECK_AGAINST_SANCTIONS = gql` - query checkAgainstSanctions( - $firstName: String - $lastName: String - $birthdate: String - ) { - checkAgainstSanctions( - firstName: $firstName - lastName: $lastName - birthdate: $birthdate - ) { + query checkAgainstSanctions($customerId: ID) { + checkAgainstSanctions(customerId: $customerId) { ofacSanctioned } } @@ -417,10 +409,7 @@ const CustomerProfile = memo(() => { }) const [checkAgainstSanctions] = useLazyQuery(CHECK_AGAINST_SANCTIONS, { - onCompleted: ({ checkAgainstSanctions: { ofacSanctioned } }) => - updateCustomer({ - sanctions: !ofacSanctioned - }) + onCompleted: () => getCustomer() }) const updateCustomer = it =>