diff --git a/lib/compliance-triggers.js b/lib/compliance-triggers.js
index da4191a3..dcbe6ab2 100644
--- a/lib/compliance-triggers.js
+++ b/lib/compliance-triggers.js
@@ -26,4 +26,9 @@ const hasPhone = hasRequirement('sms')
const hasFacephoto = hasRequirement('facephoto')
const hasIdScan = hasRequirement('idCardData')
-module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan }
\ No newline at end of file
+const AUTH_METHODS = {
+ SMS: 'SMS',
+ EMAIL: 'EMAIL'
+}
+
+module.exports = { getBackwardsCompatibleTriggers, hasSanctions, maxDaysThreshold, getCashLimit, hasPhone, hasFacephoto, hasIdScan, AUTH_METHODS }
\ No newline at end of file
diff --git a/lib/customers.js b/lib/customers.js
index e855070d..012c042e 100644
--- a/lib/customers.js
+++ b/lib/customers.js
@@ -6,13 +6,10 @@ const makeDir = require('make-dir')
const path = require('path')
const fs = require('fs')
const util = require('util')
-const { sub, differenceInHours } = require('date-fns/fp')
const db = require('./db')
-const BN = require('./bn')
const anonymous = require('../lib/constants').anonymousCustomer
const complianceOverrides = require('./compliance_overrides')
-const users = require('./users')
const writeFile = util.promisify(fs.writeFile)
const notifierQueries = require('./notifier/queries')
const notifierUtils = require('./notifier/utils')
@@ -43,6 +40,12 @@ function add (customer) {
.then(camelize)
}
+function addWithEmail (customer) {
+ const sql = 'insert into customers (id, email, email_at) values ($1, $2, now()) returning *'
+ return db.one(sql, [uuid.v4(), customer.email])
+ .then(camelize)
+}
+
/**
* Get single customer by phone
* Phone numbers are unique per customer
@@ -60,6 +63,12 @@ function get (phone) {
.then(camelize)
}
+function getWithEmail (email) {
+ const sql = 'select * from customers where email=$1'
+ return db.oneOrNone(sql, [email])
+ .then(camelize)
+}
+
/**
* Update customer record
*
@@ -308,7 +317,7 @@ const updateSubscriberData = (customerId, data, userToken) => {
*
* Used for the machine.
*/
-function getById (id, userToken) {
+function getById (id) {
const sql = 'select * from customers where id=$1'
return db.oneOrNone(sql, [id])
.then(assignCustomerData)
@@ -349,6 +358,7 @@ function camelizeDeep (customer) {
function getComplianceTypes () {
return [
'sms',
+ 'email',
'id_card_data',
'id_card_photo',
'front_camera',
@@ -482,7 +492,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
- phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
+ phone, email, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, GREATEST(created, last_transaction, last_data_provided) AS last_active, fiat AS last_tx_fiat,
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer
@@ -491,9 +501,9 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
greatest(0, date_part('day', c.suspended_until - NOW())) AS days_suspended,
c.suspended_until > NOW() AS is_suspended,
c.front_camera_path, c.front_camera_override,
- c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
+ c.phone, c.email, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
- GREATEST(c.phone_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
+ GREATEST(c.phone_at, c.email_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
c.sanctions_at, c.sanctions_override, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
row_number() OVER (partition by c.id order by t.created desc) AS rn,
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
@@ -540,7 +550,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
function getCustomerById (id) {
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_at, front_camera_override,
- phone, phone_at, phone_override, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
+ phone, phone_at, email, email_at, phone_override, sms_override, id_card_data_at, id_card_data, id_card_data_override, id_card_data_expiration,
id_card_photo_path, id_card_photo_at, id_card_photo_override, us_ssn_at, us_ssn, us_ssn_override, sanctions, sanctions_at,
sanctions_override, total_txs, total_spent, LEAST(created, last_transaction) AS last_active, fiat AS last_tx_fiat,
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, subscriber_info, subscriber_info_at, custom_fields, notes, is_test_customer
@@ -549,7 +559,7 @@ function getCustomerById (id) {
greatest(0, date_part('day', c.suspended_until - now())) AS days_suspended,
c.suspended_until > now() AS is_suspended,
c.front_camera_path, c.front_camera_override, c.front_camera_at,
- c.phone, c.phone_at, c.phone_override, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
+ c.phone, c.phone_at, c.email, c.email_at, c.phone_override, c.sms_override, c.id_card_data, c.id_card_data_at, c.id_card_data_override, c.id_card_data_expiration,
c.id_card_photo_path, c.id_card_photo_at, c.id_card_photo_override, c.us_ssn, c.us_ssn_at, c.us_ssn_override, c.sanctions,
c.sanctions_at, c.sanctions_override, c.subscriber_info, c.subscriber_info_at, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
row_number() OVER (PARTITION BY c.id ORDER BY t.created DESC) AS rn,
@@ -912,7 +922,9 @@ function disableTestCustomer (customerId) {
module.exports = {
add,
+ addWithEmail,
get,
+ getWithEmail,
batch,
getCustomersList,
getCustomerById,
@@ -930,7 +942,5 @@ module.exports = {
updateEditedPhoto,
updateTxCustomerPhoto,
enableTestCustomer,
- disableTestCustomer,
- selectLatestData,
- getEditedData
+ disableTestCustomer
}
diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js
index c442cc95..66b60061 100644
--- a/lib/graphql/resolvers.js
+++ b/lib/graphql/resolvers.js
@@ -115,6 +115,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
configManager.getOperatorInfo(settings.config),
configManager.getReceipt(settings.config),
!!configManager.getCashOut(deviceId, settings.config).active,
+ configManager.getCustomerAuthenticationMethod(settings.config)
])
.then(([
enablePaperWalletOnly,
@@ -125,6 +126,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
operatorInfo,
receiptInfo,
twoWayMode,
+ customerAuthentication,
]) =>
(currentConfigVersion && currentConfigVersion >= staticConf.configVersion) ?
null :
@@ -141,6 +143,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
},
machineInfo: { deviceId, deviceName },
twoWayMode,
+ customerAuthentication,
speedtestFiles,
urlsToPing,
}),
diff --git a/lib/graphql/types.js b/lib/graphql/types.js
index 3b4655ea..f2c4e743 100644
--- a/lib/graphql/types.js
+++ b/lib/graphql/types.js
@@ -123,6 +123,11 @@ type Terms {
details: TermsDetails
}
+enum CustomerAuthentication {
+ EMAIL
+ SMS
+}
+
type StaticConfig {
configVersion: Int!
@@ -132,6 +137,7 @@ type StaticConfig {
serverVersion: String!
timezone: Int!
twoWayMode: Boolean!
+ customerAuthentication: CustomerAuthentication!
localeInfo: LocaleInfo!
operatorInfo: OperatorInfo
diff --git a/lib/new-admin/graphql/types/customer.type.js b/lib/new-admin/graphql/types/customer.type.js
index 5af0c458..278491ad 100644
--- a/lib/new-admin/graphql/types/customer.type.js
+++ b/lib/new-admin/graphql/types/customer.type.js
@@ -12,6 +12,7 @@ const typeDef = gql`
frontCameraAt: Date
frontCameraOverride: String
phone: String
+ email: String
isAnonymous: Boolean
smsOverride: String
idCardData: JSONObject
diff --git a/lib/new-config-manager.js b/lib/new-config-manager.js
index 65c7966d..efb84450 100644
--- a/lib/new-config-manager.js
+++ b/lib/new-config-manager.js
@@ -1,3 +1,5 @@
+const {AUTH_METHODS} = require('./compliance-triggers')
+
const _ = require('lodash/fp')
const { validate } = require('uuid')
@@ -120,6 +122,10 @@ const getGlobalNotifications = config => getNotifications(null, null, config)
const getTriggers = _.get('triggers')
+function getCustomerAuthenticationMethod(config) {
+ return _.get('triggersConfig_customerAuthentication')(config)
+}
+
/* `customInfoRequests` is the result of a call to `getCustomInfoRequests` */
const getTriggersAutomation = (customInfoRequests, config, oldFormat = false) => {
return customInfoRequests
@@ -193,4 +199,5 @@ module.exports = {
getCryptosFromWalletNamespace,
getCryptoUnits,
setTermsConditions,
+ getCustomerAuthenticationMethod,
}
diff --git a/lib/plugins.js b/lib/plugins.js
index 6961a6d4..b7d05d2d 100644
--- a/lib/plugins.js
+++ b/lib/plugins.js
@@ -779,6 +779,27 @@ function plugins (settings, deviceId) {
})
}
+ function getEmailCode (toEmail) {
+ const notifications = configManager.getNotifications(settings.config)
+
+ const code = notifications.thirdParty_email === 'mock-email'
+ ? '123'
+ : randomCode()
+
+ const req = {
+ email: {
+ toEmail,
+ subject: 'Your cryptomat code',
+ text: `Your cryptomat code: ${code}`
+ }
+ }
+
+ console.log(code)
+ return Promise.resolve(code)
+ // return sms.sendMessage(settings, rec)
+ // .then(() => code)
+ }
+
function sweepHdRow (row) {
const txId = row.id
const cryptoCode = row.crypto_code
@@ -862,6 +883,7 @@ function plugins (settings, deviceId) {
isZeroConf,
getStatus,
getPhoneCode,
+ getEmailCode,
executeTrades,
pong,
clearOldLogs,
diff --git a/lib/plugins/email/mailgun/mailgun.js b/lib/plugins/email/mailgun/mailgun.js
index 357baa20..e4f602a3 100644
--- a/lib/plugins/email/mailgun/mailgun.js
+++ b/lib/plugins/email/mailgun/mailgun.js
@@ -4,10 +4,11 @@ const NAME = 'Mailgun'
function sendMessage ({apiKey, domain, fromEmail, toEmail}, rec) {
const mailgun = Mailgun({apiKey, domain})
+ const to = req.email.toEmail ?? toEmail
const emailData = {
from: `Lamassu Server ${fromEmail}`,
- to: toEmail,
+ to,
subject: rec.email.subject,
text: rec.email.body
}
diff --git a/lib/route-helpers.js b/lib/route-helpers.js
index b382b056..91cf5d5f 100644
--- a/lib/route-helpers.js
+++ b/lib/route-helpers.js
@@ -45,12 +45,12 @@ function toCashOutTx (row) {
return _.set('direction', 'cashOut', newObj)
}
-function fetchPhoneTx (phone) {
+function fetchEmailOrPhoneTx (data, type) {
const sql = `select * from cash_out_txs
- where phone=$1 and dispense=$2
+ where ${type === 'email' ? 'email' : 'phone'}=$1 and dispense=$2
and (extract(epoch from (now() - created))) * 1000 < $3`
- const values = [phone, false, TRANSACTION_EXPIRATION]
+ const values = [data, false, TRANSACTION_EXPIRATION]
return db.any(sql, values)
.then(_.map(toCashOutTx))
@@ -72,6 +72,13 @@ function fetchPhoneTx (phone) {
throw httpError('No transactions', 404)
})
}
+function fetchEmailTx (email) {
+ return fetchEmailOrPhoneTx(email, 'email')
+}
+
+function fetchPhoneTx (phone) {
+ return fetchEmailOrPhoneTx(phone, 'phone')
+}
function fetchStatusTx (txId, status) {
const sql = 'select * from cash_out_txs where id=$1'
@@ -88,6 +95,7 @@ function fetchStatusTx (txId, status) {
module.exports = {
stateChange,
fetchPhoneTx,
+ fetchEmailTx,
fetchStatusTx,
httpError
}
diff --git a/lib/routes.js b/lib/routes.js
index 8d248413..a28ce062 100644
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -77,7 +77,10 @@ app.use('/verify_user', verifyUserRoutes)
app.use('/verify_transaction', verifyTxRoutes)
app.use('/verify_promo_code', verifyPromoCodeRoutes)
+// BACKWARDS_COMPATIBILITY 9.0
+// machines before 9.0 still use the phone_code route
app.use('/phone_code', phoneCodeRoutes)
+
app.use('/customer', customerRoutes)
app.use('/tx', txRoutes)
diff --git a/lib/routes/customerRoutes.js b/lib/routes/customerRoutes.js
index 0f880a10..3e7c4a98 100644
--- a/lib/routes/customerRoutes.js
+++ b/lib/routes/customerRoutes.js
@@ -20,6 +20,9 @@ const machineLoader = require('../machine-loader')
const { loadLatestConfig } = require('../new-settings-loader')
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
const T = require('../time')
+const plugins = require('../plugins')
+const Tx = require('../tx')
+const loyalty = require('../loyalty')
function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
if (_.isNil(patch.data)) {
@@ -185,6 +188,70 @@ function sendSmsReceipt (req, res, next) {
})
}
+function addOrUpdateCustomer (customerData, config, isEmailAuth) {
+ const triggers = configManager.getTriggers(config)
+ const maxDaysThreshold = complianceTriggers.maxDaysThreshold(triggers)
+
+ const customerKey = isEmailAuth ? customerData.email : customerData.phone
+ const getFunc = isEmailAuth ? customers.getWithEmail : customers.get
+ const addFunction = isEmailAuth ? customers.addWithEmail : customers.add
+
+ return getFunc(customerKey)
+ .then(customer => {
+ if (customer) return customer
+
+ return addFunction(customerData)
+ })
+ .then(customer => customers.getById(customer.id))
+ .then(customer => {
+ return Tx.customerHistory(customer.id, maxDaysThreshold)
+ .then(result => {
+ customer.txHistory = result
+ return customer
+ })
+ })
+ .then(customer => {
+ return loyalty.getCustomerActiveIndividualDiscount(customer.id)
+ .then(discount => ({ ...customer, discount }))
+ })
+}
+
+function getOrAddCustomerPhone (req, res, next) {
+ const customerData = req.body
+
+ const pi = plugins(req.settings, req.deviceId)
+ const phone = req.body.phone
+
+ return pi.getPhoneCode(phone)
+ .then(code => {
+ return addOrUpdateCustomer(customerData, req.settings.config, false)
+ .then(customer => respond(req, res, { code, customer }))
+ })
+ .catch(err => {
+ if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
+ throw err
+ })
+ .catch(next)
+}
+
+function getOrAddCustomerEmail (req, res, next) {
+ const customerData = req.body
+
+ const pi = plugins(req.settings, req.deviceId)
+ const email = req.body.email
+
+ return pi.getEmailCode(email)
+ .then(code => {
+ return addOrUpdateCustomer(customerData, req.settings.config, true)
+ .then(customer => respond(req, res, { code, customer }))
+ })
+ .catch(err => {
+ if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
+ throw err
+ })
+ .catch(next)
+}
+
router.patch('/:id', updateCustomer)
router.patch('/:id/sanctions', triggerSanctions)
router.patch('/:id/block', triggerBlock)
@@ -192,5 +259,7 @@ router.patch('/:id/suspend', triggerSuspend)
router.patch('/:id/photos/idcarddata', updateIdCardData)
router.patch('/:id/:txId/photos/customerphoto', updateTxCustomerPhoto)
router.post('/:id/smsreceipt', sendSmsReceipt)
+router.post('/phone_code', getOrAddCustomerPhone)
+router.post('/email_code', getOrAddCustomerEmail)
module.exports = router
diff --git a/lib/routes/txRoutes.js b/lib/routes/txRoutes.js
index cfda6428..a3261147 100644
--- a/lib/routes/txRoutes.js
+++ b/lib/routes/txRoutes.js
@@ -66,8 +66,19 @@ function getPhoneTx (req, res, next) {
return next(httpError('Not Found', 404))
}
+function getEmailTx (req, res, next) {
+ if (req.query.email) {
+ return helpers.fetchEmailTx(req.query.email)
+ .then(r => res.json(r))
+ .catch(next)
+ }
+
+ return next(httpError('Not Found', 404))
+}
+
router.post('/', postTx)
router.get('/:id', getTx)
router.get('/', getPhoneTx)
+router.get('/', getEmailTx)
-module.exports = { postTx, getTx, getPhoneTx, router }
+module.exports = { postTx, getTx, getPhoneTx, getEmailTx, router }
diff --git a/migrations/1700123461281-customer-email.js b/migrations/1700123461281-customer-email.js
new file mode 100644
index 00000000..58cd358d
--- /dev/null
+++ b/migrations/1700123461281-customer-email.js
@@ -0,0 +1,14 @@
+const db = require('./db')
+
+exports.up = function (next) {
+ let sql = [
+ 'ALTER TABLE customers ADD COLUMN email text unique',
+ 'ALTER TABLE customers ADD COLUMN email_at timestamptz',
+ ]
+
+ db.multi(sql, next)
+}
+
+exports.down = function (next) {
+ next()
+}
diff --git a/migrations/1700123461282-customer-auth-advanced-trigger.js b/migrations/1700123461282-customer-auth-advanced-trigger.js
new file mode 100644
index 00000000..02e61f3f
--- /dev/null
+++ b/migrations/1700123461282-customer-auth-advanced-trigger.js
@@ -0,0 +1,18 @@
+const { migrationSaveConfig } = require('../lib/new-settings-loader')
+
+exports.up = function (next) {
+ const triggersDefault = {
+ triggersConfig_customerAuthentication: 'SMS',
+ }
+
+ return migrationSaveConfig(triggersDefault)
+ .then(() => next())
+ .catch(err => {
+ console.log(err.message)
+ return next(err)
+ })
+}
+
+exports.down = function (next) {
+ next()
+}
diff --git a/migrations/1700123461283-phone-on-tx.js b/migrations/1700123461283-phone-on-tx.js
new file mode 100644
index 00000000..1e8833f1
--- /dev/null
+++ b/migrations/1700123461283-phone-on-tx.js
@@ -0,0 +1,14 @@
+const db = require('./db')
+
+exports.up = function (next) {
+ let sql = [
+ 'ALTER TABLE cash_in_txs ADD COLUMN email text',
+ 'ALTER TABLE cash_out_txs ADD COLUMN email text',
+ ]
+
+ db.multi(sql, next)
+}
+
+exports.down = function (next) {
+ next()
+}
diff --git a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
index cbc79296..0ee9f7ae 100644
--- a/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
+++ b/new-lamassu-admin/src/pages/Customers/CustomerProfile.js
@@ -57,6 +57,7 @@ const GET_CUSTOMER = gql`
frontCameraAt
frontCameraOverride
phone
+ email
isAnonymous
smsOverride
idCardData
@@ -132,6 +133,7 @@ const SET_CUSTOMER = gql`
frontCameraPath
frontCameraOverride
phone
+ email
smsOverride
idCardData
idCardDataOverride
@@ -516,6 +518,8 @@ const CustomerProfile = memo(() => {
})) ?? []
const classes = useStyles()
+ const email = R.path(['email'])(customerData)
+ const phone = R.path(['phone'])(customerData)
return (
<>
@@ -532,10 +536,9 @@ const CustomerProfile = memo(() => {