Merge branch 'master' into VM/6510

This commit is contained in:
Valentin Mihai 2023-04-08 22:49:35 +03:00
commit 4aef8c608d
19 changed files with 316 additions and 302 deletions

View File

@ -6,7 +6,7 @@ import { MAX_RETRIES_ON_SERVER_ERROR, SERVER_ERROR_SKIP_PATHS } from './tokens';
import { ErrorService } from './error.service';
import { KeycloakService } from 'keycloak-angular';
import { IqserConfigService } from '../services';
import { KeycloakStatusService } from '../users';
import { KeycloakStatusService } from '../tenants';
function updateSeconds(seconds: number) {
if (seconds === 0 || seconds === 1) {

View File

@ -0,0 +1,42 @@
import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { KeycloakService } from 'keycloak-angular';
import { KeycloakStatusService } from '../services/keycloak-status.service';
import { keycloakInitializer } from '../keycloak-initializer';
import { TenantsService } from '../services';
export const ifLoggedIn: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
const logger = inject(NGXLogger);
logger.info('[ROUTES] Check if can activate main');
const tenantsService = inject(TenantsService);
const keycloakService = inject(KeycloakService);
const keycloakStatusService = inject(KeycloakStatusService);
const keycloakInstance = keycloakService.getKeycloakInstance();
const tenant = route.paramMap.get('tenant');
if (!keycloakInstance) {
if (!tenant) {
logger.error('[ROUTES] No tenant found, something is wrong...');
return inject(Router).navigate(['/']);
}
logger.info('[KEYCLOAK] Keycloak init...');
await keycloakInitializer(tenant);
logger.info('[KEYCLOAK] Keycloak init done!');
await tenantsService.selectTenant(tenant);
}
const isLoggedIn = await keycloakService.isLoggedIn();
if (isLoggedIn) {
logger.info('[ROUTES] Is logged in, continuing');
return true;
}
logger.warn('[ROUTES] Redirect to login');
await keycloakStatusService.createLoginUrlAndExecute();
return false;
};

View File

@ -0,0 +1,27 @@
import { ActivatedRouteSnapshot, CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { KeycloakService } from 'keycloak-angular';
export const ifNotLoggedIn: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
const logger = inject(NGXLogger);
const router = inject(Router);
const keycloakService = inject(KeycloakService);
const isLoggedIn = await keycloakService.isLoggedIn();
if (!isLoggedIn) {
logger.info('[ROUTES] Not logged in, continuing to selected route');
return true;
}
const tenant = route.paramMap.get('tenant') || keycloakService.getKeycloakInstance().realm;
if (!tenant) {
logger.error('[ROUTES] Tenant not found in route or keycloak realm');
return false;
}
logger.warn('[ROUTES] Is logged in for ' + tenant + ', redirecting to /' + tenant);
await router.navigate([tenant]);
return false;
};

View File

@ -1,4 +1,8 @@
export * from './keycloak-initializer';
export * from './guards/if-logged-in.guard';
export * from './guards/if-not-logged-in.guard';
export * from './services';
export * from './tenant.pipe';
export * from './tenants.module';
export * from './tenant-resolve/tenant-resolve.component';
export * from './services/keycloak-status.service';

View File

@ -0,0 +1,56 @@
import { BASE_HREF, IqserAppConfig } from '../utils';
import { KeycloakOptions, KeycloakService } from 'keycloak-angular';
import { KeycloakStatusService } from './services/keycloak-status.service';
import { inject } from '@angular/core';
import { getConfig } from '../services';
import { NGXLogger } from 'ngx-logger';
import { Router } from '@angular/router';
function getKeycloakOptions(baseUrl: string, config: IqserAppConfig, tenant: string): KeycloakOptions {
return {
config: {
url: config.OAUTH_URL,
realm: tenant,
clientId: config.OAUTH_CLIENT_ID,
},
initOptions: {
checkLoginIframe: false,
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html',
flow: 'standard',
enableLogging: true,
},
enableBearerInterceptor: true,
};
}
function configureAutomaticRedirectToLoginScreen(keyCloakService: KeycloakService, keycloakStatusService: KeycloakStatusService) {
const keycloakInstance = keyCloakService.getKeycloakInstance();
keycloakInstance.onAuthRefreshError = () => {
console.log('onAuthRefreshError');
keycloakStatusService.createLoginUrlAndExecute();
};
keycloakInstance.onAuthError = err => {
console.log('onAuthError', err);
};
}
export async function keycloakInitializer(tenant: string) {
const logger = inject(NGXLogger);
const router = inject(Router);
const keycloakService = inject(KeycloakService);
const keycloakStatusService = inject(KeycloakStatusService);
const baseHref = inject(BASE_HREF);
const config = getConfig();
const keycloakOptions = getKeycloakOptions(baseHref, config, tenant);
try {
await keycloakService.init(keycloakOptions);
} catch (error) {
logger.error('[KEYCLOAK] Unable to initialize Keycloak', error);
await router.navigate(['/']);
return;
}
configureAutomaticRedirectToLoginScreen(keycloakService, keycloakStatusService);
}

View File

@ -1,4 +1,4 @@
export * from './tenant-context-holder';
export * from './tenant-context';
export * from './tenants.service';
export * from './tenant-id-interceptor';
export * from './tenant-id-response-interceptor';

View File

@ -0,0 +1,45 @@
import { inject, Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { getConfig } from '../../services';
import { TenantContextHolder } from '../index';
import { BASE_HREF } from '../../utils';
import { NGXLogger } from 'ngx-logger';
@Injectable({ providedIn: 'root' })
export class KeycloakStatusService {
readonly #keycloakService = inject(KeycloakService);
readonly #config = getConfig();
readonly #tenantContextHolder = inject(TenantContextHolder);
readonly #baseHref = inject(BASE_HREF);
readonly #logger = inject(NGXLogger);
createLoginUrlAndExecute() {
const keycloakInstance = this.#keycloakService?.getKeycloakInstance();
if (keycloakInstance) {
const url = keycloakInstance.createLoginUrl({
redirectUri: this.createLoginUrl(),
idpHint: this.#config.OAUTH_IDP_HINT,
});
this.#logger.info('[KEYCLOAK] Redirect to login url: ', url);
window.location.href = url;
} else {
this.#logger.error('[KEYCLOAK] Instance not found, skip redirect to login');
}
}
createLoginUrl() {
const currentTenant = this.#tenantContextHolder.currentTenant;
if (currentTenant && window.location.href.indexOf('/' + currentTenant) > 0) {
return window.location.href;
}
const url = window.location.origin + this.#baseHref;
if (currentTenant) {
return url + '/' + currentTenant;
}
return url;
}
}

View File

@ -1,15 +1,11 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { inject, Injectable } from '@angular/core';
import { TenantsService } from './tenants.service';
@Injectable({ providedIn: 'root' })
export class TenantContextHolder {
readonly #activeTenantId$ = new BehaviorSubject<string>('');
setCurrentTenantId(tenantId: string) {
this.#activeTenantId$.next(tenantId);
}
readonly #tenantsService = inject(TenantsService);
get currentTenant() {
return this.#activeTenantId$.value;
return this.#tenantsService.currentTenant;
}
}

View File

@ -1,80 +0,0 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';
import { NGXLogger } from 'ngx-logger';
import { tap } from 'rxjs/operators';
import { IqserConfigService } from '../../services';
import { TenantContextHolder } from './tenant-context-holder';
import { BASE_HREF } from '../../utils';
export interface IBaseTenant {
tenantId: string;
displayName: string;
guid: string;
}
@Injectable({ providedIn: 'root' })
export class TenantContext {
hasMultipleTenants = false;
readonly tenantData$ = new BehaviorSubject<IBaseTenant[] | undefined>(undefined);
readonly tenantsReady$ = new BehaviorSubject<boolean>(false);
protected readonly _http = inject(HttpClient);
protected readonly _configService = inject(IqserConfigService);
protected readonly _tenantContextHolder = inject(TenantContextHolder);
protected _storageReference: Storage;
constructor(private readonly _logger: NGXLogger) {
this._storageReference = {
length: localStorage.length,
clear: localStorage.clear.bind(localStorage),
getItem: localStorage.getItem.bind(localStorage),
setItem: localStorage.setItem.bind(localStorage),
removeItem: localStorage.removeItem.bind(localStorage),
key: localStorage.key.bind(localStorage),
};
}
loadTenants() {
const tenant = this.getTenantFromRoute();
return this._http.get<IBaseTenant[]>('/tenants/simple').pipe(
tap(tenants => {
console.log('tenants: ', tenants);
this.hasMultipleTenants = tenants.length > 1;
this.tenantData$.next(tenants);
if (this.hasMultipleTenants) {
this.tenantSelected(tenant);
} else {
this.tenantSelected(tenant.length ? tenant : tenants[0].tenantId);
}
this.tenantsReady$.next(true);
}),
);
}
getTenantFromRoute() {
const base = inject(BASE_HREF);
const path = window.location.pathname;
const nextSlash = path.indexOf('/', base.length + 1);
return path.substring(base.length + 1, nextSlash >= 0 ? nextSlash : path.length);
}
tenantSelected(tenantId: string) {
if (this.tenantData$.value?.map(t => t.tenantId).includes(tenantId)) {
this._mutateStorage(tenantId);
this._tenantContextHolder.setCurrentTenantId(tenantId);
}
}
private _mutateStorage(tenant: string) {
localStorage.getItem = (key: string) => {
return this._storageReference.getItem(tenant + ':' + key);
};
localStorage.setItem = (key: string, value: string) => {
this._storageReference.setItem(tenant + ':' + key, value);
};
localStorage.removeItem = (key: string) => {
this._storageReference.removeItem(tenant + ':' + key);
};
}
}

View File

@ -12,7 +12,7 @@ export class TenantIdResponseInterceptor implements HttpInterceptor {
tap(event => {
if (event instanceof HttpResponse) {
const xTenantId = event.headers.get('X-TENANT-ID');
if (VALID_TENANT_IDS.includes(xTenantId)) {
if (xTenantId && VALID_TENANT_IDS.includes(xTenantId)) {
//TODO add logic to deny the response when backend will send the header
}
}

View File

@ -0,0 +1,103 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { NGXLogger } from 'ngx-logger';
import { BASE_HREF } from '../../utils';
import { Router } from '@angular/router';
export interface IBaseTenant {
readonly tenantId: string;
readonly displayName: string;
readonly guid: string;
}
@Injectable({ providedIn: 'root' })
export class TenantsService {
hasMultipleTenants = false;
readonly tenantData$ = new BehaviorSubject<IBaseTenant[] | undefined>(undefined);
readonly #http = inject(HttpClient);
readonly #router = inject(Router);
readonly #logger = inject(NGXLogger);
readonly #baseHref = inject(BASE_HREF);
readonly #storageReference: Storage = {
length: localStorage.length,
clear: localStorage.clear.bind(localStorage),
getItem: localStorage.getItem.bind(localStorage),
setItem: localStorage.setItem.bind(localStorage),
removeItem: localStorage.removeItem.bind(localStorage),
key: localStorage.key.bind(localStorage),
};
readonly #activeTenantId$ = new BehaviorSubject<string>('');
get currentTenant() {
return this.#activeTenantId$.value;
}
async loadTenants() {
this.#logger.info('[TENANTS] Loading tenants...');
const tenants = await firstValueFrom(this.#http.get<IBaseTenant[]>('/tenants/simple'));
this.hasMultipleTenants = tenants.length > 1;
this.tenantData$.next(tenants);
const tenant = this.getTenantFromRoute();
if (tenant) {
this.#logger.info('[TENANTS] Tenant from route: ', tenant);
if (await this.selectTenant(tenant)) {
await this.#router.navigate([tenant]);
}
return;
}
this.#logger.info('[TENANTS] No tenant in route');
if (!this.hasMultipleTenants) {
this.#logger.info('[TENANTS] Only one tenant loaded, auto-select it and redirect to login page');
const tenant = tenants[0].tenantId;
if (await this.selectTenant(tenant)) {
await this.#router.navigate([tenant]);
}
}
}
getTenantFromRoute() {
const path = window.location.pathname;
const nextSlash = path.indexOf('/', this.#baseHref.length + 1);
return path.substring(this.#baseHref.length + 1, nextSlash >= 0 ? nextSlash : path.length);
}
async selectTenant(tenantId: string) {
const tenants = this.tenantData$.value;
if (!tenants) {
throw new Error('Tenants not loaded!');
}
const unknownTenant = !tenants.map(t => t.tenantId).includes(tenantId);
if (unknownTenant) {
this.#logger.info('[TENANTS] Unknown tenant, redirecting to select tenant page');
await this.#router.navigate(['/']);
return false;
}
this.#mutateStorage(tenantId);
this.setCurrentTenantId(tenantId);
return true;
}
setCurrentTenantId(tenantId: string) {
this.#logger.info('[TENANTS] Set current tenant id: ', tenantId);
this.#activeTenantId$.next(tenantId);
}
#mutateStorage(tenant: string) {
localStorage.getItem = (key: string) => {
return this.#storageReference.getItem(tenant + ':' + key);
};
localStorage.setItem = (key: string, value: string) => {
this.#storageReference.setItem(tenant + ':' + key, value);
};
localStorage.removeItem = (key: string) => {
this.#storageReference.removeItem(tenant + ':' + key);
};
}
}

View File

@ -1,4 +1,4 @@
<div class="tenant-section" *ngIf="!(loadingService.isLoading$ | async)">
<div class="tenant-section">
<form (submit)="updateTenantSelection()" [formGroup]="form">
<div class="heading-l" translate="tenant-resolve.header"></div>
@ -9,7 +9,7 @@
class="full-width"
formControlName="tenantId"
>
<mat-option *ngFor="let option of _tenantContext.tenantData$ | async" [value]="option.tenantId">
<mat-option *ngFor="let option of _tenantsService.tenantData$ | async" [value]="option.tenantId">
{{ option.displayName || option.tenantId }}
</mat-option>
</mat-select>

View File

@ -1,14 +1,11 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Router } from '@angular/router';
import { FormBuilder, Validators } from '@angular/forms';
import { TenantContext } from '../services';
import { TenantsService } from '../services';
import { IconButtonTypes } from '../../buttons';
import { KeycloakService } from 'keycloak-angular';
import { BASE_HREF } from '../../utils';
import { LoadingService } from '../../loading';
@Component({
selector: 'iqser-tenant-resolve',
templateUrl: './tenant-resolve.component.html',
styleUrls: ['./tenant-resolve.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
@ -16,40 +13,25 @@ import { LoadingService } from '../../loading';
export class TenantResolveComponent {
readonly loadingService = inject(LoadingService);
readonly iconButtonTypes = IconButtonTypes;
form;
protected _tenantContext = inject(TenantContext);
private readonly _route = inject(ActivatedRoute);
private readonly _router = inject(Router);
private readonly _formBuilder = inject(FormBuilder);
private readonly _baseHref = inject(BASE_HREF);
private readonly _keycloakService = inject(KeycloakService);
readonly form;
protected readonly _tenantsService = inject(TenantsService);
readonly #router = inject(Router);
readonly #formBuilder = inject(FormBuilder);
constructor() {
this.loadingService.start();
this.form = this._formBuilder.group({
this.form = this.#formBuilder.group({
// eslint-disable-next-line @typescript-eslint/unbound-method
tenantId: [undefined, Validators.required],
tenantId: ['', Validators.required],
});
if (this._route.snapshot.paramMap.get('tenant')) {
this._keycloakService.isLoggedIn().then(isLoggedIn => {
if (isLoggedIn && this._route.snapshot.paramMap.get('tenant')) {
this.loadingService.stop();
this._router.navigate([this._route.snapshot.paramMap.get('tenant'), 'main']);
}
});
} else {
if (!this._tenantContext.hasMultipleTenants) {
const singleTenant = this._tenantContext.tenantData$.value[0];
console.log('single tenant: ', singleTenant);
window.location.href = window.location.origin + this._baseHref + '/' + singleTenant.tenantId;
}
}
this.loadingService.stop();
}
async updateTenantSelection() {
updateTenantSelection() {
const tenant = this.form.controls.tenantId.value;
console.log('update to: ', tenant);
window.location.href = window.location.origin + this._baseHref + '/' + tenant;
if (!tenant) {
throw new Error('No tenant selected!');
}
this.loadingService.start();
return this.#router.navigate([tenant]);
}
}

View File

@ -1,4 +1,4 @@
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { APP_INITIALIZER, inject, ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { MatDialogModule } from '@angular/material/dialog';
@ -7,10 +7,8 @@ import { TenantResolveComponent } from './tenant-resolve/tenant-resolve.componen
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { ReactiveFormsModule } from '@angular/forms';
import { IqserConfigService } from '../services';
import { TenantContext, TenantIdInterceptor, TenantIdResponseInterceptor } from './services';
import { TenantIdInterceptor, TenantIdResponseInterceptor, TenantsService } from './services';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { lastValueFrom } from 'rxjs';
const components = [TenantResolveComponent];
@ -46,15 +44,12 @@ export class TenantsModule {
{
provide: APP_INITIALIZER,
multi: true,
useFactory: tenantInitializer,
deps: [IqserConfigService, TenantContext],
useFactory() {
const tenantsService = inject(TenantsService);
return () => tenantsService.loadTenants();
},
},
],
};
}
}
export function tenantInitializer(configService: IqserConfigService, tenantContext: TenantContext) {
const tenants = lastValueFrom(tenantContext.loadTenants());
return () => tenants;
}

View File

@ -11,7 +11,7 @@ export class IqserRoleGuard implements CanActivate {
protected readonly _loadingService = inject(LoadingService);
protected readonly _userService = inject(IqserUserService);
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const currentUser = this._userService.currentUser;
if (!currentUser || !currentUser.hasAnyRole) {
await this._router.navigate([`/${this._tenantContextHolder.currentTenant}/auth-error`]);

View File

@ -8,7 +8,6 @@ export * from './types/name-pipe-options';
export * from './iqser-user.model';
export * from './services/iqser-user.service';
export * from './services/default-user.service';
export * from './services/keycloak-status.service';
export * from './iqser-users.module';
export * from './guards/iqser-auth-guard.service';
export * from './guards/iqser-role-guard.service';

View File

@ -1,15 +1,14 @@
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { KeycloakAngularModule, KeycloakEventType, KeycloakOptions, KeycloakService } from 'keycloak-angular';
import { KeycloakAngularModule } from 'keycloak-angular';
import { DefaultUserService } from './services/default-user.service';
import { IIqserUser } from './types/user.response';
import { IqserUserService } from './services/iqser-user.service';
import { BASE_HREF, ModuleOptions } from '../utils';
import { ModuleOptions } from '../utils';
import { IqserUsersModuleOptions } from './types/iqser-users-module-options';
import { IqserUser } from './iqser-user.model';
import { IqserRoleGuard } from './guards/iqser-role-guard.service';
import { IqserAuthGuard } from './guards/iqser-auth-guard.service';
import { IqserConfigService } from '../services';
import { NamePipe } from './name.pipe';
import { InitialsAvatarComponent } from './components/initials-avatar/initials-avatar.component';
import { MatTooltipModule } from '@angular/material/tooltip';
@ -18,88 +17,6 @@ import { UserButtonComponent } from './components/user-button/user-button.compon
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { TranslateModule } from '@ngx-translate/core';
import { TenantContext, TenantContextHolder } from '../tenants';
import { filter, firstValueFrom, of, switchMap } from 'rxjs';
import { KeycloakStatus, KeycloakStatusService } from './services/keycloak-status.service';
import { map, tap } from 'rxjs/operators';
function getKeycloakOptions(baseUrl: string, tenantContextHolder: TenantContextHolder, configService: IqserConfigService): KeycloakOptions {
console.log('keycloak config for: ', tenantContextHolder.currentTenant);
return {
config: {
url: configService.values.OAUTH_URL,
realm: tenantContextHolder.currentTenant,
clientId: configService.values.OAUTH_CLIENT_ID,
},
initOptions: {
checkLoginIframe: false,
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + baseUrl + '/assets/oauth/silent-refresh.html',
flow: 'standard',
enableLogging: true,
},
enableBearerInterceptor: true,
};
}
function configureAutomaticRedirectToLoginScreen(keyCloakService: KeycloakService, keycloakStatusService: KeycloakStatusService) {
keyCloakService.getKeycloakInstance().onAuthRefreshError = () => {
console.log('onAuthRefreshError');
keycloakStatusService.createLoginUrlAndExecute();
};
keyCloakService.getKeycloakInstance().onAuthError = err => {
console.log('onAuthError', err);
};
keyCloakService.getKeycloakInstance().onActionUpdate = err => {
console.log('onaction update', err);
};
}
export function keycloakInitializer(
keycloakService: KeycloakService,
configService: IqserConfigService,
baseUrl: string,
keycloakStatusService: KeycloakStatusService,
tenantContext: TenantContext,
tenantContextHolder: TenantContextHolder,
): () => Promise<boolean> {
const tenantsReady = tenantContext.tenantsReady$.pipe(
filter(t => t),
switchMap(() => {
console.log('keycloak init for: ', tenantContextHolder.currentTenant);
if (tenantContextHolder.currentTenant) {
const x = keycloakService.init(getKeycloakOptions(baseUrl, tenantContextHolder, configService));
keycloakStatusService.updateStatus(KeycloakStatus.PENDING);
configureAutomaticRedirectToLoginScreen(keycloakService, keycloakStatusService);
return x;
} else {
console.log('keycloak init skipped');
keycloakStatusService.updateStatus(KeycloakStatus.NOT_ACTIVE);
return of(true);
}
}),
);
return () => firstValueFrom(tenantsReady);
}
export function keycloakStatusInitializer(keycloakService: KeycloakService, keycloakStatusService: KeycloakStatusService) {
const pipe = keycloakStatusService.keycloakStatus$.pipe(
filter(status => status === KeycloakStatus.PENDING || status === KeycloakStatus.NOT_ACTIVE),
switchMap(status => {
if (status === KeycloakStatus.NOT_ACTIVE) {
return of(true);
} else {
return keycloakService.keycloakEvents$.pipe(
filter(event => event.type === KeycloakEventType.OnReady),
tap(() => keycloakStatusService.updateStatus(KeycloakStatus.READY)),
map(() => true),
);
}
}),
);
return () => firstValueFrom(pipe);
}
const components = [NamePipe, InitialsAvatarComponent, UserButtonComponent];
@ -120,23 +37,7 @@ export class IqserUsersModule {
return {
ngModule: IqserUsersModule,
providers: [
userService,
roleGuard,
IqserAuthGuard,
{
provide: APP_INITIALIZER,
useFactory: keycloakStatusInitializer,
multi: true,
deps: [KeycloakService, KeycloakStatusService],
},
{
provide: APP_INITIALIZER,
useFactory: keycloakInitializer,
multi: true,
deps: [KeycloakService, IqserConfigService, BASE_HREF, KeycloakStatusService, TenantContext, TenantContextHolder],
},
],
providers: [userService, roleGuard, IqserAuthGuard],
};
}
}

View File

@ -2,8 +2,8 @@ import { inject, Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { BehaviorSubject, firstValueFrom, Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { BASE_HREF, List, mapEach, RequiredParam, Validate } from '../../utils';
import { IqserConfigService, QueryParam, Toaster } from '../../services';
import { List, mapEach, RequiredParam, Validate } from '../../utils';
import { QueryParam, Toaster } from '../../services';
import { CacheApiService } from '../../caching';
import { EntitiesService } from '../../listing';
import { IIqserUser } from '../types/user.response';
@ -16,7 +16,7 @@ import { IqserUser } from '../iqser-user.model';
import { IqserPermissionsService, IqserRolesService } from '../../permissions';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { KeycloakStatusService } from './keycloak-status.service';
import { KeycloakStatusService } from '../../tenants';
@Injectable()
export abstract class IqserUserService<
@ -29,9 +29,7 @@ export abstract class IqserUserService<
protected abstract readonly _rolesFilter: (role: string) => boolean;
protected abstract readonly _entityClass: new (entityInterface: Interface | KeycloakProfile, ...args: unknown[]) => Class;
protected readonly _currentUser$ = new BehaviorSubject<Class | undefined>(undefined);
protected readonly _baseHref = inject(BASE_HREF);
protected readonly _toaster = inject(Toaster);
protected readonly _configService = inject(IqserConfigService);
protected readonly _keycloakService = inject(KeycloakService);
protected readonly _cacheApiService = inject(CacheApiService);
protected readonly _keycloakStatusService = inject(KeycloakStatusService);
@ -64,6 +62,7 @@ export abstract class IqserUserService<
await this._cacheApiService.wipeCaches();
await this._keycloakService.logout(this._keycloakStatusService.createLoginUrl());
} catch (e) {
console.log('Logout failed: ', e);
await this.redirectToLogin();
}
}

View File

@ -1,55 +0,0 @@
import { inject, Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { KeycloakService } from 'keycloak-angular';
import { IqserConfigService } from '../../services';
import { TenantContextHolder } from '../../tenants';
import { BASE_HREF } from '../../utils';
export enum KeycloakStatus {
UNKNOWN = 'UNKNOWN',
PENDING = 'PENDING',
READY = 'READY',
NOT_ACTIVE = 'NOT_ACTIVE',
}
@Injectable({ providedIn: 'root' })
export class KeycloakStatusService {
readonly keycloakStatus$ = new BehaviorSubject<KeycloakStatus>(KeycloakStatus.UNKNOWN);
private readonly _keyCloakService = inject(KeycloakService);
private readonly _configService = inject(IqserConfigService);
private readonly _tenantContextHolder = inject(TenantContextHolder);
private readonly _baseHref = inject(BASE_HREF);
updateStatus(status: KeycloakStatus) {
this.keycloakStatus$.next(status);
}
createLoginUrlAndExecute() {
const keycloakInstance = this._keyCloakService?.getKeycloakInstance();
if (keycloakInstance) {
window.location.href = keycloakInstance.createLoginUrl({
redirectUri: this.createLoginUrl(),
idpHint: this._configService.values.OAUTH_IDP_HINT,
});
}
}
createLoginUrl() {
let url;
if (
this._tenantContextHolder.currentTenant &&
window.location.href.indexOf('/' + this._tenantContextHolder.currentTenant + '/main') > 0
) {
url = window.location.href;
} else {
url = window.location.origin + this._baseHref;
if (this._tenantContextHolder.currentTenant) {
url = url + '/' + this._tenantContextHolder.currentTenant + '/main';
}
}
console.log('Created Url', url, this._baseHref, this._tenantContextHolder.currentTenant);
return url;
}
}