Fix authentication errors handling.
This commit is contained in:
@@ -7,10 +7,7 @@ import static org.springframework.http.HttpStatus.UNAUTHORIZED;
|
|||||||
import org.codiki.domain.category.exception.CategoryDeletionException;
|
import org.codiki.domain.category.exception.CategoryDeletionException;
|
||||||
import org.codiki.domain.category.exception.CategoryEditionException;
|
import org.codiki.domain.category.exception.CategoryEditionException;
|
||||||
import org.codiki.domain.category.exception.CategoryNotFoundException;
|
import org.codiki.domain.category.exception.CategoryNotFoundException;
|
||||||
import org.codiki.domain.exception.LoginFailureException;
|
import org.codiki.domain.exception.*;
|
||||||
import org.codiki.domain.exception.RefreshTokenDoesNotExistException;
|
|
||||||
import org.codiki.domain.exception.RefreshTokenExpiredException;
|
|
||||||
import org.codiki.domain.exception.UserDoesNotExistException;
|
|
||||||
import org.codiki.domain.picture.exception.PictureNotFoundException;
|
import org.codiki.domain.picture.exception.PictureNotFoundException;
|
||||||
import org.codiki.domain.picture.exception.PictureUploadException;
|
import org.codiki.domain.picture.exception.PictureUploadException;
|
||||||
import org.codiki.domain.publication.exception.*;
|
import org.codiki.domain.publication.exception.*;
|
||||||
@@ -51,7 +48,8 @@ public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHan
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler({
|
@ExceptionHandler({
|
||||||
RefreshTokenExpiredException.class
|
RefreshTokenExpiredException.class,
|
||||||
|
AuthenticationRequiredException.class,
|
||||||
})
|
})
|
||||||
public ProblemDetail handleUnauthorizedExceptions(Exception exception) {
|
public ProblemDetail handleUnauthorizedExceptions(Exception exception) {
|
||||||
return buildProblemDetail(UNAUTHORIZED, exception);
|
return buildProblemDetail(UNAUTHORIZED, exception);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'publications/:publicationId/edit',
|
path: 'publications/:publicationId/edit',
|
||||||
loadComponent: () => import('./pages/publication-edition/publication-edition.component').then(module => module.PublicationEditionComponent)
|
loadChildren: () => import('./pages/publication-edition/publication-edition.routes').then(module => module.ROUTES)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'publications',
|
path: 'publications',
|
||||||
|
|||||||
18
frontend/src/app/core/guard/authentication.guard.ts
Normal file
18
frontend/src/app/core/guard/authentication.guard.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { inject } from "@angular/core";
|
||||||
|
import { CanActivateFn, Router } from "@angular/router";
|
||||||
|
import { AuthenticationService } from "../service/authentication.service";
|
||||||
|
import { MatSnackBar } from "@angular/material/snack-bar";
|
||||||
|
|
||||||
|
export const authenticationGuard: CanActivateFn = () => {
|
||||||
|
const authenticationService = inject(AuthenticationService);
|
||||||
|
const router = inject(Router);
|
||||||
|
const snackBar = inject(MatSnackBar);
|
||||||
|
|
||||||
|
if (authenticationService.isAuthenticated()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
router.navigate(['/login']);
|
||||||
|
snackBar.open('You are unauthenticated. Please, log-in first.', 'Close', { duration: 5000 });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,88 @@
|
|||||||
import { Observable } from 'rxjs';
|
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||||
import { inject, Injectable } from '@angular/core';
|
import { inject, Injectable } from '@angular/core';
|
||||||
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
|
import { catchError, filter, Observable, Subject, switchMap, take, throwError } from 'rxjs';
|
||||||
|
import { RefreshTokenRequest } from '../rest-services/user/model/refresh-token.model';
|
||||||
|
import { UserRestService } from '../rest-services/user/user.rest-service';
|
||||||
import { AuthenticationService } from '../service/authentication.service';
|
import { AuthenticationService } from '../service/authentication.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtInterceptor implements HttpInterceptor {
|
export class JwtInterceptor implements HttpInterceptor {
|
||||||
private readonly authenticationService = inject(AuthenticationService);
|
private readonly authenticationService = inject(AuthenticationService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
private readonly userRestService = inject(UserRestService);
|
||||||
|
private readonly snackBar = inject(MatSnackBar);
|
||||||
|
private isRefreshingToken = false;
|
||||||
|
private refreshTokenSubject = new Subject<string | undefined>();
|
||||||
|
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
if (this.authenticationService.isTokenExpired()) {
|
let requestWithAuthentication = request;
|
||||||
this.authenticationService.renewToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
const jwt = this.authenticationService.getToken();
|
const jwt = this.authenticationService.getToken();
|
||||||
|
|
||||||
if (jwt) {
|
if (jwt) {
|
||||||
const cloned = request.clone({
|
requestWithAuthentication = this.addTokenInHeaders(request, jwt);
|
||||||
headers: request.headers.set('Authorization', `Bearer ${jwt}`)
|
|
||||||
});
|
|
||||||
|
|
||||||
return next.handle(cloned);
|
|
||||||
} else {
|
|
||||||
this.authenticationService.unauthenticate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return next.handle(request);
|
return next.handle(requestWithAuthentication)
|
||||||
|
.pipe(
|
||||||
|
catchError(error => {
|
||||||
|
if (error instanceof HttpErrorResponse && error.status === 401) {
|
||||||
|
return this.handleAunauthorizedError(request, next, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAunauthorizedError(request: HttpRequest<any>, next: HttpHandler, initialError: any): Observable<HttpEvent<any>> {
|
||||||
|
if (!this.isRefreshingToken) {
|
||||||
|
this.isRefreshingToken = true;
|
||||||
|
this.refreshTokenSubject.next(undefined);
|
||||||
|
|
||||||
|
const refreshToken = this.authenticationService.getRefreshToken();
|
||||||
|
if (refreshToken) {
|
||||||
|
const refreshTokenRequest: RefreshTokenRequest = {
|
||||||
|
refreshTokenValue: refreshToken
|
||||||
|
};
|
||||||
|
this.userRestService.refreshToken(refreshTokenRequest)
|
||||||
|
.then(refreshTokenResponse => {
|
||||||
|
this.authenticationService.authenticate(refreshTokenResponse.accessToken, refreshTokenResponse.refreshToken);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return this.handleNoRefreshToken(initialError);
|
||||||
|
})
|
||||||
|
.finally(() => this.isRefreshingToken = false);
|
||||||
|
} else {
|
||||||
|
return this.handleNoRefreshToken(initialError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.refreshTokenSubject.pipe(
|
||||||
|
filter(token => !!token),
|
||||||
|
take(1),
|
||||||
|
switchMap(token => {
|
||||||
|
let requestWithAuthentication = request;
|
||||||
|
if (token) {
|
||||||
|
requestWithAuthentication = this.addTokenInHeaders(request, token)
|
||||||
|
}
|
||||||
|
return next.handle(requestWithAuthentication);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTokenInHeaders(request: HttpRequest<any>, token: string): HttpRequest<any> {
|
||||||
|
return request.clone({
|
||||||
|
headers: request.headers.set('Authorization', `Bearer ${token}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleNoRefreshToken(initialError: any): Observable<HttpEvent<any>> {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
this.refreshTokenSubject.next(undefined);
|
||||||
|
this.authenticationService.unauthenticate();
|
||||||
|
this.snackBar.open('You are unauthenticated. Please, re-authenticate before retrying your action.', 'Close', { duration: 5000 });
|
||||||
|
return throwError(() => initialError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,6 @@ interface UserDetails {
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AuthenticationService {
|
export class AuthenticationService {
|
||||||
private userRestService = inject(UserRestService);
|
|
||||||
|
|
||||||
authenticate(token: string, refreshToken: string): void {
|
authenticate(token: string, refreshToken: string): void {
|
||||||
localStorage.setItem(JWT_PARAM, token);
|
localStorage.setItem(JWT_PARAM, token);
|
||||||
@@ -50,6 +49,10 @@ export class AuthenticationService {
|
|||||||
return localStorage.getItem(JWT_PARAM) ?? undefined;
|
return localStorage.getItem(JWT_PARAM) ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRefreshToken(): string | undefined {
|
||||||
|
return localStorage.getItem(REFRESH_TOKEN_PARAM) ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
isTokenExpired(): boolean {
|
isTokenExpired(): boolean {
|
||||||
let result = false;
|
let result = false;
|
||||||
|
|
||||||
@@ -65,19 +68,6 @@ export class AuthenticationService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
renewToken(): void {
|
|
||||||
const refreshToken = localStorage.getItem(REFRESH_TOKEN_PARAM);
|
|
||||||
if (refreshToken) {
|
|
||||||
const request: RefreshTokenRequest = {
|
|
||||||
refreshTokenValue: refreshToken
|
|
||||||
};
|
|
||||||
this.userRestService.refreshToken(request)
|
|
||||||
.then(refreshTokenResponse => {
|
|
||||||
this.authenticate(refreshTokenResponse.accessToken, refreshTokenResponse.refreshToken);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractUserFromLocalStorage(): User | undefined {
|
private extractUserFromLocalStorage(): User | undefined {
|
||||||
let result: User | undefined = undefined;
|
let result: User | undefined = undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,13 @@ export class PictureSelectionDialog implements OnInit {
|
|||||||
this.pictures = pictures;
|
this.pictures = pictures;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
if (error.status === 401) {
|
||||||
|
this.dialogRef.close();
|
||||||
|
} else {
|
||||||
const errorMessage = 'An error occured while loading pictures.';
|
const errorMessage = 'An error occured while loading pictures.';
|
||||||
console.error(errorMessage, error);
|
console.error(errorMessage, error);
|
||||||
this.snackBar.open(errorMessage, 'Close', { duration: 5000 });
|
this.snackBar.open(errorMessage, 'Close', { duration: 5000 });
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { CommonModule, Location } from '@angular/common';
|
import { CommonModule, Location } from '@angular/common';
|
||||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { debounceTime, map, Observable, Subscription } from 'rxjs';
|
|
||||||
import { Publication } from '../../core/rest-services/publications/model/publication';
|
|
||||||
import { PublicationEditionService } from './publication-edition.service';
|
|
||||||
import {MatDialogModule} from '@angular/material/dialog';
|
|
||||||
import { PictureSelectionDialog } from './picture-selection-dialog/picture-selection-dialog.component';
|
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
import { map, Observable, Subscription } from 'rxjs';
|
||||||
import { SubmitButtonComponent } from '../../components/submit-button/submit-button.component';
|
import { SubmitButtonComponent } from '../../components/submit-button/submit-button.component';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { Publication } from '../../core/rest-services/publications/model/publication';
|
||||||
|
import { PictureSelectionDialog } from './picture-selection-dialog/picture-selection-dialog.component';
|
||||||
|
import { PublicationEditionService } from './publication-edition.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-publication-edition',
|
selector: 'app-publication-edition',
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Route } from "@angular/router";
|
||||||
|
import { PublicationEditionComponent } from "./publication-edition.component";
|
||||||
|
import { authenticationGuard } from "../../core/guard/authentication.guard";
|
||||||
|
|
||||||
|
export const ROUTES: Route[] = [
|
||||||
|
{ path: '', component: PublicationEditionComponent, canActivate: [authenticationGuard] }
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user