argos/dmp-frontend/src/app/core/services/auth/auth.service.ts

344 lines
8.9 KiB
TypeScript

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<AuthenticationState>;
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<AuthenticationState>();
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<AuthenticationState> {
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<string>, httpParams?: Object): Observable<boolean> {
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<boolean> {
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<boolean> {
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;
}
}