Преглед на файлове

ajout des pages pour la reinitialisation des mot de passe

master
trauchessec преди 1 седмица
родител
ревизия
c7243df7cd
променени са 13 файла, в които са добавени 766 реда и са изтрити 3 реда
  1. +93
    -3
      src/app/app.config.ts
  2. +161
    -0
      src/app/pages/password-reset/finish/password-reset-finish.component.html
  3. +98
    -0
      src/app/pages/password-reset/finish/password-reset-finish.component.spec.ts
  4. +78
    -0
      src/app/pages/password-reset/finish/password-reset-finish.component.ts
  5. +11
    -0
      src/app/pages/password-reset/finish/password-reset-finish.route.ts
  6. +45
    -0
      src/app/pages/password-reset/finish/password-reset-finish.service.spec.ts
  7. +15
    -0
      src/app/pages/password-reset/finish/password-reset-finish.service.ts
  8. +88
    -0
      src/app/pages/password-reset/init/password-reset-init.component.html
  9. +58
    -0
      src/app/pages/password-reset/init/password-reset-init.component.spec.ts
  10. +49
    -0
      src/app/pages/password-reset/init/password-reset-init.component.ts
  11. +11
    -0
      src/app/pages/password-reset/init/password-reset-init.route.ts
  12. +44
    -0
      src/app/pages/password-reset/init/password-reset-init.service.spec.ts
  13. +15
    -0
      src/app/pages/password-reset/init/password-reset-init.service.ts

+ 93
- 3
src/app/app.config.ts Целия файл

