erm-cli
Version:
Reekoh CLI - command line tool for publishing reekoh plugins.
469 lines (399 loc) • 12.3 kB
JavaScript
let os = require('os')
let _ = require('lodash')
let request = require('request')
let path = require('path')
let async = require('async')
let yaml = require('js-yaml')
let archiver = require('archiver')
let getDirSize = require('get-folder-size')
let fs = Promise.promisifyAll(require('fs'))
let simpleEncryptor = require('simple-encryptor')
let cliff = require('cliff')
let si = require('systeminformation')
let CliError = require('./lib/cli-error')
let prompt = require('prompt-sync')({
history: false,
sigint: true
})
const userCredentials = path.join(os.tmpdir(), 'reekoh-credentials')
const config = require('./config.json')
const packageJson = require('./package.json')
class Service {
handleUnknownCommand (command) {
console.log(`${packageJson.name}: '${command}' is not a ${packageJson.name} command. \nSee '${packageJson.name} --help'`)
}
getEncryptionKey () {
return new Promise((resolve) => {
si.system().then(data => {
let {serial, uuid} = data
let encKey = `${serial}-${uuid}`
encKey = (encKey !== '---') ? encKey : config.encKey
resolve(encKey)
}).catch(() => {
resolve(config.encKey)
})
})
}
encrypt (data = '') {
return this.getEncryptionKey().then(encKey => {
let encryptor = simpleEncryptor(encKey)
return Promise.resolve(encryptor.encrypt(data))
})
}
decrypt (data = '') {
return this.getEncryptionKey().then(encKey => {
let encryptor = simpleEncryptor(encKey)
return Promise.resolve(encryptor.decrypt(data))
})
}
getUserCredentials (cmdOptions) {
return new Promise((resolve, reject) => {
let credentials = {}
let fields = [{
name: 'Username'
}, {
name: 'Password',
promptProps: {
echo: '*'
}
}]
async.eachLimit(fields, 1, (field, cb) => {
let key = field.name.toLowerCase()
if (cmdOptions.hasOwnProperty(key)) {
credentials[key] = cmdOptions[key]
return cb()
}
credentials[key] = prompt(`${field.name}: `, field.promptProps)
if (_.isEmpty(credentials[key])) {
return cb(new CliError(`${field.name} is required`))
}
cb()
}, (error) => {
if (error) {
if (error.message === 'canceled') {
return resolve({})
}
reject(error)
} else {
resolve(credentials)
}
})
})
}
userAuth (credentials) {
return new Promise((resolve, reject) => {
request({
method: 'post',
url: `${config.reekohApi}/users/auth`,
body: credentials,
headers: {
'Accept-Version': '1.0.0',
'Accept': 'application/json'
},
json: true
}, (error, res, body) => {
if (error) {
return reject(error)
}
if (res.statusCode !== 200) {
error = new CliError(body.message, body)
error.detail = body
return reject(error)
}
resolve(body)
})
})
}
switchRole (credentials, token) {
return new Promise((resolve, reject) => {
request({
method: 'post',
url: `${config.reekohApi}/users/switch-role`,
body: credentials,
headers: {
'Accept-Version': '1.0.0',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
},
json: true
}, (error, res, body) => {
if (error) {
return reject(error)
}
if (res.statusCode !== 200) {
error = new CliError(body.message, body)
error.detail = body
return reject(error)
}
resolve(body)
})
})
}
getUserRoles (token) {
return new Promise((resolve, reject) => {
request({
method: 'get',
url: `${config.reekohApi}/users/roles`,
headers: {
'Accept': 'application/json',
'Accept-Version': '1.0.0',
'Authorization': `Bearer ${token}`
},
json: true
}, (error, res, body) => {
if (error) {
return reject(error)
} else if (res.statusCode !== 200) {
error = new CliError(body.message, body)
error.detail = body
return reject(error)
}
resolve(body)
})
})
}
selectUserRole (userRoles) {
return new Promise((resolve, reject) => {
if (_.isEmpty(userRoles)) {
return reject(new CliError(`Sorry, you don't have any role with write permission.`))
}
let error
let field = 'accountRole'
let validOptions = []
let roleOptions = userRoles.map((userRole, i) => {
let optNumber = `${i + 1}`
validOptions.push(optNumber)
return [`${optNumber})`, userRole.account.name, userRole.role.name]
})
roleOptions.unshift(['', 'Account', 'Role'])
console.log('\n' + cliff.stringifyRows(roleOptions, ['yellow']) + '\n')
let selectedOption = prompt('Account role: ')
if (_.isEmpty(selectedOption)) {
error = new CliError(`Account role is required`)
} else if (validOptions.indexOf(`${selectedOption}`) === -1) {
error = new CliError('Invalid option selected.')
}
if (error) {
reject(error)
}
selectedOption -= 1
resolve(userRoles[selectedOption])
})
}
saveUserData (data) {
return this.encrypt(data).then(encryptedData => {
return fs.writeFileAsync(userCredentials, encryptedData)
}).then(() => {
return Promise.resolve(data)
})
}
getUserData () {
return new Promise((resolve, reject) => {
fs.readFile(userCredentials, 'utf8', (error, data) => {
let errorMessage = 'Not logged in.'
if (error) {
if (error.code === 'ENOENT') {
error = new CliError(errorMessage)
}
return reject(error)
}
this.decrypt(data).then(decryptedData => {
if (_.isEmpty(decryptedData)) {
return reject(new CliError(errorMessage))
}
resolve(decryptedData)
}).catch(reject)
})
})
}
removeUserData () {
return new Promise((resolve, reject) => {
fs.unlink(userCredentials, (error) => {
if (error) {
if (error.code === 'ENOENT') {
return reject(new CliError('No logged in user found.'))
}
return reject(error)
}
resolve()
})
})
}
getPluginDetails (pathToManifest) {
return new Promise((resolve, reject) => {
fs.readFileAsync(pathToManifest, 'utf8').then(content => {
let jsonContent = yaml.safeLoad(content)
resolve(jsonContent)
}).catch(error => {
if (error.code === 'ENOENT') {
error = new CliError(`Unable to locate manifest file.`)
}
if (error.name === 'YAMLException') {
error = new CliError(`Invalid manifest file : ${error.message}`)
}
reject(error)
})
})
}
validateFile (file, plugin) {
return new Promise((resolve, reject) => {
let errorMsg = `Invalid manifest detail on '${file.key}'. `
let filePath = _.get(plugin, file.key, null)
if (_.isNil(filePath)) {
return resolve(plugin)
}
filePath = path.resolve(file.manifestDir, filePath)
let fileExtension = path.extname(filePath).replace('.', '')
if (!_.includes(file.validExtensions, fileExtension)) {
reject(new CliError(`${errorMsg} Invalid file type. Must be one of the following format [${file.validExtensions.join(', ')}]`))
}
fs.readFileAsync(filePath, 'utf8').then(content => {
if (file.getContent) {
_.set(plugin, file.key, content)
}
resolve(plugin)
}).catch(error => {
reject(new CliError(`${errorMsg} ${error.message}`))
})
})
}
saveIcon (options, plugin) {
return new Promise((resolve, reject) => {
let iconPath = _.get(plugin, 'metadata.icon')
if (_.isEmpty(iconPath)) {
return resolve(plugin)
}
iconPath = path.resolve(options.manifestDir, iconPath)
let requestOptions = {
method: 'post',
url: `${config.reekohApi}/files`,
formData: {
type: 'plugin-icon',
file: fs.createReadStream(iconPath)
},
headers: {
Authorization: `Bearer ${options.token}`
},
json: true
}
request(requestOptions, (error, res, body) => {
if (error) {
reject(error)
} else if (res.statusCode !== 201) {
reject(new CliError(body.message))
} else {
_.set(plugin, 'metadata.icon', body._id)
resolve(plugin)
}
})
})
}
savePluginCode (options, plugin) {
return new Promise((resolve, reject) => {
let pluginRootDir = _.get(plugin, 'metadata.release.pluginRootDir')
if (_.isEmpty(pluginRootDir)) {
return reject(new CliError(`Missing plugin root directory.`))
}
pluginRootDir = path.resolve(options.manifestDir, pluginRootDir)
let createZipFile = Promise.promisify((dirPath, cb) => {
let zipFile = new Date().getTime()
zipFile = path.join(os.tmpdir(), `reekoh-${zipFile}.zip`)
let archive = archiver('zip')
let output = fs.createWriteStream(zipFile)
archive.on('error', cb)
output.on('close', () => {
cb(null, zipFile)
})
archive.pipe(output)
archive.glob('!(node_modules)')
archive.finalize()
})
let saveZipFile = Promise.promisify(pathToZip => {
let requestOptions = {
method: 'post',
url: `${config.reekohApi}/files`,
formData: {
type: 'plugin-code',
file: fs.createReadStream(pathToZip)
},
headers: {
Authorization: `Bearer ${options.token}`
},
json: true
}
request(requestOptions, (error, res, body) => {
if (error) {
reject(error)
} else if (res.statusCode !== 201) {
error = new CliError(body.message, body)
error.detail = body
return reject(error)
} else {
_.set(plugin, 'metadata.release.code', body._id)
resolve(plugin)
}
})
})
let getPluginSize = Promise.promisify(getDirSize)
fs.readdirAsync(pluginRootDir, 'utf8').then(() => {
return getPluginSize(pluginRootDir)
}).then((size) => {
if (size > config.maxZipSize) {
return reject(new CliError('The maximum allowable plugin size is 5 MB.'))
}
return createZipFile(pluginRootDir)
}).then((pathToZip) => {
return saveZipFile(pathToZip)
}).then((zipFile) => {
_.set(plugin, 'metadata.release.code', zipFile._id)
resolve(plugin)
}).catch(error => {
reject(new CliError(`${error.message}`))
})
})
}
submitPlugin (token, method, plugin) {
return new Promise((resolve, reject) => {
let requestOptions = {
method,
url: `${config.reekohApi}/plugins`,
body: plugin.metadata,
headers: {
Authorization: `Bearer ${token}`
},
json: true
}
request(requestOptions, (error, res, body) => {
if (error) {
return reject(error)
} else if (!_.includes([200, 201], res.statusCode)) {
error = new CliError(body.message, body)
error.detail = body
return reject(error)
} else {
resolve(plugin)
}
})
})
}
handleError (error) {
if (error instanceof CliError) {
console.error(`Error: ${error.message}`)
let eData = error.data
if (!_.isEmpty(eData.details)) {
eData.details.forEach((detail) => {
console.log(` - ${detail.msg}`)
})
}
} else {
console.error(config.genericErrorMessage)
}
process.exit()
}
processExit () {
console.log('\n')
process.exit()
}
}
module.exports = new Service()