Finish KeyToken operations

This commit is contained in:
Mike Schwörer 2025-04-18 18:56:17 +02:00
parent 1f0f280286
commit 78c895547e
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
23 changed files with 1089 additions and 211 deletions

View File

@ -445,6 +445,16 @@ class APIClient {
); );
} }
static Future<void> deleteKeyToken(AppAuth acc, String keytokenID) {
return _request(
name: 'deleteKeyToken',
method: 'DELETE',
relURL: 'users/${acc.getUserID()}/keys/${keytokenID}',
fn: (_) => null,
authToken: acc.getToken(),
);
}
static Future<KeyToken> updateKeyToken(TokenSource auth, String kid, {String? name, bool? allChannels, List<String>? channels, String? permissions}) async { static Future<KeyToken> updateKeyToken(TokenSource auth, String kid, {String? name, bool? allChannels, List<String>? channels, String? permissions}) async {
return await _request( return await _request(
name: 'updateKeyToken', name: 'updateKeyToken',
@ -468,7 +478,7 @@ class APIClient {
relURL: 'users/${auth.getUserID()}/keys', relURL: 'users/${auth.getUserID()}/keys',
jsonBody: { jsonBody: {
'name': name, 'name': name,
'pem': perm, 'permissions': perm,
'all_channels': allChannels, 'all_channels': allChannels,
if (channels != null) 'channels': channels, if (channels != null) 'channels': channels,
}, },

View File

@ -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),
),
),
],
),
);
}
}

View File

