Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add machine action auditing #1396

Open
wants to merge 4 commits into
base: future-stuff-old-dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 91 additions & 10 deletions lib/machine-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ function toMachineObject (r) {
pairedAt: new Date(r.created),
lastPing: new Date(r.last_online),
name: r.name,
paired: r.paired
paired: r.paired,
location: r.machine_location
// TODO: we shall start using this JSON field at some point
// location: r.location,
}
Expand Down Expand Up @@ -85,13 +86,21 @@ function addName (pings, events, config) {
}

function getMachineNames (config) {
return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
.then(([rawMachines, config, heartbeat, performance]) => Promise.all(
[rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance(), getMachineLocations()])
.then(([rawMachines, config, heartbeat, performance, locations]) => Promise.all(
[rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance, locations]
))
.then(([rawMachines, pings, events, config, heartbeat, performance]) => {
.then(([rawMachines, pings, events, config, heartbeat, performance, locations]) => {
const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y)))
const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance)
const mergeByLocationId = (x, y) => _.map(it => {
if (_.isNil(it.location)) return it
return { ...it, location: _.find(ite => it.location === ite.id, y) }
}, x)
const machines = _.flow([
x => mergeByDeviceId(x, heartbeat),
x => mergeByDeviceId(x, performance),
x => mergeByLocationId(x, locations)
])(rawMachines)

return machines.map(addName(pings, events, config))
})
Expand Down Expand Up @@ -121,13 +130,18 @@ function getMachine (machineId, config) {
})

