Skip to content

Commit

Permalink
feat(globe_cli): API token support for cli login and deploy (#36)
Browse files Browse the repository at this point in the history
* feat: add fvm to gitignore

* feat: added api token login support

* feat: added support for token and project in deploy command

* feat: tests fixed

* chore: improved formatting

* chore: updated original typo
  • Loading branch information
kaziwaseef committed Feb 7, 2024
1 parent 5a148e7 commit 48b6e41
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 15 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ dist/
.nvim.lua
.idea
*.iml
.dart_tool
.dart_tool
.fvm
29 changes: 29 additions & 0 deletions packages/globe_cli/lib/src/commands/deploy_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ class DeployCommand extends BaseGlobeCommand {
..addFlag(
'logs',
help: 'Shows build logs for the deployment.',
)
..addOption(
'token',
abbr: 't',
help: 'Set the API token for deployment. Also needs --project',
)
..addOption(
'project',
abbr: 'p',
help:
'Set the project for deployment with API token. Used with --token',
);
}

Expand All @@ -43,6 +54,24 @@ class DeployCommand extends BaseGlobeCommand {
Future<int> run() async {
requireAuth();

if (argResults?['token'] != null && argResults?['project'] != null) {
final token = argResults!['token'] as String;
final project = argResults!['project'] as String;
api.auth.loginWithApiToken(jwt: token);

final organizations = await api.getOrganizations();
logger.detail('Found ${organizations.length} organizations');

if (organizations.isEmpty) {
logger.err(
'API Token provided is invalid or is not associated with any organizations.',
);
return ExitCode.usage.code;
}

scope.setScope(orgId: organizations.first.id, projectId: project);
}

// If there is no scope, ask the user to link the project.
if (!scope.hasScope()) {
await linkProject(logger: logger, api: api);
Expand Down
19 changes: 16 additions & 3 deletions packages/globe_cli/lib/src/commands/login_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ import '../utils/open_url.dart';
/// {@endtemplate}
class LoginCommand extends BaseGlobeCommand {
/// {@macro login_command}
LoginCommand({GlobeHttpServer? httpServer})
: _httpServer = httpServer ?? GlobeHttpServer();
LoginCommand({GlobeHttpServer? httpServer}) {
_httpServer = httpServer ?? GlobeHttpServer();
argParser.addOption(
'token',
abbr: 't',
help: 'Login with an API token instead of the browser.',
);
}

final GlobeHttpServer _httpServer;
late final GlobeHttpServer _httpServer;

@override
String get name => 'login';
Expand All @@ -35,6 +41,13 @@ class LoginCommand extends BaseGlobeCommand {
return ExitCode.success.code;
}

if (argResults?['token'] != null) {
final token = argResults!['token'] as String;
auth.loginWithApiToken(jwt: token);
logger.info('API token set.');
return ExitCode.success.code;
}

final sessionToken = await _httpServer.getSessionToken(
logger: logger,
onConnected: (port) async {
Expand Down
6 changes: 5 additions & 1 deletion packages/globe_cli/lib/src/utils/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ class GlobeApi {
return {
'X-Globe-Platform': 'globe_cli',
'X-Globe-Platform-Version': package_info.version,
if (currentSession != null)
if (currentSession != null &&
currentSession.authenticationMethod == AuthenticationMethod.jwt)
'Authorization': 'Bearer ${currentSession.jwt}',
if (currentSession != null &&
currentSession.authenticationMethod == AuthenticationMethod.apiToken)
'x-api-token': currentSession.jwt,
};
}

Expand Down
58 changes: 52 additions & 6 deletions packages/globe_cli/lib/src/utils/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,21 @@ class GlobeAuth {
///
/// Note this is still validated on the server side.
void login({required String jwt}) {
final session = _session = GlobeSession(jwt: jwt);
final session = _session = GlobeSession(
jwt: jwt,
authenticationMethod: AuthenticationMethod.jwt,
);
_flushSession(session);
}

/// Logs the user in with the given [jwt]. as an api token.
///
/// Note this is still validated on the server side.
void loginWithApiToken({required String jwt}) {
final session = _session = GlobeSession(
jwt: jwt,
authenticationMethod: AuthenticationMethod.apiToken,
);
_flushSession(session);
}

Expand Down Expand Up @@ -72,22 +86,54 @@ class GlobeAuth {
}
}

enum AuthenticationMethod {
/// The user is authenticated via a JWT token.
jwt,

/// The user is authenticated via an api token.
apiToken,
}

/// A data class representing a user's authentication session.
class GlobeSession {
/// Creates a session with the given [jwt].
const GlobeSession({required this.jwt});
/// Creates a session with the given [jwt] and [authenticationMethod].
const GlobeSession({
required this.jwt,
required this.authenticationMethod,
});

/// Creates a session from the given [json] object.
factory GlobeSession.fromJson(Map<String, Object?> json) {
return switch (json) {
{'jwt': final String jwt} => GlobeSession(jwt: jwt),
_ => throw ArgumentError(),
{
'jwt': final String jwt,
'authenticationMethod': final String authMethod,
} =>
GlobeSession(
jwt: jwt,
authenticationMethod: AuthenticationMethod.values.firstWhere(
(e) => e.name == authMethod,
orElse: () => throw ArgumentError('Invalid AuthenticationMethod'),
),
),
{
'jwt': final String jwt,
} =>
GlobeSession(
jwt: jwt,
authenticationMethod: AuthenticationMethod.jwt,
),
_ => throw ArgumentError('Invalid JSON object.')
};
}

/// The users JWT token, used for authentication via the API.
final String jwt;
final AuthenticationMethod authenticationMethod;

/// Converts this session to a JSON object.
Map<String, dynamic> toJson() => {'jwt': jwt};
Map<String, dynamic> toJson() => {
'jwt': jwt,
'authenticationMethod': authenticationMethod.name,
};
}
12 changes: 9 additions & 3 deletions packages/globe_cli/lib/src/utils/prompts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:pubspec_parse/pubspec_parse.dart';

import '../exit.dart';
import 'api.dart';
import 'auth.dart';
import 'env.dart';
import 'project_settings.dart';
import 'scope.dart';
Expand All @@ -30,7 +31,7 @@ Future<ScopeMetadata> linkProject({
}
} else {
if (!logger.confirm(
'❓ Link this project to a Globe ${cyan.wrap('"${Directory.current.path}"')}?',
'❓ Link this project to Globe ${cyan.wrap('"${Directory.current.path}"')}?',
defaultValue: true,
)) {
exitOverride(0);
Expand Down Expand Up @@ -290,8 +291,13 @@ Future<Project> selectProject(

// Select a project or create a new one.
final selectedProject = logger.chooseOne(
'❓ Please a project you want to deploy to:',
choices: [createSymbol, ...projects.map((p) => p.id)],
'❓ Please select a project you want to deploy to:',
choices: [
if (api.auth.currentSession?.authenticationMethod !=
AuthenticationMethod.apiToken)
createSymbol,
...projects.map((p) => p.id),
],
display: (choice) {
if (choice == createSymbol) {
return 'Create a new project';
Expand Down
2 changes: 1 addition & 1 deletion packages/globe_cli/test/login_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ void main() {

expect(
workspace.remoteAuthFile.readAsStringSync(),
'{"jwt":"my-token"}',
'{"jwt":"my-token","authenticationMethod":"jwt"}',
);
});

Expand Down

0 comments on commit 48b6e41

Please sign in to comment.