Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Password reset start component #1059

Merged
merged 4 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading