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: Token create & delete from CLI #39

Merged
merged 23 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 19 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
47 changes: 47 additions & 0 deletions docs/cli/commands/token.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: Globe Tokens
description: Create, Delete & List globe auth tokens from the command line.
---

# Create

The `create` command allows you to create auth tokens for your projects. You can use this token to
login to Globe in any environment.

## Usage

You can run the command interactively by running

```bash
globe token create
```

or in-lined by providing necessary arguments

- `--name`- specify name to identity token.
codekeyz marked this conversation as resolved.
Show resolved Hide resolved
- `--expiry` - specify lifespan of token.
codekeyz marked this conversation as resolved.
Show resolved Hide resolved
- `--project` - specify projects(s) to associate token with.
codekeyz marked this conversation as resolved.
Show resolved Hide resolved

```bash
globe token create --name="Foo Bar" --expiry="yyyy-mm-dd" --project="project-ids-go-here"
```

# List Tokens

The `list` command lists all tokens associated with the current project.

## Usage

```bash
globe token list
```

# Delete Token

The `delete` command allows you to delete token by providing token ID.

## Usage

```bash
globe token delete
codekeyz marked this conversation as resolved.
Show resolved Hide resolved
```
1 change: 1 addition & 0 deletions packages/globe_cli/lib/src/command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class GlobeCliCommandRunner extends CompletionCommandRunner<int> {
addCommand(LinkCommand());
addCommand(UnlinkCommand());
addCommand(BuildLogsCommand());
addCommand(TokenCommand());
}

final Logger _logger;
Expand Down
1 change: 1 addition & 0 deletions packages/globe_cli/lib/src/commands/commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export 'deploy_command.dart';
export 'link_command.dart';
export 'login_command.dart';
export 'logout_command.dart';
export 'token_command.dart';
export 'unlink_command.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import 'dart:async';

import 'package:mason_logger/mason_logger.dart';

import '../../command.dart';
import '../../exit.dart';
import '../../utils/api.dart';
import '../../utils/prompts.dart';