return Promise.all([queryMachine, dbm.machineEvents(), config, getNetworkHeartbeatByDevice(machineId), getNetworkPerformanceByDevice(machineId)])
.then(([machine, events, config, heartbeat, performance]) => {
.then(([machine, events, config, heartbeat, performance]) => Promise.all([machine, events, config, heartbeat, performance, getMachineLocation(machine.location)]))
.then(([machine, events, config, heartbeat, performance, location]) => {
const pings = checkPings([machine])
const mergedMachine = {
...machine,
responseTime: _.get('responseTime', heartbeat),
packetLoss: _.get('packetLoss', heartbeat),
downloadSpeed: _.get('downloadSpeed', performance),
downloadSpeed: _.get('downloadSpeed', performance)
}

if (!_.isNil(location) && !_.isEmpty(location)) {
mergedMachine.location = location
}

return addName(pings, events, config)(mergedMachine)
Expand Down Expand Up @@ -193,8 +207,10 @@ function restartServices (rec) {
)])
}

function setMachine (rec, operatorId) {
function setMachine (rec, operatorId, userId) {
rec.operatorId = operatorId
rec.userId = userId
logMachineAction(rec)
switch (rec.action) {
case 'rename': return renameMachine(rec)
case 'emptyCashInBills': return emptyCashInBills(rec)
Expand All @@ -204,10 +220,17 @@ function setMachine (rec, operatorId) {
case 'reboot': return reboot(rec)
case 'shutdown': return shutdown(rec)
case 'restartServices': return restartServices(rec)
case 'editLocation': return editMachineLocation(rec.location, rec.deviceId)
case 'deleteLocation': return deleteMachineLocation(rec.location.id)
case 'createLocation': return createMachineLocation(rec.location, rec.deviceId)
default: throw new Error('No such action: ' + rec.action)
}
}

function testLocation (rec) {
console.log(rec)
}

function updateNetworkPerformance (deviceId, data) {
if (_.isEmpty(data)) return Promise.resolve(true)
const downloadSpeed = _.head(data)
Expand Down Expand Up @@ -266,6 +289,58 @@ function getNetworkHeartbeatByDevice (deviceId) {
.then(res => _.mapKeys(_.camelCase, _.find(it => it.device_id === deviceId, res)))
}

function getMachineLocations () {
const sql = `SELECT * FROM machine_locations`
return db.any(sql)
.then(_.map(_.mapKeys(_.camelCase)))
}

function getMachineLocation (locationId) {
const sql = `SELECT * FROM machine_locations WHERE id = $1`
return db.oneOrNone(sql, [locationId])
.then(_.mapKeys(_.camelCase))
}

function editMachineLocation ({ id, label, addressLine1, addressLine2, zipCode, country }, deviceId) {
const sql = `UPDATE machine_locations SET label = $1, address_line_1 = $2, address_line_2 = $3, zip_code = $4, country = $5 WHERE id = $6`
const sql2 = `UPDATE devices SET machine_location = $1 WHERE device_id = $2`
return db.none(sql, [label, addressLine1, addressLine2, zipCode, country, id])
.then(() => _.isNil(deviceId) ? Promise.resolve() : db.none(sql2, [id, deviceId]))
}

function createMachineLocation ({ id, label, addressLine1, addressLine2, zipCode, country }, deviceId) {
const _id = _.isEmpty(id) ? uuid.v4() : id
const sql = `INSERT INTO machine_locations (id, label, address_line_1, address_line_2, zip_code, country) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT DO NOTHING`
const sql2 = `UPDATE devices SET machine_location = $1 WHERE device_id = $2`
return db.none(sql, [_id, label, addressLine1, addressLine2, zipCode, country])
.then(() => _.isNil(deviceId) ? Promise.resolve() : db.none(sql2, [_id, deviceId]))
}

function deleteMachineLocation (id) {
return db.tx(t => {
const q1 = t.none(`UPDATE devices SET machine_location = NULL WHERE machine_location = $1`, [id])
const q2 = t.none(`UPDATE pairing_tokens SET machine_location = NULL WHERE machine_location = $1`, [id])
const q3 = t.none(`DELETE FROM machine_locations WHERE id = $1`, [id])

return t.batch([q1, q2, q3])
})
}

function assignLocation (machineId, locationId) {
const sql = `UPDATE devices SET machine_location = $1 WHERE device_id = $2`
return db.none(sql, [locationId, machineId])
}

function logMachineAction (rec) {
const userId = rec.userId
const deviceId = rec.deviceId
const action = rec.action
const values = _.omit(['userId', 'operatorId', 'deviceId', 'action'], rec)
const sql = `INSERT INTO machine_action_logs (id, device_id, action, values, performed_by) VALUES ($1, $2, $3, $4, $5)`
// console.log([uuid.v4(), deviceId, _.kebabCase(action), values, userId])
return db.none(sql, [uuid.v4(), deviceId, _.kebabCase(action), values, userId])
}

module.exports = {
getMachineName,
getMachines,
Expand All @@ -277,5 +352,11 @@ module.exports = {
updateNetworkHeartbeat,
getNetworkPerformance,
getNetworkHeartbeat,
getConfig
getConfig,
getMachineLocations,
getMachineLocation,
editMachineLocation,
createMachineLocation,
deleteMachineLocation,
assignLocation
}
7 changes: 4 additions & 3 deletions lib/new-admin/graphql/resolvers/machine.resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ const resolvers = {
Query: {
machines: () => machineLoader.getMachineNames(),
machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId),
unpairedMachines: () => machineLoader.getUnpairedMachines()
unpairedMachines: () => machineLoader.getUnpairedMachines(),
machineLocations: () => machineLoader.getMachineLocations()
},
Mutation: {
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context]) =>
machineAction({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context)
machineAction: (...[, { deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName, location }, context]) =>
machineAction({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName, location }, context)
}
}

Expand Down
4 changes: 3 additions & 1 deletion lib/new-admin/graphql/resolvers/pairing.resolver.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const pairing = require('../../services/pairing')
const machine = require('../../../machine-loader')

const resolvers = {
Mutation: {
createPairingTotem: (...[, { name }]) => pairing.totem(name)
createPairingTotem: (...[, { name, location }]) => machine.createMachineLocation(location)
.then(() => pairing.totem(name, location))
}
}

Expand Down
16 changes: 15 additions & 1 deletion lib/new-admin/graphql/types/machine.type.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ const typeDef = gql`
downloadSpeed: String
responseTime: String
packetLoss: String
location: MachineLocation
}

type MachineLocation {
id: ID
label: String
addressLine1: String
addressLine2: String
zipCode: String
country: String
}

type UnpairedMachine {
Expand Down Expand Up @@ -55,16 +65,20 @@ const typeDef = gql`
reboot
shutdown
restartServices
editLocation
deleteLocation
createLocation
}

type Query {
machines: [Machine] @auth
machine(deviceId: ID!): Machine @auth
unpairedMachines: [UnpairedMachine!]! @auth
machineLocations: [MachineLocation] @auth
}

type Mutation {
machineAction(deviceId:ID!, action: MachineAction!, cashbox: Int, cassette1: Int, cassette2: Int, cassette3: Int, cassette4: Int, newName: String): Machine @auth
machineAction(deviceId:ID!, action: MachineAction!, cashbox: Int, cassette1: Int, cassette2: Int, cassette3: Int, cassette4: Int, newName: String, location: JSONObject): Machine @auth
}
`

Expand Down
2 changes: 1 addition & 1 deletion lib/new-admin/graphql/types/pairing.type.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { gql } = require('apollo-server-express')

const typeDef = gql`
type Mutation {
createPairingTotem(name: String!): String @auth
createPairingTotem(name: String!, location: JSONObject!): String @auth
}
`

Expand Down
5 changes: 3 additions & 2 deletions lib/new-admin/services/machines.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ function getMachine (machineId) {
.then(machines => machines.find(({ deviceId }) => deviceId === machineId))
}

function machineAction ({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName }, context) {
function machineAction ({ deviceId, action, cashbox, cassette1, cassette2, cassette3, cassette4, newName, location }, context) {
const operatorId = context.res.locals.operatorId
const userId = context.req.session.user.id
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, cassette3, cassette4], newName }, operatorId))
.then(machineLoader.setMachine({ deviceId, action, cashbox, cassettes: [cassette1, cassette2, cassette3, cassette4], newName, location }, operatorId, userId))
.then(getMachine(deviceId))
}

Expand Down
6 changes: 3 additions & 3 deletions lib/new-admin/services/pairing.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ const HOSTNAME = process.env.HOSTNAME

const unpair = pairing.unpair

function totem (name) {
function totem (name, location) {
return readFile(CA_PATH)
.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)'
const sql = 'insert into pairing_tokens (token, name, machine_location) values ($1, $3, $4), ($2, $3, $4)'

return db.none(sql, [hexToken, caHexToken, name])
return db.none(sql, [hexToken, caHexToken, name, location.id])
.then(() => bsAlpha.encode(buf))
})
}
Expand Down
6 changes: 3 additions & 3 deletions lib/pairing.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const DEFAULT_NUMBER_OF_CASSETTES = 2
function pullToken (token) {
const sql = `delete from pairing_tokens
where token=$1
returning name, created < now() - interval '1 hour' as expired`
returning name, created < now() - interval '1 hour' as expired, machine_location`
return db.one(sql, [token])
}

Expand All @@ -41,11 +41,11 @@ function pair (token, deviceId, machineModel, numOfCassettes = DEFAULT_NUMBER_OF
.then(r => {
if (r.expired) return false

const insertSql = `insert into devices (device_id, name, number_of_cassettes) values ($1, $2, $3)
const insertSql = `insert into devices (device_id, name, number_of_cassettes, machine_location) values ($1, $2, $3, $4)
on conflict (device_id)
do update set paired=TRUE, display=TRUE`

return db.none(insertSql, [deviceId, r.name, numOfCassettes])
return db.none(insertSql, [deviceId, r.name, numOfCassettes, r.machine_location])
.then(() => true)
})
.catch(err => {
Expand Down
23 changes: 23 additions & 0 deletions migrations/1664411947220-machine-location.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var db = require('./db')

exports.up = function (next) {
var sql = [
`CREATE TABLE machine_locations (
id UUID PRIMARY KEY,
label TEXT NOT NULL,
address_line_1 TEXT NOT NULL,
address_line_2 TEXT,
zip_code TEXT NOT NULL,
country TEXT NOT NULL,
created TIMESTAMPTZ NOT NULL DEFAULT now()
)`,
`ALTER TABLE devices ADD COLUMN machine_location UUID REFERENCES machine_locations(id)`,
`ALTER TABLE pairing_tokens ADD COLUMN machine_location UUID REFERENCES machine_locations(id)`
]

db.multi(sql, next)
}

exports.down = function (next) {
next()
}
35 changes: 35 additions & 0 deletions migrations/1664748434695-machine-actions-auditing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
var db = require('./db')

exports.up = function (next) {
var sql = [
`CREATE TYPE machine_action AS ENUM (
'rename',
'empty-cash-in-bills',
'reset-cash-out-bills',
'set-cassette-bills',
'unpair',
'reboot',
'shutdown',
'restart-services',
'edit-location',
'delete-location',
'create-location',
'disable',
'enable'
)`,
`CREATE TABLE machine_action_logs (
id UUID PRIMARY KEY,
device_id TEXT NOT NULL REFERENCES devices(device_id),
action machine_action NOT NULL,
values JSONB NOT NULL,
performed_by UUID NOT NULL REFERENCES users(id),
performed_at TIMESTAMPTZ NOT NULL DEFAULT now()
)`
]

db.multi(sql, next)
}

exports.down = function (next) {
next()
}
Loading