@@ -1,9 +1,99 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
]
};
export const APP_CONFIG = {

app: {
name: 'AIT',
version: '1.0.0',
defaultLanguage: 'fr',
},


password: {
minLength: 4,
maxLength: 50,
requireUppercase: false,
requireLowercase: false,
requireNumbers: false,
requireSpecialChars: false,
},


email: {
minLength: 5,
maxLength: 254,
},


user: {
username: {
minLength: 1,
maxLength: 50,
pattern: /^[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$|^[_.@A-Za-z0-9-]+$/, // Email ou username
},
firstName: {
minLength: 1,
maxLength: 50,
},
lastName: {
minLength: 1,
maxLength: 50,
},
},

messages: {
defaultErrorMessage: 'Une erreur est survenue. Veuillez réessayer.',
defaultSuccessMessage: 'Opération effectuée avec succès.',
},

pagination: {
defaultPageSize: 20,
pageSizeOptions: [10, 20, 50, 100],
},
files: {
maxUploadSizeMB: 10,
allowedImageTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
allowedDocumentTypes: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
},
} as const;


export type AppConfig = typeof APP_CONFIG;


export const VALIDATION_MESSAGES = {
password: {
required: 'Le mot de passe est obligatoire.',
minLength: `Le mot de passe doit contenir au moins ${APP_CONFIG.password.minLength} caractères.`,
maxLength: `Le mot de passe ne peut pas dépasser ${APP_CONFIG.password.maxLength} caractères.`,
mismatch: 'Les mots de passe ne correspondent pas.',
},
email: {
required: "L'adresse e-mail est obligatoire.",
invalid: 'Veuillez entrer une adresse e-mail valide.',
minLength: `L'adresse e-mail doit contenir au moins ${APP_CONFIG.email.minLength} caractères.`,
maxLength: `L'adresse e-mail ne peut pas dépasser ${APP_CONFIG.email.maxLength} caractères.`,
},
username: {
required: "Le nom d'utilisateur est obligatoire.",
minLength: `Le nom d'utilisateur doit contenir au moins ${APP_CONFIG.user.username.minLength} caractères.`,
maxLength: `Le nom d'utilisateur ne peut pas dépasser ${APP_CONFIG.user.username.maxLength} caractères.`,
pattern: "Le nom d'utilisateur ne peut contenir que des lettres, chiffres, tirets et underscores.",
},
firstName: {
required: 'Le prénom est obligatoire.',
minLength: `Le prénom doit contenir au moins ${APP_CONFIG.user.firstName.minLength} caractère.`,
maxLength: `Le prénom ne peut pas dépasser ${APP_CONFIG.user.firstName.maxLength} caractères.`,
},
lastName: {
required: 'Le nom est obligatoire.',
minLength: `Le nom doit contenir au moins ${APP_CONFIG.user.lastName.minLength} caractère.`,
maxLength: `Le nom ne peut pas dépasser ${APP_CONFIG.user.lastName.maxLength} caractères.`,
},
} as const;

+ 161
- 0
src/app/pages/password-reset/finish/password-reset-finish.component.html Целия файл

@@ -0,0 +1,161 @@
<div class="auth-page-container forgot-password-page">
<div class="auth-card">
<!-- Header -->
<div class="auth-header">
<div class="logo">{{APP_CONFIG.app.name}}</div>
<h1 class="auth-title">Nouveau mot de passe</h1>
<p class="auth-subtitle">
Choisissez un nouveau mot de passe sécurisé pour votre compte.
</p>
</div>

<!-- Content -->
<div class="auth-content">
@if (!initialized()) {
<!-- Loading State -->
<div class="form-field">
<p class="info-message">Chargement en cours...</p>
</div>
} @else if (success()) {
<!-- Success Message -->
<div class="form-field">
<div class="success-message">
<strong>Votre mot de passe a été réinitialisé avec succès !</strong>
<br />
Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.
</div>
</div>

<!-- Login Link Button -->
<a routerLink="/login" class="auth-btn">
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
<polyline points="10 17 15 12 10 7"></polyline>
<line x1="15" y1="12" x2="3" y2="12"></line>
</svg>
Se connecter
</span>
</a>
} @else {
<!-- Reset Password Form -->
<form class="auth-form" [formGroup]="passwordForm" (ngSubmit)="finishReset()">

<!-- Global Error Messages -->
@if (error()) {
<div class="form-field">
<p class="error-message">
<strong>Une erreur est survenue !</strong>
<br />
La réinitialisation de votre mot de passe n'a pas pu être effectuée. Veuillez demander un nouveau lien de réinitialisation.
</p>
</div>
}

@if (doNotMatch()) {
<div class="form-field">
<p class="error-message">
<strong>{{ validationMessages.password.mismatch }}</strong>
<br />
Veuillez vous assurer que les deux mots de passe sont identiques.
</p>
</div>
}

<!-- New Password Field -->
<div class="form-field">
<label for="newPassword" class="form-label">Nouveau mot de passe</label>
<div class="input-wrapper">
<input
#newPassword
type="password"
id="newPassword"
formControlName="newPassword"
class="form-input"
[class.error]="passwordForm.get('newPassword')?.invalid && passwordForm.get('newPassword')?.touched"
placeholder="Entrez votre nouveau mot de passe"
autocomplete="new-password"
/>
<span class="input-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</span>
</div>

<!-- Error Messages for New Password -->
@if (passwordForm.get('newPassword')?.hasError('required') && passwordForm.get('newPassword')?.touched) {
<p class="error-message">{{ validationMessages.password.required }}</p>
}
@if (passwordForm.get('newPassword')?.hasError('minlength') && passwordForm.get('newPassword')?.touched) {
<p class="error-message">{{ validationMessages.password.minLength }}</p>
}
@if (passwordForm.get('newPassword')?.hasError('maxlength')) {
<p class="error-message">{{ validationMessages.password.maxLength }}</p>
}

<p class="info-message">
Le mot de passe doit contenir entre {{ passwordConfig.minLength }} et {{ passwordConfig.maxLength }} caractères.
</p>
</div>

<!-- Confirm Password Field -->
<div class="form-field">
<label for="confirmPassword" class="form-label">Confirmer le mot de passe</label>
<div class="input-wrapper">
<input
type="password"
id="confirmPassword"
formControlName="confirmPassword"
class="form-input"
[class.error]="(passwordForm.get('confirmPassword')?.invalid && passwordForm.get('confirmPassword')?.touched) || doNotMatch()"
placeholder="Confirmez votre nouveau mot de passe"
autocomplete="new-password"
/>
<span class="input-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</span>
</div>

<!-- Error Messages for Confirm Password -->
@if (passwordForm.get('confirmPassword')?.hasError('required') && passwordForm.get('confirmPassword')?.touched) {
<p class="error-message">{{ validationMessages.password.required }}</p>
}
@if (passwordForm.get('confirmPassword')?.hasError('minlength') && passwordForm.get('confirmPassword')?.touched) {
<p class="error-message">{{ validationMessages.password.minLength }}</p>
}
@if (passwordForm.get('confirmPassword')?.hasError('maxlength')) {
<p class="error-message">{{ validationMessages.password.maxLength }}</p>
}
</div>

<!-- Submit Button -->
<button
type="submit"
class="auth-btn"
[disabled]="passwordForm.invalid"
>
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Réinitialiser le mot de passe
</span>
</button>
</form>
}
</div>

<!-- Footer -->
<div class="auth-footer">
<p>
Vous vous souvenez de votre mot de passe ?
<a routerLink="/login" class="back-to-login-link">Retour à la connexion</a>
</p>
</div>
</div>
</div>

+ 98
- 0
src/app/pages/password-reset/finish/password-reset-finish.component.spec.ts Целия файл

@@ -0,0 +1,98 @@
import { ElementRef, signal } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, inject, tick } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { of, throwError } from 'rxjs';

