diff --git a/src/lib/error/server-error-interceptor.ts b/src/lib/error/server-error-interceptor.ts index f23b15e..4f97871 100644 --- a/src/lib/error/server-error-interceptor.ts +++ b/src/lib/error/server-error-interceptor.ts @@ -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) { diff --git a/src/lib/tenants/guards/if-logged-in.guard.ts b/src/lib/tenants/guards/if-logged-in.guard.ts new file mode 100644 index 0000000..651f5c9 --- /dev/null +++ b/src/lib/tenants/guards/if-logged-in.guard.ts @@ -0,0 +1,39 @@ +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'; + +export const ifLoggedIn: CanActivateFn = async (route: ActivatedRouteSnapshot) => { + const logger = inject(NGXLogger); + logger.info('[ROUTES] Check if can activate main'); + + 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!'); + } + + const isLoggedIn = await keycloakService.isLoggedIn(); + + if (isLoggedIn && tenant) { + logger.warn('[ROUTES] Is logged in, continuing'); + return true; + } + + logger.warn('[ROUTES] Redirect to login'); + await keycloakStatusService.createLoginUrlAndExecute(); + return false; +}; diff --git a/src/lib/tenants/guards/if-not-logged-in.guard.ts b/src/lib/tenants/guards/if-not-logged-in.guard.ts new file mode 100644 index 0000000..47deb5d --- /dev/null +++ b/src/lib/tenants/guards/if-not-logged-in.guard.ts @@ -0,0 +1,21 @@ +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 tenant = route.paramMap.get('tenant'); + const isLoggedIn = await inject(KeycloakService).isLoggedIn(); + + if (isLoggedIn && !!tenant) { + logger.warn('[ROUTES] Is logged in for ' + tenant + ', redirecting to /' + tenant); + await router.navigate([tenant]); + return false; + } + + logger.info('[ROUTES] Not logged in, continuing to select tenant page'); + return true; +}; diff --git a/src/lib/tenants/index.ts b/src/lib/tenants/index.ts index 07c61f4..4c6d75d 100644 --- a/src/lib/tenants/index.ts +++ b/src/lib/tenants/index.ts @@ -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'; diff --git a/src/lib/tenants/keycloak-initializer.ts b/src/lib/tenants/keycloak-initializer.ts new file mode 100644 index 0000000..e542c35 --- /dev/null +++ b/src/lib/tenants/keycloak-initializer.ts @@ -0,0 +1,46 @@ +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'; + +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 keycloakService = inject(KeycloakService); + const keycloakStatusService = inject(KeycloakStatusService); + const baseHref = inject(BASE_HREF); + const config = getConfig(); + + const keycloakOptions = getKeycloakOptions(baseHref, config, tenant); + await keycloakService.init(keycloakOptions); + configureAutomaticRedirectToLoginScreen(keycloakService, keycloakStatusService); +} diff --git a/src/lib/tenants/services/index.ts b/src/lib/tenants/services/index.ts index 6880cf3..eb0cded 100644 --- a/src/lib/tenants/services/index.ts +++ b/src/lib/tenants/services/index.ts @@ -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'; diff --git a/src/lib/tenants/services/keycloak-status.service.ts b/src/lib/tenants/services/keycloak-status.service.ts new file mode 100644 index 0000000..1841b2a --- /dev/null +++ b/src/lib/tenants/services/keycloak-status.service.ts @@ -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.error('[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; + } +} diff --git a/src/lib/tenants/services/tenant-context-holder.ts b/src/lib/tenants/services/tenant-context-holder.ts index 28f3301..dafb467 100644 --- a/src/lib/tenants/services/tenant-context-holder.ts +++ b/src/lib/tenants/services/tenant-context-holder.ts @@ -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(''); - - setCurrentTenantId(tenantId: string) { - this.#activeTenantId$.next(tenantId); - } + readonly #tenantsService = inject(TenantsService); get currentTenant() { - return this.#activeTenantId$.value; + return this.#tenantsService.currentTenant; } } diff --git a/src/lib/tenants/services/tenant-context.ts b/src/lib/tenants/services/tenant-context.ts deleted file mode 100644 index 18aa889..0000000 --- a/src/lib/tenants/services/tenant-context.ts +++ /dev/null @@ -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(undefined); - readonly tenantsReady$ = new BehaviorSubject(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('/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); - }; - } -} diff --git a/src/lib/tenants/services/tenant-id-response-interceptor.ts b/src/lib/tenants/services/tenant-id-response-interceptor.ts index 25449f0..e8c0c12 100644 --- a/src/lib/tenants/services/tenant-id-response-interceptor.ts +++ b/src/lib/tenants/services/tenant-id-response-interceptor.ts @@ -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 } } diff --git a/src/lib/tenants/services/tenants.service.ts b/src/lib/tenants/services/tenants.service.ts new file mode 100644 index 0000000..fc800a3 --- /dev/null +++ b/src/lib/tenants/services/tenants.service.ts @@ -0,0 +1,98 @@ +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(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(''); + + get currentTenant() { + return this.#activeTenantId$.value; + } + + async loadTenants() { + this.#logger.info('[TENANTS] Loading tenants...'); + const tenants = await firstValueFrom(this.#http.get('/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); + await this.selectTenant(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'); + await this.selectTenant(tenants[0].tenantId); + } + } + + 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; + } + + this.#mutateStorage(tenantId); + this.setCurrentTenantId(tenantId); + await this.#router.navigate([tenantId]); + } + + 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); + }; + } +} diff --git a/src/lib/tenants/tenant-resolve/tenant-resolve.component.html b/src/lib/tenants/tenant-resolve/tenant-resolve.component.html index 6ac6e53..f6a6857 100644 --- a/src/lib/tenants/tenant-resolve/tenant-resolve.component.html +++ b/src/lib/tenants/tenant-resolve/tenant-resolve.component.html @@ -1,4 +1,4 @@ -
+
@@ -9,7 +9,7 @@ class="full-width" formControlName="tenantId" > - + {{ option.displayName || option.tenantId }} diff --git a/src/lib/tenants/tenant-resolve/tenant-resolve.component.ts b/src/lib/tenants/tenant-resolve/tenant-resolve.component.ts index 4a646ec..99000d1 100644 --- a/src/lib/tenants/tenant-resolve/tenant-resolve.component.ts +++ b/src/lib/tenants/tenant-resolve/tenant-resolve.component.ts @@ -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]); } } diff --git a/src/lib/tenants/tenants.module.ts b/src/lib/tenants/tenants.module.ts index d0366c7..0bfe705 100644 --- a/src/lib/tenants/tenants.module.ts +++ b/src/lib/tenants/tenants.module.ts @@ -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; -} diff --git a/src/lib/users/guards/iqser-role-guard.service.ts b/src/lib/users/guards/iqser-role-guard.service.ts index c31d834..1dfcdcb 100644 --- a/src/lib/users/guards/iqser-role-guard.service.ts +++ b/src/lib/users/guards/iqser-role-guard.service.ts @@ -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 { + async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { const currentUser = this._userService.currentUser; if (!currentUser || !currentUser.hasAnyRole) { await this._router.navigate([`/${this._tenantContextHolder.currentTenant}/auth-error`]); diff --git a/src/lib/users/index.ts b/src/lib/users/index.ts index 4877496..4ccf4a5 100644 --- a/src/lib/users/index.ts +++ b/src/lib/users/index.ts @@ -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'; diff --git a/src/lib/users/iqser-users.module.ts b/src/lib/users/iqser-users.module.ts index ca8365e..32ac1c2 100644 --- a/src/lib/users/iqser-users.module.ts +++ b/src/lib/users/iqser-users.module.ts @@ -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 { - 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], }; } } diff --git a/src/lib/users/services/iqser-user.service.ts b/src/lib/users/services/iqser-user.service.ts index 757b239..085960b 100644 --- a/src/lib/users/services/iqser-user.service.ts +++ b/src/lib/users/services/iqser-user.service.ts @@ -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(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); diff --git a/src/lib/users/services/keycloak-status.service.ts b/src/lib/users/services/keycloak-status.service.ts deleted file mode 100644 index 60bdf1b..0000000 --- a/src/lib/users/services/keycloak-status.service.ts +++ /dev/null @@ -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.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; - } -}