From 78c895547e2bcc77dd49850a6cb9d1b7fb2bc8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Fri, 18 Apr 2025 18:56:17 +0200 Subject: [PATCH] Finish KeyToken operations --- flutter/lib/api/api_client.dart | 12 +- .../badge_display/badge_display.dart | 51 ++++ .../modals/filter_modal_channel.dart | 47 ++-- .../modals/filter_modal_keytoken.dart | 41 ++- .../modals/filter_modal_sendername.dart | 39 ++- flutter/lib/main.dart | 13 + flutter/lib/models/keytoken.dart | 15 + flutter/lib/models/keytoken.g.dart | 65 +++++ flutter/lib/pages/account/account.dart | 106 ++++--- .../lib/pages/channel_view/channel_view.dart | 6 +- .../lib/pages/debug/debug_request_view.dart | 33 +-- flutter/lib/pages/debug/debug_requests.dart | 25 +- .../keytoken_list/keytoken_create_modal.dart | 216 +++++++++++++++ .../keytoken_list/keytoken_created_modal.dart | 97 +++++++ .../pages/keytoken_list/keytoken_list.dart | 24 ++ .../keytoken_view/keytoken_channel_modal.dart | 112 ++++++++ .../keytoken_permission_modal.dart | 82 ++++++ .../pages/keytoken_view/keytoken_view.dart | 259 +++++++++++++++--- .../lib/pages/message_view/message_view.dart | 12 +- .../subscription_view/subscription_view.dart | 6 +- flutter/lib/state/scn_data_cache.dart | 19 ++ flutter/lib/types/immediate_future.dart | 6 + scnserver/api/handler/apiKeyToken.go | 14 +- 23 files changed, 1089 insertions(+), 211 deletions(-) create mode 100644 flutter/lib/components/badge_display/badge_display.dart create mode 100644 flutter/lib/models/keytoken.g.dart create mode 100644 flutter/lib/pages/keytoken_list/keytoken_create_modal.dart create mode 100644 flutter/lib/pages/keytoken_list/keytoken_created_modal.dart create mode 100644 flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart create mode 100644 flutter/lib/pages/keytoken_view/keytoken_permission_modal.dart diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index 5de81c4..b89db78 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -445,6 +445,16 @@ class APIClient { ); } + static Future deleteKeyToken(AppAuth acc, String keytokenID) { + return _request( + name: 'deleteKeyToken', + method: 'DELETE', + relURL: 'users/${acc.getUserID()}/keys/${keytokenID}', + fn: (_) => null, + authToken: acc.getToken(), + ); + } + static Future updateKeyToken(TokenSource auth, String kid, {String? name, bool? allChannels, List? channels, String? permissions}) async { return await _request( name: 'updateKeyToken', @@ -468,7 +478,7 @@ class APIClient { relURL: 'users/${auth.getUserID()}/keys', jsonBody: { 'name': name, - 'pem': perm, + 'permissions': perm, 'all_channels': allChannels, if (channels != null) 'channels': channels, }, diff --git a/flutter/lib/components/badge_display/badge_display.dart b/flutter/lib/components/badge_display/badge_display.dart new file mode 100644 index 0000000..c2ba8fb --- /dev/null +++ b/flutter/lib/components/badge_display/badge_display.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +enum BadgeMode { error, warn, info } + +class BadgeDisplay extends StatelessWidget { + final String text; + final BadgeMode mode; + final IconData? icon; + + const BadgeDisplay({ + Key? key, + required this.text, + required this.mode, + required this.icon, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var col = Colors.grey; + var colFG = Colors.black; + + if (mode == BadgeMode.error) col = Colors.red; + if (mode == BadgeMode.warn) col = Colors.orange; + if (mode == BadgeMode.info) col = Colors.blue; + + if (mode == BadgeMode.error) colFG = Colors.red[900]!; + if (mode == BadgeMode.warn) colFG = Colors.black; + if (mode == BadgeMode.info) colFG = Colors.black; + + return Container( + padding: const EdgeInsets.fromLTRB(8, 2, 8, 2), + decoration: BoxDecoration( + color: col[100], + border: Border.all(color: col[300]!), + borderRadius: BorderRadius.circular(4.0), + ), + child: Row( + children: [ + if (icon != null) Icon(icon!, color: colFG, size: 16.0), + Expanded( + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle(color: colFG, fontSize: 14.0), + ), + ), + ], + ), + ); + } +} diff --git a/flutter/lib/components/modals/filter_modal_channel.dart b/flutter/lib/components/modals/filter_modal_channel.dart index b0c64dd..597364a 100644 --- a/flutter/lib/components/modals/filter_modal_channel.dart +++ b/flutter/lib/components/modals/filter_modal_channel.dart @@ -17,13 +17,12 @@ class FilterModalChannel extends StatefulWidget { class _FilterModalChannelState extends State { Set _selectedEntries = {}; - late ImmediateFuture>? _futureChannels; + ImmediateFuture> _futureChannels = ImmediateFuture.ofPending(); @override void initState() { super.initState(); - _futureChannels = null; _futureChannels = ImmediateFuture.ofFuture(() async { final userAcc = Provider.of(context, listen: false); if (!userAcc.isAuth()) throw new Exception('not logged in'); @@ -51,45 +50,39 @@ class _FilterModalChannelState extends State { content: Container( width: 9000, height: 9000, - child: () { - if (_futureChannels == null) { - return Center(child: CircularProgressIndicator()); - } - - return FutureBuilder( - future: _futureChannels!.future, - builder: ((context, snapshot) { - if (_futureChannels?.value != null) { - return _buildList(context, _futureChannels!.value!); - } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { - return ErrorDisplay(errorMessage: '${snapshot.error}'); - } else if (snapshot.connectionState == ConnectionState.done) { - return _buildList(context, snapshot.data!); - } else { - return Center(child: CircularProgressIndicator()); - } - }), - ); - }(), + child: FutureBuilder( + future: _futureChannels.future, + builder: ((context, snapshot) { + if (_futureChannels.value != null) { + return _buildList(context, _futureChannels.value!); + } else if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { + return ErrorDisplay(errorMessage: '${snapshot.error}'); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { + return _buildList(context, snapshot.data!); + } else { + return ErrorDisplay(errorMessage: 'Invalid future state'); + } + }), + ), ), actions: [ TextButton( style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: const Text('Apply'), - onPressed: () { - onOkay(); - }, + onPressed: _onOkay, ), ], ); } - void onOkay() { + void _onOkay() { Navi.popDialog(context); final chiplets = _selectedEntries .map((e) => MessageFilterChiplet( - label: _futureChannels?.get()?.map((e) => e as Channel?).firstWhere((p) => p?.channelID == e, orElse: () => null)?.displayName ?? '???', + label: _futureChannels.get()?.map((e) => e as Channel?).firstWhere((p) => p?.channelID == e, orElse: () => null)?.displayName ?? '???', value: e, type: MessageFilterChipletType.channel, )) diff --git a/flutter/lib/components/modals/filter_modal_keytoken.dart b/flutter/lib/components/modals/filter_modal_keytoken.dart index bac9456..c29d2e5 100644 --- a/flutter/lib/components/modals/filter_modal_keytoken.dart +++ b/flutter/lib/components/modals/filter_modal_keytoken.dart @@ -17,13 +17,12 @@ class FilterModalKeytoken extends StatefulWidget { class _FilterModalKeytokenState extends State { Set _selectedEntries = {}; - late ImmediateFuture>? _futureKeyTokens; + ImmediateFuture> _futureKeyTokens = ImmediateFuture.ofPending(); @override void initState() { super.initState(); - _futureKeyTokens = null; _futureKeyTokens = ImmediateFuture.ofFuture(() async { final userAcc = Provider.of(context, listen: false); if (!userAcc.isAuth()) throw new Exception('not logged in'); @@ -51,26 +50,22 @@ class _FilterModalKeytokenState extends State { content: Container( width: 9000, height: 9000, - child: () { - if (_futureKeyTokens == null) { - return Center(child: CircularProgressIndicator()); - } - - return FutureBuilder( - future: _futureKeyTokens!.future, - builder: ((context, snapshot) { - if (_futureKeyTokens?.value != null) { - return _buildList(context, _futureKeyTokens!.value!); - } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { - return ErrorDisplay(errorMessage: '${snapshot.error}'); - } else if (snapshot.connectionState == ConnectionState.done) { - return _buildList(context, snapshot.data!); - } else { - return Center(child: CircularProgressIndicator()); - } - }), - ); - }(), + child: FutureBuilder( + future: _futureKeyTokens.future, + builder: ((context, snapshot) { + if (_futureKeyTokens.value != null) { + return _buildList(context, _futureKeyTokens.value!); + } else if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { + return ErrorDisplay(errorMessage: '${snapshot.error}'); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { + return _buildList(context, snapshot.data!); + } else { + return ErrorDisplay(errorMessage: 'Invalid future state'); + } + }), + ), ), actions: [ TextButton( @@ -89,7 +84,7 @@ class _FilterModalKeytokenState extends State { final chiplets = _selectedEntries .map((e) => MessageFilterChiplet( - label: _futureKeyTokens?.get()?.map((e) => e as KeyToken?).firstWhere((p) => p?.keytokenID == e, orElse: () => null)?.name ?? '???', + label: _futureKeyTokens.get()?.map((e) => e as KeyToken?).firstWhere((p) => p?.keytokenID == e, orElse: () => null)?.name ?? '???', value: e, type: MessageFilterChipletType.sender, )) diff --git a/flutter/lib/components/modals/filter_modal_sendername.dart b/flutter/lib/components/modals/filter_modal_sendername.dart index d8a79ce..2527009 100644 --- a/flutter/lib/components/modals/filter_modal_sendername.dart +++ b/flutter/lib/components/modals/filter_modal_sendername.dart @@ -15,13 +15,12 @@ class FilterModalSendername extends StatefulWidget { class _FilterModalSendernameState extends State { Set _selectedEntries = {}; - late ImmediateFuture>? _futureSenders; + ImmediateFuture> _futureSenders = ImmediateFuture.ofPending(); @override void initState() { super.initState(); - _futureSenders = null; _futureSenders = ImmediateFuture.ofFuture(() async { final userAcc = Provider.of(context, listen: false); if (!userAcc.isAuth()) throw new Exception('not logged in'); @@ -49,26 +48,22 @@ class _FilterModalSendernameState extends State { content: Container( width: 9000, height: 9000, - child: () { - if (_futureSenders == null) { - return Center(child: CircularProgressIndicator()); - } - - return FutureBuilder( - future: _futureSenders!.future, - builder: ((context, snapshot) { - if (_futureSenders?.value != null) { - return _buildList(context, _futureSenders!.value!); - } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { - return ErrorDisplay(errorMessage: '${snapshot.error}'); - } else if (snapshot.connectionState == ConnectionState.done) { - return _buildList(context, snapshot.data!); - } else { - return Center(child: CircularProgressIndicator()); - } - }), - ); - }(), + child: FutureBuilder( + future: _futureSenders.future, + builder: ((context, snapshot) { + if (_futureSenders.value != null) { + return _buildList(context, _futureSenders.value!); + } else if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { + return ErrorDisplay(errorMessage: '${snapshot.error}'); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { + return _buildList(context, snapshot.data!); + } else { + return ErrorDisplay(errorMessage: 'Invalid future state'); + } + }), + ), ), actions: [ TextButton( diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 20bf8f1..99cec8c 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -9,6 +9,7 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/client.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/nav_layout.dart'; import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart'; @@ -50,6 +51,7 @@ void main() async { Hive.registerAdapter(SCNMessageAdapter()); Hive.registerAdapter(ChannelAdapter()); Hive.registerAdapter(FBMessageAdapter()); + Hive.registerAdapter(KeyTokenAdapter()); print('[INIT] Load Hive...'); @@ -106,6 +108,17 @@ void main() async { ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace}); } + print('[INIT] Load Hive...'); + + try { + await Hive.openBox('scn-keytoken-value-cache'); + } catch (exc, trace) { + Hive.deleteBoxFromDisk('scn-keytoken-value-cache'); + await Hive.openBox('scn-keytoken-value-cache'); + ApplicationLog.error('Failed to open Hive-Box: scn-keytoken-value-cache' + exc.toString(), trace: trace); + ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-keytoken-value-cache', {'error': exc.toString(), 'trace': trace}); + } + print('[INIT] Load AppAuth...'); final appAuth = AppAuth(); // ensure UserAccount is loaded diff --git a/flutter/lib/models/keytoken.dart b/flutter/lib/models/keytoken.dart index d83beca..918dd15 100644 --- a/flutter/lib/models/keytoken.dart +++ b/flutter/lib/models/keytoken.dart @@ -1,12 +1,27 @@ +import 'package:hive_flutter/hive_flutter.dart'; + +part 'keytoken.g.dart'; + +@HiveType(typeId: 107) class KeyToken { + @HiveField(0) final String keytokenID; + + @HiveField(10) final String name; + @HiveField(11) final String timestampCreated; + @HiveField(13) final String? timestampLastUsed; + @HiveField(14) final String ownerUserID; + @HiveField(15) final bool allChannels; + @HiveField(16) final List channels; + @HiveField(17) final String permissions; + @HiveField(18) final int messagesSent; const KeyToken({ diff --git a/flutter/lib/models/keytoken.g.dart b/flutter/lib/models/keytoken.g.dart new file mode 100644 index 0000000..4b01c58 --- /dev/null +++ b/flutter/lib/models/keytoken.g.dart @@ -0,0 +1,65 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'keytoken.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class KeyTokenAdapter extends TypeAdapter { + @override + final int typeId = 107; + + @override + KeyToken read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return KeyToken( + keytokenID: fields[0] as String, + name: fields[10] as String, + timestampCreated: fields[11] as String, + timestampLastUsed: fields[13] as String?, + ownerUserID: fields[14] as String, + allChannels: fields[15] as bool, + channels: (fields[16] as List).cast(), + permissions: fields[17] as String, + messagesSent: fields[18] as int, + ); + } + + @override + void write(BinaryWriter writer, KeyToken obj) { + writer + ..writeByte(9) + ..writeByte(0) + ..write(obj.keytokenID) + ..writeByte(10) + ..write(obj.name) + ..writeByte(11) + ..write(obj.timestampCreated) + ..writeByte(13) + ..write(obj.timestampLastUsed) + ..writeByte(14) + ..write(obj.ownerUserID) + ..writeByte(15) + ..write(obj.allChannels) + ..writeByte(16) + ..write(obj.channels) + ..writeByte(17) + ..write(obj.permissions) + ..writeByte(18) + ..write(obj.messagesSent); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is KeyTokenAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/flutter/lib/pages/account/account.dart b/flutter/lib/pages/account/account.dart index bc9ea2f..6802b60 100644 --- a/flutter/lib/pages/account/account.dart +++ b/flutter/lib/pages/account/account.dart @@ -35,14 +35,13 @@ class AccountRootPage extends StatefulWidget { } class _AccountRootPageState extends State { - late ImmediateFuture? futureSubscriptionCount; - late ImmediateFuture? futureClientCount; - late ImmediateFuture? futureKeyCount; - late ImmediateFuture? futureChannelAllCount; - late ImmediateFuture? futureChannelSubscribedCount; - late ImmediateFuture? futureChannelOwnedCount; - late ImmediateFuture? futureSenderNamesCount; - late ImmediateFuture? futureUser; + ImmediateFuture _futureSubscriptionCount = ImmediateFuture.ofPending(); + ImmediateFuture _futureClientCount = ImmediateFuture.ofPending(); + ImmediateFuture _futureKeyCount = ImmediateFuture.ofPending(); + ImmediateFuture _futureChannelAllCount = ImmediateFuture.ofPending(); + ImmediateFuture _futureChannelOwnedCount = ImmediateFuture.ofPending(); + ImmediateFuture _futureSenderNamesCount = ImmediateFuture.ofPending(); + ImmediateFuture _futureUser = ImmediateFuture.ofPending(); late AppAuth userAcc; @@ -92,58 +91,51 @@ class _AccountRootPageState extends State { } void _createFutures() { - futureSubscriptionCount = null; - futureClientCount = null; - futureKeyCount = null; - futureChannelAllCount = null; - futureChannelSubscribedCount = null; - futureChannelOwnedCount = null; - futureSenderNamesCount = null; + _futureSubscriptionCount = ImmediateFuture.ofPending(); + _futureClientCount = ImmediateFuture.ofPending(); + _futureKeyCount = ImmediateFuture.ofPending(); + _futureChannelAllCount = ImmediateFuture.ofPending(); + _futureChannelOwnedCount = ImmediateFuture.ofPending(); + _futureSenderNamesCount = ImmediateFuture.ofPending(); if (userAcc.isAuth()) { - futureChannelAllCount = ImmediateFuture.ofFuture(() async { + _futureChannelAllCount = ImmediateFuture.ofFuture(() async { if (!userAcc.isAuth()) throw new Exception('not logged in'); final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all); return channels.length; }()); - futureChannelSubscribedCount = ImmediateFuture.ofFuture(() async { - if (!userAcc.isAuth()) throw new Exception('not logged in'); - final channels = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed); - return channels.length; - }()); - - futureChannelOwnedCount = ImmediateFuture.ofFuture(() async { + _futureChannelOwnedCount = ImmediateFuture.ofFuture(() async { if (!userAcc.isAuth()) throw new Exception('not logged in'); final channels = await APIClient.getChannelList(userAcc, ChannelSelector.owned); return channels.length; }()); - futureSubscriptionCount = ImmediateFuture.ofFuture(() async { + _futureSubscriptionCount = ImmediateFuture.ofFuture(() async { if (!userAcc.isAuth()) throw new Exception('not logged in'); final subs = await APIClient.getSubscriptionList(userAcc); return subs.length; }()); - futureClientCount = ImmediateFuture.ofFuture(() async { + _futureClientCount = ImmediateFuture.ofFuture(() async { if (!userAcc.isAuth()) throw new Exception('not logged in'); final clients = await APIClient.getClientList(userAcc); return clients.length; }()); - futureKeyCount = ImmediateFuture.ofFuture(() async { + _futureKeyCount = ImmediateFuture.ofFuture(() async { if (!userAcc.isAuth()) throw new Exception('not logged in'); final keys = await APIClient.getKeyTokenList(userAcc); return keys.length; }()); - futureSenderNamesCount = ImmediateFuture.ofFuture(() async { + _futureSenderNamesCount = ImmediateFuture.ofFuture(() async { if (!userAcc.isAuth()) throw new Exception('not logged in'); final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList(); return senders.length; }()); - futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false)); + _futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false)); } } @@ -157,7 +149,6 @@ class _AccountRootPageState extends State { // refresh all data and then replace teh futures used in build() final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all); - final channelsSubscribed = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed); final subs = await APIClient.getSubscriptionList(userAcc); final clients = await APIClient.getClientList(userAcc); final keys = await APIClient.getKeyTokenList(userAcc); @@ -165,13 +156,12 @@ class _AccountRootPageState extends State { final user = await userAcc.loadUser(force: true); setState(() { - futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length); - futureChannelSubscribedCount = ImmediateFuture.ofValue(channelsSubscribed.length); - futureSubscriptionCount = ImmediateFuture.ofValue(subs.length); - futureClientCount = ImmediateFuture.ofValue(clients.length); - futureKeyCount = ImmediateFuture.ofValue(keys.length); - futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length); - futureUser = ImmediateFuture.ofValue(user); + _futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length); + _futureSubscriptionCount = ImmediateFuture.ofValue(subs.length); + _futureClientCount = ImmediateFuture.ofValue(clients.length); + _futureKeyCount = ImmediateFuture.ofValue(keys.length); + _futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length); + _futureUser = ImmediateFuture.ofValue(user); }); } catch (exc, trace) { ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace); @@ -192,10 +182,10 @@ class _AccountRootPageState extends State { return _buildNoAuth(context); } else { return FutureBuilder( - future: futureUser!.future, + future: _futureUser.future, builder: ((context, snapshot) { - if (futureUser?.value != null) { - return _buildShowAccount(context, acc, futureUser!.value!); + if (_futureUser.value != null) { + return _buildShowAccount(context, acc, _futureUser.value!); } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { return ErrorDisplay(errorMessage: '${snapshot.error}'); } else if (snapshot.connectionState == ConnectionState.done) { @@ -354,10 +344,12 @@ class _AccountRootPageState extends State { children: [ SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))), FutureBuilder( - future: futureChannelOwnedCount!.future, + future: _futureChannelOwnedCount.future, builder: (context, snapshot) { - if (futureChannelOwnedCount?.value != null) { - return Text('${futureChannelOwnedCount!.value}'); + if (_futureChannelOwnedCount.value != null) { + return Text('${_futureChannelOwnedCount.value}'); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { + return Text('ERROR: ${snapshot.error}', style: TextStyle(color: Colors.red)); } else if (snapshot.connectionState == ConnectionState.done) { return Text('${snapshot.data}'); } else { @@ -393,11 +385,11 @@ class _AccountRootPageState extends State { List _buildCards(BuildContext context, User user) { return [ - _buildNumberCard(context, 'Subscription', 's', futureSubscriptionCount, () => Navi.push(context, () => SubscriptionListPage())), - _buildNumberCard(context, 'Client', 's', futureClientCount, () => Navi.push(context, () => ClientListPage())), - _buildNumberCard(context, 'Key', 's', futureKeyCount, () => Navi.push(context, () => KeyTokenListPage())), - _buildNumberCard(context, 'Channel', 's', futureChannelAllCount, () => Navi.push(context, () => ChannelListExtendedPage())), - _buildNumberCard(context, 'Sender', '', futureSenderNamesCount, () => Navi.push(context, () => SenderListPage())), + _buildNumberCard(context, 'Subscription', 's', _futureSubscriptionCount, () => Navi.push(context, () => SubscriptionListPage())), + _buildNumberCard(context, 'Client', 's', _futureClientCount, () => Navi.push(context, () => ClientListPage())), + _buildNumberCard(context, 'Key', 's', _futureKeyCount, () => Navi.push(context, () => KeyTokenListPage())), + _buildNumberCard(context, 'Channel', 's', _futureChannelAllCount, () => Navi.push(context, () => ChannelListExtendedPage())), + _buildNumberCard(context, 'Sender', '', _futureSenderNamesCount, () => Navi.push(context, () => SenderListPage())), UI.buttonCard( context: context, margin: EdgeInsets.fromLTRB(0, 4, 0, 4), @@ -415,17 +407,19 @@ class _AccountRootPageState extends State { ]; } - Widget _buildNumberCard(BuildContext context, String txt, String pluralSuffix, ImmediateFuture? future, void Function() action) { + Widget _buildNumberCard(BuildContext context, String txt, String pluralSuffix, ImmediateFuture future, void Function() action) { return UI.buttonCard( context: context, margin: EdgeInsets.fromLTRB(0, 4, 0, 4), child: Row( children: [ FutureBuilder( - future: future?.future, + future: future.future, builder: (context, snapshot) { - if (future?.value != null) { - return Text('${future!.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); + if (future.value != null) { + return Text('${future.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { + return Text('ERROR: ${snapshot.error}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red)); } else if (snapshot.connectionState == ConnectionState.done) { return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); } else { @@ -435,10 +429,12 @@ class _AccountRootPageState extends State { ), const SizedBox(width: 12), FutureBuilder( - future: future?.future, + future: future.future, builder: (context, snapshot) { - if (future?.value != null) { - return Text('${txt}${((future!.value != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); + if (future.value != null) { + return Text('${txt}${((future.value != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); + } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { + return Text('ERROR: ${snapshot.error}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red)); } else if (snapshot.connectionState == ConnectionState.done) { return Text('${txt}${((snapshot.data != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); } else { @@ -562,7 +558,7 @@ class _AccountRootPageState extends State { try { final user = await APIClient.updateUser(acc, acc.userID!, username: newusername); setState(() { - futureUser = ImmediateFuture.ofValue(user); + _futureUser = ImmediateFuture.ofValue(user); }); Toaster.success("Success", 'Username changed'); diff --git a/flutter/lib/pages/channel_view/channel_view.dart b/flutter/lib/pages/channel_view/channel_view.dart index e768157..61e6fd5 100644 --- a/flutter/lib/pages/channel_view/channel_view.dart +++ b/flutter/lib/pages/channel_view/channel_view.dart @@ -44,9 +44,9 @@ enum EditState { none, editing, saving } enum ChannelViewPageInitState { loading, okay, error } class _ChannelViewPageState extends State { - late ImmediateFuture _futureSubscribeKey; - late ImmediateFuture> _futureSubscriptions; - late ImmediateFuture _futureOwner; + ImmediateFuture _futureSubscribeKey = ImmediateFuture.ofPending(); + ImmediateFuture> _futureSubscriptions = ImmediateFuture.ofPending(); + ImmediateFuture _futureOwner = ImmediateFuture.ofPending(); final TextEditingController _ctrlDisplayName = TextEditingController(); final TextEditingController _ctrlDescriptionName = TextEditingController(); diff --git a/flutter/lib/pages/debug/debug_request_view.dart b/flutter/lib/pages/debug/debug_request_view.dart index a91b1f5..0ca48fc 100644 --- a/flutter/lib/pages/debug/debug_request_view.dart +++ b/flutter/lib/pages/debug/debug_request_view.dart @@ -32,24 +32,7 @@ class _DebugRequestViewPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.start, - children: [ - ...buildRow(context, "name", "Name", widget.request.name), - ...buildRow(context, "timestampStart", "Timestamp (Start)", widget.request.timestampStart.toString()), - ...buildRow(context, "timestampEnd", "Timestamp (End)", widget.request.timestampEnd.toString()), - ...buildRow(context, "duration", "Duration", widget.request.timestampEnd.difference(widget.request.timestampStart).toString()), - Divider(), - ...buildRow(context, "method", "Method", widget.request.method), - ...buildRow(context, "url", "URL", widget.request.url, mono: true), - if (widget.request.requestHeaders.isNotEmpty) ...buildRow(context, "request_headers", "Request->Headers", widget.request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n'), mono: true), - if (widget.request.requestBody != '') ...buildRow(context, "request_body", "Request->Body", widget.request.requestBody, mono: true, json: true), - Divider(), - if (widget.request.responseStatusCode != 0) ...buildRow(context, "response_statuscode", "Response->Statuscode", widget.request.responseStatusCode.toString()), - if (widget.request.responseBody != '') ...buildRow(context, "response_body", "Reponse->Body", widget.request.responseBody, mono: true, json: true), - if (widget.request.responseHeaders.isNotEmpty) ...buildRow(context, "response_headers", "Reponse->Headers", widget.request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n'), mono: true, json: true), - Divider(), - if (widget.request.error != '') ...buildRow(context, "error", "Error", widget.request.error, mono: true), - if (widget.request.stackTrace != '') ...buildRow(context, "trace", "Stacktrace", widget.request.stackTrace, mono: true), - ], + children: [...buildRow(context, "name", "Name", widget.request.name), ...buildRow(context, "timestampStart", "Timestamp (Start)", widget.request.timestampStart.toString()), ...buildRow(context, "timestampEnd", "Timestamp (End)", widget.request.timestampEnd.toString()), ...buildRow(context, "duration", "Duration", widget.request.timestampEnd.difference(widget.request.timestampStart).toString()), Divider(), ...buildRow(context, "method", "Method", widget.request.method), ...buildRow(context, "url", "URL", widget.request.url, mono: true), if (widget.request.requestHeaders.isNotEmpty) ...buildRow(context, "request_headers", "Request->Headers", widget.request.requestHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n'), mono: true), if (widget.request.requestBody != '') ...buildRow(context, "request_body", "Request->Body", widget.request.requestBody, mono: true, json: true), Divider(), if (widget.request.responseStatusCode != 0) ...buildRow(context, "response_statuscode", "Response->Statuscode", widget.request.responseStatusCode.toString()), if (widget.request.responseBody != '') ...buildRow(context, "response_body", "Reponse->Body", widget.request.responseBody, mono: true, json: true), if (widget.request.responseHeaders.isNotEmpty) ...buildRow(context, "response_headers", "Reponse->Headers", widget.request.responseHeaders.entries.map((v) => '${v.key} = ${v.value}').join('\n'), mono: true, json: true), Divider(), if (widget.request.error != '') ...buildRow(context, "error", "Error", widget.request.error, mono: true), if (widget.request.stackTrace != '') ...buildRow(context, "trace", "Stacktrace", widget.request.stackTrace, mono: true), Divider(), UI.button(text: "Copy as curl", onPressed: _copyCurl, tonal: true)], ), ), ), @@ -130,4 +113,18 @@ class _DebugRequestViewPageState extends State { ), ]; } + + void _copyCurl() { + final method = '-X ${widget.request.method}'; + final header = widget.request.requestHeaders.entries.map((v) => '-H "${v.key}: ${v.value}"').join(' '); + final body = widget.request.requestBody.isNotEmpty ? '-d "${widget.request.requestBody}"' : ''; + + final curlParts = ['curl', method, header, '"${widget.request.url}"', body]; + + final txt = curlParts.where((part) => part.isNotEmpty).join(' '); + + Clipboard.setData(new ClipboardData(text: txt)); + Toaster.info("Clipboard", 'Copied text to Clipboard'); + print('================= [CLIPBOARD] =================\n${txt}\n================= [/CLIPBOARD] ================='); + } } diff --git a/flutter/lib/pages/debug/debug_requests.dart b/flutter/lib/pages/debug/debug_requests.dart index 91d2c0e..cf1e4c1 100644 --- a/flutter/lib/pages/debug/debug_requests.dart +++ b/flutter/lib/pages/debug/debug_requests.dart @@ -47,10 +47,6 @@ class _DebugRequestsPageState extends State { textColor: Theme.of(context).colorScheme.onErrorContainer, title: Row( children: [ - SizedBox( - width: 120, - child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), - ), Expanded( child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), ), @@ -61,7 +57,14 @@ class _DebugRequestsPageState extends State { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(req.type), + Row( + children: [ + Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), + Expanded(child: SizedBox()), + Text(req.type), + ], + ), + SizedBox(height: 16), Text( req.error, maxLines: 1, @@ -81,10 +84,6 @@ class _DebugRequestsPageState extends State { child: ListTile( title: Row( children: [ - SizedBox( - width: 120, - child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), - ), Expanded( child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), ), @@ -92,7 +91,13 @@ class _DebugRequestsPageState extends State { Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)), ], ), - subtitle: Text(req.type), + subtitle: Row( + children: [ + Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)), + Expanded(child: SizedBox()), + Text(req.type), + ], + ), ), ), ); diff --git a/flutter/lib/pages/keytoken_list/keytoken_create_modal.dart b/flutter/lib/pages/keytoken_list/keytoken_create_modal.dart new file mode 100644 index 0000000..285a7e4 --- /dev/null +++ b/flutter/lib/pages/keytoken_list/keytoken_create_modal.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; +import 'package:simplecloudnotifier/components/error_display/error_display.dart'; +import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; +import 'package:simplecloudnotifier/state/app_auth.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/types/immediate_future.dart'; +import 'package:simplecloudnotifier/utils/toaster.dart'; + +class KeyTokenCreateDialog extends StatefulWidget { + final void Function(KeyToken, String) onCreated; + + const KeyTokenCreateDialog({ + required this.onCreated, + Key? key, + }) : super(key: key); + + @override + _KeyTokenCreateDialogState createState() => _KeyTokenCreateDialogState(); +} + +class _KeyTokenCreateDialogState extends State { + TextEditingController _ctrlName = TextEditingController(); + Set selectedPermissions = {'CS'}; + + ImmediateFuture> _futureOwnedChannels = ImmediateFuture.ofPending(); + + bool allChannels = true; + Set selectedChannels = new Set(); + + @override + void initState() { + super.initState(); + + final userAcc = Provider.of(context, listen: false); + + setState(() { + _futureOwnedChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.owned).then((p) => p.map((c) => c.channel).toList())); + }); + } + + @override + void dispose() { + _ctrlName.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Create new key'), + content: Container( + width: 0, + height: 400, + child: SingleChildScrollView( + child: Column( + children: [ + _buildNameCtrl(context), + SizedBox(height: 32), + _buildPermissionCtrl(context), + SizedBox(height: 32), + _buildChannelCtrl(context), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Cancel'), + ), + TextButton( + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), + child: const Text('Create'), + onPressed: _create, + ), + ], + ); + } + + Widget _buildNameCtrl(BuildContext context) { + return TextField( + controller: _ctrlName, + decoration: const InputDecoration( + labelText: 'Key name', + hintText: 'Enter a name for the new key', + ), + ); + } + + Widget _buildPermissionCtrl(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Permissions:', style: Theme.of(context).textTheme.labelLarge, textAlign: TextAlign.start), + ListView.builder( + shrinkWrap: true, + primary: false, + itemBuilder: (builder, index) { + final txt = (['Admin', 'Read messages', 'Send messages', 'Read userdata'])[index]; + final prm = (['A', 'CR', 'CS', 'UR'])[index]; + + return ListTile( + contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0), + visualDensity: VisualDensity(horizontal: 0, vertical: -4), + title: Text(txt), + leading: Icon( + selectedPermissions.contains(prm) ? Icons.check_box : Icons.check_box_outline_blank, + color: Theme.of(context).primaryColor, + ), + onTap: () { + setState(() { + if (selectedPermissions.contains(prm)) { + selectedPermissions.remove(prm); + } else { + selectedPermissions.add(prm); + } + }); + }, + ); + }, + itemCount: 4, + ) + ], + ); + } + + Widget _buildChannelCtrl(BuildContext context) { + return FutureBuilder>( + future: _futureOwnedChannels.future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return ErrorDisplay(errorMessage: '${snapshot.error}'); + } + + final ownChannels = snapshot.data!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Channels:', style: Theme.of(context).textTheme.labelLarge, textAlign: TextAlign.start), + ListTile( + contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0), + visualDensity: VisualDensity(horizontal: 0, vertical: -4), + title: Text('All Channels'), + leading: Icon( + allChannels ? Icons.check_box : Icons.check_box_outline_blank, + color: Theme.of(context).primaryColor, + ), + onTap: () { + setState(() { + allChannels = !allChannels; + }); + }, + ), + SizedBox(height: 16), + if (!allChannels) + ListView.builder( + shrinkWrap: true, + primary: false, + itemBuilder: (builder, index) { + return ListTile( + contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0), + visualDensity: VisualDensity(horizontal: 0, vertical: -4), + title: Text(ownChannels[index].displayName), + leading: Icon( + selectedChannels.contains(ownChannels[index].channelID) ? Icons.check_box : Icons.check_box_outline_blank, + color: Theme.of(context).primaryColor, + ), + onTap: () { + setState(() { + if (selectedChannels.contains(ownChannels[index].channelID)) { + selectedChannels.remove(ownChannels[index].channelID); + } else { + selectedChannels.add(ownChannels[index].channelID); + } + }); + }, + ); + }, + itemCount: ownChannels.length, + ), + ], + ); + }, + ); + } + + void _create() async { + final userAcc = Provider.of(context, listen: false); + + if (!userAcc.isAuth()) return; + + if (_ctrlName.text.isEmpty) { + Toaster.error('Missing data', 'Please enter a name for the key'); + return; + } + + try { + final perm = selectedPermissions.join(';'); + final channels = allChannels ? [] : selectedChannels.toList(); + + var kt = await APIClient.createKeyToken(userAcc, _ctrlName.text, perm, allChannels, channels: channels); + Toaster.success('Success', 'Key created successfully'); + Navigator.of(context).pop(); + widget.onCreated(kt.keyToken, kt.token); + } catch (exc, trace) { + ApplicationLog.error('Failed to create keytoken: ' + exc.toString(), trace: trace); + Toaster.error("Error", 'Failed to create key: ${exc.toString()}'); + } + } +} diff --git a/flutter/lib/pages/keytoken_list/keytoken_created_modal.dart b/flutter/lib/pages/keytoken_list/keytoken_created_modal.dart new file mode 100644 index 0000000..57ae4a4 --- /dev/null +++ b/flutter/lib/pages/keytoken_list/keytoken_created_modal.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:simplecloudnotifier/components/badge_display/badge_display.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; +import 'package:simplecloudnotifier/utils/toaster.dart'; +import 'package:simplecloudnotifier/utils/ui.dart'; + +class KeyTokenCreatedModal extends StatelessWidget { + final KeyToken keytoken; + final String tokenValue; + + const KeyTokenCreatedModal({ + Key? key, + required this.keytoken, + required this.tokenValue, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('A new key was created'), + content: Container( + width: 0, + height: 350, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidIdCardClip, + title: 'KeyTokenID', + values: [keytoken.keytokenID], + ), + UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidInputText, + title: 'Name', + values: [keytoken.name], + ), + UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidShieldKeyhole, + title: 'Permissions', + values: _formatPermissions(keytoken.permissions), + ), + const SizedBox(height: 16), + const BadgeDisplay( + text: "Please copy and save the token now, it cannot be retrieved later.", + icon: null, + mode: BadgeMode.warn, + ), + const SizedBox(height: 4), + UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidKey, + title: 'Token', + values: [tokenValue.substring(0, 12) + '...'], + iconActions: [(FontAwesomeIcons.copy, null, _copy)], + ), + ], + ), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } + + List _formatPermissions(String v) { + var splt = v.split(';'); + + if (splt.length == 0) return ["None"]; + + List result = []; + + if (splt.contains("A")) result.add("Admin"); + if (splt.contains("UR")) result.add("Read Account"); + if (splt.contains("CR")) result.add("Read Messages"); + if (splt.contains("CS")) result.add("Send Messages"); + + return result; + } + + void _copy() { + Clipboard.setData(new ClipboardData(text: tokenValue)); + Toaster.info("Clipboard", 'Copied text to Clipboard'); + print('================= [CLIPBOARD] =================\n${tokenValue}\n================= [/CLIPBOARD] ================='); + } +} diff --git a/flutter/lib/pages/keytoken_list/keytoken_list.dart b/flutter/lib/pages/keytoken_list/keytoken_list.dart index dbee27c..493191e 100644 --- a/flutter/lib/pages/keytoken_list/keytoken_list.dart +++ b/flutter/lib/pages/keytoken_list/keytoken_list.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/keytoken.dart'; +import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_create_modal.dart'; +import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_created_modal.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart'; @@ -81,6 +84,27 @@ class _KeyTokenListPageState extends State { ), ), ), + floatingActionButton: FloatingActionButton( + heroTag: 'fab_keytokenlist_plus', + onPressed: () { + showDialog( + context: context, + builder: (context) => KeyTokenCreateDialog(onCreated: _created), + ); + }, + child: const Icon(FontAwesomeIcons.plus), + ), + ); + } + + void _created(KeyToken token, String tokValue) { + setState(() { + _pagingController.itemList?.insert(0, token); + }); + + showDialog( + context: context, + builder: (context) => KeyTokenCreatedModal(keytoken: token, tokenValue: tokValue), ); } } diff --git a/flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart b/flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart new file mode 100644 index 0000000..c4ffeb4 --- /dev/null +++ b/flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; + +class EditKeyTokenChannelsDialog extends StatefulWidget { + final List ownedChannels; + final KeyTokenPreview keytoken; + + final void Function(Set) onUpdateChannels; + final void Function() onUpdateSetAllChannels; + + const EditKeyTokenChannelsDialog({ + required this.ownedChannels, + required this.keytoken, + required this.onUpdateChannels, + required this.onUpdateSetAllChannels, + Key? key, + }) : super(key: key); + + @override + _EditKeyTokenChannelsDialogState createState() => _EditKeyTokenChannelsDialogState(); +} + +class _EditKeyTokenChannelsDialogState extends State { + late bool allChannels; + late Set selectedEntries; + + @override + void initState() { + super.initState(); + allChannels = widget.keytoken.allChannels; + selectedEntries = (widget.keytoken.channels).toSet(); + } + + @override + Widget build(BuildContext context) { + var ownChannels = widget.ownedChannels.toList(); + ownChannels.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase())); + + return AlertDialog( + title: const Text('Channels'), + content: Container( + width: 0, + height: 400, + child: Column( + children: [ + ListTile( + contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0), + visualDensity: VisualDensity(horizontal: 0, vertical: -4), + title: Text('All Channels'), + leading: Icon( + allChannels ? Icons.check_box : Icons.check_box_outline_blank, + color: Theme.of(context).primaryColor, + ), + onTap: () { + setState(() { + allChannels = !allChannels; + }); + }, + ), + SizedBox(height: 16), + if (!allChannels) + Expanded( + child: ListView.builder( + shrinkWrap: true, + itemBuilder: (builder, index) { + return ListTile( + contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0), + visualDensity: VisualDensity(horizontal: 0, vertical: -4), + title: Text(ownChannels[index].displayName), + leading: Icon( + selectedEntries.contains(ownChannels[index].channelID) ? Icons.check_box : Icons.check_box_outline_blank, + color: Theme.of(context).primaryColor, + ), + onTap: () { + setState(() { + if (selectedEntries.contains(ownChannels[index].channelID)) { + selectedEntries.remove(ownChannels[index].channelID); + } else { + selectedEntries.add(ownChannels[index].channelID); + } + }); + }, + ); + }, + itemCount: ownChannels.length, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Cancel'), + ), + TextButton( + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), + child: const Text('Update'), + onPressed: () { + if (allChannels) { + widget.onUpdateSetAllChannels(); + } else { + widget.onUpdateChannels(selectedEntries); + } + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/flutter/lib/pages/keytoken_view/keytoken_permission_modal.dart b/flutter/lib/pages/keytoken_view/keytoken_permission_modal.dart new file mode 100644 index 0000000..deab35e --- /dev/null +++ b/flutter/lib/pages/keytoken_view/keytoken_permission_modal.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; + +class EditKeyTokenPermissionsDialog extends StatefulWidget { + final KeyTokenPreview keytoken; + + final void Function(String) onUpdatePermissions; + + const EditKeyTokenPermissionsDialog({ + required this.keytoken, + required this.onUpdatePermissions, + Key? key, + }) : super(key: key); + + @override + _EditKeyTokenPermissionsDialogState createState() => _EditKeyTokenPermissionsDialogState(); +} + +class _EditKeyTokenPermissionsDialogState extends State { + Set selectedPermissions = new Set(); + + @override + void initState() { + super.initState(); + + for (var p in widget.keytoken.permissions.split(';')) { + if (p.isNotEmpty) selectedPermissions.add(p); + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Permissions'), + content: Container( + width: 0, + height: 400, + child: ListView.builder( + shrinkWrap: true, + itemBuilder: (builder, index) { + final txt = (['Admin', 'Read messages', 'Send messages', 'Read userdata'])[index]; + final prm = (['A', 'CR', 'CS', 'UR'])[index]; + + return ListTile( + contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0), + visualDensity: VisualDensity(horizontal: 0, vertical: -4), + title: Text(txt), + leading: Icon( + selectedPermissions.contains(prm) ? Icons.check_box : Icons.check_box_outline_blank, + color: Theme.of(context).primaryColor, + ), + onTap: () { + setState(() { + if (selectedPermissions.contains(prm)) { + selectedPermissions.remove(prm); + } else { + selectedPermissions.add(prm); + } + }); + }, + ); + }, + itemCount: 4, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Cancel'), + ), + TextButton( + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), + child: const Text('Update'), + onPressed: () { + widget.onUpdatePermissions(selectedPermissions.join(';')); + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/flutter/lib/pages/keytoken_view/keytoken_view.dart b/flutter/lib/pages/keytoken_view/keytoken_view.dart index 2bdb955..390f0d7 100644 --- a/flutter/lib/pages/keytoken_view/keytoken_view.dart +++ b/flutter/lib/pages/keytoken_view/keytoken_view.dart @@ -1,16 +1,23 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; +import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart'; +import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_channel_modal.dart'; +import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_permission_modal.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/state/scn_data_cache.dart'; import 'package:simplecloudnotifier/types/immediate_future.dart'; +import 'package:simplecloudnotifier/utils/dialogs.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/ui.dart'; @@ -40,7 +47,11 @@ enum KeyTokenViewPageInitState { loading, okay, error } class _KeyTokenViewPageState extends State { static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting - late ImmediateFuture _futureOwner; + ImmediateFuture _futureOwner = ImmediateFuture.ofPending(); + + ImmediateFuture> _futureAllChannels = ImmediateFuture.ofPending(); + + ImmediateFuture> _futureOwnedChannels = ImmediateFuture.ofPending(); final TextEditingController _ctrlName = TextEditingController(); @@ -55,6 +66,9 @@ class _KeyTokenViewPageState extends State { KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading; String errorMessage = ''; + KeyToken? keytokenUserAccAdmin; + KeyToken? keytokenUserAccSend; + @override void initState() { _initStateAsync(true); @@ -102,14 +116,48 @@ class _KeyTokenViewPageState extends State { if (this.keytokenPreview!.ownerUserID == userAcc.userID) { var cacheUser = userAcc.getUserOrNull(); if (cacheUser != null) { - _futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview()); + _futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview()); } else { - _futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc)); + _futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc)); } } else { - _futureOwner = ImmediateFuture.ofFuture(APIClient.getUserPreview(userAcc, this.keytokenPreview!.ownerUserID)); + _futureOwner = ImmediateFuture.ofFuture(APIClient.getUserPreview(userAcc, this.keytokenPreview!.ownerUserID)); } }); + + setState(() { + _futureAllChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.allAny).then((lst) async { + Map result = {}; + + for (var c in lst) result[c.channel.channelID] = c.channel.toPreview(c.subscription); + + if (keytokenPreview != null) { + for (var cid in keytokenPreview!.channels) { + if (!result.containsKey(cid)) { + result[cid] = await APIClient.getChannelPreview(userAcc, cid); + } + } + } + + return result; + })); + }); + + setState(() { + _futureOwnedChannels = ImmediateFuture.ofFuture(APIClient.getChannelList(userAcc, ChannelSelector.owned).then((p) => p.map((c) => c.channel).toList())); + }); + + SCNDataCache().getOrQueryTokenByValue(userAcc.userID!, userAcc.tokenAdmin!).then((token) { + setState(() { + keytokenUserAccAdmin = token; + }); + }); + + SCNDataCache().getOrQueryTokenByValue(userAcc.userID!, userAcc.tokenSend!).then((token) { + setState(() { + keytokenUserAccSend = token; + }); + }); } @override @@ -158,18 +206,22 @@ class _KeyTokenViewPageState extends State { context: context, icon: FontAwesomeIcons.solidIdCardClip, title: 'KeyTokenID', - values: [keytoken.keytokenID], + values: [ + keytoken.keytokenID, + if (keytokenUserAccAdmin?.keytokenID == keytoken.keytokenID) '(Currently used as Admin-Token)', + if (keytokenUserAccSend?.keytokenID == keytoken.keytokenID) '(Currently used as Send-Token)', + ], ), _buildNameCard(context, true), UI.metaCard( context: context, - icon: FontAwesomeIcons.clock, + icon: FontAwesomeIcons.solidClock, title: 'Created', values: [_KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())], ), UI.metaCard( context: context, - icon: FontAwesomeIcons.clockTwo, + icon: FontAwesomeIcons.solidClockTwo, title: 'Last Used', values: [(keytoken.timestampLastUsed == null) ? 'Never' : _KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())], ), @@ -297,7 +349,6 @@ class _KeyTokenViewPageState extends State { setState(() { _ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? ''; _editName = EditState.editing; - if (_editName == EditState.editing) _editName = EditState.none; }); } @@ -355,7 +406,7 @@ class _KeyTokenViewPageState extends State { if (isOwned) { w1 = UI.metaCard( context: context, - icon: FontAwesomeIcons.shieldKeyhole, + icon: FontAwesomeIcons.solidShieldKeyhole, title: 'Permissions', values: _formatPermissions(keyToken.permissions), iconActions: [(FontAwesomeIcons.penToSquare, null, _editPermissions)], @@ -363,28 +414,53 @@ class _KeyTokenViewPageState extends State { } else { w1 = UI.metaCard( context: context, - icon: FontAwesomeIcons.shieldKeyhole, + icon: FontAwesomeIcons.solidShieldKeyhole, title: 'Permissions', values: _formatPermissions(keyToken.permissions), ); } - if (isOwned) { - w2 = UI.metaCard( - context: context, - icon: FontAwesomeIcons.solidSnake, - title: 'Channels', - values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, //TODO show channel names - iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)], - ); - } else { - w2 = UI.metaCard( - context: context, - icon: FontAwesomeIcons.solidSnake, - title: 'Channels', - values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, //TODO show channel names - ); - } + w2 = FutureBuilder( + future: _futureAllChannels.future, + builder: (context, snapshot) { + if (snapshot.hasData) { + var cmap = snapshot.data!; + if (isOwned) { + return UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidSnake, + title: 'Channels', + values: (keyToken.allChannels) ? (['All Channels']) : (keyToken.channels.isEmpty ? ['(None)'] : (keyToken.channels.map((c) => cmap[c]?.displayName ?? c).toList())), + iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)], + ); + } else { + return UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidSnake, + title: 'Channels', + values: (keyToken.allChannels) ? (['All Channels']) : (keyToken.channels.isEmpty ? ['(None)'] : (keyToken.channels.map((c) => cmap[c]?.displayName ?? c).toList())), + ); + } + } else { + if (isOwned) { + return UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidSnake, + title: 'Channels', + values: (keyToken.allChannels) ? ['All Channels'] : (keyToken.channels.isEmpty ? ['(None)'] : keyToken.channels), + iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)], + ); + } else { + return UI.metaCard( + context: context, + icon: FontAwesomeIcons.solidSnake, + title: 'Channels', + values: (keyToken.allChannels) ? ['All Channels'] : (keyToken.channels.isEmpty ? ['(None)'] : keyToken.channels), + ); + } + } + }, + ); return [w1, w2]; } @@ -404,33 +480,138 @@ class _KeyTokenViewPageState extends State { return result; } - void _editPermissions() { + void _editPermissions() async { final acc = Provider.of(context, listen: false); - //TODO prevent editing current admin/read token + if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) { + Toaster.error("Error", "You cannot edit the currently used token"); + return; + } + if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) { + Toaster.error("Error", "You cannot edit the currently used token"); + return; + } - //TODO - - Toaster.info("Not implemented", "Currently not implemented"); + await showDialog( + context: context, + builder: (context) => EditKeyTokenPermissionsDialog( + keytoken: keytokenPreview!, + onUpdatePermissions: _updatePermissions, + ), + ); } - void _editChannels() { + void _editChannels() async { final acc = Provider.of(context, listen: false); - //TODO prevent editing current admin/read token + if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) { + Toaster.error("Error", "You cannot edit the currently used token"); + return; + } + if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) { + Toaster.error("Error", "You cannot edit the currently used token"); + return; + } - //TODO + var ownChannels = (await _futureOwnedChannels.future); + ownChannels.sort((a, b) => a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase())); - Toaster.info("Not implemented", "Currently not implemented"); + await showDialog( + context: context, + builder: (context) => EditKeyTokenChannelsDialog( + ownedChannels: ownChannels, + keytoken: keytokenPreview!, + onUpdateChannels: _updateChannelsSelected, + onUpdateSetAllChannels: _updateChannelsAll, + ), + ); } - void _deleteKey() { + void _deleteKey() async { final acc = Provider.of(context, listen: false); - //TODO prevent deleting current admin/read token + if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) { + Toaster.error("Error", "You cannot delete the currently used token"); + return; + } + if (keytokenUserAccSend == null || keytokenUserAccSend!.keytokenID == keytokenPreview!.keytokenID) { + Toaster.error("Error", "You cannot delete the currently used token"); + return; + } - //TODO + try { + final r = await UIDialogs.showConfirmDialog(context, 'Really (permanently) delete this Key?', okText: 'Unsubscribe', cancelText: 'Cancel'); + if (!r) return; - Toaster.info("Not implemented", "Currently not implemented"); + await APIClient.deleteKeyToken(acc, keytokenPreview!.keytokenID); + widget.needsReload?.call(); + + Toaster.info('Logout', 'Successfully deleted the key'); + + Navi.pop(context); + } catch (exc, trace) { + Toaster.error("Error", 'Failed to delete key'); + ApplicationLog.error('Failed to delete key: ' + exc.toString(), trace: trace); + } + } + + void _updateChannelsSelected(Set selectedEntries) async { + final acc = Provider.of(context, listen: false); + + try { + final r = await APIClient.updateKeyToken(acc, widget.keytokenID, channels: selectedEntries.toList(), allChannels: false); + + setState(() { + keytoken = r; + keytokenPreview = r.toPreview(); + }); + + Toaster.info("Success", "Key updated"); + + widget.needsReload?.call(); + } catch (exc, trace) { + ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); + Toaster.error("Error", 'Failed to update key'); + } + } + + void _updateChannelsAll() async { + final acc = Provider.of(context, listen: false); + + try { + final r = await APIClient.updateKeyToken(acc, widget.keytokenID, channels: [], allChannels: true); + + setState(() { + keytoken = r; + keytokenPreview = r.toPreview(); + }); + + Toaster.info("Success", "Key updated"); + + widget.needsReload?.call(); + } catch (exc, trace) { + ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); + Toaster.error("Error", 'Failed to update key'); + } + } + + void _updatePermissions(String perm) async { + final acc = Provider.of(context, listen: false); + + try { + final r = await APIClient.updateKeyToken(acc, widget.keytokenID, permissions: perm); + + setState(() { + keytoken = r; + keytokenPreview = r.toPreview(); + }); + + Toaster.info("Success", "Key updated"); + + widget.needsReload?.call(); + } catch (exc, trace) { + ApplicationLog.error('Failed to update key: ' + exc.toString(), trace: trace); + Toaster.error("Error", 'Failed to update key'); + } } } diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index 75993bd..bf3f411 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -12,6 +12,7 @@ import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart'; import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart'; +import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; @@ -35,6 +36,7 @@ class MessageViewPage extends StatefulWidget { class _MessageViewPageState extends State { late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture; (SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null; + static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting final ScrollController _controller = ScrollController(); @@ -164,8 +166,12 @@ class _MessageViewPageState extends State { icon: FontAwesomeIcons.solidGearCode, title: 'KeyToken', values: [message.usedKeyID, token?.name ?? '...'], - mainAction: () => { - Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID]))) + mainAction: () { + if (message.senderUserID == userAccUserID) { + Navi.push(context, () => KeyTokenViewPage(keytokenID: message.usedKeyID, preloadedData: null, needsReload: null)); + } else { + Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID]))); + } }, ), UI.metaCard( @@ -210,7 +216,7 @@ class _MessageViewPageState extends State { ), ); - var showScrollbar = true; + var showScrollbar = false; if (!_monospaceMode && (message.content ?? '').length > 4096) showScrollbar = true; if (_monospaceMode && (message.content ?? '').split('\n').length > 64) showScrollbar = true; diff --git a/flutter/lib/pages/subscription_view/subscription_view.dart b/flutter/lib/pages/subscription_view/subscription_view.dart index 5158950..4ad0f6a 100644 --- a/flutter/lib/pages/subscription_view/subscription_view.dart +++ b/flutter/lib/pages/subscription_view/subscription_view.dart @@ -42,9 +42,9 @@ enum SubscriptionViewPageInitState { loading, okay, error } class _SubscriptionViewPageState extends State { static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting - late ImmediateFuture _futureChannelOwner; - late ImmediateFuture _futureSubscriber; - late ImmediateFuture _futureChannel; + ImmediateFuture _futureChannelOwner = ImmediateFuture.ofPending(); + ImmediateFuture _futureSubscriber = ImmediateFuture.ofPending(); + ImmediateFuture _futureChannel = ImmediateFuture.ofPending(); int _loadingIndeterminateCounter = 0; diff --git a/flutter/lib/state/scn_data_cache.dart b/flutter/lib/state/scn_data_cache.dart index f5da15a..9c9ad0f 100644 --- a/flutter/lib/state/scn_data_cache.dart +++ b/flutter/lib/state/scn_data_cache.dart @@ -1,5 +1,7 @@ import 'package:hive_flutter/hive_flutter.dart'; +import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/settings/app_settings.dart'; @@ -57,4 +59,21 @@ class SCNDataCache { return cacheMessages; } + + Future getOrQueryTokenByValue(String uid, String tokVal) async { + final cache = Hive.box('scn-keytoken-value-cache'); + + final cacheVal = cache.get(tokVal); + if (cacheVal != null) { + print('[SCNDataCache] Found Token(${tokVal}) in cache'); + return Future.value(cacheVal); + } + + final tok = await APIClient.getKeyTokenByToken(uid, tokVal); + + print('[SCNDataCache] Queried Token(${tokVal}) from API'); + await cache.put(tokVal, tok); + + return tok; + } } diff --git a/flutter/lib/types/immediate_future.dart b/flutter/lib/types/immediate_future.dart index 07e65a2..2a9eb0c 100644 --- a/flutter/lib/types/immediate_future.dart +++ b/flutter/lib/types/immediate_future.dart @@ -2,6 +2,8 @@ // Unfortunately Future.value(x) in FutureBuilder always results in one frame were snapshot.connectionState is waiting // This way we can set the ImmediateFuture.value directly and circumvent that. +import 'dart:async'; + class ImmediateFuture { final Future future; final T? value; @@ -20,6 +22,10 @@ class ImmediateFuture { : future = Future.value(v), value = v; + ImmediateFuture.ofPending() + : future = Completer().future, + value = null; + T? get() { return value ?? _futureValue; } diff --git a/scnserver/api/handler/apiKeyToken.go b/scnserver/api/handler/apiKeyToken.go index f4dcbcf..c12f0e4 100644 --- a/scnserver/api/handler/apiKeyToken.go +++ b/scnserver/api/handler/apiKeyToken.go @@ -104,7 +104,7 @@ func (h APIHandler) GetCurrentUserKey(pctx ginext.PreContext) ginext.HTTPRespons return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) } if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query keyToken", err) } return finishSuccess(ginext.JSONWithFilter(http.StatusOK, keytoken, "INCLUDE_TOKEN")) @@ -153,7 +153,7 @@ func (h APIHandler) GetUserKey(pctx ginext.PreContext) ginext.HTTPResponse { return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) } if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query key", err) } return finishSuccess(ginext.JSON(http.StatusOK, keytoken)) @@ -210,7 +210,7 @@ func (h APIHandler) UpdateUserKey(pctx ginext.PreContext) ginext.HTTPResponse { return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) } if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query key", err) } if b.Name != nil { @@ -372,12 +372,12 @@ func (h APIHandler) DeleteUserKey(pctx ginext.PreContext) ginext.HTTPResponse { return *permResp } - client, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) + token, err := h.database.GetKeyToken(ctx, u.UserID, u.KeyID) if errors.Is(err, sql.ErrNoRows) { return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) } if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query key", err) } if u.KeyID == *ctx.GetPermissionKeyTokenID() { @@ -386,10 +386,10 @@ func (h APIHandler) DeleteUserKey(pctx ginext.PreContext) ginext.HTTPResponse { err = h.database.DeleteKeyToken(ctx, u.KeyID) if err != nil { - return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete client", err) + return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to delete key", err) } - return finishSuccess(ginext.JSON(http.StatusOK, client)) + return finishSuccess(ginext.JSON(http.StatusOK, token)) }) }