Fix username validation and exception handling #89
|
@ -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.
|
||||
NaiJi marked this conversation as resolved
Outdated
|
||||
///
|
||||
/// - 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.
|
||||
NaiJi marked this conversation as resolved
Outdated
inex
commented
Outdated
Review
Same, wrong docstring formatting Same, wrong docstring formatting
|
||||
///
|
||||
/// - 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 {
|
||||
NaiJi marked this conversation as resolved
inex
commented
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()]));
|
||||
}
|
||||
|
|
|
@ -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
Incorrect docstring formatting. Never use this style of comments.
https://dart.dev/guides/language/effective-dart/documentation#dont-use-block-comments-for-documentation
This also could have been a doctring.