Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit e87e5a0

Browse files
codekeyzexaby73
andauthored
feat: Token create & delete from CLI (#39)
* 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 <nabeelparkar99@gmail.com> * Update docs/cli/commands/token.mdx Co-authored-by: Nabeel Parkar <nabeelparkar99@gmail.com> * Update docs/cli/commands/token.mdx Co-authored-by: Nabeel Parkar <nabeelparkar99@gmail.com> * wip --------- Co-authored-by: Nabeel Parkar <nabeelparkar99@gmail.com>
1 parent d98f7ff commit e87e5a0

File tree

9 files changed

+423
-0
lines changed

9 files changed

+423
-0
lines changed

docs/cli/commands/token.mdx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
title: Globe Tokens
3+
description: Create, Delete & List globe auth tokens from the command line.
4+
---
5+
6+
# Create
7+
8+
The `create` command allows you to create auth tokens for your projects. You can use this token to
9+
login to Globe in any environment.
10+
11+
## Usage
12+
13+
You can run the command interactively by running
14+
15+
```bash
16+
globe token create
17+
```
18+
19+
or in-lined by providing necessary arguments
20+
21+
- `--name`- specify name to identity the token.
22+
- `--expiry` - specify lifespan of the token.
23+
- `--project` - specify projects(s) to associate the token with.
24+
25+
```bash
26+
globe token create --name="Foo Bar" --expiry="yyyy-mm-dd" --project="project-ids-go-here"
27+
```
28+
29+
# List Tokens
30+
31+
The `list` command lists all tokens associated with the current project.
32+
33+
## Usage
34+
35+
```bash
36+
globe token list
37+
```
38+
39+
# Delete Token
40+
41+
The `delete` command allows you to delete token by providing token ID.
42+
43+
## Usage
44+
45+
```bash
46+
globe token delete --tokenId="token-id-goes-here"
47+
```

packages/globe_cli/lib/src/command_runner.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class GlobeCliCommandRunner extends CompletionCommandRunner<int> {
5050
addCommand(LinkCommand());
5151
addCommand(UnlinkCommand());
5252
addCommand(BuildLogsCommand());
53+
addCommand(TokenCommand());
5354
}
5455

5556
final Logger _logger;

packages/globe_cli/lib/src/commands/commands.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export 'deploy_command.dart';
22
export 'link_command.dart';
33
export 'login_command.dart';
44
export 'logout_command.dart';
5+
export 'token_command.dart';
56
export 'unlink_command.dart';
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import 'dart:async';
2+
3+
import 'package:mason_logger/mason_logger.dart';
4+
5+
import '../../command.dart';
6+
import '../../exit.dart';
7+
import '../../utils/api.dart';
8+
import '../../utils/prompts.dart';
9+
10+
class TokenCreateCommand extends BaseGlobeCommand {
11+
TokenCreateCommand() {
12+
argParser
13+
..addOption(
14+
'name',
15+
abbr: 'n',
16+
help: 'Specify name to identity token.',
17+
)
18+
..addOption(
19+
'expiry',
20+
abbr: 'e',
21+
help: 'Specify lifespan of token.',
22+
)
23+
..addMultiOption(
24+
'project',
25+
help: 'Specify projects(s) to associate token with.',
26+
);
27+
}
28+
29+
@override
30+
String get description => 'Create globe auth token.';
31+
32+
@override
33+
String get name => 'create';
34+
35+
@override
36+
FutureOr<int> run() async {
37+
requireAuth();
38+
39+
final validated = await scope.validate();
40+
41+
final name = argResults?['name']?.toString() ??
42+
logger.prompt('❓ Provide name for token:');
43+
final dateString = argResults?['expiry']?.toString() ??
44+
logger.prompt('❓ Set Expiry (yyyy-mm-dd):');
45+
46+
final expiry = DateTime.tryParse(dateString);
47+
if (expiry == null) {
48+
logger.err(
49+
'Invalid date format.\nDate format should be ${cyan.wrap('2012-02-27')} or ${cyan.wrap('2012-02-27 13:27:00')}',
50+
);
51+
exitOverride(1);
52+
}
53+
54+
final projects = await selectProjects(
55+
validated.organization,
56+
logger: logger,
57+
api: api,
58+
scope: scope,
59+
ids: argResults?['project'] as List<String>?,
60+
);
61+
final projectNames = projects.map((e) => cyan.wrap(e.slug)).join(', ');
62+
63+
final createTokenProgress =
64+
logger.progress('Creating Token for $projectNames');
65+
66+
try {
67+
final token = await api.createToken(
68+
orgId: validated.organization.id,
69+
name: name,
70+
projectUuids: projects.map((e) => e.id).toList(),
71+
expiresAt: expiry,
72+
);
73+
createTokenProgress.complete(
74+
"Here's your token:\nID: ${cyan.wrap(token.id)}\nToken: ${cyan.wrap(token.value)}",
75+
);
76+
return ExitCode.success.code;
77+
} on ApiException catch (e) {
78+
createTokenProgress.fail('✗ Failed to create token: ${e.message}');
79+
return ExitCode.software.code;
80+
} catch (e, s) {
81+
createTokenProgress.fail('✗ Failed to create token: $e');
82+
logger.detail(s.toString());
83+
return ExitCode.software.code;
84+
}
85+
}
86+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import 'dart:async';
2+
3+
import 'package:mason_logger/mason_logger.dart';
4+
5+
import '../../command.dart';
6+
import '../../utils/api.dart';
7+
8+
class TokenDeleteCommand extends BaseGlobeCommand {
9+
TokenDeleteCommand() {
10+
argParser.addOption(
11+
'tokenId',
12+
abbr: 't',
13+
help: 'Specify globe auth token id.',
14+
);
15+
}
16+
@override
17+
String get description => 'Delete globe auth token.';
18+
19+
@override
20+
String get name => 'delete';
21+
22+
@override
23+
FutureOr<int> run() async {
24+
requireAuth();
25+
26+
final validated = await scope.validate();
27+
final tokenId = (argResults?['tokenId'] as String?) ??
28+
logger.prompt('❓ Provide id for token:');
29+
30+
final deleteTokenProgress =
31+
logger.progress('Deleting Token: ${cyan.wrap(tokenId)}');
32+
33+
try {
34+
await api.deleteToken(
35+
orgId: validated.organization.id,
36+
tokenId: tokenId,
37+
);
38+
deleteTokenProgress.complete('Token deleted');
39+
} on ApiException catch (e) {
40+
deleteTokenProgress.fail('✗ Failed to delete token: ${e.message}');
41+
return ExitCode.software.code;
42+
} catch (e, s) {
43+
deleteTokenProgress.fail('✗ Failed to delete token: $e');
44+
logger.detail(s.toString());
45+
return ExitCode.software.code;
46+
}
47+
48+
return 0;
49+
}
50+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'dart:async';
2+
3+
import 'package:mason_logger/mason_logger.dart';
4+
5+
import '../../command.dart';
6+
import '../../utils/api.dart';
7+
8+
class TokenListCommand extends BaseGlobeCommand {
9+
@override
10+
String get description => 'List globe auth tokens for current project';
11+
12+
@override
13+
String get name => 'list';
14+
15+
@override
16+
FutureOr<int>? run() async {
17+
requireAuth();
18+
19+
final validated = await scope.validate();
20+
final projectName = cyan.wrap(validated.project.slug);
21+
22+
final listTokenProgress =
23+
logger.progress('Listing Tokens for $projectName');
24+
25+
try {
26+
final tokens = await api.listTokens(
27+
orgId: validated.organization.id,
28+
projectUuids: [validated.project.id],
29+
);
30+
if (tokens.isEmpty) {
31+
listTokenProgress.fail('No Tokens found for $projectName');
32+
return ExitCode.success.code;
33+
}
34+
35+
String tokenLog(Token token) => '''
36+
----------------------------------
37+
ID: ${cyan.wrap(token.uuid)}
38+
Name: ${token.name}
39+
Expiry: ${token.expiresAt.toLocal()}''';
40+
41+
listTokenProgress.complete(
42+
'Tokens for $projectName\n${tokens.map(tokenLog).join('\n')}',
43+
);
44+
45+
return ExitCode.success.code;
46+
} on ApiException catch (e) {
47+
listTokenProgress.fail('✗ Failed to list tokens: ${e.message}');
48+
return ExitCode.software.code;
49+
} catch (e, s) {
50+
listTokenProgress.fail('✗ Failed to list tokens: $e');
51+
logger.detail(s.toString());
52+
return ExitCode.software.code;
53+
}
54+
}
55+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import '../command.dart';
2+
import 'token/token_create_command.dart';
3+
import 'token/token_delete_command.dart';
4+
import 'token/token_list_command.dart';
5+
6+
class TokenCommand extends BaseGlobeCommand {
7+
TokenCommand() {
8+
addSubcommand(TokenCreateCommand());
9+
addSubcommand(TokenDeleteCommand());
10+
addSubcommand(TokenListCommand());
11+
}
12+
@override
13+
String get description => 'Manage globe auth tokens.';
14+
15+
@override
16+
String get name => 'token';
17+
}

packages/globe_cli/lib/src/utils/api.dart

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,73 @@ class GlobeApi {
219219

220220
return Deployment.fromJson(response);
221221
}
222+
223+
Future<({String id, String value})> createToken({
224+
required String orgId,
225+
required String name,
226+
required List<String> projectUuids,
227+
required DateTime expiresAt,
228+
}) async {
229+
requireAuth();
230+
231+
final createTokenPath = '/orgs/$orgId/api-tokens';
232+
logger.detail('API Request: POST $createTokenPath');
233+
234+
final body = json.encode({
235+
'name': name,
236+
'projectUuids': projectUuids,
237+
'expiresAt': expiresAt.toUtc().toIso8601String(),
238+
});
239+
240+
// create token
241+
final response = _handleResponse(
242+
await http.post(_buildUri(createTokenPath), headers: headers, body: body),
243+
)! as Map<String, Object?>;
244+
final token = Token.fromJson(response);
245+
246+
final generateTokenPath = '/orgs/$orgId/api-tokens/${token.uuid}/generate';
247+
logger.detail('API Request: GET $generateTokenPath');
248+
249+
// get token value
250+
final tokenValue = _handleResponse(
251+
await http.get(_buildUri(generateTokenPath), headers: headers),
252+
)! as String;
253+
254+
return (id: token.uuid, value: tokenValue);
255+
}
256+
257+
Future<List<Token>> listTokens({
258+
required String orgId,
259+
required List<String> projectUuids,
260+
}) async {
261+
requireAuth();
262+
263+
final listTokensPath =
264+
'/orgs/$orgId/api-tokens?projects=${projectUuids.join(',')}';
265+
logger.detail('API Request: GET $listTokensPath');
266+
267+
final response = _handleResponse(
268+
await http.get(_buildUri(listTokensPath), headers: headers),
269+
)! as List<dynamic>;
270+
271+
return response
272+
.map((e) => Token.fromJson(e as Map<String, dynamic>))
273+
.toList();
274+
}
275+
276+
Future<void> deleteToken({
277+
required String orgId,
278+
required String tokenId,
279+
}) async {
280+
requireAuth();
281+
282+
final deleteTokenPath = '/orgs/$orgId/api-tokens/$tokenId';
283+
logger.detail('API Request: DELETE $deleteTokenPath');
284+
285+
_handleResponse(
286+
await http.delete(_buildUri(deleteTokenPath), headers: headers),
287+
)! as Map<String, Object?>;
288+
}
222289
}
223290

224291
class Settings {
@@ -564,3 +631,41 @@ enum OrganizationType {
564631
}
565632
}
566633
}
634+
635+
class Token {
636+
final String uuid;
637+
final String name;
638+
final String organizationUuid;
639+
final DateTime expiresAt;
640+
final List<String> cliTokenClaimProject;
641+
642+
const Token._({
643+
required this.uuid,
644+
required this.name,
645+
required this.organizationUuid,
646+
required this.expiresAt,
647+
required this.cliTokenClaimProject,
648+
});
649+
650+
factory Token.fromJson(Map<String, dynamic> json) {
651+
return switch (json) {
652+
{
653+
'uuid': final String uuid,
654+
'name': final String name,
655+
'organizationUuid': final String organizationUuid,
656+
'expiresAt': final String expiresAt,
657+
'projects': final List<dynamic> projects,
658+
} =>
659+
Token._(
660+
uuid: uuid,
661+
name: name,
662+
organizationUuid: organizationUuid,
663+
expiresAt: DateTime.parse(expiresAt),
664+
cliTokenClaimProject: projects
665+
.map((e) => (e as Map)['projectUuid'].toString())
666+
.toList(),
667+
),
668+
_ => throw const FormatException('Token'),
669+
};
670+
}
671+
}

0 commit comments

Comments
 (0)