feat(back-end and front-end): back-office

This commit is contained in:
2026-02-27 15:07:32 +01:00
parent 65e1ee3be6
commit 949770a741
38 changed files with 2558 additions and 345 deletions

View File

@@ -1,8 +1,9 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component, inject, OnDestroy } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AdminAuthService } from '../services/admin-auth.service';
import { HttpErrorResponse } from '@angular/common/http';
import { AdminAuthResponse, AdminAuthService } from '../services/admin-auth.service';
const SUPPORTED_LANGS = new Set(['it', 'en', 'de', 'fr']);
@@ -13,7 +14,7 @@ const SUPPORTED_LANGS = new Set(['it', 'en', 'de', 'fr']);
templateUrl: './admin-login.component.html',
styleUrl: './admin-login.component.scss'
})
export class AdminLoginComponent {
export class AdminLoginComponent implements OnDestroy {
private readonly authService = inject(AdminAuthService);
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
@@ -21,9 +22,11 @@ export class AdminLoginComponent {
password = '';
loading = false;
errorMessage: string | null = null;
lockSecondsRemaining = 0;
private lockTimer: ReturnType<typeof setInterval> | null = null;
submit(): void {
if (!this.password.trim() || this.loading) {
if (!this.password.trim() || this.loading || this.lockSecondsRemaining > 0) {
return;
}
@@ -31,24 +34,25 @@ export class AdminLoginComponent {
this.errorMessage = null;
this.authService.login(this.password).subscribe({
next: (isAuthenticated) => {
next: (response: AdminAuthResponse) => {
this.loading = false;
if (!isAuthenticated) {
this.errorMessage = 'Password non valida.';
if (!response?.authenticated) {
this.handleLoginFailure(response?.retryAfterSeconds);
return;
}
this.clearLock();
const redirect = this.route.snapshot.queryParamMap.get('redirect');
if (redirect && redirect.startsWith('/')) {
void this.router.navigateByUrl(redirect);
return;
}
void this.router.navigate(['/', this.resolveLang(), 'admin']);
void this.router.navigate(['/', this.resolveLang(), 'admin', 'orders']);
},
error: () => {
error: (error: HttpErrorResponse) => {
this.loading = false;
this.errorMessage = 'Password non valida.';
this.handleLoginFailure(this.extractRetryAfterSeconds(error));
}
});
}
@@ -62,4 +66,59 @@ export class AdminLoginComponent {
}
return 'it';
}
private handleLoginFailure(retryAfterSeconds: number | undefined): void {
const timeout = this.normalizeTimeout(retryAfterSeconds);
this.errorMessage = 'Password non valida.';
this.startLock(timeout);
}
private extractRetryAfterSeconds(error: HttpErrorResponse): number {
const fromBody = Number(error?.error?.retryAfterSeconds);
if (Number.isFinite(fromBody) && fromBody > 0) {
return Math.floor(fromBody);
}
const fromHeader = Number(error?.headers?.get('Retry-After'));
if (Number.isFinite(fromHeader) && fromHeader > 0) {
return Math.floor(fromHeader);
}
return 2;
}
private normalizeTimeout(value: number | undefined): number {
const parsed = Number(value);
if (Number.isFinite(parsed) && parsed > 0) {
return Math.floor(parsed);
}
return 2;
}
private startLock(seconds: number): void {
this.lockSecondsRemaining = Math.max(this.lockSecondsRemaining, seconds);
this.stopTimer();
this.lockTimer = setInterval(() => {
this.lockSecondsRemaining = Math.max(0, this.lockSecondsRemaining - 1);
if (this.lockSecondsRemaining === 0) {
this.stopTimer();
}
}, 1000);
}
private clearLock(): void {
this.lockSecondsRemaining = 0;
this.stopTimer();
}
private stopTimer(): void {
if (this.lockTimer !== null) {
clearInterval(this.lockTimer);
this.lockTimer = null;
}
}
ngOnDestroy(): void {
this.stopTimer();
}
}