import PasswordResetFinishComponent from './password-reset-finish.component';
import { PasswordResetFinishService } from './password-reset-finish.service';

describe('PasswordResetFinishComponent', () => {
let fixture: ComponentFixture<PasswordResetFinishComponent>;
let comp: PasswordResetFinishComponent;

beforeEach(() => {
fixture = TestBed.configureTestingModule({
imports: [PasswordResetFinishComponent],
providers: [
provideHttpClient(),
FormBuilder,
{
provide: ActivatedRoute,
useValue: { queryParams: of({ key: 'XYZPDQ' }) },
},
],
})
.overrideTemplate(PasswordResetFinishComponent, '')
.createComponent(PasswordResetFinishComponent);
});

beforeEach(() => {
fixture = TestBed.createComponent(PasswordResetFinishComponent);
comp = fixture.componentInstance;
comp.ngOnInit();
});

it('should define its initial state', () => {
expect(comp.initialized()).toBe(true);
expect(comp.key()).toEqual('XYZPDQ');
});

it('sets focus after the view has been initialized', () => {
const node = {
focus: jest.fn(),
};
comp.newPassword = signal<ElementRef>(new ElementRef(node));

comp.ngAfterViewInit();

expect(node.focus).toHaveBeenCalled();
});

it('should ensure the two passwords entered match', () => {
comp.passwordForm.patchValue({
newPassword: 'password',
confirmPassword: 'non-matching',
});

comp.finishReset();

expect(comp.doNotMatch()).toBe(true);
});

it('should update success to true after resetting password', inject(
[PasswordResetFinishService],
fakeAsync((service: PasswordResetFinishService) => {
jest.spyOn(service, 'save').mockReturnValue(of({}));
comp.passwordForm.patchValue({
newPassword: 'password',
confirmPassword: 'password',
});

comp.finishReset();
tick();

expect(service.save).toHaveBeenCalledWith('XYZPDQ', 'password');
expect(comp.success()).toBe(true);
}),
));

it('should notify of generic error', inject(
[PasswordResetFinishService],
fakeAsync((service: PasswordResetFinishService) => {
jest.spyOn(service, 'save').mockReturnValue(throwError(Error));
comp.passwordForm.patchValue({
newPassword: 'password',
confirmPassword: 'password',
});

comp.finishReset();
tick();

expect(service.save).toHaveBeenCalledWith('XYZPDQ', 'password');
expect(comp.success()).toBe(false);
expect(comp.error()).toBe(true);
}),
));
});

+ 78
- 0
src/app/pages/password-reset/finish/password-reset-finish.component.ts Целия файл

@@ -0,0 +1,78 @@
import { AfterViewInit, Component, ElementRef, OnInit, inject, signal, viewChild } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, RouterModule } from '@angular/router';

import { PasswordResetFinishService } from './password-reset-finish.service';
import SharedModule from '../../../shared/shared.module';
import {APP_CONFIG, appConfig, VALIDATION_MESSAGES} from '../../../app.config';

