diff --git a/bin/lamassu-register b/bin/lamassu-register index 1e73f714..1fbb98a4 100755 --- a/bin/lamassu-register +++ b/bin/lamassu-register @@ -1,5 +1,6 @@ #!/usr/bin/env node +const { asyncLocalStorage, defaultStore } = require('../lib/async-storage') const authentication = require('../lib/new-admin/graphql/modules/authentication') const options = require('../lib/options') @@ -29,20 +30,22 @@ if (role !== 'user' && role !== 'superuser') { process.exit(2) } -authentication.createRegisterToken(name, role).then(token => { - if (!token) { - console.log(`A user named ${name} already exists!`) - process.exit(2) - } +asyncLocalStorage.run(defaultStore(), () => { + authentication.createRegisterToken(name, role).then(token => { + if (!token) { + console.log(`A user named ${name} already exists!`) + process.exit(2) + } - if (domain === 'localhost') { - console.log(`https://${domain}:3001/register?t=${token.token}`) - } else { - console.log(`https://${domain}/register?t=${token.token}`) - } + if (domain === 'localhost') { + console.log(`https://${domain}:3001/register?t=${token.token}`) + } else { + console.log(`https://${domain}/register?t=${token.token}`) + } - process.exit(0) -}).catch(err => { - console.log('Error: %s', err) - process.exit(3) + process.exit(0) + }).catch(err => { + console.log('Error: %s', err) + process.exit(3) + }) }) diff --git a/lib/app.js b/lib/app.js index 14ace5aa..72394605 100644 --- a/lib/app.js +++ b/lib/app.js @@ -64,8 +64,7 @@ function loadSanctions (settings) { function startServer (settings) { return Promise.resolve() .then(() => { - poller.start(settings) - + poller.setup(['public']) const httpsServerOptions = { key: fs.readFileSync(options.keyPath), cert: fs.readFileSync(options.certPath), diff --git a/lib/compute-schema.js b/lib/compute-schema.js new file mode 100644 index 00000000..e065281c --- /dev/null +++ b/lib/compute-schema.js @@ -0,0 +1,8 @@ +const { asyncLocalStorage, defaultStore } = require('./async-storage') + +const computeSchema = (req, res, next) => { + const store = defaultStore() + return asyncLocalStorage.run(store, () => next()) +} + +module.exports = computeSchema diff --git a/lib/constants.js b/lib/constants.js index aa7319fa..9e526ce6 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,3 +1,5 @@ +const T = require('./time') + const anonymousCustomer = { uuid: '47ac1184-8102-11e7-9079-8f13a7117867', name: 'anonymous' @@ -8,6 +10,8 @@ const cassetteMaxCapacity = 500 const AUTHENTICATOR_ISSUER_ENTITY = 'Lamassu' const AUTH_TOKEN_EXPIRATION_TIME = '30 minutes' const REGISTRATION_TOKEN_EXPIRATION_TIME = '30 minutes' +const USER_SESSIONS_TABLE_NAME = 'user_sessions' +const USER_SESSIONS_CLEAR_INTERVAL = 1 * T.hour const AUTOMATIC = 'automatic' const MANUAL = 'manual' @@ -19,5 +23,7 @@ module.exports = { AUTH_TOKEN_EXPIRATION_TIME, REGISTRATION_TOKEN_EXPIRATION_TIME, AUTOMATIC, - MANUAL + MANUAL, + USER_SESSIONS_TABLE_NAME, + USER_SESSIONS_CLEAR_INTERVAL } diff --git a/lib/db.js b/lib/db.js index 55c65983..43c25537 100644 --- a/lib/db.js +++ b/lib/db.js @@ -21,7 +21,8 @@ const stripDefaultDbFuncs = dbCtx => { tx: dbCtx.$tx, task: dbCtx.$task, batch: dbCtx.batch, - multi: dbCtx.$multi + multi: dbCtx.$multi, + connect: dbCtx.connect } } @@ -37,7 +38,10 @@ const _task = (obj, opts, cb) => { }) } -const getSchema = () => 'public' +const getSchema = () => { + const store = asyncLocalStorage.getStore() ?? defaultStore() + return asyncLocalStorage.run(store, () => store.get('schema')) +} const getDefaultSchema = () => 'ERROR_SCHEMA' const searchPathWrapper = (t, cb) => { diff --git a/lib/machine-loader.js b/lib/machine-loader.js index 41838bde..d3c6b7a3 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -157,18 +157,37 @@ function unpair (rec) { } function reboot (rec) { - return axios.post(`http://localhost:3030/reboot?device_id=${rec.deviceId}`) + return db.none('NOTIFY $1:name, $2', ['poller', JSON.stringify( + { + type: 'machineAction', + action: 'reboot', + value: _.pick(['deviceId', 'operatorId', 'action'], rec) + } + )]) } function shutdown (rec) { - return axios.post(`http://localhost:3030/shutdown?device_id=${rec.deviceId}`) + return db.none('NOTIFY $1:name, $2', ['poller', JSON.stringify( + { + type: 'machineAction', + action: 'shutdown', + value: _.pick(['deviceId', 'operatorId', 'action'], rec) + } + )]) } function restartServices (rec) { - return axios.post(`http://localhost:3030/restartServices?device_id=${rec.deviceId}`) + return db.none('NOTIFY $1:name, $2', ['poller', JSON.stringify( + { + type: 'machineAction', + action: 'restartServices', + value: _.pick(['deviceId', 'operatorId', 'action'], rec) + } + )]) } -function setMachine (rec) { +function setMachine (rec, operatorId) { + rec.operatorId = operatorId switch (rec.action) { case 'rename': return renameMachine(rec) case 'emptyCashInBills': return emptyCashInBills(rec) diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index b666333f..b1ccda5d 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -16,9 +16,12 @@ const options = require('../options') const users = require('../users') const logger = require('../logger') -const session = require('./middlewares/session') const { AuthDirective } = require('./graphql/directives') const { typeDefs, resolvers } = require('./graphql/schema') +const findOperatorId = require('../middlewares/operatorId') +const computeSchema = require('../compute-schema') +const { USER_SESSIONS_CLEAR_INTERVAL } = require('../constants') +const { session, cleanUserSessions, buildApolloContext } = require('./middlewares') const devMode = require('minimist')(process.argv.slice(2)).dev const idPhotoCardBasedir = _.get('idPhotoCardDir', options) @@ -32,6 +35,7 @@ if (!hostname) { } const app = express() + app.use(helmet()) app.use(compression()) app.use(nocache()) @@ -39,6 +43,9 @@ app.use(cookieParser()) app.use(express.json()) app.use(express.urlencoded({ extended: true })) // support encoded bodies app.use(express.static(path.resolve(__dirname, '..', '..', 'public'))) +app.use(cleanUserSessions(USER_SESSIONS_CLEAR_INTERVAL)) +app.use(computeSchema) +app.use(findOperatorId) app.use(session) const apolloServer = new ApolloServer({ @@ -53,27 +60,7 @@ const apolloServer = new ApolloServer({ logger.error(error) return error }, - context: async ({ req, res }) => { - if (!req.session.user) return { req } - - const user = await users.verifyAndUpdateUser( - req.session.user.id, - req.headers['user-agent'] || 'Unknown', - req.ip - ) - if (!user || !user.enabled) throw new AuthenticationError('Authentication failed') - - req.session.ua = req.headers['user-agent'] || 'Unknown' - req.session.ipAddress = req.ip - req.session.lastUsed = new Date(Date.now()).toISOString() - req.session.user.id = user.id - req.session.user.role = user.role - - res.set('role', user.role) - res.set('Access-Control-Expose-Headers', 'role') - - return { req } - } + context: async (obj) => buildApolloContext(obj) }) apolloServer.applyMiddleware({ diff --git a/lib/new-admin/graphql/modules/authentication.js b/lib/new-admin/graphql/modules/authentication.js index 798e6aa1..c5be75b6 100644 --- a/lib/new-admin/graphql/modules/authentication.js +++ b/lib/new-admin/graphql/modules/authentication.js @@ -30,8 +30,7 @@ const authenticateUser = (username, password) => { const destroySessionIfSameUser = (context, user) => { const sessionUser = getUserFromCookie(context) - if (sessionUser && user.id === sessionUser.id) - context.req.session.destroy() + if (sessionUser && user.id === sessionUser.id) { context.req.session.destroy() } } const destroySessionIfBeingUsed = (sessID, context) => { @@ -45,7 +44,7 @@ const getUserFromCookie = context => { } const getLamassuCookie = context => { - return context.req.cookies && context.req.cookies.lid + return context.req.cookies && context.req.cookies.lamassu_sid } const initializeSession = (context, user, rememberMe) => { @@ -60,7 +59,7 @@ const executeProtectedAction = (code, id, context, action) => { if (user.role !== 'superuser') { return action() } - + return confirm2FA(code, context) .then(() => action()) }) diff --git a/lib/new-admin/graphql/resolvers/customer.resolver.js b/lib/new-admin/graphql/resolvers/customer.resolver.js index f33c3276..6863e8b3 100644 --- a/lib/new-admin/graphql/resolvers/customer.resolver.js +++ b/lib/new-admin/graphql/resolvers/customer.resolver.js @@ -13,7 +13,7 @@ const resolvers = { }, Mutation: { setCustomer: (root, { customerId, customerInput }, context, info) => { - const token = !!context.req.cookies.lid && context.req.session.user.id + const token = !!context.req.cookies.lamassu_sid && context.req.session.user.id if (customerId === anonymous.uuid) return customers.getCustomerById(customerId) return customers.updateCustomer(customerId, customerInput, token) } diff --git a/lib/new-admin/graphql/resolvers/machine.resolver.js b/lib/new-admin/graphql/resolvers/machine.resolver.js index c2e5bc2c..771bb479 100644 --- a/lib/new-admin/graphql/resolvers/machine.resolver.js +++ b/lib/new-admin/graphql/resolvers/machine.resolver.js @@ -18,7 +18,7 @@ const resolvers = { machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId) }, Mutation: { - machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }) + machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, newName }, context]) => machineAction({ deviceId, action, cashbox, cassette1, cassette2, newName }, context) } } diff --git a/lib/new-admin/graphql/resolvers/settings.resolver.js b/lib/new-admin/graphql/resolvers/settings.resolver.js index 329be89e..383f9309 100644 --- a/lib/new-admin/graphql/resolvers/settings.resolver.js +++ b/lib/new-admin/graphql/resolvers/settings.resolver.js @@ -1,11 +1,5 @@ -const got = require('got') - -const logger = require('../../../logger') const settingsLoader = require('../../../new-settings-loader') -const notify = () => got.post('http://localhost:3030/dbChange') - .catch(e => logger.error('lamassu-server not responding')) - const resolvers = { Query: { accounts: () => settingsLoader.showAccounts(), @@ -14,10 +8,7 @@ const resolvers = { Mutation: { saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts), // resetAccounts: (...[, { schemaVersion }]) => settingsLoader.resetAccounts(schemaVersion), - saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config).then(it => { - notify() - return it - }), + saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config), // resetConfig: (...[, { schemaVersion }]) => settingsLoader.resetConfig(schemaVersion), // migrateConfigAndAccounts: () => settingsLoader.migrate() } diff --git a/lib/new-admin/middlewares/cleanUserSessions.js b/lib/new-admin/middlewares/cleanUserSessions.js new file mode 100644 index 00000000..2f4e10fe --- /dev/null +++ b/lib/new-admin/middlewares/cleanUserSessions.js @@ -0,0 +1,24 @@ +const { asyncLocalStorage } = require('../../async-storage') +const db = require('../../db') +const { USER_SESSIONS_TABLE_NAME } = require('../../constants') +const logger = require('../../logger') + +const schemaCache = {} + +const cleanUserSessions = (cleanInterval) => (req, res, next) => { + const schema = asyncLocalStorage.getStore() ? asyncLocalStorage.getStore().get('schema') : null + const now = Date.now() + + if (!schema) return next() + if (schema && schemaCache.schema + cleanInterval > now) return next() + + logger.debug(`Clearing expired sessions for schema ${schema}`) + return db.none('DELETE FROM $1^ WHERE expire < to_timestamp($2 / 1000.0)', [USER_SESSIONS_TABLE_NAME, now]) + .then(() => { + schemaCache.schema = now + return next() + }) + .catch(next) +} + +module.exports = cleanUserSessions diff --git a/lib/new-admin/middlewares/context.js b/lib/new-admin/middlewares/context.js new file mode 100644 index 00000000..70e7d62f --- /dev/null +++ b/lib/new-admin/middlewares/context.js @@ -0,0 +1,29 @@ +const { AuthenticationError } = require('apollo-server-express') +const base64 = require('base-64') +const users = require('../../users') + +const buildApolloContext = async ({ req, res }) => { + if (!req.session.user) return { req, res } + + const user = await users.verifyAndUpdateUser( + req.session.user.id, + req.headers['user-agent'] || 'Unknown', + req.ip + ) + if (!user || !user.enabled) throw new AuthenticationError('Authentication failed') + + req.session.ua = req.headers['user-agent'] || 'Unknown' + req.session.ipAddress = req.ip + req.session.lastUsed = new Date(Date.now()).toISOString() + req.session.user.id = user.id + req.session.user.username = user.username + req.session.user.role = user.role + + res.set('lamassu_role', user.role) + res.cookie('pazuz_operatoridentifier', base64.encode(user.username)) + res.set('Access-Control-Expose-Headers', 'lamassu_role') + + return { req, res } +} + +module.exports = buildApolloContext diff --git a/lib/new-admin/middlewares/index.js b/lib/new-admin/middlewares/index.js new file mode 100644 index 00000000..cedba31c --- /dev/null +++ b/lib/new-admin/middlewares/index.js @@ -0,0 +1,9 @@ +const cleanUserSessions = require('./cleanUserSessions') +const buildApolloContext = require('./context') +const session = require('./session') + +module.exports = { + cleanUserSessions, + buildApolloContext, + session +} diff --git a/lib/new-admin/middlewares/session.js b/lib/new-admin/middlewares/session.js index edb323d8..fbdf4465 100644 --- a/lib/new-admin/middlewares/session.js +++ b/lib/new-admin/middlewares/session.js @@ -3,10 +3,11 @@ const express = require('express') const router = express.Router() const hkdf = require('futoin-hkdf') const session = require('express-session') -const pgSession = require('connect-pg-simple')(session) +const PgSession = require('connect-pg-simple')(session) const mnemonicHelpers = require('../../mnemonic-helpers') const db = require('../../db') const options = require('../../options') +const { USER_SESSIONS_TABLE_NAME } = require('../../constants') const getSecret = () => { const mnemonic = fs.readFileSync(options.mnemonicPath, 'utf8') @@ -20,11 +21,11 @@ const getSecret = () => { const hostname = options.hostname router.use('*', session({ - store: new pgSession({ + store: new PgSession({ pgPromise: db, - tableName: 'user_sessions' + tableName: USER_SESSIONS_TABLE_NAME }), - name: 'lid', + name: 'lamassu_sid', secret: getSecret(), resave: false, saveUninitialized: false, diff --git a/lib/new-admin/services/machines.js b/lib/new-admin/services/machines.js index 9a612935..85796601 100644 --- a/lib/new-admin/services/machines.js +++ b/lib/new-admin/services/machines.js @@ -6,13 +6,14 @@ function getMachine (machineId) { .then(machines => machines.find(({ deviceId }) => deviceId === machineId)) } -function machineAction ({ deviceId, action, cashbox, cassette1, cassette2, newName }) { +function machineAction ({ deviceId, action, cashbox, cassette1, cassette2, newName }, context) { + const operatorId = context.res.locals.operatorId return getMachine(deviceId) .then(machine => { if (!machine) throw new UserInputError(`machine:${deviceId} not found`, { deviceId }) return machine }) - .then(machineLoader.setMachine({ deviceId, action, cashbox, cassettes: [cassette1, cassette2], newName })) + .then(machineLoader.setMachine({ deviceId, action, cashbox, cassettes: [cassette1, cassette2], newName }, operatorId)) .then(getMachine(deviceId)) } diff --git a/lib/new-admin/services/pairing.js b/lib/new-admin/services/pairing.js index ffe2ceed..14effa24 100644 --- a/lib/new-admin/services/pairing.js +++ b/lib/new-admin/services/pairing.js @@ -3,6 +3,7 @@ const pify = require('pify') const readFile = pify(fs.readFile) const crypto = require('crypto') const baseX = require('base-x') +const { NIL } = require('uuid') const options = require('../../options') const db = require('../../db') @@ -19,7 +20,7 @@ function totem (name) { return readFile(caPath) .then(data => { const caHash = crypto.createHash('sha256').update(data).digest() - const token = crypto.randomBytes(32) + const token = Buffer.concat([crypto.randomBytes(32), NIL]) const hexToken = token.toString('hex') const caHexToken = crypto.createHash('sha256').update(hexToken).digest('hex') const buf = Buffer.concat([caHash, token, Buffer.from(options.hostname)]) diff --git a/lib/new-settings-loader.js b/lib/new-settings-loader.js index fd0cc435..18da5c61 100644 --- a/lib/new-settings-loader.js +++ b/lib/new-settings-loader.js @@ -1,6 +1,7 @@ const _ = require('lodash/fp') const db = require('./db') const migration = require('./config-migration') +const { asyncLocalStorage } = require('./async-storage') const OLD_SETTINGS_LOADER_SCHEMA_VERSION = 1 const NEW_SETTINGS_LOADER_SCHEMA_VERSION = 2 @@ -73,7 +74,10 @@ function saveConfig (config) { return loadLatestConfigOrNone() .then(currentConfig => { const newConfig = _.assign(currentConfig, config) - return db.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION]) + return db.tx(t => { + return t.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION]) + .then(() => t.none('NOTIFY $1:name, $2', ['poller', JSON.stringify({ type: 'reload', schema: asyncLocalStorage.getStore().get('schema') })])) + }).catch(console.error) }) } diff --git a/lib/options-loader.js b/lib/options-loader.js index 81061d7c..29ec60ec 100644 --- a/lib/options-loader.js +++ b/lib/options-loader.js @@ -6,6 +6,8 @@ const _ = require('lodash/fp') require('dotenv').config() +const DATABASE = process.env.LAMASSU_DB ?? 'DEV' + const dbMapping = psqlConf => ({ STRESS_TEST: _.replace('lamassu', 'lamassu_stress', psqlConf), RELEASE: _.replace('lamassu', 'lamassu_release', psqlConf), @@ -39,7 +41,7 @@ function load () { opts: JSON.parse(fs.readFileSync(globalConfigPath)) } - config.opts.postgresql = dbMapping(config.opts.postgresql)[process.env.LAMASSU_DB] + config.opts.postgresql = dbMapping(config.opts.postgresql)[DATABASE] return config } catch (_) { @@ -50,7 +52,7 @@ function load () { opts: JSON.parse(fs.readFileSync(homeConfigPath)) } - config.opts.postgresql = dbMapping(config.opts.postgresql)[process.env.LAMASSU_DB] + config.opts.postgresql = dbMapping(config.opts.postgresql)[DATABASE] return config } catch (_) { diff --git a/lib/poller.js b/lib/poller.js index 52d43f76..c3ba3334 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -1,5 +1,5 @@ const _ = require('lodash/fp') - +const Queue = require('queue-promise') const plugins = require('./plugins') const notifier = require('./notifier') const T = require('./time') @@ -11,6 +11,12 @@ const sanctions = require('./ofac/index') const coinAtmRadar = require('./coinatmradar/coinatmradar') const configManager = require('./new-config-manager') const complianceTriggers = require('./compliance-triggers') +const { asyncLocalStorage, defaultStore } = require('./async-storage') +const settingsLoader = require('./new-settings-loader') +const NodeCache = require('node-cache') +const util = require('util') +const db = require('./db') +const state = require('./middlewares/state') const INCOMING_TX_INTERVAL = 30 * T.seconds const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds @@ -24,25 +30,106 @@ const LOGS_CLEAR_INTERVAL = 1 * T.day const SANCTIONS_INITIAL_DOWNLOAD_INTERVAL = 5 * T.minutes const SANCTIONS_UPDATE_INTERVAL = 1 * T.week const RADAR_UPDATE_INTERVAL = 5 * T.minutes -const PRUNE_MACHINES_HEARBEAT = 1 * T.day +const PRUNE_MACHINES_HEARTBEAT = 1 * T.day const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds - const PENDING_INTERVAL = 10 * T.seconds +const CACHE_ENTRY_TTL = 3600 // seconds -const coinFilter = ['ETH'] +const FAST_QUEUE_WAIT = 1 * T.seconds +const SLOW_QUEUE_WAIT = 10 * T.seconds -let _pi, _settings +const FAST_QUEUE = new Queue({ + concurrent: 600, + interval: FAST_QUEUE_WAIT +}) -function reload (__settings) { - _settings = __settings - _pi = plugins(_settings) - logger.debug('settings reloaded in poller') - updateAndLoadSanctions() +const SLOW_QUEUE = new Queue({ + concurrent: 10, + interval: SLOW_QUEUE_WAIT +}) + +// Fix for asyncLocalStorage store being lost due to callback-based queue +FAST_QUEUE.enqueue = util.promisify(FAST_QUEUE.enqueue) +SLOW_QUEUE.enqueue = util.promisify(SLOW_QUEUE.enqueue) + +const QUEUE = { + FAST: FAST_QUEUE, + SLOW: SLOW_QUEUE } -function pi () { return _pi } -function settings () { return _settings } +const coinFilter = ['ETH'] +const schemaCallbacks = new Map() + +const cachedVariables = new NodeCache({ + stdTTL: CACHE_ENTRY_TTL, + checkperiod: CACHE_ENTRY_TTL, + deleteOnExpire: false, + useClones: false // pass values by reference instead of cloning +}) + +cachedVariables.on('expired', (key, val) => { + if (!val.isReloading) { + // since val is passed by reference we don't need to do cachedVariables.set() + val.isReloading = true + return reload(key) + } +}) + +db.connect({ direct: true }).then(sco => { + sco.client.on('notification', data => { + const parsedData = JSON.parse(data.payload) + switch (parsedData.type) { + case 'reload': + return reload(parsedData.schema) + case 'machineAction': + return machineAction(parsedData.action, parsedData.value) + default: + break + } + }) + return sco.none('LISTEN $1:name', 'poller') +}).catch(console.error) + +function reload (schema) { + const store = defaultStore() + store.set('schema', schema) + // set asyncLocalStorage so settingsLoader loads settings for the right schema + return asyncLocalStorage.run(store, () => { + return settingsLoader.loadLatest().then(settings => { + const pi = plugins(settings) + cachedVariables.set(schema, { settings, pi, isReloading: false }) + logger.debug(`Settings for schema '${schema}' reloaded in poller`) + return updateAndLoadSanctions() + }) + }) +} + +function machineAction (type, value) { + const deviceId = value.deviceId + const operatorId = value.operatorId + const pid = state.pids?.[operatorId]?.[deviceId]?.pid + + switch (type) { + case 'reboot': + logger.debug(`Rebooting machine '${deviceId}' from operator ${operatorId}`) + state.reboots[operatorId] = { [deviceId]: pid } + break + case 'shutdown': + logger.debug(`Shutting down machine '${deviceId}' from operator ${operatorId}`) + state.shutdowns[operatorId] = { [deviceId]: pid } + break + case 'restartServices': + logger.debug(`Restarting services of machine '${deviceId}' from operator ${operatorId}`) + state.restartServicesMap[operatorId] = { [deviceId]: pid } + break + default: + break + } +} + +function pi () { return cachedVariables.get(asyncLocalStorage.getStore().get('schema')).pi } +function settings () { return cachedVariables.get(asyncLocalStorage.getStore().get('schema')).settings } function initialSanctionsDownload () { const structs = sanctions.getStructs() @@ -70,9 +157,40 @@ function updateCoinAtmRadar () { .then(rates => coinAtmRadar.update(rates, settings())) } -function start (__settings) { - reload(__settings) +function initializeEachSchema (schemas = ['public']) { + // for each schema set "thread variables" and do polling + return _.forEach(schema => { + const store = defaultStore() + store.set('schema', schema) + return asyncLocalStorage.run(store, () => { + return settingsLoader.loadLatest().then(settings => { + // prevent inadvertedly clearing the array without clearing timeouts + if (schemaCallbacks.has(schema)) throw new Error(`The schema "${schema}" cannot be initialized twice on poller`) + const pi = plugins(settings) + cachedVariables.set(schema, { settings, pi, isReloading: false }) + schemaCallbacks.set(schema, []) + return doPolling(schema) + }) + }).catch(console.error) + }, schemas) +} +function addToQueue (func, interval, schema, queue, ...vars) { + return schemaCallbacks.get(schema).push(setInterval(() => { + return queue.enqueue().then(() => { + // get plugins or settings from the cache every time func is run + const loadVariables = vars.length > 0 && typeof vars[0] === 'function' + if (loadVariables) { + const funcVars = [...vars] + funcVars[0] = vars[0]() + return func(...funcVars) + } + return func(...vars) + }).catch(console.error) + }, interval)) +} + +function doPolling (schema) { pi().executeTrades() pi().pong() pi().clearOldLogs() @@ -87,23 +205,37 @@ function start (__settings) { notifier.checkNotification(pi()) updateCoinAtmRadar() - setInterval(() => pi().executeTrades(), TRADE_INTERVAL) - setInterval(() => cashOutTx.monitorLiveIncoming(settings(), false, coinFilter), LIVE_INCOMING_TX_INTERVAL) - setInterval(() => cashOutTx.monitorStaleIncoming(settings(), false, coinFilter), INCOMING_TX_INTERVAL) + addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST) + addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings, false, coinFilter) + addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings, false, coinFilter) if (!_.isEmpty(coinFilter)) { - setInterval(() => cashOutTx.monitorLiveIncoming(settings(), true, coinFilter), LIVE_INCOMING_TX_INTERVAL_FILTER) - setInterval(() => cashOutTx.monitorStaleIncoming(settings(), true, coinFilter), INCOMING_TX_INTERVAL_FILTER) + addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL_FILTER, schema, QUEUE.FAST, settings, true, coinFilter) + addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL_FILTER, schema, QUEUE.FAST, settings, true, coinFilter) } - setInterval(() => cashOutTx.monitorUnnotified(settings()), UNNOTIFIED_INTERVAL) - setInterval(() => cashInTx.monitorPending(settings()), PENDING_INTERVAL) - setInterval(() => pi().sweepHd(), SWEEP_HD_INTERVAL) - setInterval(() => pi().pong(), PONG_INTERVAL) - setInterval(() => pi().clearOldLogs(), LOGS_CLEAR_INTERVAL) - setInterval(() => notifier.checkNotification(pi()), CHECK_NOTIFICATION_INTERVAL) - setInterval(initialSanctionsDownload, SANCTIONS_INITIAL_DOWNLOAD_INTERVAL) - setInterval(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL) - setInterval(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL) - setInterval(() => pi().pruneMachinesHeartbeat(), PRUNE_MACHINES_HEARBEAT) + addToQueue(cashOutTx.monitorUnnotified, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings) + addToQueue(cashInTx.monitorPending, PENDING_INTERVAL, schema, QUEUE.FAST, settings) + addToQueue(pi().sweepHd, SWEEP_HD_INTERVAL, schema, QUEUE.FAST, settings) + addToQueue(pi().pong, PONG_INTERVAL, schema, QUEUE.FAST) + addToQueue(pi().clearOldLogs, LOGS_CLEAR_INTERVAL, schema, QUEUE.SLOW) + addToQueue(notifier.checkNotification, CHECK_NOTIFICATION_INTERVAL, schema, QUEUE.FAST, pi) + addToQueue(initialSanctionsDownload, SANCTIONS_INITIAL_DOWNLOAD_INTERVAL, schema, QUEUE.SLOW) + addToQueue(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL, schema, QUEUE.SLOW) + addToQueue(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL, schema, QUEUE.SLOW) + addToQueue(pi().pruneMachinesHeartbeat(), PRUNE_MACHINES_HEARTBEAT, schema, QUEUE.SLOW, settings) } -module.exports = { start, reload } +function setup (schemasToAdd = [], schemasToRemove = []) { + // clear callback array for each schema in schemasToRemove and clear cached variables + _.forEach(schema => { + const callbacks = schemaCallbacks.get(schema) + _.forEach(clearInterval, callbacks) + schemaCallbacks.delete(schema) + cachedVariables.del(schema) + }, schemasToRemove) + + return initializeEachSchema(schemasToAdd) +} + +const getActiveSchemas = () => Array.from(schemaCallbacks.keys()) + +module.exports = { setup, reload, getActiveSchemas } diff --git a/lib/routes.js b/lib/routes.js index d2c25a40..22b72bf2 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -25,15 +25,12 @@ const phoneCodeRoutes = require('./routes/phoneCodeRoutes') const pollingRoutes = require('./routes/pollingRoutes') const stateRoutes = require('./routes/stateRoutes') const termsAndConditionsRoutes = require('./routes/termsAndConditionsRoutes') -const txRoutes = require('./routes/txRoutes') +const { router: txRoutes } = require('./routes/txRoutes') const verifyUserRoutes = require('./routes/verifyUserRoutes') const verifyTxRoutes = require('./routes/verifyTxRoutes') const verifyPromoCodeRoutes = require('./routes/verifyPromoCodeRoutes') -const localAppRoutes = require('./routes/localAppRoutes') - const app = express() -const localApp = express() const configRequiredRoutes = [ '/poll', @@ -87,7 +84,4 @@ app.use((req, res) => { res.status(404).json({ error: 'No such route' }) }) -// localapp routes -localApp.use('/', localAppRoutes) - -module.exports = { app, localApp } +module.exports = { app } diff --git a/lib/routes/cashboxRoutes.js b/lib/routes/cashboxRoutes.js index 6c740c08..1747187a 100644 --- a/lib/routes/cashboxRoutes.js +++ b/lib/routes/cashboxRoutes.js @@ -8,6 +8,7 @@ const { getCashInSettings } = require('../new-config-manager') const { AUTOMATIC } = require('../constants.js') function notifyCashboxRemoval (req, res, next) { + const operatorId = res.locals.operatorId return Promise.all([getMachine(req.deviceId), loadLatestConfig()]) .then(([machine, config]) => { const cashInSettings = getCashInSettings(config) @@ -15,7 +16,7 @@ function notifyCashboxRemoval (req, res, next) { return res.status(200).send({ status: 'OK' }) } return cashbox.createCashboxBatch(req.deviceId, machine.cashbox) - .then(() => setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' })) + .then(() => setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId)) .then(() => res.status(200).send({ status: 'OK' })) }) .catch(next) diff --git a/lib/routes/localAppRoutes.js b/lib/routes/localAppRoutes.js deleted file mode 100644 index d235faa8..00000000 --- a/lib/routes/localAppRoutes.js +++ /dev/null @@ -1,48 +0,0 @@ -const express = require('express') -const router = express.Router() - -const state = require('../middlewares/state') - -router.get('/pid', (req, res) => { - const deviceId = req.query.device_id - const pidRec = state.pids[deviceId] - res.json(pidRec) -}) - -router.post('/reboot', (req, res) => { - const deviceId = req.query.device_id - const pid = state.pids[deviceId] && state.pids[deviceId].pid - - if (!deviceId || !pid) { - return res.sendStatus(400) - } - - state.reboots[deviceId] = pid - res.sendStatus(200) -}) - -router.post('/shutdown', (req, res) => { - const deviceId = req.query.device_id - const pid = state.pids[deviceId] && state.pids[deviceId].pid - - if (!deviceId || !pid) { - return res.sendStatus(400) - } - - state.shutdowns[deviceId] = pid - res.sendStatus(200) -}) - -router.post('/restartServices', (req, res) => { - const deviceId = req.query.device_id - const pid = state.pids[deviceId] && state.pids[deviceId].pid - - if (!deviceId || !pid) { - return res.sendStatus(400) - } - - state.restartServicesMap[deviceId] = pid - res.sendStatus(200) -}) - -module.exports = router diff --git a/lib/routes/pollingRoutes.js b/lib/routes/pollingRoutes.js index af732e69..b9126953 100644 --- a/lib/routes/pollingRoutes.js +++ b/lib/routes/pollingRoutes.js @@ -31,6 +31,7 @@ function poll (req, res, next) { const serialNumber = req.query.sn const pid = req.query.pid const settings = req.settings + const operatorId = res.locals.operatorId const localeConfig = configManager.getLocale(deviceId, settings.config) const zeroConfLimits = _.reduce((acc, cryptoCode) => { acc[cryptoCode] = configManager.getWalletSettings(cryptoCode, settings.config).zeroConfLimit @@ -48,15 +49,15 @@ function poll (req, res, next) { const receipt = configManager.getReceipt(settings.config) const terms = configManager.getTermsConditions(settings.config) - state.pids[deviceId] = { pid, ts: Date.now() } + state.pids[operatorId] = { [deviceId]: { pid, ts: Date.now() } } return pi.pollQueries(serialNumber, deviceTime, req.query, machineVersion, machineModel) .then(results => { const cassettes = results.cassettes - const reboot = pid && state.reboots[deviceId] && state.reboots[deviceId] === pid - const shutdown = pid && state.shutdowns[deviceId] && state.shutdowns[deviceId] === pid - const restartServices = pid && state.restartServicesMap[deviceId] && state.restartServicesMap[deviceId] === pid + const reboot = pid && state.reboots?.[operatorId]?.[deviceId] === pid + const shutdown = pid && state.shutdowns?.[operatorId]?.[deviceId] === pid + const restartServices = pid && state.restartServicesMap?.[operatorId]?.[deviceId] === pid const langs = localeConfig.languages const locale = { diff --git a/lib/routes/txRoutes.js b/lib/routes/txRoutes.js index 218d223f..0bfbfd07 100644 --- a/lib/routes/txRoutes.js +++ b/lib/routes/txRoutes.js @@ -66,4 +66,4 @@ router.post('/', postTx) router.get('/:id', getTx) router.get('/', getPhoneTx) -module.exports = router +module.exports = { postTx, getTx, getPhoneTx, router } diff --git a/lib/users.js b/lib/users.js index b11a0bf3..3eaa1b74 100644 --- a/lib/users.js +++ b/lib/users.js @@ -59,7 +59,7 @@ function verifyAndUpdateUser (id, ua, ip) { .then(user => { if (!user) return null - const sql2 = `UPDATE users SET last_accessed=now(), last_accessed_from=$1, last_accessed_address=$2 WHERE id=$3 RETURNING id, role, enabled` + const sql2 = `UPDATE users SET last_accessed=now(), last_accessed_from=$1, last_accessed_address=$2 WHERE id=$3 RETURNING id, username, role, enabled` return db.one(sql2, [ua, ip, id]) }) .then(user => user) diff --git a/new-lamassu-admin/package-lock.json b/new-lamassu-admin/package-lock.json index f6429e07..cad5fe31 100644 --- a/new-lamassu-admin/package-lock.json +++ b/new-lamassu-admin/package-lock.json @@ -8192,6 +8192,11 @@ } } }, + "base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" + }, "base-x": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz", diff --git a/new-lamassu-admin/package.json b/new-lamassu-admin/package.json index 24c17fa3..6d4a0e43 100644 --- a/new-lamassu-admin/package.json +++ b/new-lamassu-admin/package.json @@ -14,6 +14,7 @@ "apollo-link-error": "^1.1.13", "apollo-link-http": "^1.5.17", "axios": "0.21.1", + "base-64": "^1.0.0", "bignumber.js": "9.0.0", "classnames": "2.2.6", "countries-and-timezones": "^2.4.0", @@ -96,8 +97,8 @@ "storybook": "start-storybook -p 9009 -s public", "postinstall": "patch-package", "build-storybook": "build-storybook -s public", - "start-lamassu": "REACT_APP_BUILD_TARGET=LAMASSU react-scripts start", - "start-pazuz": "REACT_APP_BUILD_TARGET=PAZUZ react-scripts start" + "lamassu": "REACT_APP_BUILD_TARGET=LAMASSU react-scripts start", + "pazuz": "REACT_APP_BUILD_TARGET=PAZUZ react-scripts start" }, "browserslist": { "production": [ diff --git a/new-lamassu-admin/src/App.js b/new-lamassu-admin/src/App.js index 48373584..565d8b47 100644 --- a/new-lamassu-admin/src/App.js +++ b/new-lamassu-admin/src/App.js @@ -154,7 +154,7 @@ const App = () => { const [userData, setUserData] = useState(null) const setRole = role => { - if (userData && userData.role !== role) { + if (userData && role && userData.role !== role) { setUserData({ ...userData, role }) } } diff --git a/new-lamassu-admin/src/pages/ATMWallet/ATMWallet.js b/new-lamassu-admin/src/pages/ATMWallet/ATMWallet.js index cd35530e..9c501948 100644 --- a/new-lamassu-admin/src/pages/ATMWallet/ATMWallet.js +++ b/new-lamassu-admin/src/pages/ATMWallet/ATMWallet.js @@ -1,9 +1,12 @@ +import { useQuery } from '@apollo/react-hooks' import { Paper } from '@material-ui/core' import { makeStyles } from '@material-ui/core/styles' import classnames from 'classnames' +import gql from 'graphql-tag' import * as R from 'ramda' -import React from 'react' +import React, { useContext } from 'react' +import AppContext from 'src/AppContext' import TitleSection from 'src/components/layout/TitleSection' import { H3, Info2, Label2, Label3, P } from 'src/components/typography' import { ReactComponent as BitcoinLogo } from 'src/styling/logos/icon-bitcoin-colour.svg' @@ -17,6 +20,40 @@ import styles from './ATMWallet.styles' const useStyles = makeStyles(styles) +const GET_OPERATOR_BY_USERNAME = gql` + query operatorByUsername($username: String) { + operatorByUsername(username: $username) { + id + entityId + name + fiatBalances + cryptoBalances + machines + joined + assetValue + preferredFiatCurrency + contactInfo { + name + email + } + fundings { + id + origin + destination + fiatAmount + fiatBalanceAfter + fiatCurrency + created + status + description + } + } + } +` + +const formatCurrency = amount => + amount.toLocaleString('en-US', { maximumFractionDigits: 2 }) + const CHIPS_PER_ROW = 6 const Assets = ({ balance, wallets, currency }) => { @@ -32,10 +69,10 @@ const Assets = ({ balance, wallets, currency }) => {
Available balance
Total balance in wallets
Total assets
{it.extraInfo}
@@ -122,18 +129,15 @@ const Accounting = () => { size: 'sm', textAlign: 'right', view: it => - `${ - it.direction === 'in' - ? formatCurrency(it.amount) - : formatCurrency(-it.amount) - } ${it.currency}` + `${formatCurrency(it.fiatAmount)} ${R.toUpper(it.fiatCurrency)}` }, { header: 'Balance after operation', width: 250, size: 'sm', textAlign: 'right', - view: it => `${formatCurrency(it.balanceAfterTx)} ${it.currency}` + view: it => + `${formatCurrency(it.fiatBalanceAfter)} ${R.toUpper(it.fiatCurrency)}` }, { header: 'Date', @@ -152,18 +156,26 @@ const Accounting = () => { ] return ( - <> -