From 7e4066d05503ce328e628bf0364306630519ccfe Mon Sep 17 00:00:00 2001 From: Abhishek Tiwari <68281476+abhi9720@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:06:49 +0530 Subject: [PATCH] resolves abhi9720/BankingPortal-API#13 Add methods to reset pin and password (#10) * implement ui for forget password * added validations for password * account password reset implemented and validation added to form for password --- src/app/app-routing.module.ts | 2 + src/app/app.module.ts | 2 + src/app/components/login/login.component.html | 13 +- src/app/components/otp/otp.component.ts | 22 ++- .../register/register.component.html | 35 +++-- .../components/register/register.component.ts | 23 +-- .../reset-password.component.css | 11 ++ .../reset-password.component.html | 90 ++++++++++++ .../reset-password.component.spec.ts | 23 +++ .../reset-password.component.ts | 138 ++++++++++++++++++ src/app/services/auth.service.ts | 18 ++- src/app/util/formutil.ts | 24 +++ 12 files changed, 360 insertions(+), 41 deletions(-) create mode 100644 src/app/components/reset-password/reset-password.component.css create mode 100644 src/app/components/reset-password/reset-password.component.html create mode 100644 src/app/components/reset-password/reset-password.component.spec.ts create mode 100644 src/app/components/reset-password/reset-password.component.ts create mode 100644 src/app/util/formutil.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 8fc756b..d5a207e 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -18,6 +18,7 @@ import { RegisterComponent } from './components/register/register.component'; import { OtpComponent } from './components/otp/otp.component'; import { NotfoundpageComponent } from './components/notfoundpage/notfoundpage.component'; import { ProfileComponent } from './components/profile/profile.component'; +import { ResetPasswordComponent } from './components/reset-password/reset-password.component'; const routes: Routes = [ { path: '', component: HomeComponent, pathMatch: 'full' }, // Root route (HomeComponent) without AuthGuard @@ -32,6 +33,7 @@ const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: 'register', component: RegisterComponent }, { path: 'login/otp', component: OtpComponent }, + { path: 'forget-password', component: ResetPasswordComponent }, { path: '**', component: NotfoundpageComponent }, // Handle 404 - Page Not Found ]; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 26d2a78..c8e8d4b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -43,6 +43,7 @@ import { ApiService } from './services/api.service'; import { AuthService } from './services/auth.service'; import { LoadermodelService } from './services/loadermodel.service'; import { NgOtpInputModule } from 'ng-otp-input'; +import { ResetPasswordComponent } from './components/reset-password/reset-password.component'; @NgModule({ declarations: [ @@ -70,6 +71,7 @@ import { NgOtpInputModule } from 'ng-otp-input'; TransactionComponent, MonthlyTransactionChartComponent, DonwloadtransactionsComponent, + ResetPasswordComponent ], imports: [ RouterModule, diff --git a/src/app/components/login/login.component.html b/src/app/components/login/login.component.html index 9cb1d52..66a6011 100644 --- a/src/app/components/login/login.component.html +++ b/src/app/components/login/login.component.html @@ -31,9 +31,16 @@

Login

