diff --git a/assets/translations/en.json b/assets/translations/en.json index 45faa3e8..73c3575e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -320,12 +320,13 @@ "delete_ssh_key": "Delete SSH key for {}" }, "validations": { - "required": "Required", - "invalid_format": "Invalid format", - "root_name": "User name cannot be 'root'", - "key_format": "Invalid key format", - "length": "Length is [] should be {}", - "user_already_exist": "Already exists", - "key_already_exists": "This key already exists" + "required": "Required.", + "invalid_format": "Invalid format.", + "root_name": "User name cannot be 'root'.", + "key_format": "Invalid key format.", + "length_not_equal": "Length is []. Should be {}.", + "length_longer": "Length is []. Should be shorter than or equal to {}.", + "user_already_exist": "This user already exists.", + "key_already_exists": "This key already exists." } } diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 619096a7..ee085ed4 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -323,9 +323,10 @@ "validations": { "required": "Обязательное поле.", "invalid_format": "Неверный формат.", - "root_name": "Имя пользователя не может быть'root'.", + "root_name": "Имя пользователя не может быть 'root'.", "key_format": "Неверный формат.", - "length": "Длина строки [] должна быть {}.", + "length_not_equal": "Длина строки []. Должно быть равно {}.", + "length_longer": "Длина строки []. Должно быть меньше либо равно {}.", "user_already_exist": "Имя уже используется.", "key_already_exists": "Этот ключ уже добавлен." } diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 0e8930e5..6302611e 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -18,7 +18,7 @@ class ApiResponse { final String? errorMessage; final D data; - get isSuccess => statusCode >= 200 && statusCode < 300; + bool get isSuccess => statusCode >= 200 && statusCode < 300; ApiResponse({ required this.statusCode, @@ -65,27 +65,47 @@ class ServerApi extends ApiMap { } Future> createUser(User user) async { - Response response; - var client = await getClient(); - // POST request with JSON body containing username and password - response = await client.post( - '/users', - data: { - 'username': user.login, - 'password': user.password, - }, - options: Options( - contentType: 'application/json', - ), - ); - - close(client); - - if (response.statusCode == HttpStatus.created) { + var makeErrorApiReponse = (int status) { return ApiResponse( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + statusCode: status, + data: User( + login: user.login, + password: user.password, + isFoundOnServer: false, + ), + ); + }; + + late Response response; + + try { + response = await client.post( + '/users', + data: { + 'username': user.login, + 'password': user.password, + }, + options: Options( + contentType: 'application/json', + receiveDataWhenStatusError: true, + followRedirects: false, + validateStatus: (status) { + return (status != null) && + (status < HttpStatus.internalServerError); + }), + ); + } catch (e) { + return makeErrorApiReponse(HttpStatus.internalServerError); + } finally { + close(client); + } + + if ((response.statusCode != null) && + (response.statusCode == HttpStatus.created)) { + return ApiResponse( + statusCode: response.statusCode!, data: User( login: user.login, password: user.password, @@ -93,18 +113,11 @@ class ServerApi extends ApiMap { ), ); } else { - return ApiResponse( - statusCode: response.statusCode ?? HttpStatus.internalServerError, - data: User( - login: user.login, - password: user.password, - isFoundOnServer: false, - note: response.data['message'] ?? null, - ), - errorMessage: response.data?.containsKey('error') ?? false - ? response.data['error'] - : null, - ); + print(response.statusCode.toString() + + ": " + + (response.statusMessage ?? "")); + return makeErrorApiReponse( + response.statusCode ?? HttpStatus.internalServerError); } } diff --git a/lib/logic/cubit/forms/factories/field_cubit_factory.dart b/lib/logic/cubit/forms/factories/field_cubit_factory.dart new file mode 100644 index 00000000..d3255a5f --- /dev/null +++ b/lib/logic/cubit/forms/factories/field_cubit_factory.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; + +class FieldCubitFactory { + FieldCubitFactory(this.context); + + /// A common user login field. + /// + /// - Available characters are lowercase a-z, digits and underscore _ + /// - Must start with either a-z or underscore + /// - Must be no longer than 'userMaxLength' characters + /// - Must not be empty + /// - Must not be a reserved root login + /// - Must be unique + FieldCubit createUserLoginField() { + final userAllowedRegExp = RegExp(r"^[a-z_][a-z0-9_]+$"); + const userMaxLength = 31; + return FieldCubit( + initalValue: '', + validations: [ + ValidationModel( + (s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()), + ValidationModel( + (login) => context.read().state.isLoginRegistered(login), + 'validations.user_already_exist'.tr(), + ), + RequiredStringValidation('validations.required'.tr()), + LengthStringLongerValidation(userMaxLength), + ValidationModel((s) => !userAllowedRegExp.hasMatch(s), + 'validations.invalid_format'.tr()), + ], + ); + } + + /// A common user password field. + /// + /// - Must fail on the regural expression of invalid matches: [\n\r\s]+ + /// - Must not be empty + FieldCubit createUserPasswordField() { + var passwordForbiddenRegExp = RegExp(r"[\n\r\s]+"); + return FieldCubit( + initalValue: '', + validations: [ + RequiredStringValidation('validations.required'.tr()), + ValidationModel( + (password) => passwordForbiddenRegExp.hasMatch(password), + 'validations.invalid_format'.tr()), + ], + ); + } + + final BuildContext context; +} diff --git a/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart index eda06939..d8777fa8 100644 --- a/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart @@ -12,9 +12,6 @@ class BackblazeFormCubit extends FormCubit { initalValue: '', validations: [ RequiredStringValidation('validations.required'.tr()), - //ValidationModel( - //(s) => regExp.hasMatch(s), 'invalid key format'), - //LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64') ], ); @@ -22,9 +19,6 @@ class BackblazeFormCubit extends FormCubit { initalValue: '', validations: [ RequiredStringValidation('required'), - //ValidationModel( - //(s) => regExp.hasMatch(s), 'invalid key format'), - //LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64') ], ); diff --git a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart index 7ee8c8fa..d811843b 100644 --- a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart @@ -4,8 +4,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; - -import '../validations/validations.dart'; +import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class CloudFlareFormCubit extends FormCubit { CloudFlareFormCubit(this.initializingCubit) { @@ -16,12 +15,7 @@ class CloudFlareFormCubit extends FormCubit { RequiredStringValidation('validations.required'.tr()), ValidationModel( (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), - LengthStringValidationWithLengthShowing( - 40, - 'validations.length'.tr( - args: ["40"], - ), - ) + LengthStringNotEqualValidation(40) ], ); diff --git a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart index 55af50d9..ce3e5aa9 100644 --- a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart @@ -4,8 +4,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; - -import '../validations/validations.dart'; +import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class HetznerFormCubit extends FormCubit { HetznerFormCubit(this.initializingCubit) { @@ -16,8 +15,7 @@ class HetznerFormCubit extends FormCubit { RequiredStringValidation('validations.required'.tr()), ValidationModel( (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), - LengthStringValidationWithLengthShowing( - 64, 'validations.length'.tr(args: ["64"])) + LengthStringNotEqualValidation(64) ], ); diff --git a/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart index 41b26582..102d7ac7 100644 --- a/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart @@ -2,33 +2,14 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/models/user.dart'; -import 'package:easy_localization/easy_localization.dart'; class RootUserFormCubit extends FormCubit { - RootUserFormCubit(this.initializingCubit) { - var userRegExp = RegExp(r"\W"); - var passwordRegExp = RegExp(r"[\n\r\s]+"); - - userName = FieldCubit( - initalValue: '', - validations: [ - ValidationModel( - (s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()), - RequiredStringValidation('validations.required'.tr()), - ValidationModel( - (s) => userRegExp.hasMatch(s), 'validations.invalid_format'.tr()), - ], - ); - - password = FieldCubit( - initalValue: '', - validations: [ - RequiredStringValidation('validations.required'.tr()), - ValidationModel((s) => passwordRegExp.hasMatch(s), - 'validations.invalid_format'.tr()), - ], - ); + RootUserFormCubit( + this.initializingCubit, final FieldCubitFactory fieldFactory) { + userName = fieldFactory.createUserLoginField(); + password = fieldFactory.createUserPasswordField(); isVisible = FieldCubit(initalValue: false); diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index 24f67437..b65cfb47 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; import 'package:selfprivacy/logic/models/user.dart'; @@ -10,38 +10,16 @@ import 'package:selfprivacy/utils/password_generator.dart'; class UserFormCubit extends FormCubit { UserFormCubit({ required this.jobsCubit, - required List users, + required FieldCubitFactory fieldFactory, User? user, }) { var isEdit = user != null; - var userRegExp = RegExp(r"\W"); - var passwordRegExp = RegExp(r"[\n\r\s]+"); - - login = FieldCubit( - initalValue: isEdit ? user!.login : '', - validations: [ - ValidationModel( - (s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()), - ValidationModel( - (login) => users.any((user) => user.login == login), - 'validations.user_already_exist'.tr(), - ), - RequiredStringValidation('validations.required'.tr()), - ValidationModel( - (s) => userRegExp.hasMatch(s), 'validations.invalid_format'.tr()), - ], - ); - - password = FieldCubit( - initalValue: - isEdit ? (user?.password ?? '') : StringGenerators.userPassword(), - validations: [ - RequiredStringValidation('validations.required'.tr()), - ValidationModel((s) => passwordRegExp.hasMatch(s), - 'validations.invalid_format'.tr()), - ], - ); + login = fieldFactory.createUserLoginField(); + login.setValue(isEdit ? user!.login : ''); + password = fieldFactory.createUserPasswordField(); + password.setValue( + isEdit ? (user?.password ?? '') : StringGenerators.userPassword()); super.addFields([login, password]); } diff --git a/lib/logic/cubit/forms/validations/validations.dart b/lib/logic/cubit/forms/validations/validations.dart index aff4ec92..91a8f75c 100644 --- a/lib/logic/cubit/forms/validations/validations.dart +++ b/lib/logic/cubit/forms/validations/validations.dart @@ -1,13 +1,28 @@ import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; -class LengthStringValidationWithLengthShowing extends ValidationModel { - LengthStringValidationWithLengthShowing(int length, String errorText) - : super((n) => n.length != length, errorText); +abstract class LengthStringValidation extends ValidationModel { + LengthStringValidation(bool Function(String) predicate, String errorMessage) + : super(predicate, errorMessage); @override - String? check(String val) { - var length = val.length; - var errorMassage = this.errorMassage.replaceAll("[]", length.toString()); - return test(val) ? errorMassage : null; + String? check(String value) { + var length = value.length; + var errorMessage = this.errorMassage.replaceAll("[]", length.toString()); + return test(value) ? errorMessage : null; } } + +class LengthStringNotEqualValidation extends LengthStringValidation { + /// String must be equal to [length] + LengthStringNotEqualValidation(int length) + : super((n) => n.length != length, + 'validations.length_not_equal'.tr(args: [length.toString()])); +} + +class LengthStringLongerValidation extends LengthStringValidation { + /// String must be shorter than or equal to [length] + LengthStringLongerValidation(int length) + : super((n) => n.length > length, + 'validations.length_longer'.tr(args: [length.toString()])); +} diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 65967a9a..0fd27064 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -160,7 +160,12 @@ class UsersCubit extends AppConfigDependendCubit { if (user.login == 'root' || user.login == state.primaryUser.login) { return; } + // If API returned error, do nothing final result = await api.createUser(user); + if (!result.isSuccess) { + return; + } + var loadedUsers = List.from(state.users); loadedUsers.add(result.data); await box.clear(); diff --git a/lib/logic/cubit/users/users_state.dart b/lib/logic/cubit/users/users_state.dart index 1ee1903f..d15789c9 100644 --- a/lib/logic/cubit/users/users_state.dart +++ b/lib/logic/cubit/users/users_state.dart @@ -22,5 +22,11 @@ class UsersState extends AppConfigDependendState { ); } + bool isLoginRegistered(String login) { + return users.any((user) => user.login == login) || + login == rootUser.login || + login == primaryUser.login; + } + bool get isEmpty => users.isEmpty; } diff --git a/lib/main.dart b/lib/main.dart index db3f3b96..e5af3656 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,8 +22,8 @@ void main() async { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); try { - /* Wakelock support for Linux - * desktop is not yet implemented */ + /// Wakelock support for Linux + /// desktop is not yet implemented await Wakelock.enable(); } on PlatformException catch (e) { print(e); diff --git a/lib/ui/pages/initializing/initializing.dart b/lib/ui/pages/initializing/initializing.dart index 95ba575c..d30569ca 100644 --- a/lib/ui/pages/initializing/initializing.dart +++ b/lib/ui/pages/initializing/initializing.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/backblaze_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/cloudflare_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/domain_cloudflare.dart'; @@ -352,7 +353,8 @@ class InitializingPage extends StatelessWidget { Widget _stepUser(AppConfigCubit initializingCubit) { return BlocProvider( - create: (context) => RootUserFormCubit(initializingCubit), + create: (context) => + RootUserFormCubit(initializingCubit, FieldCubitFactory(context)), child: Builder(builder: (context) { var formCubitState = context.watch().state; diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 4f6da178..58559c8f 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -24,7 +24,7 @@ class _NewUser extends StatelessWidget { } return UserFormCubit( jobsCubit: jobCubit, - users: users, + fieldFactory: FieldCubitFactory(context), ); }, child: Builder(builder: (context) { diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 72c519a0..013f65b0 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -6,6 +6,7 @@ import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_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/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';