Skip to content

Commit

Permalink
Merge pull request #1059 from ORCID/passwordResetStartComponent
Browse files Browse the repository at this point in the history
Password reset start component
  • Loading branch information
auumgn authored Nov 13, 2023
2 parents 9805a00 + fdaeef9 commit c85e9e1
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 2 deletions.
5 changes: 3 additions & 2 deletions ui/src/app/account/account.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
9 changes: 9 additions & 0 deletions ui/src/app/account/account.route.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
]
46 changes: 46 additions & 0 deletions ui/src/app/account/password/password-reset-init.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<div class="container m-3">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 jhiTranslate="reset.request.title.string">Reset your password</h1>

<div class="alert alert-danger" jhiTranslate="reset.request.messages.notfound.string" *ngIf="errorEmailNotExists">
<strong>Email address isn't registered!</strong> Please check and try again.
</div>

<div class="alert alert-warning" *ngIf="!success">
<p jhiTranslate="reset.request.messages.info.string">Enter the email address you used to register.</p>
</div>

<div class="alert alert-success" *ngIf="success === 'OK'">
<p jhiTranslate="reset.request.messages.success.string">Check your emails for details on how to reset your password.</p>
</div>

<form *ngIf="!success" name="form" role="form" (ngSubmit)="requestReset()" [formGroup]="resetRequestForm">
<div class="form-group">
<label class="form-control-label" for="email" jhiTranslate="global.form.email.label.string">Email</label>
<input type="email" class="form-control" id="email" name="email" placeholder="{{'global.form.email.placeholder.string'}}"
formControlName="email">
<div *ngIf="resetRequestForm.get('email')?.invalid && (resetRequestForm.get('email')?.dirty || resetRequestForm.get('email')?.touched)">
<small class="form-text text-danger"
*ngIf="resetRequestForm.get('email')?.errors!['required']" jhiTranslate="global.messages.validate.email.required.string">
Your email is required.
</small>
<small class="form-text text-danger"
*ngIf="resetRequestForm.get('email')?.errors!['email']" jhiTranslate="global.messages.validate.email.invalid.string">
Your email is invalid.
</small>
<small class="form-text text-danger"
*ngIf="resetRequestForm.get('email')?.errors!['minlength']" jhiTranslate="global.messages.validate.email.minlength.string">
Your email is required to be at least 5 characters.
</small>
<small class="form-text text-danger" id="maxlengthError"
*ngIf="resetRequestForm.get('email')?.errors!['maxlength']" jhiTranslate="global.messages.validate.email.maxlength.string">
Your email cannot be longer than 100 characters.
</small>
</div>
</div>
<button type="submit" id="reset" [disabled]="resetRequestForm.invalid" class="btn btn-primary" jhiTranslate="reset.request.form.button.string">Reset</button>
</form>
</div>
</div>
</div>
146 changes: 146 additions & 0 deletions ui/src/app/account/password/password-reset-init.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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<PasswordResetInitComponent>
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: '[email protected]',
})

comp.requestReset()
const emailControl = comp.resetRequestForm.get('email')!

Check warning on line 37 in ui/src/app/account/password/password-reset-init.component.spec.ts

View workflow job for this annotation

GitHub Actions / format

Forbidden non-null assertion
emailControl.setValue('[email protected]')
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: '[email protected]',
})
comp.requestReset()

expect(service.initPasswordReset).toHaveBeenCalledWith('[email protected]')
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: '[email protected]',
})
comp.requestReset()

expect(service.initPasswordReset).toHaveBeenCalledWith('[email protected]')
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')!

Check warning on line 94 in ui/src/app/account/password/password-reset-init.component.spec.ts

View workflow job for this annotation

GitHub Actions / format

Forbidden non-null assertion
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')!

Check warning on line 107 in ui/src/app/account/password/password-reset-init.component.spec.ts

View workflow job for this annotation

GitHub Actions / format

Forbidden non-null assertion
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()
})
})
})
49 changes: 49 additions & 0 deletions ui/src/app/account/password/password-reset-init.component.ts
Original file line number Diff line number Diff line change
@@ -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'
}
},
})
}
}
}
12 changes: 12 additions & 0 deletions ui/src/app/account/service/password-reset-init.service.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
return this.http.post('/services/userservice/api/account/reset-password/init', mail)
}
}
2 changes: 2 additions & 0 deletions ui/src/app/app.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
4 changes: 4 additions & 0 deletions ui/src/app/layout/footer/footer.component.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
:host {
margin-top: auto;
}


a {
color: rgba(0,0,0,0.87);
Expand Down
6 changes: 6 additions & 0 deletions ui/src/content/scss/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
/* ==============================================================
Bootstrap tweaks
===============================================================*/
app-root {
min-height: 100vh;
display: flex;
flex-direction: column;
}

html,
body {
height: 100%;
Expand Down

0 comments on commit c85e9e1

Please sign in to comment.