feat(auth): implement authentication service, guards, and interceptors
- Add AuthService for handling login, registration, and session management. - Create auth guards to protect routes based on authentication status. - Implement auth interceptor to attach JWT token to HTTP requests and handle 401 errors. - Add unit tests for AuthService, guards, and interceptor. - Create login and registration components with form validation and error handling. - Update routing to include guards for login and registration pages.
This commit is contained in:
@@ -1,29 +1,67 @@
|
||||
<header class="login-page__header text-center">
|
||||
<h1 id="login-title" class="auth-title">Iniciar sesión</h1>
|
||||
<h1 id="login-title" class="auth-title">Iniciar sesion</h1>
|
||||
</header>
|
||||
|
||||
<form class="login-page__form d-grid gap-3" novalidate aria-labelledby="login-title">
|
||||
<form
|
||||
class="login-page__form d-grid gap-3"
|
||||
novalidate
|
||||
aria-labelledby="login-title"
|
||||
[formGroup]="form"
|
||||
(ngSubmit)="onSubmit()"
|
||||
>
|
||||
<div class="d-grid gap-3">
|
||||
<label class="visually-hidden" for="login-email">Email</label>
|
||||
<app-input id="login-email" type="email" placeholder="Email" />
|
||||
<app-input
|
||||
id="login-email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
[value]="form.controls.email.value"
|
||||
[invalid]="showControlError('email')"
|
||||
(valueChange)="updateEmail($event)"
|
||||
/>
|
||||
@if (getControlError('email'); as errorMessage) {
|
||||
<small class="text-danger">{{ errorMessage }}</small>
|
||||
}
|
||||
|
||||
<div class="d-grid gap-1">
|
||||
<label class="visually-hidden" for="login-password">Contraseña</label>
|
||||
<app-input id="login-password" type="password" placeholder="Contrasena" />
|
||||
<label class="visually-hidden" for="login-password">Contrasena</label>
|
||||
<app-input
|
||||
id="login-password"
|
||||
type="password"
|
||||
placeholder="Contrasena"
|
||||
[value]="form.controls.password.value"
|
||||
[invalid]="showControlError('password')"
|
||||
(valueChange)="updatePassword($event)"
|
||||
/>
|
||||
@if (getControlError('password'); as errorMessage) {
|
||||
<small class="text-danger">{{ errorMessage }}</small>
|
||||
}
|
||||
<div class="text-end">
|
||||
<button type="button" class="login-page__forgot-password btn btn-link p-0 border-0 text-decoration-none">
|
||||
Olvidé mi contraseña
|
||||
Olvide mi contrasena
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 text-center">
|
||||
<app-button hostClass="w-100 d-block" buttonClass="w-100">Ingresar</app-button>
|
||||
@if (serverError(); as errorMessage) {
|
||||
<small class="text-danger">{{ errorMessage }}</small>
|
||||
}
|
||||
<app-button
|
||||
type="submit"
|
||||
hostClass="w-100 d-block"
|
||||
buttonClass="w-100"
|
||||
[disabled]="isSubmitting()"
|
||||
>
|
||||
{{ isSubmitting() ? 'Ingresando...' : 'Ingresar' }}
|
||||
</app-button>
|
||||
<app-button
|
||||
type="button"
|
||||
variant="borderless"
|
||||
hostClass="w-100 d-block text-center"
|
||||
buttonClass="login-page__create-account px-3 py-2"
|
||||
[disabled]="isSubmitting()"
|
||||
(click)="goToRegister()"
|
||||
>
|
||||
Crear cuenta
|
||||
@@ -38,6 +76,7 @@
|
||||
</div>
|
||||
|
||||
<app-button
|
||||
type="button"
|
||||
variant="neutral-outline"
|
||||
hostClass="w-100 d-block"
|
||||
buttonClass="w-100 d-inline-flex align-items-center justify-content-center gap-2 py-2 fw-semibold"
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideRouter, Router } from '@angular/router';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
|
||||
describe('LoginPageComponent', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('submits credentials and navigates to home on success', async () => {
|
||||
const authService = {
|
||||
login: vi.fn().mockReturnValue(
|
||||
of({
|
||||
id: 1,
|
||||
nombre_apellido: 'Ada Lovelace',
|
||||
email: 'ada@example.com'
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(LoginPageComponent);
|
||||
const component = fixture.componentInstance as any;
|
||||
const router = TestBed.inject(Router);
|
||||
const navigateSpy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
|
||||
component.form.setValue({
|
||||
email: 'ada@example.com',
|
||||
password: 'secret123'
|
||||
});
|
||||
|
||||
component.onSubmit();
|
||||
|
||||
expect(authService.login).toHaveBeenCalledWith({
|
||||
email: 'ada@example.com',
|
||||
password: 'secret123'
|
||||
});
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['/']);
|
||||
});
|
||||
|
||||
it('surfaces backend login errors', async () => {
|
||||
const authService = {
|
||||
login: vi.fn().mockReturnValue(
|
||||
throwError(() => ({
|
||||
error: {
|
||||
errors: {
|
||||
email: ['Las credenciales son invalidas.']
|
||||
}
|
||||
}
|
||||
}))
|
||||
)
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(LoginPageComponent);
|
||||
const component = fixture.componentInstance as any;
|
||||
|
||||
component.form.setValue({
|
||||
email: 'ada@example.com',
|
||||
password: 'wrong-password'
|
||||
});
|
||||
|
||||
component.onSubmit();
|
||||
|
||||
expect(component.serverError()).toBe('Las credenciales son invalidas.');
|
||||
});
|
||||
|
||||
it('validates email length and password minimum length before submit', async () => {
|
||||
const authService = {
|
||||
login: vi.fn()
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(LoginPageComponent);
|
||||
const component = fixture.componentInstance as any;
|
||||
|
||||
component.form.setValue({
|
||||
email: `${'a'.repeat(250)}@example.com`,
|
||||
password: '1234567'
|
||||
});
|
||||
component.onSubmit();
|
||||
|
||||
expect(authService.login).not.toHaveBeenCalled();
|
||||
expect(component.getControlError('email')).toBe('No puede superar los 255 caracteres.');
|
||||
expect(component.getControlError('password')).toBe('Debe tener al menos 8 caracteres.');
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,122 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
||||
import { InputComponent } from '../../../../shared/components/input/input.component';
|
||||
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
const EMAIL_MAX_LENGTH = 255;
|
||||
|
||||
@Component({
|
||||
selector: 'app-login-page',
|
||||
imports: [InputComponent, ButtonComponent],
|
||||
imports: [ReactiveFormsModule, InputComponent, ButtonComponent],
|
||||
templateUrl: './login-page.component.html',
|
||||
styleUrl: './login-page.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class LoginPageComponent {
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
private readonly router = inject(Router);
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
private readonly submittedState = signal(false);
|
||||
private readonly serverErrorState = signal<string | null>(null);
|
||||
private readonly isSubmittingState = signal(false);
|
||||
|
||||
protected readonly form = this.formBuilder.nonNullable.group({
|
||||
email: ['', [Validators.required, Validators.email, Validators.maxLength(EMAIL_MAX_LENGTH)]],
|
||||
password: ['', [Validators.required, Validators.minLength(PASSWORD_MIN_LENGTH)]]
|
||||
});
|
||||
protected readonly submitted = this.submittedState.asReadonly();
|
||||
protected readonly serverError = this.serverErrorState.asReadonly();
|
||||
protected readonly isSubmitting = this.isSubmittingState.asReadonly();
|
||||
|
||||
goToRegister(): void {
|
||||
void this.router.navigate(['/register']);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
this.submittedState.set(true);
|
||||
this.serverErrorState.set(null);
|
||||
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSubmittingState.set(true);
|
||||
|
||||
this.authService.login(this.form.getRawValue()).subscribe({
|
||||
next: () => {
|
||||
this.isSubmittingState.set(false);
|
||||
void this.router.navigate(['/']);
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.isSubmittingState.set(false);
|
||||
this.serverErrorState.set(this.resolveErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected updateEmail(value: string | number): void {
|
||||
this.form.controls.email.setValue(String(value));
|
||||
}
|
||||
|
||||
protected updatePassword(value: string | number): void {
|
||||
this.form.controls.password.setValue(String(value));
|
||||
}
|
||||
|
||||
protected showControlError(controlName: 'email' | 'password'): boolean {
|
||||
const control = this.form.controls[controlName];
|
||||
|
||||
return control.invalid && (control.touched || this.submitted());
|
||||
}
|
||||
|
||||
protected getControlError(controlName: 'email' | 'password'): string | null {
|
||||
const control = this.form.controls[controlName];
|
||||
|
||||
if (!this.showControlError(controlName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (control.hasError('required')) {
|
||||
return 'Este campo es obligatorio.';
|
||||
}
|
||||
|
||||
if (control.hasError('maxlength')) {
|
||||
return `No puede superar los ${EMAIL_MAX_LENGTH} caracteres.`;
|
||||
}
|
||||
|
||||
if (control.hasError('email')) {
|
||||
return 'Ingresa un email valido.';
|
||||
}
|
||||
|
||||
if (control.hasError('minlength')) {
|
||||
return `Debe tener al menos ${PASSWORD_MIN_LENGTH} caracteres.`;
|
||||
}
|
||||
|
||||
return 'El valor ingresado no es valido.';
|
||||
}
|
||||
|
||||
private resolveErrorMessage(error: unknown): string {
|
||||
const errorPayload =
|
||||
typeof error === 'object' && error !== null && 'error' in error
|
||||
? (error as { error?: { errors?: Record<string, string[]>; message?: string } }).error
|
||||
: undefined;
|
||||
const apiErrors = errorPayload?.errors;
|
||||
const emailMessages = apiErrors?.['email'];
|
||||
const emailError = Array.isArray(emailMessages) ? emailMessages[0] : null;
|
||||
|
||||
if (typeof emailError === 'string' && emailError.trim()) {
|
||||
return emailError;
|
||||
}
|
||||
|
||||
if (typeof errorPayload?.message === 'string' && errorPayload.message.trim()) {
|
||||
return errorPayload.message;
|
||||
}
|
||||
|
||||
return 'No se pudo iniciar sesion. Intenta nuevamente.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,84 @@
|
||||
<h1 id="register-title" class="auth-title">CREAR CUENTA</h1>
|
||||
</header>
|
||||
|
||||
<form class="register-page__form d-grid gap-3" novalidate aria-labelledby="register-title">
|
||||
<form
|
||||
class="register-page__form d-grid gap-3"
|
||||
novalidate
|
||||
aria-labelledby="register-title"
|
||||
[formGroup]="form"
|
||||
(ngSubmit)="onSubmit()"
|
||||
>
|
||||
<div class="d-grid gap-3">
|
||||
<label class="visually-hidden" for="register-full-name">Nombre y Apellido</label>
|
||||
<app-input id="register-full-name" placeholder="Nombre y Apellido" />
|
||||
<app-input
|
||||
id="register-full-name"
|
||||
placeholder="Nombre y Apellido"
|
||||
[value]="form.controls.nombre_apellido.value"
|
||||
[invalid]="showControlError('nombre_apellido')"
|
||||
(valueChange)="updateField('nombre_apellido', $event)"
|
||||
/>
|
||||
@if (getControlError('nombre_apellido'); as errorMessage) {
|
||||
<small class="text-danger">{{ errorMessage }}</small>
|
||||
}
|
||||
|
||||
<label class="visually-hidden" for="register-email">Email</label>
|
||||
<app-input id="register-email" type="email" placeholder="Email" />
|
||||
<app-input
|
||||
id="register-email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
[value]="form.controls.email.value"
|
||||
[invalid]="showControlError('email')"
|
||||
(valueChange)="updateField('email', $event)"
|
||||
/>
|
||||
@if (getControlError('email'); as errorMessage) {
|
||||
<small class="text-danger">{{ errorMessage }}</small>
|
||||
}
|
||||
|
||||
<label class="visually-hidden" for="register-password">Contraseña</label>
|
||||
<app-input id="register-password" type="password" placeholder="Contraseña" />
|
||||
<label class="visually-hidden" for="register-password">Contrasena</label>
|
||||
<app-input
|
||||
id="register-password"
|
||||
type="password"
|
||||
placeholder="Contrasena"
|
||||
[value]="form.controls.password.value"
|
||||
[invalid]="showControlError('password')"
|
||||
(valueChange)="updateField('password', $event)"
|
||||
/>
|
||||
@if (getControlError('password'); as errorMessage) {
|
||||
<small class="text-danger">{{ errorMessage }}</small>
|
||||
}
|
||||
|
||||
<label class="visually-hidden" for="register-password-repeat">Repetir Contraseña</label>
|
||||
<label class="visually-hidden" for="register-password-repeat">Repetir Contrasena</label>
|
||||
<app-input
|
||||
id="register-password-repeat"
|
||||
type="password"
|
||||
placeholder="Repetir Contraseña"
|
||||
placeholder="Repetir Contrasena"
|
||||
[value]="form.controls.password_confirmation.value"
|
||||
[invalid]="showControlError('password_confirmation')"
|
||||
(valueChange)="updateField('password_confirmation', $event)"
|
||||
/>
|
||||
@if (getControlError('password_confirmation'); as errorMessage) {
|
||||
<small class="text-danger">{{ errorMessage }}</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 text-center">
|
||||
<app-button hostClass="w-100 d-block" buttonClass="w-100">Crear Cuenta</app-button>
|
||||
@if (serverError(); as errorMessage) {
|
||||
<small class="text-danger">{{ errorMessage }}</small>
|
||||
}
|
||||
<app-button
|
||||
type="submit"
|
||||
hostClass="w-100 d-block"
|
||||
buttonClass="w-100"
|
||||
[disabled]="isSubmitting()"
|
||||
>
|
||||
{{ isSubmitting() ? 'Creando cuenta...' : 'Crear Cuenta' }}
|
||||
</app-button>
|
||||
<app-button
|
||||
type="button"
|
||||
variant="borderless"
|
||||
hostClass="w-100 d-block text-center"
|
||||
buttonClass="register-page__back-action px-3 py-2"
|
||||
[disabled]="isSubmitting()"
|
||||
(click)="goToLogin()"
|
||||
>
|
||||
Volver
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideRouter, Router } from '@angular/router';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||
import { RegisterPageComponent } from './register-page.component';
|
||||
|
||||
describe('RegisterPageComponent', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('submits registration data and redirects to /login on success', async () => {
|
||||
const authService = {
|
||||
register: vi.fn().mockReturnValue(
|
||||
of({
|
||||
message: 'Usuario registrado correctamente.',
|
||||
data: {
|
||||
id: 1,
|
||||
nombre_apellido: 'Ada Lovelace',
|
||||
email: 'ada@example.com'
|
||||
}
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RegisterPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(RegisterPageComponent);
|
||||
const component = fixture.componentInstance as any;
|
||||
const router = TestBed.inject(Router);
|
||||
const navigateSpy = vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
|
||||
component.form.setValue({
|
||||
nombre_apellido: 'Ada Lovelace',
|
||||
email: 'ada@example.com',
|
||||
password: 'secret123',
|
||||
password_confirmation: 'secret123'
|
||||
});
|
||||
|
||||
component.onSubmit();
|
||||
|
||||
expect(authService.register).toHaveBeenCalledWith({
|
||||
nombre_apellido: 'Ada Lovelace',
|
||||
email: 'ada@example.com',
|
||||
password: 'secret123',
|
||||
password_confirmation: 'secret123'
|
||||
});
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['/login']);
|
||||
});
|
||||
|
||||
it('shows a mismatch message when passwords do not match', async () => {
|
||||
const authService = {
|
||||
register: vi.fn()
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RegisterPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(RegisterPageComponent);
|
||||
const component = fixture.componentInstance as any;
|
||||
|
||||
component.form.setValue({
|
||||
nombre_apellido: 'Ada Lovelace',
|
||||
email: 'ada@example.com',
|
||||
password: 'secret123',
|
||||
password_confirmation: 'different'
|
||||
});
|
||||
component.onSubmit();
|
||||
|
||||
expect(authService.register).not.toHaveBeenCalled();
|
||||
expect(component.getControlError('password_confirmation')).toBe('Las contrasenas no coinciden.');
|
||||
});
|
||||
|
||||
it('validates max length and password minimum length before submit', async () => {
|
||||
const authService = {
|
||||
register: vi.fn()
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RegisterPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(RegisterPageComponent);
|
||||
const component = fixture.componentInstance as any;
|
||||
|
||||
component.form.setValue({
|
||||
nombre_apellido: 'A'.repeat(256),
|
||||
email: `${'a'.repeat(250)}@example.com`,
|
||||
password: '1234567',
|
||||
password_confirmation: '1234567'
|
||||
});
|
||||
component.onSubmit();
|
||||
|
||||
expect(authService.register).not.toHaveBeenCalled();
|
||||
expect(component.getControlError('nombre_apellido')).toBe('No puede superar los 255 caracteres.');
|
||||
expect(component.getControlError('email')).toBe('No puede superar los 255 caracteres.');
|
||||
expect(component.getControlError('password')).toBe('Debe tener al menos 8 caracteres.');
|
||||
});
|
||||
|
||||
it('surfaces backend register errors', async () => {
|
||||
const authService = {
|
||||
register: vi.fn().mockReturnValue(
|
||||
throwError(() => ({
|
||||
error: {
|
||||
errors: {
|
||||
email: ['El email ya esta en uso.']
|
||||
}
|
||||
}
|
||||
}))
|
||||
)
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RegisterPageComponent],
|
||||
providers: [provideRouter([]), { provide: AuthService, useValue: authService }]
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(RegisterPageComponent);
|
||||
const component = fixture.componentInstance as any;
|
||||
|
||||
component.form.setValue({
|
||||
nombre_apellido: 'Ada Lovelace',
|
||||
email: 'ada@example.com',
|
||||
password: 'secret123',
|
||||
password_confirmation: 'secret123'
|
||||
});
|
||||
component.onSubmit();
|
||||
|
||||
expect(component.serverError()).toBe('El email ya esta en uso.');
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,164 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { AuthService } from '../../../../core/services/auth/auth.service';
|
||||
import { ButtonComponent } from '../../../../shared/components/button/button.component';
|
||||
import { InputComponent } from '../../../../shared/components/input/input.component';
|
||||
|
||||
const PASSWORD_MIN_LENGTH = 8;
|
||||
const TEXT_MAX_LENGTH = 255;
|
||||
|
||||
const passwordsMatchValidator: ValidatorFn = (
|
||||
control: AbstractControl
|
||||
): ValidationErrors | null => {
|
||||
const password = control.get('password')?.value;
|
||||
const passwordConfirmation = control.get('password_confirmation')?.value;
|
||||
|
||||
if (!password || !passwordConfirmation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return password === passwordConfirmation ? null : { passwordMismatch: true };
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-register-page',
|
||||
imports: [InputComponent, ButtonComponent],
|
||||
imports: [ReactiveFormsModule, InputComponent, ButtonComponent],
|
||||
templateUrl: './register-page.component.html',
|
||||
styleUrl: './register-page.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RegisterPageComponent {
|
||||
private readonly formBuilder = inject(FormBuilder);
|
||||
private readonly router = inject(Router);
|
||||
private readonly authService = inject(AuthService);
|
||||
|
||||
private readonly submittedState = signal(false);
|
||||
private readonly serverErrorState = signal<string | null>(null);
|
||||
private readonly isSubmittingState = signal(false);
|
||||
|
||||
protected readonly form = this.formBuilder.nonNullable.group({
|
||||
nombre_apellido: ['', [Validators.required, Validators.maxLength(TEXT_MAX_LENGTH)]],
|
||||
email: [
|
||||
'',
|
||||
[Validators.required, Validators.email, Validators.maxLength(TEXT_MAX_LENGTH)]
|
||||
],
|
||||
password: ['', [Validators.required, Validators.minLength(PASSWORD_MIN_LENGTH)]],
|
||||
password_confirmation: ['', [Validators.required]]
|
||||
}, {
|
||||
validators: [passwordsMatchValidator]
|
||||
});
|
||||
protected readonly submitted = this.submittedState.asReadonly();
|
||||
protected readonly serverError = this.serverErrorState.asReadonly();
|
||||
protected readonly isSubmitting = this.isSubmittingState.asReadonly();
|
||||
|
||||
goToLogin(): void {
|
||||
void this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
this.submittedState.set(true);
|
||||
this.serverErrorState.set(null);
|
||||
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSubmittingState.set(true);
|
||||
|
||||
this.authService.register(this.form.getRawValue()).subscribe({
|
||||
next: () => {
|
||||
this.isSubmittingState.set(false);
|
||||
void this.router.navigate(['/login']);
|
||||
},
|
||||
error: (error: unknown) => {
|
||||
this.isSubmittingState.set(false);
|
||||
this.serverErrorState.set(this.resolveErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected updateField(
|
||||
controlName: 'nombre_apellido' | 'email' | 'password' | 'password_confirmation',
|
||||
value: string | number
|
||||
): void {
|
||||
this.form.controls[controlName].setValue(String(value));
|
||||
}
|
||||
|
||||
protected showControlError(
|
||||
controlName: 'nombre_apellido' | 'email' | 'password' | 'password_confirmation'
|
||||
): boolean {
|
||||
const control = this.form.controls[controlName];
|
||||
const hasPasswordMismatch =
|
||||
controlName === 'password_confirmation' &&
|
||||
this.form.hasError('passwordMismatch') &&
|
||||
(control.touched || this.submitted());
|
||||
|
||||
return (control.invalid && (control.touched || this.submitted())) || hasPasswordMismatch;
|
||||
}
|
||||
|
||||
protected getControlError(
|
||||
controlName: 'nombre_apellido' | 'email' | 'password' | 'password_confirmation'
|
||||
): string | null {
|
||||
const control = this.form.controls[controlName];
|
||||
|
||||
if (!this.showControlError(controlName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (control.hasError('required')) {
|
||||
return 'Este campo es obligatorio.';
|
||||
}
|
||||
|
||||
if (control.hasError('maxlength')) {
|
||||
return `No puede superar los ${TEXT_MAX_LENGTH} caracteres.`;
|
||||
}
|
||||
|
||||
if (control.hasError('email')) {
|
||||
return 'Ingresa un email valido.';
|
||||
}
|
||||
|
||||
if (control.hasError('minlength')) {
|
||||
return `Debe tener al menos ${PASSWORD_MIN_LENGTH} caracteres.`;
|
||||
}
|
||||
|
||||
if (controlName === 'password_confirmation' && this.form.hasError('passwordMismatch')) {
|
||||
return 'Las contrasenas no coinciden.';
|
||||
}
|
||||
|
||||
return 'El valor ingresado no es valido.';
|
||||
}
|
||||
|
||||
private resolveErrorMessage(error: unknown): string {
|
||||
const errorPayload =
|
||||
typeof error === 'object' && error !== null && 'error' in error
|
||||
? (error as { error?: { errors?: Record<string, string[]>; message?: string } }).error
|
||||
: undefined;
|
||||
const apiErrors = errorPayload?.errors;
|
||||
|
||||
if (apiErrors && typeof apiErrors === 'object') {
|
||||
for (const field of ['nombre_apellido', 'email', 'password'] as const) {
|
||||
const messages = apiErrors[field];
|
||||
|
||||
if (Array.isArray(messages) && typeof messages[0] === 'string' && messages[0].trim()) {
|
||||
return messages[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof errorPayload?.message === 'string' && errorPayload.message.trim()) {
|
||||
return errorPayload.message;
|
||||
}
|
||||
|
||||
return 'No se pudo crear la cuenta. Intenta nuevamente.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Routes } from '@angular/router';
|
||||
|
||||
import { AuthLayoutComponent } from '../../core/layout/auth-layout/auth-layout.component';
|
||||
import { StoreLayoutComponent } from '../../core/layout/store-layout/store-layout.component';
|
||||
import { guestOnlyGuard } from '../../core/services/auth/auth.guards';
|
||||
import { LoginPageComponent } from './pages/login-page/login-page.component';
|
||||
import { RegisterPageComponent } from './pages/register-page/register-page.component';
|
||||
import { StoreHomePageComponent } from './pages/store-home-page/store-home-page.component';
|
||||
@@ -17,6 +18,7 @@ export const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
canActivate: [guestOnlyGuard],
|
||||
component: AuthLayoutComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -27,6 +29,7 @@ export const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'register',
|
||||
canActivate: [guestOnlyGuard],
|
||||
component: AuthLayoutComponent,
|
||||
children: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user