From a2fe4458a6d1d3e9b61b0588def28e23fc693e3d Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 10 Dec 2024 16:20:04 +0100 Subject: [PATCH] Password rules enhancement (#492) * update new rules, add visual indicators and more * update * adding check backend side for format * Update routes/users.js * add eye on login and profile page * fix a bug --------- Co-authored-by: mboudet --- CHANGELOG.md | 10 ++ bin/my-accounts.js | 2 +- core/tps.service.js | 2 +- core/user.service.js | 4 +- core/user_db.service.js | 2 +- .../src/app/auth/login/login.component.css | 34 ++++- .../src/app/auth/login/login.component.html | 49 ++++++-- .../src/app/auth/login/login.component.ts | 5 + manager2/src/app/user/user.component.css | 68 ++++++++++ manager2/src/app/user/user.component.html | 117 +++++++++++++++++- manager2/src/app/user/user.component.ts | 116 ++++++++++++++--- manager2/src/tsconfig.app.json | 2 +- routes/users.js | 6 + 13 files changed, 378 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e639052a2..83dbfc34f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,16 @@ ## 1.4.30 (2024-08-02) +* Password enhancement + * Add an eye for visible/hidden on the password field in login/profile page + * Disable "update password" button until both fields are entered on profile page + * Centered input fields + * Added dynamic rules requirements in order for user to know what to add to his password + * Password rules is 12 char, with 1 spec char, 1 digit and no spaces + * Updated generated password via generate-password to 12 when account activated + * Updated also db user password generated to 12 + * Added a red cross/green tick in the input for passwork and password confirmation + * Added a `project.terms_and_conditions_hds` setting in the config: If true: "Ask Admin" button to submit project creation form became available after 'project name' and 'expiration date' are filled, 'project description' is at least 30 char and a checkbox appears and 'terms and conditions hds' should also be checked diff --git a/bin/my-accounts.js b/bin/my-accounts.js index fe21fdb92..0f226d578 100644 --- a/bin/my-accounts.js +++ b/bin/my-accounts.js @@ -78,7 +78,7 @@ program process.exit(1); } - let new_password = usrv.new_password(8); + let new_password = usrv.new_password(16); user.password = new_password; let fid = new Date().getTime(); try { diff --git a/core/tps.service.js b/core/tps.service.js index b7ff0ecbe..99d461284 100644 --- a/core/tps.service.js +++ b/core/tps.service.js @@ -146,7 +146,7 @@ async function create_tp_users_db (owner, quantity, duration, end_date, userGrou expiration: end_date + 1000*3600*24*(duration+CONFIG.tp.extra_expiration), }; user = await usrsrv.create_user(user); - user.password = usrsrv.new_password(10); + user.password = usrsrv.new_password(16); await usrsrv.activate_user(user); users.push(user); diff --git a/core/user.service.js b/core/user.service.js index 054189188..ac5231d49 100644 --- a/core/user.service.js +++ b/core/user.service.js @@ -85,7 +85,7 @@ function get_user_home(user) { async function activate_user(user, action_owner = 'auto') { if (!user.password) { - user.password = new_password(10); + user.password = new_password(16); //user.password = Math.random().toString(36).slice(-10); } if (!user.created_at) { @@ -217,7 +217,7 @@ async function create_user(user, action_owner = 'auto') { // todo should be factorysed with "normal" user creation async function create_extra_user(user_name, group, internal_user){ //let password = Math.random().toString(36).slice(-10); - let password = new_password(10); + let password = new_password(16); if(process.env.MY_ADMIN_PASSWORD){ password = process.env.MY_ADMIN_PASSWORD; } diff --git a/core/user_db.service.js b/core/user_db.service.js index a653afe88..4e2fe3a34 100644 --- a/core/user_db.service.js +++ b/core/user_db.service.js @@ -104,7 +104,7 @@ async function create_db(new_db, user, id) { } //let password = Math.random().toString(36).slice(-10); - let password = usrsrv.new_password(10); + let password = usrsrv.new_password(16); let createuser = `CREATE USER '${id}'@'%' IDENTIFIED BY '${password}';\n`; try { await querydb(createuser); diff --git a/manager2/src/app/auth/login/login.component.css b/manager2/src/app/auth/login/login.component.css index 84d878c66..f925a74de 100644 --- a/manager2/src/app/auth/login/login.component.css +++ b/manager2/src/app/auth/login/login.component.css @@ -27,13 +27,14 @@ z-index: 2; } -.form-signin input[type="text"] +#sign_user_id { margin-bottom: -1px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; } -.form-signin input[type="password"] + +#sign_password { margin-bottom: 10px; border-top-left-radius: 0; @@ -46,4 +47,31 @@ font-size: 18px; font-weight: 400; display: block; -} \ No newline at end of file +} + +.password-field { + position: relative; +} + +.password-field .password-input { + width: 100%; + padding-right: 2rem; +} + +.password-field .toggle-password { + position: absolute; + top: 50%; + right: 1rem; + transform: translateY(-50%); + cursor: pointer; + color: #aaa; +} + +.password-field .toggle-password:hover { + color: #333; +} + +/* Prevent icon from disappearing when input is focused */ +.password-field:focus-within .toggle-password { + z-index: 9; + } diff --git a/manager2/src/app/auth/login/login.component.html b/manager2/src/app/auth/login/login.component.html index c2a2189c3..f1b50905f 100644 --- a/manager2/src/app/auth/login/login.component.html +++ b/manager2/src/app/auth/login/login.component.html @@ -9,20 +9,53 @@

Sign in to access your account
diff --git a/manager2/src/app/auth/login/login.component.ts b/manager2/src/app/auth/login/login.component.ts index 989ed6be8..b5088a8d5 100644 --- a/manager2/src/app/auth/login/login.component.ts +++ b/manager2/src/app/auth/login/login.component.ts @@ -29,6 +29,7 @@ export class LoginComponent implements OnInit { msg: string error_msg: string msgstatus: number + passwordVisible: boolean = false u2f: any uid: string @@ -170,4 +171,8 @@ export class LoginComponent implements OnInit { }); } + togglePasswordVisibility() { + this.passwordVisible = !this.passwordVisible; + } + } diff --git a/manager2/src/app/user/user.component.css b/manager2/src/app/user/user.component.css index e69de29bb..c420312bb 100644 --- a/manager2/src/app/user/user.component.css +++ b/manager2/src/app/user/user.component.css @@ -0,0 +1,68 @@ + +.valid-indicator { + color: green; +} + +.invalid-indicator { + color: red; +} + +.invalid-feedback { + color: red; +} + +.password-rules { + margin-top: 10px; +} + +.rule-satisfied { + color: green; +} + +.rule-not-satisfied { + color: red; +} + +.btn-disabled { + background-color: #d6d6d6; + border-color: #d6d6d6; + cursor: not-allowed; + pointer-events: none; + color: #888888; +} + +.btn-disabled:hover { + background-color: #d6d6d6; + border-color: #d6d6d6; +} + +.password-field { + position: relative; +} + +.password-field .password-input { + width: 100%; + padding-right: 2rem; +} + +.password-field .toggle-password { + position: absolute; + top: 50%; + right: 1rem; + transform: translateY(-50%); + cursor: pointer; + color: #aaa; +} + +.password-field:has(.input-group-append .valid-indicator, .input-group-append .invalid-indicator) .toggle-password { + right: 3rem; +} + +.password-field .toggle-password:hover { + color: #333; +} + +/* Prevent icon from disappearing when input is focused */ +.password-field:focus-within .toggle-password { + z-index: 9; + } diff --git a/manager2/src/app/user/user.component.html b/manager2/src/app/user/user.component.html index 153a28866..b885917f7 100644 --- a/manager2/src/app/user/user.component.html +++ b/manager2/src/app/user/user.component.html @@ -326,7 +326,6 @@

Update password

{{update_passwd}}
-
{{wrong_confirm_passwd}}
@@ -334,16 +333,122 @@

Update password

-
+
- +
+ +
+ + + ✓ + + + ✗ + +
+
+
+

Password rules requirements:

+
+ - 12 characters minimum +
+
+ - At least one digit +
+
+ - At least one lowercase letter +
+
+ - At least one uppercase letter +
+
+ - At least one special character +
+
+ - No spaces allowed +
+
-
+ +
- +
+ +
+ + + ✓ + + + ✗ + +
+
- + +
diff --git a/manager2/src/app/user/user.component.ts b/manager2/src/app/user/user.component.ts index 6e81648eb..8a6997a39 100644 --- a/manager2/src/app/user/user.component.ts +++ b/manager2/src/app/user/user.component.ts @@ -15,6 +15,7 @@ import { FlashMessagesService } from '../utils/flash/flash.component'; import { solveRegistrationChallenge } from '@webauthn/client'; +import { NgModel } from '@angular/forms' /* function _window() : any { @@ -151,10 +152,20 @@ export class UserComponent implements OnInit { panel: number = 0; // Password mngt - wrong_confirm_passwd: string update_passwd: string - password1: string - password2: string + password1: string = '' + password2: string = '' + + passwordVisible: boolean = false + passwordConfirmVisible: boolean = false + + // Flags for password rules + passwordLengthValid: boolean = false; + hasDigit: boolean = false; + hasLowercase: boolean = false; + hasUppercase: boolean = false; + hasSpecialChar: boolean = false; + hasSpaces: boolean = false; // Error messages msg: string @@ -618,23 +629,96 @@ export class UserComponent implements OnInit { ) } - update_password() { - this.wrong_confirm_passwd = ""; - this.update_passwd = ""; - if ((this.password1 != this.password2) || (this.password1 == "")) { - this.wrong_confirm_passwd = "Passwords are not identical"; - return; + update_password(password1Model: NgModel, password2Model: NgModel) { + this.validateInput(password1Model) + this.validateInput(password2Model) + + const passwordErrors = this.validatePassword(this.password1) + const confirmPasswordErrors = this.password1 !== this.password2 ? { mustMatch: true } : null + + password1Model.control.setErrors(passwordErrors) + password2Model.control.setErrors(confirmPasswordErrors) + + if (!passwordErrors && !confirmPasswordErrors) { + this.userService.updatePassword(this.user.uid, this.password1).subscribe( + resp => this.update_passwd = resp['message'], + err => console.log('failed to update password', err) + ) + } else { + console.log('Passwords are invalid or do not match.') + } + } + + validateInput(model: NgModel) { + model.control.markAsTouched() + model.control.markAsDirty() + model.control.updateValueAndValidity() + } + + checkPasswordRules(password: string) { + this.passwordLengthValid = password.length >= 12 + this.hasDigit = /[0-9]/.test(password) + this.hasLowercase = /[a-z]/.test(password) + this.hasUppercase = /[A-Z]/.test(password) + this.hasSpecialChar = /[\W_]/.test(password) + this.hasSpaces = / /.test(password) + } + + validatePassword(password: string) { + if (!password) { + return { required: true } } - if (this.password1.length < 10) { - this.wrong_confirm_passwd = "Password must have 10 characters minimum"; - return; + + const errors: any = {}; + if (password.length < 12) { + errors.minlength = { requiredLength: 12, actualLength: password.length } } - this.userService.updatePassword(this.user.uid, this.password1).subscribe( - resp => this.update_passwd = resp['message'], - err => console.log('failed to update password') - ); + if (!/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_])(?!.* ).{12,}$/.test(password)) { + errors.pattern = { requiredPattern: 'at least one digit, one lowercase letter, one uppercase letter, one special character, and no spaces' } + } + + return Object.keys(errors).length ? errors : null + } + + isFormValid(): boolean { + return this.passwordLengthValid && + this.hasDigit && + this.hasLowercase && + this.hasUppercase && + this.hasSpecialChar && + !this.hasSpaces; } + checkPasswordMatch(password1Model: NgModel, password2Model: NgModel) { + if (this.password1 !== this.password2) { + password2Model.control.setErrors({ mustMatch: true }); + } else { + password2Model.control.setErrors(null); // Clear error if passwords match + } + } + + togglePasswordVisibility(field: number): void { + if (field === 1) { + this.passwordVisible = !this.passwordVisible; + } else if (field === 2) { + this.passwordConfirmVisible = !this.passwordConfirmVisible; + } + } + + // update_password() { + // this.wrong_confirm_passwd = ""; + // this.update_passwd = ""; + // if ((this.password1 != this.password2) || (this.password1 == "")) { + // this.wrong_confirm_passwd = "Passwords are not identical"; + // return; + // } + // if (this.password1.length < 10) { + // this.wrong_confirm_passwd = "Password must have 10 characters minimum"; + // return; + // } + + // } + update_info() { this.update_msg = ''; this.update_error_msg = ''; diff --git a/manager2/src/tsconfig.app.json b/manager2/src/tsconfig.app.json index f3a1b8018..c42b60bea 100644 --- a/manager2/src/tsconfig.app.json +++ b/manager2/src/tsconfig.app.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", - "types": [] + "types": [], }, "files": [ "main.ts", diff --git a/routes/users.js b/routes/users.js index 98e676390..b8d04e60b 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1010,6 +1010,12 @@ router.post('/user/:id/passwordreset', async function(req, res) { } user.password=req.body.password; + const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_])(?!.*\s).{12,}$/; + if (!passwordRegex.test(user.password)) { + res.status(400).send({ message: 'Password does not meet the required format: 12 characters min, including 1 digit, 1 lowercase, 1 uppercase, and 1 special character, and no spaces.' }); + return; + } + await dbsrv.mongo_events().insertOne({ 'owner': session_user.uid, 'date': new Date().getTime(),