UNPKG

now-flow

Version:

Add deployment workflows to Zeit now

353 lines (292 loc) 8.39 kB
// Native const { stringify: stringifyQuery } = require('querystring') const { hostname } = require('os') // Packaages const fetch = require('node-fetch') const debug = require('debug')('now:sh:login') const promptEmail = require('email-prompt') const ms = require('ms') const { validate: validateEmail } = require('email-validator') const chalk = require('chalk') const mri = require('mri') // Utilities const ua = require('../util/ua') const error = require('../../../util/output/error') const aborted = require('../../../util/output/aborted') const wait = require('../../../util/output/wait') const highlight = require('../../../util/output/highlight') const info = require('../../../util/output/info') const ok = require('../../../util/output/ok') const cmd = require('../../../util/output/cmd') const ready = require('../../../util/output/ready') const param = require('../../../util/output/param') const eraseLines = require('../../../util/output/erase-lines') const sleep = require('../../../util/sleep') const getUser = require('../../../util/get-user') const { writeToAuthConfigFile, writeToConfigFile } = require('../../../util/config-files') const getNowDir = require('../../../config/global-path') const hp = require('../../../util/humanize-path') const logo = require('../../../util/output/logo') const exit = require('../../../util/exit') /*eslint-disable */ const getProcess = () => process /*eslint-enable */ const help = () => { console.log(` ${chalk.bold(`${logo} now login`)} <email> ${chalk.dim('Options:')} -h, --help Output usage information -A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline( 'FILE' )} Path to the local ${'`now.json`'} file -Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline( 'DIR' )} Path to the global ${'`.now`'} directory ${chalk.dim('Examples:')} ${chalk.gray('–')} Log into the Now platform ${chalk.cyan('$ now login')} ${chalk.gray('–')} Log in using a specific email address ${chalk.cyan('$ now login john@doe.com')} `) } // POSTs to /now/registration – either creates an account or performs a login // returns {token, securityCode} // token: should be used to verify the status of the login process // securityCode: will be sent to the user in the email body const getVerificationData = async ({ apiUrl, email }) => { const hyphens = new RegExp('-', 'g') const host = hostname().replace(hyphens, ' ').replace('.local', '') const tokenName = `Now CLI on ${host}` const data = JSON.stringify({ email, tokenName }) debug('POST /now/registration') let res try { res = await fetch(`${apiUrl}/now/registration`, { method: 'POST', headers: { 'Content-Type': 'application/json', /*eslint-disable */ 'Content-Length': Buffer.byteLength(data), /*eslint-enable */ 'User-Agent': ua }, body: data }) } catch (err) { debug('error fetching /now/registration: %O', err.stack) throw new Error( error( `An unexpected error occurred while trying to login: ${err.message}` ) ) } debug('parsing response from POST /now/registration') let body try { body = await res.json() } catch (err) { debug( 'error parsing the response from /now/registration as JSON – got %O', err.stack ) throw new Error( error( `An unexpected error occurred while trying to log in: ${err.message}` ) ) } if (!res.ok) { debug('error response from POST /now/registration: %d %j', res.status, body) const { error = {} } = body const message = error.code === 'invalid_email' ? 'Invalid email address' : `Unexpected error: ${error.message}` throw new Error(message) } return body } const verify = async ({ apiUrl, email, verificationToken }) => { const query = { email, token: verificationToken } debug('GET /now/registration/verify') let res try { res = await fetch( `${apiUrl}/now/registration/verify?${stringifyQuery(query)}`, { headers: { 'User-Agent': ua } } ) } catch (err) { debug('error fetching /now/registration/verify: $O', err.stack) throw new Error( error( `An unexpected error occurred while trying to verify your login: ${err.message}` ) ) } debug('parsing response from GET /now/registration/verify') let body try { body = await res.json() } catch (err) { debug( 'error parsing the response from /now/registration/verify: $O', err.stack ) throw new Error( error( `An unexpected error occurred while trying to verify your login: ${err.message}` ) ) } return body.token } const readEmail = async () => { let email try { email = await promptEmail({ start: info('Enter your email: ') }) } catch (err) { console.log() // \n if (err.message === 'User abort') { throw new Error(aborted('No changes made.')) } if (err.message === 'stdin lacks setRawMode support') { throw new Error( error( `Interactive mode not supported – please run ${cmd( 'now login you@domain.com' )}` ) ) } } console.log() // \n return email } const login = async ctx => { const argv = mri(ctx.argv.slice(2), { boolean: ['help'], alias: { help: 'h' } }) if (argv.help) { help() await exit(0) } argv._ = argv._.slice(1) const apiUrl = ctx.apiUrl let email let emailIsValid = false let stopSpinner const possibleAddress = argv._[0] // if the last arg is not the command itself, then maybe it's an email if (possibleAddress) { if (!validateEmail(possibleAddress)) { // if it's not a valid email, let's just error console.log(error(`Invalid email: ${param(possibleAddress)}.`)) return 1 } // valid email, no need to prompt the user email = possibleAddress } else { do { try { email = await readEmail() } catch (err) { let erase = '' if (err.message.includes('Aborted')) { // no need to keep the prompt if the user `ctrl+c`ed erase = eraseLines(2) } console.log(erase + err.message) return 1 } emailIsValid = validateEmail(email) if (!emailIsValid) { // let's erase the `> Enter email [...]` // we can't use `console.log()` because it appends a `\n` // we need this check because `email-prompt` doesn't print // anything if there's no TTY getProcess().stdout.write(eraseLines(2)) } } while (!emailIsValid) } let verificationToken let securityCode stopSpinner = wait('Sending you an email') try { const data = await getVerificationData({ apiUrl, email }) verificationToken = data.token securityCode = data.securityCode } catch (err) { stopSpinner() console.log(err.message) return 1 } stopSpinner() // Clear up `Sending email` success message getProcess().stdout.write(eraseLines(possibleAddress ? 1 : 2)) console.log(info( `We sent an email to ${highlight(email)}. Please follow the steps provided`, ` inside it and make sure the security code matches ${highlight(securityCode)}.` )) stopSpinner = wait('Waiting for your confirmation') let token while (!token) { try { await sleep(ms('1s')) token = await verify({ apiUrl, email, verificationToken }) } catch (err) { if (/invalid json response body/.test(err.message)) { // /now/registraton is currently returning plain text in that case // we just wait for the user to click on the link } else { stopSpinner() console.log(err.message) return 1 } } } stopSpinner() console.log(ok('Email confirmed')) stopSpinner = wait('Feching your personal details') let user try { user = await getUser({ apiUrl, token }) } catch (err) { stopSpinner() console.log(err) return 1 } const index = ctx.authConfig.credentials.findIndex(c => c.provider === 'sh') const obj = { provider: 'sh', token } if (index === -1) { // wasn't logged in before ctx.authConfig.credentials.push(obj) } else { // let's just replace the existing object ctx.authConfig.credentials[index] = obj } // NOTE: this will override any existing config for `sh` ctx.config.sh = { user } writeToAuthConfigFile(ctx.authConfig) writeToConfigFile(ctx.config) stopSpinner() console.log(ok('Fetched your personal details')) console.log( ready( `Authentication token and personal details saved in ${param( hp(getNowDir()) )}` ) ) return ctx } module.exports = login