forked from SelfPrivacy/selfprivacy.org.app
Merge pull request 'Fix username validation and exception handling' (#89) from naiji-dev into master
Reviewed-on: kherel/selfprivacy.org.app#89
commit
c4ae2b3b4f
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": "Этот ключ уже добавлен."
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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')
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -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)
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -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)
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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()]));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class _NewUser extends StatelessWidget {
|
|||
}
|
||||
return UserFormCubit(
|
||||
jobsCubit: jobCubit,
|
||||
users: users,
|
||||
fieldFactory: FieldCubitFactory(context),
|
||||
);
|
||||
},
|
||||
child: Builder(builder: (context) {
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue