Browse Source

Add system admin OAuth2 settings

pull/3557/head
Vladyslav_Prykhodko 6 years ago
parent
commit
7af3fe0050
  1. 18
      ui-ngx/src/app/core/http/admin.service.ts
  2. 8
      ui-ngx/src/app/core/services/menu.service.ts
  3. 14
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  4. 4
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  5. 364
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html
  6. 28
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss
  7. 284
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts
  8. 3
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html
  9. 11
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts
  10. 3
      ui-ngx/src/app/shared/models/constants.ts
  11. 56
      ui-ngx/src/app/shared/models/settings.models.ts
  12. 61
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -18,7 +18,13 @@ import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models';
import {
AdminSettings,
MailServerSettings,
OAuth2Settings,
SecuritySettings,
UpdateMessage
} from '@shared/models/settings.models';
@Injectable({
providedIn: 'root'
@ -53,6 +59,16 @@ export class AdminService {
defaultHttpOptionsFromConfig(config));
}
public getOAuth2Settings(config?: RequestConfig): Observable<OAuth2Settings> {
return this.http.get<OAuth2Settings>(`/api/oauth2/config`, 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> {
return this.http.get<UpdateMessage>(`/api/admin/updates`, defaultHttpOptionsFromConfig(config));
}

8
ui-ngx/src/app/core/services/menu.service.ts

@ -95,7 +95,7 @@ export class MenuService {
name: 'admin.system-settings',
type: 'toggle',
path: '/settings',
height: '120px',
height: '160px',
icon: 'settings',
pages: [
{
@ -115,6 +115,12 @@ export class MenuService {
type: 'link',
path: '/settings/security-settings',
icon: 'security'
},
{
name: 'admin.oauth2.settings',
type: 'link',
path: '/settings/oauth2-settings',
icon: 'security'
}
]
}

14
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -22,6 +22,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { Authority } from '@shared/models/authority.enum';
import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component';
const routes: Routes = [
{
@ -77,6 +78,19 @@ const routes: Routes = [
icon: 'security'
}
}
},
{
path: 'oauth2-settings',
component: OAuth2SettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN],
title: 'admin.oauth2.settings',
breadcrumb: {
label: 'admin.oauth2.settings',
icon: 'security'
}
}
}
]
}

