Improve auth error handling and session expiration behavior
This commit is contained in:
parent
dbe1ab4588
commit
a2f7152202
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,9 +54,14 @@
|
|||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="password" #password id="password" class="form-control" placeholder="Password"
|
||||
[field]="loginForm.password" />
|
||||
<button class="input-group-text bg-white"
|
||||
<button class="input-group-text bg-white" type="button" tabindex="-1"
|
||||
(click)="password.type = password.type === 'password' ? 'text' : 'password'"
|
||||
[disabled]="loginFormModel().password.length === 0"><i class="bi bi-eye"></i></button>
|
||||
[disabled]="loginFormModel().password.length === 0">
|
||||
<i
|
||||
class="bi"
|
||||
[class.bi-eye]="password.type === 'password'"
|
||||
[class.bi-eye-slash]="password.type === 'text'"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sign up -->
|
||||
|
|
@ -75,4 +80,4 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,12 @@
|
|||
placeholder="********" />
|
||||
<button class="input-group-text bg-white" type="button" tabindex="-1"
|
||||
(click)="password.type = password.type === 'password' ? 'text' : 'password'"
|
||||
[disabled]="signupModel().password.length === 0"><i class="bi bi-eye"></i></button>
|
||||
[disabled]="signupModel().password.length === 0">
|
||||
<i
|
||||
class="bi"
|
||||
[class.bi-eye]="password.type === 'password'"
|
||||
[class.bi-eye-slash]="password.type === 'text'"></i>
|
||||
</button>
|
||||
@if (isInvalid(signupForm.password())) {
|
||||
<small class="invalid-feedback">{{ signupForm.password().errors()[0].message }}</small>
|
||||
}
|
||||
|
|
@ -62,8 +67,12 @@
|
|||
[field]="signupForm.confirmPassword" placeholder="********" />
|
||||
<button class="input-group-text bg-white" type="button" tabindex="-1"
|
||||
(click)="confirmPassword.type = confirmPassword.type === 'password' ? 'text' : 'password'"
|
||||
[disabled]="signupModel().confirmPassword.length === 0"><i
|
||||
class="bi bi-eye"></i></button>
|
||||
[disabled]="signupModel().confirmPassword.length === 0">
|
||||
<i
|
||||
class="bi"
|
||||
[class.bi-eye]="confirmPassword.type === 'password'"
|
||||
[class.bi-eye-slash]="confirmPassword.type === 'text'"></i>
|
||||
</button>
|
||||
@if (!isInvalid(signupForm.password()) && isInvalid(signupForm.confirmPassword())) {
|
||||
<small class="invalid-feedback">{{
|
||||
signupForm.confirmPassword().errors()[0].message}}</small>
|
||||
|
|
@ -101,4 +110,4 @@
|
|||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export class AuthorizationCallFakeService extends AuthorizationCallServiceBase {
|
|||
return new Observable<User>((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 {
|
|||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue