commit
e381efed6d
36 changed files with 5881 additions and 216 deletions
|
|
@ -225,6 +225,7 @@
|
|||
"it-CH",
|
||||
"it-IT",
|
||||
"ja-JP",
|
||||
"ka-GE",
|
||||
"ko-KR",
|
||||
"ky-KG",
|
||||
"lt-LT",
|
||||
|
|
|
|||
|
|
@ -1,22 +1,52 @@
|
|||
const db = require('./db')
|
||||
|
||||
function blocked (address, cryptoCode) {
|
||||
const sql = `select * from blacklist where address = $1 and crypto_code = $2`
|
||||
return db.any(sql, [
|
||||
address,
|
||||
cryptoCode
|
||||
])
|
||||
// Get all blacklist rows from the DB "blacklist" table
|
||||
const getBlacklist = () => {
|
||||
return db.any('select * from blacklist').then(res =>
|
||||
res.map(item => ({
|
||||
cryptoCode: item.crypto_code,
|
||||
address: item.address,
|
||||
createdByOperator: item.created_by_operator
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
function addToUsedAddresses (address, cryptoCode) {
|
||||
// Delete row from blacklist table by crypto code and address
|
||||
const deleteFromBlacklist = (cryptoCode, address) => {
|
||||
return db.none(
|
||||
'delete from blacklist where crypto_code = $1 and address = $2;',
|
||||
[cryptoCode, address]
|
||||
)
|
||||
}
|
||||
|
||||
const insertIntoBlacklist = (cryptoCode, address) => {
|
||||
return db
|
||||
.any(
|
||||
'insert into blacklist(crypto_code, address, created_by_operator) values($1, $2, $3);',
|
||||
[cryptoCode, address, true]
|
||||
)
|
||||
.then(() => {
|
||||
return { cryptoCode, address }
|
||||
})
|
||||
}
|
||||
|
||||
function blocked(address, cryptoCode) {
|
||||
const sql = `select * from blacklist where address = $1 and crypto_code = $2`
|
||||
return db.any(sql, [address, cryptoCode])
|
||||
}
|
||||
|
||||
function addToUsedAddresses(address, cryptoCode) {
|
||||
// ETH reuses addresses
|
||||
if (cryptoCode === 'ETH') return Promise.resolve()
|
||||
|
||||
const sql = `insert into blacklist(crypto_code, address, created_by_operator) values ($1, $2, 'f')`
|
||||
return db.oneOrNone(sql, [
|
||||
cryptoCode,
|
||||
address
|
||||
])
|
||||
return db.oneOrNone(sql, [cryptoCode, address])
|
||||
}
|
||||
|
||||
module.exports = { blocked, addToUsedAddresses }
|
||||
module.exports = {
|
||||
blocked,
|
||||
addToUsedAddresses,
|
||||
getBlacklist,
|
||||
deleteFromBlacklist,
|
||||
insertIntoBlacklist
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,6 @@ connections=40
|
|||
keypool=10000
|
||||
prune=4000
|
||||
daemon=0
|
||||
addresstype=p2sh-segwit`
|
||||
addresstype=p2sh-segwit
|
||||
walletrbf=1`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,24 +25,24 @@ const BINARIES = {
|
|||
dir: 'bitcoin-0.20.1/bin'
|
||||
},
|
||||
ETH: {
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.23-8c2f2715.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.9.23-8c2f2715'
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.24-cc05b050.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.9.24-cc05b050'
|
||||
},
|
||||
ZEC: {
|
||||
url: 'https://download.z.cash/downloads/zcash-4.1.0-linux64-debian-stretch.tar.gz',
|
||||
dir: 'zcash-4.1.0/bin'
|
||||
url: 'https://z.cash/downloads/zcash-4.1.1-linux64-debian-stretch.tar.gz',
|
||||
dir: 'zcash-4.1.1/bin'
|
||||
},
|
||||
DASH: {
|
||||
url: 'https://github.com/dashpay/dash/releases/download/v0.16.0.1/dashcore-0.16.0.1-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'dashcore-0.16.0/bin'
|
||||
url: 'https://github.com/dashpay/dash/releases/download/v0.16.1.1/dashcore-0.16.1.1-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'dashcore-0.16.1/bin'
|
||||
},
|
||||
LTC: {
|
||||
url: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'litecoin-0.18.1/bin'
|
||||
},
|
||||
BCH: {
|
||||
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v22.1.0/bitcoin-cash-node-22.1.0-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-cash-node-22.1.0/bin',
|
||||
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v22.2.0/bitcoin-cash-node-22.2.0-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-cash-node-22.2.0/bin',
|
||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ module.exports = { update }
|
|||
function mapCoin (rates, deviceId, settings, cryptoCode) {
|
||||
const config = settings.config
|
||||
const buildedRates = plugins(settings, deviceId).buildRates(rates)[cryptoCode] || { cashIn: null, cashOut: null }
|
||||
|
||||
const commissions = configManager.getCommissions(cryptoCode, deviceId, config)
|
||||
const coinAtmRadar = configManager.getCoinAtmRadar(config)
|
||||
|
||||
|
|
@ -64,28 +63,17 @@ function mapMachine (rates, settings, machineRow) {
|
|||
|
||||
const coinAtmRadar = configManager.getCoinAtmRadar(config)
|
||||
const triggers = configManager.getTriggers(config)
|
||||
const triggerCashLimit = complianceTriggers.getCashLimit(triggers)
|
||||
const locale = configManager.getLocale(deviceId, config)
|
||||
const cashOutConfig = configManager.getCashOut(deviceId, config)
|
||||
const cashOutEnabled = cashOutConfig.active ? cashOutConfig.active : false
|
||||
|
||||
const lastOnline = machineRow.last_online.toISOString()
|
||||
const status = machineRow.stale ? 'online' : 'offline'
|
||||
const showSupportedCryptocurrencies = coinAtmRadar.supportedCryptocurrencies
|
||||
const showSupportedFiat = coinAtmRadar.supportedFiat
|
||||
const showSupportedBuySellDirection = coinAtmRadar.supportedBuySellDirection
|
||||
const showLimitsAndVerification = coinAtmRadar.limitsAndVerification
|
||||
|
||||
const cashLimit = showLimitsAndVerification ? ( triggerCashLimit || Infinity ) : null
|
||||
|
||||
const cashLimit = showLimitsAndVerification ? ( complianceTriggers.getCashLimit(triggers)?.threshold || Infinity ) : null
|
||||
const cryptoCurrencies = locale.cryptoCurrencies
|
||||
const cashInEnabled = showSupportedBuySellDirection ? true : null
|
||||
const cashOutEnabled = showSupportedBuySellDirection ? cashOutConfig.active : null
|
||||
const fiat = showSupportedFiat ? locale.fiatCurrency : null
|
||||
const identification = mapIdentification(config)
|
||||
const coins = showSupportedCryptocurrencies ?
|
||||
_.map(_.partial(mapCoin, [rates, deviceId, settings]), cryptoCurrencies)
|
||||
: null
|
||||
|
||||
const coins = _.map(_.partial(mapCoin, [rates, deviceId, settings]), cryptoCurrencies)
|
||||
return {
|
||||
machineId: deviceId,
|
||||
address: {
|
||||
|
|
@ -102,14 +90,14 @@ function mapMachine (rates, settings, machineRow) {
|
|||
},
|
||||
status,
|
||||
lastOnline,
|
||||
cashIn: cashInEnabled,
|
||||
cashIn: true,
|
||||
cashOut: cashOutEnabled,
|
||||
manufacturer: 'lamassu',
|
||||
cashInTxLimit: cashLimit,
|
||||
cashOutTxLimit: cashLimit,
|
||||
cashInDailyLimit: cashLimit,
|
||||
cashOutDailyLimit: cashLimit,
|
||||
fiatCurrency: fiat,
|
||||
fiatCurrency: locale.fiatCurrency,
|
||||
identification,
|
||||
coins
|
||||
}
|
||||
|
|
@ -120,7 +108,6 @@ function getMachines (rates, settings) {
|
|||
where display=TRUE and
|
||||
paired=TRUE
|
||||
order by created`
|
||||
|
||||
return db.any(sql, [STALE_INTERVAL])
|
||||
.then(_.map(_.partial(mapMachine, [rates, settings])))
|
||||
}
|
||||
|
|
@ -140,9 +127,7 @@ function sendRadar (data) {
|
|||
maxContentLength: MAX_CONTENT_LENGTH
|
||||
}
|
||||
|
||||
console.log('%j', data)
|
||||
|
||||
return axios(config)
|
||||
return axios.default(config)
|
||||
.then(r => console.log(r.status))
|
||||
}
|
||||
|
||||
|
|
|
|||
259
lib/coinatmradar/test/coinatmradar.test.js
Normal file
259
lib/coinatmradar/test/coinatmradar.test.js
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
const yup = require('yup')
|
||||
const BigNumber = require('../../../lib/bn')
|
||||
const car = require('../coinatmradar')
|
||||
const db = require('../../db')
|
||||
|
||||
jest.mock('../../db')
|
||||
|
||||
afterEach(() => {
|
||||
// https://stackoverflow.com/questions/58151010/difference-between-resetallmocks-resetmodules-resetmoduleregistry-restoreallm
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
const settings = {
|
||||
config: {
|
||||
wallets_BTC_coin: 'BTC',
|
||||
wallets_BTC_wallet: 'mock-wallet',
|
||||
wallets_BTC_ticker: 'kraken',
|
||||
wallets_BTC_exchange: 'mock-exchange',
|
||||
wallets_BTC_zeroConf: 'all-zero-conf',
|
||||
locale_id: '1983951f-6c73-4308-ae6e-f6f56dfa5e11',
|
||||
locale_country: 'US',
|
||||
locale_fiatCurrency: 'USD',
|
||||
locale_languages: ['en-US'],
|
||||
locale_cryptoCurrencies: ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH'],
|
||||
commissions_minimumTx: 1,
|
||||
commissions_fixedFee: 2,
|
||||
commissions_cashOut: 11,
|
||||
commissions_cashIn: 11,
|
||||
commissions_id: '960bb192-db37-40eb-9b59-2c2c78620de6',
|
||||
wallets_ETH_active: true,
|
||||
wallets_ETH_ticker: 'bitstamp',
|
||||
wallets_ETH_wallet: 'mock-wallet',
|
||||
wallets_ETH_exchange: 'mock-exchange',
|
||||
wallets_ETH_zeroConf: 'mock-zero-conf',
|
||||
wallets_LTC_active: true,
|
||||
wallets_LTC_ticker: 'kraken',
|
||||
wallets_LTC_wallet: 'mock-wallet',
|
||||
wallets_LTC_exchange: 'mock-exchange',
|
||||
wallets_LTC_zeroConf: 'mock-zero-conf',
|
||||
wallets_DASH_active: true,
|
||||
wallets_DASH_ticker: 'coinbase',
|
||||
wallets_DASH_wallet: 'mock-wallet',
|
||||
wallets_DASH_exchange: 'mock-exchange',
|
||||
wallets_DASH_zeroConf: 'mock-zero-conf',
|
||||
wallets_ZEC_active: true,
|
||||
wallets_ZEC_ticker: 'coinbase',
|
||||
wallets_ZEC_wallet: 'mock-wallet',
|
||||
wallets_ZEC_exchange: 'mock-exchange',
|
||||
wallets_ZEC_zeroConf: 'mock-zero-conf',
|
||||
wallets_BCH_active: true,
|
||||
wallets_BCH_ticker: 'bitpay',
|
||||
wallets_BCH_wallet: 'mock-wallet',
|
||||
wallets_BCH_exchange: 'mock-exchange',
|
||||
wallets_BCH_zeroConf: 'mock-zero-conf',
|
||||
cashOut_7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4_zeroConfLimit: 50,
|
||||
cashOut_7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4_bottom: 20,
|
||||
cashOut_7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4_top: 5,
|
||||
cashOut_7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4_active: true,
|
||||
cashOut_f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05_zeroConfLimit: 200,
|
||||
cashOut_f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05_bottom: 20,
|
||||
cashOut_f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05_top: 5,
|
||||
cashOut_f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05_active: true,
|
||||
notifications_email_active: false,
|
||||
notifications_sms_active: true,
|
||||
notifications_email_errors: false,
|
||||
notifications_sms_errors: true,
|
||||
coinAtmRadar_active: true,
|
||||
coinAtmRadar_commissions: true,
|
||||
coinAtmRadar_limitsAndVerification: true,
|
||||
triggers: [
|
||||
{
|
||||
requirement: 'suspend',
|
||||
suspensionDays: 1,
|
||||
threshold: 123,
|
||||
id: '9c3b5af8-b1d1-4125-b169-0e913b33894c',
|
||||
direction: 'both',
|
||||
triggerType: 'txAmount'
|
||||
},
|
||||
{
|
||||
requirement: 'sms',
|
||||
threshold: 999,
|
||||
thresholdDays: 1,
|
||||
id: 'b0e1e6a8-be1b-4e43-ac5f-3e4951e86f8b',
|
||||
direction: 'both',
|
||||
triggerType: 'txVelocity'
|
||||
},
|
||||
{
|
||||
requirement: 'sms',
|
||||
threshold: 888,
|
||||
thresholdDays: 1,
|
||||
id: '6ac38fe6-172c-48a4-8a7f-605213cbd600',
|
||||
direction: 'both',
|
||||
triggerType: 'txVolume'
|
||||
}
|
||||
],
|
||||
notifications_sms_transactions: true,
|
||||
notifications_highValueTransaction: 50
|
||||
},
|
||||
accounts: {}
|
||||
}
|
||||
|
||||
const rates = [
|
||||
{
|
||||
rates: {
|
||||
ask: BigNumber(19164.3),
|
||||
bid: BigNumber(19164.2)
|
||||
},
|
||||
timestamp: +new Date()
|
||||
},
|
||||
{
|
||||
rates: {
|
||||
ask: BigNumber(594.54),
|
||||
bid: BigNumber(594.09)
|
||||
},
|
||||
timestamp: +new Date()
|
||||
},
|
||||
{
|
||||
rates: {
|
||||
ask: BigNumber(84.38),
|
||||
bid: BigNumber(84.37)
|
||||
},
|
||||
timestamp: +new Date()
|
||||
},
|
||||
{
|
||||
rates: {
|
||||
ask: BigNumber(102.8),
|
||||
bid: BigNumber(101.64)
|
||||
},
|
||||
timestamp: +new Date()
|
||||
},
|
||||
{
|
||||
rates: {
|
||||
ask: BigNumber(74.91),
|
||||
bid: BigNumber(74.12)
|
||||
},
|
||||
timestamp: +new Date()
|
||||
},
|
||||
{
|
||||
rates: {
|
||||
ask: BigNumber(284.4),
|
||||
bid: BigNumber(284.4)
|
||||
},
|
||||
timestamp: +new Date()
|
||||
}
|
||||
]
|
||||
|
||||
const dbResponse = [
|
||||
{
|
||||
device_id:
|
||||
'mock7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||
last_online: new Date('2020-11-16T13:11:03.169Z'),
|
||||
stale: false
|
||||
},
|
||||
{
|
||||
device_id:
|
||||
'9871e58aa2643ff9445cbc299b50397430ada75157d6c29b4c93548fff0f48f7',
|
||||
last_online: new Date('2020-11-16T16:21:35.948Z'),
|
||||
stale: false
|
||||
},
|
||||
{
|
||||
device_id:
|
||||
'5ae0d02dedeb77b6521bd5eb7c9159bdc025873fa0bcb6f87aaddfbda0c50913',
|
||||
last_online: new Date('2020-11-19T15:07:57.089Z'),
|
||||
stale: false
|
||||
},
|
||||
{
|
||||
device_id:
|
||||
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||
last_online: new Date('2020-11-26T20:05:57.792Z'),
|
||||
stale: false
|
||||
},
|
||||
{
|
||||
device_id:
|
||||
'490ab16ee0c124512dc769be1f3e7ee3894ce1e5b4b8b975e134fb326e551e88',
|
||||
last_online: new Date('2020-12-04T16:48:05.129Z'),
|
||||
stale: false
|
||||
}
|
||||
]
|
||||
|
||||
function validateData(data) {
|
||||
const schema = yup.object().shape({
|
||||
operatorId: yup.string().required('operatorId not provided'),
|
||||
operator: yup.object().shape({
|
||||
name: yup.string().nullable(),
|
||||
phone: yup.string().nullable(),
|
||||
email: yup.string().email().nullable()
|
||||
}),
|
||||
timestamp: yup.string().required('timestamp not provided'),
|
||||
machines: yup.array().of(
|
||||
yup.object().shape({
|
||||
machineId: yup.string().required('machineId not provided'),
|
||||
address: yup.object().required('address object not provided').shape({
|
||||
streetAddress: yup.string().nullable(),
|
||||
city: yup.string().nullable(),
|
||||
region: yup.string().nullable(),
|
||||
postalCode: yup.string().nullable(),
|
||||
country: yup.string().nullable()
|
||||
}),
|
||||
location: yup.object().required('location object not provided').shape({
|
||||
name: yup.string().nullable(),
|
||||
url: yup.string().nullable(),
|
||||
phone: yup.string().nullable()
|
||||
}),
|
||||
status: yup
|
||||
.string()
|
||||
.required('status not provided')
|
||||
.oneOf(['online', 'offline']),
|
||||
lastOnline: yup
|
||||
.string()
|
||||
.required('date in isostring format not provided'),
|
||||
cashIn: yup.boolean().required('cashIn boolean not defined'),
|
||||
cashOut: yup.boolean().required('cashOut boolean not defined'),
|
||||
manufacturer: yup.string().required('manufacturer not provided'),
|
||||
cashInTxLimit: yup.number().nullable(),
|
||||
cashOutTxLimit: yup.number().nullable(),
|
||||
cashInDailyLimit: yup.number().nullable(),
|
||||
cashOutDailyLimit: yup.number().nullable(),
|
||||
fiatCurrency: yup.string().required('fiatCurrency not provided'),
|
||||
identification: yup.object().shape({
|
||||
isPhone: yup.boolean().required('isPhone boolean not defined'),
|
||||
isPalmVein: yup.boolean().required('isPalmVein boolean not defined'),
|
||||
isPhoto: yup.boolean().required('isPhoto boolean not defined'),
|
||||
isIdDocScan: yup
|
||||
.boolean()
|
||||
.required('isIdDocScan boolean not defined'),
|
||||
isFingerprint: yup
|
||||
.boolean()
|
||||
.required('isFingerprint boolean not defined')
|
||||
}),
|
||||
coins: yup.array().of(
|
||||
yup.object().shape({
|
||||
cryptoCode: yup.string().required('cryptoCode not provided'),
|
||||
cashInFee: yup.number().nullable(),
|
||||
cashOutFee: yup.number().nullable(),
|
||||
cashInFixedFee: yup.number().nullable(),
|
||||
cashInRate: yup.number().nullable(),
|
||||
cashOutRate: yup.number().nullable()
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
return schema.validate(data)
|
||||
}
|
||||
|
||||
test('Verify axios request schema', async () => {
|
||||
const axios = require('axios')
|
||||
|
||||
jest.spyOn(axios, 'default').mockImplementation(
|
||||
jest.fn(req =>
|
||||
validateData(req.data)
|
||||
.then(() => ({ status: 'mock status 200' }))
|
||||
.catch(e => fail(e))
|
||||
)
|
||||
)
|
||||
|
||||
db.any.mockResolvedValue(dbResponse)
|
||||
await car.update(rates, settings)
|
||||
})
|
||||
|
|
@ -15,9 +15,8 @@ function maxDaysThreshold (triggers) {
|
|||
}
|
||||
|
||||
function getCashLimit (triggers) {
|
||||
const withFiat = _.filter(({ triggerType }) => _.includes(['txVolume', 'txAmount'])(triggerType))
|
||||
const blocking = _.filter(({ requirement }) => _.includes(['block', 'suspend'])(requirement))
|
||||
|
||||
const withFiat = _.filter(({ triggerType }) => _.includes(triggerType, ['txVolume', 'txAmount']))
|
||||
const blocking = _.filter(({ requirement }) => _.includes(requirement, ['block', 'suspend']))
|
||||
return _.compose(_.minBy('threshold'), blocking, withFiat)(triggers)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ function update (id, data, userToken, txId) {
|
|||
*
|
||||
* @returns {Promise} Newly updated Customer
|
||||
*/
|
||||
async function updateCustomer (id, data) {
|
||||
async function updateCustomer (id, data, userToken) {
|
||||
const formattedData = _.pick(
|
||||
[
|
||||
'authorized_override',
|
||||
|
|
@ -110,7 +110,10 @@ async function updateCustomer (id, data) {
|
|||
],
|
||||
_.mapKeys(_.snakeCase, data))
|
||||
|
||||
const sql = Pgp.helpers.update(formattedData, _.keys(formattedData), 'customers') +
|
||||
const enhancedUpdateData = enhanceAtFields(enhanceOverrideFields(formattedData, userToken))
|
||||
const updateData = updateOverride(enhancedUpdateData)
|
||||
|
||||
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
|
||||
' where id=$1'
|
||||
|
||||
await db.none(sql, [id])
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const dbm = require('./postgresql_interface')
|
|||
const configManager = require('./new-config-manager')
|
||||
const settingsLoader = require('./new-settings-loader')
|
||||
|
||||
module.exports = {getMachineName, getMachines, getMachineNames, setMachine}
|
||||
module.exports = {getMachineName, getMachines, getMachine, getMachineNames, setMachine}
|
||||
|
||||
function getMachines () {
|
||||
return db.any('select * from devices where display=TRUE order by created')
|
||||
|
|
@ -88,6 +88,11 @@ function getMachineName (machineId) {
|
|||
.then(it => it.name)
|
||||
}
|
||||
|
||||
function getMachine (machineId) {
|
||||
const sql = 'select * from devices where device_id=$1'
|
||||
return db.oneOrNone(sql, [machineId]).then(res => _.mapKeys(_.camelCase)(res))
|
||||
}
|
||||
|
||||
function renameMachine (rec) {
|
||||
const sql = 'update devices set name=$1 where device_id=$2'
|
||||
return db.none(sql, [rec.newName, rec.deviceId])
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ const apolloServer = new ApolloServer({
|
|||
|
||||
const success = await login.authenticate(token)
|
||||
if (!success) throw new AuthenticationError('Authentication failed')
|
||||
return { req: { ...req } }
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -68,10 +69,12 @@ app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false })
|
|||
|
||||
app.get('/api/register', (req, res, next) => {
|
||||
const otp = req.query.otp
|
||||
const ua = req.headers['user-agent']
|
||||
const ip = req.ip
|
||||
|
||||
if (!otp) return next()
|
||||
|
||||
return login.register(otp)
|
||||
return login.register(otp, ua, ip)
|
||||
.then(r => {
|
||||
if (r.expired) return res.status(401).send('OTP expired, generate new registration link')
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,16 @@ const { parseAsync } = require('json2csv')
|
|||
const { GraphQLDateTime } = require('graphql-iso-date')
|
||||
const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
|
||||
const got = require('got')
|
||||
const DataLoader = require('dataloader')
|
||||
|
||||
const machineLoader = require('../../machine-loader')
|
||||
const customers = require('../../customers')
|
||||
const { machineAction } = require('../machines')
|
||||
const logs = require('../../logs')
|
||||
const settingsLoader = require('../../new-settings-loader')
|
||||
const tokenManager = require('../../token-manager')
|
||||
const blacklist = require('../../blacklist')
|
||||
const machineEventsByIdBatch = require("../../postgresql_interface").machineEventsByIdBatch
|
||||
|
||||
const serverVersion = require('../../../package.json').version
|
||||
|
||||
|
|
@ -17,7 +21,13 @@ const funding = require('../funding')
|
|||
const supervisor = require('../supervisor')
|
||||
const serverLogs = require('../server-logs')
|
||||
const pairing = require('../pairing')
|
||||
const { accounts: accountsConfig, coins, countries, currencies, languages } = require('../config')
|
||||
const {
|
||||
accounts: accountsConfig,
|
||||
coins,
|
||||
countries,
|
||||
currencies,
|
||||
languages
|
||||
} = require('../config')
|
||||
|
||||
const typeDefs = gql`
|
||||
scalar JSON
|
||||
|
|
@ -61,6 +71,7 @@ const typeDefs = gql`
|
|||
cassette1: Int
|
||||
cassette2: Int
|
||||
statuses: [MachineStatus]
|
||||
latestEvent: MachineEvent
|
||||
}
|
||||
|
||||
type Customer {
|
||||
|
|
@ -155,6 +166,14 @@ const typeDefs = gql`
|
|||
uptime: Int!
|
||||
}
|
||||
|
||||
type UserToken {
|
||||
token: String!
|
||||
name: String!
|
||||
created: Date!
|
||||
user_agent: String
|
||||
ip_address: String
|
||||
}
|
||||
|
||||
type Transaction {
|
||||
id: ID!
|
||||
txClass: String!
|
||||
|
|
@ -183,7 +202,7 @@ const typeDefs = gql`
|
|||
customerId: ID
|
||||
txVersion: Int!
|
||||
termsAccepted: Boolean
|
||||
commissionPercentage: String
|
||||
commissionPercentage: String
|
||||
rawTickerPrice: String
|
||||
isPaperWallet: Boolean
|
||||
customerPhone: String
|
||||
|
|
@ -197,6 +216,22 @@ const typeDefs = gql`
|
|||
machineName: String
|
||||
}
|
||||
|
||||
type Blacklist {
|
||||
createdByOperator: Boolean!
|
||||
cryptoCode: String!
|
||||
address: String!
|
||||
}
|
||||
|
||||
type MachineEvent {
|
||||
id: ID
|
||||
deviceId: String
|
||||
eventType: String
|
||||
note: String
|
||||
created: Date
|
||||
age: Float
|
||||
deviceTime: Date
|
||||
}
|
||||
|
||||
type Query {
|
||||
countries: [Country]
|
||||
currencies: [Currency]
|
||||
|
|
@ -204,6 +239,7 @@ const typeDefs = gql`
|
|||
accountsConfig: [AccountConfig]
|
||||
cryptoCurrencies: [CryptoCurrency]
|
||||
machines: [Machine]
|
||||
machine(deviceId: ID!): Machine
|
||||
customers: [Customer]
|
||||
customer(customerId: ID!): Customer
|
||||
machineLogs(deviceId: ID!, from: Date, until: Date, limit: Int, offset: Int): [MachineLog]
|
||||
|
|
@ -217,6 +253,8 @@ const typeDefs = gql`
|
|||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int): String
|
||||
accounts: JSONObject
|
||||
config: JSONObject
|
||||
blacklist: [Blacklist]
|
||||
userTokens: [UserToken]
|
||||
}
|
||||
|
||||
enum MachineAction {
|
||||
|
|
@ -235,9 +273,17 @@ const typeDefs = gql`
|
|||
saveConfig(config: JSONObject): JSONObject
|
||||
createPairingTotem(name: String!): String
|
||||
saveAccounts(accounts: JSONObject): JSONObject
|
||||
revokeToken(token: String!): UserToken
|
||||
deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist
|
||||
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist
|
||||
}
|
||||
`
|
||||
|
||||
const transactionsLoader = new DataLoader(ids => transactions.getCustomerTransactionsBatch(ids))
|
||||
const machineEventsLoader = new DataLoader(ids => {
|
||||
return machineEventsByIdBatch(ids)
|
||||
}, { cache: false })
|
||||
|
||||
const notify = () => got.post('http://localhost:3030/dbChange')
|
||||
.catch(e => console.error('Error: lamassu-server not responding'))
|
||||
|
||||
|
|
@ -246,7 +292,10 @@ const resolvers = {
|
|||
JSONObject: GraphQLJSONObject,
|
||||
Date: GraphQLDateTime,
|
||||
Customer: {
|
||||
transactions: parent => transactions.getCustomerTransactions(parent.id)
|
||||
transactions: parent => transactionsLoader.load(parent.id)
|
||||
},
|
||||
Machine: {
|
||||
latestEvent: parent => machineEventsLoader.load(parent.deviceId)
|
||||
},
|
||||
Query: {
|
||||
countries: () => countries,
|
||||
|
|
@ -255,6 +304,7 @@ const resolvers = {
|
|||
accountsConfig: () => accountsConfig,
|
||||
cryptoCurrencies: () => coins,
|
||||
machines: () => machineLoader.getMachineNames(),
|
||||
machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId),
|
||||
customers: () => customers.getCustomersList(),
|
||||
customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
|
||||
funding: () => funding.getFunding(),
|
||||
|
|
@ -273,18 +323,28 @@ const resolvers = {
|
|||
transactionsCsv: (...[, { from, until, limit, offset }]) =>
|
||||
transactions.batch(from, until, limit, offset).then(parseAsync),
|
||||
config: () => settingsLoader.loadLatestConfigOrNone(),
|
||||
accounts: () => settingsLoader.loadAccounts()
|
||||
accounts: () => settingsLoader.loadAccounts(),
|
||||
blacklist: () => blacklist.getBlacklist(),
|
||||
userTokens: () => tokenManager.getTokenList()
|
||||
},
|
||||
Mutation: {
|
||||
machineAction: (...[, { deviceId, action, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cassette1, cassette2, newName }),
|
||||
createPairingTotem: (...[, { name }]) => pairing.totem(name),
|
||||
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
|
||||
setCustomer: (...[, { customerId, customerInput } ]) => customers.updateCustomer(customerId, customerInput),
|
||||
setCustomer: (root, args, context, info) => {
|
||||
const token = context.req.cookies && context.req.cookies.token
|
||||
return customers.updateCustomer(args.customerId, args.customerInput, token)
|
||||
},
|
||||
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
|
||||
.then(it => {
|
||||
notify()
|
||||
return it
|
||||
})
|
||||
}),
|
||||
deleteBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.deleteFromBlacklist(cryptoCode, address),
|
||||
insertBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.insertIntoBlacklist(cryptoCode, address),
|
||||
revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@ function validateOTP (otp) {
|
|||
.catch(() => ({ success: false, expired: false }))
|
||||
}
|
||||
|
||||
function register (otp) {
|
||||
function register (otp, ua, ip) {
|
||||
return validateOTP(otp)
|
||||
.then(r => {
|
||||
if (!r.success) return r
|
||||
|
||||
const token = crypto.randomBytes(32).toString('hex')
|
||||
const sql = 'insert into user_tokens (token, name) values ($1, $2)'
|
||||
const sql = 'insert into user_tokens (token, name, user_agent, ip_address) values ($1, $2, $3, $4)'
|
||||
|
||||
return db.none(sql, [token, r.name])
|
||||
return db.none(sql, [token, r.name, ua, ip])
|
||||
.then(() => ({ success: true, token: token }))
|
||||
})
|
||||
.catch(() => ({ success: false, expired: false }))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const _ = require('lodash/fp')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
const db = require('../db')
|
||||
const machineLoader = require('../machine-loader')
|
||||
|
|
@ -65,9 +66,8 @@ function batch (from = new Date(0).toISOString(), until = new Date().toISOString
|
|||
.then(packager)
|
||||
}
|
||||
|
||||
function getCustomerTransactions (customerId) {
|
||||
function getCustomerTransactionsBatch (ids) {
|
||||
const packager = _.flow(it => {
|
||||
console.log()
|
||||
return it
|
||||
}, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ function getCustomerTransactions (customerId) {
|
|||
((not txs.send_confirmed) and (txs.created <= now() - interval $2)) as expired
|
||||
from cash_in_txs as txs
|
||||
left outer join customers c on txs.customer_id = c.id
|
||||
where c.id = $1
|
||||
where c.id IN ($1^)
|
||||
order by created desc limit $3`
|
||||
|
||||
const cashOutSql = `select 'cashOut' as tx_class,
|
||||
|
|
@ -100,14 +100,16 @@ function getCustomerTransactions (customerId) {
|
|||
inner join cash_out_actions actions on txs.id = actions.tx_id
|
||||
and actions.action = 'provisionAddress'
|
||||
left outer join customers c on txs.customer_id = c.id
|
||||
where c.id = $1
|
||||
where c.id IN ($1^)
|
||||
order by created desc limit $2`
|
||||
|
||||
return Promise.all([
|
||||
db.any(cashInSql, [customerId, cashInTx.PENDING_INTERVAL, NUM_RESULTS]),
|
||||
db.any(cashOutSql, [customerId, NUM_RESULTS, REDEEMABLE_AGE])
|
||||
db.any(cashInSql, [_.map(pgp.as.text, ids).join(','), cashInTx.PENDING_INTERVAL, NUM_RESULTS]),
|
||||
db.any(cashOutSql, [_.map(pgp.as.text, ids).join(','), NUM_RESULTS, REDEEMABLE_AGE])
|
||||
])
|
||||
.then(packager)
|
||||
.then(packager).then(transactions => {
|
||||
const transactionMap = _.groupBy('customerId', transactions)
|
||||
return ids.map(id => transactionMap[id])
|
||||
})
|
||||
}
|
||||
|
||||
function single (txId) {
|
||||
|
|
@ -156,4 +158,4 @@ function cancel (txId) {
|
|||
.then(() => single(txId))
|
||||
}
|
||||
|
||||
module.exports = { batch, getCustomerTransactions, single, cancel }
|
||||
module.exports = { batch, single, cancel, getCustomerTransactionsBatch }
|
||||
|
|
|
|||
|
|
@ -637,7 +637,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
|
||||
|
||||
const machineName = device.machineName
|
||||
const machineName = device.name
|
||||
|
||||
const cashInAlert = device.cashbox > notifications.cashInAlertThreshold
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
const _ = require('lodash/fp')
|
||||
const db = require('./db')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
function getInsertQuery (tableName, fields) {
|
||||
// outputs string like: '$1, $2, $3...' with proper No of items
|
||||
|
|
@ -48,6 +50,16 @@ exports.machineEvent = function machineEvent (rec) {
|
|||
.then(() => db.none(deleteSql))
|
||||
}
|
||||
|
||||
exports.machineEventsByIdBatch = function machineEventsByIdBatch (machineIds) {
|
||||
const formattedIds = _.map(pgp.as.text, machineIds).join(',')
|
||||
const sql = `SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events WHERE device_id IN ($1^) ORDER BY age ASC LIMIT 1`
|
||||
return db.any(sql, [formattedIds]).then(res => {
|
||||
const events = _.map(_.mapKeys(_.camelCase))(res)
|
||||
const eventMap = _.groupBy('deviceId', events)
|
||||
return machineIds.map(id => _.prop([0], eventMap[id]))
|
||||
})
|
||||
}
|
||||
|
||||
exports.machineEvents = function machineEvents () {
|
||||
const sql = 'SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events'
|
||||
|
||||
|
|
|
|||
13
lib/token-manager.js
Normal file
13
lib/token-manager.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const db = require('./db')
|
||||
|
||||
function getTokenList () {
|
||||
const sql = `select * from user_tokens`
|
||||
return db.any(sql)
|
||||
}
|
||||
|
||||
function revokeToken (token) {
|
||||
const sql = `delete from user_tokens where token = $1`
|
||||
return db.none(sql, [token])
|
||||
}
|
||||
|
||||
module.exports = { getTokenList, revokeToken }
|
||||
14
migrations/1603438527057-add-browser-os-info.js
Normal file
14
migrations/1603438527057-add-browser-os-info.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
'ALTER TABLE user_tokens ADD COLUMN user_agent text',
|
||||
'ALTER TABLE user_tokens ADD COLUMN ip_address inet',
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import {
|
||||
StylesProvider,
|
||||
jssPreset,
|
||||
|
|
@ -8,12 +9,18 @@ import {
|
|||
import { create } from 'jss'
|
||||
import extendJss from 'jss-plugin-extend'
|
||||
import React, { createContext, useContext, useState } from 'react'
|
||||
import { useLocation, BrowserRouter as Router } from 'react-router-dom'
|
||||
import {
|
||||
useLocation,
|
||||
useHistory,
|
||||
BrowserRouter as Router
|
||||
} from 'react-router-dom'
|
||||
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import ApolloProvider from 'src/utils/apollo'
|
||||
|
||||
import Header from './components/layout/Header'
|
||||
import { tree, Routes } from './routing/routes'
|
||||
import { tree, hasSidebar, Routes, getParent } from './routing/routes'
|
||||
import global from './styling/global'
|
||||
import theme from './styling/theme'
|
||||
import { backgroundColor, mainWidth } from './styling/variables'
|
||||
|
|
@ -46,6 +53,18 @@ const useStyles = makeStyles({
|
|||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection
|
||||
},
|
||||
grid: {
|
||||
flex: 1,
|
||||
height: '100%'
|
||||
},
|
||||
contentWithSidebar: {
|
||||
flex: 1,
|
||||
marginLeft: 48,
|
||||
paddingTop: 15
|
||||
},
|
||||
contentWithoutSidebar: {
|
||||
width: mainWidth
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -54,15 +73,45 @@ const AppContext = createContext()
|
|||
const Main = () => {
|
||||
const classes = useStyles()
|
||||
const location = useLocation()
|
||||
const history = useHistory()
|
||||
const { wizardTested } = useContext(AppContext)
|
||||
|
||||
const route = location.pathname
|
||||
|
||||
const sidebar = hasSidebar(route)
|
||||
const parent = sidebar ? getParent(route) : {}
|
||||
|
||||
const is404 = location.pathname === '/404'
|
||||
|
||||
const isSelected = it => location.pathname === it.route
|
||||
|
||||
const onClick = it => history.push(it.route)
|
||||
|
||||
const contentClassName = sidebar
|
||||
? classes.contentWithSidebar
|
||||
: classes.contentWithoutSidebar
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{!is404 && wizardTested && <Header tree={tree} />}
|
||||
<main className={classes.wrapper}>
|
||||
<Routes />
|
||||
{sidebar && !is404 && wizardTested && (
|
||||
<TitleSection title={parent.title}></TitleSection>
|
||||
)}
|
||||
|
||||
<Grid container className={classes.grid}>
|
||||
{sidebar && !is404 && wizardTested && (
|
||||
<Sidebar
|
||||
data={parent.children}
|
||||
isSelected={isSelected}
|
||||
displayName={it => it.label}
|
||||
onClick={onClick}
|
||||
/>
|
||||
)}
|
||||
<div className={contentClassName}>
|
||||
<Routes />
|
||||
</div>
|
||||
</Grid>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export const ConfirmDialog = memo(
|
|||
onConfirmed,
|
||||
onDissmised,
|
||||
initialValue = '',
|
||||
disabled = false,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
|
@ -101,6 +102,7 @@ export const ConfirmDialog = memo(
|
|||
<DialogContent className={classes.dialogContent}>
|
||||
{message && <P>{message}</P>}
|
||||
<TextInput
|
||||
disabled={disabled}
|
||||
label={confirmationMessage}
|
||||
name="confirm-input"
|
||||
autoFocus
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import classnames from 'classnames'
|
|||
import React, { memo, useState } from 'react'
|
||||
import { NavLink, useHistory } from 'react-router-dom'
|
||||
|
||||
import { Link } from 'src/components/buttons'
|
||||
import ActionButton from 'src/components/buttons/ActionButton'
|
||||
import { H4 } from 'src/components/typography'
|
||||
import AddMachine from 'src/pages/AddMachine'
|
||||
import { ReactComponent as AddIconReverse } from 'src/styling/icons/button/add/white.svg'
|
||||
import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg'
|
||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||
|
||||
import styles from './Header.styles'
|
||||
|
|
@ -76,9 +78,13 @@ const Header = memo(({ tree }) => {
|
|||
</NavLink>
|
||||
))}
|
||||
</ul>
|
||||
<Link color="action" onClick={() => setOpen(true)}>
|
||||
Add Machine
|
||||
</Link>
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
Icon={AddIcon}
|
||||
InverseIcon={AddIconReverse}
|
||||
onClick={() => setOpen(true)}>
|
||||
Add machine
|
||||
</ActionButton>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
181
new-lamassu-admin/src/pages/Blacklist/Blacklist.js
Normal file
181
new-lamassu-admin/src/pages/Blacklist/Blacklist.js
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { Box } from '@material-ui/core'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Tooltip from 'src/components/Tooltip'
|
||||
import { Link } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { H4, Label2, P } from 'src/components/typography'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
|
||||
import styles from './Blacklist.styles'
|
||||
import BlackListModal from './BlacklistModal'
|
||||
import BlacklistTable from './BlacklistTable'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const groupByCode = R.groupBy(obj => obj.cryptoCode)
|
||||
|
||||
const DELETE_ROW = gql`
|
||||
mutation DeleteBlacklistRow($cryptoCode: String!, $address: String!) {
|
||||
deleteBlacklistRow(cryptoCode: $cryptoCode, address: $address) {
|
||||
cryptoCode
|
||||
address
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const GET_BLACKLIST = gql`
|
||||
query getBlacklistData {
|
||||
blacklist {
|
||||
cryptoCode
|
||||
address
|
||||
}
|
||||
cryptoCurrencies {
|
||||
display
|
||||
code
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const SAVE_CONFIG = gql`
|
||||
mutation Save($config: JSONObject) {
|
||||
saveConfig(config: $config)
|
||||
}
|
||||
`
|
||||
|
||||
const GET_INFO = gql`
|
||||
query getData {
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const ADD_ROW = gql`
|
||||
mutation InsertBlacklistRow($cryptoCode: String!, $address: String!) {
|
||||
insertBlacklistRow(cryptoCode: $cryptoCode, address: $address) {
|
||||
cryptoCode
|
||||
address
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Blacklist = () => {
|
||||
const { data: blacklistResponse } = useQuery(GET_BLACKLIST)
|
||||
const { data: configData } = useQuery(GET_INFO)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [clickedItem, setClickedItem] = useState({
|
||||
code: 'BTC',
|
||||
display: 'Bitcoin'
|
||||
})
|
||||
const [deleteEntry] = useMutation(DELETE_ROW, {
|
||||
onError: () => console.error('Error while deleting row'),
|
||||
refetchQueries: () => ['getBlacklistData']
|
||||
})
|
||||
|
||||
const [addEntry] = useMutation(ADD_ROW, {
|
||||
onError: () => console.error('Error while adding row'),
|
||||
onCompleted: () => setShowModal(false),
|
||||
refetchQueries: () => ['getBlacklistData']
|
||||
})
|
||||
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
refetchQueries: () => ['getData']
|
||||
})
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? []
|
||||
const availableCurrencies =
|
||||
R.path(['cryptoCurrencies'], blacklistResponse) ?? []
|
||||
|
||||
const formattedData = groupByCode(blacklistData)
|
||||
|
||||
const complianceConfig =
|
||||
configData?.config && fromNamespace('compliance')(configData.config)
|
||||
|
||||
const rejectAddressReuse = complianceConfig?.rejectAddressReuse ?? false
|
||||
|
||||
const addressReuseSave = rawConfig => {
|
||||
const config = toNamespace('compliance')(rawConfig)
|
||||
return saveConfig({ variables: { config } })
|
||||
}
|
||||
|
||||
const onClickSidebarItem = e => {
|
||||
setClickedItem({ code: e.code, display: e.display })
|
||||
}
|
||||
|
||||
const handleDeleteEntry = (cryptoCode, address) => {
|
||||
deleteEntry({ variables: { cryptoCode, address } })
|
||||
}
|
||||
|
||||
const addToBlacklist = (cryptoCode, address) => {
|
||||
addEntry({ variables: { cryptoCode, address } })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="Blacklisted addresses">
|
||||
<Link onClick={() => setShowModal(false)}>Blacklist new addresses</Link>
|
||||
</TitleSection>
|
||||
<Grid container className={classes.grid}>
|
||||
<Sidebar
|
||||
data={availableCurrencies}
|
||||
isSelected={R.propEq('code', clickedItem.code)}
|
||||
displayName={it => it.display}
|
||||
onClick={onClickSidebarItem}
|
||||
/>
|
||||
<div className={classes.content}>
|
||||
<Box display="flex" justifyContent="space-between" mb={3}>
|
||||
<H4 noMargin className={classes.subtitle}>
|
||||
{clickedItem.display
|
||||
? `${clickedItem.display} blacklisted addresses`
|
||||
: ''}{' '}
|
||||
</H4>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="end"
|
||||
mr="-5px">
|
||||
<P>Reject reused addresses</P>
|
||||
<Switch
|
||||
checked={rejectAddressReuse}
|
||||
onChange={event => {
|
||||
addressReuseSave({ rejectAddressReuse: event.target.checked })
|
||||
}}
|
||||
value={rejectAddressReuse}
|
||||
/>
|
||||
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
||||
<Tooltip width={304}>
|
||||
<P>
|
||||
The "Reject reused addresses" option means that all addresses
|
||||
that are used once will be automatically rejected if there's
|
||||
an attempt to use them again on a new transaction.
|
||||
</P>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<BlacklistTable
|
||||
data={formattedData}
|
||||
selectedCoin={clickedItem}
|
||||
handleDeleteEntry={handleDeleteEntry}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
{showModal && (
|
||||
<BlackListModal
|
||||
onClose={() => setShowModal(false)}
|
||||
selectedCoin={clickedItem}
|
||||
addToBlacklist={addToBlacklist}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Blacklist
|
||||
39
new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js
Normal file
39
new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { spacer, fontPrimary, primaryColor, white } from 'src/styling/variables'
|
||||
|
||||
export default {
|
||||
grid: {
|
||||
flex: 1,
|
||||
height: '100%'
|
||||
},
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
marginLeft: spacer * 6
|
||||
},
|
||||
footer: {
|
||||
margin: [['auto', 0, spacer * 3, 'auto']]
|
||||
},
|
||||
modalTitle: {
|
||||
lineHeight: '120%',
|
||||
color: primaryColor,
|
||||
fontSize: 14,
|
||||
fontFamily: fontPrimary,
|
||||
fontWeight: 900
|
||||
},
|
||||
subtitle: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
white: {
|
||||
color: white
|
||||
},
|
||||
deleteButton: {
|
||||
paddingLeft: 13
|
||||
},
|
||||
addressRow: {
|
||||
marginLeft: 8
|
||||
}
|
||||
}
|
||||
76
new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js
Normal file
76
new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { Formik, Form, Field } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Link } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import { H3 } from 'src/components/typography'
|
||||
|
||||
import styles from './Blacklist.styles'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const BlackListModal = ({ onClose, selectedCoin, addToBlacklist }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const handleAddToBlacklist = address => {
|
||||
addToBlacklist(selectedCoin.code, address)
|
||||
}
|
||||
|
||||
const placeholderAddress = {
|
||||
BTC: '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD',
|
||||
ETH: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
|
||||
LTC: 'LPKvbjwV1Kaksktzkr7TMK3FQtQEEe6Wqa',
|
||||
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
|
||||
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
|
||||
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm'
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={676}
|
||||
height={200}
|
||||
handleClose={onClose}
|
||||
open={true}>
|
||||
<Formik
|
||||
initialValues={{
|
||||
address: ''
|
||||
}}
|
||||
validationSchema={Yup.object({
|
||||
address: Yup.string()
|
||||
.trim()
|
||||
.required('An address is required')
|
||||
})}
|
||||
onSubmit={({ address }, { resetForm }) => {
|
||||
handleAddToBlacklist(address)
|
||||
resetForm()
|
||||
}}>
|
||||
<Form id="address-form">
|
||||
<H3>
|
||||
{selectedCoin.display
|
||||
? `Blacklist ${R.toLower(selectedCoin.display)} address`
|
||||
: ''}
|
||||
</H3>
|
||||
<Field
|
||||
name="address"
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
label="Paste new address to blacklist here"
|
||||
placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`}
|
||||
component={TextInput}
|
||||
/>
|
||||
</Form>
|
||||
</Formik>
|
||||
<div className={classes.footer}>
|
||||
<Link type="submit" form="address-form">
|
||||
Blacklist address
|
||||
</Link>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlackListModal
|
||||
60
new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js
Normal file
60
new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { Label1 } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
|
||||
import styles from './Blacklist.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const BlacklistTable = ({ data, selectedCoin, handleDeleteEntry }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: 'address',
|
||||
header: <Label1 className={classes.white}>{'Addresses'}</Label1>,
|
||||
width: 800,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: it => (
|
||||
<div className={classes.addressRow}>
|
||||
<CopyToClipboard>{R.path(['address'], it)}</CopyToClipboard>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'deleteButton',
|
||||
header: <Label1 className={classes.white}>{'Delete'}</Label1>,
|
||||
width: 130,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: it => (
|
||||
<IconButton
|
||||
className={classes.deleteButton}
|
||||
onClick={() =>
|
||||
handleDeleteEntry(
|
||||
R.path(['cryptoCode'], it),
|
||||
R.path(['address'], it)
|
||||
)
|
||||
}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
]
|
||||
const dataToShow = selectedCoin
|
||||
? data[selectedCoin.code]
|
||||
: data[R.keys(data)[0]]
|
||||
|
||||
return (
|
||||
<DataTable data={dataToShow} elements={elements} name="blacklistTable" />
|
||||
)
|
||||
}
|
||||
|
||||
export default BlacklistTable
|
||||
3
new-lamassu-admin/src/pages/Blacklist/index.js
Normal file
3
new-lamassu-admin/src/pages/Blacklist/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Blacklist from './Blacklist'
|
||||
|
||||
export default Blacklist
|
||||
|
|
@ -89,7 +89,7 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
|||
<Tooltip width={304}>
|
||||
<P>
|
||||
Automatically accept customer deposits as complete if their
|
||||
received amount is 10 crypto atoms or less.
|
||||
received amount is 100 crypto atoms or less.
|
||||
</P>
|
||||
<P>
|
||||
(Crypto atoms are the smallest unit in each cryptocurrency. E.g.,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMutation } from '@apollo/react-hooks'
|
||||
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||
import { Grid, Divider } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import gql from 'graphql-tag'
|
||||
|
|
@ -32,6 +32,16 @@ const MACHINE_ACTION = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const MACHINE = gql`
|
||||
query getMachine($deviceId: ID!) {
|
||||
machine(deviceId: $deviceId) {
|
||||
latestEvent {
|
||||
note
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const supportArtices = [
|
||||
{
|
||||
// Default article for non-maped statuses
|
||||
|
|
@ -43,6 +53,24 @@ const supportArtices = [
|
|||
// TODO add Stuck and Fully Functional statuses articles for the new-admins
|
||||
]
|
||||
|
||||
const isStaticState = machineState => {
|
||||
if (!machineState) {
|
||||
return true
|
||||
}
|
||||
const staticStates = [
|
||||
'chooseCoin',
|
||||
'idle',
|
||||
'pendingIdle',
|
||||
'dualIdle',
|
||||
'networkDown',
|
||||
'unpaired',
|
||||
'maintenance',
|
||||
'virgin',
|
||||
'wifiList'
|
||||
]
|
||||
return staticStates.includes(machineState)
|
||||
}
|
||||
|
||||
const article = ({ code: status }) =>
|
||||
supportArtices.find(({ code: article }) => article === status)
|
||||
|
||||
|
|
@ -68,11 +96,37 @@ const Item = ({ children, ...props }) => (
|
|||
</Grid>
|
||||
)
|
||||
|
||||
const getState = machineEventsLazy =>
|
||||
JSON.parse(machineEventsLazy.machine.latestEvent?.note ?? '{"state": null}')
|
||||
.state
|
||||
|
||||
const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
||||
const [action, setAction] = useState(null)
|
||||
const [action, setAction] = useState({ command: null })
|
||||
const [errorMessage, setErrorMessage] = useState(null)
|
||||
const classes = useMDStyles()
|
||||
|
||||
const warningMessage = (
|
||||
<span className={classes.warning}>
|
||||
A user may be in the middle of a transaction and they could lose their
|
||||
funds if you continue.
|
||||
</span>
|
||||
)
|
||||
|
||||
const [fetchMachineEvents, { loading: loadingEvents }] = useLazyQuery(
|
||||
MACHINE,
|
||||
{
|
||||
variables: {
|
||||
deviceId: machine.deviceId
|
||||
},
|
||||
onCompleted: machineEventsLazy => {
|
||||
const message = !isStaticState(getState(machineEventsLazy))
|
||||
? warningMessage
|
||||
: null
|
||||
setAction(action => ({ ...action, message }))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const [machineAction, { loading }] = useMutation(MACHINE_ACTION, {
|
||||
onError: ({ message }) => {
|
||||
const errorMessage = message ?? 'An error ocurred'
|
||||
|
|
@ -80,11 +134,12 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
|||
},
|
||||
onCompleted: () => {
|
||||
onActionSuccess && onActionSuccess()
|
||||
setAction(null)
|
||||
setAction({ command: null })
|
||||
}
|
||||
})
|
||||
|
||||
const confirmDialogOpen = Boolean(action)
|
||||
const confirmDialogOpen = Boolean(action.command)
|
||||
const disabled = !!(action?.command === 'restartServices' && loadingEvents)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -127,25 +182,26 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
|||
className={classes.separator}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
disabled={disabled}
|
||||
open={confirmDialogOpen}
|
||||
title={`${action?.command} this machine?`}
|
||||
title={`${action?.display} this machine?`}
|
||||
errorMessage={errorMessage}
|
||||
toBeConfirmed={machine.name}
|
||||
message={action?.message}
|
||||
confirmationMessage={action?.confirmationMessage}
|
||||
saveButtonAlwaysEnabled={action?.command === 'Rename'}
|
||||
saveButtonAlwaysEnabled={action?.command === 'rename'}
|
||||
onConfirmed={value => {
|
||||
setErrorMessage(null)
|
||||
machineAction({
|
||||
variables: {
|
||||
deviceId: machine.deviceId,
|
||||
action: `${action?.command}`.toLowerCase(),
|
||||
...(action?.command === 'Rename' && { newName: value })
|
||||
action: `${action?.command}`,
|
||||
...(action?.command === 'rename' && { newName: value })
|
||||
}
|
||||
})
|
||||
}}
|
||||
onDissmised={() => {
|
||||
setAction(null)
|
||||
setAction({ command: null })
|
||||
setErrorMessage(null)
|
||||
}}
|
||||
/>
|
||||
|
|
@ -174,7 +230,8 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
|||
InverseIcon={EditReversedIcon}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'Rename',
|
||||
command: 'rename',
|
||||
display: 'Rename',
|
||||
confirmationMessage: 'Write the new name for this machine'
|
||||
})
|
||||
}>
|
||||
|
|
@ -188,7 +245,8 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
|||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'Unpair'
|
||||
command: 'unpair',
|
||||
display: 'Unpair'
|
||||
})
|
||||
}>
|
||||
Unpair
|
||||
|
|
@ -201,26 +259,43 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
|||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'Reboot'
|
||||
command: 'reboot',
|
||||
display: 'Reboot'
|
||||
})
|
||||
}>
|
||||
Reboot
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
className={classes.inlineChip}
|
||||
className={classes.mr}
|
||||
disabled={loading}
|
||||
color="primary"
|
||||
Icon={ShutdownIcon}
|
||||
InverseIcon={ShutdownReversedIcon}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'Shutdown',
|
||||
command: 'shutdown',
|
||||
display: 'Shutdown',
|
||||
message:
|
||||
'In order to bring it back online, the machine will need to be visited and its power reset.'
|
||||
})
|
||||
}>
|
||||
Shutdown
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.inlineChip}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
fetchMachineEvents()
|
||||
setAction({
|
||||
command: 'restartServices',
|
||||
display: 'Restart services for'
|
||||
})
|
||||
}}>
|
||||
Restart Services
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Item>
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ import {
|
|||
detailsRowStyles,
|
||||
labelStyles
|
||||
} from 'src/pages/Transactions/Transactions.styles'
|
||||
import { spacer, comet, primaryColor, fontSize4 } from 'src/styling/variables'
|
||||
import {
|
||||
spacer,
|
||||
comet,
|
||||
primaryColor,
|
||||
fontSize4,
|
||||
errorColor
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const machineDetailsStyles = {
|
||||
...detailsRowStyles,
|
||||
|
|
@ -58,6 +64,9 @@ const machineDetailsStyles = {
|
|||
marginRight: 60,
|
||||
marginLeft: 'auto',
|
||||
background: fade(comet, 0.5)
|
||||
},
|
||||
warning: {
|
||||
color: errorColor
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import Autocomplete from 'src/components/inputs/formik/Autocomplete'
|
|||
import NotificationsCtx from '../NotificationsContext'
|
||||
import { transformNumber } from '../helper'
|
||||
|
||||
const CASSETTE_1_KEY = 'cassette1'
|
||||
const CASSETTE_2_KEY = 'cassette2'
|
||||
const CASSETTE_1_KEY = 'fiatBalanceCassette1'
|
||||
const CASSETTE_2_KEY = 'fiatBalanceCassette2'
|
||||
const MACHINE_KEY = 'machine'
|
||||
const NAME = 'fiatBalanceOverrides'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import React from 'react'
|
||||
import {
|
||||
Route,
|
||||
Switch,
|
||||
Redirect,
|
||||
useLocation,
|
||||
useHistory
|
||||
} from 'react-router-dom'
|
||||
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
|
||||
import CoinAtmRadar from './CoinATMRadar'
|
||||
import ContactInfo from './ContactInfo'
|
||||
import ReceiptPrinting from './ReceiptPrinting'
|
||||
import TermsConditions from './TermsConditions'
|
||||
|
||||
const styles = {
|
||||
grid: {
|
||||
flex: 1,
|
||||
height: '100%'
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
marginLeft: 48,
|
||||
paddingTop: 15
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const innerRoutes = [
|
||||
{
|
||||
label: 'Contact information',
|
||||
route: '/settings/operator-info/contact-info',
|
||||
component: ContactInfo
|
||||
},
|
||||
{
|
||||
label: 'Receipt',
|
||||
route: '/settings/operator-info/receipt-printing',
|
||||
component: ReceiptPrinting
|
||||
},
|
||||
{
|
||||
label: 'Coin ATM Radar',
|
||||
route: '/settings/operator-info/coin-atm-radar',
|
||||
component: CoinAtmRadar
|
||||
},
|
||||
{
|
||||
label: 'Terms & Conditions',
|
||||
route: '/settings/operator-info/terms-conditions',
|
||||
component: TermsConditions
|
||||
}
|
||||
]
|
||||
|
||||
const Routes = ({ wizard }) => (
|
||||
<Switch>
|
||||
<Redirect
|
||||
exact
|
||||
from="/settings/operator-info"
|
||||
to="/settings/operator-info/contact-info"
|
||||
/>
|
||||
<Route exact path="/" />
|
||||
{innerRoutes.map(({ route, component: Page, key }) => (
|
||||
<Route path={route} key={key}>
|
||||
<Page name={key} wizard={wizard} />
|
||||
</Route>
|
||||
))}
|
||||
</Switch>
|
||||
)
|
||||
|
||||
const OperatorInfo = ({ wizard = false }) => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
|
||||
const isSelected = it => location.pathname === it.route
|
||||
|
||||
const onClick = it => history.push(it.route)
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="Operator information"></TitleSection>
|
||||
<Grid container className={classes.grid}>
|
||||
<Sidebar
|
||||
data={innerRoutes}
|
||||
isSelected={isSelected}
|
||||
displayName={it => it.label}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<div className={classes.content}>
|
||||
<Routes wizard={wizard} />
|
||||
</div>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OperatorInfo
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import gql from 'graphql-tag'
|
||||
import moment from 'moment'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
|
||||
const GET_USER_TOKENS = gql`
|
||||
query userTokens {
|
||||
userTokens {
|
||||
token
|
||||
name
|
||||
created
|
||||
user_agent
|
||||
ip_address
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const REVOKE_USER_TOKEN = gql`
|
||||
mutation revokeToken($token: String!) {
|
||||
revokeToken(token: $token) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Tokens = () => {
|
||||
const { data: tknResponse } = useQuery(GET_USER_TOKENS)
|
||||
|
||||
const [revokeToken] = useMutation(REVOKE_USER_TOKEN, {
|
||||
refetchQueries: () => ['userTokens']
|
||||
})
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Name',
|
||||
width: 257,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: t => t.name
|
||||
},
|
||||
{
|
||||
header: 'Token',
|
||||
width: 505,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: t => t.token
|
||||
},
|
||||
{
|
||||
header: 'Date (UTC)',
|
||||
width: 145,
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
view: t => moment.utc(t.created).format('YYYY-MM-DD')
|
||||
},
|
||||
{
|
||||
header: 'Time (UTC)',
|
||||
width: 145,
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
view: t => moment.utc(t.created).format('HH:mm:ss')
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
width: 80,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: t => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
revokeToken({ variables: { token: t.token } })
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="Token Management" />
|
||||
<DataTable
|
||||
elements={elements}
|
||||
data={R.path(['userTokens'])(tknResponse)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tokens
|
||||
|
|
@ -76,7 +76,10 @@ const threshold = Yup.object().shape({
|
|||
})
|
||||
const requirement = Yup.object().shape({
|
||||
requirement: Yup.string().required(),
|
||||
suspensionDays: Yup.number()
|
||||
suspensionDays: Yup.number().when('requirement', {
|
||||
is: 'suspend',
|
||||
then: Yup.number().required()
|
||||
})
|
||||
})
|
||||
|
||||
const Schema = Yup.object().shape({
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
|
||||
import { AppContext } from 'src/App'
|
||||
import AuthRegister from 'src/pages/AuthRegister'
|
||||
import Blacklist from 'src/pages/Blacklist'
|
||||
import Cashout from 'src/pages/Cashout'
|
||||
import Commissions from 'src/pages/Commissions'
|
||||
import { Customers, CustomerProfile } from 'src/pages/Customers'
|
||||
|
|
@ -19,9 +20,13 @@ import MachineLogs from 'src/pages/MachineLogs'
|
|||
import CashCassettes from 'src/pages/Maintenance/CashCassettes'
|
||||
import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||
import Notifications from 'src/pages/Notifications/Notifications'
|
||||
import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo'
|
||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||
import ServerLogs from 'src/pages/ServerLogs'
|
||||
import Services from 'src/pages/Services/Services'
|
||||
import TokenManagement from 'src/pages/TokenManagement/TokenManagement'
|
||||
import Transactions from 'src/pages/Transactions/Transactions'
|
||||
import Triggers from 'src/pages/Triggers'
|
||||
import WalletSettings from 'src/pages/Wallet/Wallet'
|
||||
|
|
@ -123,7 +128,36 @@ const tree = [
|
|||
key: namespaces.OPERATOR_INFO,
|
||||
label: 'Operator Info',
|
||||
route: '/settings/operator-info',
|
||||
component: OperatorInfo
|
||||
title: 'Operator Information',
|
||||
get component() {
|
||||
return () => <Redirect to={this.children[0].route} />
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: 'contact-info',
|
||||
label: 'Contact information',
|
||||
route: '/settings/operator-info/contact-info',
|
||||
component: ContactInfo
|
||||
},
|
||||
{
|
||||
key: 'receipt-printing',
|
||||
label: 'Receipt',
|
||||
route: '/settings/operator-info/receipt-printing',
|
||||
component: ReceiptPrinting
|
||||
},
|
||||
{
|
||||
key: 'coin-atm-radar',
|
||||
label: 'Coin ATM Radar',
|
||||
route: '/settings/operator-info/coin-atm-radar',
|
||||
component: CoinAtmRadar
|
||||
},
|
||||
{
|
||||
key: 'terms-conditions',
|
||||
label: 'Terms & Conditions',
|
||||
route: '/settings/operator-info/terms-conditions',
|
||||
component: TermsConditions
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -147,20 +181,66 @@ const tree = [
|
|||
route: '/compliance/customers',
|
||||
component: Customers
|
||||
},
|
||||
{
|
||||
key: 'blacklist',
|
||||
label: 'Blacklist',
|
||||
route: '/compliance/blacklist',
|
||||
component: Blacklist
|
||||
},
|
||||
{
|
||||
key: 'customer',
|
||||
route: '/compliance/customer/:id',
|
||||
component: CustomerProfile
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
label: 'System',
|
||||
route: '/system',
|
||||
get component() {
|
||||
return () => <Redirect to={this.children[0].route} />
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: 'token-management',
|
||||
label: 'Token Management',
|
||||
route: '/system/token-management',
|
||||
component: TokenManagement
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const map = R.map(R.when(R.has('children'), R.prop('children')))
|
||||
const leafRoutes = R.compose(R.flatten, map)(tree)
|
||||
const parentRoutes = R.filter(R.has('children'))(tree)
|
||||
const mappedRoutes = R.compose(R.flatten, map)(tree)
|
||||
const parentRoutes = R.filter(R.has('children'))(mappedRoutes).concat(
|
||||
R.filter(R.has('children'))(tree)
|
||||
)
|
||||
const leafRoutes = R.compose(R.flatten, map)(mappedRoutes)
|
||||
|
||||
const flattened = R.concat(leafRoutes, parentRoutes)
|
||||
|
||||
const hasSidebar = route =>
|
||||
R.any(r => r.route === route)(
|
||||
R.compose(
|
||||
R.flatten,
|
||||
R.map(R.prop('children')),
|
||||
R.filter(R.has('children'))
|
||||
)(mappedRoutes)
|
||||
)
|
||||
|
||||
const getParent = route =>
|
||||
R.find(
|
||||
R.propEq(
|
||||
'route',
|
||||
R.dropLast(
|
||||
1,
|
||||
R.dropLastWhile(x => x !== '/', route)
|
||||
)
|
||||
)
|
||||
)(flattened)
|
||||
|
||||
const Routes = () => {
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
|
|
@ -191,4 +271,4 @@ const Routes = () => {
|
|||
</Switch>
|
||||
)
|
||||
}
|
||||
export { tree, Routes }
|
||||
export { tree, getParent, hasSidebar, Routes }
|
||||
|
|
|
|||
13
new-lamassu-admin/src/styling/icons/menu/search-zodiac.svg
Normal file
13
new-lamassu-admin/src/styling/icons/menu/search-zodiac.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="nav-/-primary-/-1440" transform="translate(-1239.000000, -19.000000)" stroke="#1B2559" stroke-width="2">
|
||||
<g id="icon/menu/search" transform="translate(1240.000000, 20.000000)">
|
||||
<path d="M12.3100952,6.15542857 C12.3100952,9.55504762 9.55428571,12.3108571 6.15466667,12.3108571 C2.75580952,12.3108571 -2.72670775e-13,9.55504762 -2.72670775e-13,6.15542857 C-2.72670775e-13,2.75580952 2.75580952,8.08242362e-14 6.15466667,8.08242362e-14 C9.55428571,8.08242362e-14 12.3100952,2.75580952 12.3100952,6.15542857 Z" id="Stroke-1"></path>
|
||||
<line x1="10.5820952" y1="10.5829333" x2="15.2068571" y2="15.2076952" id="Stroke-3" stroke-linecap="round"></line>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4714
package-lock.json
generated
4714
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -23,6 +23,7 @@
|
|||
"console-log-level": "^1.4.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"cors": "^2.8.5",
|
||||
"dataloader": "^2.0.0",
|
||||
"ethereumjs-tx": "^1.3.3",
|
||||
"ethereumjs-util": "^5.2.0",
|
||||
"ethereumjs-wallet": "^0.6.3",
|
||||
|
|
@ -67,13 +68,15 @@
|
|||
"socket.io-client": "^2.0.3",
|
||||
"talisman": "^0.20.0",
|
||||
"twilio": "^3.6.1",
|
||||
"ua-parser-js": "^0.7.22",
|
||||
"uuid": "^3.1.0",
|
||||
"web3": "^0.20.6",
|
||||
"winston": "^2.4.2",
|
||||
"winston-transport": "^4.3.0",
|
||||
"ws": "^3.1.0",
|
||||
"xml-stream": "^0.4.5",
|
||||
"xmlrpc": "^1.3.2"
|
||||
"xmlrpc": "^1.3.2",
|
||||
"yup": "^0.31.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -107,6 +110,7 @@
|
|||
"scripts": {
|
||||
"start": "node bin/lamassu-server",
|
||||
"test": "mocha --recursive tests",
|
||||
"jtest": "jest",
|
||||
"build-admin": "npm run build-admin:css && npm run build-admin:main && npm run build-admin:lamassu",
|
||||
"server": "nodemon bin/lamassu-server --mockSms",
|
||||
"admin-server": "nodemon bin/lamassu-admin-server --dev",
|
||||
|
|
@ -121,6 +125,7 @@
|
|||
"devDependencies": {
|
||||
"ava": "3.8.2",
|
||||
"concurrently": "^5.3.0",
|
||||
"jest": "^26.6.3",
|
||||
"mocha": "^5.0.1",
|
||||
"nodemon": "^2.0.6",
|
||||
"rewire": "^4.0.1",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue