RED-3800, refactor multitenancy

This commit is contained in:
George 2023-11-29 17:06:17 +02:00
parent 59fbd1f78f
commit d1c0559099
11 changed files with 32 additions and 73 deletions

View File

@ -21,7 +21,6 @@ import { CircleButtonComponent, IconButtonComponent } from '../buttons';
import { MatIconModule } from '@angular/material/icon';
import { EmptyStateComponent } from '../empty-state';
import { InputWithActionComponent, RoundCheckboxComponent } from '../inputs';
import { TenantPipe } from '../tenants/tenant.pipe';
const matModules = [MatTooltipModule, MatIconModule];
const components = [
@ -51,7 +50,6 @@ const modules = [DragDropModule, TranslateModule, IqserFiltersModule, ScrollingM
RoundCheckboxComponent,
InputWithActionComponent,
SyncWidthDirective,
TenantPipe,
],
})
export class IqserListingModule {}

View File

@ -17,7 +17,7 @@
[class.help-mode-active]="helpModeService?.isHelpModeActive$ | async"
[id]="'item-' + entity.id"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="entity.routerLink | tenant"
[routerLink]="entity.routerLink"
>
<iqser-table-item
(click)="multiSelect(entity, $event)"
@ -31,7 +31,7 @@
[class.help-mode-active]="helpModeService?.isHelpModeActive$ | async"
[id]="'item-' + entity.id"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="entity.routerLink | tenant"
[routerLink]="entity.routerLink"
>
<iqser-table-item
(click)="multiSelect(entity, $event)"

View File

@ -2,12 +2,12 @@ import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/c
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { getConfig } from './iqser-config.service';
import { BASE_HREF } from '../utils';
import { UI_ROOT_PATH_FN } from '../utils';
@Injectable()
export class ApiPathInterceptor implements HttpInterceptor {
readonly #config = getConfig();
readonly #baseHref = inject(BASE_HREF);
readonly #convertPath = inject(UI_ROOT_PATH_FN);
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (!req.url.startsWith('/assets')) {
@ -15,7 +15,7 @@ export class ApiPathInterceptor implements HttpInterceptor {
return next.handle(req.clone({ url: apiUrl }));
}
const url = this.#baseHref + req.url;
const url = this.#convertPath(req.url);
return next.handle(req.clone({ url }));
}
}

View File

