Finish KeyToken operations

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

View File

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