mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-03-31 18:56:21 +00:00
feat: added reset password tile and did some widget refactoring
This commit is contained in:
parent
71c2b526b9
commit
d59c3dcda3
19 changed files with 729 additions and 761 deletions
lib
logic
api_maps/graphql_maps
cubit/forms/user
get_it
models
ui
utils
|
@ -632,7 +632,6 @@ type User {
|
|||
|
||||
input UserMutationInput {
|
||||
username: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
type UserMutationReturn implements MutationReturnInterface {
|
||||
|
|
|
@ -1629,13 +1629,9 @@ class _CopyWithStubImpl$Input$UseRecoveryKeyInput<TRes>
|
|||
}
|
||||
|
||||
class Input$UserMutationInput {
|
||||
factory Input$UserMutationInput({
|
||||
required String username,
|
||||
required String password,
|
||||
}) =>
|
||||
factory Input$UserMutationInput({required String username}) =>
|
||||
Input$UserMutationInput._({
|
||||
r'username': username,
|
||||
r'password': password,
|
||||
});
|
||||
|
||||
Input$UserMutationInput._(this._$data);
|
||||
|
@ -1644,8 +1640,6 @@ class Input$UserMutationInput {
|
|||
final result$data = <String, dynamic>{};
|
||||
final l$username = data['username'];
|
||||
result$data['username'] = (l$username as String);
|
||||
final l$password = data['password'];
|
||||
result$data['password'] = (l$password as String);
|
||||
return Input$UserMutationInput._(result$data);
|
||||
}
|
||||
|
||||
|
@ -1653,14 +1647,10 @@ class Input$UserMutationInput {
|
|||
|
||||
String get username => (_$data['username'] as String);
|
||||
|
||||
String get password => (_$data['password'] as String);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final result$data = <String, dynamic>{};
|
||||
final l$username = username;
|
||||
result$data['username'] = l$username;
|
||||
final l$password = password;
|
||||
result$data['password'] = l$password;
|
||||
return result$data;
|
||||
}
|
||||
|
||||
|
@ -1684,22 +1674,13 @@ class Input$UserMutationInput {
|
|||
if (l$username != lOther$username) {
|
||||
return false;
|
||||
}
|
||||
final l$password = password;
|
||||
final lOther$password = other.password;
|
||||
if (l$password != lOther$password) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
final l$username = username;
|
||||
final l$password = password;
|
||||
return Object.hashAll([
|
||||
l$username,
|
||||
l$password,
|
||||
]);
|
||||
return Object.hashAll([l$username]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1712,10 +1693,7 @@ abstract class CopyWith$Input$UserMutationInput<TRes> {
|
|||
factory CopyWith$Input$UserMutationInput.stub(TRes res) =
|
||||
_CopyWithStubImpl$Input$UserMutationInput;
|
||||
|
||||
TRes call({
|
||||
String? username,
|
||||
String? password,
|
||||
});
|
||||
TRes call({String? username});
|
||||
}
|
||||
|
||||
class _CopyWithImpl$Input$UserMutationInput<TRes>
|
||||
|
@ -1731,16 +1709,11 @@ class _CopyWithImpl$Input$UserMutationInput<TRes>
|
|||
|
||||
static const _undefined = <dynamic, dynamic>{};
|
||||
|
||||
TRes call({
|
||||
Object? username = _undefined,
|
||||
Object? password = _undefined,
|
||||
}) =>
|
||||
TRes call({Object? username = _undefined}) =>
|
||||
_then(Input$UserMutationInput._({
|
||||
..._instance._$data,
|
||||
if (username != _undefined && username != null)
|
||||
'username': (username as String),
|
||||
if (password != _undefined && password != null)
|
||||
'password': (password as String),
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -1750,11 +1723,7 @@ class _CopyWithStubImpl$Input$UserMutationInput<TRes>
|
|||
|
||||
TRes _res;
|
||||
|
||||
call({
|
||||
String? username,
|
||||
String? password,
|
||||
}) =>
|
||||
_res;
|
||||
call({String? username}) => _res;
|
||||
}
|
||||
|
||||
enum Enum$BackupProvider {
|
||||
|
|
|
@ -45,14 +45,11 @@ mixin UsersApi on GraphQLApiMap {
|
|||
return user;
|
||||
}
|
||||
|
||||
Future<GenericResult<User?>> createUser(
|
||||
final String username,
|
||||
final String password,
|
||||
) async {
|
||||
Future<GenericResult<User?>> createUser(final String username) async {
|
||||
try {
|
||||
final GraphQLClient client = await getClient();
|
||||
final variables = Variables$Mutation$CreateUser(
|
||||
user: Input$UserMutationInput(username: username, password: password),
|
||||
user: Input$UserMutationInput(username: username),
|
||||
);
|
||||
final mutation = Options$Mutation$CreateUser(variables: variables);
|
||||
final response = await client.mutate$CreateUser(mutation);
|
||||
|
@ -100,36 +97,6 @@ mixin UsersApi on GraphQLApiMap {
|
|||
}
|
||||
}
|
||||
|
||||
Future<GenericResult<User?>> updateUser(
|
||||
final String username,
|
||||
final String password,
|
||||
) async {
|
||||
try {
|
||||
final GraphQLClient client = await getClient();
|
||||
final variables = Variables$Mutation$UpdateUser(
|
||||
user: Input$UserMutationInput(username: username, password: password),
|
||||
);
|
||||
final mutation = Options$Mutation$UpdateUser(variables: variables);
|
||||
final response = await client.mutate$UpdateUser(mutation);
|
||||
return GenericResult(
|
||||
success: true,
|
||||
code: response.parsedData?.users.updateUser.code ?? 500,
|
||||
message: response.parsedData?.users.updateUser.message,
|
||||
data: response.parsedData?.users.updateUser.user != null
|
||||
? User.fromGraphQL(response.parsedData!.users.updateUser.user!)
|
||||
: null,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
return GenericResult(
|
||||
data: null,
|
||||
success: false,
|
||||
code: 0,
|
||||
message: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<GenericResult<User?>> addSshKey(
|
||||
final String username,
|
||||
final String sshKey,
|
||||
|
|
88
lib/logic/cubit/forms/user/reset_password_bloc.dart
Normal file
88
lib/logic/cubit/forms/user/reset_password_bloc.dart
Normal file
|
@ -0,0 +1,88 @@
|
|||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/utils/app_logger.dart';
|
||||
|
||||
class ResetPasswordBloc extends Bloc<ResetPasswordEvent, ResetPasswordState> {
|
||||
ResetPasswordBloc({
|
||||
required this.user,
|
||||
}) : super(
|
||||
const ResetPasswordState(),
|
||||
) {
|
||||
log('ResetPasswordBloc created for user: ${user.login}');
|
||||
|
||||
on<RequestNewPassword>(
|
||||
_mapResetPasswordRequestedToState,
|
||||
transformer: droppable(),
|
||||
);
|
||||
}
|
||||
|
||||
static final log = const AppLogger(name: 'ResetPasswordBloc').log;
|
||||
|
||||
final User user;
|
||||
|
||||
Future<void> _mapResetPasswordRequestedToState(
|
||||
final RequestNewPassword event,
|
||||
final Emitter<ResetPasswordState> emit,
|
||||
) async {
|
||||
log('Reset password requested for user: ${user.login}');
|
||||
if (state.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
const ResetPasswordState(
|
||||
passwordResetLink: null,
|
||||
isLoading: true,
|
||||
),
|
||||
);
|
||||
|
||||
log('Load start');
|
||||
final (link, message) =
|
||||
await getIt<ApiConnectionRepository>().generatePasswordResetLink(user);
|
||||
|
||||
log('Got link: $link, message: $message');
|
||||
|
||||
emit(
|
||||
link != null
|
||||
? ResetPasswordState(
|
||||
passwordResetLink: link,
|
||||
passwordResetMessage: message,
|
||||
)
|
||||
: ResetPasswordState(
|
||||
errorMessage: message,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ResetPasswordEvent extends Equatable {
|
||||
const ResetPasswordEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class RequestNewPassword extends ResetPasswordEvent {
|
||||
const RequestNewPassword();
|
||||
}
|
||||
|
||||
class ResetPasswordState extends Equatable {
|
||||
const ResetPasswordState({
|
||||
this.passwordResetLink,
|
||||
this.passwordResetMessage = '',
|
||||
this.errorMessage = '',
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
final Uri? passwordResetLink;
|
||||
final bool isLoading;
|
||||
final String passwordResetMessage;
|
||||
final String errorMessage;
|
||||
|
||||
@override
|
||||
List<Object?> get props =>
|
||||
[passwordResetMessage, isLoading, passwordResetLink, errorMessage];
|
||||
}
|
|
@ -1,65 +1,68 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:cubit_form/cubit_form.dart';
|
||||
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/logic/models/job.dart';
|
||||
import 'package:selfprivacy/utils/password_generator.dart';
|
||||
|
||||
class UserFormCubit extends FormCubit {
|
||||
UserFormCubit({
|
||||
required this.jobsCubit,
|
||||
required final FieldCubitFactory fieldFactory,
|
||||
this.initialUser,
|
||||
}) {
|
||||
if (initialUser == null) {
|
||||
login = fieldFactory.createUserLoginField();
|
||||
login.setValue('');
|
||||
password = fieldFactory.createUserPasswordField();
|
||||
password.setValue(
|
||||
StringGenerators.userPassword(),
|
||||
);
|
||||
required this.initialUser,
|
||||
}) : userCreated = initialUser != null {
|
||||
login = initialUser == null
|
||||
? fieldFactory.createUserLoginField()
|
||||
: fieldFactory.createRequiredStringField();
|
||||
login.setValue(initialUser?.login ?? '');
|
||||
|
||||
super.addFields([login, password]);
|
||||
} else {
|
||||
login = fieldFactory.createRequiredStringField();
|
||||
login.setValue(initialUser!.login);
|
||||
password = fieldFactory.createUserPasswordField();
|
||||
password.setValue(
|
||||
initialUser?.password ?? '',
|
||||
);
|
||||
|
||||
super.addFields([login, password]);
|
||||
}
|
||||
super.addFields([login]);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> onSubmit() {
|
||||
if (initialUser == null) {
|
||||
FutureOr<void> onSubmit() async {
|
||||
if (!userCreated) {
|
||||
final User user = User(
|
||||
login: login.state.value,
|
||||
type: UserType.normal,
|
||||
password: password.state.value,
|
||||
);
|
||||
jobsCubit.addJob(CreateUserJob(user: user));
|
||||
final (result, message) =
|
||||
await getIt<ApiConnectionRepository>().createUser(user);
|
||||
|
||||
if (result) {
|
||||
initialUser = user;
|
||||
userCreated = true;
|
||||
userCreationMessage = message;
|
||||
errorMessage = '';
|
||||
} else {
|
||||
errorMessage = message;
|
||||
getIt<NavigationService>().showSnackBar(errorMessage);
|
||||
}
|
||||
} else {
|
||||
/// We got request to reset password
|
||||
final User user = User(
|
||||
login: initialUser?.login ?? login.state.value,
|
||||
type: initialUser?.type ?? UserType.normal,
|
||||
password: password.state.value,
|
||||
);
|
||||
jobsCubit.addJob(ResetUserPasswordJob(user: user));
|
||||
final (link, message) = await getIt<ApiConnectionRepository>()
|
||||
.generatePasswordResetLink(user);
|
||||
|
||||
if (link != null) {
|
||||
passwordResetLink = link;
|
||||
passwordResetMessage = message;
|
||||
} else {
|
||||
passwordResetMessage = message;
|
||||
getIt<NavigationService>().showSnackBar(passwordResetMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
late FieldCubit<String> login;
|
||||
late FieldCubit<String> password;
|
||||
User? initialUser;
|
||||
|
||||
void genNewPassword() {
|
||||
password.externalSetValue(StringGenerators.userPassword());
|
||||
}
|
||||
bool userCreated;
|
||||
String? userCreationMessage = '';
|
||||
String errorMessage = '';
|
||||
|
||||
final JobsCubit jobsCubit;
|
||||
final User? initialUser;
|
||||
Uri? passwordResetLink;
|
||||
String passwordResetMessage = '';
|
||||
}
|
||||
|
|
|
@ -87,13 +87,10 @@ class ApiConnectionRepository {
|
|||
.any((final User u) => u.login == user.login && u.isFoundOnServer)) {
|
||||
return (false, 'users.user_already_exists'.tr());
|
||||
}
|
||||
final String? password = user.password;
|
||||
if (password == null) {
|
||||
return (false, 'users.could_not_create_user'.tr());
|
||||
}
|
||||
|
||||
// If API returned error, do nothing
|
||||
final GenericResult<User?> result =
|
||||
await api.createUser(user.login, password);
|
||||
final GenericResult<User?> result = await api.createUser(user.login);
|
||||
|
||||
if (result.data == null) {
|
||||
return (false, result.message ?? 'users.could_not_create_user'.tr());
|
||||
}
|
||||
|
@ -126,23 +123,26 @@ class ApiConnectionRepository {
|
|||
return (true, result.message ?? 'basis.done'.tr());
|
||||
}
|
||||
|
||||
Future<(bool, String)> generatePasswordResetLink(
|
||||
// url and error message
|
||||
Future<(Uri?, String)> generatePasswordResetLink(
|
||||
final User user,
|
||||
) async {
|
||||
String errorMessage = 'users.user_modify_protected'.tr();
|
||||
if (user.type == UserType.root) {
|
||||
return (false, errorMessage);
|
||||
return (null, errorMessage);
|
||||
}
|
||||
final GenericResult<String?> result = await api.generatePasswordResetLink(
|
||||
user.login,
|
||||
);
|
||||
if (result.data == null) {
|
||||
final GenericResult<String?> result =
|
||||
await api.generatePasswordResetLink(user.login);
|
||||
|
||||
// check if got valid url
|
||||
final uri = Uri.tryParse(result.data ?? '');
|
||||
if (uri == null) {
|
||||
errorMessage =
|
||||
result.message ?? 'users.could_not_generate_password_link'.tr();
|
||||
getIt<NavigationService>().showSnackBar(errorMessage);
|
||||
return (false, errorMessage);
|
||||
return (null, errorMessage);
|
||||
}
|
||||
return (true, result.message ?? 'basis.done'.tr());
|
||||
|
||||
return (uri, result.message ?? 'basis.done'.tr());
|
||||
}
|
||||
|
||||
Future<(bool, String)> addSshKey(
|
||||
|
|
|
@ -46,7 +46,14 @@ class User extends Equatable {
|
|||
final UserType type;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [login, password, sshKeys, isFoundOnServer, note];
|
||||
List<Object?> get props => [
|
||||
login,
|
||||
password,
|
||||
sshKeys,
|
||||
isFoundOnServer,
|
||||
note,
|
||||
type,
|
||||
];
|
||||
|
||||
Color get color => stringToColor(login);
|
||||
|
||||
|
|
|
@ -146,66 +146,6 @@ class RebootServerJob extends ClientJob {
|
|||
);
|
||||
}
|
||||
|
||||
class CreateUserJob extends ClientJob {
|
||||
CreateUserJob({
|
||||
required this.user,
|
||||
super.status,
|
||||
super.message,
|
||||
super.id,
|
||||
}) : super(title: '${"jobs.create_user".tr()} ${user.login}');
|
||||
|
||||
final User user;
|
||||
|
||||
@override
|
||||
Future<(bool, String)> execute() async =>
|
||||
getIt<ApiConnectionRepository>().createUser(user);
|
||||
|
||||
@override
|
||||
List<Object> get props => [...super.props, user];
|
||||
|
||||
@override
|
||||
CreateUserJob copyWithNewStatus({
|
||||
required final JobStatusEnum status,
|
||||
final String? message,
|
||||
}) =>
|
||||
CreateUserJob(
|
||||
user: user,
|
||||
status: status,
|
||||
message: message,
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
|
||||
class ResetUserPasswordJob extends ClientJob {
|
||||
ResetUserPasswordJob({
|
||||
required this.user,
|
||||
super.status,
|
||||
super.message,
|
||||
super.id,
|
||||
}) : super(title: '${"jobs.reset_user_password".tr()} ${user.login}');
|
||||
|
||||
final User user;
|
||||
|
||||
@override
|
||||
Future<(bool, String)> execute() async => (false, '');
|
||||
//getIt<ApiConnectionRepository>().changeUserPassword(user, user.password!);
|
||||
|
||||
@override
|
||||
List<Object> get props => [...super.props, user];
|
||||
|
||||
@override
|
||||
ResetUserPasswordJob copyWithNewStatus({
|
||||
required final JobStatusEnum status,
|
||||
final String? message,
|
||||
}) =>
|
||||
ResetUserPasswordJob(
|
||||
user: user,
|
||||
status: status,
|
||||
message: message,
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
|
||||
class DeleteUserJob extends ClientJob {
|
||||
DeleteUserJob({
|
||||
required this.user,
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
import 'package:cubit_form/cubit_form.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/user/user_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/ui/atoms/buttons/brand_button.dart';
|
||||
|
||||
class ResetPasswordModal extends StatelessWidget {
|
||||
const ResetPasswordModal({
|
||||
required this.user,
|
||||
required this.scrollController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final User user;
|
||||
final ScrollController scrollController;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => BlocProvider(
|
||||
create: (final BuildContext context) => UserFormCubit(
|
||||
jobsCubit: context.read<JobsCubit>(),
|
||||
fieldFactory: FieldCubitFactory(context),
|
||||
initialUser: user,
|
||||
),
|
||||
child: Builder(
|
||||
builder: (final BuildContext context) {
|
||||
final FormCubitState formCubitState =
|
||||
context.watch<UserFormCubit>().state;
|
||||
|
||||
return BlocListener<UserFormCubit, FormCubitState>(
|
||||
listener:
|
||||
(final BuildContext context, final FormCubitState state) {
|
||||
if (state.isSubmitted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
const Gap(16),
|
||||
Text(
|
||||
'users.reset_password'.tr(),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(16),
|
||||
CubitFormTextField(
|
||||
autofocus: true,
|
||||
formFieldCubit: context.read<UserFormCubit>().password,
|
||||
decoration: InputDecoration(
|
||||
alignLabelWithHint: false,
|
||||
labelText: 'basis.password'.tr(),
|
||||
filled: true,
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.refresh,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
onPressed:
|
||||
context.read<UserFormCubit>().genNewPassword,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
BrandButton.filled(
|
||||
onPressed: formCubitState.isSubmitting
|
||||
? null
|
||||
: () => context.read<UserFormCubit>().trySubmit(),
|
||||
title: 'basis.apply'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
|
@ -2,20 +2,46 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:cubit_form/cubit_form.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/user/user_form_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
import 'package:selfprivacy/ui/atoms/buttons/brand_button.dart';
|
||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||
import 'package:selfprivacy/utils/ui_helpers.dart';
|
||||
|
||||
@RoutePage()
|
||||
class NewUserPage extends StatelessWidget {
|
||||
const NewUserPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => BlocProvider(
|
||||
create: (final BuildContext context) => UserFormCubit(
|
||||
fieldFactory: FieldCubitFactory(context),
|
||||
initialUser: null,
|
||||
),
|
||||
child: BlocConsumer<UserFormCubit, FormCubitState>(
|
||||
listener: (
|
||||
final BuildContext context,
|
||||
final FormCubitState state,
|
||||
) {
|
||||
if (state.isSubmitted) {
|
||||
context.router.maybePop();
|
||||
}
|
||||
},
|
||||
builder: (
|
||||
final BuildContext context,
|
||||
final FormCubitState state,
|
||||
) =>
|
||||
NewUserScreen(state: state),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NewUserScreen extends StatelessWidget {
|
||||
const NewUserScreen({required this.state, super.key});
|
||||
final FormCubitState state;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final ServerInstallationState config =
|
||||
|
@ -23,115 +49,44 @@ class NewUserPage extends StatelessWidget {
|
|||
|
||||
final String domainName = UiHelpers.getDomainName(config);
|
||||
|
||||
return BlocProvider(
|
||||
create: (final BuildContext context) {
|
||||
final jobCubit = context.read<JobsCubit>();
|
||||
|
||||
// final jobsState = jobCubit.state;
|
||||
// final users = <User>[
|
||||
// ...context.read<UsersBloc>().state.users,
|
||||
// if (jobsState is JobsStateWithJobs)
|
||||
// ...jobsState.clientJobList
|
||||
// .whereType<CreateUserJob>()
|
||||
// .map((final job) => job.user),
|
||||
// ];
|
||||
|
||||
return UserFormCubit(
|
||||
jobsCubit: jobCubit,
|
||||
fieldFactory: FieldCubitFactory(context),
|
||||
);
|
||||
},
|
||||
child: Builder(
|
||||
builder: (final BuildContext context) {
|
||||
final FormCubitState formCubitState =
|
||||
context.watch<UserFormCubit>().state;
|
||||
|
||||
return BlocListener<UserFormCubit, FormCubitState>(
|
||||
listener: (final BuildContext context, final FormCubitState state) {
|
||||
if (state.isSubmitted) {
|
||||
context.router.maybePop();
|
||||
}
|
||||
},
|
||||
child: BrandHeroScreen(
|
||||
heroTitle: 'users.new_user'.tr(),
|
||||
heroIcon: Icons.person_add_outlined,
|
||||
children: [
|
||||
if (formCubitState.isErrorShown)
|
||||
Text(
|
||||
'users.username_rule'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
IntrinsicHeight(
|
||||
child: CubitFormTextField(
|
||||
autofocus: true,
|
||||
formFieldCubit: context.read<UserFormCubit>().login,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'users.login'.tr(),
|
||||
suffixText: '@$domainName',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
CubitFormTextField(
|
||||
formFieldCubit: context.read<UserFormCubit>().password,
|
||||
decoration: InputDecoration(
|
||||
alignLabelWithHint: false,
|
||||
labelText: 'basis.password'.tr(),
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.copy,
|
||||
size: 24.0,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
onPressed: () {
|
||||
final String currentPassword = context
|
||||
.read<UserFormCubit>()
|
||||
.password
|
||||
.state
|
||||
.value;
|
||||
PlatformAdapter.setClipboard(currentPassword);
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
'basis.copied_to_clipboard'.tr(),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.refresh,
|
||||
size: 24.0,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
onPressed:
|
||||
context.read<UserFormCubit>().genNewPassword,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
BrandButton.filled(
|
||||
onPressed: formCubitState.isSubmitting
|
||||
? null
|
||||
: () => context.read<UserFormCubit>().trySubmit(),
|
||||
title: 'basis.create'.tr(),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Text('users.new_user_info_note'.tr()),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
return BrandHeroScreen(
|
||||
heroTitle: 'users.new_user'.tr(),
|
||||
heroIcon: Icons.person_add_outlined,
|
||||
children: [
|
||||
if (state.isErrorShown)
|
||||
Text(
|
||||
'users.username_rule'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(14),
|
||||
IntrinsicHeight(
|
||||
child: CubitFormTextField(
|
||||
/// should make this read-only when the user is created
|
||||
autofocus: true,
|
||||
formFieldCubit: context.read<UserFormCubit>().login,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'users.login'.tr(),
|
||||
suffixText: '@$domainName',
|
||||
),
|
||||
),
|
||||
),
|
||||
// if (state.userCreated) ...[
|
||||
// const Gap(20),
|
||||
// Text('users.generate_password_reset_link'.tr()),
|
||||
// ],
|
||||
const Gap(30),
|
||||
BrandButton.filled(
|
||||
onPressed: state.isSubmitting
|
||||
? null
|
||||
: () => context.read<UserFormCubit>().trySubmit(),
|
||||
title: 'basis.create'.tr(),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Text('users.new_user_info_note'.tr()),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,312 +0,0 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/bloc/users/users_bloc.dart';
|
||||
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/logic/models/job.dart';
|
||||
import 'package:selfprivacy/ui/atoms/cards/filled_card.dart';
|
||||
import 'package:selfprivacy/ui/atoms/list_tiles/list_tile_on_surface_variant.dart';
|
||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||
import 'package:selfprivacy/ui/molecules/info_box/info_box.dart';
|
||||
import 'package:selfprivacy/ui/organisms/modals/new_ssh_key_modal.dart';
|
||||
import 'package:selfprivacy/ui/organisms/modals/reset_password_modal.dart';
|
||||
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||
import 'package:selfprivacy/utils/ui_helpers.dart';
|
||||
|
||||
@RoutePage()
|
||||
class UserDetailsPage extends StatelessWidget {
|
||||
const UserDetailsPage({
|
||||
required this.login,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String login;
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final ServerInstallationState config =
|
||||
context.watch<ServerInstallationCubit>().state;
|
||||
|
||||
final String domainName = UiHelpers.getDomainName(config);
|
||||
|
||||
final User user = context.watch<UsersBloc>().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,
|
||||
hasFlashButton: true,
|
||||
heroTitle: 'ssh.root_title'.tr(),
|
||||
heroSubtitle: 'ssh.root_subtitle'.tr(),
|
||||
children: [
|
||||
_SshKeysCard(user: user),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return BrandHeroScreen(
|
||||
hasBackButton: true,
|
||||
hasFlashButton: true,
|
||||
heroTitle: user.login,
|
||||
heroIconWidget: CircleAvatar(
|
||||
backgroundColor: user.color,
|
||||
child: Text(
|
||||
user.login[0].toUpperCase(),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
_UserLogins(user: user, domainName: domainName),
|
||||
const SizedBox(height: 8),
|
||||
_SshKeysCard(user: user),
|
||||
const SizedBox(height: 8),
|
||||
ListTile(
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
onTap: () => showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (final BuildContext context) => DraggableScrollableSheet(
|
||||
expand: false,
|
||||
maxChildSize: 0.9,
|
||||
minChildSize: 0.3,
|
||||
initialChildSize: 0.5,
|
||||
builder: (final context, final scrollController) =>
|
||||
ResetPasswordModal(
|
||||
user: user,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
),
|
||||
),
|
||||
leading: const Icon(Icons.lock_reset_outlined),
|
||||
title: Text(
|
||||
'users.reset_password'.tr(),
|
||||
),
|
||||
),
|
||||
if (user.type == UserType.normal) _DeleteUserTile(user: user),
|
||||
const Divider(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: InfoBox(
|
||||
text: 'users.no_ssh_notice'.tr(),
|
||||
isWarning: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
// useRootNavigator: false,
|
||||
builder: (final BuildContext context) => AlertDialog(
|
||||
title: Text('basis.confirmation'.tr()),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'users.delete_confirm_question'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('basis.cancel'.tr()),
|
||||
onPressed: () {
|
||||
context.router.maybePop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'basis.delete'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<JobsCubit>().addJob(DeleteUserJob(user: user));
|
||||
context.router.childControllers.first.maybePop();
|
||||
context.router.maybePop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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) {
|
||||
final email = '${user.login}@$domainName';
|
||||
return FilledCard(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTileOnSurfaceVariant(
|
||||
onTap: () {
|
||||
PlatformAdapter.setClipboard(email);
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
'basis.copied_to_clipboard'.tr(),
|
||||
);
|
||||
},
|
||||
title: email,
|
||||
subtitle: 'users.email_login'.tr(),
|
||||
leadingIcon: Icons.alternate_email_outlined,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SshKeysCard extends StatelessWidget {
|
||||
const _SshKeysCard({
|
||||
required this.user,
|
||||
});
|
||||
|
||||
final User user;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final serverDetailsState = context.watch<ServerDetailsCubit>().state;
|
||||
final bool sshDisabled =
|
||||
serverDetailsState is Loaded && !serverDetailsState.sshSettings.enable;
|
||||
|
||||
return FilledCard(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTileOnSurfaceVariant(
|
||||
title: 'ssh.title'.tr(),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
ListTileOnSurfaceVariant(
|
||||
title: 'ssh.create'.tr(),
|
||||
leadingIcon: Icons.add_circle_outline,
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (final BuildContext context) =>
|
||||
DraggableScrollableSheet(
|
||||
expand: false,
|
||||
maxChildSize: 0.9,
|
||||
minChildSize: 0.3,
|
||||
initialChildSize: 0.5,
|
||||
builder: (final context, final scrollController) =>
|
||||
NewSshKeyModal(
|
||||
user: user,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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 ListTileOnSurfaceVariant(
|
||||
title: '$keyName ($keyType)',
|
||||
disableSubtitleOverflow: true,
|
||||
// do not overflow text
|
||||
subtitle: publicKey,
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (final BuildContext context) => 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: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<JobsCubit>().addJob(
|
||||
DeleteSSHKeyJob(
|
||||
user: user,
|
||||
publicKey: key,
|
||||
),
|
||||
);
|
||||
context.maybePop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if (sshDisabled)
|
||||
Column(
|
||||
children: [
|
||||
const Divider(height: 0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InfoBox(
|
||||
text: 'ssh.ssh_disabled_warning'.tr(),
|
||||
isWarning: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
78
lib/ui/pages/users/user_details_page/user_details.dart
Normal file
78
lib/ui/pages/users/user_details_page/user_details.dart
Normal file
|
@ -0,0 +1,78 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:selfprivacy/logic/bloc/users/users_bloc.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
|
||||
import 'package:selfprivacy/ui/molecules/info_box/info_box.dart';
|
||||
import 'package:selfprivacy/ui/pages/users/user_details_page/widgets/widgets.dart';
|
||||
import 'package:selfprivacy/utils/ui_helpers.dart';
|
||||
|
||||
@RoutePage()
|
||||
class UserDetailsPage extends StatelessWidget {
|
||||
const UserDetailsPage({
|
||||
required this.login,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String login;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final ServerInstallationState config =
|
||||
context.watch<ServerInstallationCubit>().state;
|
||||
|
||||
final String domainName = UiHelpers.getDomainName(config);
|
||||
|
||||
final User user = context.watch<UsersBloc>().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,
|
||||
hasFlashButton: true,
|
||||
heroTitle: 'ssh.root_title'.tr(),
|
||||
heroSubtitle: 'ssh.root_subtitle'.tr(),
|
||||
children: [
|
||||
SshKeysCard(user: user),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return BrandHeroScreen(
|
||||
hasBackButton: true,
|
||||
hasFlashButton: true,
|
||||
heroTitle: user.login,
|
||||
heroIconWidget: CircleAvatar(
|
||||
backgroundColor: user.color,
|
||||
child: Text(
|
||||
user.login[0].toUpperCase(),
|
||||
),
|
||||
),
|
||||
children: [
|
||||
UserLoginTile(user: user, domainName: domainName),
|
||||
const Gap(8),
|
||||
SshKeysCard(user: user),
|
||||
const Gap(8),
|
||||
ResetPasswordTile(user: user),
|
||||
const Gap(8),
|
||||
if (user.type == UserType.normal) DeleteUserTile(user: user),
|
||||
const Divider(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: InfoBox(
|
||||
text: 'users.no_ssh_notice'.tr(),
|
||||
isWarning: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/logic/models/job.dart';
|
||||
|
||||
class DeleteUserTile extends StatelessWidget {
|
||||
const DeleteUserTile({
|
||||
required this.user,
|
||||
super.key,
|
||||
});
|
||||
|
||||
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,
|
||||
// useRootNavigator: false,
|
||||
builder: (final BuildContext context) => AlertDialog(
|
||||
title: Text('basis.confirmation'.tr()),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'users.delete_confirm_question'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('basis.cancel'.tr()),
|
||||
onPressed: () {
|
||||
context.router.maybePop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'basis.delete'.tr(),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<JobsCubit>().addJob(DeleteUserJob(user: user));
|
||||
context.router.childControllers.first.maybePop();
|
||||
context.router.maybePop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
leading: const Icon(Icons.person_remove_outlined),
|
||||
title: Text(
|
||||
'users.delete_user'.tr(),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/cubit/forms/user/reset_password_bloc.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||
|
||||
class ResetPasswordTile extends StatelessWidget {
|
||||
const ResetPasswordTile({required this.user, super.key});
|
||||
|
||||
final User user;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => BlocProvider<ResetPasswordBloc>(
|
||||
create: (final BuildContext context) => ResetPasswordBloc(user: user),
|
||||
child: BlocConsumer<ResetPasswordBloc, ResetPasswordState>(
|
||||
listener:
|
||||
(final BuildContext context, final ResetPasswordState state) {
|
||||
/// check on error
|
||||
if (state.errorMessage != '') {
|
||||
getIt<NavigationService>().showSnackBar(state.errorMessage);
|
||||
}
|
||||
|
||||
/// check on success
|
||||
if (state.passwordResetLink != null) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (final BuildContext context) =>
|
||||
_ResetPasswordLinkDialog(state: state),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder:
|
||||
(final BuildContext context, final ResetPasswordState state) =>
|
||||
_Tile(state: state),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ResetPasswordLinkDialog extends StatelessWidget {
|
||||
const _ResetPasswordLinkDialog({required this.state});
|
||||
final ResetPasswordState state;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => AlertDialog(
|
||||
title: Text(state.passwordResetMessage),
|
||||
content: Text(
|
||||
state.passwordResetLink.toString(),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('basis.copy'.tr()),
|
||||
onPressed: () {
|
||||
PlatformAdapter.setClipboard(state.passwordResetLink.toString());
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
'basis.copied_to_clipboard'.tr(),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
'basis.close'.tr(),
|
||||
),
|
||||
onPressed: () {
|
||||
context.router.maybePop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _Tile extends StatelessWidget {
|
||||
const _Tile({required this.state});
|
||||
final ResetPasswordState state;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => ListTile(
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
onTap: state.isLoading
|
||||
? null
|
||||
: () {
|
||||
context
|
||||
.read<ResetPasswordBloc>()
|
||||
.add(const RequestNewPassword());
|
||||
},
|
||||
leading: const Icon(Icons.lock_reset_outlined),
|
||||
trailing: state.isLoading ? const CircularProgressIndicator() : null,
|
||||
title: Text(
|
||||
'users.request_password_reset_link'.tr(),
|
||||
),
|
||||
);
|
||||
}
|
155
lib/ui/pages/users/user_details_page/widgets/ssh_key_card.dart
Normal file
155
lib/ui/pages/users/user_details_page/widgets/ssh_key_card.dart
Normal file
|
@ -0,0 +1,155 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
|
||||
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/logic/models/job.dart';
|
||||
import 'package:selfprivacy/ui/atoms/cards/filled_card.dart';
|
||||
import 'package:selfprivacy/ui/atoms/list_tiles/list_tile_on_surface_variant.dart';
|
||||
import 'package:selfprivacy/ui/molecules/info_box/info_box.dart';
|
||||
import 'package:selfprivacy/ui/organisms/modals/new_ssh_key_modal.dart';
|
||||
|
||||
class SshKeysCard extends StatelessWidget {
|
||||
const SshKeysCard({required this.user, super.key});
|
||||
|
||||
final User user;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final serverDetailsState = context.watch<ServerDetailsCubit>().state;
|
||||
final bool sshDisabled =
|
||||
serverDetailsState is Loaded && !serverDetailsState.sshSettings.enable;
|
||||
|
||||
return FilledCard(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTileOnSurfaceVariant(
|
||||
title: 'ssh.title'.tr(),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
ListTileOnSurfaceVariant(
|
||||
title: 'ssh.create'.tr(),
|
||||
leadingIcon: Icons.add_circle_outline,
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (final BuildContext context) =>
|
||||
DraggableScrollableSheet(
|
||||
expand: false,
|
||||
maxChildSize: 0.9,
|
||||
minChildSize: 0.3,
|
||||
initialChildSize: 0.5,
|
||||
builder: (final context, final scrollController) =>
|
||||
NewSshKeyModal(
|
||||
user: user,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Column(
|
||||
children: user.sshKeys.map((final String fullKey) {
|
||||
final keyParts = fullKey.split(' ');
|
||||
final keyType = keyParts[0];
|
||||
final publicKey = keyParts.length > 1 ? keyParts[1] : fullKey;
|
||||
final keyName =
|
||||
keyParts.length > 2 ? keyParts[2] : 'ssh.no_key_name'.tr();
|
||||
|
||||
return ListTileOnSurfaceVariant(
|
||||
title: '$keyName ($keyType)',
|
||||
disableSubtitleOverflow: true,
|
||||
// do not overflow text
|
||||
subtitle: publicKey,
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (final BuildContext context) =>
|
||||
_DeleteConfirmationDialog(
|
||||
fullKey: fullKey,
|
||||
keyType: keyType,
|
||||
publicKey: publicKey,
|
||||
keyName: keyName,
|
||||
user: user,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if (sshDisabled)
|
||||
Column(
|
||||
children: [
|
||||
const Divider(height: 0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InfoBox(
|
||||
text: 'ssh.ssh_disabled_warning'.tr(),
|
||||
isWarning: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteConfirmationDialog extends StatelessWidget {
|
||||
const _DeleteConfirmationDialog({
|
||||
required this.fullKey,
|
||||
required this.keyName,
|
||||
required this.keyType,
|
||||
required this.publicKey,
|
||||
required this.user,
|
||||
});
|
||||
|
||||
final String fullKey;
|
||||
final String keyName;
|
||||
final String keyType;
|
||||
final String publicKey;
|
||||
final User user;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => 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: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<JobsCubit>().addJob(
|
||||
DeleteSSHKeyJob(
|
||||
user: user,
|
||||
publicKey: fullKey,
|
||||
),
|
||||
);
|
||||
context.maybePop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/models/hive/user.dart';
|
||||
import 'package:selfprivacy/ui/atoms/cards/filled_card.dart';
|
||||
import 'package:selfprivacy/ui/atoms/list_tiles/list_tile_on_surface_variant.dart';
|
||||
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||
|
||||
class UserLoginTile extends StatelessWidget {
|
||||
const UserLoginTile({
|
||||
required this.user,
|
||||
required this.domainName,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final User user;
|
||||
final String domainName;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final email = '${user.login}@$domainName';
|
||||
|
||||
return FilledCard(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTileOnSurfaceVariant(
|
||||
onTap: () {
|
||||
PlatformAdapter.setClipboard(email);
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
'basis.copied_to_clipboard'.tr(),
|
||||
);
|
||||
},
|
||||
title: email,
|
||||
subtitle: 'users.email_login'.tr(),
|
||||
leadingIcon: Icons.alternate_email_outlined,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export 'delete_user_tile.dart';
|
||||
export 'reset_password_tile.dart';
|
||||
export 'ssh_key_card.dart';
|
||||
export 'user_login_tile.dart';
|
|
@ -33,7 +33,7 @@ import 'package:selfprivacy/ui/pages/services/services.dart';
|
|||
import 'package:selfprivacy/ui/pages/setup/initializing/initializing.dart';
|
||||
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
|
||||
import 'package:selfprivacy/ui/pages/users/new_user.dart';
|
||||
import 'package:selfprivacy/ui/pages/users/user_details.dart';
|
||||
import 'package:selfprivacy/ui/pages/users/user_details_page/user_details.dart';
|
||||
import 'package:selfprivacy/ui/pages/users/users.dart';
|
||||
|
||||
part 'router.gr.dart';
|
||||
|
|
|
@ -5,88 +5,90 @@ Random _rnd = Random.secure();
|
|||
typedef StringGeneratorFunction = String Function();
|
||||
|
||||
class StringGenerators {
|
||||
static const String letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
static const String numbers = '1234567890';
|
||||
static const String symbols = '_';
|
||||
static final List<String> letters = 'abcdefghijklmnopqrstuvwxyz'.split('');
|
||||
static final List<String> upperCaseLetters =
|
||||
'abcdefghijklmnopqrstuvwxyz'.toUpperCase().split('');
|
||||
static final List<String> numbers = '1234567890'.split('');
|
||||
static const List<String> symbols = ['_'];
|
||||
|
||||
static String getRandomString(
|
||||
final int length, {
|
||||
final hasLowercaseLetters = false,
|
||||
final hasUppercaseLetters = false,
|
||||
final hasNumbers = false,
|
||||
final hasSymbols = false,
|
||||
hasLowercaseLetters = false,
|
||||
hasUppercaseLetters = false,
|
||||
hasNumbers = false,
|
||||
hasSymbols = false,
|
||||
final isStrict = false,
|
||||
}) {
|
||||
String chars = '';
|
||||
|
||||
if (hasLowercaseLetters) {
|
||||
chars += letters;
|
||||
}
|
||||
|
||||
if (hasUppercaseLetters) {
|
||||
chars += letters.toUpperCase();
|
||||
}
|
||||
|
||||
if (hasNumbers) {
|
||||
chars += numbers;
|
||||
}
|
||||
|
||||
if (hasSymbols) {
|
||||
chars += symbols;
|
||||
}
|
||||
final List<String> chars = [
|
||||
if (hasLowercaseLetters) ...letters,
|
||||
if (hasUppercaseLetters) ...upperCaseLetters,
|
||||
if (hasNumbers) ...numbers,
|
||||
if (hasSymbols) ...symbols,
|
||||
];
|
||||
|
||||
assert(chars.isNotEmpty, 'chart empty');
|
||||
late List<String> res;
|
||||
|
||||
if (!isStrict) {
|
||||
return genString(length, chars);
|
||||
}
|
||||
res = genString(length, chars);
|
||||
} else {
|
||||
int sum = (hasLowercaseLetters ? 1 : 0) +
|
||||
(hasUppercaseLetters ? 1 : 0) +
|
||||
(hasNumbers ? 1 : 0) +
|
||||
(hasSymbols ? 1 : 0);
|
||||
|
||||
String res = '';
|
||||
int loose = length;
|
||||
if (hasLowercaseLetters) {
|
||||
loose -= 1;
|
||||
res += genString(1, letters);
|
||||
}
|
||||
if (hasUppercaseLetters) {
|
||||
loose -= 1;
|
||||
res += genString(1, letters.toUpperCase());
|
||||
}
|
||||
if (hasNumbers) {
|
||||
loose -= 1;
|
||||
res += genString(1, numbers.toUpperCase());
|
||||
}
|
||||
if (hasSymbols) {
|
||||
loose -= 1;
|
||||
res += genString(1, symbols);
|
||||
}
|
||||
res += genString(loose, chars);
|
||||
/// disable flags one by one when the len of generated string is less
|
||||
/// than count of flags
|
||||
/// so we wouldn't generate 4 len string when we need 2 or 3 symbols
|
||||
while (sum > length) {
|
||||
final int step = _rnd.nextInt(4);
|
||||
|
||||
final List<String> shuffledlist = res.split('')..shuffle();
|
||||
return shuffledlist.join();
|
||||
switch (step) {
|
||||
case 0:
|
||||
if (hasLowercaseLetters) {
|
||||
hasLowercaseLetters = false;
|
||||
sum--;
|
||||
continue;
|
||||
}
|
||||
case 1:
|
||||
if (hasUppercaseLetters) {
|
||||
hasUppercaseLetters = false;
|
||||
sum--;
|
||||
continue;
|
||||
}
|
||||
case 2:
|
||||
if (hasNumbers) {
|
||||
hasNumbers = false;
|
||||
sum--;
|
||||
continue;
|
||||
}
|
||||
case 3: // symbols
|
||||
if (hasSymbols) {
|
||||
hasSymbols = false;
|
||||
sum--;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res = [
|
||||
if (hasLowercaseLetters) ...genString(1, letters),
|
||||
if (hasUppercaseLetters) ...genString(1, upperCaseLetters),
|
||||
if (hasNumbers) ...genString(1, numbers),
|
||||
if (hasSymbols) ...genString(1, symbols),
|
||||
...genString(
|
||||
length - sum,
|
||||
chars,
|
||||
),
|
||||
];
|
||||
}
|
||||
res.shuffle();
|
||||
return res.join();
|
||||
}
|
||||
|
||||
static String genString(final int length, final String chars) =>
|
||||
String.fromCharCodes(
|
||||
Iterable.generate(
|
||||
length,
|
||||
(final _) => chars.codeUnitAt(
|
||||
_rnd.nextInt(chars.length),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
static StringGeneratorFunction userPassword = () => getRandomString(
|
||||
8,
|
||||
hasLowercaseLetters: true,
|
||||
hasUppercaseLetters: true,
|
||||
hasNumbers: true,
|
||||
isStrict: true,
|
||||
);
|
||||
|
||||
static StringGeneratorFunction passwordSalt = () => getRandomString(
|
||||
8,
|
||||
hasLowercaseLetters: true,
|
||||
);
|
||||
static List<String> genString(final int length, final List<String> chars) => [
|
||||
for (int i = 0; i < length; i++) chars[_rnd.nextInt(chars.length)],
|
||||
];
|
||||
|
||||
static StringGeneratorFunction simpleId = () => getRandomString(
|
||||
5,
|
||||
|
|
Loading…
Add table
Reference in a new issue