From 981b9865cd0e79adcb44e521551db52f513777ec Mon Sep 17 00:00:00 2001 From: inexcode Date: Thu, 8 Sep 2022 18:13:18 +0300 Subject: [PATCH] Fix users not changing SSH keys and remove SSH keys screen --- lib/logic/cubit/users/users_cubit.dart | 14 +- lib/ui/pages/more/more.dart | 7 +- lib/ui/pages/ssh_keys/new_ssh_key.dart | 76 ---- lib/ui/pages/ssh_keys/ssh_keys.dart | 144 -------- lib/ui/pages/users/user.dart | 2 +- lib/ui/pages/users/user_details.dart | 465 ++++++++++++++++--------- lib/ui/pages/users/users.dart | 2 +- 7 files changed, 315 insertions(+), 395 deletions(-) delete mode 100644 lib/ui/pages/ssh_keys/new_ssh_key.dart delete mode 100644 lib/ui/pages/ssh_keys/ssh_keys.dart diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 1ff0e9a3..87abcffd 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -47,8 +47,6 @@ class UsersCubit extends ServerInstallationDependendCubit { return; } emit(state.copyWith(isLoading: true)); - // sleep for 10 seconds to simulate a slow connection - await Future.delayed(const Duration(seconds: 10)); final List usersFromServer = await api.getAllUsers(); if (usersFromServer.isNotEmpty) { emit( @@ -58,8 +56,8 @@ class UsersCubit extends ServerInstallationDependendCubit { ), ); // Update the users it the box - box.clear(); - box.addAll(usersFromServer); + await box.clear(); + await box.addAll(usersFromServer); } else { getIt() .showSnackBar('users.could_not_fetch_users'.tr()); @@ -139,7 +137,9 @@ class UsersCubit extends ServerInstallationDependendCubit { await api.addSshKey(user.login, publicKey); if (result.success) { final User updatedUser = result.user!; - await box.putAt(box.values.toList().indexOf(user), updatedUser); + final int index = + state.users.indexWhere((final User u) => u.login == user.login); + await box.putAt(index, updatedUser); emit( state.copyWith( users: box.values.toList(), @@ -156,7 +156,9 @@ class UsersCubit extends ServerInstallationDependendCubit { await api.removeSshKey(user.login, publicKey); if (result.success) { final User updatedUser = result.user!; - await box.putAt(box.values.toList().indexOf(user), updatedUser); + final int index = + state.users.indexWhere((final User u) => u.login == user.login); + await box.putAt(index, updatedUser); emit( state.copyWith( users: box.values.toList(), diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 5a02da3c..fce3f640 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -11,10 +11,9 @@ import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart'; import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; import 'package:selfprivacy/ui/pages/root_route.dart'; -import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; +import 'package:selfprivacy/ui/pages/users/users.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/ui/pages/more/about/about.dart'; import 'package:selfprivacy/ui/pages/more/app_settings/app_setting.dart'; import 'package:selfprivacy/ui/pages/more/console/console.dart'; @@ -53,8 +52,8 @@ class MorePage extends StatelessWidget { _MoreMenuItem( title: 'more.create_ssh_key'.tr(), iconData: Ionicons.key_outline, - goTo: SshKeysPage( - user: context.read().state.rootUser, + goTo: const UserDetails( + login: 'root', ), ), if (isReady) diff --git a/lib/ui/pages/ssh_keys/new_ssh_key.dart b/lib/ui/pages/ssh_keys/new_ssh_key.dart deleted file mode 100644 index 042707ef..00000000 --- a/lib/ui/pages/ssh_keys/new_ssh_key.dart +++ /dev/null @@ -1,76 +0,0 @@ -part of 'ssh_keys.dart'; - -class NewSshKey extends StatelessWidget { - const NewSshKey(this.user, {final super.key}); - final User user; - - @override - Widget build(final BuildContext context) => BrandBottomSheet( - child: BlocProvider( - create: (final context) { - final jobCubit = context.read(); - final jobState = jobCubit.state; - if (jobState is JobsStateWithJobs) { - final jobs = jobState.clientJobList; - for (final job in jobs) { - if (job is CreateSSHKeyJob && job.user.login == user.login) { - user.sshKeys.add(job.publicKey); - } - } - } - return SshFormCubit( - jobsCubit: jobCubit, - user: user, - ); - }, - child: Builder( - builder: (final context) { - final formCubitState = context.watch().state; - - return BlocListener( - listener: (final context, final state) { - if (state.isSubmitted) { - Navigator.pop(context); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - BrandHeader( - title: user.login, - ), - const SizedBox(width: 14), - Padding( - padding: paddingH15V0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IntrinsicHeight( - child: CubitFormTextField( - formFieldCubit: context.read().key, - decoration: InputDecoration( - labelText: 'ssh.input_label'.tr(), - ), - ), - ), - const SizedBox(height: 30), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => - context.read().trySubmit(), - text: 'ssh.create'.tr(), - ), - const SizedBox(height: 30), - ], - ), - ), - ], - ), - ); - }, - ), - ), - ); -} diff --git a/lib/ui/pages/ssh_keys/ssh_keys.dart b/lib/ui/pages/ssh_keys/ssh_keys.dart deleted file mode 100644 index 4bc48e8a..00000000 --- a/lib/ui/pages/ssh_keys/ssh_keys.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:cubit_form/cubit_form.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:selfprivacy/logic/cubit/forms/user/ssh_form_cubit.dart'; -import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; -import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; -import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; - -import 'package:selfprivacy/config/brand_colors.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart'; -import 'package:selfprivacy/logic/models/hive/user.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; - -part 'new_ssh_key.dart'; - -// Get user object as a parameter -class SshKeysPage extends StatefulWidget { - const SshKeysPage({required this.user, final super.key}); - final User user; - - @override - State createState() => _SshKeysPageState(); -} - -class _SshKeysPageState extends State { - @override - Widget build(final BuildContext context) => BrandHeroScreen( - heroTitle: 'ssh.title'.tr(), - heroSubtitle: widget.user.login, - heroIcon: BrandIcons.key, - children: [ - if (widget.user.login == 'root') - Column( - children: [ - // Show alert card if user is root - BrandCards.outlined( - child: ListTile( - leading: Icon( - Icons.warning_rounded, - color: Theme.of(context).colorScheme.error, - ), - title: Text('ssh.root.title'.tr()), - subtitle: Text('ssh.root.subtitle'.tr()), - ), - ) - ], - ), - BrandCards.outlined( - child: Column( - children: [ - ListTile( - title: Text( - 'ssh.create'.tr(), - style: Theme.of(context).textTheme.headline6, - ), - leading: const Icon(Icons.add_circle_outline_rounded), - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => Padding( - padding: MediaQuery.of(context).viewInsets, - child: NewSshKey(widget.user), - ), - ); - }, - ), - const Divider(height: 0), - // show a list of ListTiles with ssh keys - // Clicking on one should delete it - Column( - children: widget.user.sshKeys.map((final String key) { - final publicKey = - key.split(' ').length > 1 ? key.split(' ')[1] : key; - final keyType = key.split(' ')[0]; - final keyName = key.split(' ').length > 2 - ? key.split(' ')[2] - : 'ssh.no_key_name'.tr(); - return ListTile( - title: Text('$keyName ($keyType)'), - // do not overflow text - subtitle: Text( - publicKey, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - onTap: () { - showDialog( - context: context, - builder: (final BuildContext context) => AlertDialog( - title: Text('ssh.delete'.tr()), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text('ssh.delete_confirm_question'.tr()), - Text('$keyName ($keyType)'), - Text(publicKey), - ], - ), - ), - actions: [ - TextButton( - child: Text('basis.cancel'.tr()), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text( - 'basis.delete'.tr(), - style: const TextStyle( - color: BrandColors.red1, - ), - ), - onPressed: () { - context.read().addJob( - DeleteSSHKeyJob( - user: widget.user, - publicKey: key, - ), - ); - Navigator.of(context) - ..pop() - ..pop(); - }, - ), - ], - ), - ); - }, - ); - }).toList(), - ) - ], - ), - ), - ], - ); -} diff --git a/lib/ui/pages/users/user.dart b/lib/ui/pages/users/user.dart index 1602427d..1d781811 100644 --- a/lib/ui/pages/users/user.dart +++ b/lib/ui/pages/users/user.dart @@ -12,7 +12,7 @@ class _User extends StatelessWidget { Widget build(final BuildContext context) => InkWell( onTap: () { Navigator.of(context).push( - materialRoute(_UserDetails(user: user, isRootUser: isRootUser)), + materialRoute(UserDetails(login: user.login)), ); }, child: Container( diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index 0bfbce8c..dfa08dc6 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -1,13 +1,12 @@ part of 'users.dart'; -class _UserDetails extends StatelessWidget { - const _UserDetails({ - required this.user, - required this.isRootUser, +class UserDetails extends StatelessWidget { + const UserDetails({ + required this.login, + final super.key, }); - final User user; - final bool isRootUser; + final String login; @override Widget build(final BuildContext context) { final ServerInstallationState config = @@ -15,118 +14,46 @@ class _UserDetails extends StatelessWidget { final String domainName = UiHelpers.getDomainName(config); + final User user = context.watch().state.users.firstWhere( + (final User user) => user.login == login, + orElse: () => const User( + type: UserType.normal, + login: 'error', + ), + ); + + if (user.type == UserType.root) { + return BrandHeroScreen( + hasBackButton: true, + heroTitle: user.login, + heroSubtitle: 'ssh.root.title'.tr(), + children: [ + _SshKeysCard(user: user), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.warning_amber_outlined, size: 24), + const SizedBox(height: 16), + Text( + 'ssh.root.subtitle'.tr(), + ), + ], + ), + ), + ], + ); + } + return BrandHeroScreen( hasBackButton: true, heroTitle: user.login, children: [ - BrandCards.filled( - child: Column( - children: [ - ListTile( - title: Text('${user.login}@$domainName'), - subtitle: Text('users.email_login'.tr()), - textColor: Theme.of(context).colorScheme.onSurfaceVariant, - leading: const Icon(Icons.alternate_email_outlined), - iconColor: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ], - ), - ), + _UserLogins(user: user, domainName: domainName), const SizedBox(height: 8), - BrandCards.filled( - child: Column( - children: [ - ListTile( - title: Text('ssh.title'.tr()), - textColor: Theme.of(context).colorScheme.onSurfaceVariant, - ), - const Divider(height: 0), - ListTile( - iconColor: Theme.of(context).colorScheme.onSurfaceVariant, - textColor: Theme.of(context).colorScheme.onSurfaceVariant, - title: Text( - 'ssh.create'.tr(), - ), - leading: const Icon(Icons.add_circle_outlined), - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (final BuildContext context) => Padding( - padding: MediaQuery.of(context).viewInsets, - child: NewSshKey(user), - ), - ); - }, - ), - Column( - children: user.sshKeys.map((final String key) { - final publicKey = - key.split(' ').length > 1 ? key.split(' ')[1] : key; - final keyType = key.split(' ')[0]; - final keyName = key.split(' ').length > 2 - ? key.split(' ')[2] - : 'ssh.no_key_name'.tr(); - return ListTile( - textColor: Theme.of(context).colorScheme.onSurfaceVariant, - title: Text('$keyName ($keyType)'), - // do not overflow text - subtitle: Text( - publicKey, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - onTap: () { - showDialog( - context: context, - builder: (final BuildContext context) => AlertDialog( - title: Text('ssh.delete'.tr()), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text('ssh.delete_confirm_question'.tr()), - Text('$keyName ($keyType)'), - Text(publicKey), - ], - ), - ), - actions: [ - TextButton( - child: Text('basis.cancel'.tr()), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text( - 'basis.delete'.tr(), - style: const TextStyle( - color: BrandColors.red1, - ), - ), - onPressed: () { - context.read().addJob( - DeleteSSHKeyJob( - user: user, - publicKey: key, - ), - ); - Navigator.of(context) - ..pop() - ..pop(); - }, - ), - ], - ), - ); - }, - ); - }).toList(), - ), - ], - ), - ), + _SshKeysCard(user: user), const SizedBox(height: 8), ListTile( iconColor: Theme.of(context).colorScheme.onBackground, @@ -136,56 +63,7 @@ class _UserDetails extends StatelessWidget { 'users.reset_password'.tr(), ), ), - if (!isRootUser) - ListTile( - iconColor: Theme.of(context).colorScheme.error, - textColor: Theme.of(context).colorScheme.error, - onTap: () => { - showDialog( - context: context, - builder: (final BuildContext context) => AlertDialog( - title: Text('basis.confirmation'.tr()), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text( - 'users.delete_confirm_question'.tr(), - ), - ], - ), - ), - actions: [ - TextButton( - child: Text('basis.cancel'.tr()), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: Text( - 'basis.delete'.tr(), - style: const TextStyle( - color: BrandColors.red1, - ), - ), - onPressed: () { - context - .read() - .addJob(DeleteUserJob(user: user)); - Navigator.of(context) - ..pop() - ..pop(); - }, - ), - ], - ), - ) - }, - leading: const Icon(Icons.person_remove_outlined), - title: Text( - 'users.delete_user'.tr(), - ), - ), + if (user.type == UserType.normal) _DeleteUserTile(user: user), const Divider(height: 8), Padding( padding: const EdgeInsets.all(16.0), @@ -204,3 +82,264 @@ class _UserDetails extends StatelessWidget { ); } } + +class _DeleteUserTile extends StatelessWidget { + const _DeleteUserTile({ + required this.user, + }); + + final User user; + + @override + Widget build(final BuildContext context) => ListTile( + iconColor: Theme.of(context).colorScheme.error, + textColor: Theme.of(context).colorScheme.error, + onTap: () => { + showDialog( + context: context, + builder: (final BuildContext context) => AlertDialog( + title: Text('basis.confirmation'.tr()), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text( + 'users.delete_confirm_question'.tr(), + ), + ], + ), + ), + actions: [ + TextButton( + child: Text('basis.cancel'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text( + 'basis.delete'.tr(), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + onPressed: () { + context.read().addJob(DeleteUserJob(user: user)); + Navigator.of(context) + ..pop() + ..pop(); + }, + ), + ], + ), + ) + }, + leading: const Icon(Icons.person_remove_outlined), + title: Text( + 'users.delete_user'.tr(), + ), + ); +} + +class _UserLogins extends StatelessWidget { + const _UserLogins({ + required this.user, + required this.domainName, + }); + + final User user; + final String domainName; + + @override + Widget build(final BuildContext context) => BrandCards.filled( + child: Column( + children: [ + ListTile( + title: Text('${user.login}@$domainName'), + subtitle: Text('users.email_login'.tr()), + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + leading: const Icon(Icons.alternate_email_outlined), + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ), + ); +} + +class _SshKeysCard extends StatelessWidget { + const _SshKeysCard({ + required this.user, + }); + + final User user; + + @override + Widget build(final BuildContext context) => BrandCards.filled( + child: Column( + children: [ + ListTile( + title: Text('ssh.title'.tr()), + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const Divider(height: 0), + ListTile( + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + title: Text( + 'ssh.create'.tr(), + ), + leading: const Icon(Icons.add_circle_outlined), + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => Padding( + padding: MediaQuery.of(context).viewInsets, + child: NewSshKey(user), + ), + ); + }, + ), + Column( + children: user.sshKeys.map((final String key) { + final publicKey = + key.split(' ').length > 1 ? key.split(' ')[1] : key; + final keyType = key.split(' ')[0]; + final keyName = key.split(' ').length > 2 + ? key.split(' ')[2] + : 'ssh.no_key_name'.tr(); + return ListTile( + textColor: Theme.of(context).colorScheme.onSurfaceVariant, + title: Text('$keyName ($keyType)'), + // do not overflow text + subtitle: Text( + publicKey, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + showDialog( + context: context, + builder: (final BuildContext context) => AlertDialog( + title: Text('ssh.delete'.tr()), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text('ssh.delete_confirm_question'.tr()), + Text('$keyName ($keyType)'), + Text(publicKey), + ], + ), + ), + actions: [ + TextButton( + child: Text('basis.cancel'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text( + 'basis.delete'.tr(), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + onPressed: () { + context.read().addJob( + DeleteSSHKeyJob( + user: user, + publicKey: key, + ), + ); + Navigator.of(context) + ..pop() + ..pop(); + }, + ), + ], + ), + ); + }, + ); + }).toList(), + ), + ], + ), + ); +} + +class NewSshKey extends StatelessWidget { + const NewSshKey(this.user, {final super.key}); + final User user; + + @override + Widget build(final BuildContext context) => BrandBottomSheet( + child: BlocProvider( + create: (final context) { + final jobCubit = context.read(); + final jobState = jobCubit.state; + if (jobState is JobsStateWithJobs) { + final jobs = jobState.clientJobList; + for (final job in jobs) { + if (job is CreateSSHKeyJob && job.user.login == user.login) { + user.sshKeys.add(job.publicKey); + } + } + } + return SshFormCubit( + jobsCubit: jobCubit, + user: user, + ); + }, + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; + + return BlocListener( + listener: (final context, final state) { + if (state.isSubmitted) { + Navigator.pop(context); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + BrandHeader( + title: user.login, + ), + const SizedBox(width: 14), + Padding( + padding: paddingH15V0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicHeight( + child: CubitFormTextField( + formFieldCubit: context.read().key, + decoration: InputDecoration( + labelText: 'ssh.input_label'.tr(), + ), + ), + ), + const SizedBox(height: 30), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => + context.read().trySubmit(), + text: 'ssh.create'.tr(), + ), + const SizedBox(height: 30), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ); +} diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 9c215ef8..bf76ba89 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/forms/user/ssh_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/user/user_form_cubit.dart'; @@ -19,7 +20,6 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; -import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart';