From fa866e330f3d2b83c1c19081398c018b131c9743 Mon Sep 17 00:00:00 2001 From: Zahra Masoumi <102908292+asAlwaysZahra@users.noreply.github.com> Date: Mon, 9 Sep 2024 04:58:28 +0330 Subject: [PATCH] feat(user): Recover and reset pass added * fix(login): fix recover pass (get email) page * feat(login): reset pass component added * fix(test): fix new components test --- api-config/api-url.ts | 4 +- src/app/app-routing.module.ts | 6 + .../recover-pass-form.component.html | 40 +----- .../recover-pass-form.component.spec.ts | 3 + .../recover-pass-form.component.ts | 45 ++++--- .../reset-password.component.html | 82 ++++++++++++ .../reset-password.component.scss | 119 ++++++++++++++++++ .../reset-password.component.spec.ts | 51 ++++++++ .../reset-password.component.ts | 119 ++++++++++++++++++ src/app/user/models/User.ts | 12 +- src/app/user/services/user/user.service.ts | 28 +++-- src/app/user/user.module.ts | 2 + 12 files changed, 444 insertions(+), 67 deletions(-) create mode 100644 src/app/user/components/login/reset-password/reset-password.component.html create mode 100644 src/app/user/components/login/reset-password/reset-password.component.scss create mode 100644 src/app/user/components/login/reset-password/reset-password.component.spec.ts create mode 100644 src/app/user/components/login/reset-password/reset-password.component.ts diff --git a/api-config/api-url.ts b/api-config/api-url.ts index e8dd07c..5ee4a0e 100644 --- a/api-config/api-url.ts +++ b/api-config/api-url.ts @@ -1,4 +1,4 @@ export const environment = { - // apiUrl: 'https://localhost:44322', - apiUrl: 'http://localhost:8085', + apiUrl: 'https://localhost:44322', + // apiUrl: 'http://localhost:8085', }; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index aef76c8..04b9e01 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -14,6 +14,7 @@ import { AssignFileComponent } from './user/components/dashboard/assign-file/ass import { CategoryComponent } from './graph/components/category/category.component'; import { RecoverPassFormComponent } from './user/components/login/recover-pass-form/recover-pass-form.component'; import { LoginFormComponent } from './user/components/login/login-form/login-form.component'; +import { ResetPasswordComponent } from './user/components/login/reset-password/reset-password.component'; const routes: Routes = [ { @@ -84,6 +85,11 @@ const routes: Routes = [ }, ], }, + { + path: 'reset-password', + component: ResetPasswordComponent, + title: 'StarData | Reset Password', + }, { path: '', component: AppComponent, diff --git a/src/app/user/components/login/recover-pass-form/recover-pass-form.component.html b/src/app/user/components/login/recover-pass-form/recover-pass-form.component.html index 6d02e92..966ab1b 100644 --- a/src/app/user/components/login/recover-pass-form/recover-pass-form.component.html +++ b/src/app/user/components/login/recover-pass-form/recover-pass-form.component.html @@ -4,52 +4,24 @@

StarData

Recover Password

+

+ Enter your email address and we will send you a link to reset your password. +

Recovery Email - + - @if (isTrueRecoverCode) { - - New Password - - - - } diff --git a/src/app/user/components/login/recover-pass-form/recover-pass-form.component.spec.ts b/src/app/user/components/login/recover-pass-form/recover-pass-form.component.spec.ts index 7fc78e2..2789bd9 100644 --- a/src/app/user/components/login/recover-pass-form/recover-pass-form.component.spec.ts +++ b/src/app/user/components/login/recover-pass-form/recover-pass-form.component.spec.ts @@ -8,6 +8,8 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatButtonModule } from '@angular/material/button'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideHttpClient } from '@angular/common/http'; describe('RecoverPassFormComponent', () => { let component: RecoverPassFormComponent; @@ -25,6 +27,7 @@ describe('RecoverPassFormComponent', () => { MatButtonModule, BrowserAnimationsModule, ], + providers: [provideHttpClient(), provideHttpClientTesting()], }).compileComponents(); fixture = TestBed.createComponent(RecoverPassFormComponent); diff --git a/src/app/user/components/login/recover-pass-form/recover-pass-form.component.ts b/src/app/user/components/login/recover-pass-form/recover-pass-form.component.ts index b291b25..1795031 100644 --- a/src/app/user/components/login/recover-pass-form/recover-pass-form.component.ts +++ b/src/app/user/components/login/recover-pass-form/recover-pass-form.component.ts @@ -1,6 +1,9 @@ -import { Component, signal } from '@angular/core'; +import { Component } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; import { LoadingService } from '../../../../shared/services/loading.service'; +import { UserService } from '../../../services/user/user.service'; +import { DangerSuccessNotificationComponent } from '../../../../shared/components/danger-success-notification/danger-success-notification.component'; +import { Router } from '@angular/router'; @Component({ selector: 'app-recover-pass-form', @@ -8,33 +11,41 @@ import { LoadingService } from '../../../../shared/services/loading.service'; styleUrl: './recover-pass-form.component.scss', }) export class RecoverPassFormComponent { - hide = signal(true); - username = ''; - password = ''; isLoading = false; recover_email = ''; - isTrueRecoverCode = false; constructor( private _snackBar: MatSnackBar, private loadingService: LoadingService, + private userService: UserService, + private router: Router, ) { this.loadingService.setLoading(false); } recoverClick(event: Event) { event.preventDefault(); - console.log(1); - } - - sendCodeClick(event: Event) { - event.preventDefault(); - console.log(2); - this.isTrueRecoverCode = true; - } - - hidePassClick(event: MouseEvent) { - this.hide.set(!this.hide()); - event.stopPropagation(); + this.isLoading = true; + this.userService.requestResetPassword(this.recover_email).subscribe({ + next: () => { + this._snackBar.openFromComponent(DangerSuccessNotificationComponent, { + data: 'Password reset link sent to your email.\nPlease check your email.', + panelClass: ['notification-class-success'], + duration: 5000, + }); + this.router.navigate(['/login']); + this.loadingService.setLoading(false); + this.isLoading = false; + }, + error: (error) => { + this._snackBar.openFromComponent(DangerSuccessNotificationComponent, { + data: error.error.message, + panelClass: ['notification-class-danger'], + duration: 2000, + }); + this.loadingService.setLoading(false); + this.isLoading = false; + }, + }); } } diff --git a/src/app/user/components/login/reset-password/reset-password.component.html b/src/app/user/components/login/reset-password/reset-password.component.html new file mode 100644 index 0000000..c2ee5b2 --- /dev/null +++ b/src/app/user/components/login/reset-password/reset-password.component.html @@ -0,0 +1,82 @@ +
+ +
+
+ +
diff --git a/src/app/user/components/login/reset-password/reset-password.component.scss b/src/app/user/components/login/reset-password/reset-password.component.scss new file mode 100644 index 0000000..5828701 --- /dev/null +++ b/src/app/user/components/login/reset-password/reset-password.component.scss @@ -0,0 +1,119 @@ +.container { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: var(--mat-app-background-color); + color: var(--mat-sidenav-content-text-color); + gap: 1rem; + + .form-container { + display: flex; + justify-content: flex-start; + flex-direction: column; + gap: 1rem; + width: 35rem; + + .logo { + display: flex; + align-items: center; + gap: 0.5rem; + + .logo-text { + font-size: 1.6rem; + margin: 0; + } + + .logo-image { + height: 45px; + filter: var(--logo-filter); + } + } + + > h2 { + font-weight: 600; + font-size: 2rem; + } + + form { + display: flex; + justify-content: center; + align-items: flex-start; + flex-direction: column; + width: 100%; + + .form-field { + width: 100%; + } + + > button { + font-size: 1rem; + width: 100%; + height: 3.3rem; + border-radius: 4rem; + margin-block: 1.6rem; + } + + > .forget { + display: flex; + gap: 0.4rem; + font-size: 0.8rem; + + .recover { + text-decoration: none; + color: var(--mdc-filled-button-container-color); + font-weight: 550; + cursor: pointer; + position: relative; + + &::after { + content: ""; + position: absolute; + width: 100%; + transform: scaleX(0); + height: 1px; + bottom: 0; + left: 0; + background-color: var(--mdc-filled-button-container-color); + transform-origin: bottom right; + transition: transform 0.25s ease-out; + } + + &:hover::after { + transform: scaleX(1); + transform-origin: bottom left; + } + } + } + } + } + + .login-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + + ul { + list-style-type: disc; + } + } + + .vertical-line { + height: 600px; + //border-left: 1px solid var(--mat-sidenav-content-text-color); + } + + #network { + height: 100%; + width: 100%; + } + + .theme-changer-container { + position: fixed; + bottom: 3rem; + left: 3rem; + } +} diff --git a/src/app/user/components/login/reset-password/reset-password.component.spec.ts b/src/app/user/components/login/reset-password/reset-password.component.spec.ts new file mode 100644 index 0000000..0b39a91 --- /dev/null +++ b/src/app/user/components/login/reset-password/reset-password.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResetPasswordComponent } from './reset-password.component'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of } from 'rxjs'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { SharedModule } from '../../../../shared/shared.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('ResetPasswordComponent', () => { + let component: ResetPasswordComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ResetPasswordComponent], + imports: [ + SharedModule, + MatFormFieldModule, + MatIconModule, + ReactiveFormsModule, + FormsModule, + MatInputModule, + BrowserAnimationsModule, + ], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { + provide: ActivatedRoute, + useValue: { + queryParams: of({ token: '123', email: 'a.a@a.com' }), + }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ResetPasswordComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/user/components/login/reset-password/reset-password.component.ts b/src/app/user/components/login/reset-password/reset-password.component.ts new file mode 100644 index 0000000..0b6991d --- /dev/null +++ b/src/app/user/components/login/reset-password/reset-password.component.ts @@ -0,0 +1,119 @@ +import { + AfterViewInit, + Component, + ElementRef, + signal, + ViewChild, +} from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { LoadingService } from '../../../../shared/services/loading.service'; +import { UserService } from '../../../services/user/user.service'; +import { DangerSuccessNotificationComponent } from '../../../../shared/components/danger-success-notification/danger-success-notification.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ForgetPasswordRequest } from '../../../models/User'; +import { Data, DataSet, Edge, Network, Node } from 'vis'; +import { ThemeService } from '../../../../shared/services/theme.service'; +import { getOptions, GRAPH_EDGES, GRAPH_NODES } from '../login-graph'; + +@Component({ + selector: 'app-reset-password', + templateUrl: './reset-password.component.html', + styleUrl: './reset-password.component.scss', +}) +export class ResetPasswordComponent implements AfterViewInit { + hide = signal(true); + password = ''; + confirmPassword = ''; + isLoading = false; + @ViewChild('network') el!: ElementRef; + private networkInstance!: Network; + + constructor( + private _snackBar: MatSnackBar, + private loadingService: LoadingService, + private userService: UserService, + private router: Router, + private route: ActivatedRoute, + private themeService: ThemeService, + ) { + this.loadingService.setLoading(false); + } + + hidePassClick(event: MouseEvent) { + this.hide.set(!this.hide()); + event.stopPropagation(); + } + + resetClick() { + this.isLoading = true; + this.loadingService.setLoading(true); + + const request: ForgetPasswordRequest = { + email: this.route.snapshot.queryParams['email'], + resetPasswordToken: this.route.snapshot.queryParams['token'], + newPassword: this.password, + confirmPassword: this.confirmPassword, + }; + + this.userService.resetPassword(request).subscribe({ + next: () => { + this._snackBar.openFromComponent(DangerSuccessNotificationComponent, { + data: 'Password reset successfully. Now you can login with your new password.', + panelClass: ['notification-class-success'], + duration: 5000, + }); + this.router.navigate(['/login']); + this.loadingService.setLoading(false); + this.isLoading = false; + }, + error: (error) => { + this._snackBar.openFromComponent(DangerSuccessNotificationComponent, { + data: error.error.message, + panelClass: ['notification-class-danger'], + duration: 2000, + }); + this.loadingService.setLoading(false); + this.isLoading = false; + }, + }); + } + + changeTheme() { + this.themeService.changeThemeState(); + this.themeService.theme$.subscribe((data) => { + const themeChanger = document.getElementById( + 'theme-changer-icon', + ) as HTMLElement; + themeChanger.textContent = data === 'dark' ? 'light_mode' : 'dark_mode'; + this.networkInstance.setOptions({ + nodes: { + font: { + color: data === 'dark' ? 'rgba(255,255,255,0.9)' : '#424242', + }, + }, + }); + }); + } + + ngAfterViewInit() { + const dataSetValue = document.body.getAttribute('data-theme'); + const labelColor: string = + dataSetValue == 'dark' ? 'rgba(255,255,255,0.9)' : '#424242'; + + const container = this.el.nativeElement; + + this.createGraph(labelColor, container); + } + + private createGraph(labelColor: string, container: HTMLElement) { + const nodes = new DataSet(GRAPH_NODES as unknown as Node[]); + const edges = new DataSet(GRAPH_EDGES as Edge[]); + const data: Data = { nodes, edges }; + this.networkInstance = new Network(container, data, getOptions(labelColor)); + + this.networkInstance.moveTo({ + animation: true, + scale: 0.1, + }); + } +} diff --git a/src/app/user/models/User.ts b/src/app/user/models/User.ts index 8adb6db..1317fde 100644 --- a/src/app/user/models/User.ts +++ b/src/app/user/models/User.ts @@ -32,14 +32,16 @@ export interface UpdateUserRequest { } export interface ForgetPasswordRequest { - newPassword: 'string'; - confirmPassword: 'string'; + email: string; + resetPasswordToken: string; + newPassword: string; + confirmPassword: string; } export interface NewPasswordRequest { - oldPassword: 'string'; - newPassword: 'string'; - confirmPassword: 'string'; + oldPassword: string; + newPassword: string; + confirmPassword: string; } export interface Role { diff --git a/src/app/user/services/user/user.service.ts b/src/app/user/services/user/user.service.ts index fd70039..d882979 100644 --- a/src/app/user/services/user/user.service.ts +++ b/src/app/user/services/user/user.service.ts @@ -1,10 +1,10 @@ -import {Injectable} from '@angular/core'; -import {HttpClient} from '@angular/common/http'; -import {Observable} from 'rxjs'; -import {ForgetPasswordRequest, NewPasswordRequest} from '../../models/User'; -import {environment} from '../../../../../api-config/api-url'; -import {LoadingService} from '../../../shared/services/loading.service'; -import {UserInformation} from '../../models/ManageUsers'; +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ForgetPasswordRequest, NewPasswordRequest } from '../../models/User'; +import { environment } from '../../../../../api-config/api-url'; +import { LoadingService } from '../../../shared/services/loading.service'; +import { UserInformation } from '../../models/ManageUsers'; @Injectable({ providedIn: 'root', @@ -15,10 +15,20 @@ export class UserService { constructor( private http: HttpClient, private loadingService: LoadingService, - ) { + ) {} + + requestResetPassword(email: string) { + this.loadingService.setLoading(true); + return this.http.post( + `${this.apiUrl}/request-reset-password`, + { email }, + { + withCredentials: true, + }, + ); } - forgetPassword(request: ForgetPasswordRequest): Observable { + resetPassword(request: ForgetPasswordRequest): Observable { this.loadingService.setLoading(true); return this.http.post(`${this.apiUrl}/reset-password`, request, { withCredentials: true, diff --git a/src/app/user/user.module.ts b/src/app/user/user.module.ts index 7a70d7a..dad7502 100644 --- a/src/app/user/user.module.ts +++ b/src/app/user/user.module.ts @@ -38,6 +38,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { LoginFormComponent } from './components/login/login-form/login-form.component'; import { RecoverPassFormComponent } from './components/login/recover-pass-form/recover-pass-form.component'; import { ProfileHeaderComponent } from './components/dashboard/manage-account/profile-header/profile-header.component'; +import { ResetPasswordComponent } from './components/login/reset-password/reset-password.component'; @NgModule({ declarations: [ @@ -54,6 +55,7 @@ import { ProfileHeaderComponent } from './components/dashboard/manage-account/pr LoginFormComponent, RecoverPassFormComponent, ProfileHeaderComponent, + ResetPasswordComponent, ], imports: [ CommonModule,