Browse Source

UI: refactoring OAuth2/Login Page and added oauth mapperType GITHUB

pull/3575/head
Vladyslav_Prykhodko 6 years ago
parent
commit
1f7623352a
  1. 9
      ui-ngx/src/app/core/auth/auth.service.ts
  2. 22
      ui-ngx/src/app/core/http/admin.service.ts
  3. 44
      ui-ngx/src/app/core/http/oauth2.service.ts
  4. 1
      ui-ngx/src/app/core/http/public-api.ts
  5. 35
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html
  6. 11
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss
  7. 158
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts
  8. 28
      ui-ngx/src/app/modules/login/pages/login/login.component.html
  9. 13
      ui-ngx/src/app/modules/login/pages/login/login.component.scss
  10. 4
      ui-ngx/src/app/modules/login/pages/login/login.component.ts
  11. 6
      ui-ngx/src/app/shared/models/login.models.ts
  12. 119
      ui-ngx/src/app/shared/models/oauth2.models.ts
  13. 1
      ui-ngx/src/app/shared/models/public-api.ts
  14. 99
      ui-ngx/src/app/shared/models/settings.models.ts

9
ui-ngx/src/app/core/auth/auth.service.ts

@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http';
import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs'; import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators'; import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { LoginRequest, LoginResponse, OAuth2Client, PublicLoginRequest } from '@shared/models/login.models'; import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
import { ActivatedRoute, Router, UrlTree } from '@angular/router'; import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { defaultHttpOptions } from '../http/http-utils'; import { defaultHttpOptions } from '../http/http-utils';
import { UserService } from '../http/user.service'; import { UserService } from '../http/user.service';
@ -44,6 +44,7 @@ import { AdminService } from '@core/http/admin.service';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
import { OAuth2ClientInfo } from '@shared/models/oauth2.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -67,7 +68,7 @@ export class AuthService {
} }
redirectUrl: string; redirectUrl: string;
oauth2Clients: Array<OAuth2Client> = null; oauth2Clients: Array<OAuth2ClientInfo> = null;
private refreshTokenSubject: ReplaySubject<LoginResponse> = null; private refreshTokenSubject: ReplaySubject<LoginResponse> = null;
private jwtHelper = new JwtHelperService(); private jwtHelper = new JwtHelperService();
@ -197,8 +198,8 @@ export class AuthService {
}); });
} }
public loadOAuth2Clients(): Observable<Array<OAuth2Client>> { public loadOAuth2Clients(): Observable<Array<OAuth2ClientInfo>> {
return this.http.post<Array<OAuth2Client>>(`/api/noauth/oauth2Clients`, return this.http.post<Array<OAuth2ClientInfo>>(`/api/noauth/oauth2Clients`,
null, defaultHttpOptions()).pipe( null, defaultHttpOptions()).pipe(
tap((OAuth2Clients) => { tap((OAuth2Clients) => {
this.oauth2Clients = OAuth2Clients; this.oauth2Clients = OAuth2Clients;

22
ui-ngx/src/app/core/http/admin.service.ts

@ -18,14 +18,7 @@ import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models';
AdminSettings,
ClientProviderTemplated,
MailServerSettings,
OAuth2Settings,
SecuritySettings,
UpdateMessage
} from '@shared/models/settings.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -60,19 +53,6 @@ export class AdminService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getOAuth2Settings(config?: RequestConfig): Observable<OAuth2Settings> {
return this.http.get<OAuth2Settings>(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config));
}
public getOAuth2Template(config?: RequestConfig): Observable<Array<ClientProviderTemplated>> {
return this.http.get<Array<ClientProviderTemplated>>(`/api/oauth2/config/template`, defaultHttpOptionsFromConfig(config));
}
public saveOAuth2Settings(OAuth2Setting: OAuth2Settings, config?: RequestConfig): Observable<OAuth2Settings> {
return this.http.post<OAuth2Settings>('/api/oauth2/config', OAuth2Setting,
defaultHttpOptionsFromConfig(config));
}
public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> { public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> {
return this.http.get<UpdateMessage>(`/api/admin/updates`, defaultHttpOptionsFromConfig(config)); return this.http.get<UpdateMessage>(`/api/admin/updates`, defaultHttpOptionsFromConfig(config));
} }

44
ui-ngx/src/app/core/http/oauth2.service.ts

@ -0,0 +1,44 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs';
import { OAuth2ClientRegistrationTemplate, OAuth2ClientsParams } from '@shared/models/oauth2.models';
@Injectable({
providedIn: 'root'
})
export class OAuth2Service {
constructor(
private http: HttpClient
) { }
public getOAuth2Settings(config?: RequestConfig): Observable<OAuth2ClientsParams> {
return this.http.get<OAuth2ClientsParams>(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config));
}
public getOAuth2Template(config?: RequestConfig): Observable<Array<OAuth2ClientRegistrationTemplate>> {
return this.http.get<Array<OAuth2ClientRegistrationTemplate>>(`/api/oauth2/config/template`, defaultHttpOptionsFromConfig(config));
}
public saveOAuth2Settings(OAuth2Setting: OAuth2ClientsParams, config?: RequestConfig): Observable<OAuth2ClientsParams> {
return this.http.post<OAuth2ClientsParams>('/api/oauth2/config', OAuth2Setting,
defaultHttpOptionsFromConfig(config));
}
}

1
ui-ngx/src/app/core/http/public-api.ts

@ -28,6 +28,7 @@ export * from './entity-relation.service';
export * from './entity-view.service'; export * from './entity-view.service';
export * from './event.service'; export * from './event.service';
export * from './http-utils'; export * from './http-utils';
export * from './oauth2.service';
export * from './queue.service'; export * from './queue.service';
export * from './rule-chain.service'; export * from './rule-chain.service';
export * from './tenant.service'; export * from './tenant.service';

35
ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html

@ -33,7 +33,7 @@
<mat-checkbox formControlName="enabled"> <mat-checkbox formControlName="enabled">
{{ 'admin.oauth2.enable' | translate }} {{ 'admin.oauth2.enable' | translate }}
</mat-checkbox> </mat-checkbox>
<section *ngIf="oauth2SettingsForm.get('enabled').value && !(isLoading$ | async)" style="margin-top: 1em;"> <section *ngIf="oauth2SettingsForm.get('enabled').value" style="margin-top: 1em;">
<ng-container formArrayName="domainsParams"> <ng-container formArrayName="domainsParams">
<div class="container"> <div class="container">
<mat-accordion multi> <mat-accordion multi>
@ -59,7 +59,7 @@
<section *ngFor="let domainInfo of clientDomainInfos(domain).controls; let n = index; trackBy: trackByParams" <section *ngFor="let domainInfo of clientDomainInfos(domain).controls; let n = index; trackBy: trackByParams"
class="domains-list"> class="domains-list">
<div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px"> <div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px"> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
<div fxLayout="column" fxFlex.sm="60" fxFlex.gt-sm="50"> <div fxLayout="column" fxFlex.sm="60" fxFlex.gt-sm="50">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px"> <div fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
<mat-form-field fxFlex="30" fxFlex.xs class="mat-block"> <mat-form-field fxFlex="30" fxFlex.xs class="mat-block">
@ -160,7 +160,7 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div [tb-help]="getHelpLink(registration)" *ngIf="getProviderName(registration) !== 'Custom'"></div> <div [tb-help]="getHelpLink(registration)" *ngIf="!isCustomProvider(registration)"></div>
</section> </section>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
@ -182,9 +182,9 @@
</div> </div>
<mat-expansion-panel class="mat-elevation-z0 custom-settings" <mat-expansion-panel class="mat-elevation-z0 custom-settings"
[disabled]="getProviderName(registration) === 'Custom'" [disabled]="isCustomProvider(registration)"
[expanded]="getProviderName(registration) === 'Custom'"> [expanded]="isCustomProvider(registration)">
<mat-expansion-panel-header [fxHide]="getProviderName(registration) === 'Custom'"> <mat-expansion-panel-header [fxHide]="isCustomProvider(registration)">
<mat-panel-description fxLayoutAlign="end center"> <mat-panel-description fxLayoutAlign="end center">
{{ 'admin.oauth2.custom-setting' | translate }} {{ 'admin.oauth2.custom-setting' | translate }}
</mat-panel-description> </mat-panel-description>
@ -199,7 +199,7 @@
<button mat-icon-button matSuffix <button mat-icon-button matSuffix
type="button" type="button"
(click)="toggleEditMode(registration, 'accessTokenUri')" (click)="toggleEditMode(registration, 'accessTokenUri')"
*ngIf="getProviderName(registration) !== 'Custom'"> *ngIf="!isCustomProvider(registration)">
<mat-icon class="material-icons">create</mat-icon> <mat-icon class="material-icons">create</mat-icon>
</button> </button>
<mat-error *ngIf="registration.get('accessTokenUri').hasError('required')"> <mat-error *ngIf="registration.get('accessTokenUri').hasError('required')">
@ -216,7 +216,7 @@
<button mat-icon-button matSuffix <button mat-icon-button matSuffix
type="button" type="button"
(click)="toggleEditMode(registration, 'authorizationUri')" (click)="toggleEditMode(registration, 'authorizationUri')"
*ngIf="getProviderName(registration) !== 'Custom'"> *ngIf="!isCustomProvider(registration)">
<mat-icon class="material-icons">create</mat-icon> <mat-icon class="material-icons">create</mat-icon>
</button> </button>
<mat-error *ngIf="registration.get('authorizationUri').hasError('required')"> <mat-error *ngIf="registration.get('authorizationUri').hasError('required')">
@ -235,7 +235,7 @@
<button mat-icon-button matSuffix <button mat-icon-button matSuffix
type="button" aria-label="Clear" type="button" aria-label="Clear"
(click)="toggleEditMode(registration, 'jwkSetUri')" (click)="toggleEditMode(registration, 'jwkSetUri')"
*ngIf="getProviderName(registration) !== 'Custom'"> *ngIf="!isCustomProvider(registration)">
<mat-icon class="material-icons">create</mat-icon> <mat-icon class="material-icons">create</mat-icon>
</button> </button>
<mat-error *ngIf="registration.get('jwkSetUri').hasError('pattern')"> <mat-error *ngIf="registration.get('jwkSetUri').hasError('pattern')">
@ -249,7 +249,7 @@
<button mat-icon-button matSuffix <button mat-icon-button matSuffix
type="button" type="button"
(click)="toggleEditMode(registration, 'userInfoUri')" (click)="toggleEditMode(registration, 'userInfoUri')"
*ngIf="getProviderName(registration) !== 'Custom'"> *ngIf="!isCustomProvider(registration)">
<mat-icon class="material-icons">create</mat-icon> <mat-icon class="material-icons">create</mat-icon>
</button> </button>
<mat-error *ngIf="registration.get('userInfoUri').hasError('required')"> <mat-error *ngIf="registration.get('userInfoUri').hasError('required')">
@ -271,7 +271,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" *ngIf="getProviderName(registration) === 'Custom'"> <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" *ngIf="isCustomProvider(registration)">
<mat-form-field fxFlex class="mat-block" floatLabel="always"> <mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>admin.oauth2.login-button-label</mat-label> <mat-label translate>admin.oauth2.login-button-label</mat-label>
<input matInput formControlName="loginButtonLabel" <input matInput formControlName="loginButtonLabel"
@ -332,16 +332,17 @@
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.type</mat-label> <mat-label translate>admin.oauth2.type</mat-label>
<mat-select formControlName="type"> <mat-select formControlName="type">
<mat-option *ngFor="let converterTypeExternalUser of converterTypesExternalUser" <mat-option *ngFor="let mapperConfigType of mapperConfigTypes"
[value]="converterTypeExternalUser"> [value]="mapperConfigType">
{{ converterTypeExternalUser }} {{ mapperConfigType }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<section formGroupName="basic" <section formGroupName="basic"
*ngIf="registration.get('mapperConfig.type').value === 'BASIC'"> *ngIf="registration.get('mapperConfig.type').value !== mapperConfigType.CUSTOM">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block"
*ngIf="registration.get('mapperConfig.type').value !== mapperConfigType.GITHUB">
<mat-label translate>admin.oauth2.email-attribute-key</mat-label> <mat-label translate>admin.oauth2.email-attribute-key</mat-label>
<input matInput formControlName="emailAttributeKey" required> <input matInput formControlName="emailAttributeKey" required>
<mat-error <mat-error
@ -403,7 +404,7 @@
</section> </section>
<section formGroupName="custom" <section formGroupName="custom"
*ngIf="registration.get('mapperConfig.type').value === 'CUSTOM'"> *ngIf="registration.get('mapperConfig.type').value === mapperConfigType.CUSTOM">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>admin.oauth2.url</mat-label> <mat-label translate>admin.oauth2.url</mat-label>
<input matInput formControlName="url" required> <input matInput formControlName="url" required>

11
ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss

@ -41,6 +41,14 @@
padding-bottom: 0; padding-bottom: 0;
} }
} }
.mat-expansion-panel {
.mat-expansion-panel-header {
&.mat-expanded {
height: 48px;
}
}
}
} }
:host ::ng-deep{ :host ::ng-deep{
@ -49,9 +57,6 @@
.mat-expansion-panel-body{ .mat-expansion-panel-body{
padding: 0 2px 1em; padding: 0 2px 1em;
} }
.mat-tab-label{
text-transform: none;
}
.mat-form-field-suffix .mat-icon-button .mat-icon{ .mat-form-field-suffix .mat-icon-button .mat-icon{
font-size: 18px; font-size: 18px;
} }

158
ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts

@ -18,22 +18,21 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { import {
ClientAuthenticationMethod, ClientAuthenticationMethod,
ClientProviderTemplated,
ClientRegistration, ClientRegistration,
DomainInfo, DomainInfo,
DomainSchema, DomainSchema,
domainSchemaTranslations, domainSchemaTranslations,
DomainsParam,
MapperConfig, MapperConfig,
MapperConfigBasic, MapperConfigBasic,
MapperConfigCustom, MapperConfigCustom,
MapperConfigType, MapperConfigType,
OAuth2Settings, OAuth2ClientRegistrationTemplate,
OAuth2ClientsDomainParams,
OAuth2ClientsParams,
TenantNameStrategy TenantNameStrategy
} from '@shared/models/settings.models'; } from '@shared/models/oauth2.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
@ -42,7 +41,8 @@ import { WINDOW } from '@core/services/window.service';
import { forkJoin, Subscription } from 'rxjs'; import { forkJoin, Subscription } from 'rxjs';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { isDefined } from '@core/utils'; import { isDefined, isDefinedAndNotNull } from '@core/utils';
import { OAuth2Service } from '@core/http/oauth2.service';
@Component({ @Component({
selector: 'tb-oauth2-settings', selector: 'tb-oauth2-settings',
@ -53,7 +53,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/;
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
private templates = new Map<string, ClientProviderTemplated>(); private templates = new Map<string, OAuth2ClientRegistrationTemplate>();
private defaultProvider = { private defaultProvider = {
additionalInfo: { additionalInfo: {
providerName: 'Custom' providerName: 'Custom'
@ -75,10 +75,11 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
readonly separatorKeysCodes: number[] = [ENTER, COMMA]; readonly separatorKeysCodes: number[] = [ENTER, COMMA];
oauth2SettingsForm: FormGroup; oauth2SettingsForm: FormGroup;
oauth2Settings: OAuth2Settings; auth2ClientsParams: OAuth2ClientsParams;
clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod); clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod);
converterTypesExternalUser = Object.keys(MapperConfigType); mapperConfigType = MapperConfigType;
mapperConfigTypes = Object.keys(MapperConfigType);
tenantNameStrategies = Object.keys(TenantNameStrategy); tenantNameStrategies = Object.keys(TenantNameStrategy);
protocols = Object.keys(DomainSchema); protocols = Object.keys(DomainSchema);
domainSchemaTranslations = domainSchemaTranslations; domainSchemaTranslations = domainSchemaTranslations;
@ -86,7 +87,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
templateProvider = ['Custom']; templateProvider = ['Custom'];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private adminService: AdminService, private oauth2Service: OAuth2Service,
private fb: FormBuilder, private fb: FormBuilder,
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService, private translate: TranslateService,
@ -97,13 +98,13 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
ngOnInit(): void { ngOnInit(): void {
this.buildOAuth2SettingsForm(); this.buildOAuth2SettingsForm();
forkJoin([ forkJoin([
this.adminService.getOAuth2Template(), this.oauth2Service.getOAuth2Template(),
this.adminService.getOAuth2Settings() this.oauth2Service.getOAuth2Settings()
]).subscribe( ]).subscribe(
([templates, oauth2Settings]) => { ([templates, auth2ClientsParams]) => {
this.initTemplates(templates); this.initTemplates(templates);
this.oauth2Settings = oauth2Settings; this.auth2ClientsParams = auth2ClientsParams;
this.initOAuth2Settings(this.oauth2Settings); this.initOAuth2Settings(this.auth2ClientsParams);
} }
); );
} }
@ -115,8 +116,11 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
}); });
} }
private initTemplates(templates: ClientProviderTemplated[]): void { private initTemplates(templates: OAuth2ClientRegistrationTemplate[]): void {
templates.map(provider => this.templates.set(provider.name, provider)); templates.map(provider => {
delete provider.additionalInfo;
this.templates.set(provider.name, provider);
});
this.templateProvider.push(...Array.from(this.templates.keys())); this.templateProvider.push(...Array.from(this.templates.keys()));
this.templateProvider.sort(); this.templateProvider.sort();
} }
@ -169,10 +173,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
}); });
} }
private initOAuth2Settings(oauth2Settings: OAuth2Settings): void { private initOAuth2Settings(auth2ClientsParams: OAuth2ClientsParams): void {
if (oauth2Settings) { if (auth2ClientsParams) {
this.oauth2SettingsForm.patchValue({enabled: oauth2Settings.enabled}, {emitEvent: false}); this.oauth2SettingsForm.patchValue({enabled: auth2ClientsParams.enabled}, {emitEvent: false});
oauth2Settings.domainsParams.forEach((domain) => { auth2ClientsParams.domainsParams.forEach((domain) => {
this.domainsParams.push(this.buildDomainsForm(domain)); this.domainsParams.push(this.buildDomainsForm(domain));
}); });
} }
@ -204,17 +208,17 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
return this.translate.instant('admin.oauth2.new-domain'); return this.translate.instant('admin.oauth2.new-domain');
} }
private buildDomainsForm(domainParams?: DomainsParam): FormGroup { private buildDomainsForm(auth2ClientsDomainParams?: OAuth2ClientsDomainParams): FormGroup {
const formDomain = this.fb.group({ const formDomain = this.fb.group({
domainInfos: this.fb.array([], Validators.required), domainInfos: this.fb.array([], Validators.required),
clientRegistrations: this.fb.array([], Validators.required) clientRegistrations: this.fb.array([], Validators.required)
}); });
if (domainParams) { if (auth2ClientsDomainParams) {
domainParams.domainInfos.forEach((domain) => { auth2ClientsDomainParams.domainInfos.forEach((domain) => {
this.clientDomainInfos(formDomain).push(this.buildDomainForm(domain)); this.clientDomainInfos(formDomain).push(this.buildDomainForm(domain));
}); });
domainParams.clientRegistrations.forEach((registration) => { auth2ClientsDomainParams.clientRegistrations.forEach((registration) => {
this.clientDomainProviders(formDomain).push(this.buildProviderForm(registration)); this.clientDomainProviders(formDomain).push(this.buildProviderForm(registration));
}); });
} else { } else {
@ -235,10 +239,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
return domain; return domain;
} }
private buildProviderForm(registrationData?: ClientRegistration): FormGroup { private buildProviderForm(clientRegistration?: ClientRegistration): FormGroup {
let additionalInfo = null; let additionalInfo = null;
if (registrationData?.additionalInfo) { if (isDefinedAndNotNull(clientRegistration?.additionalInfo)) {
additionalInfo = JSON.parse(registrationData.additionalInfo); additionalInfo = clientRegistration.additionalInfo;
if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) { if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) {
additionalInfo.providerName = 'Custom'; additionalInfo.providerName = 'Custom';
} }
@ -248,73 +252,70 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
defaultProviderName = 'Google'; defaultProviderName = 'Google';
} }
const clientRegistration = this.fb.group({ const clientRegistrationFormGroup = this.fb.group({
id: this.fb.group({
id: [registrationData?.id?.id ? registrationData.id.id : null],
entityType: [registrationData?.id?.entityType ? registrationData.id.entityType : null]
}),
additionalInfo: this.fb.group({ additionalInfo: this.fb.group({
providerName: [additionalInfo?.providerName ? additionalInfo?.providerName : defaultProviderName, Validators.required] providerName: [additionalInfo?.providerName ? additionalInfo?.providerName : defaultProviderName, Validators.required]
}), }),
loginButtonLabel: [registrationData?.loginButtonLabel ? registrationData.loginButtonLabel : null, Validators.required], loginButtonLabel: [clientRegistration?.loginButtonLabel ? clientRegistration.loginButtonLabel : null, Validators.required],
loginButtonIcon: [registrationData?.loginButtonIcon ? registrationData.loginButtonIcon : null], loginButtonIcon: [clientRegistration?.loginButtonIcon ? clientRegistration.loginButtonIcon : null],
clientId: [registrationData?.clientId ? registrationData.clientId : '', Validators.required], clientId: [clientRegistration?.clientId ? clientRegistration.clientId : '', Validators.required],
clientSecret: [registrationData?.clientSecret ? registrationData.clientSecret : '', Validators.required], clientSecret: [clientRegistration?.clientSecret ? clientRegistration.clientSecret : '', Validators.required],
accessTokenUri: [registrationData?.accessTokenUri ? registrationData.accessTokenUri : '', accessTokenUri: [clientRegistration?.accessTokenUri ? clientRegistration.accessTokenUri : '',
[Validators.required, [Validators.required,
Validators.pattern(this.URL_REGEXP)]], Validators.pattern(this.URL_REGEXP)]],
authorizationUri: [registrationData?.authorizationUri ? registrationData.authorizationUri : '', authorizationUri: [clientRegistration?.authorizationUri ? clientRegistration.authorizationUri : '',
[Validators.required, [Validators.required,
Validators.pattern(this.URL_REGEXP)]], Validators.pattern(this.URL_REGEXP)]],
scope: this.fb.array(registrationData?.scope ? registrationData.scope : [], Validators.required), scope: this.fb.array(clientRegistration?.scope ? clientRegistration.scope : [], Validators.required),
jwkSetUri: [registrationData?.jwkSetUri ? registrationData.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], jwkSetUri: [clientRegistration?.jwkSetUri ? clientRegistration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)],
userInfoUri: [registrationData?.userInfoUri ? registrationData.userInfoUri : '', userInfoUri: [clientRegistration?.userInfoUri ? clientRegistration.userInfoUri : '',
[Validators.required, [Validators.required,
Validators.pattern(this.URL_REGEXP)]], Validators.pattern(this.URL_REGEXP)]],
clientAuthenticationMethod: [ clientAuthenticationMethod: [
registrationData?.clientAuthenticationMethod ? registrationData.clientAuthenticationMethod : ClientAuthenticationMethod.POST, clientRegistration?.clientAuthenticationMethod ? clientRegistration.clientAuthenticationMethod : ClientAuthenticationMethod.POST,
Validators.required], Validators.required],
userNameAttributeName: [ userNameAttributeName: [
registrationData?.userNameAttributeName ? registrationData.userNameAttributeName : 'email', Validators.required], clientRegistration?.userNameAttributeName ? clientRegistration.userNameAttributeName : 'email', Validators.required],
mapperConfig: this.fb.group({ mapperConfig: this.fb.group({
allowUserCreation: [registrationData?.mapperConfig?.allowUserCreation ? registrationData.mapperConfig.allowUserCreation : true], allowUserCreation: [
activateUser: [registrationData?.mapperConfig?.activateUser ? registrationData.mapperConfig.activateUser : false], clientRegistration?.mapperConfig?.allowUserCreation ? clientRegistration.mapperConfig.allowUserCreation : true
type: [registrationData?.mapperConfig?.type ? registrationData.mapperConfig.type : MapperConfigType.BASIC, Validators.required] ],
activateUser: [clientRegistration?.mapperConfig?.activateUser ? clientRegistration.mapperConfig.activateUser : false],
type: [
clientRegistration?.mapperConfig?.type ? clientRegistration.mapperConfig.type : MapperConfigType.BASIC, Validators.required
]
} }
) )
}); });
if (registrationData) { if (clientRegistration) {
this.changeMapperConfigType(clientRegistration, registrationData.mapperConfig.type, registrationData.mapperConfig); this.changeMapperConfigType(clientRegistrationFormGroup, clientRegistration.mapperConfig.type, clientRegistration.mapperConfig);
} else { } else {
this.changeMapperConfigType(clientRegistration, MapperConfigType.BASIC); this.changeMapperConfigType(clientRegistrationFormGroup, MapperConfigType.BASIC);
this.setProviderDefaultValue(defaultProviderName, clientRegistration); this.setProviderDefaultValue(defaultProviderName, clientRegistrationFormGroup);
} }
this.subscriptions.push(clientRegistration.get('mapperConfig.type').valueChanges.subscribe((value) => { this.subscriptions.push(clientRegistrationFormGroup.get('mapperConfig.type').valueChanges.subscribe((value) => {
this.changeMapperConfigType(clientRegistration, value); this.changeMapperConfigType(clientRegistrationFormGroup, value);
})); }));
this.subscriptions.push(clientRegistration.get('additionalInfo.providerName').valueChanges.subscribe((provider) => { this.subscriptions.push(clientRegistrationFormGroup.get('additionalInfo.providerName').valueChanges.subscribe((provider) => {
(clientRegistration.get('scope') as FormArray).clear(); (clientRegistrationFormGroup.get('scope') as FormArray).clear();
this.setProviderDefaultValue(provider, clientRegistration); this.setProviderDefaultValue(provider, clientRegistrationFormGroup);
})); }));
return clientRegistration; return clientRegistrationFormGroup;
} }
private setProviderDefaultValue(provider: string, clientRegistration: FormGroup) { private setProviderDefaultValue(provider: string, clientRegistration: FormGroup) {
if (provider === 'Custom') { if (provider === 'Custom') {
const defaultSettings = {...this.defaultProvider, ...{id: clientRegistration.get('id').value}}; clientRegistration.reset(this.defaultProvider, {emitEvent: false});
clientRegistration.reset(defaultSettings, {emitEvent: false});
clientRegistration.get('accessTokenUri').enable(); clientRegistration.get('accessTokenUri').enable();
clientRegistration.get('authorizationUri').enable(); clientRegistration.get('authorizationUri').enable();
clientRegistration.get('jwkSetUri').enable(); clientRegistration.get('jwkSetUri').enable();
clientRegistration.get('userInfoUri').enable(); clientRegistration.get('userInfoUri').enable();
} else { } else {
const template = this.templates.get(provider); const template = this.templates.get(provider);
delete template.id;
delete template.additionalInfo;
template.clientId = ''; template.clientId = '';
template.clientSecret = ''; template.clientSecret = '';
template.scope.forEach(() => { template.scope.forEach(() => {
@ -324,44 +325,33 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
clientRegistration.get('authorizationUri').disable(); clientRegistration.get('authorizationUri').disable();
clientRegistration.get('jwkSetUri').disable(); clientRegistration.get('jwkSetUri').disable();
clientRegistration.get('userInfoUri').disable(); clientRegistration.get('userInfoUri').disable();
clientRegistration.patchValue(template, {emitEvent: false}); clientRegistration.patchValue(template);
} }
} }
private changeMapperConfigType(control: AbstractControl, type: MapperConfigType, predefinedValue?: MapperConfig) { private changeMapperConfigType(control: AbstractControl, type: MapperConfigType, predefinedValue?: MapperConfig) {
const mapperConfig = control.get('mapperConfig') as FormGroup; const mapperConfig = control.get('mapperConfig') as FormGroup;
if (type === MapperConfigType.BASIC) { if (type === MapperConfigType.CUSTOM) {
mapperConfig.removeControl('custom');
mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic));
} else {
mapperConfig.removeControl('basic'); mapperConfig.removeControl('basic');
mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom)); mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom));
} else {
mapperConfig.removeControl('custom');
mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic));
} }
} }
save(): void { save(): void {
const setting = this.prepareFormValue(this.oauth2SettingsForm.getRawValue()); const setting = this.oauth2SettingsForm.getRawValue();
this.adminService.saveOAuth2Settings(setting).subscribe( this.oauth2Service.saveOAuth2Settings(setting).subscribe(
(oauth2Settings) => { (oauth2Settings) => {
this.oauth2Settings = oauth2Settings; this.auth2ClientsParams = oauth2Settings;
this.oauth2SettingsForm.markAsPristine(); this.oauth2SettingsForm.patchValue(this.oauth2SettingsForm, {emitEvent: false});
this.oauth2SettingsForm.markAsUntouched(); this.oauth2SettingsForm.markAsUntouched();
this.oauth2SettingsForm.markAsPristine();
} }
); );
} }
private prepareFormValue(formValue: OAuth2Settings): OAuth2Settings{
formValue.domainsParams.forEach((setting, index) => {
setting.clientRegistrations.forEach((registration) => {
registration.additionalInfo = JSON.stringify(registration.additionalInfo);
if (registration.id.id === null) {
delete registration.id;
}
});
});
return formValue;
}
confirmForm(): FormGroup { confirmForm(): FormGroup {
return this.oauth2SettingsForm; return this.oauth2SettingsForm;
} }
@ -451,6 +441,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
return controller.get('additionalInfo.providerName').value; return controller.get('additionalInfo.providerName').value;
} }
isCustomProvider(controller: AbstractControl): boolean {
return this.getProviderName(controller) === 'Custom';
}
getHelpLink(controller: AbstractControl): string { getHelpLink(controller: AbstractControl): string {
const provider = controller.get('additionalInfo.providerName').value; const provider = controller.get('additionalInfo.providerName').value;
if (provider === null || provider === 'Custom') { if (provider === null || provider === 'Custom') {

28
ui-ngx/src/app/modules/login/pages/login/login.component.html

@ -28,6 +28,19 @@
<span style="height: 4px;" *ngIf="!(isLoading$ | async)"></span> <span style="height: 4px;" *ngIf="!(isLoading$ | async)"></span>
<div tb-toast fxLayout="column" class="layout-padding"> <div tb-toast fxLayout="column" class="layout-padding">
<span style="height: 50px;"></span> <span style="height: 50px;"></span>
<div class="oauth-container tb-default" fxLayout="column" fxLayoutGap="16px" *ngIf="oauth2Clients?.length">
<ng-container *ngFor="let oauth2Client of oauth2Clients">
<a mat-raised-button class="login-with-button" href="{{ oauth2Client.url }}">
<mat-icon svgIcon="{{ oauth2Client.icon }}"></mat-icon>
{{ 'login.login-with' | translate: {name: oauth2Client.name} }}
</a>
</ng-container>
<div class="container-divider">
<div class="line"><mat-divider></mat-divider></div>
<div class="text mat-typography">{{ "login.or" | translate | uppercase }}</div>
<div class="line"><mat-divider></mat-divider></div>
</div>
</div>
<mat-form-field> <mat-form-field>
<mat-label translate>login.username</mat-label> <mat-label translate>login.username</mat-label>
<input id="username-input" matInput type="email" autofocus formControlName="username" email required/> <input id="username-input" matInput type="email" autofocus formControlName="username" email required/>
@ -41,7 +54,7 @@
<input id="password-input" matInput type="password" formControlName="password"/> <input id="password-input" matInput type="password" formControlName="password"/>
<mat-icon matPrefix>lock</mat-icon> <mat-icon matPrefix>lock</mat-icon>
</mat-form-field> </mat-form-field>
<div fxLayoutAlign="end center"> <div fxLayoutAlign="end center" class="forgot-password">
<button class="tb-reset-password" mat-button type="button" routerLink="/login/resetPasswordRequest">{{ 'login.forgot-password' | translate }} <button class="tb-reset-password" mat-button type="button" routerLink="/login/resetPasswordRequest">{{ 'login.forgot-password' | translate }}
</button> </button>
</div> </div>
@ -49,19 +62,6 @@
<button mat-raised-button color="accent" [disabled]="(isLoading$ | async)" <button mat-raised-button color="accent" [disabled]="(isLoading$ | async)"
type="submit">{{ 'login.login' | translate }}</button> type="submit">{{ 'login.login' | translate }}</button>
</div> </div>
<div class="oauth-container" fxLayout="column" fxLayoutGap="16px" *ngIf="oauth2Clients?.length">
<div class="container-divider">
<div class="line"><mat-divider></mat-divider></div>
<div class="text mat-typography">{{ "login.or" | translate | uppercase }}</div>
<div class="line"><mat-divider></mat-divider></div>
</div>
<ng-container *ngFor="let oauth2Client of oauth2Clients">
<a mat-raised-button color="warn" class="centered" href="{{ oauth2Client.url }}">
<mat-icon svgIcon="{{ oauth2Client.icon }}"></mat-icon>
{{ 'login.login-with' | translate: {name: oauth2Client.name} }}
</a>
</ng-container>
</div>
</div> </div>
</fieldset> </fieldset>
</form> </form>

13
ui-ngx/src/app/modules/login/pages/login/login.component.scss

@ -27,8 +27,11 @@
width: 550px !important; width: 550px !important;
} }
.tb-reset-password{ .forgot-password {
padding: 0 6px; padding: 0 0.5em 1em;
.tb-reset-password {
padding: 0 6px;
}
} }
.tb-action-button{ .tb-action-button{
@ -65,9 +68,11 @@
min-width: 20px; min-width: 20px;
} }
a.centered { a.login-with-button {
color: rgba(black, 0.87);
&:hover { &:hover {
border-bottom: 0px; border-bottom: 0;
} }
} }

4
ui-ngx/src/app/modules/login/pages/login/login.component.ts

@ -23,7 +23,7 @@ import { FormBuilder } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { Constants } from '@shared/models/constants'; import { Constants } from '@shared/models/constants';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { OAuth2Client } from '@shared/models/login.models'; import { OAuth2ClientInfo } from '@shared/models/oauth2.models';
@Component({ @Component({
selector: 'tb-login', selector: 'tb-login',
@ -36,7 +36,7 @@ export class LoginComponent extends PageComponent implements OnInit {
username: '', username: '',
password: '' password: ''
}); });
oauth2Clients: Array<OAuth2Client> = null; oauth2Clients: Array<OAuth2ClientInfo> = null;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private authService: AuthService, private authService: AuthService,

6
ui-ngx/src/app/shared/models/login.models.ts

@ -27,9 +27,3 @@ export interface LoginResponse {
token: string; token: string;
refreshToken: string; refreshToken: string;
} }
export interface OAuth2Client {
name: string;
icon?: string;
url: string;
}

119
ui-ngx/src/app/shared/models/oauth2.models.ts

@ -0,0 +1,119 @@
///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { HasUUID } from '@shared/models/id/has-uuid';
export interface OAuth2ClientsParams {
enabled: boolean;
domainsParams: OAuth2ClientsDomainParams[];
}
export interface OAuth2ClientsDomainParams {
clientRegistrations: ClientRegistration[];
domainInfos: DomainInfo[];
}
export interface DomainInfo {
name: string;
scheme: DomainSchema;
}
export enum DomainSchema{
HTTP = 'HTTP',
HTTPS = 'HTTPS',
MIXED = 'MIXED'
}
export const domainSchemaTranslations = new Map<DomainSchema, string>(
[
[DomainSchema.HTTP, 'admin.oauth2.domain-schema-http'],
[DomainSchema.HTTPS, 'admin.oauth2.domain-schema-https'],
[DomainSchema.MIXED, 'admin.oauth2.domain-schema-mixed']
]
);
export enum MapperConfigType{
BASIC = 'BASIC',
CUSTOM = 'CUSTOM',
GITHUB = 'GITHUB'
}
export enum TenantNameStrategy{
DOMAIN = 'DOMAIN',
EMAIL = 'EMAIL',
CUSTOM = 'CUSTOM'
}
export interface OAuth2ClientRegistrationTemplate extends ClientRegistration{
comment: string;
createdTime: number;
helpLink: string;
name: string;
providerId: string;
id: HasUUID;
}
export interface ClientRegistration {
loginButtonLabel: string;
loginButtonIcon: string;
clientId: string;
clientSecret: string;
accessTokenUri: string;
authorizationUri: string;
scope: string[];
jwkSetUri?: string;
userInfoUri: string;
clientAuthenticationMethod: ClientAuthenticationMethod;
userNameAttributeName: string;
mapperConfig: MapperConfig;
additionalInfo: string;
}
export enum ClientAuthenticationMethod {
BASIC = 'BASIC',
POST = 'POST'
}
export interface MapperConfig {
allowUserCreation: boolean;
activateUser: boolean;
type: MapperConfigType;
basic?: MapperConfigBasic;
custom?: MapperConfigCustom;
}
export interface MapperConfigBasic {
emailAttributeKey: string;
firstNameAttributeKey?: string;
lastNameAttributeKey?: string;
tenantNameStrategy: TenantNameStrategy;
tenantNamePattern?: string;
customerNamePattern?: string;
defaultDashboardName?: string;
alwaysFullScreen?: boolean;
}
export interface MapperConfigCustom {
url: string;
username?: string;
password?: string;
}
export interface OAuth2ClientInfo {
name: string;
icon?: string;
url: string;
}

1
ui-ngx/src/app/shared/models/public-api.ts

@ -36,6 +36,7 @@ export * from './error.models';
export * from './event.models'; export * from './event.models';
export * from './login.models'; export * from './login.models';
export * from './material.models'; export * from './material.models';
export * from './oauth2.models';
export * from './queue.models'; export * from './queue.models';
export * from './relation.models'; export * from './relation.models';
export * from './rule-chain.models'; export * from './rule-chain.models';

99
ui-ngx/src/app/shared/models/settings.models.ts

@ -14,9 +14,6 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { EntityId } from '@shared/models/id/entity-id';
import { TenantId } from '@shared/models/id/tenant-id';
export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/; export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;
export interface AdminSettings<T> { export interface AdminSettings<T> {
@ -63,99 +60,3 @@ export interface UpdateMessage {
message: string; message: string;
updateAvailable: boolean; updateAvailable: boolean;
} }
export interface OAuth2Settings {
enabled: boolean;
domainsParams: DomainsParam[];
}
export interface DomainsParam {
clientRegistrations: ClientRegistration[];
domainInfos: DomainInfo[];
}
export interface DomainInfo {
name: string;
scheme: DomainSchema;
}
export enum DomainSchema{
HTTP = 'HTTP',
HTTPS = 'HTTPS',
MIXED = 'MIXED'
}
export const domainSchemaTranslations = new Map<DomainSchema, string>(
[
[DomainSchema.HTTP, 'admin.oauth2.domain-schema-http'],
[DomainSchema.HTTPS, 'admin.oauth2.domain-schema-https'],
[DomainSchema.MIXED, 'admin.oauth2.domain-schema-mixed']
]
);
export enum MapperConfigType{
BASIC = 'BASIC',
CUSTOM = 'CUSTOM'
}
export enum TenantNameStrategy{
DOMAIN = 'DOMAIN',
EMAIL = 'EMAIL',
CUSTOM = 'CUSTOM'
}
export interface ClientProviderTemplated extends ClientRegistration{
comment: string;
createdTime: number;
helpLink: string;
name: string;
providerId: string;
tenantId: TenantId;
}
export interface ClientRegistration {
loginButtonLabel: string;
loginButtonIcon: string;
clientId: string;
clientSecret: string;
accessTokenUri: string;
authorizationUri: string;
scope: string[];
jwkSetUri?: string;
userInfoUri: string;
clientAuthenticationMethod: ClientAuthenticationMethod;
userNameAttributeName: string;
mapperConfig: MapperConfig;
id?: EntityId;
additionalInfo: string;
}
export enum ClientAuthenticationMethod {
BASIC = 'BASIC',
POST = 'POST'
}
export interface MapperConfig {
allowUserCreation: boolean;
activateUser: boolean;
type: MapperConfigType;
basic?: MapperConfigBasic;
custom?: MapperConfigCustom;
}
export interface MapperConfigBasic {
emailAttributeKey: string;
firstNameAttributeKey?: string;
lastNameAttributeKey?: string;
tenantNameStrategy: TenantNameStrategy;
tenantNamePattern?: string;
customerNamePattern?: string;
defaultDashboardName?: string;
alwaysFullScreen?: boolean;
}
export interface MapperConfigCustom {
url: string;
username?: string;
password?: string;
}

Loading…
Cancel
Save