Skip to content

Commit

Permalink
Enhance Login & Registration Pages (#57)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Samuel Lim <[email protected]>
  • Loading branch information
3 people authored Oct 26, 2024
1 parent 6bf6136 commit 89cb0bb
Show file tree
Hide file tree
Showing 18 changed files with 234 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
}
12 changes: 12 additions & 0 deletions frontend/src/app/account/_validators/lowercase-password.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
12 changes: 12 additions & 0 deletions frontend/src/app/account/_validators/numeric-password.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
12 changes: 12 additions & 0 deletions frontend/src/app/account/_validators/short-password.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
12 changes: 12 additions & 0 deletions frontend/src/app/account/_validators/special-password.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
12 changes: 12 additions & 0 deletions frontend/src/app/account/_validators/uppercase-password.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
16 changes: 12 additions & 4 deletions frontend/src/app/account/_validators/weak-password.validator.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
10 changes: 10 additions & 0 deletions frontend/src/app/account/account.component.css
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/app/account/layout.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
<div class="flex flex-column h-full w-full justify-content-center align-items-center p-2" style="margin-top: -80px">
<h2 class="mb-2">Welcome to PeerPrep</h2>
<div class="layout-container">
<router-outlet></router-outlet>
</div>
1 change: 1 addition & 0 deletions frontend/src/app/account/layout.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/app/account/login.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ <h2 class="mt-0 align-self-start">Log In</h2>

<form #loginForm="ngForm" (ngSubmit)="onSubmit()" class="form-container">
<div class="form-field">
<label for="username">Username</label>
<div class="flex flex-row align-items-center gap-2">
<i class="pi pi-user"></i>
<label for="username">Username</label>
</div>
<input pInputText required type="text" id="username" name="username" [(ngModel)]="userForm.username" />
</div>

<div class="form-field">
<label for="password">Password</label>
<div class="flex flex-row align-items-center gap-2">
<i class="pi pi-lock"></i>
<label for="password">Password</label>
</div>
<p-password
required
inputId="password"
Expand Down
69 changes: 47 additions & 22 deletions frontend/src/app/account/register.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ <h2 class="mt-0 align-self-start">Register</h2>

