Merge pull request #9576 from abpframework/feat/9383-angular

Add tenant resolver from query params to the angular client
pull/9580/head
Bunyamin Coskuner 4 years ago committed by GitHub
commit 3b2188f080
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -296,6 +296,32 @@ services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
});
````
If you change the `TenantKey`, make sure to pass it to `CoreModule` in the Angular client as follows:
```js
@NgModule({
imports: [
CoreModule.forRoot({
// ...
tenantKey: 'MyTenantKey'
}),
],
// ...
})
export class AppModule {}
```
If you need to access it, you can inject it as follows:
```js
import { Inject } from '@angular/core';
import { TENANT_KEY } from '@abp/ng.core';
class SomeComponent {
constructor(@Inject(TENANT_KEY) private tenantKey: string) {}
}
```
> However, we don't suggest to change this value since some clients may assume the the `__tenant` as the parameter name and they might need to manually configure then.
##### Domain/Subdomain Tenant Resolver

@ -17,14 +17,12 @@ export class AuthWrapperService {
tenantBoxKey = 'Account.TenantBoxComponent';
route: ActivatedRoute;
private _tenantBoxVisible = true;
private setTenantBoxVisibility = () => {
this._tenantBoxVisible = this.route.snapshot.firstChild.data.tenantBoxVisible ?? true;
};
get isTenantBoxVisibleForCurrentRoute() {
return this.getMostInnerChild().data.tenantBoxVisible ?? true;
}
get isTenantBoxVisible() {
return this._tenantBoxVisible && this.multiTenancy.isTenantBoxVisible;
return this.isTenantBoxVisibleForCurrentRoute && this.multiTenancy.isTenantBoxVisible;
}
constructor(
@ -33,6 +31,16 @@ export class AuthWrapperService {
injector: Injector,
) {
this.route = injector.get(ActivatedRoute);
this.setTenantBoxVisibility();
}
private getMostInnerChild() {
let child = this.route.snapshot;
let depth = 0;
const depthLimit = 10;
while (child.firstChild && depth < depthLimit) {
child = child.firstChild;
depth++;
}
return child;
}
}

@ -18,7 +18,6 @@ export class ResetPasswordComponent implements OnInit {
inProgress = false;
isPasswordReset = false;
tenantId = '';
mapErrorsFn: Validation.MapErrorsFn = (errors, groupErrors, control) => {
if (PASSWORD_FIELDS.indexOf(String(control.name)) < 0) return errors;
@ -35,8 +34,7 @@ export class ResetPasswordComponent implements OnInit {
) {}
ngOnInit(): void {
this.route.queryParams.subscribe(({ userId, resetToken, tenantId }) => {
this.tenantId = tenantId;
this.route.queryParams.subscribe(({ userId, resetToken }) => {
if (!userId || !resetToken) this.router.navigateByUrl('/account/login');
this.form = this.fb.group(
@ -63,7 +61,6 @@ export class ResetPasswordComponent implements OnInit {
userId: this.form.get('userId').value,
resetToken: this.form.get('resetToken').value,
password: this.form.get('password').value,
tenantId: this.tenantId || undefined, // if this.tenantId is empty, we should not send it at all
})
.pipe(finalize(() => (this.inProgress = false)))
.subscribe(() => {

@ -9,7 +9,6 @@ export interface RegisterDto extends ExtensibleObject {
export interface ResetPasswordDto {
userId?: string;
tenantId?: string;
resetToken: string;
password: string;
}

@ -33,6 +33,7 @@ import { coreOptionsFactory, CORE_OPTIONS } from './tokens/options.token';
import { noop } from './utils/common-utils';
import './utils/date-extensions';
import { getInitialData, localeInitializer } from './utils/initial-utils';
import { TENANT_KEY } from './tokens/tenant-key.token';
export function storageFactory(): OAuthStorage {
return oAuthStorage;
@ -178,6 +179,7 @@ export class CoreModule {
useFactory: noop,
},
{ provide: OAuthStorage, useFactory: storageFactory },
{ provide: TENANT_KEY, useValue: options.tenantKey || '__tenant' },
],
};
}

@ -1,9 +1,10 @@
import { HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Injectable, Inject } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
import { finalize } from 'rxjs/operators';
import { SessionStateService } from '../services/session-state.service';
import { HttpWaitService } from '../services/http-wait.service';
import { TENANT_KEY } from '../tokens/tenant-key.token';
@Injectable({
providedIn: 'root',
@ -13,6 +14,7 @@ export class ApiInterceptor implements HttpInterceptor {
private oAuthService: OAuthService,
private sessionState: SessionStateService,
private httpWaitService: HttpWaitService,
@Inject(TENANT_KEY) private tenantKey: string,
) {}
intercept(request: HttpRequest<any>, next: HttpHandler) {
@ -40,8 +42,8 @@ export class ApiInterceptor implements HttpInterceptor {
}
const tenant = this.sessionState.getTenant();
if (!existingHeaders?.has('__tenant') && tenant?.id) {
headers['__tenant'] = tenant.id;
if (!existingHeaders?.has(this.tenantKey) && tenant?.id) {
headers[this.tenantKey] = tenant.id;
}
return headers;

@ -10,6 +10,7 @@ export namespace ABP {
registerLocaleFn: (locale: string) => Promise<any>;
skipGetAppConfiguration?: boolean;
sendNullsAsQueryParam?: boolean;
tenantKey?: string;
}
export interface HasPolicy {
@ -74,7 +75,6 @@ export namespace ABP {
[key: string]: T;
}
export type ExtractFromOutput<
T extends EventEmitter<any> | Subject<any>
> = T extends EventEmitter<infer X> ? X : T extends Subject<infer Y> ? Y : never;
export type ExtractFromOutput<T extends EventEmitter<any> | Subject<any>> =
T extends EventEmitter<infer X> ? X : T extends Subject<infer Y> ? Y : never;
}

@ -8,11 +8,12 @@ import type { FindTenantResultDto } from '../../../volo/abp/asp-net-core/mvc/mul
export class AbpTenantService {
apiName = 'abp';
findTenantById = (id: string) =>
findTenantById = (id: string, headers: Record<string, string>) =>
this.restService.request<any, FindTenantResultDto>(
{
method: 'GET',
url: `/api/abp/multi-tenancy/tenants/by-id/${id}`,
headers,
},
{ apiName: this.apiName },
);

@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { ApplicationConfigurationDto } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/models';
import { InternalStore } from '../utils/internal-store-utils';
import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service';
@Injectable({
providedIn: 'root',
@ -14,10 +15,16 @@ export class ConfigStateService {
return this.store.sliceUpdate;
}
constructor(private abpConfigService: AbpApplicationConfigurationService) {}
setState(state: ApplicationConfigurationDto) {
this.store.set(state);
}
refreshAppState() {
return this.abpConfigService.get().pipe(tap(res => this.setState(res)));
}
getOne$(key: string) {
return this.store.sliceState(state => state[key]);
}

@ -1,31 +1,37 @@
import { Injectable } from '@angular/core';
import { Injectable, Inject } from '@angular/core';
import { switchMap, map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { ABP } from '../models/common';
import {
CurrentTenantDto,
FindTenantResultDto,
CurrentTenantDto,
} from '../proxy/volo/abp/asp-net-core/mvc/multi-tenancy/models';
import { RestService } from './rest.service';
import { AbpTenantService } from '../proxy/pages/abp/multi-tenancy';
import { ConfigStateService } from './config-state.service';
import { SessionStateService } from './session-state.service';
import { TENANT_KEY } from '../tokens/tenant-key.token';
@Injectable({ providedIn: 'root' })
export class MultiTenancyService {
private _domainTenant: CurrentTenantDto = null;
set domainTenant(value: CurrentTenantDto) {
this._domainTenant = value;
this.sessionState.setTenant(value);
}
get domainTenant() {
return this._domainTenant;
}
domainTenant: CurrentTenantDto = null;
isTenantBoxVisible = true;
apiName = 'abp';
constructor(private restService: RestService, private sessionState: SessionStateService) {}
private setTenantToState = (tenant: FindTenantResultDto) => {
this.sessionState.setTenant({ id: tenant.tenantId, name: tenant.name, isAvailable: true });
return this.configStateService.refreshAppState().pipe(map(_ => tenant));
};
constructor(
private restService: RestService,
private sessionState: SessionStateService,
private tenantService: AbpTenantService,
private configStateService: ConfigStateService,
@Inject(TENANT_KEY) public tenantKey: string,
) {}
/**
* @deprecated Use AbpTenantService.findTenantByName method instead. To be deleted in v5.0.
@ -50,4 +56,16 @@ export class MultiTenancyService {
{ apiName: this.apiName },
);
}
setTenantByName(tenantName: string) {
return this.tenantService
.findTenantByName(tenantName, { [this.tenantKey]: '' })
.pipe(switchMap(this.setTenantToState));
}
setTenantById(tenantId: string) {
return this.tenantService
.findTenantById(tenantId, { [this.tenantKey]: '' })
.pipe(switchMap(this.setTenantToState));
}
}

@ -19,6 +19,7 @@ import { EnvironmentService } from '../services/environment.service';
import { SessionStateService } from '../services/session-state.service';
import { removeRememberMe, setRememberMe } from '../utils/auth-utils';
import { noop } from '../utils/common-utils';
import { TENANT_KEY } from '../tokens/tenant-key.token';
export const oAuthStorage = localStorage;
@ -32,6 +33,7 @@ export abstract class AuthFlowStrategy {
protected oAuthConfig: AuthConfig;
protected sessionState: SessionStateService;
protected appConfigService: AbpApplicationConfigurationService;
protected tenantKey: string;
abstract checkIfInternalAuth(queryParams?: Params): boolean;
abstract navigateToLogin(queryParams?: Params): void;
@ -48,6 +50,7 @@ export abstract class AuthFlowStrategy {
this.appConfigService = injector.get(AbpApplicationConfigurationService);
this.sessionState = injector.get(SessionStateService);
this.oAuthConfig = this.environment.getEnvironment().oAuthConfig;
this.tenantKey = injector.get(TENANT_KEY);
this.listenToOauthErrors();
}
@ -176,7 +179,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
this.oAuthService.fetchTokenUsingPasswordFlow(
params.username,
params.password,
new HttpHeaders({ ...(tenant && tenant.id && { __tenant: tenant.id }) }),
new HttpHeaders({ ...(tenant && tenant.id && { [this.tenantKey]: tenant.id }) }),
),
).pipe(this.pipeToLogin(params));
}

@ -5,6 +5,7 @@ import { OAuthService } from 'angular-oauth2-oidc';
import { Subject, timer } from 'rxjs';
import { ApiInterceptor } from '../interceptors';
import { HttpWaitService, SessionStateService } from '../services';
import { TENANT_KEY } from '../tokens/tenant-key.token';
describe('ApiInterceptor', () => {
let spectator: SpectatorService<ApiInterceptor>;
@ -13,9 +14,12 @@ describe('ApiInterceptor', () => {
let sessionState: SpyObject<SessionStateService>;
let httpWaitService: SpyObject<HttpWaitService>;
const testTenantKey = 'TEST_TENANT_KEY';
const createService = createServiceFactory({
service: ApiInterceptor,
mocks: [OAuthService, SessionStateService],
providers: [{ provide: TENANT_KEY, useValue: testTenantKey }],
});
beforeEach(() => {
@ -38,7 +42,7 @@ describe('ApiInterceptor', () => {
handle: (req: HttpRequest<any>) => {
expect(req.headers.get('Authorization')).toEqual('Bearer ey892mkwa8^2jk');
expect(req.headers.get('Accept-Language')).toEqual('tr');
expect(req.headers.get('__tenant')).toEqual('Volosoft');
expect(req.headers.get(testTenantKey)).toEqual('Volosoft');
done();
return handleRes$;
},

@ -5,8 +5,9 @@ import {
CurrentUserDto,
} from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/models';
import { ConfigStateService } from '../services';
import { CoreTestingModule } from '@abp/ng.core/testing';
export const CONFIG_STATE_DATA = ({
export const CONFIG_STATE_DATA = {
environment: {
production: false,
application: {
@ -97,7 +98,7 @@ export const CONFIG_STATE_DATA = ({
},
},
registerLocaleFn: () => Promise.resolve(),
} as any) as ApplicationConfigurationDto;
} as any as ApplicationConfigurationDto;
describe('ConfigState', () => {
let spectator: SpectatorService<ConfigStateService>;
@ -105,6 +106,7 @@ describe('ConfigState', () => {
const createService = createServiceFactory({
service: ConfigStateService,
imports: [CoreTestingModule.withConfig()],
});
beforeEach(() => {

@ -15,7 +15,7 @@ describe('Date Utils', () => {
let config: ConfigStateService;
beforeEach(() => {
config = new ConfigStateService();
config = new ConfigStateService(null);
});
describe('#getShortDateFormat', () => {

@ -1,14 +1,15 @@
import { Component } from '@angular/core';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import clone from 'just-clone';
import { BehaviorSubject } from 'rxjs';
import { AbpTenantService } from '../proxy/pages/abp/multi-tenancy/abp-tenant.service';
import { of } from 'rxjs';
import {
CurrentTenantDto,
FindTenantResultDto,
} from '../proxy/volo/abp/asp-net-core/mvc/multi-tenancy/models';
import { EnvironmentService, MultiTenancyService } from '../services';
import { parseTenantFromUrl } from '../utils';
import { TENANT_KEY } from '../tokens';
const environment = {
production: false,
@ -45,6 +46,8 @@ const setHref = url => {
});
};
const testTenantKey = 'TEST_TENANT_KEY';
@Component({
selector: 'abp-dummy',
template: '',
@ -56,7 +59,7 @@ describe('MultiTenancyUtils', () => {
const createComponent = createComponentFactory({
component: DummyComponent,
mocks: [EnvironmentService, MultiTenancyService],
providers: [{ provide: AbpTenantService, useValue: { findTenantByName: () => {} } }],
providers: [{ provide: TENANT_KEY, useValue: testTenantKey }],
});
beforeEach(() => (spectator = createComponent()));
@ -65,27 +68,30 @@ describe('MultiTenancyUtils', () => {
test('should get the tenancyName, set replaced environment and call the findTenantByName method of AbpTenantService', async () => {
const environmentService = spectator.inject(EnvironmentService);
const multiTenancyService = spectator.inject(MultiTenancyService);
const abpTenantService = spectator.inject(AbpTenantService);
const findTenantByNameSpy = jest.spyOn(abpTenantService, 'findTenantByName');
const setTenantByName = jest.spyOn(multiTenancyService, 'setTenantByName');
const getEnvironmentSpy = jest.spyOn(environmentService, 'getEnvironment');
const setStateSpy = jest.spyOn(environmentService, 'setState');
getEnvironmentSpy.mockReturnValue(clone(environment));
const testTenant: FindTenantResultDto = {
name: 'abp',
tenantId: '1',
isActive: true,
success: true,
};
setHref('https://abp.volosoft.com/');
findTenantByNameSpy.mockReturnValue(
new BehaviorSubject({ name: 'abp', tenantId: '1', success: true } as FindTenantResultDto),
);
setTenantByName.mockReturnValue(of(testTenant));
const mockInjector = {
get: arg => {
if (arg === EnvironmentService) return environmentService;
if (arg === AbpTenantService) return abpTenantService;
if (arg === MultiTenancyService) return multiTenancyService;
},
};
parseTenantFromUrl(mockInjector);
await parseTenantFromUrl(mockInjector);
const replacedEnv = {
...environment,
@ -106,10 +112,10 @@ describe('MultiTenancyUtils', () => {
};
expect(setStateSpy).toHaveBeenCalledWith(replacedEnv);
expect(findTenantByNameSpy).toHaveBeenCalledWith('abp', { __tenant: '' });
expect(multiTenancyService.domainTenant).toEqual({
id: '1',
name: 'abp',
id: testTenant.tenantId,
name: testTenant.name,
isAvailable: true,
} as CurrentTenantDto);
});
});

@ -3,3 +3,4 @@ export * from './lodaer-delay.token';
export * from './manage-profile.token';
export * from './options.token';
export * from './app-config.token';
export * from './tenant-key.token';

@ -0,0 +1,3 @@
import { InjectionToken } from '@angular/core';
export const TENANT_KEY = new InjectionToken<string>('TENANT_KEY');

@ -1,10 +1,10 @@
import { Injector } from '@angular/core';
import clone from 'just-clone';
import { of } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { tap } from 'rxjs/operators';
import { Environment } from '../models/environment';
import { AbpTenantService } from '../proxy/pages/abp/multi-tenancy/abp-tenant.service';
import { CurrentTenantDto } from '../proxy/volo/abp/asp-net-core/mvc/multi-tenancy/models';
import { FindTenantResultDto } from '../proxy/volo/abp/asp-net-core/mvc/multi-tenancy/models';
import { EnvironmentService } from '../services/environment.service';
import { MultiTenancyService } from '../services/multi-tenancy.service';
import { createTokenParser } from './string-utils';
@ -19,39 +19,61 @@ function getCurrentTenancyName(appBaseUrl: string): string {
return parseTokens(window.location.href)[token]?.[0];
}
function getCurrentTenancyNameFromUrl(tenantKey: string) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(tenantKey);
}
export async function parseTenantFromUrl(injector: Injector) {
const environmentService = injector.get(EnvironmentService);
const multiTenancyService = injector.get(MultiTenancyService);
const abpTenantService = injector.get(AbpTenantService);
const baseUrl = environmentService.getEnvironment()?.application?.baseUrl || '';
const tenancyName = getCurrentTenancyName(baseUrl);
if (tenancyName) {
const hideTenantBox = () => {
multiTenancyService.isTenantBoxVisible = false;
setEnvironment(injector, tenancyName);
return of(null)
.pipe(
switchMap(() => abpTenantService.findTenantByName(tenancyName, { __tenant: '' })),
tap(res => {
multiTenancyService.domainTenant = res.success
? ({ id: res.tenantId, name: res.name } as CurrentTenantDto)
: null;
}),
)
};
const setDomainTenant = (tenant: FindTenantResultDto) => {
multiTenancyService.domainTenant = {
id: tenant.tenantId,
name: tenant.name,
isAvailable: true,
};
};
const setEnvironmentWithDomainTenant = (tenant: FindTenantResultDto) => {
hideTenantBox();
setDomainTenant(tenant);
replaceTenantNameWithinEnvironment(injector, tenant.name);
};
if (tenancyName) {
return multiTenancyService
.setTenantByName(tenancyName)
.pipe(tap(setEnvironmentWithDomainTenant))
.toPromise();
} else {
/**
* If there is no tenant, we still have to clean up {0}. from baseUrl to avoid incorrect http requests.
*/
setEnvironment(injector, '', tenancyPlaceholder + '.');
replaceTenantNameWithinEnvironment(injector, '', tenancyPlaceholder + '.');
const tenantIdFromQueryParams = getCurrentTenancyNameFromUrl(multiTenancyService.tenantKey);
if (tenantIdFromQueryParams) {
return multiTenancyService.setTenantById(tenantIdFromQueryParams).toPromise();
}
}
return Promise.resolve();
}
function setEnvironment(injector: Injector, tenancyName: string, placeholder = tenancyPlaceholder) {
function replaceTenantNameWithinEnvironment(
injector: Injector,
tenancyName: string,
placeholder = tenancyPlaceholder,
) {
const environmentService = injector.get(EnvironmentService);
const environment = clone(environmentService.getEnvironment()) as Environment;

Loading…
Cancel
Save