Merge pull request 'Fix username validation and exception handling' (#89) from naiji-dev into master

Reviewed-on: https://git.selfprivacy.org/kherel/selfprivacy.org.app/pulls/89
This commit is contained in:
NaiJi 2022-05-04 22:38:09 +03:00
commit c4ae2b3b4f
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 {
/// 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';