mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-09-28 14:27:50 +00:00
Add SSH key adding and deleting
This commit is contained in:
parent
d240e493b1
commit
85235a2e7c
|
@ -62,6 +62,20 @@
|
|||
"6": "This removes the Server. It will be no longer accessible"
|
||||
}
|
||||
},
|
||||
"ssh": {
|
||||
"title": "SSH keys",
|
||||
"create": "Create SSH key",
|
||||
"delete": "Delete SSH key",
|
||||
"delete_confirm_question": "Are you sure you want to delete SSH key?",
|
||||
"subtitle_with_keys": "{} keys",
|
||||
"subtitle_without_keys": "No keys",
|
||||
"no_key_name": "Unnamed key",
|
||||
"root": {
|
||||
"title": "These are superuser keys",
|
||||
"subtitle": "Owners of these keys get full access to the server and can do anything on it. Only add your own keys to the server."
|
||||
},
|
||||
"input_label": "Public ED25519 or RSA key"
|
||||
},
|
||||
"onboarding": {
|
||||
"_comment": "Onboarding pages",
|
||||
"page1_title": "Digital independence, available to all of us",
|
||||
|
@ -311,6 +325,7 @@
|
|||
"root_name": "User name cannot be 'root'",
|
||||
"key_format": "Invalid key format",
|
||||
"length": "Length is [] should be {}",
|
||||
"user_already_exist": "Already exists"
|
||||
"user_already_exist": "Already exists",
|
||||
"key_already_exists": "This key already exists"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,16 @@
|
|||
"ssh": {
|
||||
"title": "SSH ключи",
|
||||
"create": "Добавить SSH ключ",
|
||||
"delete": "Удалить SSH ключ"
|
||||
"delete": "Удалить SSH ключ",
|
||||
"delete_confirm_question": "Вы уверены что хотите удалить следующий ключ?",
|
||||
"subtitle_with_keys": "Ключей: {}",
|
||||
"subtitle_without_keys": "Ключей нет",
|
||||
"no_key_name": "Безымянный ключ",
|
||||
"root": {
|
||||
"title": "Это ключи суперпользователя",
|
||||
"subtitle": "Владельцы указанных здесь ключей получают полный доступ к данным и настройкам сервера. Добавляйте исключительно свои ключи."
|
||||
},
|
||||
"input_label": "Публичный ED25519 или RSA ключ"
|
||||
},
|
||||
"onboarding": {
|
||||
"_comment": "страницы онбординга",
|
||||
|
@ -317,6 +326,7 @@
|
|||
"root_name": "Имя пользователя не может быть'root'.",
|
||||
"key_format": "Неверный формат.",
|
||||
"length": "Длина строки [] должна быть {}.",
|
||||
"user_already_exist": "Имя уже используется."
|
||||
"user_already_exist": "Имя уже используется.",
|
||||
"key_already_exists": "Этот ключ уже добавлен."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,16 +109,17 @@ class ServerApi extends ApiMap {
|
|||
}
|
||||
|
||||
Future<ApiResponse<List<String>>> getUsersList() async {
|
||||
List<String> res;
|
||||
List<String> res = [];
|
||||
Response response;
|
||||
|
||||
var client = await getClient();
|
||||
response = await client.get('/users');
|
||||
try {
|
||||
res = (json.decode(response.data) as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList();
|
||||
for (var user in response.data) {
|
||||
res.add(user.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
res = [];
|
||||
}
|
||||
|
||||
|
|
46
lib/logic/cubit/forms/user/ssh_form_cubit.dart
Normal file
46
lib/logic/cubit/forms/user/ssh_form_cubit.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:cubit_form/cubit_form.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/job.dart';
|
||||
import 'package:selfprivacy/logic/models/user.dart';
|
||||
|
||||
class SshFormCubit extends FormCubit {
|
||||
SshFormCubit({
|
||||
required this.jobsCubit,
|
||||
required this.user,
|
||||
}) {
|
||||
var keyRegExp = RegExp(
|
||||
r"^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$");
|
||||
|
||||
key = FieldCubit(
|
||||
initalValue: '',
|
||||
validations: [
|
||||
ValidationModel(
|
||||
(newKey) => user.sshKeys.any((key) => key == newKey),
|
||||
'validations.key_already_exists'.tr(),
|
||||
),
|
||||
RequiredStringValidation('validations.required'.tr()),
|
||||
ValidationModel<String>((s) {
|
||||
print(s);
|
||||
print(keyRegExp.hasMatch(s));
|
||||
return !keyRegExp.hasMatch(s);
|
||||
}, 'validations.invalid_format'.tr()),
|
||||
],
|
||||
);
|
||||
|
||||
super.addFields([key]);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> onSubmit() {
|
||||
print(key.state.isValid);
|
||||
jobsCubit.addJob(CreateSSHKeyJob(user: user, publicKey: key.state.value));
|
||||
}
|
||||
|
||||
late FieldCubit<String> key;
|
||||
|
||||
final JobsCubit jobsCubit;
|
||||
final User user;
|
||||
}
|
|
@ -5,9 +5,7 @@ import 'package:selfprivacy/config/get_it_config.dart';
|
|||
import 'package:selfprivacy/logic/api_maps/server.dart';
|
||||
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
|
||||
import 'package:selfprivacy/logic/get_it/ssh.dart';
|
||||
import 'package:selfprivacy/logic/models/job.dart';
|
||||
import 'package:selfprivacy/logic/models/user.dart';
|
||||
|
||||
export 'package:provider/provider.dart';
|
||||
|
||||
|
@ -100,7 +98,6 @@ class JobsCubit extends Cubit<JobsState> {
|
|||
if (state is JobsStateWithJobs) {
|
||||
var jobs = (state as JobsStateWithJobs).jobList;
|
||||
emit(JobsStateLoading());
|
||||
var newUsers = <User>[];
|
||||
var hasServiceJobs = false;
|
||||
for (var job in jobs) {
|
||||
if (job is CreateUserJob) {
|
||||
|
@ -114,8 +111,10 @@ class JobsCubit extends Cubit<JobsState> {
|
|||
await api.switchService(job.type, job.needToTurnOn);
|
||||
}
|
||||
if (job is CreateSSHKeyJob) {
|
||||
await getIt<SSHModel>().generateKeys();
|
||||
api.addRootSshKey(getIt<SSHModel>().savedPubKey!);
|
||||
await usersCubit.addSshKey(job.user, job.publicKey);
|
||||
}
|
||||
if (job is DeleteSSHKeyJob) {
|
||||
await usersCubit.deleteSshKey(job.user, job.publicKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,8 +125,6 @@ class JobsCubit extends Cubit<JobsState> {
|
|||
}
|
||||
|
||||
emit(JobsStateEmpty());
|
||||
|
||||
getIt<NavigationService>().navigator!.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,11 +128,11 @@ class UsersCubit extends Cubit<UsersState> {
|
|||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
List<User> updatedUsers = state.users;
|
||||
List<User> updatedUsers = List<User>.from(state.users);
|
||||
final usersFromServer = await api.getUsersList();
|
||||
if (usersFromServer.isSuccess) {
|
||||
updatedUsers =
|
||||
mergeLocalAndServerUsers(state.users, usersFromServer.data);
|
||||
mergeLocalAndServerUsers(updatedUsers, usersFromServer.data);
|
||||
}
|
||||
final usersWithSshKeys = await loadSshKeys(updatedUsers);
|
||||
box.clear();
|
||||
|
@ -157,8 +157,11 @@ class UsersCubit extends Cubit<UsersState> {
|
|||
return;
|
||||
}
|
||||
final result = await api.createUser(user);
|
||||
await box.add(result.data);
|
||||
emit(state.copyWith(users: box.values.toList()));
|
||||
var loadedUsers = List<User>.from(state.users);
|
||||
loadedUsers.add(result.data);
|
||||
await box.clear();
|
||||
await box.addAll(loadedUsers);
|
||||
emit(state.copyWith(users: loadedUsers));
|
||||
}
|
||||
|
||||
Future<void> deleteUser(User user) async {
|
||||
|
@ -166,10 +169,13 @@ class UsersCubit extends Cubit<UsersState> {
|
|||
if (user.login == state.primaryUser.login || user.login == 'root') {
|
||||
return;
|
||||
}
|
||||
var loadedUsers = List<User>.from(state.users);
|
||||
final result = await api.deleteUser(user);
|
||||
if (result) {
|
||||
await box.deleteAt(box.values.toList().indexOf(user));
|
||||
emit(state.copyWith(users: box.values.toList()));
|
||||
loadedUsers.removeWhere((u) => u.login == user.login);
|
||||
await box.clear();
|
||||
await box.addAll(loadedUsers);
|
||||
emit(state.copyWith(users: loadedUsers));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,7 +205,8 @@ class UsersCubit extends Cubit<UsersState> {
|
|||
if (result.isSuccess) {
|
||||
// If it is primary user, update primary user
|
||||
if (user.login == state.primaryUser.login) {
|
||||
List<String> primaryUserKeys = state.primaryUser.sshKeys;
|
||||
List<String> primaryUserKeys =
|
||||
List<String>.from(state.primaryUser.sshKeys);
|
||||
primaryUserKeys.add(publicKey);
|
||||
final updatedUser = User(
|
||||
login: state.primaryUser.login,
|
||||
|
@ -214,7 +221,7 @@ class UsersCubit extends Cubit<UsersState> {
|
|||
));
|
||||
} else {
|
||||
// If it is not primary user, update user
|
||||
List<String> userKeys = user.sshKeys;
|
||||
List<String> userKeys = List<String>.from(user.sshKeys);
|
||||
userKeys.add(publicKey);
|
||||
final updatedUser = User(
|
||||
login: user.login,
|
||||
|
@ -258,7 +265,8 @@ class UsersCubit extends Cubit<UsersState> {
|
|||
return;
|
||||
}
|
||||
if (user.login == state.primaryUser.login) {
|
||||
List<String> primaryUserKeys = state.primaryUser.sshKeys;
|
||||
List<String> primaryUserKeys =
|
||||
List<String>.from(state.primaryUser.sshKeys);
|
||||
primaryUserKeys.remove(publicKey);
|
||||
final updatedUser = User(
|
||||
login: state.primaryUser.login,
|
||||
|
@ -273,7 +281,7 @@ class UsersCubit extends Cubit<UsersState> {
|
|||
));
|
||||
return;
|
||||
}
|
||||
List<String> userKeys = user.sshKeys;
|
||||
List<String> userKeys = List<String>.from(user.sshKeys);
|
||||
userKeys.remove(publicKey);
|
||||
final updatedUser = User(
|
||||
login: user.login,
|
||||
|
@ -291,7 +299,9 @@ class UsersCubit extends Cubit<UsersState> {
|
|||
|
||||
@override
|
||||
void onChange(Change<UsersState> change) {
|
||||
print(change);
|
||||
super.onChange(change);
|
||||
|
||||
print('UsersState changed');
|
||||
print(change);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,36 +35,36 @@ class MyApp extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Localization(
|
||||
child: BlocAndProviderConfig(
|
||||
child: Builder(builder: (context) {
|
||||
var appSettings = context.watch<AppSettingsCubit>().state;
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light, // Manually changing appbar color
|
||||
child: MaterialApp(
|
||||
scaffoldMessengerKey:
|
||||
getIt.get<NavigationService>().scaffoldMessengerKey,
|
||||
navigatorKey: getIt.get<NavigationService>().navigatorKey,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'SelfPrivacy',
|
||||
theme: appSettings.isDarkModeOn ? darkTheme : lightTheme,
|
||||
home: appSettings.isOnbordingShowing
|
||||
? OnboardingPage(nextPage: InitializingPage())
|
||||
: RootPage(),
|
||||
builder: (BuildContext context, Widget? widget) {
|
||||
Widget error = Text('...rendering error...');
|
||||
if (widget is Scaffold || widget is Navigator)
|
||||
error = Scaffold(body: Center(child: error));
|
||||
ErrorWidget.builder =
|
||||
(FlutterErrorDetails errorDetails) => error;
|
||||
return widget!;
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
child: AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light, // Manually changing appbar color
|
||||
child: BlocAndProviderConfig(
|
||||
child: BlocBuilder<AppSettingsCubit, AppSettingsState>(
|
||||
builder: (context, appSettings) {
|
||||
return MaterialApp(
|
||||
scaffoldMessengerKey:
|
||||
getIt.get<NavigationService>().scaffoldMessengerKey,
|
||||
navigatorKey: getIt.get<NavigationService>().navigatorKey,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'SelfPrivacy',
|
||||
theme: appSettings.isDarkModeOn ? darkTheme : lightTheme,
|
||||
home: appSettings.isOnbordingShowing
|
||||
? OnboardingPage(nextPage: InitializingPage())
|
||||
: RootPage(),
|
||||
builder: (BuildContext context, Widget? widget) {
|
||||
Widget error = Text('...rendering error...');
|
||||
if (widget is Scaffold || widget is Navigator)
|
||||
error = Scaffold(body: Center(child: error));
|
||||
ErrorWidget.builder =
|
||||
(FlutterErrorDetails errorDetails) => error;
|
||||
return widget!;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,9 +26,6 @@ class MorePage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var jobsCubit = context.watch<JobsCubit>();
|
||||
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
|
||||
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
child: BrandHeader(
|
||||
|
@ -114,30 +111,6 @@ class _NavItem extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _MoreMenuTapItem extends StatelessWidget {
|
||||
const _MoreMenuTapItem({
|
||||
Key? key,
|
||||
required this.iconData,
|
||||
required this.onTap,
|
||||
required this.title,
|
||||
}) : super(key: key);
|
||||
|
||||
final IconData iconData;
|
||||
final VoidCallback? onTap;
|
||||
final String title;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: _MoreMenuItem(
|
||||
isActive: onTap != null,
|
||||
iconData: iconData,
|
||||
title: title,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MoreMenuItem extends StatelessWidget {
|
||||
const _MoreMenuItem({
|
||||
Key? key,
|
||||
|
|
76
lib/ui/pages/ssh_keys/new_ssh_key.dart
Normal file
76
lib/ui/pages/ssh_keys/new_ssh_key.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
part of 'ssh_keys.dart';
|
||||
|
||||
class _NewSshKey extends StatelessWidget {
|
||||
final User user;
|
||||
|
||||
_NewSshKey(this.user);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BrandBottomSheet(
|
||||
child: BlocProvider(
|
||||
create: (context) {
|
||||
var jobCubit = context.read<JobsCubit>();
|
||||
var jobState = jobCubit.state;
|
||||
if (jobState is JobsStateWithJobs) {
|
||||
var jobs = jobState.jobList;
|
||||
jobs.forEach((job) {
|
||||
if (job is CreateSSHKeyJob && job.user.login == user.login) {
|
||||
user.sshKeys.add(job.publicKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
return SshFormCubit(
|
||||
jobsCubit: jobCubit,
|
||||
user: user,
|
||||
);
|
||||
},
|
||||
child: Builder(builder: (context) {
|
||||
var formCubitState = context.watch<SshFormCubit>().state;
|
||||
|
||||
return BlocListener<SshFormCubit, FormCubitState>(
|
||||
listener: (context, state) {
|
||||
if (state.isSubmitted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BrandHeader(
|
||||
title: user.login,
|
||||
),
|
||||
SizedBox(width: 14),
|
||||
Padding(
|
||||
padding: paddingH15V0,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IntrinsicHeight(
|
||||
child: CubitFormTextField(
|
||||
formFieldCubit: context.read<SshFormCubit>().key,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'ssh.input_label'.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
BrandButton.rised(
|
||||
onPressed: formCubitState.isSubmitting
|
||||
? null
|
||||
: () => context.read<SshFormCubit>().trySubmit(),
|
||||
text: 'ssh.create'.tr(),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,21 @@
|
|||
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 '../../../config/brand_colors.dart';
|
||||
import '../../../config/brand_theme.dart';
|
||||
import '../../../logic/cubit/jobs/jobs_cubit.dart';
|
||||
import '../../../logic/models/user.dart';
|
||||
import '../../components/brand_button/brand_button.dart';
|
||||
import '../../components/brand_header/brand_header.dart';
|
||||
|
||||
part 'new_ssh_key.dart';
|
||||
|
||||
// Get user object as a parameter
|
||||
class SshKeysPage extends StatefulWidget {
|
||||
|
@ -49,20 +60,77 @@ class _SshKeysPageState extends State<SshKeysPage> {
|
|||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: Icon(Icons.add_circle_outline_rounded),
|
||||
onTap: () {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
return Padding(
|
||||
padding: MediaQuery.of(context).viewInsets,
|
||||
child: _NewSshKey(widget.user));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Divider(height: 0),
|
||||
// show a list of ListTiles with ssh keys
|
||||
// Clicking on one should delete it
|
||||
Column(
|
||||
children: widget.user.sshKeys.map((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('${key.split(' ')[2]} (${key.split(' ')[0]})'),
|
||||
title: Text('$keyName ($keyType)'),
|
||||
// do not overflow text
|
||||
subtitle: Text(key.split(' ')[1],
|
||||
subtitle: Text(publicKey,
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
onTap: () {
|
||||
// TODO: delete ssh key
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('ssh.delete'.tr()),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: <Widget>[
|
||||
Text('ssh.delete_confirm_question'.tr()),
|
||||
Text('$keyName ($keyType)'),
|
||||
Text(publicKey),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('basis.cancel'.tr()),
|
||||
onPressed: () {
|
||||
Navigator.of(context)..pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'basis.delete'.tr(),
|
||||
style: TextStyle(
|
||||
color: BrandColors.red1,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<JobsCubit>().addJob(
|
||||
DeleteSSHKeyJob(
|
||||
user: widget.user, publicKey: key));
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}).toList(),
|
||||
)
|
||||
|
|
|
@ -157,17 +157,6 @@ class _UserDetails extends StatelessWidget {
|
|||
SizedBox(height: 24),
|
||||
BrandDivider(),
|
||||
SizedBox(height: 20),
|
||||
BrandButton.emptyWithIconText(
|
||||
title: 'users.send_registration_data'.tr(),
|
||||
icon: Icon(BrandIcons.share),
|
||||
onPressed: () {
|
||||
Share.share(
|
||||
'login: ${user.login}, password: ${user.password}');
|
||||
},
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
BrandDivider(),
|
||||
SizedBox(height: 20),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
|
@ -179,6 +168,17 @@ class _UserDetails extends StatelessWidget {
|
|||
.tr(args: [user.sshKeys.length.toString()]))
|
||||
: Text('ssh.subtitle_without_keys'.tr()),
|
||||
trailing: Icon(BrandIcons.key)),
|
||||
SizedBox(height: 20),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Share.share(
|
||||
'login: ${user.login}, password: ${user.password}');
|
||||
},
|
||||
title: Text(
|
||||
'users.send_registration_data'.tr(),
|
||||
),
|
||||
trailing: Icon(BrandIcons.share),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
|
@ -36,37 +36,38 @@ class UsersPage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final usersCubitState = context.watch<UsersCubit>().state;
|
||||
// final usersCubitState = context.watch<UsersCubit>().state;
|
||||
var isReady = context.watch<AppConfigCubit>().state is AppConfigFinished;
|
||||
final primaryUser = usersCubitState.primaryUser;
|
||||
final users = [primaryUser, ...usersCubitState.users];
|
||||
final isEmpty = users.isEmpty;
|
||||
// final primaryUser = usersCubitState.primaryUser;
|
||||
// final users = [primaryUser, ...usersCubitState.users];
|
||||
// final isEmpty = users.isEmpty;
|
||||
Widget child;
|
||||
|
||||
if (!isReady) {
|
||||
child = isNotReady();
|
||||
} else {
|
||||
child = isEmpty
|
||||
? Container(
|
||||
alignment: Alignment.center,
|
||||
child: _NoUsers(
|
||||
text: 'users.add_new_user'.tr(),
|
||||
),
|
||||
)
|
||||
: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<UsersCubit>().refresh();
|
||||
child = BlocBuilder<UsersCubit, UsersState>(
|
||||
builder: (context, state) {
|
||||
print('Rebuild users page');
|
||||
final primaryUser = state.primaryUser;
|
||||
final users = [primaryUser, ...state.users];
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<UsersCubit>().refresh();
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: users.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _User(
|
||||
user: users[index],
|
||||
isRootUser: index == 0,
|
||||
);
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: users.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _User(
|
||||
user: users[index],
|
||||
isRootUser: index == 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: selfprivacy
|
||||
description: selfprivacy.org
|
||||
publish_to: 'none'
|
||||
version: 0.4.2+10
|
||||
version: 0.5.0+11
|
||||
|
||||
environment:
|
||||
sdk: '>=2.13.4 <3.0.0'
|
||||
|
|
Loading…
Reference in a new issue