diff --git a/npm/ng-packs/packages/theme-shared/src/lib/constants/default-errors.ts b/npm/ng-packs/packages/theme-shared/src/lib/constants/default-errors.ts new file mode 100644 index 0000000000..46a0605b95 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/constants/default-errors.ts @@ -0,0 +1,53 @@ +export const DEFAULT_ERROR_MESSAGES = { + defaultError: { + title: 'An error has occurred!', + details: 'Error detail not sent by server.', + }, + defaultError401: { + title: 'You are not authenticated!', + details: 'You should be authenticated (sign in) in order to perform this operation.', + }, + defaultError403: { + title: 'You are not authorized!', + details: 'You are not allowed to perform this operation.', + }, + defaultError404: { + title: 'Resource not found!', + details: 'The resource requested could not found on the server.', + }, + defaultError500: { + title: 'Internal server error', + details: 'Error detail not sent by server.', + }, +}; + +export const DEFAULT_ERROR_LOCALIZATIONS = { + defaultError: { + title: 'AbpUi::DefaultErrorMessage', + details: 'AbpUi::DefaultErrorMessageDetail', + }, + defaultError401: { + title: 'AbpUi::DefaultErrorMessage401', + details: 'AbpUi::DefaultErrorMessage401Detail', + }, + defaultError403: { + title: 'AbpUi::DefaultErrorMessage403', + details: 'AbpUi::DefaultErrorMessage403Detail', + }, + defaultError404: { + title: 'AbpUi::DefaultErrorMessage404', + details: 'AbpUi::DefaultErrorMessage404Detail', + }, + defaultError500: { + title: 'AbpUi::500Message', + details: 'AbpUi::DefaultErrorMessage', + }, +}; + +export const CUSTOM_HTTP_ERROR_HANDLER_PRIORITY = Object.freeze({ + veryLow: -99, + low: -9, + normal: 0, + high: 9, + veryHigh: 99, +}); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts index cc7f733798..3ae0c25d3c 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts @@ -1,132 +1,45 @@ - import { - AuthService, - HttpErrorReporterService, - LocalizationParam, - RouterEvents, - SessionStateService, -} from '@abp/ng.core'; +import { HttpErrorReporterService } from '@abp/ng.core'; import { HttpErrorResponse } from '@angular/common/http'; -import { - ApplicationRef, - ComponentFactoryResolver, - ComponentRef, - EmbeddedViewRef, - Injectable, - Injector, - RendererFactory2, -} from '@angular/core'; -import { NavigationError, ResolveEnd } from '@angular/router'; -import { Observable, of, Subject, throwError } from 'rxjs'; +import { inject, Injectable, Injector } from '@angular/core'; +import { Observable, of, throwError } from 'rxjs'; import { catchError, filter, switchMap } from 'rxjs/operators'; -import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; -import { ErrorScreenErrorCodes, HttpErrorConfig } from '../models/common'; +import { CustomHttpErrorHandlerService } from '../models/common'; import { Confirmation } from '../models/confirmation'; import { ConfirmationService } from '../services/confirmation.service'; -import { HTTP_ERROR_HANDLER } from '../tokens/http-error.token'; - -export const DEFAULT_ERROR_MESSAGES = { - defaultError: { - title: 'An error has occurred!', - details: 'Error detail not sent by server.', - }, - defaultError401: { - title: 'You are not authenticated!', - details: 'You should be authenticated (sign in) in order to perform this operation.', - }, - defaultError403: { - title: 'You are not authorized!', - details: 'You are not allowed to perform this operation.', - }, - defaultError404: { - title: 'Resource not found!', - details: 'The resource requested could not found on the server.', - }, - defaultError500: { - title: 'Internal server error', - details: 'Error detail not sent by server.', - }, -}; - -export const DEFAULT_ERROR_LOCALIZATIONS = { - defaultError: { - title: 'AbpUi::DefaultErrorMessage', - details: 'AbpUi::DefaultErrorMessageDetail', - }, - defaultError401: { - title: 'AbpUi::DefaultErrorMessage401', - details: 'AbpUi::DefaultErrorMessage401Detail', - }, - defaultError403: { - title: 'AbpUi::DefaultErrorMessage403', - details: 'AbpUi::DefaultErrorMessage403Detail', - }, - defaultError404: { - title: 'AbpUi::DefaultErrorMessage404', - details: 'AbpUi::DefaultErrorMessage404Detail', - }, - defaultError500: { - title: 'AbpUi::500Message', - details: 'AbpUi::DefaultErrorMessage', - }, -}; +import { CUSTOM_ERROR_HANDLERS, HTTP_ERROR_HANDLER } from '../tokens/http-error.token'; +import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors'; +import { RouterErrorHandlerService } from '../services/router-error-handler.service'; +import { HTTP_ERROR_CONFIG } from '../tokens/http-error.token'; @Injectable({ providedIn: 'root' }) export class ErrorHandler { - componentRef: ComponentRef | null = null; - - protected httpErrorHandler = this.injector.get(HTTP_ERROR_HANDLER, (_, err: HttpErrorResponse) => - throwError(err), - ); - - protected httpErrorReporter: HttpErrorReporterService; - protected routerEvents: RouterEvents; - protected confirmationService: ConfirmationService; - protected cfRes: ComponentFactoryResolver; - protected rendererFactory: RendererFactory2; - protected httpErrorConfig: HttpErrorConfig; - protected sessionStateService: SessionStateService; - private authService: AuthService; - + private httpErrorReporter = inject(HttpErrorReporterService); + private confirmationService = inject(ConfirmationService); + private routerErrorHandlerService = inject(RouterErrorHandlerService); + protected httpErrorConfig = inject(HTTP_ERROR_CONFIG); + private customErrorHandlers = inject(CUSTOM_ERROR_HANDLERS); + private defaultHttpErrorHandler = (_, err: HttpErrorResponse) => throwError(() => err); + private httpErrorHandler = + inject(HTTP_ERROR_HANDLER, { optional: true }) || this.defaultHttpErrorHandler; constructor(protected injector: Injector) { - this.httpErrorReporter = injector.get(HttpErrorReporterService); - this.routerEvents = injector.get(RouterEvents); - this.confirmationService = injector.get(ConfirmationService); - this.cfRes = injector.get(ComponentFactoryResolver); - this.rendererFactory = injector.get(RendererFactory2); - this.httpErrorConfig = injector.get('HTTP_ERROR_CONFIG'); - this.authService = this.injector.get(AuthService); - this.sessionStateService = this.injector.get(SessionStateService); - this.listenToRestError(); this.listenToRouterError(); - this.listenToRouterDataResolved(); } protected listenToRouterError() { - this.routerEvents - .getNavigationEvents('Error') - .pipe(filter(this.filterRouteErrors)) - .subscribe(() => this.show404Page()); - } - - protected listenToRouterDataResolved() { - this.routerEvents - .getEvents(ResolveEnd) - .pipe(filter(() => !!this.componentRef)) - .subscribe(() => { - this.componentRef?.destroy(); - this.componentRef = null; - }); + this.routerErrorHandlerService.listen(); } protected listenToRestError() { this.httpErrorReporter.reporter$ .pipe(filter(this.filterRestErrors), switchMap(this.executeErrorHandler)) - .subscribe(); + .subscribe(err => { + this.handleError(err); + }); } - private executeErrorHandler = (error: any) => { + private executeErrorHandler = (error: HttpErrorResponse) => { const errHandler = this.httpErrorHandler(this.injector, error); const isObservable = errHandler instanceof Observable; const response = isObservable ? errHandler : of(null); @@ -139,212 +52,41 @@ export class ErrorHandler { ); }; - private handleError(err: any) { - const body = err?.error?.error || { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, - }; - - if (err instanceof HttpErrorResponse && err.headers.get('Abp-Tenant-Resolve-Error')) { - this.sessionStateService.setTenant(null) - this.authService.logout().subscribe(); - return; - } - - if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) { - const confirmation$ = this.showErrorWithRequestBody(body); + protected sortHttpErrorHandlers( + a: CustomHttpErrorHandlerService, + b: CustomHttpErrorHandlerService, + ) { + return (b.priority || 0) - (a.priority || 0); + } - if (err.status === 401) { - confirmation$.subscribe(() => { - this.navigateToLogin(); - }); - } - } else { - switch (err.status) { - case 401: - this.canCreateCustomError(401) - ? this.show401Page() - : this.showError( - { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title, - }, - { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.details, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.details, - }, - ).subscribe(() => this.navigateToLogin()); - break; - case 403: - this.createErrorComponent({ - title: { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError403.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.title, - }, - details: { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError403.details, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.details, - }, - status: 403, - }); - break; - case 404: - this.canCreateCustomError(404) - ? this.show404Page() - : this.showError( - { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.details, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details, - }, - { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, - }, - ); - break; - case 500: - this.createErrorComponent({ - title: { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError500.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.title, - }, - details: { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError500.details, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.details, - }, - status: 500, - }); - break; - case 0: - if (err.statusText === 'Unknown Error') { - this.createErrorComponent({ - title: { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, - }, - details: err.message, - isHomeShow: false, - }); - } - break; - default: - this.showError( - { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.details, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.details, - }, - { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, - }, - ); - break; + private handleError(err: unknown) { + if (this.customErrorHandlers && this.customErrorHandlers.length) { + const canHandleService = this.customErrorHandlers + .sort(this.sortHttpErrorHandlers) + .find(service => service.canHandle(err)); + if (canHandleService) { + canHandleService.execute(); + return; } } + this.showError().subscribe(); } - protected show401Page() { - this.createErrorComponent({ - title: { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title, - }, - status: 401, - }); - } - - protected show404Page() { - this.createErrorComponent({ - title: { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, - }, - status: 404, - }); - } - - protected showErrorWithRequestBody(body: any) { - let message: LocalizationParam; - let title: LocalizationParam; - - if (body.details) { - message = body.details; - title = body.message; - } else if (body.message) { - title = { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, - }; - message = body.message; - } else { - message = body.message || { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, - }; - title = ''; - } - - return this.showError(message, title); - } - - protected showError( - message: LocalizationParam, - title: LocalizationParam, - ): Observable { + protected showError(): Observable { + const title = { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }; + const message = { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.details, + }; return this.confirmationService.error(message, title, { hideCancelBtn: true, yesText: 'AbpAccount::Close', }); } - private navigateToLogin() { - this.authService.navigateToLogin(); - } - - createErrorComponent(instance: Partial) { - const renderer = this.rendererFactory.createRenderer(null, null); - const host = renderer.selectRootElement(document.body, true); - - this.componentRef = this.cfRes - .resolveComponentFactory(HttpErrorWrapperComponent) - .create(this.injector); - - for (const key in instance) { - /* istanbul ignore else */ - if (Object.prototype.hasOwnProperty.call(this.componentRef.instance, key)) { - (this.componentRef.instance as any)[key] = (instance as any)[key]; - } - } - - this.componentRef.instance.hideCloseIcon = !!this.httpErrorConfig.errorScreen?.hideCloseIcon; - const appRef = this.injector.get(ApplicationRef); - - if (this.canCreateCustomError(instance.status as ErrorScreenErrorCodes)) { - this.componentRef.instance.cfRes = this.cfRes; - this.componentRef.instance.appRef = appRef; - this.componentRef.instance.injector = this.injector; - this.componentRef.instance.customComponent = this.httpErrorConfig.errorScreen?.component; - } - - appRef.attachView(this.componentRef.hostView); - renderer.appendChild(host, (this.componentRef.hostView as EmbeddedViewRef).rootNodes[0]); - - const destroy$ = new Subject(); - this.componentRef.instance.destroy$ = destroy$; - destroy$.subscribe(() => { - this.componentRef?.destroy(); - this.componentRef = null; - }); - } - - canCreateCustomError(status: ErrorScreenErrorCodes): boolean { - return !!( - this.httpErrorConfig?.errorScreen?.component && - this.httpErrorConfig?.errorScreen?.forWhichErrors && - this.httpErrorConfig?.errorScreen?.forWhichErrors.indexOf(status) > -1 - ); - } - protected filterRestErrors = ({ status }: HttpErrorResponse): boolean => { if (typeof status !== 'number') return false; @@ -353,12 +95,4 @@ export class ErrorHandler { this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === status) < 0 ); }; - - protected filterRouteErrors = (navigationError: NavigationError): boolean => { - return ( - navigationError.error?.message?.indexOf('Cannot match') > -1 && - !!this.httpErrorConfig.skipHandledErrorCodes && - this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === 404) < 0 - ); - }; } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts b/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts index 9ddbe0f5be..6fd9c015d3 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts @@ -20,10 +20,11 @@ export interface HttpErrorConfig { hideCloseIcon?: boolean; }; } - -export type HttpErrorHandler = ( - injector: Injector, - httpError: HttpErrorResponse, -) => Observable; - +export type HttpErrorHandler = (httpError: HttpErrorResponse) => Observable; export type LocaleDirection = 'ltr' | 'rtl'; + +export interface CustomHttpErrorHandlerService { + readonly priority: number; + canHandle(error: unknown): boolean; + execute(); +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts b/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts new file mode 100644 index 0000000000..0d177ec510 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts @@ -0,0 +1,29 @@ +import { Provider } from '@angular/core'; +import { CUSTOM_ERROR_HANDLERS } from '../tokens'; +import { TenantResolveErrorHandlerService } from '../services/tenant-resolve-error-handler.service'; +import { AbpFormatErrorHandlerService } from '../services/abp-format-error-handler.service'; +import { StatusCodeErrorHandlerService } from '../services/status-code-error-handler.service'; +import { UnknownStatusCodeErrorHandlerService } from '../services/unknown-status-code-error-handler.service'; + +export const ERROR_HANDLERS_PROVIDERS: Provider[] = [ + { + provide: CUSTOM_ERROR_HANDLERS, + multi: true, + useClass: TenantResolveErrorHandlerService, + }, + { + provide: CUSTOM_ERROR_HANDLERS, + multi: true, + useClass: AbpFormatErrorHandlerService, + }, + { + provide: CUSTOM_ERROR_HANDLERS, + multi: true, + useClass: StatusCodeErrorHandlerService, + }, + { + provide: CUSTOM_ERROR_HANDLERS, + multi: true, + useClass: UnknownStatusCodeErrorHandlerService, + }, +]; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/providers/index.ts b/npm/ng-packs/packages/theme-shared/src/lib/providers/index.ts index 40efb9df39..cac8aa5d94 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/providers/index.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/providers/index.ts @@ -1,2 +1,3 @@ export * from './ng-bootstrap-config.provider'; export * from './route.provider'; +export * from './error-handlers.provider'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/abp-format-error-handler.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/abp-format-error-handler.service.ts new file mode 100644 index 0000000000..a453c25226 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/abp-format-error-handler.service.ts @@ -0,0 +1,41 @@ +import { inject, Injectable } from '@angular/core'; +import { AuthService } from '@abp/ng.core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { getErrorFromRequestBody } from '../utils/error.utils'; +import { CustomHttpErrorHandlerService } from '../models/common'; +import { ConfirmationService } from '../services/confirmation.service'; +import { CUSTOM_HTTP_ERROR_HANDLER_PRIORITY } from '../constants/default-errors'; + +@Injectable({ providedIn: 'root' }) +export class AbpFormatErrorHandlerService implements CustomHttpErrorHandlerService { + readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.high; + private confirmationService = inject(ConfirmationService); + private authService = inject(AuthService); + private error: HttpErrorResponse | undefined = undefined; + + private navigateToLogin() { + return this.authService.navigateToLogin(); + } + + canHandle(error: unknown): boolean { + if (error instanceof HttpErrorResponse && error.headers.get('_AbpErrorFormat')) { + this.error = error; + return true; + } + return false; + } + + execute() { + const { message, title } = getErrorFromRequestBody(this.error?.error?.error); + this.confirmationService + .error(message, title, { + hideCancelBtn: true, + yesText: 'AbpAccount::Close', + }) + .subscribe(() => { + if (this.error?.status === 401) { + this.navigateToLogin(); + } + }); + } +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts new file mode 100644 index 0000000000..6972a93f34 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts @@ -0,0 +1,95 @@ +import { + ApplicationRef, + ComponentFactoryResolver, + ComponentRef, + EmbeddedViewRef, + inject, + Injectable, + Injector, + RendererFactory2, +} from '@angular/core'; +import { Subject } from 'rxjs'; +import { ResolveEnd } from '@angular/router'; +import { filter } from 'rxjs/operators'; +import { RouterEvents } from '@abp/ng.core'; +import { HTTP_ERROR_CONFIG } from '../tokens/http-error.token'; +import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; +import { ErrorScreenErrorCodes } from '../models/common'; + +@Injectable({ providedIn: 'root' }) +export class CreateErrorComponentService { + protected rendererFactory = inject(RendererFactory2); + protected cfRes = inject(ComponentFactoryResolver); + private routerEvents = inject(RouterEvents); + private injector = inject(Injector); + private httpErrorConfig = inject(HTTP_ERROR_CONFIG); + + componentRef: ComponentRef | null = null; + + private getErrorHostElement() { + return document.body; + } + + public canCreateCustomError(status: ErrorScreenErrorCodes) { + return !!( + this.httpErrorConfig?.errorScreen?.component && + this.httpErrorConfig?.errorScreen?.forWhichErrors && + this.httpErrorConfig?.errorScreen?.forWhichErrors.indexOf(status) > -1 + ); + } + + constructor() { + this.listenToRouterDataResolved(); + } + + protected listenToRouterDataResolved() { + this.routerEvents + .getEvents(ResolveEnd) + .pipe(filter(() => !!this.componentRef)) + .subscribe(() => { + this.componentRef?.destroy(); + this.componentRef = null; + }); + } + + private isCloseIconHidden() { + return !!this.httpErrorConfig.errorScreen?.hideCloseIcon; + } + + execute(instance: Partial) { + const renderer = this.rendererFactory.createRenderer(null, null); + const hostElement = this.getErrorHostElement(); + const host = renderer.selectRootElement(hostElement, true); + + this.componentRef = this.cfRes + .resolveComponentFactory(HttpErrorWrapperComponent) + .create(this.injector); + + for (const key in instance) { + /* istanbul ignore else */ + if (Object.prototype.hasOwnProperty.call(this.componentRef.instance, key)) { + (this.componentRef.instance as any)[key] = (instance as any)[key]; + } + } + + this.componentRef.instance.hideCloseIcon = this.isCloseIconHidden(); + const appRef = this.injector.get(ApplicationRef); + + if (this.canCreateCustomError(instance.status as ErrorScreenErrorCodes)) { + this.componentRef.instance.cfRes = this.cfRes; + this.componentRef.instance.appRef = appRef; + this.componentRef.instance.injector = this.injector; + this.componentRef.instance.customComponent = this.httpErrorConfig.errorScreen?.component; + } + + appRef.attachView(this.componentRef.hostView); + renderer.appendChild(host, (this.componentRef.hostView as EmbeddedViewRef).rootNodes[0]); + + const destroy$ = new Subject(); + this.componentRef.instance.destroy$ = destroy$; + destroy$.subscribe(() => { + this.componentRef?.destroy(); + this.componentRef = null; + }); + } +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/index.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/index.ts index 0f98819137..41f1b84039 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/services/index.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/index.ts @@ -3,3 +3,9 @@ export * from './nav-items.service'; export * from './page-alert.service'; export * from './toaster.service'; export * from './user-menu.service'; +export * from './create-error-component.service'; +export * from './abp-format-error-handler.service'; +export * from './tenant-resolve-error-handler.service'; +export * from './status-code-error-handler.service'; +export * from './unknown-status-code-error-handler.service'; +export * from './router-error-handler.service'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts new file mode 100644 index 0000000000..14a4e79266 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts @@ -0,0 +1,41 @@ +import { inject, Injectable } from '@angular/core'; +import { filter } from 'rxjs/operators'; +import { RouterEvents } from '@abp/ng.core'; +import { NavigationError } from '@angular/router'; +import { HTTP_ERROR_CONFIG } from '../tokens/'; +import { CreateErrorComponentService } from '../services'; +import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors'; + +@Injectable({ providedIn: 'root' }) +export class RouterErrorHandlerService { + private readonly routerEvents = inject(RouterEvents); + private httpErrorConfig = inject(HTTP_ERROR_CONFIG); + private createErrorComponentService = inject(CreateErrorComponentService); + + listen() { + this.routerEvents + .getNavigationEvents('Error') + .pipe(filter(this.filterRouteErrors)) + .subscribe(() => this.show404Page()); + } + + protected filterRouteErrors = (navigationError: NavigationError): boolean => { + return ( + navigationError.error?.message?.indexOf('Cannot match') > -1 && + !!this.httpErrorConfig.skipHandledErrorCodes && + this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === 404) < 0 + ); + }; + + show404Page() { + const instance = { + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, + }, + status: 404, + }; + + this.createErrorComponentService.execute(instance); + } +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts new file mode 100644 index 0000000000..e566ccb792 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts @@ -0,0 +1,94 @@ +import { Confirmation, CustomHttpErrorHandlerService } from '../models'; +import { + CUSTOM_HTTP_ERROR_HANDLER_PRIORITY, + DEFAULT_ERROR_LOCALIZATIONS, + DEFAULT_ERROR_MESSAGES, +} from '../constants/default-errors'; +import { AuthService, LocalizationParam } from '@abp/ng.core'; +import { Observable } from 'rxjs'; +import { inject, Injectable } from '@angular/core'; +import { ConfirmationService } from './confirmation.service'; +import { CreateErrorComponentService } from './create-error-component.service'; + +@Injectable({ providedIn: 'root' }) +export class StatusCodeErrorHandlerService implements CustomHttpErrorHandlerService { + private readonly confirmationService = inject(ConfirmationService); + private readonly createErrorComponentService = inject(CreateErrorComponentService); + private readonly authService = inject(AuthService); + readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.normal; + private status: typeof this.handledStatusCodes[number]; + private readonly handledStatusCodes = [401, 403, 404, 500] as const; + + canHandle({ status }): boolean { + this.status = status; + return this.handledStatusCodes.indexOf(status) > -1; + } + + execute() { + const key = `defaultError${this.status}`; + const title = { + key: DEFAULT_ERROR_LOCALIZATIONS[key]?.title, + defaultValue: DEFAULT_ERROR_MESSAGES[key]?.title, + }; + const message = { + key: DEFAULT_ERROR_LOCALIZATIONS[key]?.details, + defaultValue: DEFAULT_ERROR_MESSAGES[key]?.details, + }; + + const canCreateCustomError = this.createErrorComponentService.canCreateCustomError(this.status); + switch (this.status) { + case 401: + case 404: + if (canCreateCustomError) { + this.showPage(); + break; + } + this.showConfirmation(title, message).subscribe(() => { + if (this.status === 401) { + this.navigateToLogin(); + } + }); + break; + + case 403: + case 500: + this.showPage(); + break; + } + } + + private navigateToLogin() { + this.authService.navigateToLogin(); + } + + protected showConfirmation( + message: LocalizationParam, + title: LocalizationParam, + ): Observable { + return this.confirmationService.error(message, title, { + hideCancelBtn: true, + yesText: 'AbpAccount::Close', + }); + } + + protected showPage() { + const key = `defaultError${this.status}`; + + const instance = { + title: { + key: DEFAULT_ERROR_LOCALIZATIONS[key]?.title, + defaultValue: DEFAULT_ERROR_MESSAGES[key]?.title, + }, + details: { + key: DEFAULT_ERROR_LOCALIZATIONS[key]?.details, + defaultValue: DEFAULT_ERROR_MESSAGES[key]?.details, + }, + status: this.status, + }; + const shouldRemoveDetail = [401, 404].indexOf(this.status) > -1; + if (shouldRemoveDetail) { + delete instance.details; + } + this.createErrorComponentService.execute(instance); + } +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/tenant-resolve-error-handler.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/tenant-resolve-error-handler.service.ts new file mode 100644 index 0000000000..f772d8b518 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/tenant-resolve-error-handler.service.ts @@ -0,0 +1,24 @@ +import { CustomHttpErrorHandlerService } from '../models/common'; +import { inject, Injectable } from '@angular/core'; +import { AuthService, SessionStateService } from '@abp/ng.core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { CUSTOM_HTTP_ERROR_HANDLER_PRIORITY } from '../constants/default-errors'; + +@Injectable({ providedIn: 'root' }) +export class TenantResolveErrorHandlerService implements CustomHttpErrorHandlerService { + protected readonly sessionService = inject(SessionStateService); + readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.high; + private authService = inject(AuthService); + private isTenantResolveError(error: unknown) { + return error instanceof HttpErrorResponse && !!error.headers.get('Abp-Tenant-Resolve-Error'); + } + + canHandle(error: unknown): boolean { + return this.isTenantResolveError(error); + } + + execute() { + this.sessionService.setTenant(null); + this.authService.logout().subscribe(); + } +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/unknown-status-code-error-handler.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/unknown-status-code-error-handler.service.ts new file mode 100644 index 0000000000..129c9ed5b9 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/unknown-status-code-error-handler.service.ts @@ -0,0 +1,35 @@ +import { CustomHttpErrorHandlerService } from '../models'; +import { + CUSTOM_HTTP_ERROR_HANDLER_PRIORITY, + DEFAULT_ERROR_LOCALIZATIONS, + DEFAULT_ERROR_MESSAGES, +} from '../constants/default-errors'; +import { inject, Injectable } from '@angular/core'; +import { CreateErrorComponentService } from './create-error-component.service'; + +@Injectable({ providedIn: 'root' }) +export class UnknownStatusCodeErrorHandlerService implements CustomHttpErrorHandlerService { + readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.normal; + private statusText = 'Unknown Error'; + private message = ''; + private createErrorComponentService = inject(CreateErrorComponentService); + + canHandle(error: { status: number; statusText: string; message: string } | undefined): boolean { + if (error && error.status === 0 && error.statusText === this.statusText) { + this.message = error.message; + return true; + } + return false; + } + + execute() { + this.createErrorComponentService.execute({ + title: { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }, + details: this.message, + isHomeShow: false, + }); + } +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts index e6564ea99f..8688572436 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts @@ -7,7 +7,8 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; import { OAuthService } from 'angular-oauth2-oidc'; import { of, Subject } from 'rxjs'; import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; -import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES, ErrorHandler } from '../handlers'; +import { ErrorHandler } from '../handlers'; +import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors'; import { ConfirmationService } from '../services'; import { httpErrorConfigFactory } from '../tokens/http-error.token'; @@ -16,7 +17,7 @@ const reporter$ = new Subject(); @NgModule({ exports: [HttpErrorWrapperComponent], declarations: [HttpErrorWrapperComponent], - entryComponents: [HttpErrorWrapperComponent], + //entryComponents: [HttpErrorWrapperComponent], imports: [CoreTestingModule], }) class MockModule {} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts index 0fea3f166c..1009d9dd60 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts @@ -29,7 +29,7 @@ import { NgxDatatableListDirective } from './directives/ngx-datatable-list.direc import { DocumentDirHandlerService } from './handlers/document-dir.handler'; import { ErrorHandler } from './handlers/error.handler'; import { RootParams } from './models/common'; -import { NG_BOOTSTRAP_CONFIG_PROVIDERS } from './providers'; +import { ERROR_HANDLERS_PROVIDERS, NG_BOOTSTRAP_CONFIG_PROVIDERS } from './providers'; import { THEME_SHARED_ROUTE_PROVIDERS } from './providers/route.provider'; import { THEME_SHARED_APPEND_CONTENT } from './tokens/append-content.token'; import { HTTP_ERROR_CONFIG, httpErrorConfigFactory } from './tokens/http-error.token'; @@ -58,7 +58,7 @@ const declarationsWithExports = [ ModalCloseDirective, AbpVisibleDirective, FormInputComponent, - FormCheckboxComponent + FormCheckboxComponent, ]; @NgModule({ @@ -69,7 +69,6 @@ const declarationsWithExports = [ NgbPaginationModule, EllipsisModule, CardModule, - ], declarations: [...declarationsWithExports, HttpErrorWrapperComponent], exports: [ @@ -77,11 +76,11 @@ const declarationsWithExports = [ EllipsisModule, NgxValidateCoreModule, CardModule, - ...declarationsWithExports + ...declarationsWithExports, ], providers: [DatePipe], }) -export class BaseThemeSharedModule { } +export class BaseThemeSharedModule {} @NgModule({ imports: [BaseThemeSharedModule], @@ -144,6 +143,7 @@ export class ThemeSharedModule { ...(confirmationIcons || {}), }, }, + ERROR_HANDLERS_PROVIDERS, ], }; } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts b/npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts index 39cb43a350..a2784f4332 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts @@ -1,5 +1,5 @@ import { InjectionToken } from '@angular/core'; -import { HttpErrorConfig, HttpErrorHandler } from '../models/common'; +import { CustomHttpErrorHandlerService, HttpErrorConfig, HttpErrorHandler } from '../models/common'; export function httpErrorConfigFactory(config = {} as HttpErrorConfig) { if (config.errorScreen && config.errorScreen.component && !config.errorScreen.forWhichErrors) { @@ -16,3 +16,7 @@ export function httpErrorConfigFactory(config = {} as HttpErrorConfig) { export const HTTP_ERROR_CONFIG = new InjectionToken('HTTP_ERROR_CONFIG'); export const HTTP_ERROR_HANDLER = new InjectionToken('HTTP_ERROR_HANDLER'); + +export const CUSTOM_ERROR_HANDLERS = new InjectionToken( + 'CUSTOM_ERROR_HANDLERS', +); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/utils/error.utils.ts b/npm/ng-packs/packages/theme-shared/src/lib/utils/error.utils.ts new file mode 100644 index 0000000000..e417b53eb8 --- /dev/null +++ b/npm/ng-packs/packages/theme-shared/src/lib/utils/error.utils.ts @@ -0,0 +1,26 @@ +import { LocalizationParam } from '@abp/ng.core'; +import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors'; + +export function getErrorFromRequestBody(body: { details?: string; message?: string } | undefined) { + let message: LocalizationParam; + let title: LocalizationParam; + + if (body.details) { + message = body.details; + title = body.message; + } else if (body.message) { + title = { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }; + message = body.message; + } else { + message = { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }; + title = ''; + } + + return { message, title }; +} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/utils/index.ts b/npm/ng-packs/packages/theme-shared/src/lib/utils/index.ts index 8f159e0a49..9bf1e72a8f 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/utils/index.ts @@ -1,2 +1,3 @@ export * from './date-parser-formatter'; export * from './validation-utils'; +export * from './error.utils'; diff --git a/npm/ng-packs/packages/theme-shared/src/public-api.ts b/npm/ng-packs/packages/theme-shared/src/public-api.ts index 56cd9936a7..22661e3855 100644 --- a/npm/ng-packs/packages/theme-shared/src/public-api.ts +++ b/npm/ng-packs/packages/theme-shared/src/public-api.ts @@ -5,6 +5,7 @@ export * from './lib/animations'; export * from './lib/components'; export * from './lib/constants/validation'; +export * from './lib/constants/default-errors'; export * from './lib/directives'; export * from './lib/enums'; export * from './lib/handlers';