Fix username validation and exception handling #89

Merged
NaiJi merged 3 commits from naiji-dev into master 2022-05-04 22:38:10 +03:00
16 changed files with 167 additions and 122 deletions

View File

@ -320,12 +320,13 @@
"delete_ssh_key": "Delete SSH key for {}" "delete_ssh_key": "Delete SSH key for {}"
}, },
"validations": { "validations": {
"required": "Required", "required": "Required.",
"invalid_format": "Invalid format", "invalid_format": "Invalid format.",
"root_name": "User name cannot be 'root'", "root_name": "User name cannot be 'root'.",
"key_format": "Invalid key format", "key_format": "Invalid key format.",
"length": "Length is [] should be {}", "length_not_equal": "Length is []. Should be {}.",
"user_already_exist": "Already exists", "length_longer": "Length is []. Should be shorter than or equal to {}.",
"key_already_exists": "This key already exists" "user_already_exist": "This user already exists.",
"key_already_exists": "This key already exists."
} }
} }

View File

@ -323,9 +323,10 @@
"validations": { "validations": {
"required": "Обязательное поле.", "required": "Обязательное поле.",
"invalid_format": "Неверный формат.", "invalid_format": "Неверный формат.",
"root_name": "Имя пользователя не может быть'root'.", "root_name": "Имя пользователя не может быть 'root'.",
"key_format": "Неверный формат.", "key_format": "Неверный формат.",
"length": "Длина строки [] должна быть {}.", "length_not_equal": "Длина строки []. Должно быть равно {}.",
"length_longer": "Длина строки []. Должно быть меньше либо равно {}.",
"user_already_exist": "Имя уже используется.", "user_already_exist": "Имя уже используется.",
"key_already_exists": "Этот ключ уже добавлен." "key_already_exists": "Этот ключ уже добавлен."
} }

View File

