Merge pull request #1879 from balena-io/convert-login

Convert commands login, logout, whoami to oclif.
This commit is contained in:
bulldozer-balena[bot] 2020-06-24 11:13:10 +00:00 committed by GitHub
commit 9e98e7142c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 322 additions and 223 deletions

View File

@ -45,7 +45,11 @@ const capitanoDoc = {
},
{
title: 'Authentication',
files: ['build/actions/auth.js'],
files: [
'build/actions-oclif/login.js',
'build/actions-oclif/logout.js',
'build/actions-oclif/whoami.js',
],
},
{
title: 'Device',

View File

@ -395,11 +395,11 @@ application name or numeric ID
## login
Use this command to login to your balena account.
Login to your balena account.
This command will prompt you to login using the following login types:
- Web authorization: open your web browser and prompt you to authorize the CLI
- Web authorization: open your web browser and prompt to authorize the CLI
from the dashboard.
- Credentials: using email/password and 2FA.
@ -414,31 +414,41 @@ Examples:
$ balena login --credentials
$ balena login --credentials --email johndoe@gmail.com --password secret
### Arguments
#### TOKEN
### Options
#### --token, -t <token>
session token or API key
#### --web, -w
#### -w, --web
web-based login
#### --credentials, -c
#### -t, --token
session token or API key
#### -c, --credentials
credential-based login
#### --email, -e, -u <email>
#### -e, --email EMAIL
email
#### --password, -p <password>
#### -u, --user USER
#### -p, --password PASSWORD
password
## logout
Use this command to logout from your balena account.
Logout from your balena account.
Examples:
@ -446,7 +456,7 @@ Examples:
## whoami
Use this command to find out the current logged in username and email address.
Get the username and email address of the currently logged in user.
Examples:

View File

@ -65,7 +65,8 @@ export default class DeviceIdentifyCmd extends Command {
try {
await balena.models.device.identify(params.uuid);
} catch (e) {
if (e.message === 'Request error: No online device(s) found') {
// Expected message: 'Request error: No online device(s) found'
if (e.message?.toLowerCase().includes('online')) {
throw new ExpectedError(`Device ${params.uuid} is not online`);
} else {
throw e;

View File

@ -69,7 +69,8 @@ export default class DeviceShutdownCmd extends Command {
try {
await balena.models.device.shutdown(params.uuid, options);
} catch (e) {
if (e.message === 'Request error: No online device(s) found') {
// Expected message: 'Request error: No online device(s) found'
if (e.message?.toLowerCase().includes('online')) {
throw new ExpectedError(`Device ${params.uuid} is not online`);
} else {
throw e;

188
lib/actions-oclif/login.ts Normal file
View File

@ -0,0 +1,188 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flags } from '@oclif/command';
import { stripIndent } from 'common-tags';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getBalenaSdk } from '../utils/lazy';
import { ExpectedError } from '../errors';
interface FlagsDef {
token: boolean;
web: boolean;
credentials: boolean;
email?: string;
user?: string;
password?: string;
help: void;
}
interface ArgsDef {
token?: string;
}
export default class LoginCmd extends Command {
public static description = stripIndent`
Login to balena.
Login to your balena account.
This command will prompt you to login using the following login types:
- Web authorization: open your web browser and prompt to authorize the CLI
from the dashboard.
- Credentials: using email/password and 2FA.
- Token: using a session token or API key from the preferences page.
`;
public static examples = [
'$ balena login',
'$ balena login --web',
'$ balena login --token "..."',
'$ balena login --credentials',
'$ balena login --credentials --email johndoe@gmail.com --password secret',
];
public static args = [
{
// Capitano allowed -t to be type boolean|string, which oclif does not.
// So -t is now bool, and we check first arg for token content.
name: 'token',
hidden: true,
},
];
public static usage = 'login';
public static flags: flags.Input<FlagsDef> = {
web: flags.boolean({
char: 'w',
description: 'web-based login',
}),
token: flags.boolean({
char: 't',
description: 'session token or API key',
}),
credentials: flags.boolean({
char: 'c',
description: 'credential-based login',
}),
email: flags.string({
char: 'e',
description: 'email',
exclusive: ['user'],
dependsOn: ['credentials'],
}),
// Capitano version of this command had a second alias for email, 'u'.
// Using an oclif hidden flag to support the same behaviour.
user: flags.string({
char: 'u',
hidden: true,
exclusive: ['email'],
dependsOn: ['credentials'],
}),
password: flags.string({
char: 'p',
description: 'password',
dependsOn: ['credentials'],
}),
help: cf.help,
};
public static primary = true;
public async run() {
const { flags: options, args: params } = this.parse<FlagsDef, ArgsDef>(
LoginCmd,
);
const balena = getBalenaSdk();
const messages = await import('../utils/messages');
const balenaUrl = await balena.settings.get('balenaUrl');
// Consolidate user/email options
if (options.user != null) {
options.email = options.user;
}
console.log(messages.balenaAsciiArt);
console.log(`\nLogging in to ${balenaUrl}`);
await this.doLogin(options, params.token);
const username = await balena.auth.whoami();
console.info(`Successfully logged in as: ${username}`);
console.info(`\
Find out about the available commands by running:
$ balena help
${messages.reachingOut}`);
if (options.web) {
const { shutdownServer } = await import('../auth');
shutdownServer();
}
}
async doLogin(loginOptions: FlagsDef, token?: string): Promise<void> {
const patterns = await import('../utils/patterns');
const balena = getBalenaSdk();
// Token
if (loginOptions.token) {
if (!token) {
const form = await import('resin-cli-form');
token = await form.ask({
message: 'Session token or API key from the preferences page',
name: 'token',
type: 'input',
});
}
await balena.auth.loginWithToken(token!);
if (!(await balena.auth.whoami())) {
throw new ExpectedError('Token authentication failed');
}
return;
}
// Credentials
else if (loginOptions.credentials) {
return patterns.authenticate(loginOptions);
}
// Web
else if (loginOptions.web) {
const auth = await import('../auth');
await auth.login();
return;
}
// User had not selected login preference, prompt interactively
const loginType = await patterns.askLoginType();
if (loginType === 'register') {
const signupUrl = 'https://dashboard.balena-cloud.com/signup';
const open = await import('open');
open(signupUrl, { wait: false });
throw new ExpectedError(`Please sign up at ${signupUrl}`);
}
// Set login options flag from askLoginType, and run again
loginOptions[loginType] = true;
return this.doLogin(loginOptions);
}
}

View File

@ -0,0 +1,36 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { stripIndent } from 'common-tags';
import Command from '../command';
import { getBalenaSdk } from '../utils/lazy';
export default class LogoutCmd extends Command {
public static description = stripIndent`
Logout from balena.
Logout from your balena account.
`;
public static examples = ['$ balena logout'];
public static usage = 'logout';
public async run() {
this.parse<{}, {}>(LogoutCmd);
await getBalenaSdk().auth.logout();
}
}

View File

@ -0,0 +1,54 @@
/**
* @license
* Copyright 2016-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { stripIndent } from 'common-tags';
import Command from '../command';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
export default class WhoamiCmd extends Command {
public static description = stripIndent`
Get current username and email address.
Get the username and email address of the currently logged in user.
`;
public static examples = ['$ balena whoami'];
public static usage = 'whoami';
public static authenticated = true;
public async run() {
this.parse<{}, {}>(WhoamiCmd);
const balena = getBalenaSdk();
const [username, email, url] = await Promise.all([
balena.auth.whoami(),
balena.auth.getEmail(),
balena.settings.get('balenaUrl'),
]);
console.log(
getVisuals().table.vertical({ username, email, url }, [
'$account information$',
'username',
'email',
'url',
]),
);
}
}

View File

@ -1,196 +0,0 @@
/*
Copyright 2016-2020 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { CommandDefinition } from 'capitano';
import { getBalenaSdk, getVisuals } from '../utils/lazy';
export const login: CommandDefinition<
{},
{
token: string | boolean;
web: boolean;
credentials: boolean;
email: string;
password: string;
}
> = {
signature: 'login',
description: 'login to balena',
help: `\
Use this command to login to your balena account.
This command will prompt you to login using the following login types:
- Web authorization: open your web browser and prompt you to authorize the CLI
from the dashboard.
- Credentials: using email/password and 2FA.
- Token: using a session token or API key from the preferences page.
Examples:
$ balena login
$ balena login --web
$ balena login --token "..."
$ balena login --credentials
$ balena login --credentials --email johndoe@gmail.com --password secret\
`,
options: [
{
signature: 'token',
description: 'session token or API key',
parameter: 'token',
alias: 't',
},
{
signature: 'web',
description: 'web-based login',
boolean: true,
alias: 'w',
},
{
signature: 'credentials',
description: 'credential-based login',
boolean: true,
alias: 'c',
},
{
signature: 'email',
parameter: 'email',
description: 'email',
alias: ['e', 'u'],
},
{
signature: 'password',
parameter: 'password',
description: 'password',
alias: 'p',
},
],
primary: true,
async action(_params, options) {
type Options = typeof options;
const balena = getBalenaSdk();
const patterns = await import('../utils/patterns');
const messages = await import('../utils/messages');
const { exitWithExpectedError } = await import('../errors');
const doLogin = async (loginOptions: Options): Promise<void> => {
if (loginOptions.token != null) {
let token: string;
if (typeof loginOptions.token === 'string') {
token = loginOptions.token;
} else {
const form = await import('resin-cli-form');
token = await form.ask({
message: 'Session token or API key from the preferences page',
name: 'token',
type: 'input',
});
}
await balena.auth.loginWithToken(token);
if (!(await balena.auth.whoami())) {
exitWithExpectedError('Token authentication failed');
}
return;
} else if (loginOptions.credentials) {
return patterns.authenticate(loginOptions);
} else if (loginOptions.web) {
const auth = await import('../auth');
await auth.login();
return;
}
const loginType = await patterns.askLoginType();
if (loginType === 'register') {
const signupUrl = 'https://dashboard.balena-cloud.com/signup';
const open = await import('open');
open(signupUrl, { wait: false });
return exitWithExpectedError(`Please sign up at ${signupUrl}`);
}
loginOptions[loginType] = true;
return doLogin(loginOptions);
};
const balenaUrl = await balena.settings.get('balenaUrl');
console.log(messages.balenaAsciiArt);
console.log(`\nLogging in to ${balenaUrl}`);
await doLogin(options);
const username = await balena.auth.whoami();
console.info(`Successfully logged in as: ${username}`);
console.info(`\
Find out about the available commands by running:
$ balena help
${messages.reachingOut}`);
if (options.web) {
const { shutdownServer } = await import('../auth');
shutdownServer();
}
},
};
export const logout: CommandDefinition = {
signature: 'logout',
description: 'logout from balena',
help: `\
Use this command to logout from your balena account.
Examples:
$ balena logout\
`,
async action(_params) {
await getBalenaSdk().auth.logout();
},
};
export const whoami: CommandDefinition = {
signature: 'whoami',
description: 'get current username and email address',
help: `\
Use this command to find out the current logged in username and email address.
Examples:
$ balena whoami\
`,
permission: 'user',
async action() {
const balena = getBalenaSdk();
const [username, email, url] = await Promise.all([
balena.auth.whoami(),
balena.auth.getEmail(),
balena.settings.get('balenaUrl'),
]);
console.log(
getVisuals().table.vertical({ username, email, url }, [
'$account information$',
'username',
'email',
'url',
]),
);
},
};

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as auth from './auth';
import * as config from './config';
import * as device from './device';
import * as help from './help';
@ -26,7 +25,7 @@ import * as ssh from './ssh';
import * as tunnel from './tunnel';
import * as util from './util';
export { auth, device, logs, local, help, os, config, ssh, util, push, tunnel };
export { device, logs, local, help, os, config, ssh, util, push, tunnel };
export { build } from './build';

View File

@ -47,11 +47,6 @@ capitano.globalOption({
// ---------- Help Module ----------
capitano.command(actions.help.help);
// ---------- Auth Module ----------
capitano.command(actions.auth.login);
capitano.command(actions.auth.logout);
capitano.command(actions.auth.whoami);
// ---------- Device Module ----------
capitano.command(actions.device.init);

View File

@ -137,9 +137,11 @@ const EXPECTED_ERROR_REGEXES = [
/^BalenaDeviceNotFound/, // balena-sdk
/^BalenaExpiredToken/, // balena-sdk
/^Missing \w+$/, // Capitano,
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError, RequiredFlagError
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError
/Missing required flag/, // oclif parser: RequiredFlagError
/^Unexpected argument/, // oclif parser: UnexpectedArgsError
/to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError
/must also be provided when using/, // oclif parser (depends-on)
];
// Support unit testing of handleError

View File

@ -165,14 +165,17 @@ export const convertedCommands = [
'key:add',
'key:rm',
'leave',
'login',
'logout',
'note',
'os:configure',
'scan',
'settings',
'tags',
'tag:rm',
'tag:set',
'version',
'scan',
'whoami',
];
/**

View File

@ -120,9 +120,11 @@ describe('handleError() function', () => {
'Missing uuid', // Capitano
'Missing 1 required argument', // oclif
'Missing 2 required arguments', // oclif
'Unexpected argument',
'Unexpected arguments',
'to be one of',
'Missing required flag', // oclif
'Unexpected argument', // oclif
'Unexpected arguments', // oclif
'to be one of', // oclif
'must also be provided when using', // oclif
];
messagesToMatch.forEach((message) => {