From f2df8100fc326c5b1dfd362f573abebced2a9548 Mon Sep 17 00:00:00 2001 From: McNaBry Date: Thu, 26 Sep 2024 23:49:01 +0800 Subject: [PATCH] Add Login and Registration Pages (#32) * Add basic login page * Add basic registration page * Add loading button for login and registration page * Add error toast message for login and registration pages * Change wording for login and registration pages * Update routing and UI for login and registration pages * Update routing and UI for login and registration pages * Fix linting * Clean up CSS styles for login and registration pages * Redirect account route to login * Adjust body styles * Add default spec file for register component * Register: Ensure strong password * Register: Add email error message * Register: Validate password match * Register: Minor fix * Add validation for username * Clean up code and styles * Add restricted set of characters for password * Shift registration validators into account folder * Fix linting * Add special character requirement for password * Fix styles for password input in login page * Register: Clean error messages * Login: Fix toast sizing --------- Co-authored-by: Samuel Lim --- .../_validators/invalid-password.validator.ts | 12 +++ .../_validators/invalid-username.validator.ts | 12 +++ .../mismatch-password.validator.ts | 11 +++ .../_validators/weak-password.validator.ts | 13 +++ .../src/app/account/account.component.css | 22 +++++ frontend/src/app/account/account.component.ts | 24 +++++ frontend/src/app/account/account.module.ts | 20 ++++ .../src/app/account/layout.component.html | 4 + frontend/src/app/account/layout.component.ts | 11 +++ frontend/src/app/account/login.component.html | 36 +++++++ .../src/app/account/login.component.spec.ts | 22 +++++ frontend/src/app/account/login.component.ts | 46 +++++++++ .../src/app/account/register.component.html | 73 ++++++++++++++ .../app/account/register.component.spec.ts | 22 +++++ .../src/app/account/register.component.ts | 94 +++++++++++++++++++ frontend/src/app/app.routes.ts | 9 +- frontend/src/styles.css | 7 +- 17 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/account/_validators/invalid-password.validator.ts create mode 100644 frontend/src/app/account/_validators/invalid-username.validator.ts create mode 100644 frontend/src/app/account/_validators/mismatch-password.validator.ts create mode 100644 frontend/src/app/account/_validators/weak-password.validator.ts create mode 100644 frontend/src/app/account/account.component.css create mode 100644 frontend/src/app/account/account.component.ts create mode 100644 frontend/src/app/account/account.module.ts create mode 100644 frontend/src/app/account/layout.component.html create mode 100644 frontend/src/app/account/layout.component.ts create mode 100644 frontend/src/app/account/login.component.html create mode 100644 frontend/src/app/account/login.component.spec.ts create mode 100644 frontend/src/app/account/login.component.ts create mode 100644 frontend/src/app/account/register.component.html create mode 100644 frontend/src/app/account/register.component.spec.ts create mode 100644 frontend/src/app/account/register.component.ts diff --git a/frontend/src/app/account/_validators/invalid-password.validator.ts b/frontend/src/app/account/_validators/invalid-password.validator.ts new file mode 100644 index 0000000000..dc2a686f6a --- /dev/null +++ b/frontend/src/app/account/_validators/invalid-password.validator.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +const PASSWORD_REGEX = /^[a-zA-Z0-9!"#$%&'()*+,-.:;<=>?@\\/\\[\]^_`{|}~]+$/; + +export const PASSWORD_INVALID = 'passwordInvalid'; + +export function invalidPasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const weak = !PASSWORD_REGEX.test(control.value); + return weak ? { [PASSWORD_INVALID]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/invalid-username.validator.ts b/frontend/src/app/account/_validators/invalid-username.validator.ts new file mode 100644 index 0000000000..5d6ca4d488 --- /dev/null +++ b/frontend/src/app/account/_validators/invalid-username.validator.ts @@ -0,0 +1,12 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +const USERNAME_REGEX = /^[a-zA-Z0-9._-]+$/; + +export const USERNAME_INVALID = 'usernameInvalid'; + +export function invalidUsernameValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const invalid = !USERNAME_REGEX.test(control.value); + return invalid ? { [USERNAME_INVALID]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/mismatch-password.validator.ts b/frontend/src/app/account/_validators/mismatch-password.validator.ts new file mode 100644 index 0000000000..95f45b2cb3 --- /dev/null +++ b/frontend/src/app/account/_validators/mismatch-password.validator.ts @@ -0,0 +1,11 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const PASSWORD_MISMATCH = 'passwordMismatch'; + +export function mismatchPasswordValidator(firstPasswordField: string, secondPasswordField: string): ValidatorFn { + return (formGroup: AbstractControl): ValidationErrors | null => { + const password = formGroup.get(firstPasswordField)?.value; + const confirmPassword = formGroup.get(secondPasswordField)?.value; + return password !== confirmPassword ? { [PASSWORD_MISMATCH]: true } : null; + }; +} diff --git a/frontend/src/app/account/_validators/weak-password.validator.ts b/frontend/src/app/account/_validators/weak-password.validator.ts new file mode 100644 index 0000000000..8b5fd0847b --- /dev/null +++ b/frontend/src/app/account/_validators/weak-password.validator.ts @@ -0,0 +1,13 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export const STRONG_PASSWORD_REGEX = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})(?=.*[!"#$%&'()*+,-.:;<=>?@\\/\\[\]^_`{|}~])/; + +export const PASSWORD_WEAK = 'passwordWeak'; + +export function weakPasswordValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const weak = !STRONG_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 new file mode 100644 index 0000000000..516b236f7b --- /dev/null +++ b/frontend/src/app/account/account.component.css @@ -0,0 +1,22 @@ +.container { + padding: 2rem; + background-color: var(--surface-section); + border-radius: 0.75rem; + display: flex; + flex-direction: column; + align-items: center; +} + +.form-container { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; +} + +.form-field { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; +} \ No newline at end of file diff --git a/frontend/src/app/account/account.component.ts b/frontend/src/app/account/account.component.ts new file mode 100644 index 0000000000..b5f557f1eb --- /dev/null +++ b/frontend/src/app/account/account.component.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { LoginComponent } from './login.component'; +import { RegisterComponent } from './register.component'; +import { LayoutComponent } from './layout.component'; + +const routes: Routes = [ + { + path: '', + component: LayoutComponent, + children: [ + { path: '', redirectTo: 'login', pathMatch: 'full' }, + { path: 'login', component: LoginComponent }, + { path: 'register', component: RegisterComponent }, + ], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class AccountRoutingModule {} diff --git a/frontend/src/app/account/account.module.ts b/frontend/src/app/account/account.module.ts new file mode 100644 index 0000000000..4155de4dfe --- /dev/null +++ b/frontend/src/app/account/account.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +import { LoginComponent } from './login.component'; +import { RegisterComponent } from './register.component'; +import { LayoutComponent } from './layout.component'; +import { AccountRoutingModule } from './account.component'; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + AccountRoutingModule, + LayoutComponent, + LoginComponent, + RegisterComponent, + ], +}) +export class AccountModule {} diff --git a/frontend/src/app/account/layout.component.html b/frontend/src/app/account/layout.component.html new file mode 100644 index 0000000000..21e10ec796 --- /dev/null +++ b/frontend/src/app/account/layout.component.html @@ -0,0 +1,4 @@ +
+

Welcome to PeerPrep

+ +
diff --git a/frontend/src/app/account/layout.component.ts b/frontend/src/app/account/layout.component.ts new file mode 100644 index 0000000000..2cf4d587d8 --- /dev/null +++ b/frontend/src/app/account/layout.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; + +@Component({ + standalone: true, + imports: [RouterModule], + templateUrl: './layout.component.html', +}) +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 new file mode 100644 index 0000000000..dc32bfd2c8 --- /dev/null +++ b/frontend/src/app/account/login.component.html @@ -0,0 +1,36 @@ +
+

Log In

+ +
+
+ + +
+ +
+ + +
+ + + + +

+ Don't have an account? + Register +

+ + +
diff --git a/frontend/src/app/account/login.component.spec.ts b/frontend/src/app/account/login.component.spec.ts new file mode 100644 index 0000000000..6d2f15f702 --- /dev/null +++ b/frontend/src/app/account/login.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/account/login.component.ts b/frontend/src/app/account/login.component.ts new file mode 100644 index 0000000000..41fee5af68 --- /dev/null +++ b/frontend/src/app/account/login.component.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { SelectButtonModule } from 'primeng/selectbutton'; +import { InputTextModule } from 'primeng/inputtext'; +import { PasswordModule } from 'primeng/password'; +import { ButtonModule } from 'primeng/button'; +import { ToastModule } from 'primeng/toast'; +import { MessageService } from 'primeng/api'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [RouterLink, FormsModule, InputTextModule, ButtonModule, SelectButtonModule, PasswordModule, ToastModule], + providers: [MessageService], + templateUrl: './login.component.html', + styleUrl: './account.component.css', +}) +export class LoginComponent { + constructor(private messageService: MessageService) {} + + user = { + username: '', + password: '', + }; + + isProcessingLogin = false; + + showError() { + this.messageService.add({ severity: 'error', summary: 'Log In Error', detail: 'Missing Details' }); + } + + onSubmit() { + if (this.user.username && this.user.password) { + this.isProcessingLogin = true; + this.showError(); + // Simulate API request + setTimeout(() => { + this.isProcessingLogin = false; + console.log('Form Submitted', this.user); + }, 3000); + } else { + console.log('Invalid form'); + } + } +} diff --git a/frontend/src/app/account/register.component.html b/frontend/src/app/account/register.component.html new file mode 100644 index 0000000000..bac0c2e62b --- /dev/null +++ b/frontend/src/app/account/register.component.html @@ -0,0 +1,73 @@ +
+

Register

+ +
+
+ + + @if (isUsernameInvalid) { + + The provided username can only contain alphanumeric characters, dots, dashes, and underscores + + } +
+
+ + + @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
  • +
+
+
+ @if (isPasswordWeak) { + The provided password is too weak + } @else if (isPasswordInvalid) { + The provided password contains invalid characters + } +
+
+ + + @if (hasPasswordMismatch) { + The provided passwords do not match! + } +
+ + + +

+ Already have an account? + Log In +

+ + +
diff --git a/frontend/src/app/account/register.component.spec.ts b/frontend/src/app/account/register.component.spec.ts new file mode 100644 index 0000000000..bff2420114 --- /dev/null +++ b/frontend/src/app/account/register.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegisterComponent } from './register.component'; + +describe('RegisterComponent', () => { + let component: RegisterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegisterComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/account/register.component.ts b/frontend/src/app/account/register.component.ts new file mode 100644 index 0000000000..79d3496169 --- /dev/null +++ b/frontend/src/app/account/register.component.ts @@ -0,0 +1,94 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { SelectButtonModule } from 'primeng/selectbutton'; +import { InputTextModule } from 'primeng/inputtext'; +import { PasswordModule } from 'primeng/password'; +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 { 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'; + +@Component({ + selector: 'app-register', + standalone: true, + imports: [ + RouterLink, + FormsModule, + InputTextModule, + ButtonModule, + SelectButtonModule, + PasswordModule, + DividerModule, + ToastModule, + ReactiveFormsModule, + ], + providers: [MessageService], + templateUrl: './register.component.html', + styleUrl: './account.component.css', +}) +export class RegisterComponent { + constructor(private messageService: MessageService) {} + + userForm: FormGroup = new FormGroup( + { + username: new FormControl('', [Validators.required, invalidUsernameValidator()]), + email: new FormControl('', [Validators.required, Validators.email]), + password: new FormControl('', [Validators.required, weakPasswordValidator(), invalidPasswordValidator()]), + confirmPassword: new FormControl('', [Validators.required]), + }, + { + validators: mismatchPasswordValidator('password', 'confirmPassword'), + }, + ); + isProcessingRegistration = false; + + strongPasswordRegex = STRONG_PASSWORD_REGEX.source; + + get isUsernameInvalid(): boolean { + const usernameControl = this.userForm.controls['username']; + return usernameControl.dirty && usernameControl.hasError(USERNAME_INVALID); + } + + get isEmailInvalid(): boolean { + const emailControl = this.userForm.controls['email']; + return emailControl.dirty && emailControl.invalid; + } + + get isPasswordWeak(): boolean { + const passwordControl = this.userForm.controls['password']; + return passwordControl.dirty && passwordControl.hasError(PASSWORD_WEAK); + } + + get isPasswordInvalid(): boolean { + const passwordControl = this.userForm.controls['password']; + return passwordControl.dirty && 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); + } + + showError() { + this.messageService.add({ severity: 'error', summary: 'Registration Error', detail: 'Missing Details' }); + } + + onSubmit() { + if (this.userForm.valid) { + this.isProcessingRegistration = true; + this.showError(); + setTimeout(() => { + this.isProcessingRegistration = false; + console.log('Form Submitted', this.userForm.value); + }, 3000); + } else { + console.log('Invalid form'); + } + } +} diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index dc39edb5f2..971b436069 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,3 +1,10 @@ import { Routes } from '@angular/router'; -export const routes: Routes = []; +const accountModule = () => import('./account/account.module').then(x => x.AccountModule); + +export const routes: Routes = [ + { + path: 'account', + loadChildren: accountModule, + }, +]; diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 41c9cbca25..27aea93c1a 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,4 +1,9 @@ /* You can add global styles to this file, and also import other style files */ @import "primeng/resources/themes/aura-dark-blue/theme.css"; @import "primeng/resources/primeng.css"; -@import "primeflex/primeflex.scss"; \ No newline at end of file +@import "primeflex/primeflex.scss"; + +body { + height: 100vh; + margin: 0px; +}