@Component({
selector: 'jhi-password-reset-finish',
imports: [SharedModule, RouterModule, FormsModule, ReactiveFormsModule],
templateUrl: './password-reset-finish.component.html',
})
export default class PasswordResetFinishComponent implements OnInit, AfterViewInit {
newPassword = viewChild.required<ElementRef>('newPassword');

initialized = signal(false);
doNotMatch = signal(false);
error = signal(false);
success = signal(false);
key = signal('');

readonly validationMessages = VALIDATION_MESSAGES;
readonly passwordConfig = APP_CONFIG.password;
passwordForm = new FormGroup({
newPassword: new FormControl('', {
nonNullable: true,
validators: [
Validators.required,
Validators.minLength(APP_CONFIG.password.minLength),
Validators.maxLength(APP_CONFIG.password.maxLength)
],
}),
confirmPassword: new FormControl('', {
nonNullable: true,
validators: [
Validators.required,
Validators.minLength(APP_CONFIG.password.minLength),
Validators.maxLength(APP_CONFIG.password.maxLength)
],
}),
});

private readonly passwordResetFinishService = inject(PasswordResetFinishService);
private readonly route = inject(ActivatedRoute);

ngOnInit(): void {
this.route.queryParams.subscribe(params => {
if (params["key"]) {
this.key.set(params["key"]);
}
this.initialized.set(true);
});
}

ngAfterViewInit(): void {
this.newPassword().nativeElement.focus();
}

finishReset(): void {
this.doNotMatch.set(false);
this.error.set(false);

const { newPassword, confirmPassword } = this.passwordForm.getRawValue();

if (newPassword !== confirmPassword) {
this.doNotMatch.set(true);
} else {
this.passwordResetFinishService.save(this.key(), newPassword).subscribe({
next: () => this.success.set(true),
error: () => this.error.set(true),
});
}
}

protected readonly appConfig = appConfig;
protected readonly APP_CONFIG = APP_CONFIG;
}

+ 11
- 0
src/app/pages/password-reset/finish/password-reset-finish.route.ts Целия файл

@@ -0,0 +1,11 @@
import { Route } from '@angular/router';

import PasswordResetFinishComponent from './password-reset-finish.component';

const passwordResetFinishRoute: Route = {
path: 'reset/finish',
component: PasswordResetFinishComponent,
title: 'global.menu.account.password',
};

export default passwordResetFinishRoute;

+ 45
- 0
src/app/pages/password-reset/finish/password-reset-finish.service.spec.ts Целия файл

@@ -0,0 +1,45 @@
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';

import { ApplicationConfigService } from 'app/core/config/application-config.service';
import { PasswordResetFinishService } from './password-reset-finish.service';

describe('PasswordResetFinish Service', () => {
let service: PasswordResetFinishService;
let httpMock: HttpTestingController;
let applicationConfigService: ApplicationConfigService;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});

service = TestBed.inject(PasswordResetFinishService);
applicationConfigService = TestBed.inject(ApplicationConfigService);
httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => {
httpMock.verify();
});

describe('Service methods', () => {
it('should call reset-password/finish endpoint with correct values', () => {
// GIVEN
const key = 'abc';
const newPassword = 'password';

// WHEN
service.save(key, newPassword).subscribe();

const testRequest = httpMock.expectOne({
method: 'POST',
url: applicationConfigService.getEndpointFor('api/account/reset-password/finish'),
});

// THEN
expect(testRequest.request.body).toEqual({ key, newPassword });
});
});
});

+ 15
- 0
src/app/pages/password-reset/finish/password-reset-finish.service.ts Целия файл

@@ -0,0 +1,15 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {ApplicationConfigService} from '../../../core/config/application-config.service';


@Injectable({ providedIn: 'root' })
export class PasswordResetFinishService {
private readonly http = inject(HttpClient);
private readonly applicationConfigService = inject(ApplicationConfigService);

save(key: string, newPassword: string): Observable<{}> {
return this.http.post(this.applicationConfigService.getEndpointFor('api/account/reset-password/finish'), { key, newPassword });
}
}

