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): use Google OAuth 2.0 #223

Merged
merged 7 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
98 changes: 74 additions & 24 deletions packages/shorebird_cli/lib/src/auth/auth.dart
Original file line number Diff line number Diff line change
@@ -1,54 +1,104 @@
import 'dart:convert';
import 'dart:io';

import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/auth/session.dart';
import 'package:shorebird_cli/src/config/config.dart';

export 'package:googleapis_auth/googleapis_auth.dart' show AccessCredentials;

final _clientId = ClientId(
/// Shorebird CLI's OAuth 2.0 identifier.
'30552215580-6kgm623s9l4te0rd63r18c5u6b6gkts5.apps.googleusercontent.com',

/// Shorebird CLI's OAuth 2.0 secret.
///
/// This isn't actually meant to be kept secret.
'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',
felangel marked this conversation as resolved.
Show resolved Hide resolved
);
final _scopes = ['openid', 'https://www.googleapis.com/auth/userinfo.email'];

typedef ObtainAccessCredentials = Future<AccessCredentials> Function(
ClientId clientId,
List<String> scopes,
http.Client client,
void Function(String) userPrompt,
);

class Auth {
Auth() {
_loadSession();
Auth({
http.Client? httpClient,
ObtainAccessCredentials? obtainAccessCredentials,
}) : _httpClient = httpClient ?? http.Client(),
_obtainAccessCredentials =
obtainAccessCredentials ?? obtainAccessCredentialsViaUserConsent {
_loadCredentials();
}

static const _sessionFileName = 'shorebird-session.json';
final sessionFilePath = p.join(shorebirdConfigDir, _sessionFileName);
static const _credentialsFileName = 'credentials.json';

final http.Client _httpClient;
final ObtainAccessCredentials _obtainAccessCredentials;
final credentialsFilePath = p.join(shorebirdConfigDir, _credentialsFileName);

void login({required String apiKey}) {
_session = Session(apiKey: apiKey);
_flushSession(_session!);
http.Client get client {
if (credentials == null) return _httpClient;
return autoRefreshingClient(_clientId, credentials!, _httpClient);
}

void logout() => _clearSession();
Future<void> login(void Function(String) prompt) async {
if (credentials != null) return;

Session? _session;
final client = http.Client();
try {
_credentials = await _obtainAccessCredentials(
_clientId,
_scopes,
client,
prompt,
);
_flushCredentials(_credentials!);
} finally {
client.close();
}
}

void logout() => _clearCredentials();

AccessCredentials? _credentials;

Session? get currentSession => _session;
AccessCredentials? get credentials => _credentials;

void _loadSession() {
final sessionFile = File(sessionFilePath);
void _loadCredentials() {
final credentialsFile = File(credentialsFilePath);

if (sessionFile.existsSync()) {
if (credentialsFile.existsSync()) {
try {
final contents = sessionFile.readAsStringSync();
_session = Session.fromJson(
final contents = credentialsFile.readAsStringSync();
_credentials = AccessCredentials.fromJson(
json.decode(contents) as Map<String, dynamic>,
);
} catch (_) {}
}
}

void _flushSession(Session session) {
File(sessionFilePath)
void _flushCredentials(AccessCredentials credentials) {
File(credentialsFilePath)
..createSync(recursive: true)
..writeAsStringSync(json.encode(session.toJson()));
..writeAsStringSync(json.encode(credentials.toJson()));
}

void _clearSession() {
_session = null;
void _clearCredentials() {
_credentials = null;

final sessionFile = File(sessionFilePath);
if (sessionFile.existsSync()) {
sessionFile.deleteSync(recursive: true);
final credentialsFile = File(credentialsFilePath);
if (credentialsFile.existsSync()) {
credentialsFile.deleteSync(recursive: true);
}
}

void close() {
_httpClient.close();
}
}
13 changes: 0 additions & 13 deletions packages/shorebird_cli/lib/src/auth/session.dart

This file was deleted.

3 changes: 2 additions & 1 deletion packages/shorebird_cli/lib/src/command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:http/http.dart' as http;
import 'package:mason_logger/mason_logger.dart';
import 'package:meta/meta.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
Expand All @@ -12,7 +13,7 @@ import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
typedef HashFunction = String Function(List<int> bytes);

typedef CodePushClientBuilder = CodePushClient Function({
required String apiKey,
required http.Client httpClient,
Uri? hostedUri,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ Defaults to the name in "pubspec.yaml".''',

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in.');
return ExitCode.noUser.code;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ Defaults to the app_id in "shorebird.yaml".''',

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in.');
return ExitCode.noUser.code;
}
Expand All @@ -56,7 +55,7 @@ Defaults to the app_id in "shorebird.yaml".''',
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,13 @@ class ListAppsCommand extends ShorebirdCommand with ShorebirdConfigMixin {

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in.');
return ExitCode.noUser.code;
}

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

Expand Down
2 changes: 1 addition & 1 deletion packages/shorebird_cli/lib/src/commands/build_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class BuildCommand extends ShorebirdCommand

@override
Future<int> run() async {
if (auth.currentSession == null) {
if (auth.credentials == null) {
logger
..err('You must be logged in to build.')
..err("Run 'shorebird login' to log in and try again.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@ class CreateChannelsCommand extends ShorebirdCommand with ShorebirdConfigMixin {

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in to view channels.');
return ExitCode.noUser.code;
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,13 @@ class ListChannelsCommand extends ShorebirdCommand with ShorebirdConfigMixin {

@override
Future<int>? run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in to view channels.');
return ExitCode.noUser.code;
}

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

Expand Down
5 changes: 2 additions & 3 deletions packages/shorebird_cli/lib/src/commands/init_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ class InitCommand extends ShorebirdCommand

@override
Future<int> run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in.');
return ExitCode.noUser.code;
}
Expand Down Expand Up @@ -56,7 +55,7 @@ Please make sure you are running "shorebird init" from the root of your Flutter

if (shorebirdYaml != null) {
final codePushClient = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);

Expand Down
25 changes: 15 additions & 10 deletions packages/shorebird_cli/lib/src/commands/login_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,37 @@ class LoginCommand extends ShorebirdCommand {

@override
Future<int> run() async {
final session = auth.currentSession;
if (session != null) {
final credentials = auth.credentials;
if (credentials != null) {
logger
..info('You are already logged in.')
..info("Run 'shorebird logout' to log out and try again.");
return ExitCode.success.code;
}

final apiKey = logger.prompt(
'${lightGreen.wrap('?')} Please enter your API Key:',
);
final loginProgress = logger.progress('Logging into shorebird.dev');
try {
auth.login(apiKey: apiKey);
loginProgress.complete();
await auth.login(_prompt);
logger.info('''

🎉 ${lightGreen.wrap('Welcome to Shorebird! You are now logged in.')}

🔑 Credentials are stored in ${lightCyan.wrap(auth.sessionFilePath)}.
🔑 Credentials are stored in ${lightCyan.wrap(auth.credentialsFilePath)}.
🚪 To logout use: "${lightCyan.wrap('shorebird logout')}".''');
return ExitCode.success.code;
} catch (error) {
loginProgress.fail();
logger.err(error.toString());
return ExitCode.software.code;
}
}

void _prompt(String url) {
logger.info('''
Shorebird needs your authorization to manage apps, releases, and patches on your behalf.
felangel marked this conversation as resolved.
Show resolved Hide resolved

In a browser, visit this URL to log in:

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

Waiting for your authorization...''');
}
}
3 changes: 1 addition & 2 deletions packages/shorebird_cli/lib/src/commands/logout_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ class LogoutCommand extends ShorebirdCommand {

@override
Future<int> run() async {
final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.info('You are already logged out.');
return ExitCode.success.code;
}
Expand Down
5 changes: 2 additions & 3 deletions packages/shorebird_cli/lib/src/commands/patch_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ class PatchCommand extends ShorebirdCommand
return ExitCode.config.code;
}

final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in to publish.');
return ExitCode.noUser.code;
}
Expand Down Expand Up @@ -147,7 +146,7 @@ class PatchCommand extends ShorebirdCommand
final pubspecYaml = getPubspecYaml()!;
final shorebirdYaml = getShorebirdYaml()!;
final codePushClient = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);
final version = pubspecYaml.version!;
Expand Down
5 changes: 2 additions & 3 deletions packages/shorebird_cli/lib/src/commands/release_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ make smaller updates to your app.
return ExitCode.config.code;
}

final session = auth.currentSession;
if (session == null) {
if (auth.credentials == null) {
logger.err('You must be logged in to release.');
return ExitCode.noUser.code;
}
Expand Down Expand Up @@ -117,7 +116,7 @@ make smaller updates to your app.
final pubspecYaml = getPubspecYaml()!;
final shorebirdYaml = getShorebirdYaml()!;
final codePushClient = buildCodePushClient(
apiKey: session.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);
final version = pubspecYaml.version!;
Expand Down
2 changes: 1 addition & 1 deletion packages/shorebird_cli/lib/src/commands/run_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class RunCommand extends ShorebirdCommand

@override
Future<int> run() async {
if (auth.currentSession == null) {
if (auth.credentials == null) {
logger
..err('You must be logged in to run.')
..err("Run 'shorebird login' to log in and try again.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ mixin ShorebirdCreateAppMixin on ShorebirdConfigMixin {
}

final client = buildCodePushClient(
apiKey: auth.currentSession!.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);

Expand Down
2 changes: 1 addition & 1 deletion packages/shorebird_cli/lib/src/shorebird_engine_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ mixin ShorebirdEngineMixin on ShorebirdConfigMixin {
final engineArchivePath = p.join(tempDir.path, 'engine.zip');
try {
final codePushClient = buildCodePushClient(
apiKey: auth.currentSession!.apiKey,
httpClient: auth.client,
hostedUri: hostedUri,
);
await _downloadShorebirdEngine(codePushClient, engineArchivePath);
Expand Down
1 change: 1 addition & 0 deletions packages/shorebird_cli/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies:
cli_util: ^0.4.0
collection: ^1.17.1
crypto: ^3.0.2
googleapis_auth: ^1.4.0
http: ^0.13.5
json_annotation: ^4.8.0
mason_logger: ^0.2.4
Expand Down
Loading