@ -1,7 +1,8 @@
import { inject, Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { BASE_HREF, List } from '../utils';
import { List } from '../utils';
import { GenericService } from './generic.service';
import { APP_BASE_HREF } from '@angular/common';
export type UserAttributes = Record<string, List>;
@ -12,9 +13,9 @@ const KEYS = {
@Injectable()
export abstract class IqserUserPreferenceService extends GenericService<UserAttributes> {
#userAttributes: UserAttributes = {};
protected abstract readonly _devFeaturesEnabledKey: string;
protected readonly _serviceName: string = 'tenant-user-management';
#userAttributes: UserAttributes = {};
get userAttributes(): UserAttributes {
return this.#userAttributes;
@ -72,7 +73,7 @@ export abstract class IqserUserPreferenceService extends GenericService<UserAttr
@Injectable()
export class DefaultUserPreferenceService extends IqserUserPreferenceService {
protected readonly _defaultModelPath = 'attributes';
protected readonly _devFeaturesEnabledKey = inject(BASE_HREF) + '.enable-dev-features';
protected readonly _devFeaturesEnabledKey = inject(APP_BASE_HREF) + '.enable-dev-features';
}
export function isIqserDevMode() {

View File

@ -1,6 +1,5 @@
export * from './keycloak-initializer';
export * from './services';
export * from './tenant.pipe';
export * from './tenants.module';
export * from './tenant-select/tenant-select.component';
export * from './services/keycloak-status.service';

View File

@ -1,10 +1,11 @@
import { BASE_HREF, IqserAppConfig } from '../utils';
import { 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';
import { APP_BASE_HREF } from '@angular/common';
export function getKeycloakOptions(baseUrl: string, config: IqserAppConfig, tenant: string): KeycloakOptions {
let oauthUrl = config.OAUTH_URL;
@ -48,7 +49,7 @@ export async function keycloakInitializer(tenant: string) {
const router = inject(Router);
const keycloakService = inject(KeycloakService);
const keycloakStatusService = inject(KeycloakStatusService);
const baseHref = inject(BASE_HREF);
const baseHref = inject(APP_BASE_HREF);
const config = getConfig();
const keycloakOptions = getKeycloakOptions(baseHref, config, tenant);

View File

@ -2,15 +2,15 @@ import { inject, Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { getConfig } from '../../services';
import { TenantsService } from '../index';
import { BASE_HREF } from '../../utils';
import { NGXLogger } from 'ngx-logger';
import { APP_BASE_HREF } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class KeycloakStatusService {
readonly #keycloakService = inject(KeycloakService);
readonly #config = getConfig();
readonly #tenantsService = inject(TenantsService);
readonly #baseHref = inject(BASE_HREF);
readonly #baseHref = inject(APP_BASE_HREF);
readonly #logger = inject(NGXLogger);
createLoginUrlAndExecute(username?: string | null) {
@ -23,11 +23,10 @@ export class KeycloakStatusService {
});
this.#logger.info('[KEYCLOAK] Redirect to login url: ', url);
window.location.href = url;
window.location.assign(url);
} else {
this.#logger.error('[KEYCLOAK] Instance not found, redirect to tenant select');
window.location.href = this.createLoginUrl();
window.location.assign(window.origin);
}
}
@ -48,14 +47,14 @@ export class KeycloakStatusService {
createLoginUrl(tenant?: string) {
if (tenant && window.location.href.indexOf('/' + tenant + '/') > 0) {
return window.location.href;
return window.location.href + '/main';
}
const url = window.location.origin + this.#baseHref;
const origin = window.location.origin;
if (tenant) {
return url + '/' + tenant;
return origin + '/ui/' + tenant + '/main';
}
return url;
return origin + '/ui';
}
}

View File

@ -5,10 +5,10 @@ import { KeycloakService } from 'keycloak-angular';
import { NGXLogger } from 'ngx-logger';
import { LoadingService } from '../../loading';
import { getConfig } from '../../services';
import { BASE_HREF } from '../../utils';
import { getKeycloakOptions } from '../keycloak-initializer';
import { IStoredTenantId, TenantsService } from '../services';
import { KeycloakStatusService } from '../services/keycloak-status.service';
import { APP_BASE_HREF } from '@angular/common';
@Component({
templateUrl: './tenant-select.component.html',
@ -16,7 +16,8 @@ import { KeycloakStatusService } from '../services/keycloak-status.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TenantSelectComponent {
protected readonly baseHref = inject(BASE_HREF);
@Input() isLoggedOut = false;
protected readonly baseHref = inject(APP_BASE_HREF);
protected readonly logger = inject(NGXLogger);
protected readonly tenantsService = inject(TenantsService);
protected storedTenants: IStoredTenantId[] = [];
@ -29,7 +30,6 @@ export class TenantSelectComponent {
// eslint-disable-next-line @typescript-eslint/unbound-method
tenantId: ['', Validators.required],
});
@Input() isLoggedOut = false;
constructor() {
this.#loadStoredTenants();

View File

@ -1,19 +0,0 @@
import { inject, Pipe, PipeTransform } from '@angular/core';
import { TenantsService } from './services';
@Pipe({
name: 'tenant',
pure: true,
standalone: true,
})
export class TenantPipe implements PipeTransform {
readonly #tenantsService = inject(TenantsService);
transform(value: string | string[]): string | undefined {
if (!value) {
return undefined;
}
const _value = Array.isArray(value) ? value.join('/') : value;
return '/' + this.#tenantsService.activeTenantId + _value;
}
}

View File

@ -5,13 +5,12 @@ import { KeycloakService } from 'keycloak-angular';
import { KeycloakProfile } from 'keycloak-js';
import { BehaviorSubject, firstValueFrom, Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { IProfile } from '../../../../../red-domain/src';
import { CacheApiService } from '../../caching';
import { EntitiesService } from '../../listing';
import { IqserPermissionsService, IqserRolesService } from '../../permissions';
import { QueryParam, Toaster } from '../../services';
import { KeycloakStatusService } from '../../tenants';
import { BASE_HREF, List, mapEach } from '../../utils';
import { List, mapEach, UI_ROOT } from '../../utils';
import { IqserUser } from '../iqser-user.model';
import { ICreateUserRequest } from '../types/create-user.request';
import { IMyProfileUpdateRequest } from '../types/my-profile-update.request';
@ -24,6 +23,7 @@ export abstract class IqserUserService<
Interface extends IIqserUser = IIqserUser,
Class extends IqserUser & Interface = IqserUser & Interface,
> extends EntitiesService<Interface, Class> {
readonly currentUser$: Observable<Class | undefined>;
protected abstract readonly _defaultModelPath: string;
protected abstract readonly _permissionsFilter: (role: string) => boolean;
protected abstract readonly _rolesFilter: (role: string) => boolean;
@ -35,9 +35,8 @@ export abstract class IqserUserService<
protected readonly _keycloakStatusService = inject(KeycloakStatusService);
protected readonly _permissionsService = inject(IqserPermissionsService, { optional: true });
protected readonly _rolesService = inject(IqserRolesService, { optional: true });
protected readonly _baseHref = inject(BASE_HREF);
protected readonly _serviceName: string = 'tenant-user-management';
readonly currentUser$: Observable<Class | undefined>;
readonly #uiRoot = inject(UI_ROOT);
constructor() {
super();
@ -63,7 +62,7 @@ export abstract class IqserUserService<
try {
await this._keycloakService.loadUserProfile(true);
await this._cacheApiService.wipeCaches();
const redirectUri = window.location.origin + this._baseHref + '/?isLoggedOut=true';
const redirectUri = window.location.origin + this.#uiRoot + '/?isLoggedOut=true';
await this._keycloakService.logout(redirectUri);
} catch (e) {
console.log('Logout failed: ', e);

View File

@ -1,35 +1,16 @@
import { inject, InjectionToken } from '@angular/core';
import { PlatformLocation } from '@angular/common';
export const BASE_HREF = new InjectionToken<string>('BASE_HREF', {
export const UI_ROOT = new InjectionToken<string>('UI path root - different from BASE_HREF');
export const UI_ROOT_PATH_FN = new InjectionToken<(path: string) => string>('Append UI root to path', {
factory: () => {
const baseUrl = inject(PlatformLocation).getBaseHrefFromDOM();
if (!baseUrl) {
return '';
}
if (baseUrl[baseUrl.length - 1] === '/') {
return baseUrl.substring(0, baseUrl.length - 1);
}
console.log('Base URL:', baseUrl);
return baseUrl;
},
});
export type BaseHrefFn = (path: string) => string;
export const BASE_HREF_FN = new InjectionToken<BaseHrefFn>('Convert path function', {
factory: () => {
const baseUrl = inject(BASE_HREF);
const root = inject(UI_ROOT);
return (path: string) => {
if (path[0] === '/') {
return baseUrl + path;
return root + path;
}
return baseUrl + '/' + path;
return root + '/' + path;
};
},
});