Finish KeyToken operations
This commit is contained in:
parent
1f0f280286
commit
78c895547e
@ -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 {
|
||||
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,
|
||||
},
|
||||
|
51
flutter/lib/components/badge_display/badge_display.dart
Normal file
51
flutter/lib/components/badge_display/badge_display.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -17,13 +17,12 @@ class FilterModalChannel extends StatefulWidget {
|
||||
class _FilterModalChannelState extends State<FilterModalChannel> {
|
||||
Set<String> _selectedEntries = {};
|
||||
|
||||
late ImmediateFuture<List<Channel>>? _futureChannels;
|
||||
ImmediateFuture<List<Channel>> _futureChannels = ImmediateFuture.ofPending();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_futureChannels = null;
|
||||
_futureChannels = ImmediateFuture.ofFuture(() async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
@ -51,45 +50,39 @@ class _FilterModalChannelState extends State<FilterModalChannel> {
|
||||
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: <Widget>[
|
||||
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,
|
||||
))
|
||||
|
@ -17,13 +17,12 @@ class FilterModalKeytoken extends StatefulWidget {
|
||||
class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
||||
Set<String> _selectedEntries = {};
|
||||
|
||||
late ImmediateFuture<List<KeyToken>>? _futureKeyTokens;
|
||||
ImmediateFuture<List<KeyToken>> _futureKeyTokens = ImmediateFuture.ofPending();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_futureKeyTokens = null;
|
||||
_futureKeyTokens = ImmediateFuture.ofFuture(() async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
@ -51,26 +50,22 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
||||
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: <Widget>[
|
||||
TextButton(
|
||||
@ -89,7 +84,7 @@ class _FilterModalKeytokenState extends State<FilterModalKeytoken> {
|
||||
|
||||
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,
|
||||
))
|
||||
|
@ -15,13 +15,12 @@ class FilterModalSendername extends StatefulWidget {
|
||||
class _FilterModalSendernameState extends State<FilterModalSendername> {
|
||||
Set<String> _selectedEntries = {};
|
||||
|
||||
late ImmediateFuture<List<String>>? _futureSenders;
|
||||
ImmediateFuture<List<String>> _futureSenders = ImmediateFuture.ofPending();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_futureSenders = null;
|
||||
_futureSenders = ImmediateFuture.ofFuture(() async {
|
||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
@ -49,26 +48,22 @@ class _FilterModalSendernameState extends State<FilterModalSendername> {
|
||||
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: <Widget>[
|
||||
TextButton(
|
||||
|
@ -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<scn-logs>...');
|
||||
|
||||
@ -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<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...');
|
||||
|
||||
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
||||
|
@ -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<String> channels;
|
||||
@HiveField(17)
|
||||
final String permissions;
|
||||
@HiveField(18)
|
||||
final int messagesSent;
|
||||
|
||||
const KeyToken({
|
||||
|
65
flutter/lib/models/keytoken.g.dart
Normal file
65
flutter/lib/models/keytoken.g.dart
Normal 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;
|
||||
}
|
@ -35,14 +35,13 @@ class AccountRootPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AccountRootPageState extends State<AccountRootPage> {
|
||||
late ImmediateFuture<int>? futureSubscriptionCount;
|
||||
late ImmediateFuture<int>? futureClientCount;
|
||||
late ImmediateFuture<int>? futureKeyCount;
|
||||
late ImmediateFuture<int>? futureChannelAllCount;
|
||||
late ImmediateFuture<int>? futureChannelSubscribedCount;
|
||||
late ImmediateFuture<int>? futureChannelOwnedCount;
|
||||
late ImmediateFuture<int>? futureSenderNamesCount;
|
||||
late ImmediateFuture<User>? futureUser;
|
||||
ImmediateFuture<int> _futureSubscriptionCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureClientCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureKeyCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureChannelAllCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureChannelOwnedCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<int> _futureSenderNamesCount = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<User> _futureUser = ImmediateFuture.ofPending();
|
||||
|
||||
late AppAuth userAcc;
|
||||
|
||||
@ -92,58 +91,51 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
}
|
||||
|
||||
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<AccountRootPage> {
|
||||
// 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<AccountRootPage> {
|
||||
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<AccountRootPage> {
|
||||
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<AccountRootPage> {
|
||||
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<AccountRootPage> {
|
||||
|
||||
List<Widget> _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<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(
|
||||
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<AccountRootPage> {
|
||||
),
|
||||
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<AccountRootPage> {
|
||||
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');
|
||||
|
||||
|
@ -44,9 +44,9 @@ enum EditState { none, editing, saving }
|
||||
enum ChannelViewPageInitState { loading, okay, error }
|
||||
|
||||
class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
late ImmediateFuture<String?> _futureSubscribeKey;
|
||||
late ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions;
|
||||
late ImmediateFuture<UserPreview> _futureOwner;
|
||||
ImmediateFuture<String?> _futureSubscribeKey = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<List<(Subscription, UserPreview?)>> _futureSubscriptions = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
|
||||
|
||||
final TextEditingController _ctrlDisplayName = TextEditingController();
|
||||
final TextEditingController _ctrlDescriptionName = TextEditingController();
|
||||
|
@ -32,24 +32,7 @@ class _DebugRequestViewPageState extends State<DebugRequestViewPage> {
|
||||
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<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] =================');
|
||||
}
|
||||
}
|
||||
|
@ -47,10 +47,6 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
||||
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<DebugRequestsPage> {
|
||||
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<DebugRequestsPage> {
|
||||
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<DebugRequestsPage> {
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
216
flutter/lib/pages/keytoken_list/keytoken_create_modal.dart
Normal file
216
flutter/lib/pages/keytoken_list/keytoken_create_modal.dart
Normal 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()}');
|
||||
}
|
||||
}
|
||||
}
|
97
flutter/lib/pages/keytoken_list/keytoken_created_modal.dart
Normal file
97
flutter/lib/pages/keytoken_list/keytoken_created_modal.dart
Normal 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] =================');
|
||||
}
|
||||
}
|
@ -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<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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
112
flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart
Normal file
112
flutter/lib/pages/keytoken_view/keytoken_channel_modal.dart
Normal 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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<MessageViewPage> {
|
||||
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<MessageViewPage> {
|
||||
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<MessageViewPage> {
|
||||
),
|
||||
);
|
||||
|
||||
var showScrollbar = true;
|
||||
var showScrollbar = false;
|
||||
if (!_monospaceMode && (message.content ?? '').length > 4096) showScrollbar = true;
|
||||
if (_monospaceMode && (message.content ?? '').split('\n').length > 64) showScrollbar = true;
|
||||
|
||||
|
@ -42,9 +42,9 @@ enum SubscriptionViewPageInitState { loading, okay, error }
|
||||
class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
|
||||
|
||||
late ImmediateFuture<UserPreview> _futureChannelOwner;
|
||||
late ImmediateFuture<UserPreview> _futureSubscriber;
|
||||
late ImmediateFuture<ChannelPreview> _futureChannel;
|
||||
ImmediateFuture<UserPreview> _futureChannelOwner = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<UserPreview> _futureSubscriber = ImmediateFuture.ofPending();
|
||||
ImmediateFuture<ChannelPreview> _futureChannel = ImmediateFuture.ofPending();
|
||||
|
||||
int _loadingIndeterminateCounter = 0;
|
||||
|
||||
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
|
@ -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<T> {
|
||||
final Future<T> future;
|
||||
final T? value;
|
||||
@ -20,6 +22,10 @@ class ImmediateFuture<T> {
|
||||
: future = Future.value(v),
|
||||
value = v;
|
||||
|
||||
ImmediateFuture.ofPending()
|
||||
: future = Completer<T>().future,
|
||||
value = null;
|
||||
|
||||
T? get() {
|
||||
return value ?? _futureValue;
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user