Rotate bot key

This commit is contained in:
2025-08-24 01:49:56 +08:00
parent 3959f2260b
commit 5060bd30c9
2 changed files with 116 additions and 72 deletions

View File

@@ -904,5 +904,8 @@
"revoke": "Revoke", "revoke": "Revoke",
"keyName": "Key Name", "keyName": "Key Name",
"newKeyGenerated": "New Key Generated", "newKeyGenerated": "New Key Generated",
"copyKeyHint": "Please copy this key and store it somewhere safe. You will not be able to see it again." "copyKeyHint": "Please copy this key and store it somewhere safe. You will not be able to see it again.",
"rotateKey": "Rotate Key",
"rotateBotKey": "Rotate Bot Key",
"rotateBotKeyHint": "Are you sure you want to rotate this key? The old key will become invalid immediately. This action cannot be undone."
} }

View File

@@ -6,6 +6,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/bot_key.dart'; import 'package:island/models/bot_key.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/time.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
@@ -95,7 +96,6 @@ class BotKeysScreen extends HookConsumerWidget {
isScrollControlled: true, isScrollControlled: true,
builder: builder:
(context) => SheetScaffold( (context) => SheetScaffold(
heightFactor: 0.65,
titleText: 'newBotKey'.tr(), titleText: 'newBotKey'.tr(),
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
@@ -136,6 +136,28 @@ class BotKeysScreen extends HookConsumerWidget {
); );
} }
void rotateKey(String keyId) {
showConfirmAlert('rotateBotKeyHint'.tr(), 'rotateBotKey'.tr()).then((
confirm,
) async {
if (confirm) {
try {
if (context.mounted) showLoadingModal(context);
final client = ref.read(apiClientProvider);
final resp = await client.post(
'/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys/$keyId/rotate',
);
final rotatedApiKey = SnAccountApiKey.fromJson(resp.data);
showNewKeySheet(rotatedApiKey);
} catch (err) {
showErrorAlert(err.toString());
} finally {
if (context.mounted) hideLoadingModal(context);
}
}
});
}
void revokeKey(String keyId) { void revokeKey(String keyId) {
showConfirmAlert('revokeBotKeyHint'.tr(), 'revokeBotKey'.tr()).then(( showConfirmAlert('revokeBotKeyHint'.tr(), 'revokeBotKey'.tr()).then((
confirm, confirm,
@@ -158,80 +180,99 @@ class BotKeysScreen extends HookConsumerWidget {
}); });
} }
return Column( return keys.when(
children: [ data: (data) {
ListTile( return Column(
leading: const Icon(Symbols.add), children: [
title: Text('newBotKey'.tr()), ListTile(
trailing: const Icon(Symbols.chevron_right), leading: const Icon(Symbols.add),
onTap: createKey, title: Text('newBotKey'.tr()),
), trailing: const Icon(Symbols.chevron_right),
const Divider(height: 1), onTap: createKey,
Expanded( ),
child: keys.when( const Divider(height: 1),
data: (data) { Expanded(
if (data.isEmpty) { child:
return Center(child: Text('noBotKeys'.tr())); data.isEmpty
} ? Center(child: Text('noBotKeys'.tr()))
return RefreshIndicator( : RefreshIndicator(
onRefresh: onRefresh:
() => ref.refresh( () => ref.refresh(
botKeysProvider(publisherName, projectId, botId).future, botKeysProvider(
), publisherName,
child: ListView.builder( projectId,
padding: EdgeInsets.zero, botId,
itemCount: data.length, ).future,
itemBuilder: (context, index) { ),
final apiKey = data[index]; child: ListView.builder(
return ListTile( padding: EdgeInsets.zero,
title: Text(apiKey.label), itemCount: data.length,
subtitle: Text( itemBuilder: (context, index) {
'Created: ${DateFormat.yMMMd().format(apiKey.createdAt)}', final apiKey = data[index];
), return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12), title: Text(apiKey.label),
trailing: PopupMenuButton( subtitle: Text(apiKey.createdAt.formatSystem()),
itemBuilder: contentPadding: EdgeInsets.only(
(context) => [ left: 16,
PopupMenuItem( right: 12,
value: 'revoke',
child: Row(
children: [
const Icon(
Symbols.delete,
color: Colors.red,
),
const Gap(12),
Text(
'revoke'.tr(),
style: TextStyle(color: Colors.red),
),
],
),
), ),
], trailing: PopupMenuButton(
onSelected: (value) { itemBuilder:
if (value == 'revoke') { (context) => [
revokeKey(apiKey.id); PopupMenuItem(
} value: 'rotate',
}, child: Row(
), children: [
); const Icon(Symbols.refresh),
}, const Gap(12),
), Text('rotateKey'.tr()),
); ],
}, ),
loading: () => const Center(child: CircularProgressIndicator()), ),
error: PopupMenuItem(
(err, stack) => ResponseErrorWidget( value: 'revoke',
error: err, child: Row(
onRetry: children: [
() => ref.invalidate( const Icon(
botKeysProvider(publisherName, projectId, botId), Symbols.delete,
color: Colors.red,
),
const Gap(12),
Text(
'revoke'.tr(),
style: TextStyle(
color: Colors.red,
),
),
],
),
),
],
onSelected: (value) {
if (value == 'rotate') {
rotateKey(apiKey.id);
} else if (value == 'revoke') {
revokeKey(apiKey.id);
}
},
),
);
},
),
), ),
),
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry:
() => ref.invalidate(
botKeysProvider(publisherName, projectId, botId),
), ),
), ),
),
],
); );
} }
} }