Finish KeyToken operations

This commit is contained in:
2025-04-18 18:56:17 +02:00
parent 1f0f280286
commit 78c895547e
23 changed files with 1089 additions and 211 deletions

View File

@@ -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<KeyTokenViewPage> {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
late ImmediateFuture<UserPreview> _futureOwner;
ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
ImmediateFuture<Map<String, ChannelPreview>> _futureAllChannels = ImmediateFuture.ofPending();
ImmediateFuture<List<Channel>> _futureOwnedChannels = ImmediateFuture.ofPending();
final TextEditingController _ctrlName = TextEditingController();
@@ -55,6 +66,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading;
String errorMessage = '';
KeyToken? keytokenUserAccAdmin;
KeyToken? keytokenUserAccSend;
@override
void initState() {
_initStateAsync(true);
@@ -102,14 +116,48 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
if (this.keytokenPreview!.ownerUserID == userAcc.userID) {
var cacheUser = userAcc.getUserOrNull();
if (cacheUser != null) {
_futureOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
_futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview());
} else {
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(_getOwner(userAcc));
_futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc));
}
} else {
_futureOwner = ImmediateFuture<UserPreview>.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<String, ChannelPreview> 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<KeyTokenViewPage> {
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<KeyTokenViewPage> {
setState(() {
_ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? '';
_editName = EditState.editing;
if (_editName == EditState.editing) _editName = EditState.none;
});
}
@@ -355,7 +406,7 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
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<KeyTokenViewPage> {
} 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<KeyTokenViewPage> {
return result;
}
void _editPermissions() {
void _editPermissions() async {
final acc = Provider.of<AppAuth>(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<void>(
context: context,
builder: (context) => EditKeyTokenPermissionsDialog(
keytoken: keytokenPreview!,
onUpdatePermissions: _updatePermissions,
),
);
}
void _editChannels() {
void _editChannels() async {
final acc = Provider.of<AppAuth>(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<void>(
context: context,
builder: (context) => EditKeyTokenChannelsDialog(
ownedChannels: ownChannels,
keytoken: keytokenPreview!,
onUpdateChannels: _updateChannelsSelected,
onUpdateSetAllChannels: _updateChannelsAll,
),
);
}
void _deleteKey() {
void _deleteKey() async {
final acc = Provider.of<AppAuth>(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<String> selectedEntries) async {
final acc = Provider.of<AppAuth>(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<AppAuth>(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<AppAuth>(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');
}
}
}