Browse Source

Refactoring layout oauth2 templated

pull/3557/head
Vladyslav_Prykhodko 6 years ago
parent
commit
edcaebe9ac
  1. 6
      ui-ngx/src/app/core/http/admin.service.ts
  2. 4
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  3. 49
      ui-ngx/src/app/modules/home/pages/admin/edit-redirect-uri-panel.component.html
  4. 18
      ui-ngx/src/app/modules/home/pages/admin/edit-redirect-uri-panel.component.scss
  5. 76
      ui-ngx/src/app/modules/home/pages/admin/edit-redirect-uri-panel.component.ts
  6. 570
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html
  7. 1
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss
  8. 112
      ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts
  9. 5
      ui-ngx/src/app/shared/models/settings.models.ts
  10. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -63,8 +63,12 @@ export class AdminService {
return this.http.get<OAuth2Settings>(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config)); return this.http.get<OAuth2Settings>(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config));
} }
public getOAuth2Template(config?: RequestConfig): Observable<any> {
return this.http.get<any>(`/api/oauth2/config/template`, defaultHttpOptionsFromConfig(config));
}
public saveOAuth2Settings(OAuth2Setting: OAuth2Settings, public saveOAuth2Settings(OAuth2Setting: OAuth2Settings,
config?: RequestConfig): Observable<OAuth2Settings> { config?: RequestConfig): Observable<OAuth2Settings> {
return this.http.post<OAuth2Settings>('/api/oauth2/config', OAuth2Setting, return this.http.post<OAuth2Settings>('/api/oauth2/config', OAuth2Setting,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }

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

@ -24,6 +24,7 @@ import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-sett
import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component'; import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
import { EditRedirectUriPanelComponent } from './edit-redirect-uri-panel.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -31,7 +32,8 @@ import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settin
GeneralSettingsComponent, GeneralSettingsComponent,
MailServerComponent, MailServerComponent,
SecuritySettingsComponent, SecuritySettingsComponent,
OAuth2SettingsComponent OAuth2SettingsComponent,
EditRedirectUriPanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

49
ui-ngx/src/app/modules/home/pages/admin/edit-redirect-uri-panel.component.html

@ -0,0 +1,49 @@
<!--
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.
-->
<form class="mat-elevation-z4"
[formGroup]="redirectURIFormGroup" (ngSubmit)="update()" style="width: 350px; padding: 12px 8px 0;">
<fieldset [disabled]="isLoading$ | async">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.redirect-uri-template</mat-label>
<input matInput formControlName="value" required>
<mat-error
*ngIf="redirectURIFormGroup.get('value').hasError('required')">
{{ 'admin.oauth2.redirect-uri-template-required' | translate }}
</mat-error>
<mat-error
*ngIf="redirectURIFormGroup.get('value').hasError('pattern')">
{{ 'admin.oauth2.uri-pattern-error' | translate }}
</mat-error>
</mat-form-field>
</fieldset>
<div fxLayout="row" class="tb-panel-actions">
<span fxFlex></span>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || redirectURIFormGroup.invalid || !redirectURIFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>

18
ui-ngx/src/app/modules/home/pages/admin/edit-redirect-uri-panel.component.scss

@ -0,0 +1,18 @@
/**
* 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 {
background-color: #f9f9f9;
}

76
ui-ngx/src/app/modules/home/pages/admin/edit-redirect-uri-panel.component.ts

@ -0,0 +1,76 @@
///
/// 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, InjectionToken, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { PageComponent } from '@shared/components/page.component';
import { OverlayRef } from '@angular/cdk/overlay';
export const EDIT_REDIRECT_URI_PANEL_DATA = new InjectionToken<any>('EditRedirectUriPanelData');
export interface EditRedirectUriPanelData {
redirectURI: any;
}
@Component({
selector: 'tb-edit-redirect-uri-panel',
templateUrl: './edit-redirect-uri-panel.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: EditRedirectUriPanelComponent}],
styleUrls: ['./edit-redirect-uri-panel.component.scss']
})
export class EditRedirectUriPanelComponent extends PageComponent implements OnInit, ErrorStateMatcher {
private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/;
redirectURIFormGroup: FormGroup;
result: any = null;
submitted = false;
constructor(protected store: Store<AppState>,
@Inject(EDIT_REDIRECT_URI_PANEL_DATA) public data: EditRedirectUriPanelData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public overlayRef: OverlayRef,
public fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.redirectURIFormGroup = this.fb.group({
value: [this.data.redirectURI, [Validators.required, Validators.pattern(this.URL_REGEXP)]]
});
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void {
this.overlayRef.dispose();
}
update(): void {
this.submitted = true;
this.result = this.redirectURIFormGroup.get('value').value;
this.overlayRef.dispose();
}
}

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

@ -40,300 +40,316 @@
{{ domain.get('domainName').value ? domain.get('domainName').value : ("admin.new-domain" | translate) }} {{ domain.get('domainName').value ? domain.get('domainName').value : ("admin.new-domain" | translate) }}
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end center"> <mat-panel-description fxLayoutAlign="end center">
<button mat-icon-button (click)="deleteDomain($event, i)"> <button mat-icon-button
<mat-icon>delete</mat-icon> type="button"
</button> (click)="editRedirectURI($event, i)"
matTooltip="{{ 'admin.oauth2.redirect-uri-template' | translate }}"
matTooltipPosition="above">
<mat-icon>link</mat-icon>
</button>
<button mat-icon-button
type="button"
(click)="deleteDomain($event, i)"
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<mat-form-field class="mat-block"> <ng-template matExpansionPanelContent>
<mat-label translate>admin.domain-name</mat-label> <mat-form-field class="mat-block">
<input matInput formControlName="domainName" required> <mat-label translate>admin.domain-name</mat-label>
<mat-error *ngIf="domain.get('domainName').hasError('pattern')"> <input matInput formControlName="domainName" required>
{{ 'admin.error-verification-url' | translate }} <mat-error *ngIf="domain.get('domainName').hasError('pattern')">
</mat-error> {{ 'admin.error-verification-url' | translate }}
<mat-error *ngIf="domain.get('domainName').hasError('unique')"> </mat-error>
{{ 'admin.domain-name-unique' | translate }} <mat-error *ngIf="domain.get('domainName').hasError('unique')">
</mat-error> {{ 'admin.domain-name-unique' | translate }}
</mat-form-field> </mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.redirect-uri-template</mat-label> <ng-container formArrayName="clientRegistrations">
<input matInput formControlName="redirectUriTemplate" required> <div class="container">
<mat-error <mat-expansion-panel *ngFor="let registration of clientDomainRegistrations(domain).controls; let j = index;"
*ngIf="domain.get('redirectUriTemplate').hasError('required')"> class="registration-card mat-elevation-z0">
{{ 'admin.oauth2.redirect-uri-template-required' | translate }} <mat-expansion-panel-header>
</mat-error> <mat-panel-title fxLayoutAlign="start center">
<mat-error {{ registration.get('providerName').value }}
*ngIf="domain.get('redirectUriTemplate').hasError('pattern')"> </mat-panel-title>
{{ 'admin.oauth2.uri-pattern-error' | translate }} <mat-panel-description fxLayoutAlign="end center">
</mat-error> <button mat-icon-button
</mat-form-field> type="button"
(click)="deleteRegistration($event, domain, j)"
<ng-container formArrayName="clientRegistrations"> matTooltip="{{ 'action.delete' | translate }}"
<div class="container"> matTooltipPosition="above">
<mat-card *ngFor="let registration of clientDomainRegistrations(domain).controls; let j = index;" <mat-icon>delete</mat-icon>
class="registration-card"> </button>
<section [formGroupName]="j"> </mat-panel-description>
<div fxLayoutAlign="end center"> </mat-expansion-panel-header>
<button mat-icon-button (click)="deleteRegistration($event, domain, j)">
<mat-icon>delete</mat-icon> <ng-template matExpansionPanelContent>
</button> <section [formGroupName]="j">
</div> <mat-form-field fxFlex class="mat-block">
<mat-form-field fxFlex class="mat-block"> <mat-label translate>Login Provider</mat-label>
<mat-label translate>admin.oauth2.registration-id</mat-label> <mat-select formControlName="providerName">
<input matInput formControlName="registrationId" required> <mat-option *ngFor="let provider of templateProvider" [value]="provider">
<mat-error *ngIf="registration.get('registrationId').hasError('required')"> {{ provider | uppercase }}
{{ 'admin.oauth2.registration-id-required' | translate }} </mat-option>
</mat-error> </mat-select>
<mat-error *ngIf="registration.get('registrationId').hasError('unique')">
{{ 'admin.oauth2.registration-id-unique' | 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.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-error *ngIf="registration.get('scope').hasError('required')">
{{ 'admin.oauth2.jwk-set-uri-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.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> </mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.first-name-attribute-key</mat-label> <mat-label translate>admin.oauth2.client-id</mat-label>
<input matInput formControlName="firstNameAttributeKey"> <input matInput formControlName="clientId" required>
</mat-form-field> <mat-error *ngIf="registration.get('clientId').hasError('required')">
{{ 'admin.oauth2.client-id-required' | translate }}
<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-error>
</mat-form-field> </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-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.default-dashboard-name</mat-label> <mat-label translate>admin.oauth2.client-secret</mat-label>
<input matInput formControlName="defaultDashboardName"> <input matInput formControlName="clientSecret" required>
<mat-error *ngIf="registration.get('clientSecret').hasError('required')">
{{ 'admin.oauth2.client-secret-required' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-checkbox fxFlex formControlName="alwaysFullScreen" class="checkbox-row">
{{ 'admin.oauth2.always-fullscreen' | translate}}
</mat-checkbox>
</div> </div>
</section> <mat-tab-group dynamicHeight>
<mat-tab label="General">
<section formGroupName="custom" <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" style="margin-top: 16px;">
*ngIf="registration.get('mapperConfig.type').value === 'CUSTOM'"> <mat-form-field fxFlex class="mat-block">
<mat-form-field class="mat-block"> <mat-label translate>admin.oauth2.access-token-uri</mat-label>
<mat-label translate>admin.oauth2.url</mat-label> <input matInput formControlName="accessTokenUri" required>
<input matInput formControlName="url" required> <mat-error *ngIf="registration.get('accessTokenUri').hasError('required')">
<mat-error {{ 'admin.oauth2.access-token-uri-required' | translate }}
*ngIf="registration.get('mapperConfig.custom.url').hasError('required')"> </mat-error>
{{ 'admin.oauth2.url-required' | translate }} <mat-error *ngIf="registration.get('accessTokenUri').hasError('pattern')">
</mat-error> {{ 'admin.oauth2.uri-pattern-error' | translate }}
<mat-error </mat-error>
*ngIf="registration.get('mapperConfig.custom.url').hasError('pattern')"> </mat-form-field>
{{ 'admin.oauth2.url-pattern' | translate }}
</mat-error> <mat-form-field fxFlex class="mat-block">
</mat-form-field> <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>
<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>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" *ngIf="registration.get('providerName').value === 'Custom'">
<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>
<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-error *ngIf="registration.get('scope').hasError('required')">
{{ 'admin.oauth2.jwk-set-uri-required' | translate }}
</mat-error>
</mat-form-field>
</mat-tab>
<mat-tab label="Mapper">
<mat-form-field class="mat-block" style="margin-top: 16px;">
<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" *ngIf="registration.get('mapperConfig.basic.tenantNameStrategy').value === 'CUSTOM'">
<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" autocomplete="new-username">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>common.password</mat-label>
<input matInput type="password" formControlName="password" autocomplete="new-password">
</mat-form-field>
</div>
</section>
</section>
</mat-tab>
</mat-tab-group>
<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" autocomplete="new-username">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>common.password</mat-label>
<input matInput type="password" formControlName="password" autocomplete="new-password">
</mat-form-field>
</div>
</section> </section>
</section> </ng-template>
</mat-expansion-panel>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> </div>
<mat-form-field fxFlex class="mat-block"> </ng-container>
<mat-label translate>admin.oauth2.login-button-label</mat-label>
<input matInput formControlName="loginButtonLabel" required> <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px">
<mat-error <button mat-button mat-raised-button color="primary"
*ngIf="registration.get('loginButtonLabel').hasError('required')"> [disabled]="(isLoading$ | async)"
{{ 'admin.oauth2.login-button-label-required' | translate }} (click)="addRegistration(domain)"
</mat-error> type="button">
</mat-form-field> {{'admin.add-provider' | translate}}
</button>
<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> </div>
</ng-container> </ng-template>
<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> </mat-expansion-panel>
</ng-container> </ng-container>

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

@ -20,6 +20,7 @@
.registration-card{ .registration-card{
margin-bottom: 8px; margin-bottom: 8px;
border: 1px solid rgba(0, 0, 0, 0.2);
} }
.container{ .container{

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

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit, ViewContainerRef } 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,
@ -32,9 +32,16 @@ import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { 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 { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import {
EDIT_REDIRECT_URI_PANEL_DATA,
EditRedirectUriPanelComponent,
EditRedirectUriPanelData
} from './edit-redirect-uri-panel.component';
@Component({ @Component({
selector: 'tb-oauth2-settings', selector: 'tb-oauth2-settings',
@ -45,6 +52,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 = [];
readonly separatorKeysCodes: number[] = [ENTER, COMMA]; readonly separatorKeysCodes: number[] = [ENTER, COMMA];
@ -55,8 +63,13 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
converterTypesExternalUser: MapperConfigType[] = ['BASIC', 'CUSTOM']; converterTypesExternalUser: MapperConfigType[] = ['BASIC', 'CUSTOM'];
tenantNameStrategies: TenantNameStrategy[] = ['DOMAIN', 'EMAIL', 'CUSTOM']; tenantNameStrategies: TenantNameStrategy[] = ['DOMAIN', 'EMAIL', 'CUSTOM'];
templateProvider = ['Custom'];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private adminService: AdminService, private adminService: AdminService,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private fb: FormBuilder, private fb: FormBuilder,
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService, private translate: TranslateService,
@ -66,8 +79,12 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
ngOnInit(): void { ngOnInit(): void {
this.buildOAuth2SettingsForm(); this.buildOAuth2SettingsForm();
this.adminService.getOAuth2Settings().subscribe( forkJoin([
(oauth2Settings) => { this.adminService.getOAuth2Template(),
this.adminService.getOAuth2Settings()
]).subscribe(
([templates, oauth2Settings]) => {
this.initTemplates(templates);
this.oauth2Settings = oauth2Settings; this.oauth2Settings = oauth2Settings;
this.initOAuth2Settings(this.oauth2Settings); this.initOAuth2Settings(this.oauth2Settings);
this.oauth2SettingsForm.reset(this.oauth2Settings); this.oauth2SettingsForm.reset(this.oauth2Settings);
@ -79,7 +96,13 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
super.ngOnDestroy(); super.ngOnDestroy();
this.subscriptions.forEach((subscription) => { this.subscriptions.forEach((subscription) => {
subscription.unsubscribe(); subscription.unsubscribe();
}) });
}
private initTemplates(templates: any): void {
this.templates = templates;
templates.map(provider => this.templateProvider.push(provider.name));
this.templateProvider.sort();
} }
get clientsDomainsParams(): FormArray { get clientsDomainsParams(): FormArray {
@ -114,7 +137,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
url: [null, [Validators.required, Validators.pattern(this.URL_REGEXP)]], url: [null, [Validators.required, Validators.pattern(this.URL_REGEXP)]],
username: [null], username: [null],
password: [null] password: [null]
}) });
} }
private buildOAuth2SettingsForm(): void { private buildOAuth2SettingsForm(): void {
@ -136,7 +159,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
const listDomainName = []; const listDomainName = [];
control.root.value.clientsDomainsParams.forEach((domain) => { control.root.value.clientsDomainsParams.forEach((domain) => {
listDomainName.push(domain.domainName); listDomainName.push(domain.domainName);
}) });
if (listDomainName.indexOf(control.value) > -1) { if (listDomainName.indexOf(control.value) > -1) {
return {unique: true}; return {unique: true};
} }
@ -144,21 +167,6 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
return null; return null;
} }
private uniqueRegistrationIdValidator(control: AbstractControl): { [key: string]: boolean } | null {
if (control.value !== null && control?.root) {
const listRegistration = [];
control.root.value.clientsDomainsParams.forEach((domain) => {
domain.clientRegistrations.forEach((client) => {
listRegistration.push(client.registrationId);
})
})
if (listRegistration.indexOf(control.value) > -1) {
return {unique: true};
}
}
return null;
}
private buildSettingsDomain(domainParams?: DomainParams): FormGroup { private buildSettingsDomain(domainParams?: DomainParams): FormGroup {
let url = this.window.location.protocol + '//' + this.window.location.hostname; let url = this.window.location.protocol + '//' + this.window.location.hostname;
const port = this.window.location.port; const port = this.window.location.port;
@ -174,7 +182,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
this.subscriptions.push(formDomain.get('domainName').valueChanges.subscribe((domain) => { this.subscriptions.push(formDomain.get('domainName').valueChanges.subscribe((domain) => {
if (!domain) { if (!domain) {
domain = this.window.location.hostname domain = this.window.location.hostname;
} }
const uri = this.window.location.protocol + `//${domain}/login/oauth2/code/`; const uri = this.window.location.protocol + `//${domain}/login/oauth2/code/`;
formDomain.get('redirectUriTemplate').patchValue(uri); formDomain.get('redirectUriTemplate').patchValue(uri);
@ -183,7 +191,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
if (domainParams) { if (domainParams) {
domainParams.clientRegistrations.forEach((registration) => { domainParams.clientRegistrations.forEach((registration) => {
this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration(registration)); this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration(registration));
}) });
} else { } else {
this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration()); this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration());
} }
@ -193,7 +201,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
private buildSettingsRegistration(registrationData?: ClientRegistration): FormGroup { private buildSettingsRegistration(registrationData?: ClientRegistration): FormGroup {
const clientRegistration = this.fb.group({ const clientRegistration = this.fb.group({
registrationId: [null, [Validators.required, this.uniqueRegistrationIdValidator]], providerName: ['Custom', [Validators.required]],
loginButtonLabel: [null, [Validators.required]], loginButtonLabel: [null, [Validators.required]],
loginButtonIcon: [null], loginButtonIcon: [null],
clientId: ['', [Validators.required]], clientId: ['', [Validators.required]],
@ -227,8 +235,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
if (registrationData) { if (registrationData) {
registrationData.scope.forEach(() => { registrationData.scope.forEach(() => {
(clientRegistration.get('scope') as FormArray).push(this.fb.control('')) (clientRegistration.get('scope') as FormArray).push(this.fb.control(''));
}) });
if (registrationData.mapperConfig.type !== 'BASIC') { if (registrationData.mapperConfig.type !== 'BASIC') {
clientRegistration.get('mapperConfig.type').patchValue('CUSTOM'); clientRegistration.get('mapperConfig.type').patchValue('CUSTOM');
} }
@ -288,7 +296,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
if (data) { if (data) {
this.clientsDomainsParams.removeAt(index); this.clientsDomainsParams.removeAt(index);
} }
}) });
} }
clientDomainRegistrations(control: AbstractControl): FormArray { clientDomainRegistrations(control: AbstractControl): FormArray {
@ -305,15 +313,57 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
$event.preventDefault(); $event.preventDefault();
} }
const registrationId = this.clientDomainRegistrations(controler).at(index).get('registrationId').value; const providerName = this.clientDomainRegistrations(controler).at(index).get('providerName').value;
this.dialogService.confirm( this.dialogService.confirm(
this.translate.instant('admin.oauth2.delete-registration-title', {name: registrationId || ''}), this.translate.instant('admin.oauth2.delete-registration-title', {name: providerName || ''}),
this.translate.instant('admin.oauth2.delete-registration-text'), null, this.translate.instant('admin.oauth2.delete-registration-text'), null,
this.translate.instant('action.delete') this.translate.instant('action.delete')
).subscribe((data) => { ).subscribe((data) => {
if (data) { if (data) {
this.clientDomainRegistrations(controler).removeAt(index); this.clientDomainRegistrations(controler).removeAt(index);
} }
}) });
}
editRedirectURI($event: MouseEvent, index: number) {
if ($event) {
$event.stopPropagation();
$event.preventDefault();
}
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const redirectURI = this.clientsDomainsParams.at(index).get('redirectUriTemplate').value;
const injectionTokens = new WeakMap<any, any>([
[EDIT_REDIRECT_URI_PANEL_DATA, {
redirectURI
} as EditRedirectUriPanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
const componentRef = overlayRef.attach(new ComponentPortal(EditRedirectUriPanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => {
if (componentRef.instance.result !== null) {
const attributeValue = componentRef.instance.result;
this.clientsDomainsParams.at(index).get('redirectUriTemplate').patchValue(attributeValue, {emitEvent: true});
}
});
} }
} }

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

@ -76,7 +76,6 @@ export interface DomainParams {
} }
export interface ClientRegistration { export interface ClientRegistration {
registrationId: string;
loginButtonLabel: string; loginButtonLabel: string;
loginButtonIcon: string; loginButtonIcon: string;
clientId: string; clientId: string;
@ -86,9 +85,9 @@ export interface ClientRegistration {
scope: string[]; scope: string[];
jwkSetUri: string; jwkSetUri: string;
userInfoUri: string; userInfoUri: string;
clientAuthenticationMethod: ClientAuthenticationMethod clientAuthenticationMethod: ClientAuthenticationMethod;
userNameAttributeName: string; userNameAttributeName: string;
mapperConfig: MapperConfig mapperConfig: MapperConfig;
} }
export interface MapperConfig { export interface MapperConfig {

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

@ -125,7 +125,7 @@
"error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io",
"add-domain": "Add domain", "add-domain": "Add domain",
"new-domain": "New domain", "new-domain": "New domain",
"add-registration": "Add registration", "add-provider": "Add provider",
"settings": "Settings", "settings": "Settings",
"oauth2": { "oauth2": {
"settings": "OAuth2 Settings", "settings": "OAuth2 Settings",

Loading…
Cancel
Save