UNPKG

now-flow

Version:

Add deployment workflows to Zeit now

291 lines (258 loc) 7.91 kB
// node const { parse: parseUrl } = require('url') const { encode: encodeQuery, stringify: formUrlEncode } = require('querystring') const { createServer } = require('http') // theirs const opn = require('opn') const fetch = require('node-fetch') const debug = require('debug')('now:gcp:login') // ours const error = require('../../util/output/error') const aborted = require('../../util/output/aborted') const info = require('../../util/output/info') const cmd = require('../../util/output/cmd') const link = require('../../util/output/link') const highlight = require('../../util/output/highlight') const ready = require('../../util/output/ready') const param = require('../../util/output/param') const promptBool = require('../../util/input/prompt-bool') const getNowDir = require('../../config/global-path') const humanize = require('../../util/humanize-path') const saveCredentials = require('./util/save-credentials') const promptList = require('../../util/input/list') const listProjects = require('./list-projects') const { writeToAuthConfigFile } = require('../../util/config-files') // ports that are authorized in the GCP app const PORTS = [8085, 8086, 8087, 8088] const CLIENT_ID = '258013614557-0qulvq65vqk8pi9akn7igqsquejjffil.apps.googleusercontent.com' const CLIENT_SECRET = 'SvmeeRFmKQkIe_ZQHSe1UJ-O' // instructs gcp to send the response in the query string const RESPONSE_TYPE = 'code' const SCOPES = [ 'https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/appengine.admin', 'https://www.googleapis.com/auth/compute', 'https://www.googleapis.com/auth/accounts.reauth' ] // instructs gcp to return a `refresh_token` that we'll use to seamlessly // get a new auth token every time the current one expires const ACCESS_TYPE = 'offline' // url we'll send the user to const USER_URL = 'https://accounts.google.com/o/oauth2/v2/auth' // url we'll get the access tokens from and refresh the token when needed const TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' // required by oauth2's spec const GRANT_TYPE = 'authorization_code' // this ensures google *always* asks the user for permission // we enfore this to make sure we *always* receive a `refresh_token` (if the // is already authorized by the user, a `refresh_token` will *not* // be returned) since we need it const PROMPT_CONSENT = 'consent' /*eslint-disable */ const getProcess = () => process /*eslint-enable */ const serverListen = ({ server, port }) => { return new Promise((resolve, reject) => { server.on('error', reject) // will happen if the port is already in use server.listen(port, resolve) }) } const login = ctx => new Promise(async resolve => { let credentialsIndex = ctx.authConfig.credentials.findIndex( cred => cred.provider === 'gcp' ) if (credentialsIndex !== -1) { // the user is already logged into gcp let yes try { yes = await promptBool( info( 'You already have GCP credentials – this will replace them.', ' Do you want to continue?' ) ) } catch (err) { // promptBool only `reject`s upon user abort // let's set it to false just to make it clear yes = false } if (!yes) { console.log(aborted('No changes made.')) return } } const ports = [...PORTS] const server = createServer(async function handleRequest(req, res) { const { query: { error: _error, code } } = parseUrl(req.url, true) if (!_error && !code) { // the browser requesting the favicon etc res.end('') return } res.setHeader('content-type', 'text/html') res.end( '<meta charset="UTF-8">' + '<h2>That\'s it – you can now return to your terminal!</h2>' ) if (_error) { // the user didn't give us permission console.log(aborted('No changes made.')) return } if (code) { // that's right after the user gave us permission // let's exchange the authorization code for an access + refresh codes const body = formUrlEncode({ code, client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: `http://${req.headers.host}`, grant_type: GRANT_TYPE }) const opts = { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', 'content-length': body.length // just in case }, body: body } let accessToken let expiresIn let refreshToken let response try { response = await fetch(TOKEN_URL, opts) if (response.status !== 200) { debug( `HTTP ${response.status} when trying to exchange the authorization code`, await response.text() ) console.log( error( `Got unexpected status code from Google: ${response.status}` ) ) return } } catch (err) { debug( 'unexpected error occurred while making the request to exchange the authorization code', err.message ) console.log( error( 'Unexpected error occurred while authenthing with Google', err.stack ) ) return } try { const json = await response.json() accessToken = json.access_token expiresIn = json.expires_in refreshToken = json.refresh_token } catch (err) { debug( 'unexpected error occurred while parsing the JSON from the exchange request', err.stack, 'got', await response.text() ) console.log( error( 'Unexpected error occurred while parsing the JSON response from Google', err.message ) ) return } const now = new Date() // `expires_in` is 3600 seconds const expiresAt = now.setSeconds(now.getSeconds() + expiresIn) ctx = saveCredentials({ ctx, accessToken, expiresAt, refreshToken, credentialsIndex }) const projects = await listProjects(ctx) const message = 'Select a project:' const choices = projects.map(project => { return { name: `${project.name} (${project.projectId})`, value: project.projectId, short: project.name } }) const projectId = await promptList({ message, choices, separator: false }) const { projectId: id, name } = projects.find( p => p.projectId === projectId ) credentialsIndex = ctx.authConfig.credentials.findIndex( cred => cred.provider === 'gcp' ) ctx.authConfig.credentials[credentialsIndex].project = { id, name } writeToAuthConfigFile(ctx.authConfig) console.log( ready( `Credentials and project saved in ${param(humanize(getNowDir()))}.` ) ) } resolve() }) let shouldRetry = true let portToTry = ports.shift() while (shouldRetry) { try { await serverListen({ server, port: portToTry }) shouldRetry = false // done, listening } catch (err) { if (ports.length) { // let's try again portToTry = ports.shift() } else { // we're out of ports to try shouldRetry = false } } } if (!server.listening) { console.log( error( 'Make sure you have one of the following TCP ports available:', ` ${PORTS.join(', ').replace()}` ) ) return } const query = { client_id: CLIENT_ID, redirect_uri: `http://localhost:${portToTry}`, response_type: RESPONSE_TYPE, scope: SCOPES.join(' '), access_type: ACCESS_TYPE, prompt: PROMPT_CONSENT } if(getProcess().platform === 'darwin' || getProcess().platform === 'win32') { opn(USER_URL + '?' + encodeQuery(query)) console.log(info('A Google Accounts login window has been opened in your default browser. Please log in there and check back here afterwards.')) } else { console.log(info( `We'll need you to grant us access to provision functions on your ${highlight('Google Cloud Platform')} account in order to comunicate with their API.`, `To provision a dedicated set of tokens for ${cmd('now')}, Go to ${link(USER_URL + '?' + encodeQuery(query))} and grant access to Now.` )) } }) module.exports = login