+ 88
- 0
src/app/pages/password-reset/init/password-reset-init.component.html Целия файл

@@ -0,0 +1,88 @@
<div class="auth-page-container forgot-password-page">
<div class="auth-card">
<!-- Header -->
<div class="auth-header">
<div class="logo">{{APP_CONFIG.app.name}}</div>
<h1 class="auth-title">Mot de passe oublié ?</h1>
<p class="auth-subtitle">
Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.
</p>
</div>

<!-- Content -->
<div class="auth-content">
@if (success()) {
<!-- Success Message -->
<div class="form-field">
<div class="success-message">
<strong>Un e-mail de réinitialisation a été envoyé !</strong>
<br />
Veuillez consulter votre boîte de réception et suivre les instructions pour réinitialiser votre mot de passe.
</div>
</div>
} @else {
<!-- Reset Form -->
<form class="auth-form" [formGroup]="resetRequestForm" (ngSubmit)="requestReset()">
<!-- Email Field -->
<div class="form-field">
<label for="email" class="form-label">Adresse e-mail</label>
<div class="input-wrapper">
<input
#email
type="email"
id="email"
formControlName="email"
class="form-input"
[class.error]="resetRequestForm.get('email')?.invalid && resetRequestForm.get('email')?.touched"
placeholder="votre@email.com"
autocomplete="email"
/>
<span class="input-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</span>
</div>

<!-- Error Messages -->
@if (resetRequestForm.get('email')?.hasError('required') && resetRequestForm.get('email')?.touched) {
<p class="error-message">{{ validationMessages.email.required }}</p>
}
@if (resetRequestForm.get('email')?.hasError('email') && resetRequestForm.get('email')?.touched) {
<p class="error-message">{{ validationMessages.email.invalid }}</p>
}
@if (resetRequestForm.get('email')?.hasError('minlength') && resetRequestForm.get('email')?.touched) {
<p class="error-message">{{ validationMessages.email.minLength }}</p>
}
@if (resetRequestForm.get('email')?.hasError('maxlength')) {
<p class="error-message">{{ validationMessages.email.maxLength }}</p>
}
</div>

<!-- Submit Button -->
<button
type="submit"
class="auth-btn"
[disabled]="resetRequestForm.invalid"
>
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
</svg>
Envoyer le lien de réinitialisation
</span>
</button>
</form>
}
</div>

<!-- Footer -->
<div class="auth-footer">
<p>
Vous vous souvenez de votre mot de passe ?
<a routerLink="/login" class="back-to-login-link">Retour à la connexion</a>
</p>
</div>
</div>
</div>

+ 58
- 0
src/app/pages/password-reset/init/password-reset-init.component.spec.ts Целия файл

