import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { AppAccount } from '@app/core/model/auth/principal'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; import { BaseService } from '@common/base/base.service'; import { TranslateService } from '@ngx-translate/core'; import { Observable, Subject, forkJoin, from, of } from 'rxjs'; import { exhaustMap, map, takeUntil } from 'rxjs/operators'; import { ConfigurationService } from '../configuration/configuration.service'; import { Guid } from '@common/types/guid'; import { KeycloakEventType, KeycloakService } from 'keycloak-angular'; import { NgZone } from '@angular/core'; import { PrincipalService } from '../http/principal.service'; import { AppRole } from '@app/core/common/enum/app-role'; import { AppPermission } from '@app/core/common/enum/permission.enum'; export interface ResolutionContext { roles: AppRole[]; permissions: AppPermission[]; } export interface AuthenticationState { loginStatus: LoginStatus; } export enum LoginStatus { LoggedIn = 0, LoggingOut = 1, LoggedOut = 2 } @Injectable() export class AuthService extends BaseService { public permissionEnum = AppPermission; public authenticationStateSubject: Subject; private accessToken: string; private appAccount: AppAccount; private _authState: boolean; constructor( private installationConfiguration: ConfigurationService, private language: TranslateService, private router: Router, private zone: NgZone, private keycloakService: KeycloakService, private uiNotificationService: UiNotificationService, private principalService: PrincipalService ) { super(); this.authenticationStateSubject = new Subject(); window.addEventListener('storage', (event: StorageEvent) => { // Logout if we receive event that logout action occurred in a different tab. if ( event.key && event.key === 'authState' && event.newValue === 'false' && this._authState ) { this.clear(); this.router.navigate(['/unauthorized'], { queryParams: { returnUrl: this.router.url }, }); } }); } public getAuthenticationStateObservable(): Observable { return this.authenticationStateSubject.asObservable(); } public beginLogOutProcess(): void { this.authenticationStateSubject.next({ loginStatus: LoginStatus.LoggingOut, }); } public clear(): void { this.authState(false); this.accessToken = undefined; this.appAccount = undefined; } private authState(authState?: boolean): boolean { if (authState !== undefined) { this._authState = authState; localStorage.setItem('authState', authState ? 'true' : 'false'); if (authState) { this.authenticationStateSubject.next({ loginStatus: LoginStatus.LoggedIn, }); } else { this.authenticationStateSubject.next({ loginStatus: LoginStatus.LoggedOut, }); } } if (this._authState === undefined) { this._authState = localStorage.getItem('authState') === 'true' ? true : false; } return this._authState; } public currentAccountIsAuthenticated(): boolean { return this.appAccount && this.appAccount.isAuthenticated; } //Should this be name @isAuthenticated@ instead? public hasAccessToken(): boolean { return Boolean(this.currentAuthenticationToken()); } public currentAuthenticationToken(accessToken?: string): string { if (accessToken) { this.accessToken = accessToken; } return this.accessToken; } public userId(): Guid { if ( this.appAccount && this.appAccount.principal && this.appAccount.principal.userId ) { return this.appAccount.principal.userId; } return null; } public isLoggedIn(): boolean { return this.authState(); } public prepareAuthRequest(observable: Observable, httpParams?: Object): Observable { return observable.pipe( map((x) => this.currentAuthenticationToken(x)), exhaustMap(() => forkJoin([ this.accessToken ? this.principalService.me(httpParams) : of(null) ])), map((item) => { this.currentAccount(item[0]); return true; }) ); } public refresh(): Observable { return this.principalService.me().pipe( map((item) => { this.currentAccount(item); return true; }) ); } private currentAccount( appAccount: AppAccount ): void { this.appAccount = appAccount; this.authState(true); } public getPrincipalName(): string { if (this.appAccount && this.appAccount.principal) { return this.appAccount.principal.name; } return null; } public getRoles(): AppRole[] { if (this.appAccount && this.appAccount.roles) { return this.appAccount.roles; } return null; } public getUserProfileEmail(): string { if (this.appAccount && this.appAccount.profile) { return this.appAccount.profile.email; } return null; } public getUserProfileLanguage(): string { if (this.appAccount && this.appAccount.profile) { return this.appAccount.profile.language; } return null; } public hasAnyRole(roles: AppRole[]): boolean { if (!roles) { return false; } return roles.filter((r) => this.hasRole(r)).length > 0; } public hasRole(role: AppRole): boolean { if (role === undefined) { return false; } if ( !this.appAccount || !this.appAccount.roles || this.appAccount.roles.length === 0 ) { return false; } return this.appAccount.roles .includes(role); } public getUserProfileCulture(): string { if (this.appAccount && this.appAccount.profile) { return this.appAccount.profile.culture; } return null; } public getUserProfileAvatarUrl(): string { if (this.appAccount && this.appAccount.profile) { return this.appAccount.profile.avatarUrl; } return null; } public getUserProfileTimezone(): string { if (this.appAccount && this.appAccount.profile) { return this.appAccount.profile.timezone; } return null; } public authenticate(returnUrl: string) { this.keycloakService.isLoggedIn().then((isLoggedIn) => { if (!isLoggedIn) { this.keycloakService.login({ scope: this.installationConfiguration.keycloak.scope, }) .then(() => { this.keycloakService.keycloakEvents$.subscribe({ next: (e) => { if ( e.type === KeycloakEventType.OnTokenExpired ) { this.refreshToken({}); } }, }); this.onAuthenticateSuccess(returnUrl); }) .catch((error) => this.onAuthenticateError(error)); } else { this.zone.run(() => this.router.navigate([returnUrl])); } }); } public refreshToken(httpParams?: Object): Promise { return this.keycloakService.updateToken(60).then((isRefreshed) => { if (!isRefreshed) { return false; } return this.prepareAuthRequest( from(this.keycloakService.getToken()), httpParams ) .pipe(takeUntil(this._destroyed)) .pipe( map( () => { return true; }, (error) => { this.onAuthenticateError(error); return false; } ) ) .toPromise(); }); } onAuthenticateError(errorResponse: HttpErrorResponse) { this.zone.run(() => { // const error: HttpError = // this.httpErrorHandlingService.getError(errorResponse); this.uiNotificationService.snackBarNotification( // error.getMessagesString(), errorResponse.message, SnackBarNotificationLevel.Warning ); }); } onAuthenticateSuccess(returnUrl: string): void { this.authState(true); this.uiNotificationService.snackBarNotification( this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-LOGIN'), SnackBarNotificationLevel.Success ); this.zone.run(() => this.router.navigate([returnUrl])); } public hasPermission(permission: AppPermission): boolean { // if (!this.installationConfiguration.appServiceEnabled) { return true; } //TODO: maybe reconsider return this.evaluatePermission(this.appAccount?.permissions || [], permission); } private evaluatePermission(availablePermissions: string[], permissionToCheck: string): boolean { if (!permissionToCheck) { return false; } if (this.hasRole(AppRole.Admin)) { return true; } return availablePermissions.map(x => x.toLowerCase()).includes(permissionToCheck.toLowerCase()); } public hasAnyPermission(permissions: AppPermission[]): boolean { if (!permissions) { return false; } return permissions.filter((p) => this.hasPermission(p)).length > 0; } public authorize(context: ResolutionContext): boolean { if (!context || this.hasRole(AppRole.Admin)) { return true; } let roleAuthorized = false; if (context.roles && context.roles.length > 0) { roleAuthorized = this.hasAnyRole(context.roles); } let permissionAuthorized = false; if (context.permissions && context.permissions.length > 0) { permissionAuthorized = this.hasAnyPermission(context.permissions); } if (roleAuthorized || permissionAuthorized) { return true; } return false; } }