UNPKG

5 kBJavaScriptView Raw
1/*
2 * Copyright 2019 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12
13const readline = require('readline');
14const path = require('path');
15const { fetch } = require('@adobe/helix-fetch');
16const fse = require('fs-extra');
17const chalk = require('chalk');
18const opn = require('open');
19const AbstractCommand = require('./abstract.cmd.js');
20const LoginServer = require('./auth-login-server.js');
21const cliutils = require('./cli-util');
22
23const HELIX_APP_HOST = 'https://app.project-helix.io';
24
25class AuthCommand extends AbstractCommand {
26 constructor(logger) {
27 super(logger);
28 this._token = '';
29 this._showToken = true;
30 this._openBrowser = true;
31 // those are for testing
32 this._stdout = process.stdout;
33 this._stdin = process.stdin;
34 this._askAdd = null;
35 this._askFile = null;
36 }
37
38 // eslint-disable-next-line class-methods-use-this
39 get requireConfigFile() {
40 return false;
41 }
42
43 withOpenBrowser(value) {
44 this._openBrowser = value;
45 return this;
46 }
47
48 /**
49 * @override
50 */
51 async init() {
52 await super.init();
53
54 if (!this._stdout.isTTY) {
55 throw Error('Interactive authentication tool not supported on non interactive consoles.');
56 }
57 }
58
59 async receiveToken() {
60 const srv = new LoginServer()
61 .withLogger(this.log)
62 .withOrigin(HELIX_APP_HOST);
63 await srv.start();
64 this.emit('server-start', srv.port);
65 this._loginUrl = `${HELIX_APP_HOST}/login?p=${srv.port}`;
66
67 this._stdout.write(`
68Thank you for choosing the automatic Helix Bot Authentication process.
69I will shortly open a browser with the following url. In case the
70browser doesn't open automatically, please navigate to the url manually:
71
72 ${chalk.cyan(this._loginUrl)}
73
74Oh, and I started local server, bound to 127.0.0.1:${srv.port} and am
75awaiting the completion of the process. `);
76
77 if (this._openBrowser) {
78 setTimeout(() => {
79 opn(this._loginUrl, { wait: false, url: true });
80 }, 1000);
81 }
82 const spinner = cliutils.createSpinner().start();
83 this._token = await srv.waitForToken();
84 spinner.stop();
85 await srv.stop();
86 this.emit('server-stop');
87 this._stdout.write(`
88
89The authentication process has completed. Thank you.
90You can now close the browser window if it didn't close automatically.
91
92I received an access token that I can use to access the Helix Bot on your behalf:
93`);
94 }
95
96 async showUserInfo() {
97 // fetch some user info
98 const response = await fetch('https://api.github.com/user', {
99 headers: {
100 'User-Agent': 'HelixBot',
101 Authorization: `token ${this._token}`,
102 },
103 json: true,
104 });
105
106 if (!response.ok) {
107 const e = new Error(`${response.status} - "${await response.text()}"`);
108 e.statusCode = response.status;
109 throw e;
110 }
111
112 const userInfo = await response.json();
113
114 this._stdout.write(`\n${chalk.gray(' Name: ')}${userInfo.name}\n`);
115 this._stdout.write(`${chalk.gray(' Login: ')}${userInfo.login}\n\n`);
116 }
117
118 async run() {
119 await this.init();
120
121 await this.receiveToken();
122 await this.showUserInfo();
123
124 const rl = readline.createInterface({
125 input: this._stdin,
126 output: this._stdout,
127 terminal: true,
128 });
129
130 const answer = this._askAdd !== null ? this._askAdd : (await cliutils.prompt(rl, 'If you wish, I can add it to a file? [Y/n]: ')).toLowerCase();
131 if (answer === '' || answer === 'y' || answer === 'yes') {
132 const file = this._askFile !== null ? this._askFile : (await cliutils.prompt(rl, 'which file? [.env]: ')) || '.env';
133 const p = path.resolve(this.directory, file);
134 let env = '';
135 if (await fse.pathExists(p)) {
136 env = await fse.readFile(p, 'utf-8');
137 }
138 // check if token is already present
139 if (env.indexOf('HLX_GITHUB_TOKEN') >= 0) {
140 env = env.replace(/HLX_GITHUB_TOKEN\s*=\s*[^\s]+/, `HLX_GITHUB_TOKEN=${this._token}`);
141 } else {
142 if (env && !env.endsWith('\n')) {
143 env += '\n';
144 }
145 env = `${env}HLX_GITHUB_TOKEN=${this._token}\n`;
146 }
147 await fse.writeFile(p, env, 'utf-8');
148 this._showToken = false;
149 this._stdout.write(`\nAdded github token to: ${chalk.cyan(file)}\n\n`);
150 }
151
152 rl.close();
153
154 if (this._showToken) {
155 this._stdout.write(`
156Ok, you can add it manually, then. Here it is:
157
158 ${chalk.grey(`HLX_GITHUB_TOKEN=${this._token}`)}
159
160`);
161 }
162 }
163}
164
165module.exports = AuthCommand;