@ -17,13 +17,12 @@ class FilterModalChannel extends StatefulWidget {
class _FilterModalChannelState extends State<FilterModalChannel> { class _FilterModalChannelState extends State<FilterModalChannel> {
Set<String> _selectedEntries = {}; Set<String> _selectedEntries = {};
late ImmediateFuture<List<Channel>>? _futureChannels; ImmediateFuture<List<Channel>> _futureChannels = ImmediateFuture.ofPending();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_futureChannels = null;
_futureChannels = ImmediateFuture.ofFuture(() async { _futureChannels = ImmediateFuture.ofFuture(() async {
final userAcc = Provider.of<AppAuth>(context, listen: false); final userAcc = Provider.of<AppAuth>(context, listen: false);
if (!userAcc.isAuth()) throw new Exception('not logged in'); if (!userAcc.isAuth()) throw new Exception('not logged in');
@ -51,45 +50,39 @@ class _FilterModalChannelState extends State<FilterModalChannel> {
content: Container( content: Container(
width: 9000, width: 9000,
height: 9000, height: 9000,
child: () { child: FutureBuilder(
if (_futureChannels == null) { future: _futureChannels.future,
return Center(child: CircularProgressIndicator()); builder: ((context, snapshot) {
} if (_futureChannels.value != null) {
return _buildList(context, _futureChannels.value!);
return FutureBuilder( } else if (snapshot.connectionState == ConnectionState.waiting) {
future: _futureChannels!.future, return Center(child: CircularProgressIndicator());
builder: ((context, snapshot) { } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
if (_futureChannels?.value != null) { return ErrorDisplay(errorMessage: '${snapshot.error}');
return _buildList(context, _futureChannels!.value!); } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { return _buildList(context, snapshot.data!);
return ErrorDisplay(errorMessage: '${snapshot.error}'); } else {
} else if (snapshot.connectionState == ConnectionState.done) { return ErrorDisplay(errorMessage: 'Invalid future state');
return _buildList(context, snapshot.data!); }
} else { }),
return Center(child: CircularProgressIndicator()); ),
}
}),
);
}(),
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
child: const Text('Apply'), child: const Text('Apply'),
onPressed: () { onPressed: _onOkay,
onOkay();
},
), ),
], ],
); );
} }
void onOkay() { void _onOkay() {
Navi.popDialog(context); Navi.popDialog(context);
final chiplets = _selectedEntries final chiplets = _selectedEntries
.map((e) => MessageFilterChiplet( .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, value: e,
type: MessageFilterChipletType.channel, type: MessageFilterChipletType.channel,
)) ))

View File

@ -17,13 +17,12 @@ class FilterModalKeytoken extends StatefulWidget {
class _FilterModalKeytokenState extends State<FilterModalKeytoken> { class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
Set<String> _selectedEntries = {}; Set<String> _selectedEntries = {};
late ImmediateFuture<List<KeyToken>>? _futureKeyTokens; ImmediateFuture<List<KeyToken>> _futureKeyTokens = ImmediateFuture.ofPending();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_futureKeyTokens = null;
_futureKeyTokens = ImmediateFuture.ofFuture(() async { _futureKeyTokens = ImmediateFuture.ofFuture(() async {
final userAcc = Provider.of<AppAuth>(context, listen: false); final userAcc = Provider.of<AppAuth>(context, listen: false);
if (!userAcc.isAuth()) throw new Exception('not logged in'); if (!userAcc.isAuth()) throw new Exception('not logged in');
@ -51,26 +50,22 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
content: Container( content: Container(
width: 9000, width: 9000,
height: 9000, height: 9000,
child: () { child: FutureBuilder(
if (_futureKeyTokens == null) { future: _futureKeyTokens.future,
return Center(child: CircularProgressIndicator()); builder: ((context, snapshot) {
} if (_futureKeyTokens.value != null) {
return _buildList(context, _futureKeyTokens.value!);
return FutureBuilder( } else if (snapshot.connectionState == ConnectionState.waiting) {
future: _futureKeyTokens!.future, return Center(child: CircularProgressIndicator());
builder: ((context, snapshot) { } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
if (_futureKeyTokens?.value != null) { return ErrorDisplay(errorMessage: '${snapshot.error}');
return _buildList(context, _futureKeyTokens!.value!); } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { return _buildList(context, snapshot.data!);
return ErrorDisplay(errorMessage: '${snapshot.error}'); } else {
} else if (snapshot.connectionState == ConnectionState.done) { return ErrorDisplay(errorMessage: 'Invalid future state');
return _buildList(context, snapshot.data!); }
} else { }),
return Center(child: CircularProgressIndicator()); ),
}
}),
);
}(),
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
@ -89,7 +84,7 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
final chiplets = _selectedEntries final chiplets = _selectedEntries
.map((e) => MessageFilterChiplet( .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, value: e,
type: MessageFilterChipletType.sender, type: MessageFilterChipletType.sender,
)) ))

View File

@ -15,13 +15,12 @@ class FilterModalSendername extends StatefulWidget {
class _FilterModalSendernameState extends State<FilterModalSendername> { class _FilterModalSendernameState extends State<FilterModalSendername> {
Set<String> _selectedEntries = {}; Set<String> _selectedEntries = {};
late ImmediateFuture<List<String>>? _futureSenders; ImmediateFuture<List<String>> _futureSenders = ImmediateFuture.ofPending();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_futureSenders = null;
_futureSenders = ImmediateFuture.ofFuture(() async { _futureSenders = ImmediateFuture.ofFuture(() async {
final userAcc = Provider.of<AppAuth>(context, listen: false); final userAcc = Provider.of<AppAuth>(context, listen: false);
if (!userAcc.isAuth()) throw new Exception('not logged in'); if (!userAcc.isAuth()) throw new Exception('not logged in');
@ -49,26 +48,22 @@ class _FilterModalSendernameState extends State<FilterModalSendername> {
content: Container( content: Container(
width: 9000, width: 9000,
height: 9000, height: 9000,
child: () { child: FutureBuilder(
if (_futureSenders == null) { future: _futureSenders.future,
return Center(child: CircularProgressIndicator()); builder: ((context, snapshot) {
} if (_futureSenders.value != null) {
return _buildList(context, _futureSenders.value!);
return FutureBuilder( } else if (snapshot.connectionState == ConnectionState.waiting) {
future: _futureSenders!.future, return Center(child: CircularProgressIndicator());
builder: ((context, snapshot) { } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
if (_futureSenders?.value != null) { return ErrorDisplay(errorMessage: '${snapshot.error}');
return _buildList(context, _futureSenders!.value!); } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { return _buildList(context, snapshot.data!);
return ErrorDisplay(errorMessage: '${snapshot.error}'); } else {
} else if (snapshot.connectionState == ConnectionState.done) { return ErrorDisplay(errorMessage: 'Invalid future state');
return _buildList(context, snapshot.data!); }
} else { }),
return Center(child: CircularProgressIndicator()); ),
}
}),
);
}(),
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(

View File

@ -9,6 +9,7 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/client.dart'; import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/nav_layout.dart'; import 'package:simplecloudnotifier/nav_layout.dart';
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart'; import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
@ -50,6 +51,7 @@ void main() async {
Hive.registerAdapter(SCNMessageAdapter()); Hive.registerAdapter(SCNMessageAdapter());
Hive.registerAdapter(ChannelAdapter()); Hive.registerAdapter(ChannelAdapter());
Hive.registerAdapter(FBMessageAdapter()); Hive.registerAdapter(FBMessageAdapter());
Hive.registerAdapter(KeyTokenAdapter());
print('[INIT] Load Hive<scn-logs>...'); print('[INIT] Load Hive<scn-logs>...');
@ -106,6 +108,17 @@ void main() async {
ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace}); ApplicationLog.writeRawFailure('Failed to open Hive-Box: scn-fb-messages', {'error': exc.toString(), 'trace': trace});
} }
print('[INIT] Load Hive<scn-keytoken-value-cache>...');
try {
await Hive.openBox<KeyToken>('scn-keytoken-value-cache');
} catch (exc, trace) {
Hive.deleteBoxFromDisk('scn-keytoken-value-cache');
await Hive.openBox<KeyToken>('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...'); print('[INIT] Load AppAuth...');
final appAuth = AppAuth(); // ensure UserAccount is loaded final appAuth = AppAuth(); // ensure UserAccount is loaded

View File

@ -1,12 +1,27 @@
import 'package:hive_flutter/hive_flutter.dart';
part 'keytoken.g.dart';
@HiveType(typeId: 107)
class KeyToken { class KeyToken {
@HiveField(0)
final String keytokenID; final String keytokenID;
@HiveField(10)
final String name; final String name;
@HiveField(11)
final String timestampCreated; final String timestampCreated;
@HiveField(13)
final String? timestampLastUsed; final String? timestampLastUsed;
@HiveField(14)
final String ownerUserID; final String ownerUserID;
@HiveField(15)
final bool allChannels; final bool allChannels;
@HiveField(16)
final List<String> channels; final List<String> channels;
@HiveField(17)
final String permissions; final String permissions;
@HiveField(18)
final int messagesSent; final int messagesSent;
const KeyToken({ const KeyToken({

View File

@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'keytoken.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class KeyTokenAdapter extends TypeAdapter<KeyToken> {
@override
final int typeId = 107;
@override
KeyToken read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
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<String>(),
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;
}

View File

@ -35,14 +35,13 @@ class AccountRootPage extends StatefulWidget {
} }
class _AccountRootPageState extends State<AccountRootPage> { class _AccountRootPageState extends State<AccountRootPage> {
late ImmediateFuture<int>? futureSubscriptionCount; ImmediateFuture<int> _futureSubscriptionCount = ImmediateFuture.ofPending();
late ImmediateFuture<int>? futureClientCount; ImmediateFuture<int> _futureClientCount = ImmediateFuture.ofPending();
late ImmediateFuture<int>? futureKeyCount; ImmediateFuture<int> _futureKeyCount = ImmediateFuture.ofPending();
late ImmediateFuture<int>? futureChannelAllCount; ImmediateFuture<int> _futureChannelAllCount = ImmediateFuture.ofPending();
late ImmediateFuture<int>? futureChannelSubscribedCount; ImmediateFuture<int> _futureChannelOwnedCount = ImmediateFuture.ofPending();
late ImmediateFuture<int>? futureChannelOwnedCount; ImmediateFuture<int> _futureSenderNamesCount = ImmediateFuture.ofPending();
late ImmediateFuture<int>? futureSenderNamesCount; ImmediateFuture<User> _futureUser = ImmediateFuture.ofPending();
late ImmediateFuture<User>? futureUser;
late AppAuth userAcc; late AppAuth userAcc;
@ -92,58 +91,51 @@ class _AccountRootPageState extends State<AccountRootPage> {
} }
void _createFutures() { void _createFutures() {
futureSubscriptionCount = null; _futureSubscriptionCount = ImmediateFuture.ofPending();
futureClientCount = null; _futureClientCount = ImmediateFuture.ofPending();
futureKeyCount = null; _futureKeyCount = ImmediateFuture.ofPending();
futureChannelAllCount = null; _futureChannelAllCount = ImmediateFuture.ofPending();
futureChannelSubscribedCount = null; _futureChannelOwnedCount = ImmediateFuture.ofPending();
futureChannelOwnedCount = null; _futureSenderNamesCount = ImmediateFuture.ofPending();
futureSenderNamesCount = null;
if (userAcc.isAuth()) { if (userAcc.isAuth()) {
futureChannelAllCount = ImmediateFuture.ofFuture(() async { _futureChannelAllCount = ImmediateFuture.ofFuture(() async {
if (!userAcc.isAuth()) throw new Exception('not logged in'); if (!userAcc.isAuth()) throw new Exception('not logged in');
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all); final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all);
return channels.length; return channels.length;
}()); }());
futureChannelSubscribedCount = ImmediateFuture.ofFuture(() async { _futureChannelOwnedCount = 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 {
if (!userAcc.isAuth()) throw new Exception('not logged in'); if (!userAcc.isAuth()) throw new Exception('not logged in');
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.owned); final channels = await APIClient.getChannelList(userAcc, ChannelSelector.owned);
return channels.length; return channels.length;
}()); }());
futureSubscriptionCount = ImmediateFuture.ofFuture(() async { _futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
if (!userAcc.isAuth()) throw new Exception('not logged in'); if (!userAcc.isAuth()) throw new Exception('not logged in');
final subs = await APIClient.getSubscriptionList(userAcc); final subs = await APIClient.getSubscriptionList(userAcc);
return subs.length; return subs.length;
}()); }());
futureClientCount = ImmediateFuture.ofFuture(() async { _futureClientCount = ImmediateFuture.ofFuture(() async {
if (!userAcc.isAuth()) throw new Exception('not logged in'); if (!userAcc.isAuth()) throw new Exception('not logged in');
final clients = await APIClient.getClientList(userAcc); final clients = await APIClient.getClientList(userAcc);
return clients.length; return clients.length;
}()); }());
futureKeyCount = ImmediateFuture.ofFuture(() async { _futureKeyCount = ImmediateFuture.ofFuture(() async {
if (!userAcc.isAuth()) throw new Exception('not logged in'); if (!userAcc.isAuth()) throw new Exception('not logged in');
final keys = await APIClient.getKeyTokenList(userAcc); final keys = await APIClient.getKeyTokenList(userAcc);
return keys.length; return keys.length;
}()); }());
futureSenderNamesCount = ImmediateFuture.ofFuture(() async { _futureSenderNamesCount = ImmediateFuture.ofFuture(() async {
if (!userAcc.isAuth()) throw new Exception('not logged in'); if (!userAcc.isAuth()) throw new Exception('not logged in');
final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList(); final senders = (await APIClient.getSenderNameList(userAcc)).map((p) => p.name).toList();
return senders.length; 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<AccountRootPage> {
// refresh all data and then replace teh futures used in build() // refresh all data and then replace teh futures used in build()
final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all); final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all);
final channelsSubscribed = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
final subs = await APIClient.getSubscriptionList(userAcc); final subs = await APIClient.getSubscriptionList(userAcc);
final clients = await APIClient.getClientList(userAcc); final clients = await APIClient.getClientList(userAcc);
final keys = await APIClient.getKeyTokenList(userAcc); final keys = await APIClient.getKeyTokenList(userAcc);
@ -165,13 +156,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
final user = await userAcc.loadUser(force: true); final user = await userAcc.loadUser(force: true);
setState(() { setState(() {
futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length); _futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length);
futureChannelSubscribedCount = ImmediateFuture.ofValue(channelsSubscribed.length); _futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
futureSubscriptionCount = ImmediateFuture.ofValue(subs.length); _futureClientCount = ImmediateFuture.ofValue(clients.length);
futureClientCount = ImmediateFuture.ofValue(clients.length); _futureKeyCount = ImmediateFuture.ofValue(keys.length);
futureKeyCount = ImmediateFuture.ofValue(keys.length); _futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length);
futureSenderNamesCount = ImmediateFuture.ofValue(senderNames.length); _futureUser = ImmediateFuture.ofValue(user);
futureUser = ImmediateFuture.ofValue(user);
}); });
} catch (exc, trace) { } catch (exc, trace) {
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
@ -192,10 +182,10 @@ class _AccountRootPageState extends State<AccountRootPage> {
return _buildNoAuth(context); return _buildNoAuth(context);
} else { } else {
return FutureBuilder( return FutureBuilder(
future: futureUser!.future, future: _futureUser.future,
builder: ((context, snapshot) { builder: ((context, snapshot) {
if (futureUser?.value != null) { if (_futureUser.value != null) {
return _buildShowAccount(context, acc, futureUser!.value!); return _buildShowAccount(context, acc, _futureUser.value!);
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) { } else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
return ErrorDisplay(errorMessage: '${snapshot.error}'); return ErrorDisplay(errorMessage: '${snapshot.error}');
} else if (snapshot.connectionState == ConnectionState.done) { } else if (snapshot.connectionState == ConnectionState.done) {
@ -354,10 +344,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
children: [ children: [
SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))), SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))),
FutureBuilder( FutureBuilder(
future: futureChannelOwnedCount!.future, future: _futureChannelOwnedCount.future,
builder: (context, snapshot) { builder: (context, snapshot) {
if (futureChannelOwnedCount?.value != null) { if (_futureChannelOwnedCount.value != null) {
return Text('${futureChannelOwnedCount!.value}'); 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) { } else if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}'); return Text('${snapshot.data}');
} else { } else {
@ -393,11 +385,11 @@ class _AccountRootPageState extends State<AccountRootPage> {
List<Widget> _buildCards(BuildContext context, User user) { List<Widget> _buildCards(BuildContext context, User user) {
return [ return [
_buildNumberCard(context, 'Subscription', 's', futureSubscriptionCount, () => Navi.push(context, () => SubscriptionListPage())), _buildNumberCard(context, 'Subscription', 's', _futureSubscriptionCount, () => Navi.push(context, () => SubscriptionListPage())),
_buildNumberCard(context, 'Client', 's', futureClientCount, () => Navi.push(context, () => ClientListPage())), _buildNumberCard(context, 'Client', 's', _futureClientCount, () => Navi.push(context, () => ClientListPage())),
_buildNumberCard(context, 'Key', 's', futureKeyCount, () => Navi.push(context, () => KeyTokenListPage())), _buildNumberCard(context, 'Key', 's', _futureKeyCount, () => Navi.push(context, () => KeyTokenListPage())),
_buildNumberCard(context, 'Channel', 's', futureChannelAllCount, () => Navi.push(context, () => ChannelListExtendedPage())), _buildNumberCard(context, 'Channel', 's', _futureChannelAllCount, () => Navi.push(context, () => ChannelListExtendedPage())),
_buildNumberCard(context, 'Sender', '', futureSenderNamesCount, () => Navi.push(context, () => SenderListPage())), _buildNumberCard(context, 'Sender', '', _futureSenderNamesCount, () => Navi.push(context, () => SenderListPage())),
UI.buttonCard( UI.buttonCard(
context: context, context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
@ -415,17 +407,19 @@ class _AccountRootPageState extends State<AccountRootPage> {
]; ];
} }
Widget _buildNumberCard(BuildContext context, String txt, String pluralSuffix, ImmediateFuture<int>? future, void Function() action) { Widget _buildNumberCard(BuildContext context, String txt, String pluralSuffix, ImmediateFuture<int> future, void Function() action) {
return UI.buttonCard( return UI.buttonCard(
context: context, context: context,
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
child: Row( child: Row(
children: [ children: [
FutureBuilder( FutureBuilder(
future: future?.future, future: future.future,
builder: (context, snapshot) { builder: (context, snapshot) {
if (future?.value != null) { if (future.value != null) {
return Text('${future!.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); 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) { } else if (snapshot.connectionState == ConnectionState.done) {
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
} else { } else {
@ -435,10 +429,12 @@ class _AccountRootPageState extends State<AccountRootPage> {
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
FutureBuilder( FutureBuilder(
future: future?.future, future: future.future,
builder: (context, snapshot) { builder: (context, snapshot) {
if (future?.value != null) { if (future.value != null) {
return Text('${txt}${((future!.value != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); 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) { } else if (snapshot.connectionState == ConnectionState.done) {
return Text('${txt}${((snapshot.data != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)); return Text('${txt}${((snapshot.data != 1) ? pluralSuffix : '')}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
} else { } else {
@ -562,7 +558,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
try { try {
final user = await APIClient.updateUser(acc, acc.userID!, username: newusername); final user = await APIClient.updateUser(acc, acc.userID!, username: newusername);
setState(() { setState(() {
futureUser = ImmediateFuture.ofValue(user); _futureUser = ImmediateFuture.ofValue(user);
}); });
Toaster.success("Success", 'Username changed'); Toaster.success("Success", 'Username changed');

View File

@ -44,9 +44,9 @@ enum EditState { none, editing, saving }
enum ChannelViewPageInitState { loading, okay, error } enum ChannelViewPageInitState { loading, okay, error }
class _ChannelViewPageState extends State<ChannelViewPage> { class _ChannelViewPageState extends State<ChannelViewPage> {
late ImmediateFuture<String?> _futureSubscribeKey; ImmediateFuture<String?> _futureSubscribeKey = ImmediateFuture.ofPending();
late ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions; ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions = ImmediateFuture.ofPending();
late ImmediateFuture<UserPreview> _futureOwner; ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
final TextEditingController _ctrlDisplayName = TextEditingController(); final TextEditingController _ctrlDisplayName = TextEditingController();
final TextEditingController _ctrlDescriptionName = TextEditingController(); final TextEditingController _ctrlDescriptionName = TextEditingController();

View File

@ -32,24 +32,7 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ 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)],
...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),
],
), ),
), ),
), ),
@ -130,4 +113,18 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
), ),
]; ];
} }
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] =================');
}
} }

View File

@ -47,10 +47,6 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
textColor: Theme.of(context).colorScheme.onErrorContainer, textColor: Theme.of(context).colorScheme.onErrorContainer,
title: Row( title: Row(
children: [ children: [
SizedBox(
width: 120,
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
),
Expanded( Expanded(
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
), ),
@ -61,7 +57,14 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( Text(
req.error, req.error,
maxLines: 1, maxLines: 1,
@ -81,10 +84,6 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
child: ListTile( child: ListTile(
title: Row( title: Row(
children: [ children: [
SizedBox(
width: 120,
child: Text(_dateFormat.format(req.timestampStart), style: TextStyle(fontSize: 12)),
),
Expanded( Expanded(
child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)), child: Text(req.name, style: TextStyle(fontWeight: FontWeight.bold)),
), ),
@ -92,7 +91,13 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
Text('${req.timestampEnd.difference(req.timestampStart).inMilliseconds}ms', style: TextStyle(fontSize: 12)), 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),
],
),
), ),
), ),
); );

View File

@ -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<KeyTokenCreateDialog> {
TextEditingController _ctrlName = TextEditingController();
Set<String> selectedPermissions = {'CS'};
ImmediateFuture<List<Channel>> _futureOwnedChannels = ImmediateFuture.ofPending();
bool allChannels = true;
Set<String> selectedChannels = new Set<String>();
@override
void initState() {
super.initState();
final userAcc = Provider.of<AppAuth>(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: <Widget>[
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<List<Channel>>(
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<AppAuth>(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 ? <String>[] : 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()}');
}
}
}

View File

@ -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: <Widget>[
TextButton(
child: const Text('Close'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
List<String> _formatPermissions(String v) {
var splt = v.split(';');
if (splt.length == 0) return ["None"];
List<String> 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] =================');
}
}

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; 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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/keytoken.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/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart'; import 'package:simplecloudnotifier/pages/keytoken_list/keytoken_list_item.dart';
@ -81,6 +84,27 @@ class _KeyTokenListPageState extends State<KeyTokenListPage> {
), ),
), ),
), ),
floatingActionButton: FloatingActionButton(
heroTag: 'fab_keytokenlist_plus',
onPressed: () {
showDialog<void>(
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<void>(
context: context,
builder: (context) => KeyTokenCreatedModal(keytoken: token, tokenValue: tokValue),
); );
} }
} }

View File

@ -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<Channel> ownedChannels;
final KeyTokenPreview keytoken;
final void Function(Set<String>) 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<EditKeyTokenChannelsDialog> {
late bool allChannels;
late Set<String> 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: <Widget>[
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();
},
),
],
);
}
}

View File

@ -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<EditKeyTokenPermissionsDialog> {
Set<String> selectedPermissions = new Set<String>();
@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: <Widget>[
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();
},
),
],
);
}
}

View File

@ -1,16 +1,23 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart'; import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.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_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.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/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/dialogs.dart';
import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart'; import 'package:simplecloudnotifier/utils/ui.dart';
@ -40,7 +47,11 @@ enum KeyTokenViewPageInitState { loading, okay, error }
class _KeyTokenViewPageState extends State<KeyTokenViewPage> { class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting 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(); final TextEditingController _ctrlName = TextEditingController();
@ -55,6 +66,9 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading; KeyTokenViewPageInitState loadingState = KeyTokenViewPageInitState.loading;
String errorMessage = ''; String errorMessage = '';
KeyToken? keytokenUserAccAdmin;
KeyToken? keytokenUserAccSend;
@override @override
void initState() { void initState() {
_initStateAsync(true); _initStateAsync(true);
@ -102,14 +116,48 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
if (this.keytokenPreview!.ownerUserID == userAcc.userID) { if (this.keytokenPreview!.ownerUserID == userAcc.userID) {
var cacheUser = userAcc.getUserOrNull(); var cacheUser = userAcc.getUserOrNull();
if (cacheUser != null) { if (cacheUser != null) {
_futureOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview()); _futureOwner = ImmediateFuture.ofValue(cacheUser.toPreview());
} else { } else {
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(_getOwner(userAcc)); _futureOwner = ImmediateFuture.ofFuture(_getOwner(userAcc));
} }
} else { } 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 @override
@ -158,18 +206,22 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
context: context, context: context,
icon: FontAwesomeIcons.solidIdCardClip, icon: FontAwesomeIcons.solidIdCardClip,
title: 'KeyTokenID', 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), _buildNameCard(context, true),
UI.metaCard( UI.metaCard(
context: context, context: context,
icon: FontAwesomeIcons.clock, icon: FontAwesomeIcons.solidClock,
title: 'Created', title: 'Created',
values: [_KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())], values: [_KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())],
), ),
UI.metaCard( UI.metaCard(
context: context, context: context,
icon: FontAwesomeIcons.clockTwo, icon: FontAwesomeIcons.solidClockTwo,
title: 'Last Used', title: 'Last Used',
values: [(keytoken.timestampLastUsed == null) ? 'Never' : _KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())], values: [(keytoken.timestampLastUsed == null) ? 'Never' : _KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())],
), ),
@ -297,7 +349,6 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
setState(() { setState(() {
_ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? ''; _ctrlName.text = _nameOverride ?? keytokenPreview?.name ?? '';
_editName = EditState.editing; _editName = EditState.editing;
if (_editName == EditState.editing) _editName = EditState.none;
}); });
} }
@ -355,7 +406,7 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
if (isOwned) { if (isOwned) {
w1 = UI.metaCard( w1 = UI.metaCard(
context: context, context: context,
icon: FontAwesomeIcons.shieldKeyhole, icon: FontAwesomeIcons.solidShieldKeyhole,
title: 'Permissions', title: 'Permissions',
values: _formatPermissions(keyToken.permissions), values: _formatPermissions(keyToken.permissions),
iconActions: [(FontAwesomeIcons.penToSquare, null, _editPermissions)], iconActions: [(FontAwesomeIcons.penToSquare, null, _editPermissions)],
@ -363,28 +414,53 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
} else { } else {
w1 = UI.metaCard( w1 = UI.metaCard(
context: context, context: context,
icon: FontAwesomeIcons.shieldKeyhole, icon: FontAwesomeIcons.solidShieldKeyhole,
title: 'Permissions', title: 'Permissions',
values: _formatPermissions(keyToken.permissions), values: _formatPermissions(keyToken.permissions),
); );
} }
if (isOwned) { w2 = FutureBuilder(
w2 = UI.metaCard( future: _futureAllChannels.future,
context: context, builder: (context, snapshot) {
icon: FontAwesomeIcons.solidSnake, if (snapshot.hasData) {
title: 'Channels', var cmap = snapshot.data!;
values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, //TODO show channel names if (isOwned) {
iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)], return UI.metaCard(
); context: context,
} else { icon: FontAwesomeIcons.solidSnake,
w2 = UI.metaCard( title: 'Channels',
context: context, values: (keyToken.allChannels) ? (['All Channels']) : (keyToken.channels.isEmpty ? ['(None)'] : (keyToken.channels.map((c) => cmap[c]?.displayName ?? c).toList())),
icon: FontAwesomeIcons.solidSnake, iconActions: [(FontAwesomeIcons.penToSquare, null, _editChannels)],
title: 'Channels', );
values: (keyToken.allChannels) ? ['All Channels'] : keyToken.channels, //TODO show channel names } 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]; return [w1, w2];
} }
@ -404,33 +480,138 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
return result; return result;
} }
void _editPermissions() { void _editPermissions() async {
final acc = Provider.of<AppAuth>(context, listen: false); 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 await showDialog<void>(
context: context,
Toaster.info("Not implemented", "Currently not implemented"); builder: (context) => EditKeyTokenPermissionsDialog(
keytoken: keytokenPreview!,
onUpdatePermissions: _updatePermissions,
),
);
} }
void _editChannels() { void _editChannels() async {
final acc = Provider.of<AppAuth>(context, listen: false); 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); 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');
}
} }
} }

View File

@ -12,6 +12,7 @@ import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/channel_view/channel_view.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/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_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/navi.dart';
@ -35,6 +36,7 @@ class MessageViewPage extends StatefulWidget {
class _MessageViewPageState extends State<MessageViewPage> { class _MessageViewPageState extends State<MessageViewPage> {
late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture; late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null; (SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
final ScrollController _controller = ScrollController(); final ScrollController _controller = ScrollController();
@ -164,8 +166,12 @@ class _MessageViewPageState extends State<MessageViewPage> {
icon: FontAwesomeIcons.solidGearCode, icon: FontAwesomeIcons.solidGearCode,
title: 'KeyToken', title: 'KeyToken',
values: [message.usedKeyID, token?.name ?? '...'], values: [message.usedKeyID, token?.name ?? '...'],
mainAction: () => { mainAction: () {
Navi.push(context, () => FilteredMessageViewPage(title: token?.name ?? message.usedKeyID, filter: MessageFilter(usedKeys: [message.usedKeyID]))) 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( UI.metaCard(
@ -210,7 +216,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
), ),
); );
var showScrollbar = true; var showScrollbar = false;
if (!_monospaceMode && (message.content ?? '').length > 4096) showScrollbar = true; if (!_monospaceMode && (message.content ?? '').length > 4096) showScrollbar = true;
if (_monospaceMode && (message.content ?? '').split('\n').length > 64) showScrollbar = true; if (_monospaceMode && (message.content ?? '').split('\n').length > 64) showScrollbar = true;

View File

@ -42,9 +42,9 @@ enum SubscriptionViewPageInitState { loading, okay, error }
class _SubscriptionViewPageState extends State<SubscriptionViewPage> { class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
late ImmediateFuture<UserPreview> _futureChannelOwner; ImmediateFuture<UserPreview> _futureChannelOwner = ImmediateFuture.ofPending();
late ImmediateFuture<UserPreview> _futureSubscriber; ImmediateFuture<UserPreview> _futureSubscriber = ImmediateFuture.ofPending();
late ImmediateFuture<ChannelPreview> _futureChannel; ImmediateFuture<ChannelPreview> _futureChannel = ImmediateFuture.ofPending();
int _loadingIndeterminateCounter = 0; int _loadingIndeterminateCounter = 0;

View File

@ -1,5 +1,7 @@
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart'; import 'package:simplecloudnotifier/settings/app_settings.dart';
@ -57,4 +59,21 @@ class SCNDataCache {
return cacheMessages; return cacheMessages;
} }
Future<KeyToken> getOrQueryTokenByValue(String uid, String tokVal) async {
final cache = Hive.box<KeyToken>('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;
}
} }

View File

@ -2,6 +2,8 @@
// Unfortunately Future.value(x) in FutureBuilder always results in one frame were snapshot.connectionState is waiting // 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. // This way we can set the ImmediateFuture.value directly and circumvent that.
import 'dart:async';
class ImmediateFuture<T> { class ImmediateFuture<T> {
final Future<T> future; final Future<T> future;
final T? value; final T? value;
@ -20,6 +22,10 @@ class ImmediateFuture<T> {
: future = Future.value(v), : future = Future.value(v),
value = v; value = v;
ImmediateFuture.ofPending()
: future = Completer<T>().future,
value = null;
T? get() { T? get() {
return value ?? _futureValue; return value ?? _futureValue;
} }

View File

@ -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) return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
} }
if err != nil { 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")) 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) return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
} }
if err != nil { 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)) 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) return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
} }
if err != nil { 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 { if b.Name != nil {
@ -372,12 +372,12 @@ func (h APIHandler) DeleteUserKey(pctx ginext.PreContext) ginext.HTTPResponse {
return *permResp 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) { if errors.Is(err, sql.ErrNoRows) {
return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err) return ginresp.APIError(g, 404, apierr.KEY_NOT_FOUND, "Key not found", err)
} }
if err != nil { 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() { 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) err = h.database.DeleteKeyToken(ctx, u.KeyID)
if err != nil { 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))
}) })
} }