mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-01-18 18:56:25 +00:00
Merge pull request #1879 from balena-io/convert-login
Convert commands login, logout, whoami to oclif.
This commit is contained in:
commit
9e98e7142c
@ -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',
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
188
lib/actions-oclif/login.ts
Normal 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);
|
||||
}
|
||||
}
|
36
lib/actions-oclif/logout.ts
Normal file
36
lib/actions-oclif/logout.ts
Normal 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();
|
||||
}
|
||||
}
|
54
lib/actions-oclif/whoami.ts
Normal file
54
lib/actions-oclif/whoami.ts
Normal 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',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -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',
|
||||
]),
|
||||
);
|
||||
},
|
||||
};
|
@ -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';
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user