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 {}"
},
"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."
}
}

View File

@ -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": "Этот ключ уже добавлен."
}

View File

@ -18,7 +18,7 @@ class ApiResponse<D> {
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<ApiResponse<User>> 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<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(
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);
}
}

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: '',
validations: [
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: '',
validations: [
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: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<String>(
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LengthStringValidationWithLengthShowing(
40,
'validations.length'.tr(
args: ["40"],
),
)
LengthStringNotEqualValidation(40)
],
);

View File

@ -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<String>(
(s) => regExp.hasMatch(s), 'validations.key_format'.tr()),
LengthStringValidationWithLengthShowing(
64, 'validations.length'.tr(args: ["64"]))
LengthStringNotEqualValidation(64)
],
);

View File

@ -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<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()),
],
);
RootUserFormCubit(
this.initializingCubit, final FieldCubitFactory fieldFactory) {
userName = fieldFactory.createUserLoginField();
password = fieldFactory.createUserPasswordField();
isVisible = FieldCubit(initalValue: false);

View File

@ -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<User> 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<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()),
],
);
login = fieldFactory.createUserLoginField();
login.setValue(isEdit ? user!.login : '');
password = fieldFactory.createUserPasswordField();
password.setValue(
isEdit ? (user?.password ?? '') : StringGenerators.userPassword());
super.addFields([login, password]);
}

View File

@ -1,13 +1,28 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
class LengthStringValidationWithLengthShowing extends ValidationModel<String> {
LengthStringValidationWithLengthShowing(int length, String errorText)
: super((n) => n.length != length, errorText);
abstract class LengthStringValidation extends ValidationModel<String> {
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 {
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) {
return;
}
// If API returned error, do nothing
final result = await api.createUser(user);
if (!result.isSuccess) {
return;
}
var loadedUsers = List<User>.from(state.users);
loadedUsers.add(result.data);
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;
}

View File

@ -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);

View File

@ -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<RootUserFormCubit>().state;

View File

@ -24,7 +24,7 @@ class _NewUser extends StatelessWidget {
}
return UserFormCubit(
jobsCubit: jobCubit,
users: users,
fieldFactory: FieldCubitFactory(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/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';