@swell/cli
Version:
Swell's command line interface/utility
112 lines (111 loc) • 4.26 kB
JavaScript
import { ux } from '@oclif/core';
import * as http from 'node:http';
import open from 'open';
import { getLoginHost } from './constants.js';
import { getProxyUrl } from './proxy.js';
import style from './style.js';
/**
* Get the session id for the passed in store. If the session is not found,
* open the browser to login and return the session id.
*
* @param storeId - The store to get the session id for
* @returns A promise that resolves to the session id
*/
function getSessionIdOrLoginInBrowser(storeId) {
return new Promise((resolve, _reject) => {
const onSessionId = (sessionId, storeId) => resolve([sessionId, storeId]);
createServer(onSessionId, storeId);
});
}
function createServer(onSessionId, storeId) {
// the spinner is used to show progress while waiting for the browser
const spinner = ux.action;
serverForLogin({
onPost(req) {
// we received the session id from the browser so we can move on
const sessionId = req.headers['x-session'];
const storeId = req.headers['x-store'];
if (sessionId) {
spinner.stop('Browser authentication successful');
onSessionId(sessionId, storeId);
}
},
async onStart(loginUrl) {
spinner.start([
"Open this link if your browser doesn't launch it automatically:",
`${style.link(loginUrl)}`,
].join(' '));
await open(loginUrl);
},
storeId,
});
}
/**
* Start a local HTTP server to handle the POST and GET requests from the
* browser login flow.
*
* The POST request will contain the session id in the `x-session` header.
*
* The GET request will be used to close the connection after the login flow
* is complete.
*
* @param onSessionId - A callback to run when the session id is received
* from the browser
* @returns void
*/
function serverForLogin({ onPost, onStart, storeId, }) {
// flag to track if the POST and GET requests have been handled
// we only want to handle them once
let postHandled = false;
let getHandled = false;
// Create the HTTP server
const server = http.createServer(async (req, res) => {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', getLoginHost(storeId));
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Origin, Content-Type, x-session, x-store');
if (req.method === 'POST' && !postHandled) {
onPost(req);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('POST request handled');
postHandled = true;
}
else if (req.method === 'GET' && !getHandled) {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
// Close the HTTP connection to prevent `server.close()` from hanging
res.setHeader('connection', 'close');
// The browser will show this message in the window
res.end('Authentication successful, you can close this window and return to the terminal.');
getHandled = true;
}
else if (req.method === 'OPTIONS') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('ok');
}
else {
// Return 404 for subsequent requests or other request types
res.statusCode = 404;
res.end('Not Found');
}
});
server.listen(0, async () => {
const serverAddr = server.address();
const proxyUrl = await getProxyUrl(serverAddr.port);
const localServerUrl = Buffer.from(proxyUrl).toString('base64');
const loginUrl = `${getLoginHost(storeId)}/admin?cli=${localServerUrl}`;
onStart(loginUrl);
});
const closeInterval = setInterval(() => {
if (postHandled) {
server.close(() => {
clearInterval(closeInterval);
});
server.emit('close');
server.removeAllListeners('close');
}
}, 100);
}
export { getSessionIdOrLoginInBrowser };