diff --git a/CHANGELOG.md b/CHANGELOG.md index b86d7d3..c1a3f1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ +# 1.8.2 +- Updates CLI to support Raisely's expanded MFA options + +# 1.8.1 +- Fixes bug with logging in using MFA + # 1.7.0 - Now requires node v14+ LTS (esm no longer required) - Added better command-line experience (more logs) diff --git a/package.json b/package.json index 1162821..2a75775 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@raisely/cli", - "version": "1.8.1", + "version": "1.8.2", "description": "Raisely CLI for local development", "main": "./src/cli.js", "type": "module", diff --git a/src/actions/auth.js b/src/actions/auth.js index dceaf0a..be5089e 100644 --- a/src/actions/auth.js +++ b/src/actions/auth.js @@ -115,6 +115,13 @@ export async function login(body, opts = {}) { }); } +export async function logout() { + return await api({ + path: '/logout', + method: 'POST', + }); +} + export async function getToken(program, opts, warnEarly) { if (opts.$tokenFromEnv) return; let isNewToken = false; diff --git a/src/cli.js b/src/cli.js index e06b57d..721054a 100644 --- a/src/cli.js +++ b/src/cli.js @@ -21,6 +21,7 @@ const start = actionBuilder(() => import('./start.js')); const create = actionBuilder(() => import('./create.js')); const deploy = actionBuilder(() => import('./deploy.js')); const login = actionBuilder(() => import('./login.js')); +const logout = actionBuilder(() => import('./logout.js')); const local = actionBuilder(() => import('./local.js')); export async function cli() { @@ -63,6 +64,11 @@ export async function cli() { .description('Authenticate with the Raisely api') .action(login); + program + .command('logout') + .description('Logout from the Raisely api') + .action(logout); + program .command('local') .description('Start local development server for a single campaign.') diff --git a/src/helpers.js b/src/helpers.js index 109f8d6..1b41ab4 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -130,4 +130,20 @@ export function error(e, loader) { export function requiresMfa(e) { return e.subcode && e.subcode.startsWith('MFA required'); +} + +export function getMfaStrategy(e) { + // Extract if it's authy or authenticator + const subcodeArray = e.subcode.split(':'); + const authType = subcodeArray[1]; + + return { + mfaType: authType, + // if authenticator, we need to know whether to offer authy as alternative + hasAuthy: Boolean( + authType === 'AUTHY' || + (subcodeArray.length === 3 && + subcodeArray[2] === 'hasAuthy') + ) + } } \ No newline at end of file diff --git a/src/login.js b/src/login.js index cbadc53..d4a0ec7 100644 --- a/src/login.js +++ b/src/login.js @@ -4,7 +4,7 @@ import ora from 'ora'; import { login } from './actions/auth.js'; import { updateConfig } from './config.js'; -import { log, error, informUpdate, requiresMfa } from './helpers.js'; +import { log, error, informUpdate, requiresMfa, getMfaStrategy } from './helpers.js'; export async function doLogin(message) { if (message) log(message, 'white'); @@ -38,7 +38,8 @@ export async function doLogin(message) { return loginSucceed(loginLoader, loginBody); } catch (e) { if (requiresMfa(e)) { - return await loginWith2FA(loginLoader, credentials); + const mfaStrategy = getMfaStrategy(e) + return await loginWith2FA(loginLoader, credentials, mfaStrategy); } else { error(e, loginLoader); return false; @@ -46,8 +47,29 @@ export async function doLogin(message) { } } -async function loginWith2FA(loginLoader, credentials) { - loginLoader.info('Your account requires 2 factor authentication.'); +async function loginWith2FA(loginLoader, credentials, mfaStrategy) { + loginLoader.info(`Your account requires 2 factor authentication`); + let mfaType = mfaStrategy.mfaType; + if (mfaType === 'AUTHENTICATOR_APP' && mfaStrategy.hasAuthy) { + const choiceMfa = await selectMfaType(); + mfaType = choiceMfa.mfaType; + if (mfaType === 'AUTHY') { + // trigger login again with mfaType to send the prompt + try { + await login({ + ...credentials, + mfaType, + requestAdminToken: true, + }); + } catch (e) { + // don't throw error if just an error about missing MFA + if (!requiresMfa(e)) { + error(e, loginLoader); + return false; + } + } + } + } try { const response = await inquirer.prompt([ { @@ -65,6 +87,7 @@ async function loginWith2FA(loginLoader, credentials) { const loginBody = await login({ ...credentials, + mfaType, otp: response.otp, requestAdminToken: true, }); @@ -75,6 +98,31 @@ async function loginWith2FA(loginLoader, credentials) { } } +async function selectMfaType() { + const selectedMfa = await inquirer.prompt([ + { + type: 'list', + message: 'Select your preferred MFA', + name: 'mfaType', + choices: [ + { + name: 'Authenticator App', + value: 'AUTHENTICATOR_APP' + }, + { + name: 'SMS/Legacy', + value: 'AUTHY' + } + ], + validate: (value) => + value.length + ? true + : 'Please choose your preferred MFA', + }, + ]); + return selectedMfa; +} + async function loginSucceed(loginLoader, loginBody) { const { token, data: user } = loginBody; loginLoader.succeed(); diff --git a/src/logout.js b/src/logout.js new file mode 100644 index 0000000..7eee2c9 --- /dev/null +++ b/src/logout.js @@ -0,0 +1,18 @@ +import ora from 'ora'; +import { logout } from './actions/auth.js'; +import { updateConfig } from './config.js'; + +export default async function logoutAction() { + let logoutLoader = ora('Logging you out...').start(); + try { + await logout(); + logoutLoader.info(`You have been logged out`); + } catch (e) { + // don't throw error, continue + } finally { + await updateConfig({ + token: null, + }); + logoutLoader.stop() + } +}