@@ -0,0 +1,58 @@
import { ElementRef, signal } from '@angular/core';
import { ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { FormBuilder } from '@angular/forms';
import { of, throwError } from 'rxjs';

import PasswordResetInitComponent from './password-reset-init.component';
import { PasswordResetInitService } from './password-reset-init.service';

describe('PasswordResetInitComponent', () => {
let fixture: ComponentFixture<PasswordResetInitComponent>;
let comp: PasswordResetInitComponent;

beforeEach(() => {
fixture = TestBed.configureTestingModule({
imports: [PasswordResetInitComponent],
providers: [provideHttpClient(), FormBuilder],
})
.overrideTemplate(PasswordResetInitComponent, '')
.createComponent(PasswordResetInitComponent);
comp = fixture.componentInstance;
});

it('sets focus after the view has been initialized', () => {
const node = {
focus: jest.fn(),
};
comp.email = signal<ElementRef>(new ElementRef(node));

comp.ngAfterViewInit();

expect(node.focus).toHaveBeenCalled();
});

it('notifies of success upon successful requestReset', inject([PasswordResetInitService], (service: PasswordResetInitService) => {
jest.spyOn(service, 'save').mockReturnValue(of({}));
comp.resetRequestForm.patchValue({
email: 'user@domain.com',
});

comp.requestReset();

expect(service.save).toHaveBeenCalledWith('user@domain.com');
expect(comp.success()).toBe(true);
}));

it('no notification of success upon error response', inject([PasswordResetInitService], (service: PasswordResetInitService) => {
const err = { status: 503, data: 'something else' };
jest.spyOn(service, 'save').mockReturnValue(throwError(() => err));
comp.resetRequestForm.patchValue({
email: 'user@domain.com',
});
comp.requestReset();

expect(service.save).toHaveBeenCalledWith('user@domain.com');
expect(comp.success()).toBe(false);
}));
});

+ 49
- 0
src/app/pages/password-reset/init/password-reset-init.component.ts Целия файл

@@ -0,0 +1,49 @@
import { AfterViewInit, Component, ElementRef, inject, signal, viewChild } from '@angular/core';
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';

import { PasswordResetInitService } from './password-reset-init.service';
import { RouterLink } from '@angular/router';
import SharedModule from '../../../shared/shared.module';
import {APP_CONFIG, VALIDATION_MESSAGES} from '../../../app.config';

@Component({
selector: 'jhi-password-reset-init',
imports: [SharedModule, FormsModule, ReactiveFormsModule, RouterLink],
templateUrl: './password-reset-init.component.html',
})
export default class PasswordResetInitComponent implements AfterViewInit {
email = viewChild.required<ElementRef>('email');

success = signal(false);
resetRequestForm;

readonly validationMessages = VALIDATION_MESSAGES;
readonly emailConfig = APP_CONFIG.email;

private readonly passwordResetInitService = inject(PasswordResetInitService);
private readonly fb = inject(FormBuilder);

constructor() {
this.resetRequestForm = this.fb.group({
email: [
'',
[
Validators.required,
Validators.minLength(APP_CONFIG.email.minLength),
Validators.maxLength(APP_CONFIG.email.maxLength),
Validators.email
]
],
});
}

ngAfterViewInit(): void {
this.email().nativeElement.focus();
}

requestReset(): void {
this.passwordResetInitService.save(this.resetRequestForm.get(['email'])!.value).subscribe(() => this.success.set(true));
}

protected readonly APP_CONFIG = APP_CONFIG;
}

+ 11
- 0
src/app/pages/password-reset/init/password-reset-init.route.ts Целия файл

@@ -0,0 +1,11 @@
import { Route } from '@angular/router';

import PasswordResetInitComponent from './password-reset-init.component';

const passwordResetInitRoute: Route = {
path: 'reset/request',
component: PasswordResetInitComponent,
title: 'global.menu.account.password',
};

export default passwordResetInitRoute;

+ 44
- 0
src/app/pages/password-reset/init/password-reset-init.service.spec.ts Целия файл

@@ -0,0 +1,44 @@
import { TestBed } from '@angular/core/testing';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';

import { ApplicationConfigService } from 'app/core/config/application-config.service';
import { PasswordResetInitService } from './password-reset-init.service';

describe('PasswordResetInit Service', () => {
let service: PasswordResetInitService;
let httpMock: HttpTestingController;
let applicationConfigService: ApplicationConfigService;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});

service = TestBed.inject(PasswordResetInitService);
applicationConfigService = TestBed.inject(ApplicationConfigService);
httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => {
httpMock.verify();
});

describe('Service methods', () => {
it('should call reset-password/init endpoint with correct values', () => {
// GIVEN
const mail = 'test@test.com';

// WHEN
service.save(mail).subscribe();

const testRequest = httpMock.expectOne({
method: 'POST',
url: applicationConfigService.getEndpointFor('api/account/reset-password/init'),
});

// THEN
expect(testRequest.request.body).toEqual(mail);
});
});
});

+ 15
- 0
src/app/pages/password-reset/init/password-reset-init.service.ts Целия файл

@@ -0,0 +1,15 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import {ApplicationConfigService} from '../../../core/config/application-config.service';


@Injectable({ providedIn: 'root' })
export class PasswordResetInitService {
private readonly http = inject(HttpClient);
private readonly applicationConfigService = inject(ApplicationConfigService);

save(mail: string): Observable<{}> {
return this.http.post(this.applicationConfigService.getEndpointFor('api/account/reset-password/init'), mail);
}
}

Loading…
Отказ
Запис