From 89cb0bbebcba950db2811de7bd115683cb8e2655 Mon Sep 17 00:00:00 2001 From: McNaBry Date: Sat, 26 Oct 2024 12:26:34 +0800 Subject: [PATCH] Enhance Login & Registration Pages (#57) * Add UI enhancement to login and registration pages Add icons to the input field labels Remove primeNG password feedback and replace with password requirements list * Fix linting * Adjust styling for login and registration pages * Improve password requirements list * Remove weak password message * Conditionally render password list when user starts modifying the password field * Hide requirements and display a success message when user satisfies all requirements * Refactor code to render each requirement and improve naming for password validators * Fix linting * Fix nav bar Previously, the submenu's position was not relative to the body, this causes the submenu to be out of position * Update submenu to hide on scroll * Keep variables on top * Remove direct DOM manipulation * Add spinner when redirecting user to workspace --------- Co-authored-by: limcaaarl Co-authored-by: Samuel Lim --- .../_validators/invalid-password.validator.ts | 3 +- .../account/_validators/lowercase-password.ts | 12 +++ .../account/_validators/numeric-password.ts | 12 +++ .../app/account/_validators/short-password.ts | 12 +++ .../account/_validators/special-password.ts | 12 +++ .../account/_validators/uppercase-password.ts | 12 +++ .../_validators/weak-password.validator.ts | 16 +++- .../src/app/account/account.component.css | 10 +++ .../src/app/account/layout.component.html | 3 +- frontend/src/app/account/layout.component.ts | 1 + frontend/src/app/account/login.component.html | 10 ++- .../src/app/account/register.component.html | 69 +++++++++++----- .../src/app/account/register.component.ts | 80 ++++++++++++++++--- frontend/src/app/app.component.css | 17 ---- frontend/src/app/app.component.html | 7 +- .../finding-match.component.html | 5 +- .../navigation-bar.component.html | 2 +- .../navigation-bar.component.ts | 19 ++++- 18 files changed, 234 insertions(+), 68 deletions(-) create mode 100644 frontend/src/app/account/_validators/lowercase-password.ts create mode 100644 frontend/src/app/account/_validators/numeric-password.ts create mode 100644 frontend/src/app/account/_validators/short-password.ts create mode 100644 frontend/src/app/account/_validators/special-password.ts create mode 100644 frontend/src/app/account/_validators/uppercase-password.ts diff --git a/frontend/src/app/account/_validators/invalid-password.validator.ts b/frontend/src/app/account/_validators/invalid-password.validator.ts index dc2a686f6a..a18d7d9cb4 100644 --- a/frontend/src/app/account/_validators/invalid-password.validator.ts +++ b/frontend/src/app/account/_validators/invalid-password.validator.ts @@ -6,7 +6,8 @@ export const PASSWORD_INVALID = 'passwordInvalid'; export function invalidPasswordValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const weak = !PASSWORD_REGEX.test(control.value); + const password = control.value; + const weak = password && !PASSWORD_REGEX.test(password); return weak ? { [PASSWORD_INVALID]: true } : null; }; } diff --git a/frontend/src/app/account/_validators/lowercase-password.ts b/frontend/src/app/account/_validators/lowercase-password.ts new file mode 100644 index 0000000000..a515546898 --- /dev/null +++ b/frontend/src/app/account/_validators/lowercase-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const LOWERCASE_PASSWORD_REGEX = /^(?=.*[a-z])/; + +export const PASSWORD_LOWERCASE = 'passwordLowercase'; + +export function lowercasePasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const missingLowercase = !LOWERCASE_PASSWORD_REGEX.test(control.value); + return missingLowercase ? { [PASSWORD_LOWERCASE]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/numeric-password.ts b/frontend/src/app/account/_validators/numeric-password.ts new file mode 100644 index 0000000000..8e4b338af9 --- /dev/null +++ b/frontend/src/app/account/_validators/numeric-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const NUMERIC_PASSWORD_REGEX = /^(?=.*[0-9])/; + +export const PASSWORD_NUMERIC = 'passwordNumeric'; + +export function numericPasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const missingNumeric = !NUMERIC_PASSWORD_REGEX.test(control.value); + return missingNumeric ? { [PASSWORD_NUMERIC]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/short-password.ts b/frontend/src/app/account/_validators/short-password.ts new file mode 100644 index 0000000000..015817837d --- /dev/null +++ b/frontend/src/app/account/_validators/short-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const SHORT_PASSWORD_REGEX = /^(?=.{8,})/; + +export const PASSWORD_SHORT = 'passwordShort'; + +export function shortPasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const short = !SHORT_PASSWORD_REGEX.test(control.value); + return short ? { [PASSWORD_SHORT]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/special-password.ts b/frontend/src/app/account/_validators/special-password.ts new file mode 100644 index 0000000000..e79fc6200a --- /dev/null +++ b/frontend/src/app/account/_validators/special-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const SPECIAL_PASSWORD_REGEX = /^(?=.*[!"#$%&'()*+,-.:;<=>?@\\/\\[\]^_`{|}~])/; + +export const PASSWORD_SPECIAL = 'passwordSpecial'; + +export function specialPasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const missingSpecial = !SPECIAL_PASSWORD_REGEX.test(control.value); + return missingSpecial ? { [PASSWORD_SPECIAL]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/uppercase-password.ts b/frontend/src/app/account/_validators/uppercase-password.ts new file mode 100644 index 0000000000..f80a38a004 --- /dev/null +++ b/frontend/src/app/account/_validators/uppercase-password.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const UPPERCASE_PASSWORD_REGEX = /^(?=.*[A-Z])/; + +export const PASSWORD_UPPERCASE = 'passwordUppercase'; + +export function uppercasePasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const missingUppercase = !UPPERCASE_PASSWORD_REGEX.test(control.value); + return missingUppercase ? { [PASSWORD_UPPERCASE]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/weak-password.validator.ts b/frontend/src/app/account/_validators/weak-password.validator.ts index 8b5fd0847b..712f538131 100644 --- a/frontend/src/app/account/_validators/weak-password.validator.ts +++ b/frontend/src/app/account/_validators/weak-password.validator.ts @@ -1,13 +1,21 @@ import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; - -export const STRONG_PASSWORD_REGEX = - /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})(?=.*[!"#$%&'()*+,-.:;<=>?@\\/\\[\]^_`{|}~])/; +import { LOWERCASE_PASSWORD_REGEX } from './lowercase-password'; +import { UPPERCASE_PASSWORD_REGEX } from './uppercase-password'; +import { NUMERIC_PASSWORD_REGEX } from './numeric-password'; +import { SPECIAL_PASSWORD_REGEX } from './special-password'; +import { SHORT_PASSWORD_REGEX } from './short-password'; export const PASSWORD_WEAK = 'passwordWeak'; export function weakPasswordValidator(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const weak = !STRONG_PASSWORD_REGEX.test(control.value); + const weak = !( + LOWERCASE_PASSWORD_REGEX.test(control.value) && + UPPERCASE_PASSWORD_REGEX.test(control.value) && + NUMERIC_PASSWORD_REGEX.test(control.value) && + SPECIAL_PASSWORD_REGEX.test(control.value) && + SHORT_PASSWORD_REGEX.test(control.value) + ); return weak ? { [PASSWORD_WEAK]: true } : null; }; } diff --git a/frontend/src/app/account/account.component.css b/frontend/src/app/account/account.component.css index 516b236f7b..fd7126aadc 100644 --- a/frontend/src/app/account/account.component.css +++ b/frontend/src/app/account/account.component.css @@ -1,3 +1,13 @@ +.layout-container { + display: flex; + flex-direction: column; + min-height: calc(100vh - 80px); + width: 100%; + justify-content: center; + align-items: center; + padding: 1rem; +} + .container { padding: 2rem; background-color: var(--surface-section); diff --git a/frontend/src/app/account/layout.component.html b/frontend/src/app/account/layout.component.html index 0036e0a2f0..0fb48cd93b 100644 --- a/frontend/src/app/account/layout.component.html +++ b/frontend/src/app/account/layout.component.html @@ -1,4 +1,3 @@ -
-

Welcome to PeerPrep

+
diff --git a/frontend/src/app/account/layout.component.ts b/frontend/src/app/account/layout.component.ts index 2cf4d587d8..43b661ab19 100644 --- a/frontend/src/app/account/layout.component.ts +++ b/frontend/src/app/account/layout.component.ts @@ -5,6 +5,7 @@ import { Router, RouterModule } from '@angular/router'; standalone: true, imports: [RouterModule], templateUrl: './layout.component.html', + styleUrl: './account.component.css', }) export class LayoutComponent { constructor(private router: Router) {} diff --git a/frontend/src/app/account/login.component.html b/frontend/src/app/account/login.component.html index d65adba792..0c38961a7d 100644 --- a/frontend/src/app/account/login.component.html +++ b/frontend/src/app/account/login.component.html @@ -3,12 +3,18 @@

Log In

- +
+ + +
- +
+ + +
Register
- +
+ + +
@if (isUsernameInvalid) { @@ -12,42 +15,64 @@

Register

}
- +
+ + +
@if (isEmailInvalid) { The provided email is invalid }
- +
+ + +
- - -

Your password must contain:

-
    -
  • At least one lowercase
  • -
  • At least one uppercase
  • -
  • At least one numeric
  • -
  • At least one special character
  • -
  • Minimum 8 characters
  • -
-
+ [toggleMask]="true" + [feedback]="false">
- @if (isPasswordWeak) { - The provided password is too weak - } @else if (isPasswordInvalid) { + + @if (isPasswordInvalid) { The provided password contains invalid characters } + + @if (isPasswordControlDirty) { +
    + @if (isPasswordWeak) { + @for (req of passwordRequirements; track $index) { +
  • + +

    + {{ req.msg }} +

    +
  • + } + } @else if (isPasswordStrong) { +
  • + +

    Your password is strong enough!

    +
  • + } +
+ }
+
- +
+ + +
this.passwordHasNoLowercase }, + { msg: 'At least one uppercase', check: () => this.passwordHasNoUppercase }, + { msg: 'At least one numeric', check: () => this.passwordHasNoNumeric }, + { msg: 'At least one special character', check: () => this.passwordHasNoSpecial }, + { msg: 'Minimum 8 characters', check: () => this.isPasswordShort }, + ]; + + isProcessingRegistration = false; get isUsernameInvalid(): boolean { const usernameControl = this.userForm.controls['username']; @@ -70,20 +93,49 @@ export class RegisterComponent { return emailControl.dirty && emailControl.invalid; } + get passwordControl(): AbstractControl { + return this.userForm.controls['password']; + } + + get isPasswordControlDirty(): boolean { + return this.passwordControl.dirty; + } + + get passwordHasNoLowercase(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_LOWERCASE); + } + + get passwordHasNoUppercase(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_UPPERCASE); + } + + get passwordHasNoNumeric(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_NUMERIC); + } + + get passwordHasNoSpecial(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_SPECIAL); + } + + get isPasswordShort(): boolean { + return this.passwordControl.pristine || this.passwordControl.hasError(PASSWORD_SHORT); + } + get isPasswordWeak(): boolean { - const passwordControl = this.userForm.controls['password']; - return passwordControl.dirty && passwordControl.hasError(PASSWORD_WEAK); + return this.passwordControl.dirty && this.passwordControl.hasError(PASSWORD_WEAK); + } + + get isPasswordStrong(): boolean { + return this.passwordControl.dirty && !this.passwordControl.hasError(PASSWORD_WEAK); } get isPasswordInvalid(): boolean { - const passwordControl = this.userForm.controls['password']; - return passwordControl.dirty && passwordControl.hasError(PASSWORD_INVALID); + return this.passwordControl.dirty && this.passwordControl.hasError(PASSWORD_INVALID); } get hasPasswordMismatch(): boolean { - const passwordControl = this.userForm.controls['password']; const confirmPasswordControl = this.userForm.controls['confirmPassword']; - return passwordControl.valid && confirmPasswordControl.dirty && this.userForm.hasError(PASSWORD_MISMATCH); + return this.passwordControl.valid && confirmPasswordControl.dirty && this.userForm.hasError(PASSWORD_MISMATCH); } showError() { @@ -113,7 +165,11 @@ export class RegisterComponent { } else if (status === 500) { errorMessage = 'Database Server Error'; } - this.messageService.add({ severity: 'error', summary: 'Log In Error', detail: errorMessage }); + this.messageService.add({ + severity: 'error', + summary: 'Registration Error', + detail: errorMessage, + }); }, }); } else { diff --git a/frontend/src/app/app.component.css b/frontend/src/app/app.component.css index 4df7a9e503..4ccc1a23d3 100644 --- a/frontend/src/app/app.component.css +++ b/frontend/src/app/app.component.css @@ -1,21 +1,4 @@ main { width: 100%; min-height: 100%; - display: flex; - justify-content: center; - align-items: center; - padding: 1rem; - box-sizing: inherit; - position: relative; - flex-direction: column; -} - -.content { - display: flex; - flex-direction: column; - width: auto; - margin-bottom: 1rem; - padding: 2rem; - background-color: var(--surface-section); - border-radius: 0.75rem; } \ No newline at end of file diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 7088928e9a..a20a5cb8af 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,3 +1,4 @@ - - - +
+ + +
diff --git a/frontend/src/app/matching/finding-match/finding-match.component.html b/frontend/src/app/matching/finding-match/finding-match.component.html index 574fd45191..273ac368b1 100644 --- a/frontend/src/app/matching/finding-match/finding-match.component.html +++ b/frontend/src/app/matching/finding-match/finding-match.component.html @@ -55,7 +55,10 @@

Match Found!

[outlined]="true" (click)="closeDialog()" /> } @else { -

Redirecting you to the workspace...

+
+ +

Redirecting you to the workspace...

+
}
diff --git a/frontend/src/app/navigation-bar/navigation-bar.component.html b/frontend/src/app/navigation-bar/navigation-bar.component.html index d5a8a174a2..9261df7948 100644 --- a/frontend/src/app/navigation-bar/navigation-bar.component.html +++ b/frontend/src/app/navigation-bar/navigation-bar.component.html @@ -7,7 +7,7 @@ @if (user) { - +