Skip to content

Commit

Permalink
Add Login and Registration Pages (#32)
Browse files Browse the repository at this point in the history
* 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 <samuelim01@gmail.com>
  • Loading branch information
McNaBry and samuelim01 authored Sep 26, 2024
1 parent 0c7e193 commit f2df810
Show file tree
Hide file tree
Showing 17 changed files with 436 additions and 2 deletions.
12 changes: 12 additions & 0 deletions frontend/src/app/account/_validators/invalid-password.validator.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
12 changes: 12 additions & 0 deletions frontend/src/app/account/_validators/invalid-username.validator.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
Original file line number Diff line number Diff line change
@@ -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;
};
}
13 changes: 13 additions & 0 deletions frontend/src/app/account/_validators/weak-password.validator.ts
Original file line number Diff line number Diff line change
@@ -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;
};
}
22 changes: 22 additions & 0 deletions frontend/src/app/account/account.component.css
Original file line number Diff line number Diff line change
@@ -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%;
}
24 changes: 24 additions & 0 deletions frontend/src/app/account/account.component.ts
Original file line number Diff line number Diff line change
@@ -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 {}
20 changes: 20 additions & 0 deletions frontend/src/app/account/account.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
4 changes: 4 additions & 0 deletions frontend/src/app/account/layout.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="flex flex-column h-full w-full justify-content-center align-items-center p-2">
<h2 class="mb-2">Welcome to PeerPrep</h2>
<router-outlet></router-outlet>
</div>
11 changes: 11 additions & 0 deletions frontend/src/app/account/layout.component.ts
Original file line number Diff line number Diff line change
@@ -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) {}
}
36 changes: 36 additions & 0 deletions frontend/src/app/account/login.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class="container w-full sm:w-22rem">
<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>
<input pInputText required type="text" id="username" name="username" [(ngModel)]="user.username" />
</div>

<div class="form-field">
<label for="password">Password</label>
<p-password
required
inputId="password"
name="password"
[(ngModel)]="user.password"
[toggleMask]="true"
[feedback]="false"
class="p-fluid" />
</div>

<p-button
type="submit"
[label]="isProcessingLogin ? '' : 'Log In'"
[loading]="isProcessingLogin"
[disabled]="!loginForm.valid"
styleClass="w-full justify-content-center" />
</form>

<p class="mt-4 mb-0">
Don't have an account?
<a routerLink="../register" class="text-blue-500 no-underline">Register</a>
</p>

<p-toast position="bottom-right" [breakpoints]="{ '920px': { width: '90%' } }" />
</div>
22 changes: 22 additions & 0 deletions frontend/src/app/account/login.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { LoginComponent } from './login.component';

describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginComponent],
}).compileComponents();

fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
46 changes: 46 additions & 0 deletions frontend/src/app/account/login.component.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
}
73 changes: 73 additions & 0 deletions frontend/src/app/account/register.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<div class="container w-full sm:w-22rem">
<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>
<input id="username" type="text" formControlName="username" pInputText />
@if (isUsernameInvalid) {
<small class="text-red-300">
The provided username can only contain alphanumeric characters, dots, dashes, and underscores
</small>
}
</div>
<div class="form-field">
<label for="email">Email</label>
<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>
<p-password
id="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>
</p-password>
@if (isPasswordWeak) {
<small class="text-red-300">The provided password is too weak</small>
} @else if (isPasswordInvalid) {
<small class="text-red-300">The provided password contains invalid characters</small>
}
</div>
<div class="form-field">
<label for="confirmPassword">Confirm Password</label>
<p-password
id="confirmPassword"
class="p-fluid"
formControlName="confirmPassword"
[toggleMask]="true"
[feedback]="false" />
@if (hasPasswordMismatch) {
<small class="text-red-300">The provided passwords do not match!</small>
}
</div>
<p-button
type="submit"
[label]="isProcessingRegistration ? '' : 'Register'"
[loading]="isProcessingRegistration"
[disabled]="!userForm.valid"
styleClass="w-full justify-content-center" />
</form>

<p class="mt-4 mb-0">
Already have an account?
<a routerLink="../login" class="text-blue-500 no-underline">Log In</a>
</p>

<p-toast position="bottom-right" [breakpoints]="{ '920px': { width: '90%' } }" />
</div>
22 changes: 22 additions & 0 deletions frontend/src/app/account/register.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { RegisterComponent } from './register.component';

describe('RegisterComponent', () => {
let component: RegisterComponent;
let fixture: ComponentFixture<RegisterComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RegisterComponent],
}).compileComponents();

fixture = TestBed.createComponent(RegisterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Loading

0 comments on commit f2df810

Please sign in to comment.