moved l-a-s in here
This commit is contained in:
parent
1e3e55e362
commit
836ab07776
18 changed files with 3946 additions and 281 deletions
123
lib/admin/accounts.js
Normal file
123
lib/admin/accounts.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
const R = require('ramda')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const options = require('../options')
|
||||
const db = require('../db')
|
||||
|
||||
const accountRoot = options.pluginPath
|
||||
const schemas = {}
|
||||
|
||||
function fetchSchemas () {
|
||||
const files = fs.readdirSync(accountRoot)
|
||||
|
||||
files.forEach(file => {
|
||||
if (file.indexOf('lamassu-') !== 0) return
|
||||
|
||||
try {
|
||||
const schema = JSON.parse(fs.readFileSync(path.resolve(accountRoot, file, 'schema.json')))
|
||||
schemas[schema.code] = schema
|
||||
} catch (_) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function fetchAccounts () {
|
||||
return db.oneOrNone('select data from user_config where type=$1', ['accounts'])
|
||||
.then(row => {
|
||||
return row
|
||||
? Promise.resolve(row.data.accounts)
|
||||
: db.none('insert into user_config (type, data) values ($1, $2)', ['accounts', {accounts: []}])
|
||||
.then(fetchAccounts)
|
||||
})
|
||||
}
|
||||
|
||||
function selectedAccounts () {
|
||||
const mapAccount = v => v.fieldLocator.fieldType === 'account' &&
|
||||
v.fieldValue.value
|
||||
|
||||
const mapSchema = code => schemas[code]
|
||||
return db.oneOrNone('select data from user_config where type=$1', ['config'])
|
||||
.then(row => row && row.data)
|
||||
.then(data => {
|
||||
if (!data) return []
|
||||
|
||||
const accountCodes = R.uniq(data.config.map(mapAccount)
|
||||
.filter(R.identity))
|
||||
|
||||
return R.sortBy(R.prop('display'), accountCodes.map(mapSchema)
|
||||
.filter(R.identity))
|
||||
})
|
||||
}
|
||||
|
||||
function fetchAccountSchema (account) {
|
||||
return schemas[account]
|
||||
}
|
||||
|
||||
function mergeAccount (oldAccount, newAccount) {
|
||||
if (!newAccount) return oldAccount
|
||||
|
||||
const newFields = newAccount.fields
|
||||
|
||||
const updateWithData = oldField => {
|
||||
const newField = R.find(r => r.code === oldField.code, newFields)
|
||||
const newValue = newField ? newField.value : null
|
||||
return R.assoc('value', newValue, oldField)
|
||||
}
|
||||
|
||||
const updatedFields = oldAccount.fields.map(updateWithData)
|
||||
|
||||
return R.assoc('fields', updatedFields, oldAccount)
|
||||
}
|
||||
|
||||
function getAccounts (accountCode) {
|
||||
const schema = fetchAccountSchema(accountCode)
|
||||
if (!schema) return Promise.reject(new Error('No schema for: ' + accountCode))
|
||||
|
||||
return fetchAccounts()
|
||||
.then(accounts => {
|
||||
if (R.isEmpty(accounts)) return [schema]
|
||||
const account = R.find(r => r.code === accountCode, accounts)
|
||||
const mergedAccount = mergeAccount(schema, account)
|
||||
|
||||
return updateAccounts(mergedAccount, accounts)
|
||||
})
|
||||
}
|
||||
|
||||
function getAccount (accountCode) {
|
||||
return getAccounts(accountCode)
|
||||
.then(accounts => R.find(r => r.code === accountCode, accounts))
|
||||
}
|
||||
|
||||
function save (accounts) {
|
||||
return db.none('update user_config set data=$1 where type=$2', [{accounts: accounts}, 'accounts'])
|
||||
}
|
||||
|
||||
function updateAccounts (newAccount, accounts) {
|
||||
const accountCode = newAccount.code
|
||||
const isPresent = R.any(R.propEq('code', accountCode), accounts)
|
||||
const updateAccount = r => r.code === accountCode
|
||||
? newAccount
|
||||
: r
|
||||
|
||||
return isPresent
|
||||
? R.map(updateAccount, accounts)
|
||||
: R.append(newAccount, accounts)
|
||||
}
|
||||
|
||||
function updateAccount (account) {
|
||||
return getAccounts(account.code)
|
||||
.then(accounts => {
|
||||
const merged = mergeAccount(R.find(R.propEq('code', account.code), accounts), account)
|
||||
return save(updateAccounts(merged, accounts))
|
||||
})
|
||||
.then(() => getAccount(account.code))
|
||||
}
|
||||
|
||||
fetchSchemas()
|
||||
|
||||
module.exports = {
|
||||
selectedAccounts,
|
||||
getAccount,
|
||||
updateAccount
|
||||
}
|
||||
262
lib/admin/config.js
Normal file
262
lib/admin/config.js
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
const pify = require('pify')
|
||||
const fs = pify(require('fs'))
|
||||
const path = require('path')
|
||||
const R = require('ramda')
|
||||
|
||||
const currencies = require('../../currencies.json')
|
||||
const languageRec = require('../../languages.json')
|
||||
|
||||
const db = require('../db')
|
||||
const options = require('../options')
|
||||
const configManager = require('../config-manager')
|
||||
|
||||
const machines = require('./machines')
|
||||
|
||||
function fetchSchema () {
|
||||
const schemaPath = path.resolve(options.lamassuServerPath, 'lamassu-schema.json')
|
||||
|
||||
return fs.readFile(schemaPath)
|
||||
.then(JSON.parse)
|
||||
}
|
||||
|
||||
function dbFetchConfig () {
|
||||
return db.oneOrNone('select data from user_config where type=$1', ['config'])
|
||||
.then(row => row && row.data)
|
||||
}
|
||||
|
||||
function allScopes (cryptoScopes, machineScopes) {
|
||||
const scopes = []
|
||||
cryptoScopes.forEach(c => {
|
||||
machineScopes.forEach(m => scopes.push([c, m]))
|
||||
})
|
||||
|
||||
return scopes
|
||||
}
|
||||
|
||||
function allCryptoScopes (cryptos, cryptoScope) {
|
||||
const cryptoScopes = []
|
||||
|
||||
if (cryptoScope === 'global' || cryptoScope === 'both') cryptoScopes.push('global')
|
||||
if (cryptoScope === 'specific' || cryptoScope === 'both') cryptos.forEach(r => cryptoScopes.push(r))
|
||||
|
||||
return cryptoScopes
|
||||
}
|
||||
|
||||
function allMachineScopes (machineList, machineScope) {
|
||||
const machineScopes = []
|
||||
|
||||
if (machineScope === 'global' || machineScope === 'both') machineScopes.push('global')
|
||||
if (machineScope === 'specific' || machineScope === 'both') machineList.forEach(r => machineScopes.push(r))
|
||||
|
||||
return machineScopes
|
||||
}
|
||||
|
||||
function satisfiesRequire (config, cryptos, machineList, field, refFields) {
|
||||
const fieldCode = field.code
|
||||
|
||||
const scopes = allScopes(
|
||||
allCryptoScopes(cryptos, field.cryptoScope),
|
||||
allMachineScopes(machineList, field.machineScope)
|
||||
)
|
||||
|
||||
return scopes.every(scope => {
|
||||
const isEnabled = () => refFields.some(refField => {
|
||||
return isScopeEnabled(config, cryptos, machineList, refField, scope)
|
||||
})
|
||||
|
||||
const isBlank = () => R.isNil(configManager.scopedValue(scope[0], scope[1], fieldCode, config))
|
||||
const isRequired = refFields.length === 0 || isEnabled()
|
||||
|
||||
return isRequired ? !isBlank() : true
|
||||
})
|
||||
}
|
||||
|
||||
function isScopeEnabled (config, cryptos, machineList, refField, scope) {
|
||||
const [cryptoScope, machineScope] = scope
|
||||
const candidateCryptoScopes = cryptoScope === 'global'
|
||||
? allCryptoScopes(cryptos, refField.cryptoScope)
|
||||
: [cryptoScope]
|
||||
|
||||
const candidateMachineScopes = machineScope === 'global'
|
||||
? allMachineScopes(machineList, refField.machineScope)
|
||||
: [ machineScope ]
|
||||
|
||||
const allRefCandidateScopes = allScopes(candidateCryptoScopes, candidateMachineScopes)
|
||||
const getFallbackValue = scope => configManager.scopedValue(scope[0], scope[1], refField.code, config)
|
||||
const values = allRefCandidateScopes.map(getFallbackValue)
|
||||
|
||||
return values.some(r => r)
|
||||
}
|
||||
|
||||
function getCryptos (config, machineList) {
|
||||
const scopes = allScopes(['global'], allMachineScopes(machineList, 'both'))
|
||||
const scoped = scope => configManager.scopedValue(scope[0], scope[1], 'cryptoCurrencies', config)
|
||||
return scopes.reduce((acc, scope) => R.union(acc, scoped(scope)), [])
|
||||
}
|
||||
|
||||
function getGroup (schema, fieldCode) {
|
||||
return schema.groups.find(group => group.fields.find(R.equals(fieldCode)))
|
||||
}
|
||||
|
||||
function getField (schema, group, fieldCode) {
|
||||
if (!group) group = getGroup(schema, fieldCode)
|
||||
const field = schema.fields.find(r => r.code === fieldCode)
|
||||
return R.merge(R.pick(['cryptoScope', 'machineScope'], group), field)
|
||||
}
|
||||
|
||||
const fetchMachines = () => machines.getMachines()
|
||||
.then(machineList => machineList.map(r => r.deviceId))
|
||||
|
||||
function validateConfig () {
|
||||
return Promise.all([fetchSchema(), dbFetchConfig(), fetchMachines()])
|
||||
.then(([schema, configRec, machineList]) => {
|
||||
const config = configRec ? configRec.config : []
|
||||
const cryptos = getCryptos(config, machineList)
|
||||
return schema.groups.filter(group => {
|
||||
return group.fields.some(fieldCode => {
|
||||
const field = getField(schema, group, fieldCode)
|
||||
if (!field.fieldValidation.find(r => r.code === 'required')) return false
|
||||
|
||||
const refFields = (field.enabledIf || []).map(R.curry(getField)(schema, null))
|
||||
return !satisfiesRequire(config, cryptos, machineList, field, refFields)
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(arr => arr.map(r => r.code))
|
||||
}
|
||||
|
||||
function fetchConfigGroup (code) {
|
||||
const fieldLocatorCodeEq = R.pathEq(['fieldLocator', 'code'])
|
||||
return Promise.all([fetchSchema(), fetchData(), dbFetchConfig(), fetchMachines()])
|
||||
.then(([schema, data, config, machineList]) => {
|
||||
const configValues = config ? config.config : []
|
||||
const groupSchema = schema.groups.find(r => r.code === code)
|
||||
|
||||
if (!groupSchema) throw new Error('No such group schema: ' + code)
|
||||
|
||||
const schemaFields = groupSchema.fields
|
||||
.map(R.curry(getField)(schema, groupSchema))
|
||||
|
||||
const candidateFields = [
|
||||
schemaFields.map(R.prop('requiredIf')),
|
||||
schemaFields.map(R.prop('enabledIf')),
|
||||
groupSchema.fields
|
||||
]
|
||||
const configFields = R.uniq(R.flatten(candidateFields)).filter(R.identity)
|
||||
|
||||
const values = configFields
|
||||
.reduce((acc, configField) => acc.concat(configValues.filter(fieldLocatorCodeEq(configField))), [])
|
||||
|
||||
groupSchema.fields = undefined
|
||||
groupSchema.entries = schemaFields
|
||||
|
||||
return {
|
||||
schema: groupSchema,
|
||||
values: values,
|
||||
selectedCryptos: getCryptos(config.config, machineList),
|
||||
data: data
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function massageCurrencies (currencies) {
|
||||
const convert = r => ({
|
||||
code: r['Alphabetic Code'],
|
||||
display: r['Currency']
|
||||
})
|
||||
const top5Codes = ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
|
||||
const mapped = R.map(convert, currencies)
|
||||
const codeToRec = code => R.find(R.propEq('code', code), mapped)
|
||||
const top5 = R.map(codeToRec, top5Codes)
|
||||
const raw = R.uniqBy(R.prop('code'), R.concat(top5, mapped))
|
||||
return raw.filter(r => r.code[0] !== 'X' && r.display.indexOf('(') === -1)
|
||||
}
|
||||
|
||||
const mapLanguage = lang => {
|
||||
const arr = lang.split('-')
|
||||
const code = arr[0]
|
||||
const country = arr[1]
|
||||
const langNameArr = languageRec.lang[code]
|
||||
if (!langNameArr) return null
|
||||
const langName = langNameArr[0]
|
||||
if (!country) return {code: lang, display: langName}
|
||||
return {code: lang, display: `${langName} [${country}]`}
|
||||
}
|
||||
|
||||
const supportedLanguages = languageRec.supported
|
||||
const languages = supportedLanguages.map(mapLanguage).filter(r => r)
|
||||
|
||||
function fetchData () {
|
||||
return machines.getMachines()
|
||||
.then(machineList => ({
|
||||
currencies: massageCurrencies(currencies),
|
||||
cryptoCurrencies: [{crypto: 'BTC', display: 'Bitcoin'}, {crypto: 'ETH', display: 'Ethereum'}],
|
||||
languages: languages,
|
||||
accounts: [
|
||||
{code: 'bitpay', display: 'Bitpay', class: 'ticker', cryptos: ['BTC']},
|
||||
{code: 'kraken', display: 'Kraken', class: 'ticker', cryptos: ['BTC', 'ETH']},
|
||||
{code: 'bitstamp', display: 'Bitstamp', class: 'ticker', cryptos: ['BTC']},
|
||||
{code: 'coinbase', display: 'Coinbase', class: 'ticker', cryptos: ['BTC', 'ETH']},
|
||||
{code: 'bitcoind', display: 'bitcoind', class: 'wallet', cryptos: ['BTC']},
|
||||
{code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']},
|
||||
{code: 'geth', display: 'geth', class: 'wallet', cryptos: ['ETH']},
|
||||
{code: 'mock-wallet', display: 'Mock wallet', class: 'wallet', cryptos: ['BTC', 'ETH']},
|
||||
{code: 'mock-sms', display: 'Mock SMS', class: 'sms'},
|
||||
{code: 'mock-id-verify', display: 'Mock ID verifier', class: 'idVerifier'},
|
||||
{code: 'twilio', display: 'Twilio', class: 'sms'},
|
||||
{code: 'mailjet', display: 'Mailjet', class: 'email'}
|
||||
],
|
||||
machines: machineList.map(machine => ({machine: machine.deviceId, display: machine.name}))
|
||||
}))
|
||||
}
|
||||
|
||||
function dbSaveConfig (config) {
|
||||
return db.none('update user_config set data=$1 where type=$2', [config, 'config'])
|
||||
}
|
||||
|
||||
function saveConfigGroup (results) {
|
||||
return dbFetchConfig()
|
||||
.then(config => {
|
||||
return config
|
||||
? Promise.resolve(config)
|
||||
: db.none('insert into user_config (type, data) values ($1, $2)', ['config', {config: []}])
|
||||
.then(dbFetchConfig)
|
||||
})
|
||||
.then(config => {
|
||||
const oldValues = config.config
|
||||
|
||||
results.values.forEach(newValue => {
|
||||
const oldValueIndex = oldValues
|
||||
.findIndex(old => old.fieldLocator.code === newValue.fieldLocator.code &&
|
||||
old.fieldLocator.fieldScope.crypto === newValue.fieldLocator.fieldScope.crypto &&
|
||||
old.fieldLocator.fieldScope.machine === newValue.fieldLocator.fieldScope.machine
|
||||
)
|
||||
|
||||
const existingValue = oldValueIndex > -1 &&
|
||||
oldValues[oldValueIndex]
|
||||
|
||||
if (existingValue) {
|
||||
// Delete value record
|
||||
if (R.isNil(newValue.fieldValue)) {
|
||||
oldValues.splice(oldValueIndex, 1)
|
||||
return
|
||||
}
|
||||
|
||||
existingValue.fieldValue = newValue.fieldValue
|
||||
return
|
||||
}
|
||||
|
||||
if (!R.isNil(newValue.fieldValue)) oldValues.push(newValue)
|
||||
})
|
||||
|
||||
return dbSaveConfig(config)
|
||||
.then(() => fetchConfigGroup(results.groupCode))
|
||||
})
|
||||
.catch(e => console.error(e.stack))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchConfigGroup,
|
||||
saveConfigGroup,
|
||||
validateConfig
|
||||
}
|
||||
48
lib/admin/login.js
Normal file
48
lib/admin/login.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
const crypto = require('crypto')
|
||||
|
||||
const db = require('../db')
|
||||
|
||||
function generateOTP (name) {
|
||||
const otp = crypto.randomBytes(32).toString('hex')
|
||||
|
||||
const sql = 'insert into one_time_passes (token, name) values ($1, $2)'
|
||||
|
||||
return db.none(sql, [otp, name])
|
||||
.then(() => otp)
|
||||
}
|
||||
|
||||
function validateOTP (otp) {
|
||||
const sql = `delete from one_time_passes
|
||||
where token=$1
|
||||
returning name, created < now() - interval '1 hour' as expired`
|
||||
|
||||
return db.one(sql, [otp])
|
||||
.then(r => ({success: !r.expired, expired: r.expired, name: r.name}))
|
||||
.catch(() => ({success: false, expired: false}))
|
||||
}
|
||||
|
||||
function register (otp) {
|
||||
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)'
|
||||
|
||||
return db.none(sql, [token, r.name])
|
||||
.then(() => ({success: true, token: token}))
|
||||
})
|
||||
.catch(() => ({success: false, expired: false}))
|
||||
}
|
||||
|
||||
function authenticate (token) {
|
||||
const sql = 'select token from user_tokens where token=$1'
|
||||
|
||||
return db.one(sql, [token]).then(() => true).catch(() => false)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateOTP,
|
||||
register,
|
||||
authenticate
|
||||
}
|
||||
26
lib/admin/machines.js
Normal file
26
lib/admin/machines.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const db = require('../db')
|
||||
|
||||
function getMachines () {
|
||||
return db.any('select * from devices where display=TRUE order by name')
|
||||
.then(rr => rr.map(r => ({
|
||||
deviceId: r.device_id,
|
||||
name: r.name,
|
||||
cashbox: r.cashbox,
|
||||
cassette1: r.cassette1,
|
||||
cassette2: r.cassette2,
|
||||
paired: r.paired
|
||||
})))
|
||||
}
|
||||
|
||||
function resetCashOutBills (rec) {
|
||||
const sql = 'update devices set cassette1=$1, cassette2=$2 where device_id=$3'
|
||||
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId])
|
||||
}
|
||||
|
||||
function setMachine (rec) {
|
||||
switch (rec.action) {
|
||||
case 'resetCashOutBills': return resetCashOutBills(rec)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {getMachines, setMachine}
|
||||
32
lib/admin/pairing.js
Normal file
32
lib/admin/pairing.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
const fs = require('fs')
|
||||
const pify = require('pify')
|
||||
const readFile = pify(fs.readFile)
|
||||
const crypto = require('crypto')
|
||||
|
||||
const options = require('../options')
|
||||
const db = require('../db')
|
||||
|
||||
function unpair (deviceId) {
|
||||
const sql = 'update devices set paired=FALSE where device_id=$1'
|
||||
|
||||
return db.none(sql, [deviceId])
|
||||
}
|
||||
|
||||
function totem (hostname, name) {
|
||||
const caPath = options.caPath
|
||||
|
||||
return readFile(caPath)
|
||||
.then(data => {
|
||||
const caHash = crypto.createHash('sha256').update(data).digest()
|
||||
const token = crypto.randomBytes(32)
|
||||
const hexToken = token.toString('hex')
|
||||
const caHexToken = crypto.createHash('sha256').update(hexToken).digest('hex')
|
||||
const buf = Buffer.concat([caHash, token, Buffer.from(hostname)])
|
||||
const sql = 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)'
|
||||
|
||||
return db.none(sql, [hexToken, caHexToken, name])
|
||||
.then(() => buf.toString('base64'))
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {totem, unpair}
|
||||
26
lib/admin/server.js
Normal file
26
lib/admin/server.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const moment = require('moment')
|
||||
|
||||
const db = require('../db')
|
||||
|
||||
const CONSIDERED_UP = 30000
|
||||
|
||||
function status () {
|
||||
const sql = `select extract(epoch from (now() - created)) as age
|
||||
from server_events
|
||||
where event_type=$1
|
||||
order by created desc
|
||||
limit 1`
|
||||
|
||||
return db.oneOrNone(sql, ['ping'])
|
||||
.then(row => {
|
||||
if (!row) return {up: false, lastPing: null}
|
||||
|
||||
const age = moment.duration(row.age, 'seconds')
|
||||
const up = age.asMilliseconds() < CONSIDERED_UP
|
||||
const lastPing = age.humanize()
|
||||
|
||||
return {up, lastPing}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {status}
|
||||
25
lib/admin/transactions.js
Normal file
25
lib/admin/transactions.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
const db = require('../db')
|
||||
|
||||
const NUM_RESULTS = 20
|
||||
|
||||
function batch () {
|
||||
const camelize = _.mapKeys(_.camelCase)
|
||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.take(NUM_RESULTS), _.map(camelize))
|
||||
|
||||
const cashInSql = `select 'cashIn' as tx_class, devices.name as machine_name, cash_in_txs.*
|
||||
from cash_in_txs, devices
|
||||
where devices.device_id=cash_in_txs.device_id
|
||||
order by created desc limit $1`
|
||||
|
||||
const cashOutSql = `select 'cashOut' as tx_class, devices.name as machine_name, cash_out_txs.*
|
||||
from cash_out_txs, devices
|
||||
where devices.device_id=cash_out_txs.device_id
|
||||
order by created desc limit $1`
|
||||
|
||||
return Promise.all([db.any(cashInSql, [NUM_RESULTS]), db.any(cashOutSql, [NUM_RESULTS])])
|
||||
.then(packager)
|
||||
}
|
||||
|
||||
module.exports = {batch}
|
||||
Loading…
Add table
Add a link
Reference in a new issue