diff --git a/ui/src/app/account/account.module.ts b/ui/src/app/account/account.module.ts index c626c1d44..59bdfd816 100644 --- a/ui/src/app/account/account.module.ts +++ b/ui/src/app/account/account.module.ts @@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common' import { LoginComponent } from './login/login.component' import { RouterModule } from '@angular/router' import { ReactiveFormsModule } from '@angular/forms' -import { routes } from './account.route' +import { routes } from './account.route'; +import { PasswordResetInitComponent } from './password/password-reset-init.component' @NgModule({ - declarations: [LoginComponent], + declarations: [LoginComponent, PasswordResetInitComponent], imports: [CommonModule, ReactiveFormsModule, RouterModule.forChild(routes)], }) export class AccountModule {} diff --git a/ui/src/app/account/account.route.ts b/ui/src/app/account/account.route.ts index 2ff5186c3..48f4f60fd 100644 --- a/ui/src/app/account/account.route.ts +++ b/ui/src/app/account/account.route.ts @@ -1,9 +1,18 @@ import { Routes } from '@angular/router' import { LoginComponent } from './login/login.component' +import { PasswordResetInitComponent } from './password/password-reset-init.component' export const routes: Routes = [ { path: 'login', component: LoginComponent, }, + { + path: 'reset/request', + component: PasswordResetInitComponent, + data: { + authorities: [], + pageTitle: 'global.menu.account.password.string', + }, + }, ] diff --git a/ui/src/app/account/password/password-reset-init.component.html b/ui/src/app/account/password/password-reset-init.component.html new file mode 100644 index 000000000..c21462f25 --- /dev/null +++ b/ui/src/app/account/password/password-reset-init.component.html @@ -0,0 +1,46 @@ +
+
+
+

Reset your password

+ +
+ Email address isn't registered! Please check and try again. +
+ +
+

Enter the email address you used to register.

+
+ +
+

Check your emails for details on how to reset your password.