class="group relative w-full flex justify-center py-2 px-4 bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded focus:outline-none focus:shadow-outline disabled:opacity-50 disabled:cursor-not-allowed">Login - Login - via - Otp + +
+ Login + via + Otp + Forget Password? + + +
+ \ No newline at end of file diff --git a/src/app/components/otp/otp.component.ts b/src/app/components/otp/otp.component.ts index 24bdbef..3a67396 100644 --- a/src/app/components/otp/otp.component.ts +++ b/src/app/components/otp/otp.component.ts @@ -5,6 +5,7 @@ import { environment } from 'src/environment/environment'; import { Component, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; +import { finalize } from 'rxjs'; @Component({ selector: 'app-otp', @@ -21,7 +22,7 @@ export class OtpComponent { private authService: AuthService, private toastService: ToastService, private router: Router, - private loader: LoadermodelService // Inject the LoaderService here + private loader: LoadermodelService ) { } ngOnInit() { @@ -29,7 +30,7 @@ export class OtpComponent { const storedAccountNumber = sessionStorage.getItem('accountNumber'); if (storedAccountNumber) { this.accountNumber = storedAccountNumber; - this.otpGenerated = true; // If account number exists, it means OTP is generated + this.otpGenerated = true; } } @@ -53,16 +54,18 @@ export class OtpComponent { generateOTP() { this.loader.show('Generating OTP...'); // Show the loader before making the API call - this.authService.generateOTP(this.accountNumber).subscribe({ + this.authService.generateOTP(this.accountNumber).pipe( + finalize(() => { + this.loader.hide(); // Hide the loader after API call completes (success or error) + }) + ).subscribe({ next: (response: any) => { - this.loader.hide(); // Hide the loader on API response this.toastService.success(response.message + ', Check Email'); this.otpGenerated = true; // Save the account number in sessionStorage sessionStorage.setItem('accountNumber', this.accountNumber); }, error: (error: any) => { - this.loader.hide(); // Hide the loader on API error this.toastService.error(error.error); console.error(error); }, @@ -76,9 +79,13 @@ export class OtpComponent { otp: this.otp, }; - this.authService.verifyOTP(otpVerificationRequest).subscribe({ + this.authService.verifyOTP(otpVerificationRequest).pipe( + finalize(() => { + // Hide the loader after API call completes (success or error) + this.loader.hide(); + }) + ).subscribe({ next: (response: any) => { - this.loader.hide(); // Hide the loader on API response console.log(response); this.toastService.success('Account LoggedIn'); const token = response.token; @@ -86,7 +93,6 @@ export class OtpComponent { this.router.navigate(['/dashboard']); }, error: (error: any) => { - this.loader.hide(); // Hide the loader on API error this.toastService.error(error.error); console.error(error); }, diff --git a/src/app/components/register/register.component.html b/src/app/components/register/register.component.html index 96fc25d..faa5dbc 100644 --- a/src/app/components/register/register.component.html +++ b/src/app/components/register/register.component.html @@ -118,7 +118,7 @@

/>
Password is required. @@ -130,6 +130,8 @@

Password must be at most 127 characters long.

+ +
+ + +
-

Registration Successful!

-

Name: {{ registrationData.name }}

-

Email: {{ registrationData.email }}

-

Account Number: {{ registrationData.accountNumber }}

-

Branch: {{ registrationData.branch }}

+
Registration Successful!
+

Name: {{ registrationData.name || '-' }}

+

Email: {{ registrationData.email }}

+

Account Number: :{{ registrationData.accountNumber }}

+

Branch: {{ registrationData.branch }}

Continue to Login diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts index cbec1d9..ccdec4e 100644 --- a/src/app/components/register/register.component.ts +++ b/src/app/components/register/register.component.ts @@ -5,26 +5,9 @@ import { invalidPhoneNumber } from 'src/app/services/country-code.service'; import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { passwordMismatch, StrongPasswordRegx } from 'src/app/util/formutil'; -function passwordMismatch( - controlName: string, - matchingControlName: string -): any { - return (formGroup: FormGroup) => { - const control = formGroup.controls[controlName]; - const matchingControl = formGroup.controls[matchingControlName]; - if (matchingControl.errors && !matchingControl.errors.passwordMismatch) { - return; - } - - if (control.value !== matchingControl.value) { - matchingControl.setErrors({ passwordMismatch: true }); - } else { - matchingControl.setErrors(null); - } - }; -} @Component({ selector: 'app-register', @@ -35,12 +18,11 @@ export class RegisterComponent implements OnInit { registerForm!: FormGroup; showRegistrationData = false; registrationData: any; - print = console; constructor( private authService: AuthService, private _toastService: ToastService - ) {} + ) { } ngOnInit() { this.registerForm = new FormGroup( @@ -54,6 +36,7 @@ export class RegisterComponent implements OnInit { Validators.required, Validators.minLength(8), Validators.maxLength(127), + Validators.pattern(StrongPasswordRegx) ]), confirmPassword: new FormControl('', Validators.required), }, diff --git a/src/app/components/reset-password/reset-password.component.css b/src/app/components/reset-password/reset-password.component.css new file mode 100644 index 0000000..3adcc0f --- /dev/null +++ b/src/app/components/reset-password/reset-password.component.css @@ -0,0 +1,11 @@ +:host { + + display: flex; + flex: 1; +} + +.coverparentspace { + flex: 1; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/src/app/components/reset-password/reset-password.component.html b/src/app/components/reset-password/reset-password.component.html new file mode 100644 index 0000000..50cd114 --- /dev/null +++ b/src/app/components/reset-password/reset-password.component.html @@ -0,0 +1,90 @@ +
+
+
+ +
+

Reset Password

+
+ + +
+ + +
+ Email or Account Number is required. +
+
+
+ +
+
+ + + +
+ + + +
+
+ +
+
+
+ Back to Login +
+
+ +
+ +
+

Reset Password

+
+
+ + + +
+
+ + + +
+ Confirm Password is required and must match the New Password. +
+ +
+
+ Password is required. +
+
+ Password must be at least 8 characters long. +
+
+ Password must be at most 127 characters long. +
+
+ +
+
+ +
+
+
+
+
+
diff --git a/src/app/components/reset-password/reset-password.component.spec.ts b/src/app/components/reset-password/reset-password.component.spec.ts new file mode 100644 index 0000000..5bf4c9b --- /dev/null +++ b/src/app/components/reset-password/reset-password.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResetPasswordComponent } from './reset-password.component'; + +describe('ResetPasswordComponent', () => { + let component: ResetPasswordComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ResetPasswordComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ResetPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/reset-password/reset-password.component.ts b/src/app/components/reset-password/reset-password.component.ts new file mode 100644 index 0000000..80deb53 --- /dev/null +++ b/src/app/components/reset-password/reset-password.component.ts @@ -0,0 +1,138 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { ToastService } from 'angular-toastify'; +import { finalize } from 'rxjs'; +import { AuthService } from 'src/app/services/auth.service'; +import { LoadermodelService } from 'src/app/services/loadermodel.service'; +import { passwordMismatch, StrongPasswordRegx } from 'src/app/util/formutil'; + +@Component({ + selector: 'app-reset-password', + templateUrl: './reset-password.component.html', + styleUrls: ['./reset-password.component.css'] +}) +export class ResetPasswordComponent implements OnInit { + resetPasswordForm: FormGroup; + newPasswordForm: FormGroup; + showNewPasswordForm: boolean = false; + otpSentSuccessfully: boolean = false; + resetToken: string = ''; + + @ViewChild('ngOtpInput', { static: false }) ngOtpInput: any; + + config = { + allowNumbersOnly: true, + length: 6, + placeholder: '', + inputStyles: { + 'width': '50px', + 'height': '50px' + } + }; + + constructor(private fb: FormBuilder, private router: Router, private toastService: ToastService, private loader: LoadermodelService, + private authService: AuthService, + ) { + this.resetPasswordForm = this.fb.group({ + identifier: ['', [Validators.required, Validators.pattern(/^(?:(?:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})|(?:.{6}))$/)]], + otp: [''] // Added OTP field to the form + }); + + this.newPasswordForm = this.fb.group({ + newPassword: new FormControl('', [ + Validators.required, + Validators.minLength(8), + Validators.maxLength(127), + Validators.pattern(StrongPasswordRegx) + ]), + confirmPassword: new FormControl('', Validators.required), + }, { + validators: passwordMismatch('newPassword', 'confirmPassword'), + }); + } + + get f() { + return this.newPasswordForm.controls; + } + + onOtpChange(otp: string): void { + this.resetPasswordForm.patchValue({ otp: otp }); + } + + ngOnInit(): void { + } + + sendOtp(): void { + if (this.resetPasswordForm.valid) { + this.loader.show('Generating OTP...'); + const input = this.resetPasswordForm.value.identifier; + this.authService.sendOtpForPasswordReset(input).pipe( + finalize(() => { + this.loader.hide(); + }) + ).subscribe({ + next: (response: any) => { + this.toastService.success(response.message); + this.otpSentSuccessfully = true; + this.resetPasswordForm.get('otp')?.setValidators(Validators.required); + this.resetPasswordForm.get('otp')?.updateValueAndValidity(); + }, + error: (error: any) => { + this.toastService.error("Failed to send OTP: " + error.error); + console.error("Failed to send OTP: " + error.error); + } + }) + } + } + + + verifyOtp(): void { + if (this.resetPasswordForm.valid) { + this.loader.show('Verifying OTP...'); + + const identifier = this.resetPasswordForm.value.identifier; + const otp = this.resetPasswordForm.value.otp; + + this.authService.verifyOtpForPasswordReset(identifier, otp).pipe( + finalize(() => { + this.loader.hide(); + }) + ).subscribe({ + next: (response) => { + this.toastService.success('OTP Verified'); + this.showNewPasswordForm = true; + this.resetToken = response.passwordResetToken; + }, + error: (error) => { + this.toastService.error('Error verifying OTP : ' + error.error); + console.error('Error verifying OTP:', error); + } + }); + } + } + + resetPassword(): void { + if (this.newPasswordForm.valid) { + this.loader.show('Setting new Password...'); + const newPassword = this.newPasswordForm.value.newPassword; + + this.authService.resetPassword(this.resetPasswordForm.value.identifier, this.resetToken, newPassword) + .pipe( + finalize(() => { + this.loader.hide(); + }) + ).subscribe({ + next: (response) => { + this.toastService.success('Password reset successfully'); + console.log('Password reset successfully:', response); + this.router.navigate(['/login']); + }, + error: (error) => { + this.toastService.error('Error resetting password ' + error.error); + console.error('Error resetting password:', error.error); + } + }); + } + } +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index f9bae57..43be3bb 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -14,7 +14,7 @@ export class AuthService { private baseUrl = environment.apiUrl; // Replace with your actual API base URL private authtokenName = environment.tokenName; - constructor(private http: HttpClient, private router: Router) {} + constructor(private http: HttpClient, private router: Router) { } registerUser(data: any): Observable { return this.http.post(`${this.baseUrl}/users/register`, data); @@ -73,4 +73,20 @@ export class AuthService { logOutUser() { return this.http.get(`${this.baseUrl}/users/logout`); } + + // Password reset + sendOtpForPasswordReset(identifier: string): Observable { + const body = { identifier: identifier }; + return this.http.post(`${this.baseUrl}/auth/password-reset/send-otp`, body); + } + + verifyOtpForPasswordReset(identifier: string, otp: string): Observable { + const body = { identifier: identifier, otp: otp }; + return this.http.post(`${this.baseUrl}/auth/password-reset/verify-otp`, body); + } + + resetPassword(identifier: string, resetToken: string, newPassword: string): Observable { + const body = { identifier: identifier, resetToken: resetToken, newPassword: newPassword }; + return this.http.post(`${this.baseUrl}/auth/password-reset`, body); + } } diff --git a/src/app/util/formutil.ts b/src/app/util/formutil.ts new file mode 100644 index 0000000..2112277 --- /dev/null +++ b/src/app/util/formutil.ts @@ -0,0 +1,24 @@ +import { FormGroup } from "@angular/forms"; + +export function passwordMismatch( + controlName: string, + matchingControlName: string +): any { + return (formGroup: FormGroup) => { + const control = formGroup.controls[controlName]; + const matchingControl = formGroup.controls[matchingControlName]; + + if (matchingControl.errors && !matchingControl.errors.passwordMismatch) { + return; + } + + if (control.value !== matchingControl.value) { + matchingControl.setErrors({ passwordMismatch: true }); + } else { + matchingControl.setErrors(null); + } + }; +} + + +export const StrongPasswordRegx: RegExp = /^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d).{8,}$/; \ No newline at end of file