PM2IO.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. 'use strict'
  2. var cst = require('../../../constants.js');
  3. const chalk = require('chalk');
  4. const path = require('path');
  5. const fs = require('fs');
  6. const Table = require('cli-tableau');
  7. const pkg = require('../../../package.json')
  8. const IOAPI = require('@pm2/js-api')
  9. const promptly = require('promptly')
  10. var CLIStrategy = require('./auth-strategies/CliAuth')
  11. var WebStrategy = require('./auth-strategies/WebAuth')
  12. const exec = require('child_process').exec
  13. const OAUTH_CLIENT_ID_WEB = '138558311'
  14. const OAUTH_CLIENT_ID_CLI = '0943857435'
  15. module.exports = class PM2ioHandler {
  16. static usePM2Client (instance) {
  17. this.pm2 = instance
  18. }
  19. static strategy () {
  20. switch (process.platform) {
  21. case 'darwin': {
  22. return new WebStrategy({
  23. client_id: OAUTH_CLIENT_ID_WEB
  24. })
  25. }
  26. case 'win32': {
  27. return new WebStrategy({
  28. client_id: OAUTH_CLIENT_ID_WEB
  29. })
  30. }
  31. case 'linux': {
  32. const isDesktop = process.env.XDG_CURRENT_DESKTOP || process.env.XDG_SESSION_DESKTOP || process.env.DISPLAY
  33. const isSSH = process.env.SSH_TTY || process.env.SSH_CONNECTION
  34. if (isDesktop && !isSSH) {
  35. return new WebStrategy({
  36. client_id: OAUTH_CLIENT_ID_WEB
  37. })
  38. } else {
  39. return new CLIStrategy({
  40. client_id: OAUTH_CLIENT_ID_CLI
  41. })
  42. }
  43. }
  44. default: {
  45. return new CLIStrategy({
  46. client_id: OAUTH_CLIENT_ID_CLI
  47. })
  48. }
  49. }
  50. }
  51. static init () {
  52. this._strategy = this.strategy()
  53. /**
  54. * If you are using a local backend you should give those options :
  55. * {
  56. * services: {
  57. * API: 'http://localhost:3000',
  58. * OAUTH: 'http://localhost:3100'
  59. * }
  60. * }
  61. */
  62. this.io = new IOAPI().use(this._strategy)
  63. }
  64. static launch (command, opts) {
  65. // first init the strategy and the io client
  66. this.init()
  67. switch (command) {
  68. case 'connect' :
  69. case 'login' :
  70. case 'register' :
  71. case undefined :
  72. case 'authenticate' : {
  73. this.authenticate()
  74. break
  75. }
  76. case 'validate' : {
  77. this.validateAccount(opts)
  78. break
  79. }
  80. case 'help' :
  81. case 'welcome': {
  82. var dt = fs.readFileSync(path.join(__dirname, './pres/welcome'));
  83. console.log(dt.toString());
  84. return process.exit(0)
  85. }
  86. case 'logout': {
  87. this._strategy.isAuthenticated().then(isConnected => {
  88. // try to kill the agent anyway
  89. this.pm2.killAgent(err => {})
  90. if (isConnected === false) {
  91. console.log(`${cst.PM2_IO_MSG} Already disconnected`)
  92. return process.exit(0)
  93. }
  94. this._strategy._retrieveTokens((err, tokens) => {
  95. if (err) {
  96. console.log(`${cst.PM2_IO_MSG} Successfully disconnected`)
  97. return process.exit(0)
  98. }
  99. this._strategy.deleteTokens(this.io).then(_ => {
  100. console.log(`${cst.PM2_IO_MSG} Successfully disconnected`)
  101. return process.exit(0)
  102. }).catch(err => {
  103. console.log(`${cst.PM2_IO_MSG_ERR} Unexpected error: ${err.message}`)
  104. return process.exit(1)
  105. })
  106. })
  107. }).catch(err => {
  108. console.error(`${cst.PM2_IO_MSG_ERR} Failed to logout: ${err.message}`)
  109. console.error(`${cst.PM2_IO_MSG_ERR} You can also contact us to get help: contact@pm2.io`)
  110. })
  111. break
  112. }
  113. case 'create': {
  114. this._strategy.isAuthenticated().then(res => {
  115. // if the user isn't authenticated, we make them do the whole flow
  116. if (res !== true) {
  117. this.authenticate()
  118. } else {
  119. this.createBucket(this.createBucketHandler.bind(this))
  120. }
  121. }).catch(err => {
  122. console.error(`${cst.PM2_IO_MSG_ERR} Failed to create to the bucket: ${err.message}`)
  123. console.error(`${cst.PM2_IO_MSG_ERR} You can also contact us to get help: contact@pm2.io`)
  124. })
  125. break
  126. }
  127. case 'web': {
  128. this._strategy.isAuthenticated().then(res => {
  129. // if the user isn't authenticated, we make them do the whole flow
  130. if (res === false) {
  131. console.error(`${cst.PM2_IO_MSG_ERR} You need to be authenticated to do that, please use: pm2 plus login`)
  132. return process.exit(1)
  133. }
  134. this._strategy._retrieveTokens(() => {
  135. return this.openUI()
  136. })
  137. }).catch(err => {
  138. console.error(`${cst.PM2_IO_MSG_ERR} Failed to open the UI: ${err.message}`)
  139. console.error(`${cst.PM2_IO_MSG_ERR} You can also contact us to get help: contact@pm2.io`)
  140. })
  141. break
  142. }
  143. default : {
  144. console.log(`${cst.PM2_IO_MSG_ERR} Invalid command ${command}, available : login,register,validate,connect or web`)
  145. process.exit(1)
  146. }
  147. }
  148. }
  149. static openUI () {
  150. this.io.bucket.retrieveAll().then(res => {
  151. const buckets = res.data
  152. if (buckets.length === 0) {
  153. return this.createBucket((err, bucket) => {
  154. if (err) {
  155. console.error(`${cst.PM2_IO_MSG_ERR} Failed to connect to the bucket: ${err.message}`)
  156. if (bucket) {
  157. console.error(`${cst.PM2_IO_MSG_ERR} You can retry using: pm2 plus link ${bucket.secret_id} ${bucket.public_id}`)
  158. }
  159. console.error(`${cst.PM2_IO_MSG_ERR} You can also contact us to get help: contact@pm2.io`)
  160. return process.exit(0)
  161. }
  162. const targetURL = `https://app.pm2.io/#/bucket/${bucket._id}`
  163. console.log(`${cst.PM2_IO_MSG} Please follow the popup or go to this URL :`, '\n', ' ', targetURL)
  164. this.open(targetURL)
  165. return process.exit(0)
  166. })
  167. }
  168. var table = new Table({
  169. style : {'padding-left' : 1, head : ['cyan', 'bold'], compact : true},
  170. head : ['Bucket name', 'Plan type']
  171. })
  172. buckets.forEach(function(bucket) {
  173. table.push([bucket.name, bucket.credits.offer_type])
  174. })
  175. console.log(table.toString())
  176. console.log(`${cst.PM2_IO_MSG} If you don't want to open the UI to a bucket, type 'none'`)
  177. const choices = buckets.map(bucket => bucket.name)
  178. choices.push('none')
  179. promptly.choose(`${cst.PM2_IO_MSG} Type the name of the bucket you want to connect to :`, choices, (err, value) => {
  180. if (value === 'none') process.exit(0)
  181. const bucket = buckets.find(bucket => bucket.name === value)
  182. if (bucket === undefined) return process.exit(0)
  183. const targetURL = `https://app.pm2.io/#/bucket/${bucket._id}`
  184. console.log(`${cst.PM2_IO_MSG} Please follow the popup or go to this URL :`, '\n', ' ', targetURL)
  185. this.open(targetURL)
  186. return process.exit(0)
  187. })
  188. })
  189. }
  190. static validateAccount (token) {
  191. this.io.auth.validEmail(token)
  192. .then(res => {
  193. console.log(`${cst.PM2_IO_MSG} Email succesfully validated.`)
  194. console.log(`${cst.PM2_IO_MSG} You can now proceed and use: pm2 plus connect`)
  195. return process.exit(0)
  196. }).catch(err => {
  197. if (err.status === 401) {
  198. console.error(`${cst.PM2_IO_MSG_ERR} Invalid token`)
  199. return process.exit(1)
  200. } else if (err.status === 301) {
  201. console.log(`${cst.PM2_IO_MSG} Email succesfully validated.`)
  202. console.log(`${cst.PM2_IO_MSG} You can now proceed and use: pm2 plus connect`)
  203. return process.exit(0)
  204. }
  205. const msg = err.data ? err.data.error_description || err.data.msg : err.message
  206. console.error(`${cst.PM2_IO_MSG_ERR} Failed to validate your email: ${msg}`)
  207. console.error(`${cst.PM2_IO_MSG_ERR} You can also contact us to get help: contact@pm2.io`)
  208. return process.exit(1)
  209. })
  210. }
  211. static createBucketHandler (err, bucket) {
  212. if (err) {
  213. console.trace(`${cst.PM2_IO_MSG_ERR} Failed to connect to the bucket: ${err.message}`)
  214. if (bucket) {
  215. console.error(`${cst.PM2_IO_MSG_ERR} You can retry using: pm2 plus link ${bucket.secret_id} ${bucket.public_id}`)
  216. }
  217. console.error(`${cst.PM2_IO_MSG_ERR} You can also contact us to get help: contact@pm2.io`)
  218. return process.exit(0)
  219. }
  220. if (bucket === undefined) {
  221. return process.exit(0)
  222. }
  223. console.log(`${cst.PM2_IO_MSG} Successfully connected to bucket ${bucket.name}`)
  224. var targetURL = `https://app.pm2.io/#/bucket/${bucket._id}`
  225. console.log(`${cst.PM2_IO_MSG} You can use the web interface over there: ${targetURL}`)
  226. this.open(targetURL)
  227. return process.exit(0)
  228. }
  229. static createBucket (cb) {
  230. console.log(`${cst.PM2_IO_MSG} By default we allow you to trial PM2 Plus for 14 days without any credit card.`)
  231. this.io.bucket.create({
  232. name: 'PM2 Plus Monitoring'
  233. }).then(res => {
  234. const bucket = res.data.bucket
  235. console.log(`${cst.PM2_IO_MSG} Successfully created the bucket`)
  236. this.pm2.link({
  237. public_key: bucket.public_id,
  238. secret_key: bucket.secret_id,
  239. pm2_version: pkg.version
  240. }, (err) => {
  241. if (err) {
  242. return cb(new Error('Failed to connect your local PM2 to your bucket'), bucket)
  243. } else {
  244. return cb(null, bucket)
  245. }
  246. })
  247. }).catch(err => {
  248. return cb(new Error(`Failed to create a bucket: ${err.message}`))
  249. })
  250. }
  251. /**
  252. * Connect the local agent to a specific bucket
  253. * @param {Function} cb
  254. */
  255. static connectToBucket (cb) {
  256. this.io.bucket.retrieveAll().then(res => {
  257. const buckets = res.data
  258. if (buckets.length === 0) {
  259. return this.createBucket(cb)
  260. }
  261. var table = new Table({
  262. style : {'padding-left' : 1, head : ['cyan', 'bold'], compact : true},
  263. head : ['Bucket name', 'Plan type']
  264. })
  265. buckets.forEach(function(bucket) {
  266. table.push([bucket.name, bucket.payment.offer_type])
  267. })
  268. console.log(table.toString())
  269. console.log(`${cst.PM2_IO_MSG} If you don't want to connect to a bucket, type 'none'`)
  270. const choices = buckets.map(bucket => bucket.name)
  271. choices.push('none')
  272. promptly.choose(`${cst.PM2_IO_MSG} Type the name of the bucket you want to connect to :`, choices, (err, value) => {
  273. if (value === 'none') return cb()
  274. const bucket = buckets.find(bucket => bucket.name === value)
  275. if (bucket === undefined) return cb()
  276. this.pm2.link({
  277. public_key: bucket.public_id,
  278. secret_key: bucket.secret_id,
  279. pm2_version: pkg.version
  280. }, (err) => {
  281. return err ? cb(err) : cb(null, bucket)
  282. })
  283. })
  284. })
  285. }
  286. /**
  287. * Authenticate the user with either of the strategy
  288. * @param {Function} cb
  289. */
  290. static authenticate () {
  291. this._strategy._retrieveTokens((err, tokens) => {
  292. if (err) {
  293. const msg = err.data ? err.data.error_description || err.data.msg : err.message
  294. console.log(`${cst.PM2_IO_MSG_ERR} Unexpected error : ${msg}`)
  295. return process.exit(1)
  296. }
  297. console.log(`${cst.PM2_IO_MSG} Successfully authenticated`)
  298. this.io.user.retrieve().then(res => {
  299. const user = res.data
  300. this.io.user.retrieve().then(res => {
  301. const tmpUser = res.data
  302. console.log(`${cst.PM2_IO_MSG} Successfully validated`)
  303. this.connectToBucket(this.createBucketHandler.bind(this))
  304. })
  305. })
  306. })
  307. }
  308. static open (target, appName, callback) {
  309. let opener
  310. const escape = function (s) {
  311. return s.replace(/"/g, '\\"')
  312. }
  313. if (typeof (appName) === 'function') {
  314. callback = appName
  315. appName = null
  316. }
  317. switch (process.platform) {
  318. case 'darwin': {
  319. opener = appName ? `open -a "${escape(appName)}"` : `open`
  320. break
  321. }
  322. case 'win32': {
  323. opener = appName ? `start "" ${escape(appName)}"` : `start ""`
  324. break
  325. }
  326. default: {
  327. opener = appName ? escape(appName) : `xdg-open`
  328. break
  329. }
  330. }
  331. if (process.env.SUDO_USER) {
  332. opener = 'sudo -u ' + process.env.SUDO_USER + ' ' + opener
  333. }
  334. return exec(`${opener} "${escape(target)}"`, callback)
  335. }
  336. }