From c531e8207edd4023c90dc45c3eb80d4d1eb8aec5 Mon Sep 17 00:00:00 2001 From: Cathy Hu <cathy.hu@fau.de> Date: Fri, 9 Oct 2020 12:25:53 +0200 Subject: [PATCH] [frontend] Implement password reset --- .../src/app/auth/auth-routing.module.ts | 4 + kibicara-frontend/src/app/auth/auth.module.ts | 10 ++- .../src/app/auth/login/login.component.html | 14 +++- .../src/app/auth/login/login.component.scss | 7 ++ .../password-reset.component.html | 34 +++++++++ .../password-reset.component.scss | 44 +++++++++++ .../password-reset.component.spec.ts | 24 ++++++ .../password-reset.component.ts | 66 ++++++++++++++++ .../set-password/set-password.component.html | 47 ++++++++++++ .../set-password/set-password.component.scss | 44 +++++++++++ .../set-password.component.spec.ts | 24 ++++++ .../set-password/set-password.component.ts | 76 +++++++++++++++++++ 12 files changed, 389 insertions(+), 5 deletions(-) create mode 100644 kibicara-frontend/src/app/auth/password-reset/password-reset.component.html create mode 100644 kibicara-frontend/src/app/auth/password-reset/password-reset.component.scss create mode 100644 kibicara-frontend/src/app/auth/password-reset/password-reset.component.spec.ts create mode 100644 kibicara-frontend/src/app/auth/password-reset/password-reset.component.ts create mode 100644 kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.html create mode 100644 kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.scss create mode 100644 kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.spec.ts create mode 100644 kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.ts diff --git a/kibicara-frontend/src/app/auth/auth-routing.module.ts b/kibicara-frontend/src/app/auth/auth-routing.module.ts index 51d33d8..fbc0f2a 100644 --- a/kibicara-frontend/src/app/auth/auth-routing.module.ts +++ b/kibicara-frontend/src/app/auth/auth-routing.module.ts @@ -3,6 +3,8 @@ import { Routes, RouterModule } from '@angular/router'; import { LoginComponent } from './login/login.component'; import { RegisterComponent } from './register/register.component'; import { ConfirmComponent } from './confirm/confirm.component'; +import { PasswordResetComponent } from './password-reset/password-reset.component'; +import { SetPasswordComponent } from './password-reset/set-password/set-password.component'; const routes: Routes = [ { @@ -11,6 +13,8 @@ const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: 'register', component: RegisterComponent }, { path: 'confirm', component: ConfirmComponent }, + { path: 'reset', component: PasswordResetComponent }, + { path: 'password-reset', component: SetPasswordComponent }, ], }, ]; diff --git a/kibicara-frontend/src/app/auth/auth.module.ts b/kibicara-frontend/src/app/auth/auth.module.ts index 338c461..cf27768 100644 --- a/kibicara-frontend/src/app/auth/auth.module.ts +++ b/kibicara-frontend/src/app/auth/auth.module.ts @@ -4,9 +4,17 @@ import { ConfirmComponent } from './confirm/confirm.component'; import { RegisterComponent } from './register/register.component'; import { LoginComponent } from './login/login.component'; import { SharedModule } from '../shared/shared.module'; +import { PasswordResetComponent } from './password-reset/password-reset.component'; +import { SetPasswordComponent } from './password-reset/set-password/set-password.component'; @NgModule({ - declarations: [ConfirmComponent, LoginComponent, RegisterComponent], + declarations: [ + ConfirmComponent, + LoginComponent, + RegisterComponent, + PasswordResetComponent, + SetPasswordComponent, + ], imports: [AuthRoutingModule, SharedModule], }) export class AuthModule {} diff --git a/kibicara-frontend/src/app/auth/login/login.component.html b/kibicara-frontend/src/app/auth/login/login.component.html index c39f19b..b2df9f9 100644 --- a/kibicara-frontend/src/app/auth/login/login.component.html +++ b/kibicara-frontend/src/app/auth/login/login.component.html @@ -37,10 +37,16 @@ </mat-form-field> </div> <div class="buttons"> - <button mat-raised-button color="primary" [disabled]="loading"> - Log In - </button> - <a mat-button routerLink="/register">Register</a> + <div> + <button mat-raised-button color="primary" [disabled]="loading"> + Log In + </button> + <a mat-button routerLink="/register">Register</a> + </div> + <div class="spacer"></div> + <div> + <a routerLink="/reset">Forgot password?</a> + </div> </div> </form> </mat-card-content> diff --git a/kibicara-frontend/src/app/auth/login/login.component.scss b/kibicara-frontend/src/app/auth/login/login.component.scss index 80bec60..e45bb73 100644 --- a/kibicara-frontend/src/app/auth/login/login.component.scss +++ b/kibicara-frontend/src/app/auth/login/login.component.scss @@ -34,9 +34,16 @@ .buttons { margin-left: 20px; + margin-right: 30px; + display: flex; + align-items: center; } .login-form { margin-top: 10%; margin-bottom: 10%; } + +.spacer { + flex-grow: 1; +} diff --git a/kibicara-frontend/src/app/auth/password-reset/password-reset.component.html b/kibicara-frontend/src/app/auth/password-reset/password-reset.component.html new file mode 100644 index 0000000..8219c66 --- /dev/null +++ b/kibicara-frontend/src/app/auth/password-reset/password-reset.component.html @@ -0,0 +1,34 @@ +<div class="container"> + <mat-card class="login-form"> + <mat-card-header> + <h2>Reset password</h2> + </mat-card-header> + <mat-card-content> + <form [formGroup]="resetForm" (ngSubmit)="onSubmit()"> + <div class="input-container"> + <mat-form-field appearance="fill"> + <mat-label>E-Mail</mat-label> + <input type="text" formControlName="email" matInput /> + <mat-error + *ngIf=" + resetForm.controls.email.errors && + resetForm.controls.email.errors.required + " + > + Email is required + </mat-error> + </mat-form-field> + </div> + <div class="buttons"> + <div> + <button mat-raised-button color="primary" [disabled]="loading"> + Reset password + </button> + <a mat-button routerLink="/login">Login</a> + </div> + </div> + </form> + </mat-card-content> + </mat-card> + <div class="banner"></div> +</div> diff --git a/kibicara-frontend/src/app/auth/password-reset/password-reset.component.scss b/kibicara-frontend/src/app/auth/password-reset/password-reset.component.scss new file mode 100644 index 0000000..3ddd41e --- /dev/null +++ b/kibicara-frontend/src/app/auth/password-reset/password-reset.component.scss @@ -0,0 +1,44 @@ +.input-container { + display: grid; + margin-left: 5%; + margin-right: 5%; + margin-bottom: 5%; + margin-top: 3%; +} + +.mat-card:not([class*="mat-elevation-z"]) { + box-shadow: none; +} + +.container { + display: grid; + grid-template-columns: 1fr 1fr; + margin-left: 10%; + margin-right: 10%; + margin-top: 10%; + @media (max-width: 600px) { + grid-template-columns: 1fr; + margin-left: 5%; + margin-right: 5%; + } +} + +.banner { + background: url("../../../assets/hoods1.jpg"); + background-size: 100%; + display: block; + width: 100%; + height: 100%; +} + +.buttons { + margin-left: 20px; + margin-right: 30px; + display: flex; + align-items: center; +} + +.login-form { + margin-top: 10%; + margin-bottom: 10%; +} diff --git a/kibicara-frontend/src/app/auth/password-reset/password-reset.component.spec.ts b/kibicara-frontend/src/app/auth/password-reset/password-reset.component.spec.ts new file mode 100644 index 0000000..9403fbb --- /dev/null +++ b/kibicara-frontend/src/app/auth/password-reset/password-reset.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PasswordResetComponent } from './password-reset.component'; + +describe('PasswordResetComponent', () => { + let component: PasswordResetComponent; + let fixture: ComponentFixture<PasswordResetComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [PasswordResetComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordResetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/kibicara-frontend/src/app/auth/password-reset/password-reset.component.ts b/kibicara-frontend/src/app/auth/password-reset/password-reset.component.ts new file mode 100644 index 0000000..6c6c1d2 --- /dev/null +++ b/kibicara-frontend/src/app/auth/password-reset/password-reset.component.ts @@ -0,0 +1,66 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { ActivatedRoute, Router } from '@angular/router'; +import { first } from 'rxjs/operators'; +import { AdminService } from 'src/app/core/api/api/admin.service'; +import { LoginService } from 'src/app/core/auth/login.service'; + +@Component({ + selector: 'app-password-reset', + templateUrl: './password-reset.component.html', + styleUrls: ['./password-reset.component.scss'], +}) +export class PasswordResetComponent implements OnInit { + resetForm: FormGroup; + returnUrl: string; + loading = false; + submitted = false; + hide = true; + + constructor( + private adminService: AdminService, + private loginService: LoginService, + private router: Router, + private route: ActivatedRoute, + private formBuilder: FormBuilder, + private snackBar: MatSnackBar + ) { + if (this.loginService.currentHoodAdminValue) { + this.router.navigate(['/dashboard']); + } + } + + ngOnInit(): void { + this.resetForm = this.formBuilder.group({ + email: ['', Validators.required], + }); + this.returnUrl = this.route.snapshot.queryParams.returnUrl || '/dashboard'; + } + + onSubmit() { + this.submitted = true; + if (this.resetForm.invalid) { + return; + } + + this.loading = true; + this.adminService + .reset({ email: this.resetForm.controls.email.value }) + .pipe(first()) + .subscribe( + (data) => { + this.router.navigate([this.returnUrl]); + this.snackBar.open('Reset E-Mail sent!', 'Close', { + duration: 2000, + }); + }, + (error) => { + this.snackBar.open('Error sending E-Mail!', 'Close', { + duration: 2000, + }); + this.loading = false; + } + ); + } +} diff --git a/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.html b/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.html new file mode 100644 index 0000000..89f82d3 --- /dev/null +++ b/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.html @@ -0,0 +1,47 @@ +<div class="container"> + <mat-card class="login-form"> + <mat-card-header> + <h2>Enter your new password</h2> + </mat-card-header> + <mat-card-content> + <form [formGroup]="resetForm" (ngSubmit)="onSubmit()"> + <div class="input-container"> + <mat-form-field appearance="fill"> + <mat-label>Password</mat-label> + <input + type="password" + formControlName="password" + matInput + [type]="'password'" + /> + <mat-error + *ngIf=" + resetForm.controls.password.errors && + resetForm.controls.password.errors.required + " + > + Password is required + </mat-error> + <mat-error + *ngIf=" + resetForm.controls.password.errors && + resetForm.controls.password.errors.minlength + " + > + Password requires minimal length 8 + </mat-error> + </mat-form-field> + </div> + <div class="buttons"> + <div> + <button mat-raised-button color="primary" [disabled]="loading"> + Save new password + </button> + <a mat-button routerLink="/login">Login</a> + </div> + </div> + </form> + </mat-card-content> + </mat-card> + <div class="banner"></div> +</div> diff --git a/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.scss b/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.scss new file mode 100644 index 0000000..e6225a9 --- /dev/null +++ b/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.scss @@ -0,0 +1,44 @@ +.input-container { + display: grid; + margin-left: 5%; + margin-right: 5%; + margin-bottom: 5%; + margin-top: 3%; +} + +.mat-card:not([class*="mat-elevation-z"]) { + box-shadow: none; +} + +.container { + display: grid; + grid-template-columns: 1fr 1fr; + margin-left: 10%; + margin-right: 10%; + margin-top: 10%; + @media (max-width: 600px) { + grid-template-columns: 1fr; + margin-left: 5%; + margin-right: 5%; + } +} + +.banner { + background: url("../../../../assets/hoods1.jpg"); + background-size: 100%; + display: block; + width: 100%; + height: 100%; +} + +.buttons { + margin-left: 20px; + margin-right: 30px; + display: flex; + align-items: center; +} + +.login-form { + margin-top: 10%; + margin-bottom: 10%; +} diff --git a/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.spec.ts b/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.spec.ts new file mode 100644 index 0000000..1feabc3 --- /dev/null +++ b/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SetPasswordComponent } from './set-password.component'; + +describe('SetPasswordComponent', () => { + let component: SetPasswordComponent; + let fixture: ComponentFixture<SetPasswordComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [SetPasswordComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SetPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.ts b/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.ts new file mode 100644 index 0000000..69a46f1 --- /dev/null +++ b/kibicara-frontend/src/app/auth/password-reset/set-password/set-password.component.ts @@ -0,0 +1,76 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { ActivatedRoute, Router } from '@angular/router'; +import { first } from 'rxjs/operators'; +import { AdminService } from 'src/app/core/api/api/admin.service'; +import { LoginService } from 'src/app/core/auth/login.service'; + +@Component({ + selector: 'app-set-password', + templateUrl: './set-password.component.html', + styleUrls: ['./set-password.component.scss'], +}) +export class SetPasswordComponent implements OnInit { + resetForm: FormGroup; + returnUrl: string; + loading = false; + submitted = false; + hide = true; + token; + + constructor( + private adminService: AdminService, + private loginService: LoginService, + private router: Router, + private route: ActivatedRoute, + private formBuilder: FormBuilder, + private snackBar: MatSnackBar + ) { + this.token = this.route.snapshot.queryParams.token; + if (this.loginService.currentHoodAdminValue) { + this.router.navigate(['/dashboard']); + } else if (!this.token) { + this.router.navigate(['/404']); + } + } + + ngOnInit(): void { + this.resetForm = this.formBuilder.group({ + password: ['', [Validators.required, Validators.minLength(8)]], + }); + this.returnUrl = this.route.snapshot.queryParams.returnUrl || '/dashboard'; + } + + onSubmit() { + this.submitted = true; + if (this.resetForm.invalid) { + return; + } + + this.loading = true; + this.adminService + .confirmReset(this.token, { + password: this.resetForm.controls.password.value, + }) + .pipe(first()) + .subscribe( + (data) => { + this.router.navigate([this.returnUrl]); + this.snackBar.open('Password reset successful!', 'Close', { + duration: 2000, + }); + }, + (error) => { + this.snackBar.open( + 'Error resetting password! Try ordering another password reset email!', + 'Close', + { + duration: 2000, + } + ); + this.loading = false; + } + ); + } +}