+
+ +
+
+ + +
+ + Your email is required. + + + Your email is invalid. + + + Your email is required to be at least 5 characters. + + + Your email cannot be longer than 100 characters. + +
+
+ +
+
+
+
diff --git a/ui/src/app/account/password/password-reset-init.component.spec.ts b/ui/src/app/account/password/password-reset-init.component.spec.ts new file mode 100644 index 000000000..a9f124a90 --- /dev/null +++ b/ui/src/app/account/password/password-reset-init.component.spec.ts @@ -0,0 +1,146 @@ +import { ComponentFixture, TestBed, inject } from '@angular/core/testing' +import { of, throwError } from 'rxjs' + +import { PasswordResetInitService } from '../service/password-reset-init.service' +import { PasswordResetInitComponent } from './password-reset-init.component' +import { EMAIL_NOT_FOUND_TYPE } from 'src/app/app.constants' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { By } from '@angular/platform-browser' + +describe('Component Tests', () => { + describe('PasswordResetInitComponent', () => { + let fixture: ComponentFixture + let comp: PasswordResetInitComponent + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [PasswordResetInitComponent], + }).createComponent(PasswordResetInitComponent) + comp = fixture.componentInstance + }) + + it('should define its initial state', () => { + expect(comp.success).toBeUndefined() + expect(comp.error).toBeUndefined() + expect(comp.errorEmailNotExists).toBeUndefined() + }) + + it('notifies of success upon successful requestReset', inject( + [PasswordResetInitService], + (service: PasswordResetInitService) => { + spyOn(service, 'initPasswordReset').and.returnValue(of({})) + comp.resetRequestForm.patchValue({ + email: 'user@domain.com', + }) + + comp.requestReset() + const emailControl = comp.resetRequestForm.get('email')! + emailControl.setValue('valid@email.com') + fixture.detectChanges() + expect(comp.success).toEqual('OK') + expect(comp.error).toBeUndefined() + expect(comp.errorEmailNotExists).toBeUndefined() + fixture.whenStable().then(() => { + expect(true).toBeFalsy() + const button = fixture.debugElement.query(By.css('#reset')) + expect(button.nativeElement.disabled).toBeFalsy() + }) + } + )) + + it('notifies of unknown email upon email address not registered/400', inject( + [PasswordResetInitService], + (service: PasswordResetInitService) => { + spyOn(service, 'initPasswordReset').and.returnValue( + throwError({ + status: 400, + error: { type: EMAIL_NOT_FOUND_TYPE }, + }) + ) + comp.resetRequestForm.patchValue({ + email: 'user@domain.com', + }) + comp.requestReset() + + expect(service.initPasswordReset).toHaveBeenCalledWith('user@domain.com') + expect(comp.success).toBeUndefined() + expect(comp.error).toBeUndefined() + expect(comp.errorEmailNotExists).toEqual('ERROR') + } + )) + + it('notifies of error upon error response', inject( + [PasswordResetInitService], + (service: PasswordResetInitService) => { + spyOn(service, 'initPasswordReset').and.returnValue( + throwError({ + status: 503, + data: 'something else', + }) + ) + comp.resetRequestForm.patchValue({ + email: 'user@domain.com', + }) + comp.requestReset() + + expect(service.initPasswordReset).toHaveBeenCalledWith('user@domain.com') + expect(comp.success).toBeUndefined() + expect(comp.errorEmailNotExists).toBeUndefined() + expect(comp.error).toEqual('ERROR') + } + )) + + it('should disable the submit button for invalid email address', () => { + const emailControl = comp.resetRequestForm.get('email')! + emailControl.markAsTouched() + emailControl.setValue('invalid-email') + fixture.detectChanges() + const errorMessage = fixture.debugElement.query(By.css('small')) + expect(errorMessage).toBeTruthy() + const errorText = errorMessage.nativeElement.textContent.trim() + expect(errorText).toBe('Your email is invalid.') + const button = fixture.debugElement.query(By.css('#reset')) + expect(button.nativeElement.disabled).toBeTruthy() + }) + + it('should disable the submit button for empty email address field', () => { + const emailControl = comp.resetRequestForm.get('email')! + emailControl.markAsTouched() + fixture.detectChanges() + const errorMessage = fixture.debugElement.query(By.css('small')) + expect(errorMessage).toBeTruthy() + const errorText = errorMessage.nativeElement.textContent.trim() + expect(errorText).toBe('Your email is required.') + const button = fixture.debugElement.query(By.css('#reset')) + expect(button.nativeElement.disabled).toBeTruthy() + }) + + it('should disable the submit button for short email address', () => { + const emailControl = comp.resetRequestForm.get('email')! + emailControl.setValue('i@a') + emailControl.markAsTouched() + fixture.detectChanges() + const errorMessage = fixture.debugElement.query(By.css('small')) + expect(errorMessage).toBeTruthy() + const errorText = errorMessage.nativeElement.textContent.trim() + expect(errorText).toBe('Your email is required to be at least 5 characters.') + const button = fixture.debugElement.query(By.css('#reset')) + expect(button.nativeElement.disabled).toBeTruthy() + }) + + it('should disable the submit button for long email address', () => { + const emailControl = comp.resetRequestForm.get('email')! + emailControl.setValue( + 'abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde@mail.com' + ) + emailControl.markAsTouched() + fixture.detectChanges() + const errorMessage = fixture.debugElement.query(By.css('#maxlengthError')) + expect(errorMessage).toBeTruthy() + const errorText = errorMessage.nativeElement.textContent.trim() + expect(errorText).toBe('Your email cannot be longer than 100 characters.') + const button = fixture.debugElement.query(By.css('#reset')) + expect(button.nativeElement.disabled).toBeTruthy() + }) + }) +}) diff --git a/ui/src/app/account/password/password-reset-init.component.ts b/ui/src/app/account/password/password-reset-init.component.ts new file mode 100644 index 000000000..876147802 --- /dev/null +++ b/ui/src/app/account/password/password-reset-init.component.ts @@ -0,0 +1,49 @@ +import { Component, AfterViewInit, Renderer2 } from '@angular/core' +import { FormBuilder, FormGroup, Validators } from '@angular/forms' + +import { PasswordResetInitService } from '../service/password-reset-init.service' +import { EMAIL_NOT_FOUND_TYPE } from 'src/app/app.constants' + +@Component({ + selector: 'app-password-reset-init', + templateUrl: './password-reset-init.component.html', +}) +export class PasswordResetInitComponent implements AfterViewInit { + error: string | undefined + errorEmailNotExists: string | undefined + success: string | undefined + resetRequestForm = this.fb.group({ + email: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(100), Validators.email]], + }) + + constructor( + private passwordResetInitService: PasswordResetInitService, + private renderer: Renderer2, + private fb: FormBuilder + ) {} + + ngAfterViewInit() { + setTimeout(() => this.renderer.selectRootElement('#email').focus()) + } + + requestReset() { + this.error = undefined + this.errorEmailNotExists = undefined + + if (this.resetRequestForm.get(['email'])) { + this.passwordResetInitService.initPasswordReset(this.resetRequestForm.get(['email'])!.value).subscribe({ + next: () => { + this.success = 'OK' + }, + error: (response) => { + this.success = undefined + if (response.status === 400 && response.error.type === EMAIL_NOT_FOUND_TYPE) { + this.errorEmailNotExists = 'ERROR' + } else { + this.error = 'ERROR' + } + }, + }) + } + } +} diff --git a/ui/src/app/account/service/password-reset-init.service.ts b/ui/src/app/account/service/password-reset-init.service.ts new file mode 100644 index 000000000..965c0802c --- /dev/null +++ b/ui/src/app/account/service/password-reset-init.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@angular/core' +import { HttpClient } from '@angular/common/http' +import { Observable } from 'rxjs' + +@Injectable({ providedIn: 'root' }) +export class PasswordResetInitService { + constructor(private http: HttpClient) {} + + initPasswordReset(mail: string): Observable { + return this.http.post('/services/userservice/api/account/reset-password/init', mail) + } +} diff --git a/ui/src/app/app.constants.ts b/ui/src/app/app.constants.ts index e9f2a8cde..68accdfbb 100644 --- a/ui/src/app/app.constants.ts +++ b/ui/src/app/app.constants.ts @@ -4,3 +4,5 @@ export enum EventType { AFFILIATION_UPDATED = 'AFFILIATION_UPDATED', // add as we go } + +export const EMAIL_NOT_FOUND_TYPE = 'https://www.jhipster.tech/problem/email-not-found' diff --git a/ui/src/app/layout/footer/footer.component.scss b/ui/src/app/layout/footer/footer.component.scss index f22083738..034a12226 100644 --- a/ui/src/app/layout/footer/footer.component.scss +++ b/ui/src/app/layout/footer/footer.component.scss @@ -1,3 +1,7 @@ +:host { + margin-top: auto; +} + a { color: rgba(0,0,0,0.87); diff --git a/ui/src/content/scss/global.scss b/ui/src/content/scss/global.scss index a36a9ea6c..f87e7ad3b 100644 --- a/ui/src/content/scss/global.scss +++ b/ui/src/content/scss/global.scss @@ -7,6 +7,12 @@ /* ============================================================== Bootstrap tweaks ===============================================================*/ +app-root { + min-height: 100vh; + display: flex; + flex-direction: column; +} + html, body { height: 100%;