mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-26 14:59:22 +00:00
Implement password policy for account creation
This commit is contained in:
parent
482e938744
commit
277c08048d
@ -16,9 +16,23 @@ class ServerConfigurationHelper {
|
|||||||
(await APIRequest.withoutLogin("server/config").execWithThrow())
|
(await APIRequest.withoutLogin("server/config").execWithThrow())
|
||||||
.getObject();
|
.getObject();
|
||||||
|
|
||||||
|
final passwordPolicy = response["password_policy"];
|
||||||
final dataConservationPolicy = response["data_conservation_policy"];
|
final dataConservationPolicy = response["data_conservation_policy"];
|
||||||
|
|
||||||
_config = ServerConfig(
|
_config = ServerConfig(
|
||||||
|
passwordPolicy: PasswordPolicy(
|
||||||
|
allowMailInPassword: passwordPolicy["allow_email_in_password"],
|
||||||
|
allowNameInPassword: passwordPolicy["allow_name_in_password"],
|
||||||
|
minPasswordLength: passwordPolicy["min_password_length"],
|
||||||
|
minNumberUpperCaseLetters:
|
||||||
|
passwordPolicy["min_number_upper_case_letters"],
|
||||||
|
minNumberLowerCaseLetters:
|
||||||
|
passwordPolicy["min_number_lower_case_letters"],
|
||||||
|
minNumberDigits: passwordPolicy["min_number_digits"],
|
||||||
|
minNumberSpecialCharacters:
|
||||||
|
passwordPolicy["min_number_special_characters"],
|
||||||
|
minCategoriesPresence: passwordPolicy["min_categories_presence"],
|
||||||
|
),
|
||||||
dataConservationPolicy: ServerDataConservationPolicy(
|
dataConservationPolicy: ServerDataConservationPolicy(
|
||||||
minInactiveAccountLifetime:
|
minInactiveAccountLifetime:
|
||||||
dataConservationPolicy["min_inactive_account_lifetime"],
|
dataConservationPolicy["min_inactive_account_lifetime"],
|
||||||
|
@ -4,6 +4,35 @@ import 'package:flutter/widgets.dart';
|
|||||||
///
|
///
|
||||||
/// @author Pierre Hubert
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class PasswordPolicy {
|
||||||
|
final bool allowMailInPassword;
|
||||||
|
final bool allowNameInPassword;
|
||||||
|
final int minPasswordLength;
|
||||||
|
final int minNumberUpperCaseLetters;
|
||||||
|
final int minNumberLowerCaseLetters;
|
||||||
|
final int minNumberDigits;
|
||||||
|
final int minNumberSpecialCharacters;
|
||||||
|
final int minCategoriesPresence;
|
||||||
|
|
||||||
|
const PasswordPolicy({
|
||||||
|
@required this.allowMailInPassword,
|
||||||
|
@required this.allowNameInPassword,
|
||||||
|
@required this.minPasswordLength,
|
||||||
|
@required this.minNumberUpperCaseLetters,
|
||||||
|
@required this.minNumberLowerCaseLetters,
|
||||||
|
@required this.minNumberDigits,
|
||||||
|
@required this.minNumberSpecialCharacters,
|
||||||
|
@required this.minCategoriesPresence,
|
||||||
|
}) : assert(allowMailInPassword != null),
|
||||||
|
assert(allowNameInPassword != null),
|
||||||
|
assert(minPasswordLength != null),
|
||||||
|
assert(minNumberUpperCaseLetters != null),
|
||||||
|
assert(minNumberLowerCaseLetters != null),
|
||||||
|
assert(minNumberDigits != null),
|
||||||
|
assert(minNumberSpecialCharacters != null),
|
||||||
|
assert(minCategoriesPresence != null);
|
||||||
|
}
|
||||||
|
|
||||||
class ServerDataConservationPolicy {
|
class ServerDataConservationPolicy {
|
||||||
final int minInactiveAccountLifetime;
|
final int minInactiveAccountLifetime;
|
||||||
final int minNotificationLifetime;
|
final int minNotificationLifetime;
|
||||||
@ -28,9 +57,12 @@ class ServerDataConservationPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ServerConfig {
|
class ServerConfig {
|
||||||
|
final PasswordPolicy passwordPolicy;
|
||||||
final ServerDataConservationPolicy dataConservationPolicy;
|
final ServerDataConservationPolicy dataConservationPolicy;
|
||||||
|
|
||||||
const ServerConfig({
|
const ServerConfig({
|
||||||
|
@required this.passwordPolicy,
|
||||||
@required this.dataConservationPolicy,
|
@required this.dataConservationPolicy,
|
||||||
}) : assert(dataConservationPolicy != null);
|
}) : assert(passwordPolicy != null),
|
||||||
|
assert(dataConservationPolicy != null);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import 'package:comunic/helpers/account_helper.dart';
|
import 'package:comunic/helpers/account_helper.dart';
|
||||||
|
import 'package:comunic/helpers/server_config_helper.dart';
|
||||||
import 'package:comunic/models/config.dart';
|
import 'package:comunic/models/config.dart';
|
||||||
import 'package:comunic/models/new_account.dart';
|
import 'package:comunic/models/new_account.dart';
|
||||||
|
import 'package:comunic/ui/widgets/async_screen_widget.dart';
|
||||||
|
import 'package:comunic/ui/widgets/new_password_input_widget.dart';
|
||||||
import 'package:comunic/utils/input_utils.dart';
|
import 'package:comunic/utils/input_utils.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
import 'package:comunic/utils/ui_utils.dart';
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
@ -34,7 +37,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
|
|||||||
final _firstNameController = TextEditingController();
|
final _firstNameController = TextEditingController();
|
||||||
final _lastNameController = TextEditingController();
|
final _lastNameController = TextEditingController();
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
final _passwordInputKey = GlobalKey<NewPasswordInputWidgetState>();
|
||||||
final _verifyPasswordController = TextEditingController();
|
final _verifyPasswordController = TextEditingController();
|
||||||
bool _acceptedTOS = false;
|
bool _acceptedTOS = false;
|
||||||
|
|
||||||
@ -49,10 +52,11 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
|
|||||||
|
|
||||||
bool get _isEmailValid => validateEmail(_emailController.text);
|
bool get _isEmailValid => validateEmail(_emailController.text);
|
||||||
|
|
||||||
bool get _isPasswordValid => _passwordController.text.length > 3;
|
bool get _isPasswordValid => _passwordInputKey.currentState.valid;
|
||||||
|
|
||||||
bool get _isPasswordConfirmationValid =>
|
bool get _isPasswordConfirmationValid =>
|
||||||
_passwordController.text == _verifyPasswordController.text;
|
_passwordInputKey.currentState != null &&
|
||||||
|
_passwordInputKey.currentState.value == _verifyPasswordController.text;
|
||||||
|
|
||||||
bool get _isFormValid =>
|
bool get _isFormValid =>
|
||||||
_isFirstNameValid &&
|
_isFirstNameValid &&
|
||||||
@ -69,10 +73,16 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
|
|||||||
? tr(
|
? tr(
|
||||||
"Too many accounts have been created from this IP address for now. Please try again later.")
|
"Too many accounts have been created from this IP address for now. Please try again later.")
|
||||||
: tr(
|
: tr(
|
||||||
"An error occured while creating your account. Please try again.");
|
"An error occurred while creating your account. Please try again.");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => AsyncScreenWidget(
|
||||||
|
onReload: () => ServerConfigurationHelper.ensureLoaded(),
|
||||||
|
onBuild: () => _buildForm(),
|
||||||
|
errorMessage: tr("Failed to load server configuration !"),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _buildForm() {
|
||||||
if (_isCreating)
|
if (_isCreating)
|
||||||
return Center(
|
return Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
@ -124,15 +134,14 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Password
|
// Password
|
||||||
_InputEntry(
|
NewPasswordInputWidget(
|
||||||
controller: _passwordController,
|
key: _passwordInputKey,
|
||||||
label: tr("Password"),
|
label: tr("Password"),
|
||||||
onEdited: _updateUI,
|
onEdited: _updateUI,
|
||||||
icon: Icon(Icons.lock),
|
icon: Icon(Icons.lock),
|
||||||
isPassword: true,
|
email: _emailController.text,
|
||||||
error: _showErrors && !_isPasswordValid
|
firstName: _firstNameController.text,
|
||||||
? tr("Invalid password!")
|
lastName: _lastNameController.text,
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Verify password
|
// Verify password
|
||||||
@ -206,7 +215,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
|
|||||||
firstName: _firstNameController.text,
|
firstName: _firstNameController.text,
|
||||||
lastName: _lastNameController.text,
|
lastName: _lastNameController.text,
|
||||||
email: _emailController.text,
|
email: _emailController.text,
|
||||||
password: _passwordController.text,
|
password: _passwordInputKey.currentState.value,
|
||||||
));
|
));
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
164
lib/ui/widgets/new_password_input_widget.dart
Normal file
164
lib/ui/widgets/new_password_input_widget.dart
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import 'package:comunic/helpers/server_config_helper.dart';
|
||||||
|
import 'package:comunic/models/server_config.dart';
|
||||||
|
import 'package:comunic/models/user.dart';
|
||||||
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// New password input widget
|
||||||
|
///
|
||||||
|
/// @author Pierre Hubert
|
||||||
|
|
||||||
|
class NewPasswordInputWidget extends StatefulWidget {
|
||||||
|
final Widget icon;
|
||||||
|
final VoidCallback onEdited;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
final User user;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
|
final String email;
|
||||||
|
|
||||||
|
const NewPasswordInputWidget({
|
||||||
|
Key key,
|
||||||
|
this.icon,
|
||||||
|
this.onEdited,
|
||||||
|
@required this.label,
|
||||||
|
this.user,
|
||||||
|
this.firstName,
|
||||||
|
this.lastName,
|
||||||
|
this.email,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
NewPasswordInputWidgetState createState() => NewPasswordInputWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewPasswordInputWidgetState extends State<NewPasswordInputWidget> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
bool _ready = false;
|
||||||
|
|
||||||
|
String get value => _controller.text;
|
||||||
|
|
||||||
|
bool get valid => _ready && value.isNotEmpty && (_errorMessage ?? "").isEmpty;
|
||||||
|
|
||||||
|
PasswordPolicy get _policy => ServerConfigurationHelper.config.passwordPolicy;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure server configuration is loaded
|
||||||
|
void _init() async {
|
||||||
|
try {
|
||||||
|
await ServerConfigurationHelper.ensureLoaded();
|
||||||
|
setState(() => _ready = true);
|
||||||
|
} catch (e, s) {
|
||||||
|
print("Failed to initialize NewPasswordInputWidget! : $e - $s");
|
||||||
|
showSimpleSnack(context, tr("Failed to retrieve server configuration!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant NewPasswordInputWidget oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => TextField(
|
||||||
|
controller: _controller,
|
||||||
|
obscureText: true,
|
||||||
|
onChanged: (s) => _onChanged(),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
errorText: _errorMessage,
|
||||||
|
errorMaxLines: 3,
|
||||||
|
icon: widget.icon,
|
||||||
|
labelText: widget.label,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _onChanged() {
|
||||||
|
setState(() {});
|
||||||
|
if (widget.onEdited != null) widget.onEdited();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate an error message associated with current password
|
||||||
|
String get _errorMessage {
|
||||||
|
if (!_ready || value.isEmpty) return null;
|
||||||
|
|
||||||
|
// Mandatory checks
|
||||||
|
if (!_policy.allowMailInPassword &&
|
||||||
|
(widget.email ?? "").isNotEmpty &&
|
||||||
|
(widget.email.toLowerCase().contains(value.toLowerCase()) ||
|
||||||
|
value.toLowerCase().contains(widget.email.toLowerCase()))) {
|
||||||
|
return tr("Your password must not contains part of your email address!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_policy.allowMailInPassword &&
|
||||||
|
(widget.email ?? "").isNotEmpty &&
|
||||||
|
(widget.email.toLowerCase().contains(value.toLowerCase()) ||
|
||||||
|
value.toLowerCase().contains(widget.email.toLowerCase()))) {
|
||||||
|
return tr("Your password must not contains part of your email address!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_policy.allowNameInPassword &&
|
||||||
|
(widget.firstName ?? "").isNotEmpty &&
|
||||||
|
value.toLowerCase().contains(widget.firstName.toLowerCase())) {
|
||||||
|
return tr("Your password must not contains your first name!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_policy.allowNameInPassword &&
|
||||||
|
(widget.lastName ?? "").isNotEmpty &&
|
||||||
|
value.toLowerCase().contains(widget.lastName.toLowerCase())) {
|
||||||
|
return tr("Your password must not contains your last name!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_policy.minPasswordLength > value.length) {
|
||||||
|
return tr(
|
||||||
|
"Your password must be composed of at least %num% characters!",
|
||||||
|
args: {
|
||||||
|
"num": _policy.minPasswordLength.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Characteristics check
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
if (_hasCharacteristic(RegExp(r'[A-Z]'), _policy.minNumberUpperCaseLetters))
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (_hasCharacteristic(RegExp(r'[a-z]'), _policy.minNumberLowerCaseLetters))
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (_hasCharacteristic(RegExp(r'[0-9]'), _policy.minNumberDigits)) count++;
|
||||||
|
|
||||||
|
if (_hasCharacteristic(
|
||||||
|
RegExp(r'[^A-Za-z0-9]'), _policy.minNumberSpecialCharacters)) count++;
|
||||||
|
|
||||||
|
if (count >= _policy.minCategoriesPresence) return null;
|
||||||
|
|
||||||
|
return tr(
|
||||||
|
"Your password must contains characters of at least %num% of the following categories : %upper% upper case letter, %lower% lowercase letter, %digit% digit, %special% special character.",
|
||||||
|
args: {
|
||||||
|
"num": (_policy.minCategoriesPresence == 4
|
||||||
|
? tr("ALL")
|
||||||
|
: _policy.minCategoriesPresence.toString()),
|
||||||
|
"upper": _policy.minNumberUpperCaseLetters.toString(),
|
||||||
|
"lower": _policy.minNumberLowerCaseLetters.toString(),
|
||||||
|
"digit": _policy.minNumberDigits.toString(),
|
||||||
|
"special": _policy.minNumberSpecialCharacters.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasCharacteristic(RegExp exp, int requiredCount) {
|
||||||
|
if (requiredCount < 1) return true;
|
||||||
|
|
||||||
|
return exp.allMatches(value).length >= requiredCount;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user