4
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -23,13 +23,15 @@ import { MailServerComponent } from '@modules/home/pages/admin/mail-server.compo
import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
@NgModule({
declarations:
[
GeneralSettingsComponent,
MailServerComponent,
SecuritySettingsComponent
SecuritySettingsComponent,
OAuth2SettingsComponent
],
imports: [
CommonModule,

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

@ -0,0 +1,364 @@
<!--
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.
-->
<div>
<mat-card class="settings-card">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.oauth2.settings</span>
<span fxFlex></span>
<div tb-help="oauth2Settings"></div>
</div>
</mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form [formGroup]="oauth2SettingsForm" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<ng-container formArrayName="clientsDomainsParams">
<div class="container">
<mat-accordion multi>
<ng-container *ngFor="let domain of clientsDomainsParams.controls; let i = index; let last = last;">
<mat-expansion-panel [formGroupName]="i" [expanded]="last">
<mat-expansion-panel-header>
<mat-panel-title fxLayoutAlign="start center">
{{ domain.get('domainName').value ? domain.get('domainName').value : ("admin.new-domain" | translate) }}
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center">
<button mat-icon-button (click)="deleteDomain($event, i)">
<mat-icon>delete</mat-icon>
</button>
</mat-panel-description>
</mat-expansion-panel-header>
<mat-form-field class="mat-block">
<mat-label translate>admin.domain-name</mat-label>
<input matInput formControlName="domainName" required>
<mat-error *ngIf="domain.get('domainName').hasError('pattern')">
{{ 'admin.error-verification-url' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.redirect-uri-template</mat-label>
<input matInput formControlName="redirectUriTemplate" required>
<mat-error
*ngIf="domain.get('redirectUriTemplate').hasError('required')">
{{ 'admin.oauth2.redirect-uri-template-required' | translate }}
</mat-error>
<mat-error
*ngIf="domain.get('redirectUriTemplate').hasError('pattern')">
{{ 'admin.oauth2.uri-pattern-error' | translate }}
</mat-error>
</mat-form-field>
<ng-container formArrayName="clientRegistrations">
<div class="container">
<mat-card *ngFor="let registration of clientDomainRegistrations(domain).controls; let j = index;"
class="registration-card">
<section [formGroupName]="j">
<div fxLayoutAlign="end center">
<button mat-icon-button (click)="deleteRegistration($event, domain, j)">
<mat-icon>delete</mat-icon>
</button>
</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.registration-id</mat-label>
<input matInput formControlName="registrationId" required>
<mat-error *ngIf="registration.get('registrationId').hasError('required')">
{{ 'admin.oauth2.registration-id-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.client-name</mat-label>
<input matInput formControlName="clientName" required>
<mat-error *ngIf="registration.get('clientName').hasError('required')">
{{ 'admin.oauth2.client-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.client-id</mat-label>
<input matInput formControlName="clientId" required>
<mat-error *ngIf="registration.get('clientId').hasError('required')">
{{ 'admin.oauth2.client-id-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.client-secret</mat-label>
<input matInput formControlName="clientSecret" required>
<mat-error *ngIf="registration.get('clientSecret').hasError('required')">
{{ 'admin.oauth2.client-secret-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.access-token-uri</mat-label>
<input matInput formControlName="accessTokenUri" required>
<mat-error *ngIf="registration.get('accessTokenUri').hasError('required')">
{{ 'admin.oauth2.access-token-uri-required' | translate }}
</mat-error>
<mat-error *ngIf="registration.get('accessTokenUri').hasError('pattern')">
{{ 'admin.oauth2.uri-pattern-error' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.authorization-uri</mat-label>
<input matInput formControlName="authorizationUri" required>
<mat-error *ngIf="registration.get('authorizationUri').hasError('required')">
{{ 'admin.oauth2.authorization-uri-required' | translate }}
</mat-error>
<mat-error *ngIf="registration.get('authorizationUri').hasError('pattern')">
{{ 'admin.oauth2.uri-pattern-error' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.scope</mat-label>
<mat-chip-list #scopeList>
<mat-chip
*ngFor="let scope of registration.get('scope').value; let k = index;"
removable
(removed)="removeScope(k, registration)">
{{scope}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input [matChipInputFor]="scopeList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addScope($event, registration)">
</mat-chip-list>
</mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.jwk-set-uri</mat-label>
<input matInput formControlName="jwkSetUri" required>
<mat-error *ngIf="registration.get('jwkSetUri').hasError('required')">
{{ 'admin.oauth2.jwk-set-uri-required' | translate }}
</mat-error>
<mat-error *ngIf="registration.get('jwkSetUri').hasError('pattern')">
{{ 'admin.oauth2.uri-pattern-error' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.user-info-uri</mat-label>
<input matInput formControlName="userInfoUri" required>
<mat-error *ngIf="registration.get('userInfoUri').hasError('required')">
{{ 'admin.oauth2.user-info-uri-required' | translate }}
</mat-error>
<mat-error *ngIf="registration.get('userInfoUri').hasError('pattern')">
{{ 'admin.oauth2.uri-pattern-error' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.client-authentication-method</mat-label>
<mat-select formControlName="clientAuthenticationMethod">
<mat-option *ngFor="let clientAuthenticationMethod of clientAuthenticationMethods"
[value]="clientAuthenticationMethod">
{{ clientAuthenticationMethod | uppercase }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.oauth2.user-name-attribute-name</mat-label>
<input matInput formControlName="userNameAttributeName" required>
<mat-error *ngIf="registration.get('userNameAttributeName').hasError('required')">
{{ 'admin.oauth2.user-name-attribute-name-required' | translate }}
</mat-error>
</mat-form-field>
<section formGroupName="mapperConfig">
<div fxLayout="column" fxLayoutGap="8px">
<mat-checkbox formControlName="allowUserCreation">
{{ 'admin.oauth2.allow-user-creation' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="activateUser">
{{ 'admin.oauth2.activate-user' | translate }}
</mat-checkbox>
</div>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.type</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let converterTypeExternalUser of converterTypesExternalUser"
[value]="converterTypeExternalUser">
{{ converterTypeExternalUser }}
</mat-option>
</mat-select>
</mat-form-field>
<section formGroupName="basic"
*ngIf="registration.get('mapperConfig.type').value === 'BASIC'">
<mat-form-field class="mat-block">
<mat-label translate>admin.oauth2.email-attribute-key</mat-label>
<input matInput formControlName="emailAttributeKey" required>
<mat-error
*ngIf="registration.get('mapperConfig.basic.emailAttributeKey').hasError('required')">
{{ 'admin.oauth2.email-attribute-key-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.first-name-attribute-key</mat-label>
<input matInput formControlName="firstNameAttributeKey">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.last-name-attribute-key</mat-label>
<input matInput formControlName="lastNameAttributeKey">
</mat-form-field>
</div>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.tenant-name-strategy</mat-label>
<mat-select formControlName="tenantNameStrategy">
<mat-option *ngFor="let tenantNameStrategy of tenantNameStrategies"
[value]="tenantNameStrategy">
{{ tenantNameStrategy }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.tenant-name-pattern</mat-label>
<input matInput
formControlName="tenantNamePattern"
[required]="registration.get('mapperConfig.basic.tenantNameStrategy').value === 'CUSTOM'">
<mat-error
*ngIf="registration.get('mapperConfig.basic.tenantNamePattern').hasError('required')">
{{ 'admin.oauth2.tenant-name-pattern-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.customer-name-pattern</mat-label>
<input matInput formControlName="customerNamePattern">
</mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.default-dashboard-name</mat-label>
<input matInput formControlName="defaultDashboardName">
</mat-form-field>
<mat-checkbox fxFlex formControlName="alwaysFullScreen" class="checkbox-row">
{{ 'admin.oauth2.always-fullscreen' | translate}}
</mat-checkbox>
</div>
</section>
<section formGroupName="custom"
*ngIf="registration.get('mapperConfig.type').value === 'CUSTOM'">
<mat-form-field class="mat-block">
<mat-label translate>admin.oauth2.url</mat-label>
<input matInput formControlName="url" required>
<mat-error
*ngIf="registration.get('mapperConfig.custom.url').hasError('required')">
{{ 'admin.oauth2.url-required' | translate }}
</mat-error>
<mat-error
*ngIf="registration.get('mapperConfig.custom.url').hasError('pattern')">
{{ 'admin.oauth2.url-pattern' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>common.username</mat-label>
<input matInput formControlName="username">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>common.password</mat-label>
<input matInput formControlName="password">
</mat-form-field>
</div>
</section>
</section>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.login-button-label</mat-label>
<input matInput formControlName="loginButtonLabel" required>
<mat-error
*ngIf="registration.get('loginButtonLabel').hasError('required')">
{{ 'admin.oauth2.login-button-label-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.login-button-icon</mat-label>
<input matInput formControlName="loginButtonIcon">
</mat-form-field>
</div>
</section>
</mat-card>
</div>
</ng-container>
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="addRegistration(domain)"
type="button">
{{'admin.add-registration' | translate}}
</button>
</div>
</mat-expansion-panel>
</ng-container>
</mat-accordion>
</div>
</ng-container>
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="addDomain()"
type="button">
{{'admin.add-domain' | translate}}
</button>
<button mat-button mat-raised-button color="primary"
[disabled]="(isLoading$ | async) || oauth2SettingsForm.invalid || !oauth2SettingsForm.dirty"
type="submit">
{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

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

@ -0,0 +1,28 @@
/**
* 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.
*/
:host{
.checkbox-row {
margin-top: 16px;
}
.registration-card{
margin-bottom: 8px;
}
.container{
margin-bottom: 16px;
}
}

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

@ -0,0 +1,284 @@
///
/// 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 { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ClientRegistration, DomainParams, OAuth2Settings } from '@shared/models/settings.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AdminService } from '@core/http/admin.service';
import { PageComponent } from '@shared/components/page.component';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
import { WINDOW } from '@core/services/window.service';
import { Subscription } from 'rxjs';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-oauth2-settings',
templateUrl: './oauth2-settings.component.html',
styleUrls: ['./oauth2-settings.component.scss', './settings-card.scss']
})
export class OAuth2SettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/;
private subscriptions: Subscription[] = [];
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
oauth2SettingsForm: FormGroup;
oauth2Settings: OAuth2Settings;
clientAuthenticationMethods = ['basic', 'post'];
converterTypesExternalUser = ['BASIC', 'CUSTOM'];
tenantNameStrategies = ['DOMAIN', 'EMAIL', 'CUSTOM'];
constructor(protected store: Store<AppState>,
private adminService: AdminService,
private fb: FormBuilder,
private dialogService: DialogService,
private translate: TranslateService,
@Inject(WINDOW) private window: Window) {
super(store);
}
ngOnInit(): void {
this.buildOAuth2SettingsForm();
this.adminService.getOAuth2Settings().subscribe(
(oauth2Settings) => {
this.oauth2Settings = oauth2Settings;
this.initOAuth2Settings(this.oauth2Settings);
this.oauth2SettingsForm.reset(this.oauth2Settings);
}
);
}
ngOnDestroy() {
super.ngOnDestroy();
this.subscriptions.forEach((subscription) => {
subscription.unsubscribe();
})
}
get clientsDomainsParams(): FormArray {
return this.oauth2SettingsForm.get('clientsDomainsParams') as FormArray;
}
private get formBasicGroup(): FormGroup {
const basicGroup = this.fb.group({
emailAttributeKey: ['email', [Validators.required]],
firstNameAttributeKey: [''],
lastNameAttributeKey: [''],
tenantNameStrategy: ['DOMAIN'],
tenantNamePattern: [null],
customerNamePattern: [null],
defaultDashboardName: [null],
alwaysFullScreen: [false],
});
this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => {
if (domain === 'CUSTOM') {
basicGroup.get('tenantNamePattern').setValidators(Validators.required);
} else {
basicGroup.get('tenantNamePattern').clearValidators();
}
}));
return basicGroup;
}
get formCustomGroup(): FormGroup {
return this.fb.group({
url: [null, [Validators.required, Validators.pattern(this.URL_REGEXP)]],
username: [null],
password: [null]
})
}
private buildOAuth2SettingsForm(): void {
this.oauth2SettingsForm = this.fb.group({
clientsDomainsParams: this.fb.array([])
});
}
private initOAuth2Settings(oauth2Settings: OAuth2Settings): void {
oauth2Settings.clientsDomainsParams.forEach((domaindomain) => {
this.clientsDomainsParams.push(this.buildSettingsDomain(domaindomain));
});
}
private buildSettingsDomain(domainParams?: DomainParams): FormGroup {
let url = this.window.location.protocol + '//' + this.window.location.hostname;
const port = this.window.location.port;
if (port !== '80' && port !== '443') {
url += ':' + port;
}
url += '/login/oauth2/code/';
const formDomain = this.fb.group({
domainName: ['', [Validators.required, Validators.pattern('((?![:/]).)*$')]],
redirectUriTemplate: [url, [Validators.required, Validators.pattern(this.URL_REGEXP)]],
clientRegistrations: this.fb.array([])
});
this.subscriptions.push(formDomain.get('domainName').valueChanges.subscribe((domain) => {
if (!domain) {
domain = this.window.location.hostname
}
const uri = this.window.location.protocol + `//${domain}/login/oauth2/code/`;
formDomain.get('redirectUriTemplate').patchValue(uri);
}));
if(domainParams){
domainParams.clientRegistrations.forEach((registration) => {
this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration(registration));
})
} else {
this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration());
}
return formDomain;
}
private buildSettingsRegistration(registrationData?: ClientRegistration): FormGroup {
const clientRegistration = this.fb.group({
registrationId: [null, [Validators.required]],
clientName: [null, [Validators.required]],
loginButtonLabel: [null, [Validators.required]],
loginButtonIcon: [null],
clientId: ['', [Validators.required]],
clientSecret: ['', [Validators.required]],
accessTokenUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]],
authorizationUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]],
scope: this.fb.array([], [Validators.required]),
jwkSetUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]],
userInfoUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]],
clientAuthenticationMethod: ['post', [Validators.required]],
userNameAttributeName: ['email', [Validators.required]],
mapperConfig: this.fb.group({
allowUserCreation: [true],
activateUser: [false],
type: ['BASIC', [Validators.required]],
basic: this.formBasicGroup
}
)
});
this.subscriptions.push(clientRegistration.get('mapperConfig.type').valueChanges.subscribe((value) => {
const mapperConfig = clientRegistration.get('mapperConfig') as FormGroup;
if (value === 'BASIC') {
mapperConfig.removeControl('custom');
mapperConfig.addControl('basic', this.formBasicGroup);
} else {
mapperConfig.removeControl('basic');
mapperConfig.addControl('custom', this.formCustomGroup);
}
}));
if(registrationData){
registrationData.scope.forEach(() => {
(clientRegistration.get('scope') as FormArray).push(this.fb.control(''))
})
if(registrationData.mapperConfig.type !== 'BASIC'){
clientRegistration.get('mapperConfig.type').patchValue('CUSTOM');
}
}
return clientRegistration;
}
save(): void {
console.log(this.oauth2SettingsForm.value);
this.adminService.saveOAuth2Settings(this.oauth2SettingsForm.value).subscribe(
(oauth2Settings) => {
this.oauth2Settings = oauth2Settings;
this.oauth2SettingsForm.markAsPristine();
this.oauth2SettingsForm.markAsUntouched();
}
);
}
confirmForm(): FormGroup {
return this.oauth2SettingsForm;
}
addScope(event: MatChipInputEvent, control: AbstractControl): void {
const input = event.input;
const value = event.value;
const controller = control.get('scope') as FormArray;
if ((value.trim() !== '')) {
controller.push(this.fb.control(value.trim()));
}
if (input) {
input.value = '';
}
}
removeScope(i: number, control: AbstractControl): void {
const controller = control.get('scope') as FormArray;
controller.removeAt(i);
}
addDomain(): void {
this.clientsDomainsParams.push(this.buildSettingsDomain());
}
deleteDomain($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
$event.preventDefault();
}
const domainName = this.clientsDomainsParams.at(index).get('domainName').value;
this.dialogService.confirm(
this.translate.instant('admin.oauth2.delete-domain-title', {domainName}),
this.translate.instant('admin.oauth2.delete-domain-text'), null,
this.translate.instant('action.delete')
).subscribe((data) => {
if (data) {
this.clientsDomainsParams.removeAt(index);
}
})
}
clientDomainRegistrations(control: AbstractControl): FormArray {
return control.get('clientRegistrations') as FormArray;
}
addRegistration(control: AbstractControl): void {
this.clientDomainRegistrations(control).push(this.buildSettingsRegistration());
}
deleteRegistration($event: Event, controler: AbstractControl, index: number): void {
if ($event) {
$event.stopPropagation();
$event.preventDefault();
}
const registrationId = this.clientDomainRegistrations(controler).at(index).get('registrationId').value;
this.dialogService.confirm(
this.translate.instant('admin.oauth2.delete-registration-title', {name: registrationId}),
this.translate.instant('admin.oauth2.delete-registration-text'), null,
this.translate.instant('action.delete')
).subscribe((data) => {
if (data) {
this.clientDomainRegistrations(controler).removeAt(index);
}
})
}
}

3
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html

@ -54,6 +54,9 @@
<mat-label translate>tenant.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
<mat-checkbox fxFlex formControlName="allowOAuth2Configuration" style="padding-bottom: 16px;">
{{ 'tenant.allow-oauth2-configuration' | translate }}
</mat-checkbox>
</div>
<tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
<div fxLayout="column">

11
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts

@ -23,6 +23,7 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
import { TranslateService } from '@ngx-translate/core';
import { ContactBasedComponent } from '../../components/entity/contact-based.component';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { isDefined } from '@core/utils';
@Component({
selector: 'tb-tenant',
@ -55,7 +56,9 @@ export class TenantComponent extends ContactBasedComponent<Tenant> {
isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []],
additionalInfo: this.fb.group(
{
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : '']
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
allowOAuth2Configuration: [isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ?
entity.additionalInfo.allowOAuth2Configuration : true]
}
)
}
@ -66,7 +69,11 @@ export class TenantComponent extends ContactBasedComponent<Tenant> {
this.entityForm.patchValue({title: entity.title});
this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore});
this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine});
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
this.entityForm.patchValue({additionalInfo: {
description: entity.additionalInfo ? entity.additionalInfo.description : '',
allowOAuth2Configuration: isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ?
entity.additionalInfo.allowOAuth2Configuration : true
}});
}
updateFormState() {

3
ui-ngx/src/app/shared/models/constants.ts

@ -58,6 +58,7 @@ export const HelpLinks = {
linksMap: {
outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings',
securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/',
ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/',
ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node',
ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node',
@ -108,7 +109,7 @@ export const HelpLinks = {
widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest',
widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc',
widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm',
widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static'
widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static',
}
};

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

@ -23,6 +23,10 @@ export interface AdminSettings<T> {
export declare type SmtpProtocol = 'smtp' | 'smtps';
export declare type ClientAuthenticationMethod = 'basic' | 'post';
export declare type MapperConfigType = 'BASIC' | 'CUSTOM';
export declare type TenantNameStrategy = 'DOMAIN' | 'EMAIL' | 'CUSTOM';
export interface MailServerSettings {
mailFrom: string;
smtpProtocol: SmtpProtocol;
@ -60,3 +64,55 @@ export interface UpdateMessage {
message: string;
updateAvailable: boolean;
}
export interface OAuth2Settings {
clientsDomainsParams: DomainParams[];
}
export interface DomainParams {
domainName: string;
redirectUriTemplate: string;
clientRegistrations: ClientRegistration[];
}
export interface ClientRegistration {
registrationId: string;
clientName: string;
loginButtonLabel: string;
loginButtonIcon: string;
clientId: string;
clientSecret: string;
accessTokenUri: string;
authorizationUri: string;
scope: string[];
jwkSetUri: string;
userInfoUri: string;
clientAuthenticationMethod: ClientAuthenticationMethod
userNameAttributeName: string;
mapperConfig: MapperConfig
}
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;
}

61
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -119,8 +119,62 @@
"general-policy": "General policy",
"max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked",
"minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative",
"user-lockout-notification-email": "In case user account lockout, send notification to email"
},
"user-lockout-notification-email": "In case user account lockout, send notification to email",
"domain-name": "Domain name",
"error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io",
"add-domain": "Add domain",
"new-domain": "New domain",
"add-registration": "Add registration",
"oauth2": {
"settings": "OAuth2 settings",
"registration-id": "Registration ID",
"registration-id-required": "Registration ID is required.",
"client-name": "Client name",
"client-name-required": "Client name is required.",
"client-id": "Client ID",
"client-id-required": "Client ID is required.",
"client-secret": "Client secret",
"client-secret-required": "Client secret is required.",
"access-token-uri": "Access token URI",
"access-token-uri-required": "Access token URI is required.",
"authorization-uri": "Authorization URI",
"authorization-uri-required": "Authorization URI is required.",
"uri-pattern-error": "Invalid URI format.",
"scope": "Scope",
"redirect-uri-template": "Redirect URI template",
"redirect-uri-template-required": "Redirect URI template is required.",
"jwk-set-uri": "JSON Web Key URI",
"jwk-set-uri-required": "JSON Web Key URI is required.",
"user-info-uri": "User info URI",
"user-info-uri-required": "User info URI is required.",
"client-authentication-method": "Client authentication method",
"user-name-attribute-name": "User name attribute key",
"user-name-attribute-name-required": "User name attribute key is required",
"allow-user-creation": "Allow user creation",
"activate-user": "Activate user",
"type": "Mapper type",
"email-attribute-key": "Email attribute key",
"email-attribute-key-required": "Email attribute key is required.",
"first-name-attribute-key": "First name attribute key",
"last-name-attribute-key": "Last name attribute key",
"tenant-name-strategy": "Tenant name strategy",
"tenant-name-pattern": "Tenant name pattern",
"tenant-name-pattern-required": "Tenant name pattern is required.",
"customer-name-pattern": "Customer name pattern",
"default-dashboard-name": "Default dashboard name",
"always-fullscreen": "Always fullscreen",
"url": "URL",
"url-required": "URL is required.",
"url-pattern": "Invalid URL format.",
"login-button-label": "Login button label",
"login-button-label-required": "Login button label is required.",
"login-button-icon": "Login button icon",
"delete-domain-title": "Are you sure you want to delete the domain '{{domainName}}'?",
"delete-domain-text": "Be careful, after the confirmation a domain and all registration data will be unavailable.",
"delete-registration-title": "Are you sure you want to delete the registration '{{name}}'?",
"delete-registration-text": "Be careful, after the confirmation a registration data will be unavailable."
}
},
"alarm": {
"alarm": "Alarm",
"alarms": "Alarms",
@ -1561,7 +1615,8 @@
"isolated-tb-core": "Processing in isolated ThingsBoard Core container",
"isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container",
"isolated-tb-core-details": "Requires separate microservice(s) per isolated Tenant",
"isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant"
"isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant",
"allow-oauth2-configuration": "Allow OAuth2 configuration"
},
"timeinterval": {
"seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",

Loading…
Cancel
Save