@ -18,7 +18,7 @@ class ApiResponse<D> {
final String? errorMessage; final String? errorMessage;
final D data; final D data;
get isSuccess => statusCode >= 200 && statusCode < 300; bool get isSuccess => statusCode >= 200 && statusCode < 300;
ApiResponse({ ApiResponse({
required this.statusCode, required this.statusCode,
@ -65,27 +65,47 @@ class ServerApi extends ApiMap {
} }
Future<ApiResponse<User>> createUser(User user) async { Future<ApiResponse<User>> createUser(User user) async {
Response response;
var client = await getClient(); var client = await getClient();
// POST request with JSON body containing username and password
response = await client.post( var makeErrorApiReponse = (int status) {
'/users',
data: {
'username': user.login,
'password': user.password,
},
options: Options(
contentType: 'application/json',
),
);
close(client);
if (response.statusCode == HttpStatus.created) {
return ApiResponse( return ApiResponse(
statusCode: response.statusCode ?? HttpStatus.internalServerError, statusCode: status,
data: User(
login: user.login,
password: user.password,
isFoundOnServer: false,
),
);
};
late Response<dynamic> 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( data: User(
login: user.login, login: user.login,
password: user.password, password: user.password,
@ -93,18 +113,11 @@ class ServerApi extends ApiMap {
), ),
); );
} else { } else {
return ApiResponse( print(response.statusCode.toString() +
statusCode: response.statusCode ?? HttpStatus.internalServerError, ": " +
data: User( (response.statusMessage ?? ""));
login: user.login, return makeErrorApiReponse(
password: user.password, response.statusCode ?? HttpStatus.internalServerError);
isFoundOnServer: false,
note: response.data['message'] ?? null,
),
errorMessage: response.data?.containsKey('error') ?? false
? response.data['error']
: null,
);
} }
} }

View File

@ -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<String> createUserLoginField() {
final userAllowedRegExp = RegExp(r"^[a-z_][a-z0-9_]+$");
const userMaxLength = 31;
return FieldCubit(
initalValue: '',
validations: [
ValidationModel<String>(
(s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()),
ValidationModel(
(login) => context.read<UsersCubit>().state.isLoginRegistered(login),
'validations.user_already_exist'.tr(),
),
RequiredStringValidation('validations.required'.tr()),
LengthStringLongerValidation(userMaxLength),
ValidationModel<String>((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<String> createUserPasswordField() {
var passwordForbiddenRegExp = RegExp(r"[\n\r\s]+");
return FieldCubit(
initalValue: '',
validations: [
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
(password) => passwordForbiddenRegExp.hasMatch(password),
'validations.invalid_format'.tr()),
],
);
}
final BuildContext context;
}

View File

@ -12,9 +12,6 @@ class BackblazeFormCubit extends FormCubit {
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('validations.required'.tr()), RequiredStringValidation('validations.required'.tr()),
//ValidationModel<String>(
//(s) => regExp.hasMatch(s), 'invalid key format'),
//LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64')
], ],
); );
@ -22,9 +19,6 @@ class BackblazeFormCubit extends FormCubit {
initalValue: '', initalValue: '',
validations: [ validations: [
RequiredStringValidation('required'), RequiredStringValidation('required'),
//ValidationModel<String>(
//(s) => regExp.hasMatch(s), 'invalid key format'),
//LegnthStringValidationWithLenghShowing(64, 'length is [] shoud be 64')
], ],
); );

View File

@ -4,8 +4,7 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
import '../validations/validations.dart';
class CloudFlareFormCubit extends FormCubit { class CloudFlareFormCubit extends FormCubit {
CloudFlareFormCubit(this.initializingCubit) { CloudFlareFormCubit(this.initializingCubit) {
@ -16,12 +15,7 @@ class CloudFlareFormCubit extends FormCubit {
RequiredStringValidation('validations.required'.tr()), RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>( ValidationModel<String>(
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()), (s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LengthStringValidationWithLengthShowing( LengthStringNotEqualValidation(40)
40,
'validations.length'.tr(
args: ["40"],
),
)
], ],
); );

View File

@ -4,8 +4,7 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/api_maps/hetzner.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
import '../validations/validations.dart';
class HetznerFormCubit extends FormCubit { class HetznerFormCubit extends FormCubit {
HetznerFormCubit(this.initializingCubit) { HetznerFormCubit(this.initializingCubit) {
@ -16,8 +15,7 @@ class HetznerFormCubit extends FormCubit {
RequiredStringValidation('validations.required'.tr()), RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>( ValidationModel<String>(
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()), (s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LengthStringValidationWithLengthShowing( LengthStringNotEqualValidation(64)
64, 'validations.length'.tr(args: ["64"]))
], ],
); );

View File

@ -2,33 +2,14 @@ import 'dart:async';
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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:selfprivacy/logic/models/user.dart';
import 'package:easy_localization/easy_localization.dart';
class RootUserFormCubit extends FormCubit { class RootUserFormCubit extends FormCubit {
RootUserFormCubit(this.initializingCubit) { RootUserFormCubit(
var userRegExp = RegExp(r"\W"); this.initializingCubit, final FieldCubitFactory fieldFactory) {
var passwordRegExp = RegExp(r"[\n\r\s]+"); userName = fieldFactory.createUserLoginField();
password = fieldFactory.createUserPasswordField();
userName = FieldCubit(
initalValue: '',
validations: [
ValidationModel<String>(
(s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()),
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>(
(s) => userRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
],
);
password = FieldCubit(
initalValue: '',
validations: [
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>((s) => passwordRegExp.hasMatch(s),
'validations.invalid_format'.tr()),
],
);
isVisible = FieldCubit(initalValue: false); isVisible = FieldCubit(initalValue: false);

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:cubit_form/cubit_form.dart'; 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/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/models/job.dart'; import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/logic/models/user.dart'; import 'package:selfprivacy/logic/models/user.dart';
@ -10,38 +10,16 @@ import 'package:selfprivacy/utils/password_generator.dart';
class UserFormCubit extends FormCubit { class UserFormCubit extends FormCubit {
UserFormCubit({ UserFormCubit({
required this.jobsCubit, required this.jobsCubit,
required List<User> users, required FieldCubitFactory fieldFactory,
User? user, User? user,
}) { }) {
var isEdit = user != null; var isEdit = user != null;
var userRegExp = RegExp(r"\W"); login = fieldFactory.createUserLoginField();
var passwordRegExp = RegExp(r"[\n\r\s]+"); login.setValue(isEdit ? user!.login : '');
password = fieldFactory.createUserPasswordField();
login = FieldCubit( password.setValue(
initalValue: isEdit ? user!.login : '', isEdit ? (user?.password ?? '') : StringGenerators.userPassword());
validations: [
ValidationModel<String>(
(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<String>(
(s) => userRegExp.hasMatch(s), 'validations.invalid_format'.tr()),
],
);
password = FieldCubit(
initalValue:
isEdit ? (user?.password ?? '') : StringGenerators.userPassword(),
validations: [
RequiredStringValidation('validations.required'.tr()),
ValidationModel<String>((s) => passwordRegExp.hasMatch(s),
'validations.invalid_format'.tr()),
],
);
super.addFields([login, password]); super.addFields([login, password]);
} }

View File

@ -1,13 +1,28 @@
import 'package:cubit_form/cubit_form.dart'; import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
class LengthStringValidationWithLengthShowing extends ValidationModel<String> { abstract class LengthStringValidation extends ValidationModel<String> {
LengthStringValidationWithLengthShowing(int length, String errorText) LengthStringValidation(bool Function(String) predicate, String errorMessage)
: super((n) => n.length != length, errorText); : super(predicate, errorMessage);
@override @override
String? check(String val) { String? check(String value) {
var length = val.length; var length = value.length;
var errorMassage = this.errorMassage.replaceAll("[]", length.toString()); var errorMessage = this.errorMassage.replaceAll("[]", length.toString());
return test(val) ? errorMassage : null; return test(value) ? errorMessage : null;
} }
} }
class LengthStringNotEqualValidation extends LengthStringValidation {
NaiJi marked this conversation as resolved
Review

This is a documentation comment. Consider making it docstring (three slashes) or removing it entirely, if the class name is enough.

IDE will use docstrings in intellisense. It won't use usual comments.

This is a documentation comment. Consider making it docstring (three slashes) or removing it entirely, if the class name is enough. IDE will use docstrings in intellisense. It won't use usual comments.
/// 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()]));
}

View File

@ -160,7 +160,12 @@ class UsersCubit extends AppConfigDependendCubit<UsersState> {
if (user.login == 'root' || user.login == state.primaryUser.login) { if (user.login == 'root' || user.login == state.primaryUser.login) {
return; return;
} }
// If API returned error, do nothing
final result = await api.createUser(user); final result = await api.createUser(user);
if (!result.isSuccess) {
return;
}
var loadedUsers = List<User>.from(state.users); var loadedUsers = List<User>.from(state.users);
loadedUsers.add(result.data); loadedUsers.add(result.data);
await box.clear(); await box.clear();

View File

@ -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; bool get isEmpty => users.isEmpty;
} }

View File

@ -22,8 +22,8 @@ void main() async {
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
try { try {
/* Wakelock support for Linux /// Wakelock support for Linux
* desktop is not yet implemented */ /// desktop is not yet implemented
await Wakelock.enable(); await Wakelock.enable();
} on PlatformException catch (e) { } on PlatformException catch (e) {
print(e); print(e);

View File

@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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/backblaze_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/initializing/cloudflare_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/cloudflare_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/initializing/domain_cloudflare.dart'; import 'package:selfprivacy/logic/cubit/forms/initializing/domain_cloudflare.dart';
@ -352,7 +353,8 @@ class InitializingPage extends StatelessWidget {
Widget _stepUser(AppConfigCubit initializingCubit) { Widget _stepUser(AppConfigCubit initializingCubit) {
return BlocProvider( return BlocProvider(
create: (context) => RootUserFormCubit(initializingCubit), create: (context) =>
RootUserFormCubit(initializingCubit, FieldCubitFactory(context)),
child: Builder(builder: (context) { child: Builder(builder: (context) {
var formCubitState = context.watch<RootUserFormCubit>().state; var formCubitState = context.watch<RootUserFormCubit>().state;

View File

@ -24,7 +24,7 @@ class _NewUser extends StatelessWidget {
} }
return UserFormCubit( return UserFormCubit(
jobsCubit: jobCubit, jobsCubit: jobCubit,
users: users, fieldFactory: FieldCubitFactory(context),
); );
}, },
child: Builder(builder: (context) { child: Builder(builder: (context) {

View File

@ -6,6 +6,7 @@ import 'package:selfprivacy/config/brand_colors.dart';
import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/config/text_themes.dart';
import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.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/forms/user/user_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';