feat: create PasswordRequestComponent and integrate into ForgotPasswordComponent to separate responsibilities
This commit is contained in:
parent
044ad477f2
commit
a8a74761b4
7 changed files with 237 additions and 99 deletions
|
|
@ -18,7 +18,7 @@ import { emailFormatValidator } from '../../../../validators/email-format.valida
|
||||||
styleUrl: './email-request.component.scss',
|
styleUrl: './email-request.component.scss',
|
||||||
})
|
})
|
||||||
export class EmailRequestComponent implements OnInit {
|
export class EmailRequestComponent implements OnInit {
|
||||||
@Output() submittedChange = new EventEmitter<boolean>();
|
@Output() submittedChangeEmail = new EventEmitter<boolean>();
|
||||||
|
|
||||||
form: FormGroup = new FormGroup({});
|
form: FormGroup = new FormGroup({});
|
||||||
|
|
||||||
|
|
@ -86,6 +86,6 @@ export class EmailRequestComponent implements OnInit {
|
||||||
* Emits an event indicating that the form was successfully submitted.
|
* Emits an event indicating that the form was successfully submitted.
|
||||||
*/
|
*/
|
||||||
private emitFormSubmitted(): void {
|
private emitFormSubmitted(): void {
|
||||||
this.submittedChange.emit(true);
|
this.submittedChangeEmail.emit(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<section class="login-background">
|
<section class="login-background">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="headline">Forgot your password?</div>
|
<div class="headline">Forgot your password?</div>
|
||||||
@if (!queryEmail && !sendEmailSuccess && !queryEmailSuccess) {
|
@if (!queryEmail && !emailSendSuccess && !passwordChangeSuccess) {
|
||||||
<app-email-request
|
<app-email-request
|
||||||
(submittedChange)="submittedChange($event)"
|
(submittedChangeEmail)="submittedChangeEmail($event)"
|
||||||
></app-email-request>
|
></app-email-request>
|
||||||
} @else { @if (sendEmailSuccess) {
|
} @else { @if (emailSendSuccess) {
|
||||||
<div class="note">
|
<div class="note">
|
||||||
<p>
|
<p>
|
||||||
If the email you entered exists on our server, you will receive a
|
If the email you entered exists on our server, you will receive a
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
</p>
|
</p>
|
||||||
<a routerLink="/login">To the login</a>
|
<a routerLink="/login">To the login</a>
|
||||||
</div>
|
</div>
|
||||||
} @if (queryEmailSuccess) {
|
} @if (passwordChangeSuccess) {
|
||||||
<div class="note">
|
<div class="note">
|
||||||
<p>
|
<p>
|
||||||
Password change successful.<br />You can now log in with your new
|
Password change successful.<br />You can now log in with your new
|
||||||
|
|
@ -21,88 +21,12 @@
|
||||||
</p>
|
</p>
|
||||||
<a routerLink="/login">To the login</a>
|
<a routerLink="/login">To the login</a>
|
||||||
</div>
|
</div>
|
||||||
} @if (queryEmail && !queryEmailSuccess) {
|
} @if (queryEmail && !passwordChangeSuccess) {
|
||||||
<div class="note">
|
<app-password-request
|
||||||
<p>Please enter your new password and confirm it.</p>
|
[queryData]="queryData"
|
||||||
</div>
|
(submittedChangePassword)="submittedChangePassword($event)"
|
||||||
<form
|
></app-password-request>
|
||||||
#taskForm="ngForm"
|
|
||||||
(ngSubmit)="onSubmit(taskForm, password)"
|
|
||||||
onsubmit="return false"
|
|
||||||
>
|
|
||||||
<div class="password-field">
|
|
||||||
<input
|
|
||||||
[type]="authService.passwordFieldType"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
#password="ngModel"
|
|
||||||
placeholder="Enter a password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
[(ngModel)]="authData.password"
|
|
||||||
pattern="[^<>]*"
|
|
||||||
minlength="6"
|
|
||||||
[class.error-border]="
|
|
||||||
password.invalid && password.touched && authData.password.length < 6
|
|
||||||
"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<img class="password-icon" src="./assets/img/password.svg" />
|
|
||||||
<img
|
|
||||||
class="password-eye"
|
|
||||||
(click)="authService.togglePasswordVisibility()"
|
|
||||||
[src]="authService.passwordIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="error-msg">
|
|
||||||
@if (!password.valid && password.touched && authData.password.length <
|
|
||||||
1) {
|
|
||||||
<p>Please enter a password</p>
|
|
||||||
} @else { @if (authData.password && authData.password.length < 6 &&
|
|
||||||
password.touched) {
|
|
||||||
<p>Password is too short, min 6 characters</p>
|
|
||||||
} }
|
|
||||||
</div>
|
|
||||||
<div class="password-field">
|
|
||||||
<input
|
|
||||||
[type]="authService.passwordFieldType"
|
|
||||||
id="passwordConfirm"
|
|
||||||
name="passwordConfirm"
|
|
||||||
#passwordConfirm="ngModel"
|
|
||||||
placeholder="Confirm password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
[(ngModel)]="authData.passwordConfirm"
|
|
||||||
pattern="[^<>]*"
|
|
||||||
minlength="6"
|
|
||||||
[class.error-border]="
|
|
||||||
authData.password !== authData.passwordConfirm &&
|
|
||||||
passwordConfirm.touched
|
|
||||||
"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<img class="password-icon" src="./assets/img/password.svg" />
|
|
||||||
<img
|
|
||||||
class="password-eye"
|
|
||||||
(click)="authService.togglePasswordVisibility()"
|
|
||||||
[src]="authService.passwordIcon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="error-msg">
|
|
||||||
@if (authData.password !== authData.passwordConfirm &&
|
|
||||||
passwordConfirm.touched) {
|
|
||||||
<p>The passwords do not match</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<app-btn-large
|
|
||||||
[value]="'Update Password'"
|
|
||||||
[disabled]="
|
|
||||||
!isUserEmailValid(authData.email) ||
|
|
||||||
authData.password !== authData.passwordConfirm ||
|
|
||||||
!authData.password ||
|
|
||||||
!authData.passwordConfirm ||
|
|
||||||
authData.send
|
|
||||||
"
|
|
||||||
></app-btn-large>
|
|
||||||
</form>
|
|
||||||
} }
|
} }
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,25 @@ import { ActivatedRoute, Params, RouterLink } from '@angular/router';
|
||||||
import { AuthService } from '../../../services/auth.service';
|
import { AuthService } from '../../../services/auth.service';
|
||||||
import { ErrorService } from '../../../services/error.service';
|
import { ErrorService } from '../../../services/error.service';
|
||||||
import { EmailRequestComponent } from './email-request/email-request.component';
|
import { EmailRequestComponent } from './email-request/email-request.component';
|
||||||
|
import { PasswordRequestComponent } from './password-request/password-request.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-forgot-password',
|
selector: 'app-forgot-password',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
BtnLargeComponent,
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
EmailRequestComponent,
|
EmailRequestComponent,
|
||||||
|
PasswordRequestComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './forgot-password.component.html',
|
templateUrl: './forgot-password.component.html',
|
||||||
styleUrl: './forgot-password.component.scss',
|
styleUrl: './forgot-password.component.scss',
|
||||||
})
|
})
|
||||||
export class ForgotPasswordComponent implements OnInit {
|
export class ForgotPasswordComponent implements OnInit {
|
||||||
sendEmailSuccess: boolean = false;
|
emailSendSuccess: boolean = false;
|
||||||
|
passwordChangeSuccess: boolean = false;
|
||||||
|
|
||||||
queryEmail: boolean = false;
|
queryEmail: boolean = false;
|
||||||
queryEmailSuccess: boolean = false;
|
queryEmailSuccess: boolean = false;
|
||||||
|
|
||||||
|
|
@ -29,10 +32,14 @@ export class ForgotPasswordComponent implements OnInit {
|
||||||
email: '',
|
email: '',
|
||||||
token: '',
|
token: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordConfirm: '',
|
|
||||||
send: false,
|
send: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
queryData = {
|
||||||
|
email: '',
|
||||||
|
token: '',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the ForgotPasswordComponent with ActivatedRoute, AuthService, and ErrorService.
|
* Initializes the ForgotPasswordComponent with ActivatedRoute, AuthService, and ErrorService.
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,8 +57,12 @@ export class ForgotPasswordComponent implements OnInit {
|
||||||
this.handleQueryParams();
|
this.handleQueryParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
submittedChange(value: boolean): void {
|
submittedChangeEmail(success: boolean): void {
|
||||||
this.sendEmailSuccess = value;
|
this.emailSendSuccess = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
submittedChangePassword(success: boolean): void {
|
||||||
|
this.passwordChangeSuccess = success;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -60,8 +71,7 @@ export class ForgotPasswordComponent implements OnInit {
|
||||||
private handleQueryParams(): void {
|
private handleQueryParams(): void {
|
||||||
this.route.queryParams.subscribe((params) => {
|
this.route.queryParams.subscribe((params) => {
|
||||||
this.extractAuthParams(params);
|
this.extractAuthParams(params);
|
||||||
this.queryEmailSuccess = params['pw-change'] || '';
|
if (this.queryData.email && this.queryData.token) {
|
||||||
if (this.authData.email && this.authData.token) {
|
|
||||||
this.queryEmail = true;
|
this.queryEmail = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -73,8 +83,8 @@ export class ForgotPasswordComponent implements OnInit {
|
||||||
* @param params The query parameters from the URL.
|
* @param params The query parameters from the URL.
|
||||||
*/
|
*/
|
||||||
private extractAuthParams(params: Params): void {
|
private extractAuthParams(params: Params): void {
|
||||||
this.authData.email = params['email'] || '';
|
this.queryData.email = params['email'] || '';
|
||||||
this.authData.token = params['token'] || '';
|
this.queryData.token = params['token'] || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -120,11 +130,11 @@ export class ForgotPasswordComponent implements OnInit {
|
||||||
this.authData.send = true;
|
this.authData.send = true;
|
||||||
try {
|
try {
|
||||||
await this.authService.forgotPassword(body);
|
await this.authService.forgotPassword(body);
|
||||||
this.sendEmailSuccess = true;
|
this.emailSendSuccess = true;
|
||||||
this.errorService.clearError();
|
this.errorService.clearError();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.authData.send = false;
|
this.authData.send = false;
|
||||||
this.sendEmailSuccess = false;
|
this.emailSendSuccess = false;
|
||||||
this.errorService.handleError(error);
|
this.errorService.handleError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<div class="note">
|
||||||
|
<p>Please enter your new password and confirm it.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||||
|
<div class="password-field">
|
||||||
|
<input
|
||||||
|
[type]="authService.passwordFieldType"
|
||||||
|
id="password"
|
||||||
|
formControlName="password"
|
||||||
|
placeholder="Enter a password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
minlength="8"
|
||||||
|
[class.error-border]="
|
||||||
|
form.controls['password'].invalid && form.controls['password'].touched
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<img class="password-icon" src="./assets/img/password.svg" />
|
||||||
|
<img
|
||||||
|
class="password-eye"
|
||||||
|
(click)="authService.togglePasswordVisibility()"
|
||||||
|
[src]="authService.passwordIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-container">
|
||||||
|
@if (form.controls['password'].touched && form.controls['password'].invalid)
|
||||||
|
{
|
||||||
|
<div class="error-msg">
|
||||||
|
@if (form.controls['password'].errors?.['required']) {
|
||||||
|
<p role="alert">Please enter a password</p>
|
||||||
|
} @if (form.controls['password'].errors?.['minlength']) {
|
||||||
|
<p role="alert">Password is too short, min 8 characters</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="password-field">
|
||||||
|
<input
|
||||||
|
[type]="authService.passwordFieldType"
|
||||||
|
id="passwordConfirm"
|
||||||
|
formControlName="passwordConfirm"
|
||||||
|
placeholder="Confirm password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
minlength="8"
|
||||||
|
[class.error-border]="
|
||||||
|
form.controls['passwordConfirm'].invalid &&
|
||||||
|
form.controls['passwordConfirm'].touched
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<img class="password-icon" src="./assets/img/password.svg" />
|
||||||
|
<img
|
||||||
|
class="password-eye"
|
||||||
|
(click)="authService.togglePasswordVisibility()"
|
||||||
|
[src]="authService.passwordIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-container">
|
||||||
|
@if ( form.controls['passwordConfirm'].touched &&
|
||||||
|
form.controls['passwordConfirm'].invalid ) {
|
||||||
|
<div class="error-msg">
|
||||||
|
@if (form.controls['passwordConfirm'].errors?.['required']) {
|
||||||
|
<p role="alert">Please confirm your password</p>
|
||||||
|
} @if ( form.controls['password'].value !==
|
||||||
|
form.controls['passwordConfirm'].value &&
|
||||||
|
form.controls['passwordConfirm'].touched ) {
|
||||||
|
<p role="alert">The passwords do not match</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-btn-large
|
||||||
|
[value]="'Update Password'"
|
||||||
|
[disabled]="form.invalid || form.disabled"
|
||||||
|
></app-btn-large>
|
||||||
|
</form>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@use "./../../../../../assets/style/backgrounds.scss" as *;
|
||||||
|
@use "./../../../../../assets/style/form.scss" as *;
|
||||||
|
@use "./../../../../../assets/style/auth-layout.scss" as *;
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PasswordRequestComponent } from './password-request.component';
|
||||||
|
|
||||||
|
describe('PasswordRequestComponent', () => {
|
||||||
|
let component: PasswordRequestComponent;
|
||||||
|
let fixture: ComponentFixture<PasswordRequestComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PasswordRequestComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PasswordRequestComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { BtnLargeComponent } from '../../../../shared/components/buttons/btn-large/btn-large.component';
|
||||||
|
import {
|
||||||
|
FormBuilder,
|
||||||
|
FormGroup,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
Validators,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { AuthService } from '../../../../services/auth.service';
|
||||||
|
import { ErrorService } from '../../../../services/error.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-password-request',
|
||||||
|
imports: [BtnLargeComponent, FormsModule, ReactiveFormsModule],
|
||||||
|
templateUrl: './password-request.component.html',
|
||||||
|
styleUrl: './password-request.component.scss',
|
||||||
|
})
|
||||||
|
export class PasswordRequestComponent {
|
||||||
|
@Input() queryData!: { email: string; token: string };
|
||||||
|
|
||||||
|
@Output() submittedChangePassword = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
form: FormGroup = new FormGroup({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the PasswordRequestComponent with FormBuilder, AuthService, and ErrorService.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private fb: FormBuilder,
|
||||||
|
public authService: AuthService,
|
||||||
|
private errorService: ErrorService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle hook that initializes the component.
|
||||||
|
* Initializes the form on component load.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.createForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the form submission for password change.
|
||||||
|
* Validates the form, sends a password change request,
|
||||||
|
* and emits a submission event on success.
|
||||||
|
*
|
||||||
|
* @returns A Promise that resolves when the process is complete.
|
||||||
|
*/
|
||||||
|
async onSubmit(): Promise<void> {
|
||||||
|
if (this.form.valid) {
|
||||||
|
const body = this.createFormRequestBody();
|
||||||
|
this.form.disable();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.authService.changePassword(body);
|
||||||
|
this.emitFormSubmitted();
|
||||||
|
} catch (error) {
|
||||||
|
this.errorService.handleError(error);
|
||||||
|
} finally {
|
||||||
|
this.form.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the reactive form with validators.
|
||||||
|
*/
|
||||||
|
private createForm(): void {
|
||||||
|
this.form = this.fb.group({
|
||||||
|
password: ['', Validators.required],
|
||||||
|
passwordConfirm: ['', Validators.required],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the request payload from the form values.
|
||||||
|
*
|
||||||
|
* @returns An object containing the email, token, and password.
|
||||||
|
*/
|
||||||
|
private createFormRequestBody(): {
|
||||||
|
email: string;
|
||||||
|
token: string;
|
||||||
|
password: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
email: this.queryData.email,
|
||||||
|
token: this.queryData.token,
|
||||||
|
password: this.form.value.password,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event indicating that the form was successfully submitted.
|
||||||
|
*/
|
||||||
|
private emitFormSubmitted(): void {
|
||||||
|
this.submittedChangePassword.emit(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue