Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(shorebird_cli): Add account create command #334

Merged
merged 18 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions packages/shorebird_cli/lib/src/auth/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@ class Auth {
);
}

Future<void> login(void Function(String) prompt) async {
/// Logs the string returned by [prompt] to the console and obtains auth
/// credentials. If [verifyEmail] is true, check that there is a Shorebird
/// user with the same email address as the newly-authenticated user.
Future<void> login(
void Function(String) prompt, {
bool verifyEmail = true,
bryanoltman marked this conversation as resolved.
Show resolved Hide resolved
}) async {
if (_credentials != null) return;

final client = http.Client();
Expand All @@ -123,11 +129,22 @@ class Auth {
prompt,
);

final codePushClient = _buildCodePushClient(httpClient: this.client);

final user = await codePushClient.getCurrentUser();

_email = user.email;
if (_credentials?.email == null) {
throw Exception('Failed to obtain access credentials.');
}

if (verifyEmail) {
final codePushClient = _buildCodePushClient(httpClient: this.client);
final user = await codePushClient.getCurrentUser();
if (user.email != _credentials!.email) {
throw Exception(
'The email address of the authenticated user does not match the '
'email address of the user in the Shorebird database.',
);
}
}

_email = _credentials!.email;
_flushCredentials(_credentials!);
} finally {
client.close();
Expand Down
2 changes: 2 additions & 0 deletions packages/shorebird_cli/lib/src/commands/account/account.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'account_command.dart';
export 'create_account_command.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/commands/commands.dart';

/// {@template account_command}
/// `shorebird account`
/// Manage your Shorebird account.
/// {@endtemplate}
class AccountCommand extends ShorebirdCommand {
/// {@macro account_command}
AccountCommand({required super.logger, super.auth}) {
addSubcommand(CreateAccountCommand(logger: logger));
}

@override
String get name => 'account';

@override
String get description => 'Manage your Shorebird account.';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'dart:async';

import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';

/// {@template create_account_command}
/// `shorebird account create`
/// Create a new Shorebird account.
/// {@endtemplate}
class CreateAccountCommand extends ShorebirdCommand with ShorebirdConfigMixin {
/// {@macro create_account_command}
CreateAccountCommand({
required super.logger,
super.auth,
super.buildCodePushClient,
});

@override
String get description => 'Create a new Shorebird account.';

@override
String get name => 'create';

@override
Future<int> run() async {
final CodePushClient client;

if (!auth.isAuthenticated) {
try {
await auth.login(prompt, verifyEmail: false);
client = buildCodePushClient(
bryanoltman marked this conversation as resolved.
Show resolved Hide resolved
httpClient: auth.client,
hostedUri: hostedUri,
);
} catch (error) {
logger.err(error.toString());
return ExitCode.software.code;
}
} else {
// If the user already has a JWT, check if they already have an account.
logger.info(
'Already logged in as ${auth.email}, checking for existing account',
);

client = buildCodePushClient(
httpClient: auth.client,
hostedUri: hostedUri,
);

try {
final user = await client.getCurrentUser();
logger.info('''
You already have an account, ${user.displayName}!

To subscribe, run ${green.wrap('shorebird account subscribe')}.
bryanoltman marked this conversation as resolved.
Show resolved Hide resolved
''');
return ExitCode.success.code;
} catch (_) {}
}

logger.info('Authorized as ${auth.email}');

final name = logger.prompt('What is your name?');

final progress = logger.progress('Creating account');
try {
final newUser = await client.createUser(name: name);
final paymentLink = await client.createPaymentLink();
progress.complete(
'''
🎉 ${lightGreen.wrap('Welcome to Shorebird, ${newUser.displayName}! You have successfully created an account as <${auth.email}>.')}

🔑 Credentials are stored in ${lightCyan.wrap(auth.credentialsFilePath)}.
🚪 To logout, use: "${lightCyan.wrap('shorebird logout')}".

The next step is to purchase a Shorebird subscription. To subcribe, visit ${lightCyan.wrap('$paymentLink')} or run ${green.wrap('shorebird account subscribe')} later.
''',
);
return ExitCode.success.code;
} catch (error) {
progress.fail(error.toString());
return ExitCode.software.code;
}
}

void prompt(String url) {
logger.info('''
Shorebird is currently only open to trusted testers. To participate, you will need a Google account for authentication.
bryanoltman marked this conversation as resolved.
Show resolved Hide resolved

The first step is to sign in with a Google account. Please follow the sign-in link below:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow the link to sign-in with your Google account:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with "Follow the link below to authenticate:" because "your Google account" felt redundant after "Shorebird currently requires a Google account for authentication". lmk what you think


${styleBold.wrap(styleUnderlined.wrap(lightCyan.wrap(url)))}

Waiting for your authorization...''');
}
}
29 changes: 0 additions & 29 deletions packages/shorebird_cli/lib/src/commands/account_command.dart

This file was deleted.

2 changes: 1 addition & 1 deletion packages/shorebird_cli/lib/src/commands/commands.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export 'account_command.dart';
export 'account/account.dart';
export 'apps/apps.dart';
export 'build/build_command.dart';
export 'cache/cache.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:shorebird_cli/src/validators/validators.dart';
import 'package:shorebird_cli/src/version.dart';

/// {@template doctor_command}
///
/// `shorebird doctor`
/// A command that checks for potential issues with the current shorebird
/// environment.
Expand Down
9 changes: 8 additions & 1 deletion packages/shorebird_cli/lib/src/commands/login_command.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';

/// {@template login_command}
///
/// `shorebird login`
/// Login as a new Shorebird user.
/// {@endtemplate}
Expand Down Expand Up @@ -34,6 +34,13 @@ class LoginCommand extends ShorebirdCommand {
🔑 Credentials are stored in ${lightCyan.wrap(auth.credentialsFilePath)}.
🚪 To logout use: "${lightCyan.wrap('shorebird logout')}".''');
return ExitCode.success.code;
} on UserNotFoundException {
logger.err(
"""

We don't recognize that email address. Run the `shorebird account create` command to create an account.""",
);
return ExitCode.software.code;
} catch (error) {
logger.err(error.toString());
return ExitCode.software.code;
Expand Down
27 changes: 27 additions & 0 deletions packages/shorebird_cli/test/src/auth/auth_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,33 @@ void main() {
expect(auth.email, isNull);
expect(auth.isAuthenticated, isFalse);
});

test(
"throws exception if credentials email doesn't match current user",
() async {
when(() => codePushClient.getCurrentUser()).thenAnswer(
(_) async => const User(
id: 123,
email: 'email@email.com',
),
);

await expectLater(auth.login((_) {}), throwsException);

expect(auth.email, isNull);
expect(auth.isAuthenticated, isFalse);
},
);

test(
'does not fetch current user if verifyEmail is false',
() async {
await auth.login((_) {}, verifyEmail: false);
verifyNever(() => codePushClient.getCurrentUser());
expect(buildAuth().email, email);
expect(buildAuth().isAuthenticated, isTrue);
},
);
});

group('logout', () {
Expand Down
Loading