[client] Generate API Keys from the client [#15]

This commit is contained in:
Abhishek Shroff
2025-07-21 01:37:56 +05:30
parent bd51b15824
commit cc5206866f
8 changed files with 132 additions and 13 deletions
@@ -0,0 +1,29 @@
import 'dart:typed_data';
import 'package:http/http.dart';
import 'package:offtheline/offtheline.dart';
import 'package:phylum/util/time.dart';
class GenerateApiKeyRequest extends ApiRequest {
final String description;
final DateTime? expires;
final List<String> scopes;
GenerateApiKeyRequest({required this.description, required this.expires, required this.scopes});
@override
BaseRequest createRequest(ApiClient api, {Uint8List? data}) {
final uri = api.createUriBuilder('/api/v1/user/keys/generate');
final request = Request('post', uri.build());
final fields = <String, String>{
'description': description,
'scopes': scopes.join(','),
};
if (expires != null) {
fields['expires'] = expires!.formatServer();
}
request.bodyFields = fields;
return request;
}
}
@@ -3,8 +3,8 @@ import 'dart:typed_data';
import 'package:http/http.dart';
import 'package:offtheline/offtheline.dart';
class ApiKeysRequest extends ApiRequest {
const ApiKeysRequest();
class ListApiKeysRequest extends ApiRequest {
const ListApiKeysRequest();
@override
BaseRequest createRequest(ApiClient api, {Uint8List? data}) {
@@ -0,0 +1,20 @@
part of 'responses.dart';
class GenerateApiKeyResponse extends PhylumApiSuccessResponse {
final String keyId;
final String key;
final String accessToken;
GenerateApiKeyResponse._({required this.keyId, required this.key, required this.accessToken});
factory GenerateApiKeyResponse.fromResponse(Map<String, dynamic> data) {
return GenerateApiKeyResponse._(
keyId: data['id'],
key: data['key'],
accessToken: data['token'],
);
}
@override
Future<void> process(PhylumAccount account) => SynchronousFuture(null);
}
@@ -16,12 +16,12 @@ class ApiKey {
});
}
class ApiKeysResponse extends PhylumApiSuccessResponse {
class ListApiKeysResponse extends PhylumApiSuccessResponse {
final List<ApiKey> keys;
ApiKeysResponse._({required this.keys});
ListApiKeysResponse._({required this.keys});
factory ApiKeysResponse.fromResponse(List data) {
factory ListApiKeysResponse.fromResponse(List data) {
final keys = data.cast<Map>().map((map) {
final k = map.cast<String, dynamic>();
return ApiKey(
@@ -32,7 +32,7 @@ class ApiKeysResponse extends PhylumApiSuccessResponse {
scopes: (k['scopes'] as List).cast<String>(),
);
}).toList(growable: false);
return ApiKeysResponse._(keys: keys);
return ListApiKeysResponse._(keys: keys);
}
@override
@@ -10,7 +10,8 @@ import 'package:phylum/libphylum/parsers/parsers.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:phylum/util/permissions.dart';
part 'api_keys_response.dart';
part 'api_key_list_response.dart';
part 'api_key_generate_response.dart';
part 'bookmark_response.dart';
part 'bootstrap_login_response.dart';
part 'empty_response.dart';
+1 -1
View File
@@ -23,7 +23,7 @@ Future showReponsiveDialog(BuildContext context, String title, WidgetBuilder bui
return AlertDialog(
title: Text(title),
scrollable: true,
content: SizedBox(width: 360, child: builder(context)),
content: SizedBox(width: 480, child: builder(context)),
actions: [
ElevatedButton(onPressed: Navigator.of(context).pop, child: Text('OK')),
],
+69 -5
View File
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:phylum/libphylum/phylum_account.dart';
import 'package:phylum/libphylum/requests/api_keys_request.dart';
import 'package:phylum/libphylum/requests/api_key_generate_request.dart';
import 'package:phylum/libphylum/requests/api_keys_list_request.dart';
import 'package:phylum/libphylum/responses/responses.dart';
import 'package:phylum/ui/profile/generate_api_key_dialog.dart';
import 'package:phylum/util/dialogs.dart';
import 'package:phylum/util/time.dart';
import 'package:provider/provider.dart';
@@ -68,11 +70,11 @@ class _ApiKeysViewState extends State<ApiKeysView> {
void fetchKeys(BuildContext context) async {
final account = context.read<PhylumAccount>();
final response = await account.apiClient.sendRequest(
const ApiKeysRequest(),
(request, response) => parseJsonListResponse(response, ApiKeysResponse.fromResponse),
const ListApiKeysRequest(),
(request, response) => parseJsonListResponse(response, ListApiKeysResponse.fromResponse),
);
if (response is ApiKeysResponse) {
if (response is ListApiKeysResponse) {
setState(() => keys = response.keys);
} else {
setState(() => error = response.description);
@@ -80,6 +82,68 @@ class _ApiKeysViewState extends State<ApiKeysView> {
}
void createKey(BuildContext context) async {
GenerateApiKeyDialog.show(context);
final account = context.read<PhylumAccount>();
final k = await GenerateApiKeyDialog.show(context);
if (k == null) return;
final response = await account.apiClient.sendRequest(
GenerateApiKeyRequest(
description: k.description,
expires: k.expires,
scopes: k.scopes,
),
(_, response) => parseJsonMapResponse(response, GenerateApiKeyResponse.fromResponse));
if (response is GenerateApiKeyResponse) {
if (!context.mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('API Key Generated'),
content: SizedBox(
width: 480,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 6.0),
child: Text('Key ID and Key can be used as the username and password in HTTP Basic auth'),
),
Padding(
padding: const EdgeInsets.only(bottom: 6.0),
child:
Text('Access Token can be used as a Bearer token, or with HTTP Basic auth by leaving the '
'username empty'),
),
Padding(
padding: const EdgeInsets.only(bottom: 6.0),
child: Text(
'Please note down the details as it will not be possible to retreive once it is dismissed.'),
),
TextFormField(
decoration: InputDecoration(label: Text('Key ID')),
initialValue: response.keyId,
readOnly: true,
),
TextFormField(
decoration: InputDecoration(label: Text('Key')),
initialValue: response.key,
readOnly: true,
),
TextFormField(
decoration: InputDecoration(label: Text('Access Token')),
initialValue: response.accessToken,
readOnly: true,
),
],
),
),
actions: [
ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text('OK')),
],
));
} else {
if (!context.mounted) return;
showAlertDialog(context, title: 'Error Generating API Key', message: response.description);
}
}
}
+5
View File
@@ -6,6 +6,7 @@ final _formatYearMonthDate = DateFormat.yMMMd();
final _formatFullNoYear = DateFormat.MMMd().add_jm();
final _formatFull = DateFormat.yMMMd().add_jm();
final _formatNumericDateTime = DateFormat('yyyy-MM-ddTHH:mm:ss');
// final _formatServer = DateFormat('yyyy-MM-ddTHH:mm:ss\'Z\'00:00');
DateTime get _today {
final now = DateTime.now();
@@ -34,4 +35,8 @@ extension Formats on DateTime {
String formatNumericDateTime() {
return _formatNumericDateTime.format(this);
}
String formatServer() {
return toUtc().toIso8601String();
}
}