289 lines
8.9 KiB
JavaScript
289 lines
8.9 KiB
JavaScript
'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)
|
|
})
|
|
}
|
|
}
|