123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- 'use strict'
- const AuthStrategy = require('@pm2/js-api/src/auth_strategies/strategy')
- const querystring = require('querystring');
- const http = require('http')
- const fs = require('fs')
- const url = require('url')
- const exec = require('child_process').exec
- const tryEach = require('async/tryEach')
- const path = require('path')
- const os = require('os')
- const needle = require('needle')
- const chalk = require('chalk')
- const cst = require('../../../../constants.js')
- const promptly = require('promptly')
- module.exports = class CliStrategy extends AuthStrategy {
- // the client will try to call this but we handle this part ourselves
- retrieveTokens (km, cb) {
- this.authenticated = false
- this.callback = cb
- this.km = km
- this.BASE_URI = 'https://id.keymetrics.io';
- }
- // so the cli know if we need to tell user to login/register
- isAuthenticated () {
- return new Promise((resolve, reject) => {
- if (this.authenticated) return resolve(true)
- let tokensPath = cst.PM2_IO_ACCESS_TOKEN
- fs.readFile(tokensPath, (err, tokens) => {
- if (err && err.code === 'ENOENT') return resolve(false)
- if (err) return reject(err)
- // verify that the token is valid
- try {
- tokens = JSON.parse(tokens || '{}')
- } catch (err) {
- fs.unlinkSync(tokensPath)
- return resolve(false)
- }
- // if the refresh tokens is here, the user could be automatically authenticated
- return resolve(typeof tokens.refresh_token === 'string')
- })
- })
- }
- verifyToken (refresh) {
- return this.km.auth.retrieveToken({
- client_id: this.client_id,
- refresh_token: refresh
- })
- }
- // called when we are sure the user asked to be logged in
- _retrieveTokens (optionalCallback) {
- const km = this.km
- const cb = this.callback
- tryEach([
- // try to find the token via the environment
- (next) => {
- if (!process.env.PM2_IO_TOKEN) {
- return next(new Error('No token in env'))
- }
- this.verifyToken(process.env.PM2_IO_TOKEN)
- .then((res) => {
- return next(null, res.data)
- }).catch(next)
- },
- // try to find it in the file system
- (next) => {
- fs.readFile(cst.PM2_IO_ACCESS_TOKEN, (err, tokens) => {
- if (err) return next(err)
- // verify that the token is valid
- tokens = JSON.parse(tokens || '{}')
- if (new Date(tokens.expire_at) > new Date(new Date().toISOString())) {
- return next(null, tokens)
- }
- this.verifyToken(tokens.refresh_token)
- .then((res) => {
- return next(null, res.data)
- }).catch(next)
- })
- },
- // otherwise make the whole flow
- (next) => {
- return this.authenticate((err, data) => {
- if (err instanceof Error) return next(err)
- // verify that the token is valid
- this.verifyToken(data.refresh_token)
- .then((res) => {
- return next(null, res.data)
- }).catch(next)
- })
- }
- ], (err, result) => {
- // if present run the optional callback
- if (typeof optionalCallback === 'function') {
- optionalCallback(err, result)
- }
- if (result.refresh_token) {
- this.authenticated = true
- let file = cst.PM2_IO_ACCESS_TOKEN
- fs.writeFile(file, JSON.stringify(result), () => {
- return cb(err, result)
- })
- } else {
- return cb(err, result)
- }
- })
- }
- authenticate (cb) {
- console.log(`${cst.PM2_IO_MSG} Using non-browser authentication.`)
- promptly.confirm(`${cst.PM2_IO_MSG} Do you have a pm2.io account? (y/n)`, (err, answer) => {
- // Either login or register
- return answer === true ? this.login(cb) : this.register(cb)
- })
- }
- login (cb) {
- let retry = () => {
- promptly.prompt(`${cst.PM2_IO_MSG} Your username or email: `, (err, username) => {
- if (err) return retry();
- promptly.password(`${cst.PM2_IO_MSG} Your password: `, { replace : '*' }, (err, password) => {
- if (err) return retry();
- console.log(`${cst.PM2_IO_MSG} Authenticating ...`)
- this._loginUser({
- username: username,
- password: password
- }, (err, data) => {
- if (err) {
- console.error(`${cst.PM2_IO_MSG_ERR} Failed to authenticate: ${err.message}`)
- return retry()
- }
- return cb(null, data)
- })
- })
- })
- }
- retry()
- }
- register (cb) {
- console.log(`${cst.PM2_IO_MSG} No problem ! We just need few informations to create your account`)
- var retry = () => {
- promptly.prompt(`${cst.PM2_IO_MSG} Please choose an username :`, {
- validator : this._validateUsername,
- retry : true
- }, (err, username) => {
- promptly.prompt(`${cst.PM2_IO_MSG} Please choose an email :`, {
- validator : this._validateEmail,
- retry : true
- },(err, email) => {
- promptly.password(`${cst.PM2_IO_MSG} Please choose a password :`, { replace : '*' }, (err, password) => {
- promptly.confirm(`${cst.PM2_IO_MSG} Do you accept the terms and privacy policy (https://pm2.io/legals/terms_conditions.pdf) ? (y/n)`, (err, answer) => {
- if (err) {
- console.error(chalk.bold.red(err));
- return retry()
- } else if (answer === false) {
- console.error(`${cst.PM2_IO_MSG_ERR} You must accept the terms and privacy policy to contiue.`)
- return retry()
- }
- this._registerUser({
- email : email,
- password : password,
- username : username
- }, (err, data) => {
- console.log('\n')
- if (err) {
- console.error(`${cst.PM2_IO_MSG_ERR} Unexpect error: ${err.message}`)
- console.error(`${cst.PM2_IO_MSG_ERR} You can also contact us to get help: contact@pm2.io`)
- return process.exit(1)
- }
- return cb(undefined, data)
- })
- })
- })
- })
- })
- }
- retry()
- }
- /**
- * Register function
- * @param opts.username
- * @param opts.password
- * @param opts.email
- */
- _registerUser (opts, cb) {
- const data = Object.assign(opts, {
- password_confirmation: opts.password,
- accept_terms: true
- })
- needle.post(this.BASE_URI + '/api/oauth/register', data, {
- json: true,
- headers: {
- 'X-Register-Provider': 'pm2-register',
- 'x-client-id': this.client_id
- }
- }, function (err, res, body) {
- if (err) return cb(err)
- if (body.email && body.email.message) return cb(new Error(body.email.message))
- if (body.username && body.username.message) return cb(new Error(body.username.message))
- if (!body.access_token) return cb(new Error(body.msg))
- return cb(null, {
- refresh_token : body.refresh_token.token,
- access_token : body.access_token.token
- })
- });
- }
- _loginUser (user_info, cb) {
- const URL_AUTH = '/api/oauth/authorize?response_type=token&scope=all&client_id=' +
- this.client_id + '&redirect_uri=http://localhost:43532';
- needle.get(this.BASE_URI + URL_AUTH, (err, res) => {
- if (err) return cb(err);
- var cookie = res.cookies;
- needle.post(this.BASE_URI + '/api/oauth/login', user_info, {
- cookies : cookie
- }, (err, resp, body) => {
- if (err) return cb(err)
- if (resp.statusCode != 200) return cb('Wrong credentials')
- var location = resp.headers['x-redirect']
- needle.get(this.BASE_URI + location, {
- cookies : cookie
- }, (err, res) => {
- if (err) return cb(err);
- var refresh_token = querystring.parse(url.parse(res.headers.location).query).access_token;
- needle.post(this.BASE_URI + '/api/oauth/token', {
- client_id : this.client_id,
- grant_type : 'refresh_token',
- refresh_token : refresh_token,
- scope : 'all'
- }, (err, res, body) => {
- if (err) return cb(err)
- return cb(null, body)
- })
- })
- })
- })
- }
- _validateEmail (email) {
- var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
- if (re.test(email) == false)
- throw new Error('Not an email');
- return email;
- }
- _validateUsername (value) {
- if (value.length < 6) {
- throw new Error('Min length of 6');
- }
- return value;
- };
- deleteTokens (km) {
- return new Promise((resolve, reject) => {
- // revoke the refreshToken
- km.auth.revoke()
- .then(res => {
- // remove the token from the filesystem
- let file = cst.PM2_IO_ACCESS_TOKEN
- fs.unlinkSync(file)
- return resolve(res)
- }).catch(reject)
- })
- }
- }
|