diff --git a/apps/client-asset-sg/src/app/app.component.html b/apps/client-asset-sg/src/app/app.component.html index 31020944..4be1cdff 100644 --- a/apps/client-asset-sg/src/app/app.component.html +++ b/apps/client-asset-sg/src/app/app.component.html @@ -1,13 +1,26 @@ - - - - -
- -
-
- -
+ + +
+ {{error}} + +
+
+ + + + + + +
+ +
+
+ +
+
+
diff --git a/apps/client-asset-sg/src/app/app.component.scss b/apps/client-asset-sg/src/app/app.component.scss index 0da765b2..8b4f2f75 100644 --- a/apps/client-asset-sg/src/app/app.component.scss +++ b/apps/client-asset-sg/src/app/app.component.scss @@ -1,4 +1,6 @@ @use 'styles/variables'; +@use '@angular/material' as mat; +@use '../theme' as *; $app-bar-height: 88px; $menu-bar-width: 80px; @@ -44,6 +46,20 @@ asset-sg-menu-bar { } } +.error { + display: flex; + flex-direction: column; + gap: 20px; + grid-column: 1 / -1; + padding: 40px; + align-items: center; + font-size: 24px; + font-weight: bolder; + + color: mat.get-contrast-color-from-palette($asset-sg-warn, 500); + background-color: mat.get-color-from-palette($asset-sg-warn, 600); +} + .alerts { position: fixed; width: min(30rem, 95vw); diff --git a/apps/client-asset-sg/src/app/app.component.ts b/apps/client-asset-sg/src/app/app.component.ts index f6cd7e3a..5c26f564 100644 --- a/apps/client-asset-sg/src/app/app.component.ts +++ b/apps/client-asset-sg/src/app/app.component.ts @@ -8,6 +8,7 @@ import { debounceTime, fromEvent, map, startWith } from 'rxjs'; import { assert } from 'tsafe'; import { AuthService } from '@asset-sg/auth'; +import { ErrorService } from '@asset-sg/auth'; import { AppPortalService, appSharedStateActions, setCssCustomProperties } from '@asset-sg/client-shared'; import { AppState } from './state/app-state'; @@ -25,7 +26,10 @@ export class AppComponent { private _httpClient = inject(HttpClient); public appPortalService = inject(AppPortalService); private store = inject(Store); - public readonly router: Router = inject(Router); + + readonly router: Router = inject(Router); + readonly errorService = inject(ErrorService); + readonly authService = inject(AuthService); constructor(private readonly _authService: AuthService) { this._httpClient diff --git a/apps/client-asset-sg/src/app/app.module.ts b/apps/client-asset-sg/src/app/app.module.ts index adce9024..928aa613 100644 --- a/apps/client-asset-sg/src/app/app.module.ts +++ b/apps/client-asset-sg/src/app/app.module.ts @@ -18,6 +18,7 @@ import { LetModule } from '@rx-angular/template/let'; import { PushModule } from '@rx-angular/template/push'; import { AuthInterceptor, AuthModule } from '@asset-sg/auth'; +import { ErrorService } from '@asset-sg/auth'; import { AnchorComponent, ButtonComponent, @@ -114,6 +115,7 @@ registerLocaleData(locale_deCH, 'de-CH'); ], providers: [ provideSvgIcons(icons), + ErrorService, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'fill', floatLabel: 'auto' } }, { provide: CURRENT_LANG, useFactory: currentLangFactory }, diff --git a/libs/auth/src/index.ts b/libs/auth/src/index.ts index 2f757bd9..048d502d 100644 --- a/libs/auth/src/index.ts +++ b/libs/auth/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/auth.module'; export * from './lib/services/auth.interceptor'; export * from './lib/services/auth.service'; +export * from './lib/services/error.service'; diff --git a/libs/auth/src/lib/auth.module.ts b/libs/auth/src/lib/auth.module.ts index 6e6d1ae3..077f1c42 100644 --- a/libs/auth/src/lib/auth.module.ts +++ b/libs/auth/src/lib/auth.module.ts @@ -15,6 +15,8 @@ import { OAuthModule } from 'angular-oauth2-oidc'; import { AnchorComponent, ButtonComponent, icons } from '@asset-sg/client-shared'; +import { ErrorService } from './services/error.service'; + @NgModule({ imports: [ @@ -44,6 +46,9 @@ import { AnchorComponent, ButtonComponent, icons } from '@asset-sg/client-shared }), ], exports: [OAuthModule], - providers: [provideSvgIcons(icons)], + providers: [ + provideSvgIcons(icons), + ErrorService, + ], }) export class AuthModule {} diff --git a/libs/auth/src/lib/services/auth.interceptor.ts b/libs/auth/src/lib/services/auth.interceptor.ts index ca7d2d32..2a412142 100644 --- a/libs/auth/src/lib/services/auth.interceptor.ts +++ b/libs/auth/src/lib/services/auth.interceptor.ts @@ -7,11 +7,14 @@ import { EMPTY, Observable, catchError } from 'rxjs'; import { AlertType, showAlert } from '@asset-sg/client-shared'; +import { ErrorService } from './error.service'; + @Injectable() export class AuthInterceptor implements HttpInterceptor { private _oauthService = inject(OAuthService); private _router: Router = inject(Router); private readonly store = inject(Store); + private readonly errorService = inject(ErrorService); intercept(req: HttpRequest, next: HttpHandler): Observable> { const token = sessionStorage.getItem('access_token'); @@ -34,7 +37,9 @@ export class AuthInterceptor implements HttpInterceptor { } else { return next.handle(req).pipe( catchError((error: HttpErrorResponse) => { - if (error.status === 401 || error.status === 403) { + if (error.status === 403) { + this.errorService.updateMessage(error.error.error); + } else if (error.status === 401) { this.store.dispatch(showAlert({ alert: { id: `auth-error-${error.status}`, @@ -44,7 +49,6 @@ export class AuthInterceptor implements HttpInterceptor { }, })); } else { - console.log(error); this.store.dispatch(showAlert({ alert: { id: `request-error-${error.status}-${error.url}`, diff --git a/libs/auth/src/lib/services/error.service.ts b/libs/auth/src/lib/services/error.service.ts new file mode 100644 index 00000000..6d40b49b --- /dev/null +++ b/libs/auth/src/lib/services/error.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +@Injectable() +export class ErrorService { + private message$ = new BehaviorSubject(null); + + get onMessage(): Observable { + return this.message$.asObservable(); + } + + updateMessage(message: string) { + this.message$.next(message); + } + + clear(): void { + this.message$.next(null); + } +} diff --git a/libs/client-shared/src/lib/components/button/button.component.ts b/libs/client-shared/src/lib/components/button/button.component.ts index 84c70a76..8469f869 100644 --- a/libs/client-shared/src/lib/components/button/button.component.ts +++ b/libs/client-shared/src/lib/components/button/button.component.ts @@ -1,72 +1,72 @@ -import { FocusMonitor } from '@angular/cdk/a11y'; -import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; -import { - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - HostBinding, - Input, - OnDestroy, - inject, -} from '@angular/core'; - -@Component({ - standalone: true, - selector: - // eslint-disable-next-line @angular-eslint/component-selector - 'button[asset-sg-reset], button[asset-sg-icon-button], button[asset-sg-primary], button[asset-sg-secondary], button[asset-sg-icon-button-tw]', - template: '', - styleUrls: ['./button.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ButtonComponent implements AfterViewInit, OnDestroy { - protected _host = inject>(ElementRef); - private _focusMonitor = inject(FocusMonitor); - private _cd = inject(ChangeDetectorRef); - - constructor() { - const observer = new MutationObserver(mutations => { - mutations.forEach(mutation => { - if (mutation.type === 'attributes') { - if (this._host.nativeElement.attributes.getNamedItem('asset-sg-icon-button-tw')) { - this._classes = 'bg-white'; - this._cd.detectChanges(); - } - } - }); - }); - - observer.observe(this._host.nativeElement, { - attributes: true, //configure it to listen to attribute changes - }); - // console.log(this._host.nativeElement.attributes); - } - - @HostBinding('class') - private _classes: string | undefined; - - private _disabled: true | undefined; - @HostBinding('[attr.disabled]') - @HostBinding('[attr.aria-disabled]') - @Input() - get disabled(): true | false | undefined { - return this._disabled; - } - set disabled(value: BooleanInput) { - this._disabled = coerceBooleanProperty(value) || undefined; - } - - @HostBinding('attr.type') - @Input() - public type = this._host.nativeElement.tagName === 'BUTTON' ? 'button' : undefined; - - ngAfterViewInit() { - this._focusMonitor.monitor(this._host, false); - } - - ngOnDestroy() { - this._focusMonitor.stopMonitoring(this._host); - } -} +import { FocusMonitor } from '@angular/cdk/a11y'; +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + Input, + OnDestroy, + inject, +} from '@angular/core'; + +@Component({ + standalone: true, + selector: + // eslint-disable-next-line @angular-eslint/component-selector + 'button[asset-sg-reset], button[asset-sg-icon-button], button[asset-sg-primary], button[asset-sg-warn], button[asset-sg-secondary], button[asset-sg-icon-button-tw]', + template: '', + styleUrls: ['./button.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ButtonComponent implements AfterViewInit, OnDestroy { + protected _host = inject>(ElementRef); + private _focusMonitor = inject(FocusMonitor); + private _cd = inject(ChangeDetectorRef); + + constructor() { + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + if (mutation.type === 'attributes') { + if (this._host.nativeElement.attributes.getNamedItem('asset-sg-icon-button-tw')) { + this._classes = 'bg-white'; + this._cd.detectChanges(); + } + } + }); + }); + + observer.observe(this._host.nativeElement, { + attributes: true, //configure it to listen to attribute changes + }); + // console.log(this._host.nativeElement.attributes); + } + + @HostBinding('class') + private _classes: string | undefined; + + private _disabled: true | undefined; + @HostBinding('[attr.disabled]') + @HostBinding('[attr.aria-disabled]') + @Input() + get disabled(): true | false | undefined { + return this._disabled; + } + set disabled(value: BooleanInput) { + this._disabled = coerceBooleanProperty(value) || undefined; + } + + @HostBinding('attr.type') + @Input() + public type = this._host.nativeElement.tagName === 'BUTTON' ? 'button' : undefined; + + ngAfterViewInit() { + this._focusMonitor.monitor(this._host, false); + } + + ngOnDestroy() { + this._focusMonitor.stopMonitoring(this._host); + } +}