diff --git a/src/app/interceptors/auth-token.interceptor.ts b/src/app/interceptors/auth-token.interceptor.ts index 79ddf82..731d6e1 100644 --- a/src/app/interceptors/auth-token.interceptor.ts +++ b/src/app/interceptors/auth-token.interceptor.ts @@ -1,27 +1,61 @@ -import { HttpInterceptorFn } from '@angular/common/http'; +import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http'; import { Authorization } from '@services/authorization/authorization'; +import { catchError, throwError } from 'rxjs'; + +let lastSessionExpiredNotificationAt = 0; +let lastServiceErrorNotificationAt = 0; export const authTokenInterceptor: HttpInterceptorFn = (req, next) => { const token = getToken(); - if (!token) return next(req); - const requestPath = req.url.split('?')[0]; const isAuthEndpoint = requestPath.endsWith('/auth/login') || requestPath.endsWith('/auth/register'); - if (isAuthEndpoint) { - return next(req); + const hasToken = !!token; + + if (token && !isAuthEndpoint && isTokenExpired(token)) { + clearAuthStorage(); + notifySessionExpired(); + redirectToLogin(); + return throwError(() => + new HttpErrorResponse({ + status: 401, + statusText: 'Session expired', + error: { message: 'Session expired' } + }) + ); } - if (req.headers.has('Authorization')) { - return next(req); - } - - return next( - req.clone({ + let requestToSend = req; + if (token && !isAuthEndpoint && !req.headers.has('Authorization')) { + requestToSend = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } + }); + } + + return next(requestToSend).pipe( + catchError((error: unknown) => { + if ( + hasToken && + !isAuthEndpoint && + error instanceof HttpErrorResponse && + error.status === 401 + ) { + clearAuthStorage(); + notifySessionExpired(); + redirectToLogin(); + } + + if ( + error instanceof HttpErrorResponse && + (error.status === 0 || error.status >= 500) + ) { + notifyServiceContactError(); + } + + return throwError(() => error); }) ); }; @@ -40,3 +74,45 @@ function getToken(): string | null { return null; } } + +function isTokenExpired(token: string): boolean { + try { + const payloadSegment = token.split('.')[1]; + if (!payloadSegment) return false; + + const normalized = payloadSegment.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '='); + const decoded = atob(padded); + const payload = JSON.parse(decoded) as { exp?: unknown }; + if (typeof payload.exp !== 'number') return false; + + const nowSeconds = Math.floor(Date.now() / 1000); + return payload.exp <= nowSeconds; + } catch { + return false; + } +} + +function clearAuthStorage() { + localStorage.removeItem(Authorization.USER_STORAGE_KEY); + localStorage.removeItem(Authorization.TOKEN_STORAGE_KEY); +} + +function notifySessionExpired() { + const now = Date.now(); + if (now - lastSessionExpiredNotificationAt < 1200) return; + lastSessionExpiredNotificationAt = now; + window.alert('Sessione scaduta. Effettua di nuovo il login.'); +} + +function notifyServiceContactError() { + const now = Date.now(); + if (now - lastServiceErrorNotificationAt < 1200) return; + lastServiceErrorNotificationAt = now; + window.alert('Error contacting service, please retry later.'); +} + +function redirectToLogin() { + if (window.location.pathname === '/login') return; + window.location.assign('/login'); +} diff --git a/src/app/pages/auth/login/login.html b/src/app/pages/auth/login/login.html index 05a13b6..cf2a60c 100644 --- a/src/app/pages/auth/login/login.html +++ b/src/app/pages/auth/login/login.html @@ -54,9 +54,14 @@ - + [disabled]="loginFormModel().password.length === 0"> + + @@ -75,4 +80,4 @@ - \ No newline at end of file + diff --git a/src/app/pages/auth/signup/signup.html b/src/app/pages/auth/signup/signup.html index 3d31e28..7dfd2b7 100644 --- a/src/app/pages/auth/signup/signup.html +++ b/src/app/pages/auth/signup/signup.html @@ -47,7 +47,12 @@ placeholder="********" /> + [disabled]="signupModel().password.length === 0"> + + @if (isInvalid(signupForm.password())) { {{ signupForm.password().errors()[0].message }} } @@ -62,8 +67,12 @@ [field]="signupForm.confirmPassword" placeholder="********" /> + [disabled]="signupModel().confirmPassword.length === 0"> + + @if (!isInvalid(signupForm.password()) && isInvalid(signupForm.confirmPassword())) { {{ signupForm.confirmPassword().errors()[0].message}} @@ -101,4 +110,4 @@ - \ No newline at end of file + diff --git a/src/app/services/authorization/authorization-call.fake.ts b/src/app/services/authorization/authorization-call.fake.ts index a70b63f..93f3f7c 100644 --- a/src/app/services/authorization/authorization-call.fake.ts +++ b/src/app/services/authorization/authorization-call.fake.ts @@ -13,7 +13,7 @@ export class AuthorizationCallFakeService extends AuthorizationCallServiceBase { return new Observable((observer) => { const user = this.users.find(u => u.username === username); if (!user) { - observer.error(new Error('Invalid username')); + observer.error(new Error('User not found')); return; } if (password !== user.password) { @@ -45,4 +45,4 @@ export class AuthorizationCallFakeService extends AuthorizationCallServiceBase { -} \ No newline at end of file +} diff --git a/src/app/services/authorization/authorization-call.ts b/src/app/services/authorization/authorization-call.ts index 597ac74..bc2bb08 100644 --- a/src/app/services/authorization/authorization-call.ts +++ b/src/app/services/authorization/authorization-call.ts @@ -1,7 +1,7 @@ import { AuthorizationCallServiceBase } from "./authorization-call.base"; import { User, UserRegistration } from "@models/user"; -import { map, Observable } from "rxjs"; -import { HttpClient } from "@angular/common/http"; +import { catchError, map, Observable, throwError } from "rxjs"; +import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { inject } from "@angular/core"; import { environment } from "@environment"; @@ -25,6 +25,15 @@ export class AuthorizationCallService extends AuthorizationCallServiceBase { email: String(userSource["email"] ?? ''), token: typeof token === "string" && token.length > 0 ? token : undefined } satisfies User; + }), + catchError((error: unknown) => { + if (error instanceof HttpErrorResponse && error.status === 404) { + return throwError(() => new Error('User not found')); + } + if (error instanceof HttpErrorResponse && error.status === 401) { + return throwError(() => new Error('Invalid password')); + } + return throwError(() => error); }) ); }