diff --git a/npm/ng-packs/packages/core/src/lib/core.module.ts b/npm/ng-packs/packages/core/src/lib/core.module.ts index 3fd29fec53..16295b5abf 100644 --- a/npm/ng-packs/packages/core/src/lib/core.module.ts +++ b/npm/ng-packs/packages/core/src/lib/core.module.ts @@ -34,10 +34,10 @@ import { ConfigState } from './states/config.state'; import { ProfileState } from './states/profile.state'; import { ReplaceableComponentsState } from './states/replaceable-components.state'; import { SessionState } from './states/session.state'; -import { CORE_OPTIONS, coreOptionsFactory } from './tokens/options.token'; +import { coreOptionsFactory, CORE_OPTIONS } from './tokens/options.token'; import { noop } from './utils/common-utils'; import './utils/date-extensions'; -import { getInitialData, localeInitializer, configureOAuth } from './utils/initial-utils'; +import { configureOAuth, getInitialData, localeInitializer } from './utils/initial-utils'; export function storageFactory(): OAuthStorage { return localStorage; diff --git a/npm/ng-packs/packages/core/src/lib/models/config.ts b/npm/ng-packs/packages/core/src/lib/models/config.ts index d328cfe5b5..682e49e94e 100644 --- a/npm/ng-packs/packages/core/src/lib/models/config.ts +++ b/npm/ng-packs/packages/core/src/lib/models/config.ts @@ -17,6 +17,7 @@ export namespace Config { export interface Application { name: string; + baseUrl?: string; logoUrl?: string; } diff --git a/npm/ng-packs/packages/core/src/lib/models/find-tenant-result-dto.ts b/npm/ng-packs/packages/core/src/lib/models/find-tenant-result-dto.ts new file mode 100644 index 0000000000..751a9dc87a --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/models/find-tenant-result-dto.ts @@ -0,0 +1,16 @@ + +export class FindTenantResultDto { + success: boolean; + tenantId?: string; + name: string; + + constructor(initialValues: Partial = {}) { + if (initialValues) { + for (const key in initialValues) { + if (initialValues.hasOwnProperty(key)) { + this[key] = initialValues[key]; + } + } + } + } +} diff --git a/npm/ng-packs/packages/core/src/lib/models/index.ts b/npm/ng-packs/packages/core/src/lib/models/index.ts index c0950c3dee..e5776a682e 100644 --- a/npm/ng-packs/packages/core/src/lib/models/index.ts +++ b/npm/ng-packs/packages/core/src/lib/models/index.ts @@ -2,6 +2,7 @@ export * from './application-configuration'; export * from './common'; export * from './config'; export * from './dtos'; +export * from './find-tenant-result-dto'; export * from './profile'; export * from './replaceable-components'; export * from './rest'; diff --git a/npm/ng-packs/packages/core/src/lib/utils/formatted-string-value-extractor.ts b/npm/ng-packs/packages/core/src/lib/utils/formatted-string-value-extractor.ts new file mode 100644 index 0000000000..7e9e3f00da --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/formatted-string-value-extractor.ts @@ -0,0 +1,156 @@ +class ExtractionResult { + public isMatch: boolean; + public matches: any[]; + + constructor(isMatch: boolean) { + this.isMatch = isMatch; + this.matches = []; + } +} + +enum FormatStringTokenType { + ConstantText, + DynamicValue, +} + +class FormatStringToken { + public text: string; + public type: FormatStringTokenType; + + constructor(text: string, type: FormatStringTokenType) { + this.text = text; + this.type = type; + } +} + +class FormatStringTokenizer { + tokenize(format: string, includeBracketsForDynamicValues: boolean = false): FormatStringToken[] { + const tokens: FormatStringToken[] = []; + + let currentText = ''; + let inDynamicValue = false; + + for (let i = 0; i < format.length; i++) { + const c = format[i]; + switch (c) { + case '{': + if (inDynamicValue) { + throw new Error( + 'Incorrect syntax at char ' + + i + + '! format string can not contain nested dynamic value expression!', + ); + } + + inDynamicValue = true; + + if (currentText.length > 0) { + tokens.push(new FormatStringToken(currentText, FormatStringTokenType.ConstantText)); + currentText = ''; + } + + break; + case '}': + if (!inDynamicValue) { + throw new Error( + 'Incorrect syntax at char ' + + i + + '! These is no opening brackets for the closing bracket }.', + ); + } + + inDynamicValue = false; + + if (currentText.length <= 0) { + throw new Error( + 'Incorrect syntax at char ' + i + '! Brackets does not containt any chars.', + ); + } + + let dynamicValue = currentText; + if (includeBracketsForDynamicValues) { + dynamicValue = '{' + dynamicValue + '}'; + } + + tokens.push(new FormatStringToken(dynamicValue, FormatStringTokenType.DynamicValue)); + currentText = ''; + + break; + default: + currentText += c; + break; + } + } + + if (inDynamicValue) { + throw new Error('There is no closing } char for an opened { char.'); + } + + if (currentText.length > 0) { + tokens.push(new FormatStringToken(currentText, FormatStringTokenType.ConstantText)); + } + + return tokens; + } +} + +export class FormattedStringValueExtractor { + extract(str: string, format: string): ExtractionResult { + if (str === format) { + return new ExtractionResult(true); + } + + const formatTokens = new FormatStringTokenizer().tokenize(format); + if (!formatTokens) { + return new ExtractionResult(str === ''); + } + + const result = new ExtractionResult(true); + + for (let i = 0; i < formatTokens.length; i++) { + const currentToken = formatTokens[i]; + const previousToken = i > 0 ? formatTokens[i - 1] : null; + + if (currentToken.type === FormatStringTokenType.ConstantText) { + if (i === 0) { + if (str.indexOf(currentToken.text) !== 0) { + result.isMatch = false; + return result; + } + + str = str.substr(currentToken.text.length, str.length - currentToken.text.length); + } else { + const matchIndex = str.indexOf(currentToken.text); + if (matchIndex < 0) { + result.isMatch = false; + return result; + } + + result.matches.push({ name: previousToken.text, value: str.substr(0, matchIndex) }); + str = str.substring(0, matchIndex + currentToken.text.length); + } + } + } + + const lastToken = formatTokens[formatTokens.length - 1]; + if (lastToken.type === FormatStringTokenType.DynamicValue) { + result.matches.push({ name: lastToken.text, value: str }); + } + + return result; + } + + isMatch(str: string, format: string): string[] { + const result = new FormattedStringValueExtractor().extract(str, format); + if (!result.isMatch) { + return []; + } + + const values = []; + for (let i = 0; i < result.matches.length; i++) { + values.push(result.matches[i].value); + } + + return values; + } +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index 6f6136b195..b31c0320f1 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -9,4 +9,5 @@ export * from './localization-utils'; export * from './number-utils'; export * from './route-utils'; export * from './rxjs-utils'; +export * from './multi-tenancy-utils'; export * from './tree-utils'; diff --git a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts index 78677b7ca4..bca8e1e176 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts @@ -7,6 +7,7 @@ import { GetAppConfiguration } from '../actions/config.actions'; import { ABP } from '../models/common'; import { ConfigState } from '../states/config.state'; import { CORE_OPTIONS } from '../tokens/options.token'; +import { parseTenantFromUrl } from './multi-tenancy-utils'; export function configureOAuth(injector: Injector, options: ABP.Root) { const fn = () => { @@ -19,10 +20,12 @@ export function configureOAuth(injector: Injector, options: ABP.Root) { } export function getInitialData(injector: Injector) { - const fn = () => { + const fn = async () => { const store: Store = injector.get(Store); const { skipGetAppConfiguration } = injector.get(CORE_OPTIONS) as ABP.Root; + await parseTenantFromUrl(injector); + if (skipGetAppConfiguration) return; return store diff --git a/npm/ng-packs/packages/core/src/lib/utils/multi-tenancy-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/multi-tenancy-utils.ts new file mode 100644 index 0000000000..237b4b5501 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/multi-tenancy-utils.ts @@ -0,0 +1,73 @@ +import { Injector } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { ConfigState } from '../states/config.state'; +import { Config } from '../models/config'; +import { FormattedStringValueExtractor } from './formatted-string-value-extractor'; +import { MultiTenancyService } from '../services/multi-tenancy.service'; +import { tap, switchMap } from 'rxjs/operators'; +import { SetTenant, SetEnvironment } from '../actions'; +import { of } from 'rxjs'; + +const tenancyPlaceholder = '{TENANCY_NAME}'; + +export function getCurrentTenancyNameOrNull(appBaseUrl: string): string { + if (appBaseUrl.indexOf(tenancyPlaceholder) < 0) return null; + + const currentRootAddress = document.location.href; + + const formattedStringValueExtracter = new FormattedStringValueExtractor(); + const values: any[] = formattedStringValueExtracter.isMatch(currentRootAddress, appBaseUrl); + if (!values.length) { + return null; + } + + return values[0]; +} + +export async function parseTenantFromUrl(injector: Injector) { + const store: Store = injector.get(Store); + const multiTenancyService = injector.get(MultiTenancyService); + const environment = store.selectSnapshot(ConfigState.getOne('environment')) as Config.Environment; + + const { baseUrl = '' } = environment.application; + const tenancyName = getCurrentTenancyNameOrNull(baseUrl); + + if (tenancyName) { + multiTenancyService.isTenantBoxVisible = false; + + return setEnvironment(store, tenancyName) + .pipe( + switchMap(() => multiTenancyService.findTenantByName(tenancyName, { __tenant: '' })), + tap(res => { + if (!res.success) return; + const tenant = { id: res.tenantId, name: res.name }; + multiTenancyService.domainTenant = tenant; + }), + ) + .toPromise(); + } + + return Promise.resolve(); +} + +export function setEnvironment(store: Store, tenancyName: string) { + const environment = store.selectSnapshot(ConfigState.getOne('environment')) as Config.Environment; + + if (environment.application.baseUrl) { + environment.application.baseUrl = environment.application.baseUrl.replace( + tenancyPlaceholder, + tenancyName, + ); + } + + Object.keys(environment.apis).forEach(api => { + Object.keys(environment.apis[api]).forEach(key => { + environment.apis[api][key] = environment.apis[api][key].replace( + tenancyPlaceholder, + tenancyName, + ); + }); + }); + + return store.dispatch(new SetEnvironment(environment)); +}