Skip to content

Commit

Permalink
feat: Token create & delete from CLI (#39)
Browse files Browse the repository at this point in the history
* setup commands for token create & delete

* implement token create command

* use token name & expiry from command args

* fix formatting

* use multi-option for project argument

* return both token id and value

* toUtc before send to api

* tiny fix

* tiny fix

* tiny fix

* wip

* wip

* make token creation interactive when args not provided

* add command to list globe tokens

* wip

* wip

* update docs

* wip

* wip

* Update docs/cli/commands/token.mdx

Co-authored-by: Nabeel Parkar <[email protected]>

* Update docs/cli/commands/token.mdx

Co-authored-by: Nabeel Parkar <[email protected]>

* Update docs/cli/commands/token.mdx

Co-authored-by: Nabeel Parkar <[email protected]>

* wip

---------

Co-authored-by: Nabeel Parkar <[email protected]>
  • Loading branch information
codekeyz and exaby73 authored Feb 21, 2024
1 parent d98f7ff commit e87e5a0
Show file tree
Hide file tree
Showing 9 changed files with 423 additions and 0 deletions.
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 the token.
- `--expiry` - specify lifespan of the token.
- `--project` - specify projects(s) to associate the token with.

```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 --tokenId="token-id-goes-here"
```
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

0 comments on commit e87e5a0

Please sign in to comment.