From d67abda6c1370fe61c3cabb66e722950c859ccae Mon Sep 17 00:00:00 2001 From: Nazar Semets Date: Thu, 22 May 2025 12:28:44 +0300 Subject: [PATCH 1/3] feat(interceptors): added auth and error interceptor --- eslint.config.js | 2 +- src/app/app.config.ts | 10 +++--- src/app/core/constants/error-messages.ts | 8 +++++ src/app/core/interceptors/auth.interceptor.ts | 26 +++++++++++++++ .../core/interceptors/error.interceptor.ts | 33 +++++++++++++++++++ src/app/core/interceptors/index.ts | 2 ++ .../services/json-api/json-api.service.ts | 22 +++---------- 7 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 src/app/core/constants/error-messages.ts create mode 100644 src/app/core/interceptors/auth.interceptor.ts create mode 100644 src/app/core/interceptors/error.interceptor.ts create mode 100644 src/app/core/interceptors/index.ts diff --git a/eslint.config.js b/eslint.config.js index 0b8919e98..a7621bd40 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -55,7 +55,7 @@ module.exports = tseslint.config( ['^ngx-', '^@ngx', '^ng-'], // Third-party packages (primeng) - ['^primeng'], + ['^@primeng', '^primeng'], // RxJS packages (rxjs or @rxjs/...) ['^rxjs', '^rxjs/operators'], diff --git a/src/app/app.config.ts b/src/app/app.config.ts index eca2ff44c..69c381ab1 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -3,10 +3,11 @@ import { provideStore } from '@ngxs/store'; import { TranslateModule } from '@ngx-translate/core'; +import Aura from '@primeng/themes/aura'; import { ConfirmationService, MessageService } from 'primeng/api'; import { providePrimeNG } from 'primeng/config'; -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter } from '@angular/router'; @@ -14,10 +15,9 @@ import { provideRouter } from '@angular/router'; import { STATES } from '@core/constants/ngxs-states.constant'; import { provideTranslation } from '@core/helpers/i18n.helper'; +import { authInterceptor, errorInterceptor } from './core/interceptors'; import { routes } from './app.routes'; -import Aura from '@primeng/themes/aura'; - export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), @@ -36,9 +36,9 @@ export const appConfig: ApplicationConfig = { }, }), provideAnimations(), - provideHttpClient(), + provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])), + importProvidersFrom(TranslateModule.forRoot(provideTranslation())), ConfirmationService, MessageService, - importProvidersFrom(TranslateModule.forRoot(provideTranslation())), ], }; diff --git a/src/app/core/constants/error-messages.ts b/src/app/core/constants/error-messages.ts new file mode 100644 index 000000000..747b0e921 --- /dev/null +++ b/src/app/core/constants/error-messages.ts @@ -0,0 +1,8 @@ +export const ERROR_MESSAGES = { + 400: 'Invalid request. Please check your input.', + 401: 'You are not authorized to perform this action.', + 403: `You don't have permission to access this resource.`, + 404: 'The requested resource was not found.', + 500: 'An unexpected error occurred. Please try again later.', + default: 'An unknown error occurred. Please try again.', +}; diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts new file mode 100644 index 000000000..0e6e44435 --- /dev/null +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -0,0 +1,26 @@ +import { Observable } from 'rxjs'; + +import { HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http'; + +export const authInterceptor: HttpInterceptorFn = ( + req: HttpRequest, + next: HttpHandlerFn +): Observable> => { + const authToken = '2rjFZwmdDG4rtKj7hGkEMO6XyHBM2lN7XBbsA1e8OqcFhOWu6Z7fQZiheu9RXtzSeVrgOt'; + // OBJoUomBgbUuDgQo5JoaSKNya6XaYcd0ojAX1XOLmWi6J2arQPzByxyEi81fHE60drQUWv + // UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm + + if (authToken) { + const authReq = req.clone({ + setHeaders: { + Authorization: `Bearer ${authToken}`, + Accept: 'application/vnd.api+json', + 'Content-Type': 'application/vnd.api+json', + }, + }); + + return next(authReq); + } + + return next(req); +}; diff --git a/src/app/core/interceptors/error.interceptor.ts b/src/app/core/interceptors/error.interceptor.ts new file mode 100644 index 000000000..214dab280 --- /dev/null +++ b/src/app/core/interceptors/error.interceptor.ts @@ -0,0 +1,33 @@ +import { throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; + +import { ToastService } from '@osf/shared/services'; + +import { ERROR_MESSAGES } from '../constants/error-messages'; + +export const errorInterceptor: HttpInterceptorFn = (req, next) => { + const toastService = inject(ToastService); + + return next(req).pipe( + catchError((error: HttpErrorResponse) => { + let errorMessage: string; + + if (error.error instanceof ErrorEvent) { + errorMessage = error.error.message; + } else { + if (error.error?.errors?.[0]?.detail) { + errorMessage = error.error.errors[0].detail; + } else { + errorMessage = ERROR_MESSAGES[error.status as keyof typeof ERROR_MESSAGES] || ERROR_MESSAGES.default; + } + } + + toastService.showError(errorMessage); + + return throwError(() => error); + }) + ); +}; diff --git a/src/app/core/interceptors/index.ts b/src/app/core/interceptors/index.ts new file mode 100644 index 000000000..830718180 --- /dev/null +++ b/src/app/core/interceptors/index.ts @@ -0,0 +1,2 @@ +export * from './auth.interceptor'; +export * from './error.interceptor'; diff --git a/src/app/core/services/json-api/json-api.service.ts b/src/app/core/services/json-api/json-api.service.ts index 3a70ba532..e7bdd2c2a 100644 --- a/src/app/core/services/json-api/json-api.service.ts +++ b/src/app/core/services/json-api/json-api.service.ts @@ -1,6 +1,6 @@ import { map, Observable } from 'rxjs'; -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; @@ -10,19 +10,10 @@ import { JsonApiResponse } from '@core/services/json-api/json-api.entity'; }) export class JsonApiService { http: HttpClient = inject(HttpClient); - readonly #token = 'Bearer 2rjFZwmdDG4rtKj7hGkEMO6XyHBM2lN7XBbsA1e8OqcFhOWu6Z7fQZiheu9RXtzSeVrgOt'; - // OBJoUomBgbUuDgQo5JoaSKNya6XaYcd0ojAX1XOLmWi6J2arQPzByxyEi81fHE60drQUWv - // UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm - readonly #headers = new HttpHeaders({ - Authorization: this.#token, - Accept: 'application/vnd.api+json', - 'Content-Type': 'application/vnd.api+json', - }); get(url: string, params?: Record): Observable { return this.http.get(url, { params: this.buildHttpParams(params), - headers: this.#headers, }); } @@ -49,25 +40,20 @@ export class JsonApiService { post(url: string, body: unknown, params?: Record): Observable { return this.http .post>(url, body, { - headers: this.#headers, params: this.buildHttpParams(params), }) .pipe(map((response) => response.data)); } patch(url: string, body: unknown): Observable { - return this.http - .patch>(url, body, { headers: this.#headers }) - .pipe(map((response) => response.data)); + return this.http.patch>(url, body).pipe(map((response) => response.data)); } put(url: string, body: unknown): Observable { - return this.http - .put>(url, body, { headers: this.#headers }) - .pipe(map((response) => response.data)); + return this.http.put>(url, body).pipe(map((response) => response.data)); } delete(url: string, body?: unknown): Observable { - return this.http.delete(url, { headers: this.#headers, body: body }); + return this.http.delete(url, { body: body }); } } From 21befb181b9901fcb67779c44bfe096d8c5c4566 Mon Sep 17 00:00:00 2001 From: Nazar Semets Date: Thu, 22 May 2025 13:52:45 +0300 Subject: [PATCH 2/3] feat(interceptors): added global error handler --- src/app/app.config.ts | 4 +++- src/app/core/handlers/global-error.handler.ts | 8 ++++++++ src/app/core/handlers/index.ts | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/app/core/handlers/global-error.handler.ts create mode 100644 src/app/core/handlers/index.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 69c381ab1..e5217ad44 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -8,13 +8,14 @@ import { ConfirmationService, MessageService } from 'primeng/api'; import { providePrimeNG } from 'primeng/config'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; -import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; +import { ApplicationConfig, ErrorHandler, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter } from '@angular/router'; import { STATES } from '@core/constants/ngxs-states.constant'; import { provideTranslation } from '@core/helpers/i18n.helper'; +import { GlobalErrorHandler } from './core/handlers'; import { authInterceptor, errorInterceptor } from './core/interceptors'; import { routes } from './app.routes'; @@ -40,5 +41,6 @@ export const appConfig: ApplicationConfig = { importProvidersFrom(TranslateModule.forRoot(provideTranslation())), ConfirmationService, MessageService, + { provide: ErrorHandler, useClass: GlobalErrorHandler }, ], }; diff --git a/src/app/core/handlers/global-error.handler.ts b/src/app/core/handlers/global-error.handler.ts new file mode 100644 index 000000000..6d6bcc907 --- /dev/null +++ b/src/app/core/handlers/global-error.handler.ts @@ -0,0 +1,8 @@ +import { ErrorHandler, Injectable } from '@angular/core'; + +@Injectable() +export class GlobalErrorHandler implements ErrorHandler { + handleError(error: unknown): void { + console.error('Error:', error); + } +} diff --git a/src/app/core/handlers/index.ts b/src/app/core/handlers/index.ts new file mode 100644 index 000000000..400695f14 --- /dev/null +++ b/src/app/core/handlers/index.ts @@ -0,0 +1 @@ +export { GlobalErrorHandler } from './global-error.handler'; From fd35b904764fdbc7cc7364870c65cbf1696ba623 Mon Sep 17 00:00:00 2001 From: Nazar Semets Date: Thu, 22 May 2025 14:51:39 +0300 Subject: [PATCH 3/3] feat(interceptors): fixed toast --- src/assets/styles/overrides/toast.scss | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/assets/styles/overrides/toast.scss b/src/assets/styles/overrides/toast.scss index f9c486af5..1f37a0421 100644 --- a/src/assets/styles/overrides/toast.scss +++ b/src/assets/styles/overrides/toast.scss @@ -1,19 +1,15 @@ -.p-toast-message-icon { - display: none; +.p-toast-message-content { + align-items: center; } -.p-toast-summary { - --p-toast-summary-font-weight: 700; +.p-toast-message-icon { + display: none; } .p-toast-detail { display: none; } -.p-toast-close-button { - margin: -10% 0 0 0; -} - .p-toast-message-success { --p-toast-success-border-color: transparent; --p-toast-success-color: var(--white);