From 4581652ea653c4625af2c2ce3db79cf29c38e464 Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Tue, 5 Aug 2025 09:34:14 +0530 Subject: [PATCH] [client] Revoke API keys [#33] --- .../api_key_generate_revoke_request.dart | 25 ++++++++++++ .../responses/api_key_list_response.dart | 2 +- client/lib/ui/profile/api_keys_view.dart | 38 ++++++++++++++++--- 3 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 client/lib/libphylum/requests/api_key_generate_revoke_request.dart diff --git a/client/lib/libphylum/requests/api_key_generate_revoke_request.dart b/client/lib/libphylum/requests/api_key_generate_revoke_request.dart new file mode 100644 index 00000000..cde9227a --- /dev/null +++ b/client/lib/libphylum/requests/api_key_generate_revoke_request.dart @@ -0,0 +1,25 @@ +import 'dart:typed_data'; + +import 'package:http/http.dart'; +import 'package:offtheline/offtheline.dart'; + +class RevokeApiKeyRequest extends ApiRequest { + final String id; + final bool delete; + + RevokeApiKeyRequest({required this.id, required this.delete}); + + @override + BaseRequest createRequest(ApiClient api, {Uint8List? data}) { + final uri = api.createUriBuilder('/api/v1/user/keys/revoke'); + final request = Request('post', uri.build()); + final fields = { + 'id': id, + 'delete': delete.toString(), + }; + + request.bodyFields = fields; + + return request; + } +} diff --git a/client/lib/libphylum/responses/api_key_list_response.dart b/client/lib/libphylum/responses/api_key_list_response.dart index c3ab19b9..53c86dc7 100644 --- a/client/lib/libphylum/responses/api_key_list_response.dart +++ b/client/lib/libphylum/responses/api_key_list_response.dart @@ -42,7 +42,7 @@ class ListApiKeysResponse extends PhylumApiSuccessResponse { scopes: (k['scopes'] as List).cast(), lastUsed: _parseLastUsed((k['last_used'] as Map).cast()), ); - }).toList(growable: false); + }).toList(); return ListApiKeysResponse._(keys: keys); } diff --git a/client/lib/ui/profile/api_keys_view.dart b/client/lib/ui/profile/api_keys_view.dart index dca3903b..c1db2fe5 100644 --- a/client/lib/ui/profile/api_keys_view.dart +++ b/client/lib/ui/profile/api_keys_view.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:moment_dart/moment_dart.dart'; +import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/libphylum/requests/api_key_generate_request.dart'; +import 'package:phylum/libphylum/requests/api_key_generate_revoke_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'; @@ -57,7 +59,7 @@ class _ApiKeysViewState extends State { for (final k in keys) ListTile( title: k.description.isEmpty - ? Text('Unnamed', style: TextStyle(fontStyle: FontStyle.italic)) + ? const Text('No Description', style: TextStyle(fontStyle: FontStyle.italic)) : Text(k.description), subtitle: k.lastUsed.used ? Text('${k.lastUsed.device} \u2022 ${k.lastUsed.fromNow()}') @@ -71,6 +73,7 @@ class _ApiKeysViewState extends State { } void _showKeyDetails(BuildContext context, ApiKey key) async { + final account = context.read(); showDialog( context: context, builder: (context) => AlertDialog( @@ -87,7 +90,9 @@ class _ApiKeysViewState extends State { ), ListTile( title: const Text('Description'), - subtitle: Text(key.description), + subtitle: key.description.isEmpty + ? const Text('No Description', style: TextStyle(fontStyle: FontStyle.italic)) + : Text(key.description), trailing: IconButton(onPressed: () {}, icon: Icon(Icons.edit)), ), ListTile(title: const Text('Created'), subtitle: Text(key.created.fromNow())), @@ -95,14 +100,20 @@ class _ApiKeysViewState extends State { title: (key.expires?.isBefore(DateTime.now()) ?? false) ? const Text('Expired') : const Text('Expires'), subtitle: key.expires == null - ? const Text('Never') + ? const Text( + 'Never', + style: TextStyle(fontStyle: FontStyle.italic), + ) : Text('${key.expires!.fromNow()} (${key.expires!.formatShort()})')), ListTile(title: const Text('Scopes'), subtitle: Text(key.scopes.join(', '))), ListTile( title: const Text('Last Used'), subtitle: key.lastUsed.used ? Text('${key.lastUsed.device} \u2022 ${key.lastUsed.ip} \u2022 ${key.lastUsed.fromNow()}') - : const Text('Never Used'), + : const Text( + 'Never', + style: TextStyle(fontStyle: FontStyle.italic), + ), ), ], ), @@ -110,7 +121,24 @@ class _ApiKeysViewState extends State { scrollable: true, actions: [ ElevatedButton( - onPressed: () {}, + onPressed: () async { + final nav = Navigator.of(context); + showProgressDialog(context); + final result = await account.apiClient.sendRequest(RevokeApiKeyRequest(id: key.id, delete: true), + (request, response) => parseJsonMapResponse(response, EmptyResponse.fromResponse)); + nav.pop(); + if (result is ApiSuccessResponse) { + if (context.mounted) { + await showAlertDialog(context, title: 'API Key Deleted'); + nav.pop(); + setState(() => keys?.removeWhere((k) => k == key)); + } + } else { + if (context.mounted) { + await showAlertDialog(context, title: 'Unable to delete API Key', message: result.description); + } + } + }, child: Text('Delete Key', style: TextStyle(color: Theme.of(context).colorScheme.error))), ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text('OK')), ],