class TokenCreateCommand extends BaseGlobeCommand {
TokenCreateCommand() {
argParser
..addOption(
'name',
abbr: 'n',
help: 'Specify name to identity token.',
)
..addOption(
'expiry',
abbr: 'e',
help: 'Specify lifespan of token.',
)
..addMultiOption(
'project',
help: 'Specify projects(s) to associate token with.',
);
}

@override
String get description => 'Create globe auth token.';

@override
String get name => 'create';

@override
FutureOr<int> run() async {
requireAuth();

final validated = await scope.validate();

final name = argResults?['name']?.toString() ??
logger.prompt('❓ Provide name for token:');
final dateString = argResults?['expiry']?.toString() ??
logger.prompt('❓ Set Expiry (yyyy-mm-dd):');

final expiry = DateTime.tryParse(dateString);
if (expiry == null) {
logger.err(
'Invalid date format.\nDate format should be ${cyan.wrap('2012-02-27')} or ${cyan.wrap('2012-02-27 13:27:00')}',
);
exitOverride(1);
}

final projects = await selectProjects(
validated.organization,
logger: logger,
api: api,
scope: scope,
ids: argResults?['project'] as List<String>?,
);
final projectNames = projects.map((e) => cyan.wrap(e.slug)).join(', ');

final createTokenProgress =
logger.progress('Creating Token for $projectNames');

try {
final token = await api.createToken(
orgId: validated.organization.id,
name: name,
projectUuids: projects.map((e) => e.id).toList(),
expiresAt: expiry,
);
createTokenProgress.complete(
"Here's your token:\nID: ${cyan.wrap(token.id)}\nToken: ${cyan.wrap(token.value)}",
);
return ExitCode.success.code;
} on ApiException catch (e) {
createTokenProgress.fail('✗ Failed to create token: ${e.message}');
return ExitCode.software.code;
} catch (e, s) {
createTokenProgress.fail('✗ Failed to create token: $e');
logger.detail(s.toString());
return ExitCode.software.code;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'dart:async';

import 'package:mason_logger/mason_logger.dart';

import '../../command.dart';
import '../../utils/api.dart';

class TokenDeleteCommand extends BaseGlobeCommand {
TokenDeleteCommand() {
argParser.addOption(
'tokenId',
abbr: 't',
help: 'Specify globe auth token id.',
);
}
@override
String get description => 'Delete globe auth token.';

@override
String get name => 'delete';

@override
FutureOr<int> run() async {
requireAuth();

final validated = await scope.validate();
final tokenId = (argResults?['tokenId'] as String?) ??
logger.prompt('❓ Provide id for token:');

final deleteTokenProgress =
logger.progress('Deleting Token: ${cyan.wrap(tokenId)}');

try {
await api.deleteToken(
orgId: validated.organization.id,
tokenId: tokenId,
);
deleteTokenProgress.complete('Token deleted');
} on ApiException catch (e) {
deleteTokenProgress.fail('✗ Failed to delete token: ${e.message}');
return ExitCode.software.code;
} catch (e, s) {
deleteTokenProgress.fail('✗ Failed to delete token: $e');
logger.detail(s.toString());
return ExitCode.software.code;
}

return 0;
}
}
55 changes: 55 additions & 0 deletions packages/globe_cli/lib/src/commands/token/token_list_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'dart:async';

import 'package:mason_logger/mason_logger.dart';

import '../../command.dart';
import '../../utils/api.dart';

class TokenListCommand extends BaseGlobeCommand {
@override
String get description => 'List globe auth tokens for current project';

@override
String get name => 'list';

@override
FutureOr<int>? run() async {
requireAuth();

final validated = await scope.validate();
final projectName = cyan.wrap(validated.project.slug);

final listTokenProgress =
logger.progress('Listing Tokens for $projectName');

try {
final tokens = await api.listTokens(
orgId: validated.organization.id,
projectUuids: [validated.project.id],
);
if (tokens.isEmpty) {
listTokenProgress.fail('No Tokens found for $projectName');
return ExitCode.success.code;
}

String tokenLog(Token token) => '''
----------------------------------
ID: ${cyan.wrap(token.uuid)}
Name: ${token.name}
Expiry: ${token.expiresAt.toLocal()}''';

listTokenProgress.complete(
'Tokens for $projectName\n${tokens.map(tokenLog).join('\n')}',
);

return ExitCode.success.code;
} on ApiException catch (e) {
listTokenProgress.fail('✗ Failed to list tokens: ${e.message}');
return ExitCode.software.code;
} catch (e, s) {
listTokenProgress.fail('✗ Failed to list tokens: $e');
logger.detail(s.toString());
return ExitCode.software.code;
}
}
}
17 changes: 17 additions & 0 deletions packages/globe_cli/lib/src/commands/token_command.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import '../command.dart';
import 'token/token_create_command.dart';
import 'token/token_delete_command.dart';
import 'token/token_list_command.dart';

class TokenCommand extends BaseGlobeCommand {
TokenCommand() {
addSubcommand(TokenCreateCommand());
addSubcommand(TokenDeleteCommand());
addSubcommand(TokenListCommand());
}
@override
String get description => 'Manage globe auth tokens.';

@override
String get name => 'token';
}
105 changes: 105 additions & 0 deletions packages/globe_cli/lib/src/utils/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,73 @@ class GlobeApi {

return Deployment.fromJson(response);
}

Future<({String id, String value})> createToken({
required String orgId,
required String name,
required List<String> projectUuids,
required DateTime expiresAt,
}) async {
requireAuth();

final createTokenPath = '/orgs/$orgId/api-tokens';
logger.detail('API Request: POST $createTokenPath');

final body = json.encode({
'name': name,
'projectUuids': projectUuids,
'expiresAt': expiresAt.toUtc().toIso8601String(),
});

// create token
final response = _handleResponse(
await http.post(_buildUri(createTokenPath), headers: headers, body: body),
)! as Map<String, Object?>;
final token = Token.fromJson(response);

final generateTokenPath = '/orgs/$orgId/api-tokens/${token.uuid}/generate';
logger.detail('API Request: GET $generateTokenPath');

// get token value
final tokenValue = _handleResponse(
await http.get(_buildUri(generateTokenPath), headers: headers),
)! as String;

return (id: token.uuid, value: tokenValue);
}

Future<List<Token>> listTokens({
required String orgId,
required List<String> projectUuids,
}) async {
requireAuth();

final listTokensPath =
'/orgs/$orgId/api-tokens?projects=${projectUuids.join(',')}';
logger.detail('API Request: GET $listTokensPath');

final response = _handleResponse(
await http.get(_buildUri(listTokensPath), headers: headers),
)! as List<dynamic>;

return response
.map((e) => Token.fromJson(e as Map<String, dynamic>))
.toList();
}

Future<void> deleteToken({
required String orgId,
required String tokenId,
}) async {
requireAuth();

final deleteTokenPath = '/orgs/$orgId/api-tokens/$tokenId';
logger.detail('API Request: DELETE $deleteTokenPath');

_handleResponse(
await http.delete(_buildUri(deleteTokenPath), headers: headers),
)! as Map<String, Object?>;
}
}

class Settings {
Expand Down Expand Up @@ -564,3 +631,41 @@ enum OrganizationType {
}
}
}

class Token {
final String uuid;
final String name;
final String organizationUuid;
final DateTime expiresAt;
final List<String> cliTokenClaimProject;

const Token._({
required this.uuid,
required this.name,
required this.organizationUuid,
required this.expiresAt,
required this.cliTokenClaimProject,
});

factory Token.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'uuid': final String uuid,
'name': final String name,
'organizationUuid': final String organizationUuid,
'expiresAt': final String expiresAt,
'projects': final List<dynamic> projects,
} =>
Token._(
uuid: uuid,
name: name,
organizationUuid: organizationUuid,
expiresAt: DateTime.parse(expiresAt),
cliTokenClaimProject: projects
.map((e) => (e as Map)['projectUuid'].toString())
.toList(),
),
_ => throw const FormatException('Token'),
};
}
}
Loading
Loading