<form [formGroup]="userForm" (ngSubmit)="onSubmit()" class="form-container">
<div class="form-field">
<label for="username">Username</label>
<div class="flex flex-row align-items-center gap-2">
<i class="pi pi-user"></i>
<label for="username">Username</label>
</div>
<input id="username" type="text" formControlName="username" pInputText />
@if (isUsernameInvalid) {
<small class="text-red-300">
Expand All @@ -12,42 +15,64 @@ <h2 class="mt-0 align-self-start">Register</h2>
}
</div>
<div class="form-field">
<label for="email">Email</label>
<div class="flex flex-row align-items-center gap-2">
<i class="pi pi-envelope"></i>
<label for="email">Email</label>
</div>
<input id="email" type="email" formControlName="email" pInputText />
@if (isEmailInvalid) {
<small class="text-red-300">The provided email is invalid</small>
}
</div>
<div class="form-field">
<label for="password">Password</label>
<div class="flex flex-row align-items-center gap-2">
<i class="pi pi-lock"></i>
<label for="password">Password</label>
</div>
<p-password
id="password"
inputId="password"
name="password"
class="p-fluid"
formControlName="password"
[strongRegex]="strongPasswordRegex"
[toggleMask]="true">
<ng-template pTemplate="footer">
<p-divider />
<p class="mt-1 text-sm">Your password must contain:</p>
<ul class="pl-2 ml-2 mt-0 text-sm line-height-3">
<li>At least one lowercase</li>
<li>At least one uppercase</li>
<li>At least one numeric</li>
<li>At least one special character</li>
<li>Minimum 8 characters</li>
</ul>
</ng-template>
[toggleMask]="true"
[feedback]="false">
</p-password>
@if (isPasswordWeak) {
<small class="text-red-300">The provided password is too weak</small>
} @else if (isPasswordInvalid) {

@if (isPasswordInvalid) {
<small class="text-red-300">The provided password contains invalid characters</small>
}

@if (isPasswordControlDirty) {
<ul class="pl-0 m-0 text-sm line-height-3">
@if (isPasswordWeak) {
@for (req of passwordRequirements; track $index) {
<li class="flex gap-2 align-items-center">
<i
class="pi text-sm"
[ngClass]="req.check() ? 'pi-times text-gray-300' : 'pi-check text-green-300'"></i>
<p class="m-0" [ngClass]="req.check() ? 'text-gray-300' : 'text-green-300'">
{{ req.msg }}
</p>
</li>
}
} @else if (isPasswordStrong) {
<li class="flex gap-2 align-items-center">
<i class="pi pi-check text-sm text-green-300"></i>
<p class="m-0 text-sm text-green-300">Your password is strong enough!</p>
</li>
}
</ul>
}
</div>

<div class="form-field">
<label for="confirmPassword">Confirm Password</label>
<div class="flex flex-row align-items-center gap-2">
<i class="pi pi-check-square"></i>
<label for="confirmPassword">Confirm Password</label>
</div>
<p-password
id="confirmPassword"
inputId="confirmPassword"
name="confirmPassword"
class="p-fluid"
formControlName="confirmPassword"
[toggleMask]="true"
Expand Down
80 changes: 68 additions & 12 deletions frontend/src/app/account/register.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { AbstractControl, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterLink, Router, ActivatedRoute } from '@angular/router';
import { SelectButtonModule } from 'primeng/selectbutton';
import { InputTextModule } from 'primeng/inputtext';
Expand All @@ -8,7 +9,12 @@ import { ButtonModule } from 'primeng/button';
import { DividerModule } from 'primeng/divider';
import { ToastModule } from 'primeng/toast';
import { MessageService } from 'primeng/api';
import { PASSWORD_WEAK, STRONG_PASSWORD_REGEX, weakPasswordValidator } from './_validators/weak-password.validator';
import { PASSWORD_LOWERCASE, lowercasePasswordValidator } from './_validators/lowercase-password';
import { PASSWORD_UPPERCASE, uppercasePasswordValidator } from './_validators/uppercase-password';
import { PASSWORD_NUMERIC, numericPasswordValidator } from './_validators/numeric-password';
import { PASSWORD_SPECIAL, specialPasswordValidator } from './_validators/special-password';
import { PASSWORD_SHORT, shortPasswordValidator } from './_validators/short-password';
import { PASSWORD_WEAK, weakPasswordValidator } from './_validators/weak-password.validator';
import { mismatchPasswordValidator, PASSWORD_MISMATCH } from './_validators/mismatch-password.validator';
import { invalidUsernameValidator, USERNAME_INVALID } from './_validators/invalid-username.validator';
import { invalidPasswordValidator, PASSWORD_INVALID } from './_validators/invalid-password.validator';
Expand All @@ -18,6 +24,7 @@ import { AuthenticationService } from '../../_services/authentication.service';
selector: 'app-register',
standalone: true,
imports: [
CommonModule,
RouterLink,
FormsModule,
InputTextModule,
Expand Down Expand Up @@ -49,16 +56,32 @@ export class RegisterComponent {
{
username: new FormControl('', [Validators.required, invalidUsernameValidator()]),
email: new FormControl('', [Validators.required, Validators.email]),
password: new FormControl('', [Validators.required, weakPasswordValidator(), invalidPasswordValidator()]),
password: new FormControl('', [
Validators.required,
invalidPasswordValidator(),
lowercasePasswordValidator(),
uppercasePasswordValidator(),
numericPasswordValidator(),
specialPasswordValidator(),
shortPasswordValidator(),
weakPasswordValidator(),
]),
confirmPassword: new FormControl('', [Validators.required]),
},
{
validators: mismatchPasswordValidator('password', 'confirmPassword'),
},
);
isProcessingRegistration = false;

strongPasswordRegex = STRONG_PASSWORD_REGEX.source;
passwordRequirements = [
{ msg: 'At least one lowercase', check: () => 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'];
Expand All @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 89cb0bb

Please sign in to comment.