51 changed files with 2813 additions and 1449 deletions
@ -0,0 +1,56 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { Domain, DomainInfo } from '@shared/models/oauth2.models'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { PageData } from '@shared/models/page/page-data'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class DomainService { |
|||
|
|||
constructor( |
|||
private http: HttpClient |
|||
) { |
|||
} |
|||
|
|||
public saveDomain(domain: Domain, oauth2ClientIds: Array<string>, config?: RequestConfig): Observable<Domain> { |
|||
return this.http.post<Domain>(`/api/domain?oauth2ClientIds=${oauth2ClientIds.join(',')}`, |
|||
domain, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public updateOauth2Clients(id: string, oauth2ClientRegistrationIds: Array<string>, config?: RequestConfig): Observable<void> { |
|||
return this.http.put<void>(`/api/domain/${id}/oauth2Clients`, oauth2ClientRegistrationIds, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public getTenantDomainInfos(pageLink: PageLink, config?: RequestConfig): Observable<PageData<DomainInfo>> { |
|||
return this.http.get<PageData<DomainInfo>>(`/api/domain/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public getDomainInfoById(id: string, config?: RequestConfig): Observable<DomainInfo> { |
|||
return this.http.get<DomainInfo>(`/api/domain/info/${id}`, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public deleteDomain(id: string, config?: RequestConfig): Observable<void> { |
|||
return this.http.delete<void>(`/api/domain/${id}`, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { HttpClient } from '@angular/common/http'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; |
|||
import { Observable } from 'rxjs'; |
|||
import { MobileAppSettings } from '@shared/models/mobile-app.models'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class MobileApplicationService { |
|||
|
|||
constructor( |
|||
private http: HttpClient |
|||
) {} |
|||
|
|||
public getMobileAppSettings(config?: RequestConfig): Observable<MobileAppSettings> { |
|||
return this.http.get<MobileAppSettings>(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable<MobileAppSettings> { |
|||
return this.http.post<MobileAppSettings>(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
public getMobileAppDeepLink(config?: RequestConfig): Observable<string> { |
|||
return this.http.get<string>(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); |
|||
} |
|||
|
|||
} |
|||
@ -1,614 +0,0 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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 appearance="outlined" class="settings-card"> |
|||
<mat-card-header> |
|||
<mat-card-title> |
|||
<span class="mat-headline-5" translate>admin.oauth2.oauth2</span> |
|||
</mat-card-title> |
|||
<span fxFlex></span> |
|||
<div tb-help="oauth2Settings"></div> |
|||
</mat-card-header> |
|||
<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 class="tb-form-panel stroked gap-xs-12" style="margin-bottom: 16px" [disabled]="isLoading$ | async"> |
|||
<div class="tb-form-row no-border no-padding column-xs"> |
|||
<mat-chip-listbox formControlName="enabled"> |
|||
<mat-chip-option [value]="true">{{ 'admin.oauth2.enable' | translate }}</mat-chip-option> |
|||
</mat-chip-listbox> |
|||
<mat-chip-listbox formControlName="edgeEnabled"> |
|||
<mat-chip-option [value]="true">{{ 'admin.oauth2.edge-enable' | translate }}</mat-chip-option> |
|||
</mat-chip-listbox> |
|||
</div> |
|||
<section *ngIf="oauth2SettingsForm.get('enabled').value && oauth2ParamsInfos.controls.length"> |
|||
<ng-container formArrayName="oauth2ParamsInfos"> |
|||
<div> |
|||
<mat-accordion multi> |
|||
<ng-container *ngFor="let oauth2ParamsInfo of oauth2ParamsInfos.controls; let i = index; trackBy: trackByItem"> |
|||
<mat-expansion-panel [formGroupName]="i"> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title fxLayoutAlign="start center"> |
|||
{{ domainListTittle(oauth2ParamsInfo) }} |
|||
</mat-panel-title> |
|||
<mat-panel-description fxLayoutAlign="end center"> |
|||
<button mat-icon-button |
|||
type="button" |
|||
(click)="deleteOAuth2ParamsInfo($event, i)" |
|||
matTooltip="{{ 'action.delete' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</mat-panel-description> |
|||
</mat-expansion-panel-header> |
|||
|
|||
<ng-template matExpansionPanelContent> |
|||
<mat-tab-group dynamicHeight> |
|||
<mat-tab label="{{ 'admin.oauth2.domains' | translate }}"> |
|||
<ng-container formArrayName="domainInfos"> |
|||
<div style="padding-top: 16px;"></div> |
|||
<section *ngFor="let domainInfo of domainInfos(oauth2ParamsInfo).controls; let n = index; trackBy: trackByItem" |
|||
class="domains-list"> |
|||
<div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px"> |
|||
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px"> |
|||
<div fxLayout="column" fxFlex.sm="60" fxFlex.gt-sm="50"> |
|||
<div fxLayout="row" fxLayout.xs="column" fxLayout.md="column" fxLayoutGap="8px" fxLayoutGap.md="0px"> |
|||
<mat-form-field fxFlex="30" fxFlex.md fxFlex.xs class="mat-block"> |
|||
<mat-label translate>admin.oauth2.protocol</mat-label> |
|||
<mat-select formControlName="scheme"> |
|||
<mat-option *ngFor="let protocol of protocols" [value]="protocol"> |
|||
{{ domainSchemaTranslations.get(protocol) | translate | uppercase }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>admin.domain-name</mat-label> |
|||
<input matInput formControlName="name" required> |
|||
<mat-error *ngIf="domainInfo.get('name').hasError('pattern')"> |
|||
{{ 'admin.error-verification-url' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="domainInfo.get('name').hasError('maxlength')"> |
|||
{{ 'admin.domain-name-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<mat-error *ngIf="domainInfo.hasError('unique')"> |
|||
{{ 'admin.domain-name-unique' | translate }} |
|||
</mat-error> |
|||
</div> |
|||
|
|||
<div fxFlex fxLayout="column"> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>admin.oauth2.redirect-uri-template</mat-label> |
|||
<input matInput [value]="redirectURI(domainInfo)" readonly> |
|||
<tb-copy-button |
|||
matSuffix |
|||
miniButton="false" |
|||
color="primary" |
|||
[copyText]="redirectURI(domainInfo)" |
|||
tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}" |
|||
tooltipPosition="above" |
|||
icon="mdi:clipboard-arrow-left"> |
|||
</tb-copy-button> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex *ngIf="domainInfo.get('scheme').value === 'MIXED'" class="mat-block"> |
|||
<input matInput [value]="redirectURIMixed(domainInfo)" readonly> |
|||
<tb-copy-button |
|||
matSuffix |
|||
miniButton="false" |
|||
color="primary" |
|||
[copyText]="redirectURIMixed(domainInfo)" |
|||
tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}" |
|||
tooltipPosition="above" |
|||
icon="mdi:clipboard-arrow-left"> |
|||
</tb-copy-button> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
|
|||
<div fxLayout="column" fxLayoutAlign="center start"> |
|||
<button type="button" mat-icon-button color="primary" |
|||
(click)="removeDomainInfo($event, oauth2ParamsInfo, n)" |
|||
[disabled]="domainInfos(oauth2ParamsInfo).controls.length < 2" |
|||
matTooltip="{{ 'admin.oauth2.delete-domain' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
<div fxLayout="row" fxLayoutAlign="end center" style="margin-bottom: 1.25em"> |
|||
<button mat-button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="addDomainInfo(oauth2ParamsInfo)" |
|||
type="button"> |
|||
{{'admin.oauth2.add-domain' | translate}} |
|||
</button> |
|||
</div> |
|||
</ng-container> |
|||
</mat-tab> |
|||
<mat-tab label="{{ 'admin.oauth2.mobile-apps' | translate }}"> |
|||
<ng-container formArrayName="mobileInfos"> |
|||
<div style="padding-top: 16px;"></div> |
|||
<div *ngIf="mobileInfos(oauth2ParamsInfo).length === 0"> |
|||
<span fxLayoutAlign="center center" |
|||
class="tb-prompt" translate>admin.oauth2.no-mobile-apps</span> |
|||
</div> |
|||
<section *ngFor="let mobileInfo of mobileInfos(oauth2ParamsInfo).controls; let n = index; trackBy: trackByItem" |
|||
class="apps-list"> |
|||
<div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px"> |
|||
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px"> |
|||
<div fxFlex fxLayout="column"> |
|||
<mat-form-field fxFlex class="mat-block" floatLabel="always" subscriptSizing="dynamic"> |
|||
<mat-label translate>admin.oauth2.mobile-package</mat-label> |
|||
<input matInput formControlName="pkgName" placeholder="{{ 'admin.oauth2.mobile-package-placeholder' | translate }}" required> |
|||
<mat-hint translate>admin.oauth2.mobile-package-hint</mat-hint> |
|||
</mat-form-field> |
|||
<mat-error *ngIf="mobileInfo.hasError('unique')"> |
|||
{{ 'admin.oauth2.mobile-package-unique' | translate }} |
|||
</mat-error> |
|||
</div> |
|||
<div fxFlex fxLayout="row"> |
|||
<mat-form-field fxFlex class="mat-block" subscriptSizing="dynamic"> |
|||
<mat-label translate>admin.oauth2.mobile-app-secret</mat-label> |
|||
<input matInput formControlName="appSecret" required> |
|||
<tb-copy-button |
|||
matSuffix |
|||
miniButton="false" |
|||
color="primary" |
|||
[copyText]="mobileInfo.get('appSecret').value" |
|||
tooltipText="{{ 'admin.oauth2.copy-mobile-app-secret' | translate }}" |
|||
tooltipPosition="above" |
|||
icon="mdi:clipboard-arrow-left"> |
|||
</tb-copy-button> |
|||
<mat-hint translate>admin.oauth2.mobile-app-secret-hint</mat-hint> |
|||
<mat-error *ngIf="mobileInfo.get('appSecret').hasError('required')"> |
|||
{{ 'admin.oauth2.mobile-app-secret-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="mobileInfo.get('appSecret').hasError('base64')"> |
|||
{{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="mobileInfo.get('appSecret').hasError('minLength')"> |
|||
{{ 'admin.oauth2.mobile-app-secret-base64' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div fxLayout="column" fxLayoutAlign="center start"> |
|||
<button type="button" mat-icon-button color="primary" |
|||
(click)="removeMobileInfo($event, oauth2ParamsInfo, n)" |
|||
matTooltip="{{ 'admin.oauth2.delete-mobile-app' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
<div fxLayout="row" fxLayoutAlign="end center" style="margin-bottom: 1.25em"> |
|||
<button mat-button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="addMobileInfo(oauth2ParamsInfo)" |
|||
type="button"> |
|||
{{'admin.oauth2.add-mobile-app' | translate}} |
|||
</button> |
|||
</div> |
|||
</ng-container> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
<div class="mat-h3" translate>admin.oauth2.providers</div> |
|||
<ng-container formArrayName="clientRegistrations"> |
|||
<div class="container"> |
|||
<mat-expansion-panel *ngFor="let registration of clientRegistrations(oauth2ParamsInfo).controls; let j = index; trackBy: trackByItem" |
|||
class="registration-card mat-elevation-z0"> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title fxLayoutAlign="start center"> |
|||
{{ getProviderName(registration) }} |
|||
</mat-panel-title> |
|||
<mat-panel-description fxLayoutAlign="end center"> |
|||
<button mat-icon-button |
|||
type="button" |
|||
[disabled]="clientRegistrations(oauth2ParamsInfo).controls.length < 2" |
|||
(click)="deleteRegistration($event, oauth2ParamsInfo, j)" |
|||
matTooltip="{{ 'admin.oauth2.delete-provider' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</mat-panel-description> |
|||
</mat-expansion-panel-header> |
|||
|
|||
<ng-template matExpansionPanelContent> |
|||
<section [formGroupName]="j"> |
|||
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> |
|||
<section fxFlex formGroupName="additionalInfo" fxLayout="row"> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>admin.oauth2.login-provider</mat-label> |
|||
<mat-select formControlName="providerName"> |
|||
<mat-option *ngFor="let provider of templateProvider" [value]="provider"> |
|||
{{ provider }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<div [tb-help]="getHelpLink(registration)" *ngIf="!isCustomProvider(registration)"></div> |
|||
</section> |
|||
<mat-form-field floatLabel="always" fxFlex class="mat-block"> |
|||
<mat-label translate>admin.oauth2.allowed-platforms</mat-label> |
|||
<mat-select formControlName="platforms" multiple placeholder="{{ 'admin.oauth2.all-platforms' | translate }}"> |
|||
<mat-option *ngFor="let platform of platformTypes" [value]="platform"> |
|||
{{ platformTypeTranslations.get(platform) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</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-error *ngIf="registration.get('clientId').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.client-id-max-length' | 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-error *ngIf="registration.get('clientSecret').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.client-secret-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
|
|||
<mat-expansion-panel class="mat-elevation-z0 custom-settings" |
|||
[disabled]="isCustomProvider(registration)" |
|||
[expanded]="isCustomProvider(registration)"> |
|||
<mat-expansion-panel-header [fxHide]="isCustomProvider(registration)"> |
|||
<mat-panel-description fxLayoutAlign="end center"> |
|||
{{ 'admin.oauth2.custom-setting' | translate }} |
|||
</mat-panel-description> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<mat-tab-group dynamicHeight> |
|||
<mat-tab label="{{ 'admin.oauth2.general' | translate }}"> |
|||
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px" style="margin-top: 16px;"> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>admin.oauth2.access-token-uri</mat-label> |
|||
<input matInput formControlName="accessTokenUri" required> |
|||
<button mat-icon-button matSuffix |
|||
type="button" |
|||
(click)="toggleEditMode(registration, 'accessTokenUri')" |
|||
*ngIf="!isCustomProvider(registration)"> |
|||
<mat-icon class="material-icons">create</mat-icon> |
|||
</button> |
|||
<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> |
|||
<button mat-icon-button matSuffix |
|||
type="button" |
|||
(click)="toggleEditMode(registration, 'authorizationUri')" |
|||
*ngIf="!isCustomProvider(registration)"> |
|||
<mat-icon class="material-icons">create</mat-icon> |
|||
</button> |
|||
<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"> |
|||
<button mat-icon-button matSuffix |
|||
type="button" aria-label="Clear" |
|||
(click)="toggleEditMode(registration, 'jwkSetUri')" |
|||
*ngIf="!isCustomProvider(registration)"> |
|||
<mat-icon class="material-icons">create</mat-icon> |
|||
</button> |
|||
<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"> |
|||
<button mat-icon-button matSuffix |
|||
type="button" |
|||
(click)="toggleEditMode(registration, 'userInfoUri')" |
|||
*ngIf="!isCustomProvider(registration)"> |
|||
<mat-icon class="material-icons">create</mat-icon> |
|||
</button> |
|||
<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="isCustomProvider(registration)"> |
|||
<mat-form-field fxFlex class="mat-block" floatLabel="always"> |
|||
<mat-label translate>admin.oauth2.login-button-label</mat-label> |
|||
<input matInput formControlName="loginButtonLabel" |
|||
placeholder="{{ 'admin.oauth2.login-button-label-placeholder' | translate }}" |
|||
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 formGroupName="mapperConfig"> |
|||
<div fxLayout="column" fxLayoutGap="8px" style="margin-bottom: 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> |
|||
</section> |
|||
|
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>admin.oauth2.scope</mat-label> |
|||
<mat-chip-grid #scopeList> |
|||
<mat-chip-row *ngFor="let scope of registration.get('scope').value; let k = index; trackBy: trackByParams" |
|||
removable (removed)="removeScope(k, registration)"> |
|||
{{scope}} |
|||
<mat-icon matChipRemove>cancel</mat-icon> |
|||
</mat-chip-row> |
|||
<input [matChipInputFor]="scopeList" |
|||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|||
matChipInputAddOnBlur |
|||
(matChipInputTokenEnd)="addScope($event, registration)"> |
|||
</mat-chip-grid> |
|||
<mat-error *ngIf="registration.get('scope').hasError('required')"> |
|||
{{ 'admin.oauth2.scope-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<tb-error style="display: block; margin-top: -24px;" |
|||
[error]="registration.get('scope').hasError('required') |
|||
? ('admin.oauth2.scope-required' | translate) : ''"> |
|||
</tb-error> |
|||
|
|||
</mat-tab> |
|||
<mat-tab label="{{ 'admin.oauth2.mapper' | translate }}"> |
|||
<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"> |
|||
<mat-form-field fxFlex class="mat-block"> |
|||
<mat-label translate>admin.oauth2.type</mat-label> |
|||
<mat-select formControlName="type"> |
|||
<mat-option *ngFor="let mapperConfigType of mapperConfigTypes" |
|||
[value]="mapperConfigType"> |
|||
{{ mapperConfigType }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
|
|||
<section formGroupName="basic" |
|||
*ngIf="registration.get('mapperConfig.type').value !== mapperConfigType.CUSTOM"> |
|||
<mat-form-field class="mat-block" |
|||
*ngIf="registration.get('mapperConfig.type').value !== mapperConfigType.GITHUB"> |
|||
<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-error |
|||
*ngIf="registration.get('mapperConfig.basic.emailAttributeKey').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.email-attribute-key-max-length' | 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-error |
|||
*ngIf="registration.get('mapperConfig.basic.firstNameAttributeKey').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} |
|||
</mat-error> |
|||
</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-error |
|||
*ngIf="registration.get('mapperConfig.basic.lastNameAttributeKey').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.last-name-attribute-key-max-length' | 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.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" [fxShow]="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-error |
|||
*ngIf="registration.get('mapperConfig.basic.tenantNamePattern').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.tenant-name-pattern-max-length' | 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-error |
|||
*ngIf="registration.get('mapperConfig.basic.customerNamePattern').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.customer-name-pattern-max-length' | 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.default-dashboard-name</mat-label> |
|||
<input matInput formControlName="defaultDashboardName"> |
|||
<mat-error |
|||
*ngIf="registration.get('mapperConfig.basic.defaultDashboardName').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} |
|||
</mat-error> |
|||
</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 === mapperConfigType.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-error |
|||
*ngIf="registration.get('mapperConfig.custom.url').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.url-max-length' | 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-error *ngIf="registration.get('mapperConfig.custom.username').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.username-max-length' | translate }} |
|||
</mat-error> |
|||
</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"> |
|||
<tb-toggle-password matSuffix></tb-toggle-password> |
|||
<mat-error *ngIf="registration.get('mapperConfig.custom.password').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.password-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</section> |
|||
</section> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
|
|||
</section> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</div> |
|||
</ng-container> |
|||
|
|||
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> |
|||
<button mat-button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="addRegistration(oauth2ParamsInfo)" |
|||
type="button"> |
|||
{{'admin.oauth2.add-provider' | translate}} |
|||
</button> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
</mat-expansion-panel> |
|||
</ng-container> |
|||
</mat-accordion> |
|||
</div> |
|||
</ng-container> |
|||
</section> |
|||
</fieldset> |
|||
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> |
|||
<button type="button" mat-raised-button color="primary" |
|||
[disabled]="isLoading$ | async" |
|||
*ngIf="oauth2SettingsForm.get('enabled').value" |
|||
(click)="addOAuth2ParamsInfo()"> |
|||
<mat-icon>add</mat-icon> |
|||
<span translate>action.add</span> |
|||
</button> |
|||
<button mat-button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async) || oauth2SettingsForm.invalid || !oauth2SettingsForm.dirty" |
|||
type="submit"> |
|||
{{'action.save' | translate}} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
</div> |
|||
@ -1,78 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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 '../../../../../scss/constants'; |
|||
|
|||
:host{ |
|||
.gap-xs-12 { |
|||
@media #{$mat-xs} { |
|||
gap: 12px; |
|||
} |
|||
} |
|||
|
|||
.mdc-evolution-chip-set .mdc-evolution-chip { |
|||
margin-top: 0; |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
.checkbox-row { |
|||
margin-top: 1em; |
|||
} |
|||
|
|||
.registration-card{ |
|||
margin-bottom: 0.5em; |
|||
border: 1px solid rgba(0, 0, 0, 0.2); |
|||
|
|||
.custom-settings{ |
|||
font-size: 16px; |
|||
.mat-expansion-panel-header{ |
|||
padding: 0 2px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.container{ |
|||
margin-bottom: 16px; |
|||
|
|||
.tb-highlight{ |
|||
margin: 0; |
|||
} |
|||
|
|||
.tb-hint{ |
|||
padding-bottom: 0; |
|||
} |
|||
} |
|||
|
|||
.mat-expansion-panel { |
|||
.mat-expansion-panel-header { |
|||
&.mat-expanded { |
|||
height: 48px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep{ |
|||
.registration-card{ |
|||
.custom-settings{ |
|||
.mat-expansion-panel-body{ |
|||
padding: 0 2px 1em; |
|||
} |
|||
} |
|||
} |
|||
.domains-list, .apps-list { |
|||
margin-bottom: 1.5em; |
|||
} |
|||
} |
|||
@ -1,613 +0,0 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, |
|||
FormControl, |
|||
UntypedFormArray, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { |
|||
ClientAuthenticationMethod, |
|||
DomainSchema, |
|||
domainSchemaTranslations, |
|||
MapperConfig, |
|||
MapperConfigBasic, |
|||
MapperConfigCustom, |
|||
MapperConfigType, |
|||
OAuth2ClientRegistrationTemplate, |
|||
OAuth2DomainInfo, |
|||
OAuth2Info, |
|||
OAuth2MobileInfo, |
|||
OAuth2ParamsInfo, |
|||
OAuth2RegistrationInfo, |
|||
PlatformType, |
|||
platformTypeTranslations, |
|||
TenantNameStrategy |
|||
} from '@shared/models/oauth2.models'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
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 { forkJoin, Subscription } from 'rxjs'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { isDefined, isDefinedAndNotNull, randomAlphanumeric } from '@core/utils'; |
|||
import { OAuth2Service } from '@core/http/oauth2.service'; |
|||
import { ActivatedRoute } from '@angular/router'; |
|||
|
|||
@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 { |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private route: ActivatedRoute, |
|||
private oauth2Service: OAuth2Service, |
|||
private fb: UntypedFormBuilder, |
|||
private dialogService: DialogService, |
|||
private translate: TranslateService, |
|||
@Inject(WINDOW) private window: Window) { |
|||
super(store); |
|||
} |
|||
|
|||
get oauth2ParamsInfos(): UntypedFormArray { |
|||
return this.oauth2SettingsForm.get('oauth2ParamsInfos') as UntypedFormArray; |
|||
} |
|||
|
|||
private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; |
|||
private DOMAIN_AND_PORT_REGEXP = /^(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?$/; |
|||
private subscriptions: Subscription[] = []; |
|||
private templates = new Map<string, OAuth2ClientRegistrationTemplate>(); |
|||
private defaultProvider = { |
|||
additionalInfo: { |
|||
providerName: 'Custom' |
|||
}, |
|||
clientAuthenticationMethod: ClientAuthenticationMethod.POST, |
|||
userNameAttributeName: 'email', |
|||
mapperConfig: { |
|||
allowUserCreation: true, |
|||
activateUser: false, |
|||
type: MapperConfigType.BASIC, |
|||
basic: { |
|||
emailAttributeKey: 'email', |
|||
tenantNameStrategy: TenantNameStrategy.DOMAIN, |
|||
alwaysFullScreen: false |
|||
} |
|||
} |
|||
}; |
|||
|
|||
readonly separatorKeysCodes: number[] = [ENTER, COMMA]; |
|||
|
|||
oauth2SettingsForm: UntypedFormGroup; |
|||
oauth2Info: OAuth2Info; |
|||
|
|||
clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod); |
|||
mapperConfigType = MapperConfigType; |
|||
mapperConfigTypes = Object.keys(MapperConfigType); |
|||
tenantNameStrategies = Object.keys(TenantNameStrategy); |
|||
protocols = Object.values(DomainSchema); |
|||
domainSchemaTranslations = domainSchemaTranslations; |
|||
platformTypes = Object.values(PlatformType); |
|||
platformTypeTranslations = platformTypeTranslations; |
|||
|
|||
templateProvider = ['Custom']; |
|||
|
|||
showMainLoadingBar = false; |
|||
|
|||
private loginProcessingUrl: string = this.route.snapshot.data.loginProcessingUrl; |
|||
|
|||
private static validateScope(control: AbstractControl): ValidationErrors | null { |
|||
const scope: string[] = control.value; |
|||
if (!scope || !scope.length) { |
|||
return { |
|||
required: true |
|||
}; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.buildOAuth2SettingsForm(); |
|||
forkJoin([ |
|||
this.oauth2Service.getOAuth2Template(), |
|||
this.oauth2Service.getOAuth2Settings() |
|||
]).subscribe( |
|||
([templates, oauth2Info]) => { |
|||
this.initTemplates(templates); |
|||
this.oauth2Info = oauth2Info; |
|||
this.initOAuth2Settings(this.oauth2Info); |
|||
} |
|||
); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
super.ngOnDestroy(); |
|||
this.subscriptions.forEach((subscription) => { |
|||
subscription.unsubscribe(); |
|||
}); |
|||
} |
|||
|
|||
private initTemplates(templates: OAuth2ClientRegistrationTemplate[]): void { |
|||
templates.map(provider => { |
|||
delete provider.additionalInfo; |
|||
this.templates.set(provider.name, provider); |
|||
}); |
|||
this.templateProvider.push(...Array.from(this.templates.keys())); |
|||
this.templateProvider.sort(); |
|||
} |
|||
|
|||
private formBasicGroup(mapperConfigBasic?: MapperConfigBasic): UntypedFormGroup { |
|||
let tenantNamePattern; |
|||
if (mapperConfigBasic?.tenantNamePattern) { |
|||
tenantNamePattern = mapperConfigBasic.tenantNamePattern; |
|||
} else { |
|||
tenantNamePattern = {value: null, disabled: true}; |
|||
} |
|||
const basicGroup = this.fb.group({ |
|||
emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email', |
|||
[Validators.required, Validators.maxLength(31)]], |
|||
firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : '', |
|||
Validators.maxLength(31)], |
|||
lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : '', |
|||
Validators.maxLength(31)], |
|||
tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategy.DOMAIN], |
|||
tenantNamePattern: [tenantNamePattern, [Validators.required, Validators.maxLength(255)]], |
|||
customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null, |
|||
Validators.maxLength(255)], |
|||
defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null, |
|||
Validators.maxLength(255)], |
|||
alwaysFullScreen: [isDefinedAndNotNull(mapperConfigBasic?.alwaysFullScreen) ? mapperConfigBasic.alwaysFullScreen : false] |
|||
}); |
|||
|
|||
this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => { |
|||
if (domain === 'CUSTOM') { |
|||
basicGroup.get('tenantNamePattern').enable(); |
|||
} else { |
|||
basicGroup.get('tenantNamePattern').disable(); |
|||
} |
|||
})); |
|||
|
|||
return basicGroup; |
|||
} |
|||
|
|||
private formCustomGroup(mapperConfigCustom?: MapperConfigCustom): UntypedFormGroup { |
|||
return this.fb.group({ |
|||
url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, |
|||
[Validators.required, Validators.pattern(this.URL_REGEXP), Validators.maxLength(255)]], |
|||
username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null, Validators.maxLength(255)], |
|||
password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null, Validators.maxLength(255)] |
|||
}); |
|||
} |
|||
|
|||
private buildOAuth2SettingsForm(): void { |
|||
this.oauth2SettingsForm = this.fb.group({ |
|||
oauth2ParamsInfos: this.fb.array([]), |
|||
enabled: [false], |
|||
edgeEnabled: [{value: false, disabled: true}] |
|||
}); |
|||
|
|||
this.subscriptions.push(this.oauth2SettingsForm.get('enabled').valueChanges.subscribe(enabled => { |
|||
if (enabled) { |
|||
this.oauth2SettingsForm.get('edgeEnabled').enable(); |
|||
} else { |
|||
this.oauth2SettingsForm.get('edgeEnabled').patchValue(false); |
|||
this.oauth2SettingsForm.get('edgeEnabled').disable(); |
|||
} |
|||
})); |
|||
} |
|||
|
|||
private initOAuth2Settings(oauth2Info: OAuth2Info): void { |
|||
if (oauth2Info) { |
|||
this.oauth2SettingsForm.patchValue({enabled: oauth2Info.enabled, edgeEnabled: oauth2Info.edgeEnabled}); |
|||
oauth2Info.oauth2ParamsInfos.forEach((oauth2ParamsInfo) => { |
|||
this.oauth2ParamsInfos.push(this.buildOAuth2ParamsInfoForm(oauth2ParamsInfo)); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private uniqueDomainValidator(control: UntypedFormGroup): { [key: string]: boolean } | null { |
|||
if (control.parent?.value) { |
|||
const domain = control.value.name; |
|||
const listProtocols = control.parent.getRawValue() |
|||
.filter((domainInfo) => domainInfo.name === domain) |
|||
.map((domainInfo) => domainInfo.scheme); |
|||
if (listProtocols.length > 1 && listProtocols.indexOf(DomainSchema.MIXED) > -1 || |
|||
new Set(listProtocols).size !== listProtocols.length) { |
|||
return {unique: true}; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
private uniquePkgNameValidator(control: UntypedFormGroup): { [key: string]: boolean } | null { |
|||
if (control.parent?.value) { |
|||
const pkgName = control.value.pkgName; |
|||
const mobileInfosList = control.parent.getRawValue() |
|||
.filter((mobileInfo) => mobileInfo.pkgName === pkgName); |
|||
if (mobileInfosList.length > 1) { |
|||
return {unique: true}; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public domainListTittle(control: AbstractControl): string { |
|||
const domainInfos = control.get('domainInfos').value as OAuth2DomainInfo[]; |
|||
if (domainInfos.length) { |
|||
const domainList = new Set<string>(); |
|||
domainInfos.forEach((domain) => { |
|||
domainList.add(domain.name); |
|||
}); |
|||
return Array.from(domainList).join(', '); |
|||
} |
|||
return this.translate.instant('admin.oauth2.new-domain'); |
|||
} |
|||
|
|||
private buildOAuth2ParamsInfoForm(oauth2ParamsInfo?: OAuth2ParamsInfo): UntypedFormGroup { |
|||
const formOAuth2Params = this.fb.group({ |
|||
domainInfos: this.fb.array([], Validators.required), |
|||
mobileInfos: this.fb.array([]), |
|||
clientRegistrations: this.fb.array([], Validators.required) |
|||
}); |
|||
|
|||
if (oauth2ParamsInfo) { |
|||
oauth2ParamsInfo.domainInfos.forEach((domain) => { |
|||
this.domainInfos(formOAuth2Params).push(this.buildDomainInfoForm(domain)); |
|||
}); |
|||
oauth2ParamsInfo.mobileInfos.forEach((mobile) => { |
|||
this.mobileInfos(formOAuth2Params).push(this.buildMobileInfoForm(mobile)); |
|||
}); |
|||
oauth2ParamsInfo.clientRegistrations.forEach((registration) => { |
|||
this.clientRegistrations(formOAuth2Params).push(this.buildRegistrationForm(registration)); |
|||
}); |
|||
} else { |
|||
this.clientRegistrations(formOAuth2Params).push(this.buildRegistrationForm()); |
|||
this.domainInfos(formOAuth2Params).push(this.buildDomainInfoForm()); |
|||
} |
|||
|
|||
return formOAuth2Params; |
|||
} |
|||
|
|||
private buildDomainInfoForm(domainInfo?: OAuth2DomainInfo): UntypedFormGroup { |
|||
return this.fb.group({ |
|||
name: [domainInfo ? domainInfo.name : this.window.location.hostname, [ |
|||
Validators.required, Validators.maxLength(255), |
|||
Validators.pattern(this.DOMAIN_AND_PORT_REGEXP)]], |
|||
scheme: [domainInfo?.scheme ? domainInfo.scheme : DomainSchema.HTTPS, Validators.required] |
|||
}, {validators: this.uniqueDomainValidator}); |
|||
} |
|||
|
|||
private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): UntypedFormGroup { |
|||
return this.fb.group({ |
|||
pkgName: [mobileInfo?.pkgName, [Validators.required]], |
|||
appSecret: [mobileInfo?.appSecret, [Validators.required, this.base64Format]], |
|||
}, {validators: this.uniquePkgNameValidator}); |
|||
} |
|||
|
|||
private base64Format(control: FormControl): { [key: string]: boolean } | null { |
|||
if (control.value === '') { |
|||
return null; |
|||
} |
|||
try { |
|||
const value = atob(control.value); |
|||
if (value.length < 64) { |
|||
return {minLength: true}; |
|||
} |
|||
return null; |
|||
} catch (e) { |
|||
return {base64: true}; |
|||
} |
|||
} |
|||
|
|||
private buildRegistrationForm(registration?: OAuth2RegistrationInfo): UntypedFormGroup { |
|||
let additionalInfo = null; |
|||
if (isDefinedAndNotNull(registration?.additionalInfo)) { |
|||
additionalInfo = registration.additionalInfo; |
|||
if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) { |
|||
additionalInfo.providerName = 'Custom'; |
|||
} |
|||
} |
|||
let defaultProviderName = 'Custom'; |
|||
if (this.templateProvider.indexOf('Google')) { |
|||
defaultProviderName = 'Google'; |
|||
} |
|||
|
|||
const clientRegistrationFormGroup = this.fb.group({ |
|||
additionalInfo: this.fb.group({ |
|||
providerName: [additionalInfo?.providerName ? additionalInfo?.providerName : defaultProviderName, Validators.required] |
|||
}), |
|||
platforms: [registration?.platforms ? registration.platforms : []], |
|||
loginButtonLabel: [registration?.loginButtonLabel ? registration.loginButtonLabel : null, Validators.required], |
|||
loginButtonIcon: [registration?.loginButtonIcon ? registration.loginButtonIcon : null], |
|||
clientId: [registration?.clientId ? registration.clientId : '', [Validators.required, Validators.maxLength(255)]], |
|||
clientSecret: [registration?.clientSecret ? registration.clientSecret : '', [Validators.required, Validators.maxLength(2048)]], |
|||
accessTokenUri: [registration?.accessTokenUri ? registration.accessTokenUri : '', |
|||
[Validators.required, |
|||
Validators.pattern(this.URL_REGEXP)]], |
|||
authorizationUri: [registration?.authorizationUri ? registration.authorizationUri : '', |
|||
[Validators.required, |
|||
Validators.pattern(this.URL_REGEXP)]], |
|||
scope: this.fb.array(registration?.scope ? registration.scope : [], OAuth2SettingsComponent.validateScope), |
|||
jwkSetUri: [registration?.jwkSetUri ? registration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], |
|||
userInfoUri: [registration?.userInfoUri ? registration.userInfoUri : '', |
|||
[Validators.pattern(this.URL_REGEXP)]], |
|||
clientAuthenticationMethod: [ |
|||
registration?.clientAuthenticationMethod ? registration.clientAuthenticationMethod : ClientAuthenticationMethod.POST, |
|||
Validators.required], |
|||
userNameAttributeName: [ |
|||
registration?.userNameAttributeName ? registration.userNameAttributeName : 'email', Validators.required], |
|||
mapperConfig: this.fb.group({ |
|||
allowUserCreation: [ |
|||
isDefinedAndNotNull(registration?.mapperConfig?.allowUserCreation) ? |
|||
registration.mapperConfig.allowUserCreation : true |
|||
], |
|||
activateUser: [ |
|||
isDefinedAndNotNull(registration?.mapperConfig?.activateUser) ? registration.mapperConfig.activateUser : false |
|||
], |
|||
type: [ |
|||
registration?.mapperConfig?.type ? registration.mapperConfig.type : MapperConfigType.BASIC, Validators.required |
|||
] |
|||
} |
|||
) |
|||
}); |
|||
|
|||
if (registration) { |
|||
this.changeMapperConfigType(clientRegistrationFormGroup, registration.mapperConfig.type, registration.mapperConfig); |
|||
} else { |
|||
this.changeMapperConfigType(clientRegistrationFormGroup, MapperConfigType.BASIC); |
|||
this.setProviderDefaultValue(defaultProviderName, clientRegistrationFormGroup); |
|||
} |
|||
|
|||
this.subscriptions.push(clientRegistrationFormGroup.get('mapperConfig.type').valueChanges.subscribe((value) => { |
|||
this.changeMapperConfigType(clientRegistrationFormGroup, value); |
|||
})); |
|||
|
|||
this.subscriptions.push(clientRegistrationFormGroup.get('additionalInfo.providerName').valueChanges.subscribe((provider) => { |
|||
(clientRegistrationFormGroup.get('scope') as UntypedFormArray).clear(); |
|||
this.setProviderDefaultValue(provider, clientRegistrationFormGroup); |
|||
})); |
|||
|
|||
return clientRegistrationFormGroup; |
|||
} |
|||
|
|||
private setProviderDefaultValue(provider: string, clientRegistration: UntypedFormGroup) { |
|||
if (provider === 'Custom') { |
|||
clientRegistration.reset(this.defaultProvider, {emitEvent: false}); |
|||
clientRegistration.get('accessTokenUri').enable(); |
|||
clientRegistration.get('authorizationUri').enable(); |
|||
clientRegistration.get('jwkSetUri').enable(); |
|||
clientRegistration.get('userInfoUri').enable(); |
|||
} else { |
|||
const template = this.templates.get(provider); |
|||
template.clientId = ''; |
|||
template.clientSecret = ''; |
|||
template.scope.forEach(() => { |
|||
(clientRegistration.get('scope') as UntypedFormArray).push(this.fb.control('')); |
|||
}); |
|||
clientRegistration.get('accessTokenUri').disable(); |
|||
clientRegistration.get('authorizationUri').disable(); |
|||
clientRegistration.get('jwkSetUri').disable(); |
|||
clientRegistration.get('userInfoUri').disable(); |
|||
clientRegistration.patchValue(template); |
|||
} |
|||
} |
|||
|
|||
private changeMapperConfigType(control: AbstractControl, type: MapperConfigType, predefinedValue?: MapperConfig) { |
|||
const mapperConfig = control.get('mapperConfig') as UntypedFormGroup; |
|||
if (type === MapperConfigType.CUSTOM) { |
|||
mapperConfig.removeControl('basic'); |
|||
mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom)); |
|||
} else { |
|||
mapperConfig.removeControl('custom'); |
|||
if (!mapperConfig.get('basic')) { |
|||
mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic)); |
|||
} |
|||
if (type === MapperConfigType.GITHUB) { |
|||
mapperConfig.get('basic.emailAttributeKey').disable(); |
|||
mapperConfig.get('basic.emailAttributeKey').patchValue(null, {emitEvent: false}); |
|||
} else { |
|||
mapperConfig.get('basic.emailAttributeKey').enable(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
save(): void { |
|||
const setting = this.oauth2SettingsForm.getRawValue(); |
|||
this.oauth2Service.saveOAuth2Settings(setting).subscribe( |
|||
(oauth2Settings) => { |
|||
this.oauth2Info = oauth2Settings; |
|||
this.oauth2SettingsForm.patchValue(this.oauth2SettingsForm, {emitEvent: false}); |
|||
this.oauth2SettingsForm.markAsUntouched(); |
|||
this.oauth2SettingsForm.markAsPristine(); |
|||
} |
|||
); |
|||
} |
|||
|
|||
confirmForm(): UntypedFormGroup { |
|||
return this.oauth2SettingsForm; |
|||
} |
|||
|
|||
addScope(event: MatChipInputEvent, control: AbstractControl): void { |
|||
const input = event.chipInput.inputElement; |
|||
const value = event.value; |
|||
const controller = control.get('scope') as UntypedFormArray; |
|||
if ((value.trim() !== '')) { |
|||
controller.push(this.fb.control(value.trim())); |
|||
controller.markAsDirty(); |
|||
} |
|||
|
|||
if (input) { |
|||
input.value = ''; |
|||
} |
|||
} |
|||
|
|||
removeScope(i: number, control: AbstractControl): void { |
|||
const controller = control.get('scope') as UntypedFormArray; |
|||
controller.removeAt(i); |
|||
controller.markAsTouched(); |
|||
controller.markAsDirty(); |
|||
} |
|||
|
|||
addOAuth2ParamsInfo(): void { |
|||
this.oauth2ParamsInfos.push(this.buildOAuth2ParamsInfoForm()); |
|||
} |
|||
|
|||
deleteOAuth2ParamsInfo($event: Event, index: number): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
$event.preventDefault(); |
|||
} |
|||
|
|||
const domainName = this.domainListTittle(this.oauth2ParamsInfos.at(index)); |
|||
this.dialogService.confirm( |
|||
this.translate.instant('admin.oauth2.delete-domain-title', {domainName: domainName || ''}), |
|||
this.translate.instant('admin.oauth2.delete-domain-text'), null, |
|||
this.translate.instant('action.delete') |
|||
).subscribe((data) => { |
|||
if (data) { |
|||
this.oauth2ParamsInfos.removeAt(index); |
|||
this.oauth2ParamsInfos.markAsTouched(); |
|||
this.oauth2ParamsInfos.markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
clientRegistrations(control: AbstractControl): UntypedFormArray { |
|||
return control.get('clientRegistrations') as UntypedFormArray; |
|||
} |
|||
|
|||
domainInfos(control: AbstractControl): UntypedFormArray { |
|||
return control.get('domainInfos') as UntypedFormArray; |
|||
} |
|||
|
|||
mobileInfos(control: AbstractControl): UntypedFormArray { |
|||
return control.get('mobileInfos') as UntypedFormArray; |
|||
} |
|||
|
|||
addRegistration(control: AbstractControl): void { |
|||
this.clientRegistrations(control).push(this.buildRegistrationForm()); |
|||
} |
|||
|
|||
deleteRegistration($event: Event, control: AbstractControl, index: number): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
$event.preventDefault(); |
|||
} |
|||
|
|||
const providerName = this.clientRegistrations(control).at(index).get('additionalInfo.providerName').value; |
|||
this.dialogService.confirm( |
|||
this.translate.instant('admin.oauth2.delete-registration-title', {name: providerName || ''}), |
|||
this.translate.instant('admin.oauth2.delete-registration-text'), null, |
|||
this.translate.instant('action.delete') |
|||
).subscribe((data) => { |
|||
if (data) { |
|||
this.clientRegistrations(control).removeAt(index); |
|||
this.clientRegistrations(control).markAsTouched(); |
|||
this.clientRegistrations(control).markAsDirty(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
toggleEditMode(control: AbstractControl, path: string) { |
|||
control.get(path).disabled ? control.get(path).enable() : control.get(path).disable(); |
|||
} |
|||
|
|||
getProviderName(controller: AbstractControl): string { |
|||
return controller.get('additionalInfo.providerName').value; |
|||
} |
|||
|
|||
isCustomProvider(controller: AbstractControl): boolean { |
|||
return this.getProviderName(controller) === 'Custom'; |
|||
} |
|||
|
|||
getHelpLink(controller: AbstractControl): string { |
|||
const provider = controller.get('additionalInfo.providerName').value; |
|||
if (provider === null || provider === 'Custom') { |
|||
return ''; |
|||
} |
|||
return this.templates.get(provider).helpLink; |
|||
} |
|||
|
|||
addDomainInfo(control: AbstractControl): void { |
|||
this.domainInfos(control).push(this.buildDomainInfoForm({ |
|||
name: '', |
|||
scheme: DomainSchema.HTTPS |
|||
})); |
|||
} |
|||
|
|||
removeDomainInfo($event: Event, control: AbstractControl, index: number): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
$event.preventDefault(); |
|||
} |
|||
this.domainInfos(control).removeAt(index); |
|||
this.domainInfos(control).markAsTouched(); |
|||
this.domainInfos(control).markAsDirty(); |
|||
} |
|||
|
|||
addMobileInfo(control: AbstractControl): void { |
|||
this.mobileInfos(control).push(this.buildMobileInfoForm({ |
|||
pkgName: '', |
|||
appSecret: btoa(randomAlphanumeric(64)) |
|||
})); |
|||
} |
|||
|
|||
removeMobileInfo($event: Event, control: AbstractControl, index: number): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
$event.preventDefault(); |
|||
} |
|||
this.mobileInfos(control).removeAt(index); |
|||
this.mobileInfos(control).markAsTouched(); |
|||
this.mobileInfos(control).markAsDirty(); |
|||
} |
|||
|
|||
redirectURI(control: AbstractControl, schema?: DomainSchema): string { |
|||
const domainInfo = control.value as OAuth2DomainInfo; |
|||
if (domainInfo.name !== '') { |
|||
let protocol; |
|||
if (isDefined(schema)) { |
|||
protocol = schema.toLowerCase(); |
|||
} else { |
|||
protocol = domainInfo.scheme === DomainSchema.MIXED ? DomainSchema.HTTPS.toLowerCase() : domainInfo.scheme.toLowerCase(); |
|||
} |
|||
return `${protocol}://${domainInfo.name}${this.loginProcessingUrl}`; |
|||
} |
|||
return ''; |
|||
} |
|||
|
|||
redirectURIMixed(control: AbstractControl): string { |
|||
return this.redirectURI(control, DomainSchema.HTTP); |
|||
} |
|||
|
|||
trackByParams(index: number): number { |
|||
return index; |
|||
} |
|||
|
|||
trackByItem(i, item) { |
|||
return item; |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
|
|||
<a *ngFor="let client of clientsEntity?.oauth2ClientInfos" class="tb-client-chip" |
|||
routerLink="{{clientPrefixUrl}}/{{client.id.id}}" (click)="$event.stopPropagation();"> |
|||
{{ client.title }} |
|||
</a> |
|||
@ -0,0 +1,34 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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 { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 8px; |
|||
margin: 4px 0; |
|||
a.tb-client-chip { |
|||
padding: 4px 8px; |
|||
border-radius: 12px; |
|||
line-height: 16px; |
|||
background: #e0e0e0; |
|||
color: #212121; |
|||
white-space: nowrap; |
|||
border: none; |
|||
&:hover { |
|||
background: #b7b7b7; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, Input } from '@angular/core'; |
|||
import { HasOauth2Clients } from '@shared/models/oauth2.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-client-chips', |
|||
templateUrl: './client-chips.component.html', |
|||
styleUrls: ['./client-chips.component.scss'] |
|||
}) |
|||
export class ClientChipsComponent { |
|||
|
|||
clientsEntityValue?: HasOauth2Clients; |
|||
|
|||
clientPrefixUrl = '/security-settings/oauth2/clients/details/'; |
|||
|
|||
@Input() |
|||
set clientsEntity(value: HasOauth2Clients) { |
|||
this.clientsEntityValue = value; |
|||
} |
|||
|
|||
get clientsEntity(): HasOauth2Clients { |
|||
return this.clientsEntityValue; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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 (ngSubmit)="save()" style="width: 850px;max-height: 100vh"> |
|||
<mat-toolbar color="primary"> |
|||
<h2>{{ 'admin.oauth2.add-client' | translate }}</h2> |
|||
<span fxFlex></span> |
|||
<div [tb-help]="helpLinkId()"></div> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content style="padding: 0"> |
|||
<tb-client #clientComponent></tb-client> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
cdkFocusInitial |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || clientComponent.entityForm?.invalid || !clientComponent.entityForm?.dirty"> |
|||
{{ 'action.add' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,80 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, OnDestroy, SkipSelf, ViewChild } from '@angular/core'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { MatDialogRef } from '@angular/material/dialog'; |
|||
import { FormBuilder, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; |
|||
import { getProviderHelpLink, OAuth2Client } from '@shared/models/oauth2.models'; |
|||
import { OAuth2Service } from '@core/http/oauth2.service'; |
|||
import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component'; |
|||
import { ErrorStateMatcher } from '@angular/material/core'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-client-dialog', |
|||
templateUrl: './client-dialog.component.html', |
|||
providers: [{provide: ErrorStateMatcher, useExisting: ClientDialogComponent}], |
|||
styleUrls: [] |
|||
}) |
|||
export class ClientDialogComponent extends DialogComponent<ClientDialogComponent, OAuth2Client> implements OnDestroy { |
|||
|
|||
submitted = false; |
|||
|
|||
@ViewChild('clientComponent', {static: true}) clientComponent: ClientComponent; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
protected dialogRef: MatDialogRef<ClientDialogComponent, OAuth2Client>, |
|||
private fb: FormBuilder, |
|||
private oauth2Service: OAuth2Service, |
|||
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) { |
|||
super(store, router, dialogRef); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
setTimeout(() => { |
|||
this.clientComponent.entityForm.markAsDirty(); |
|||
}, 0); |
|||
} |
|||
|
|||
isErrorState(control: UntypedFormControl | 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.dialogRef.close(null); |
|||
} |
|||
|
|||
save() { |
|||
this.submitted = true; |
|||
if (this.clientComponent.entityForm.valid) { |
|||
this.oauth2Service.saveOAuth2Client(this.clientComponent.entityFormValue()).subscribe( |
|||
(client) => { |
|||
this.dialogRef.close(client); |
|||
} |
|||
); |
|||
} |
|||
} |
|||
|
|||
helpLinkId() { |
|||
return getProviderHelpLink(this.clientComponent.entityForm.get('additionalInfo.providerName')?.value); |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
<mat-form-field [formGroup]="entityListFormGroup" class="mat-block" [subscriptSizing]="subscriptSizing"> |
|||
<mat-label translate>admin.oauth2.clients</mat-label> |
|||
<mat-chip-grid #chipList formControlName="entities"> |
|||
<mat-chip-row |
|||
*ngFor="let entity of entities" |
|||
[removable]="!disabled" |
|||
(removed)="remove(entity)"> |
|||
{{entity.title}} |
|||
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> |
|||
</mat-chip-row> |
|||
<input matInput type="text" placeholder="Add client" |
|||
#entityInput |
|||
(focusin)="onFocus()" |
|||
formControlName="entity" |
|||
matAutocompleteOrigin |
|||
#origin="matAutocompleteOrigin" |
|||
[matAutocompleteConnectedTo]="origin" |
|||
[matAutocomplete]="entityAutocomplete" |
|||
[matChipInputFor]="chipList"> |
|||
</mat-chip-grid> |
|||
<mat-autocomplete #entityAutocomplete="matAutocomplete" |
|||
class="tb-autocomplete" |
|||
[displayWith]="displayEntityFn"> |
|||
<mat-option *ngFor="let entity of filteredEntities | async" [value]="entity"> |
|||
<span [innerHTML]="entity.title | highlight:searchText"></span> |
|||
</mat-option> |
|||
<mat-option *ngIf="!(filteredEntities | async)?.length" [value]="null"> |
|||
<span> |
|||
{{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }} |
|||
</span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<div matSuffix> |
|||
<ng-content select="[matSuffix]"></ng-content> |
|||
</div> |
|||
</mat-form-field> |
|||
@ -0,0 +1,182 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { Observable } from 'rxjs'; |
|||
import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@app/core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { BaseData } from '@shared/models/base-data'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { MatAutocomplete } from '@angular/material/autocomplete'; |
|||
import { MatChipGrid } from '@angular/material/chips'; |
|||
import { SubscriptSizing } from '@angular/material/form-field'; |
|||
import { OAuth2Service } from '@core/http/oauth2.service'; |
|||
import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; |
|||
import { PageLink } from '@app/shared/models/page/page-link'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-client-list', |
|||
templateUrl: './client-list.component.html', |
|||
styleUrls: [], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ClientListComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class ClientListComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
entityListFormGroup: UntypedFormGroup; |
|||
|
|||
modelValue: Array<OAuth2ClientInfo> | null; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
subscriptSizing: SubscriptSizing = 'fixed'; |
|||
|
|||
@ViewChild('entityInput') entityInput: ElementRef<HTMLInputElement>; |
|||
@ViewChild('entityAutocomplete') matAutocomplete: MatAutocomplete; |
|||
@ViewChild('chipList', {static: true}) chipList: MatChipGrid; |
|||
|
|||
entities: Array<BaseData<EntityId>> = []; |
|||
filteredEntities: Observable<Array<BaseData<EntityId>>>; |
|||
|
|||
searchText = ''; |
|||
|
|||
private dirty = false; |
|||
|
|||
private propagateChange = (v: any) => { |
|||
}; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
public translate: TranslateService, |
|||
private oauth2Service: OAuth2Service, |
|||
private fb: UntypedFormBuilder) { |
|||
this.entityListFormGroup = this.fb.group({ |
|||
entities: [this.entities], |
|||
entity: [null] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.filteredEntities = this.entityListFormGroup.get('entity').valueChanges |
|||
.pipe( |
|||
tap((value) => { |
|||
if (value && typeof value !== 'string') { |
|||
this.add(value); |
|||
} else if (value === null) { |
|||
this.clear(this.entityInput.nativeElement.value); |
|||
} |
|||
}), |
|||
filter((value) => typeof value === 'string'), |
|||
map((value) => value ? (typeof value === 'string' ? value : value.title) : ''), |
|||
mergeMap(name => this.fetchEntities(name)), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.entityListFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.entityListFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: Array<OAuth2ClientInfo> | null): void { |
|||
this.searchText = ''; |
|||
if (value != null && value.length > 0) { |
|||
this.modelValue = [...value]; |
|||
this.entities = [...value]; |
|||
} else { |
|||
this.entities = []; |
|||
this.entityListFormGroup.get('entities').setValue(this.entities); |
|||
this.modelValue = null; |
|||
} |
|||
this.dirty = true; |
|||
} |
|||
|
|||
add(entity: OAuth2ClientInfo): void { |
|||
if (!this.modelValue || !this.modelValue.find(client => client.id.id === entity.id.id)) { |
|||
if (!this.modelValue) { |
|||
this.modelValue = []; |
|||
} |
|||
this.modelValue.push(entity); |
|||
this.entities.push(entity); |
|||
this.entityListFormGroup.get('entities').setValue(this.entities); |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
this.clear(); |
|||
} |
|||
|
|||
remove(entity: OAuth2ClientInfo) { |
|||
let index = this.entities.indexOf(entity); |
|||
if (index >= 0) { |
|||
this.entities.splice(index, 1); |
|||
this.entityListFormGroup.get('entities').setValue(this.entities); |
|||
index = this.modelValue.indexOf(entity); |
|||
this.modelValue.splice(index, 1); |
|||
if (!this.modelValue.length) { |
|||
this.modelValue = null; |
|||
} |
|||
this.propagateChange(this.modelValue); |
|||
this.clear(); |
|||
} |
|||
} |
|||
|
|||
displayEntityFn(entity?: OAuth2ClientInfo): string | undefined { |
|||
return entity ? entity.title : undefined; |
|||
} |
|||
|
|||
fetchEntities(searchText?: string): Observable<Array<OAuth2ClientInfo>> { |
|||
this.searchText = searchText; |
|||
|
|||
return this.oauth2Service.findTenantOAuth2ClientInfos(new PageLink(100, 0, searchText), {ignoreLoading: true}).pipe( |
|||
map((data) => data ? data.data : [])); |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.entityListFormGroup.get('entity').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
clear(value: string = '') { |
|||
this.entityInput.nativeElement.value = value; |
|||
this.entityListFormGroup.get('entity').patchValue(value, {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.entityInput.nativeElement.blur(); |
|||
this.entityInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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 fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" |
|||
class="title-container"> |
|||
<span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span> |
|||
<div tb-help="oauth2Settings"></div> |
|||
</div> |
|||
@ -0,0 +1,34 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 } from '@angular/core'; |
|||
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { OAuth2Client, OAuth2ClientInfo } from '@shared/models/oauth2.models'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-client-table-header', |
|||
templateUrl: './client-table-header.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class ClientTableHeaderComponent extends EntityTableHeaderComponent<OAuth2Client, PageLink, OAuth2ClientInfo> { |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
} |
|||
@ -0,0 +1,331 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
<section class="tb-form-panel no-gap no-border" [class.no-padding]="isAdd" [formGroup]="entityForm"> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.title</mat-label> |
|||
<input matInput formControlName="title"> |
|||
<mat-error *ngIf="entityForm.get('title').hasError('required')"> |
|||
{{ 'admin.oauth2.client-title-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs"> |
|||
<div class="tb-flex" formGroupName="additionalInfo"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.provider</mat-label> |
|||
<mat-select formControlName="providerName"> |
|||
<mat-option *ngFor="let provider of templateProvider" [value]="provider"> |
|||
{{ provider }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<mat-form-field class="flex" floatLabel="always"> |
|||
<mat-label translate>admin.oauth2.allowed-platforms</mat-label> |
|||
<mat-select formControlName="platforms" multiple placeholder="{{ 'admin.oauth2.all-platforms' | translate }}"> |
|||
<mat-option *ngFor="let platform of platformTypes" [value]="platform"> |
|||
{{ platformTypeTranslations.get(platform) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.client-id</mat-label> |
|||
<input matInput formControlName="clientId"> |
|||
<mat-error *ngIf="entityForm.get('clientId').hasError('required')"> |
|||
{{ 'admin.oauth2.client-id-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('clientId').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.client-id-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.client-secret</mat-label> |
|||
<input matInput formControlName="clientSecret"> |
|||
<mat-error *ngIf="entityForm.get('clientSecret').hasError('required')"> |
|||
{{ 'admin.oauth2.client-secret-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('clientSecret').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.client-secret-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<mat-expansion-panel class="configuration-panel mat-elevation-z0" [expanded]=""> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title fxLayoutAlign="start center" translate> |
|||
admin.oauth2.advanced-settings |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<ng-template matExpansionPanelContent> |
|||
<section class="tb-form-panel no-border no-padding"> |
|||
<div class="tb-form-row column-xs no-border no-padding space-between"> |
|||
<span></span> |
|||
<tb-toggle-select appearance="fill" [(ngModel)]="generalSettingsMode" [ngModelOptions]="{ standalone: true }"> |
|||
<tb-toggle-option [value]="true">{{ "admin.oauth2.general" | translate }}</tb-toggle-option> |
|||
<tb-toggle-option [value]="false">{{ 'admin.oauth2.mapper' | translate }}</tb-toggle-option> |
|||
</tb-toggle-select> |
|||
</div> |
|||
<section class="tb-form-panel no-gap no-border no-padding" style="padding-bottom: 16px" |
|||
*ngIf="generalSettingsMode"> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.access-token-uri</mat-label> |
|||
<input matInput formControlName="accessTokenUri"> |
|||
<mat-error *ngIf="entityForm.get('accessTokenUri').hasError('required')"> |
|||
{{ 'admin.oauth2.access-token-uri-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('accessTokenUri').hasError('pattern')"> |
|||
{{ 'admin.oauth2.uri-pattern-error' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.authorization-uri</mat-label> |
|||
<input matInput formControlName="authorizationUri" required> |
|||
<mat-error *ngIf="entityForm.get('authorizationUri').hasError('required')"> |
|||
{{ 'admin.oauth2.authorization-uri-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('authorizationUri').hasError('pattern')"> |
|||
{{ 'admin.oauth2.uri-pattern-error' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.jwk-set-uri</mat-label> |
|||
<input matInput formControlName="jwkSetUri"> |
|||
<mat-error *ngIf="entityForm.get('jwkSetUri').hasError('pattern')"> |
|||
{{ 'admin.oauth2.uri-pattern-error' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.user-info-uri</mat-label> |
|||
<input matInput formControlName="userInfoUri"> |
|||
<mat-error *ngIf="entityForm.get('userInfoUri').hasError('pattern')"> |
|||
{{ 'admin.oauth2.uri-pattern-error' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex"> |
|||
<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> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs" *ngIf="isCustomProvider()"> |
|||
<mat-form-field class="flex" floatLabel="always"> |
|||
<mat-label translate>admin.oauth2.login-button-label</mat-label> |
|||
<input matInput formControlName="loginButtonLabel" |
|||
placeholder="{{ 'admin.oauth2.login-button-label-placeholder' | translate }}"> |
|||
<mat-error *ngIf="entityForm.get('loginButtonLabel').hasError('required')"> |
|||
{{ 'admin.oauth2.login-button-label-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.login-button-icon</mat-label> |
|||
<input matInput formControlName="loginButtonIcon"> |
|||
</mat-form-field> |
|||
</div> |
|||
<section formGroupName="mapperConfig" style="margin-bottom: 22px"> |
|||
<div class="tb-form-row no-border no-padding"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="allowUserCreation"> |
|||
{{ 'admin.oauth2.allow-user-creation' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<div class="tb-form-row no-border no-padding"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="activateUser"> |
|||
{{ 'admin.oauth2.activate-user' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</section> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.scope</mat-label> |
|||
<mat-chip-grid #scopeList [disabled]="!isEdit"> |
|||
<mat-chip-row *ngFor="let scope of entityForm.get('scope').value; let k = index; trackBy: trackByParams" |
|||
removable (removed)="removeScope(k, entityForm)"> |
|||
{{scope}} |
|||
<mat-icon matChipRemove>cancel</mat-icon> |
|||
</mat-chip-row> |
|||
<input [matChipInputFor]="scopeList" |
|||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" |
|||
matChipInputAddOnBlur |
|||
(matChipInputTokenEnd)="addScope($event, entityForm)"> |
|||
</mat-chip-grid> |
|||
<mat-error *ngIf="entityForm.get('scope').hasError('required')"> |
|||
{{ 'admin.oauth2.scope-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<tb-error style="display: block; margin-top: -24px;" |
|||
[error]="entityForm.get('scope').hasError('required') ? ('admin.oauth2.scope-required' | translate) : ''"> |
|||
</tb-error> |
|||
</section> |
|||
<section class="tb-form-panel no-border no-padding no-gap" *ngIf="!generalSettingsMode"> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.user-name-attribute-name</mat-label> |
|||
<input matInput formControlName="userNameAttributeName"> |
|||
<mat-error *ngIf="entityForm.get('userNameAttributeName').hasError('required')"> |
|||
{{ 'admin.oauth2.user-name-attribute-name-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<section class="tb-form-panel no-border no-padding no-gap" formGroupName="mapperConfig"> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.type</mat-label> |
|||
<mat-select formControlName="type"> |
|||
<mat-option *ngFor="let mapperType of mapperTypes" |
|||
[value]="mapperType"> |
|||
{{ mapperType }} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</div> |
|||
<section class="tb-form-panel no-border no-padding no-gap" formGroupName="basic" |
|||
*ngIf="entityForm.get('mapperConfig.type').value !== mapperType.CUSTOM && entityForm.get('mapperConfig.basic')"> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding" |
|||
*ngIf="entityForm.get('mapperConfig.type').value !== mapperType.GITHUB"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.email-attribute-key</mat-label> |
|||
<input matInput formControlName="emailAttributeKey"> |
|||
<mat-error |
|||
*ngIf="entityForm.get('mapperConfig.basic.emailAttributeKey').hasError('required')"> |
|||
{{ 'admin.oauth2.email-attribute-key-required' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="entityForm.get('mapperConfig.basic.emailAttributeKey').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.email-attribute-key-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.first-name-attribute-key</mat-label> |
|||
<input matInput formControlName="firstNameAttributeKey"> |
|||
<mat-error *ngIf="entityForm.get('mapperConfig.basic.firstNameAttributeKey').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.last-name-attribute-key</mat-label> |
|||
<input matInput formControlName="lastNameAttributeKey"> |
|||
<mat-error *ngIf="entityForm.get('mapperConfig.basic.lastNameAttributeKey').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.first-name-attribute-key</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 class="flex" |
|||
[fxShow]="entityForm.get('mapperConfig.basic.tenantNameStrategy').value === 'CUSTOM'"> |
|||
<mat-label translate>admin.oauth2.tenant-name-pattern</mat-label> |
|||
<input matInput formControlName="tenantNamePattern" |
|||
[required]="entityForm.get('mapperConfig.basic.tenantNameStrategy').value === 'CUSTOM'"> |
|||
<mat-error |
|||
*ngIf="entityForm.get('mapperConfig.basic.tenantNamePattern').hasError('required')"> |
|||
{{ 'admin.oauth2.tenant-name-pattern-required' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="entityForm.get('mapperConfig.basic.tenantNamePattern').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.customer-name-pattern</mat-label> |
|||
<input matInput formControlName="customerNamePattern"> |
|||
<mat-error *ngIf="entityForm.get('mapperConfig.basic.customerNamePattern').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.default-dashboard-name</mat-label> |
|||
<input matInput formControlName="defaultDashboardName"> |
|||
<mat-error *ngIf="entityForm.get('mapperConfig.basic.defaultDashboardName').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-slide-toggle class="tb-flex mat-slide" style="margin-bottom: 16px" |
|||
formControlName="alwaysFullScreen"> |
|||
{{ 'admin.oauth2.always-fullscreen' | translate}} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</section> |
|||
<section class="tb-form-panel no-border no-padding no-gap" formGroupName="custom" |
|||
*ngIf="entityForm.get('mapperConfig.type').value === mapperType.CUSTOM && entityForm.get('mapperConfig.custom')"> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.url</mat-label> |
|||
<input matInput formControlName="url"> |
|||
<mat-error *ngIf="entityForm.get('mapperConfig.custom.url').hasError('required')"> |
|||
{{ 'admin.oauth2.url-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('mapperConfig.custom.url').hasError('pattern')"> |
|||
{{ 'admin.oauth2.url-pattern' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('mapperConfig.custom.url').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.url-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding column-xs"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>common.username</mat-label> |
|||
<input matInput formControlName="username" autocomplete="new-username"> |
|||
<mat-error *ngIf="entityForm.get('mapperConfig.custom.username').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.username-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>common.password</mat-label> |
|||
<input matInput formControlName="password" autocomplete="new-password"> |
|||
<mat-error *ngIf="entityForm.get('mapperConfig.custom.password').hasError('maxlength')"> |
|||
{{ 'admin.oauth2.password-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<mat-slide-toggle class="tb-flex mat-slide" style="margin-bottom: 16px" |
|||
formControlName="sendToken"> |
|||
{{ 'admin.oauth2.send-token' | translate}} |
|||
</mat-slide-toggle> |
|||
</section> |
|||
</section> |
|||
</section> |
|||
</section> |
|||
</ng-template> |
|||
</mat-expansion-panel> |
|||
</section> |
|||
@ -0,0 +1,36 @@ |
|||
/** |
|||
* Copyright © 2016-2024 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. |
|||
*/ |
|||
|
|||
::ng-deep { |
|||
.mat-expansion-panel { |
|||
.mat-expansion-panel-header { |
|||
height: 48px; |
|||
padding: 0 12px; |
|||
|
|||
&.mat-expanded { |
|||
height: 48px; |
|||
} |
|||
} |
|||
|
|||
.mat-expansion-panel-body { |
|||
padding: 0 12px; |
|||
} |
|||
|
|||
&.configuration-panel { |
|||
border: 1px solid rgba(0, 0, 0, 0.2); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,377 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { ChangeDetectorRef, Component, Inject, Optional } from '@angular/core'; |
|||
import { EntityComponent } from '@home/components/entity/entity.component'; |
|||
import { |
|||
ClientAuthenticationMethod, |
|||
MapperType, |
|||
OAuth2BasicMapperConfig, |
|||
OAuth2Client, |
|||
OAuth2ClientInfo, |
|||
OAuth2ClientRegistrationTemplate, |
|||
OAuth2CustomMapperConfig, |
|||
OAuth2MapperConfig, |
|||
PlatformType, |
|||
platformTypeTranslations, |
|||
TenantNameStrategyType |
|||
} from '@shared/models/oauth2.models'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { |
|||
AbstractControl, |
|||
UntypedFormArray, |
|||
UntypedFormBuilder, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { OAuth2Service } from '@core/http/oauth2.service'; |
|||
import { Subscription } from 'rxjs'; |
|||
import { MatChipInputEvent } from '@angular/material/chips'; |
|||
import { COMMA, ENTER } from '@angular/cdk/keycodes'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-client', |
|||
templateUrl: './client.component.html', |
|||
styleUrls: ['./client.component.scss'] |
|||
}) |
|||
export class ClientComponent extends EntityComponent<OAuth2Client, PageLink, OAuth2ClientInfo> { |
|||
|
|||
templateProvider = ['Custom']; |
|||
private templates = new Map<string, OAuth2ClientRegistrationTemplate>(); |
|||
private defaultProvider = { |
|||
additionalInfo: { |
|||
providerName: 'Custom' |
|||
}, |
|||
clientAuthenticationMethod: ClientAuthenticationMethod.POST, |
|||
userNameAttributeName: 'email', |
|||
mapperConfig: { |
|||
allowUserCreation: true, |
|||
activateUser: false, |
|||
type: MapperType.BASIC, |
|||
basic: { |
|||
emailAttributeKey: 'email', |
|||
tenantNameStrategy: TenantNameStrategyType.DOMAIN, |
|||
alwaysFullScreen: false |
|||
} |
|||
} |
|||
}; |
|||
|
|||
private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; |
|||
|
|||
private subscriptions: Array<Subscription> = []; |
|||
|
|||
public static validateScope(control: AbstractControl): ValidationErrors | null { |
|||
const scope: string[] = control.value; |
|||
if (!scope || !scope.length) { |
|||
return { |
|||
required: true |
|||
}; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
readonly separatorKeysCodes: number[] = [ENTER, COMMA]; |
|||
|
|||
clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod); |
|||
mapperType = MapperType; |
|||
mapperTypes = Object.keys(MapperType); |
|||
tenantNameStrategies = Object.keys(TenantNameStrategyType); |
|||
platformTypes = Object.values(PlatformType); |
|||
platformTypeTranslations = platformTypeTranslations; |
|||
generalSettingsMode = true; |
|||
|
|||
readonly isDialog = false; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected translate: TranslateService, |
|||
private oauth2Service: OAuth2Service, |
|||
@Optional() @Inject('entity') protected entityValue: OAuth2Client, |
|||
@Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo>, |
|||
protected cd: ChangeDetectorRef, |
|||
public fb: UntypedFormBuilder) { |
|||
super(store, fb, entityValue, entitiesTableConfigValue, cd); |
|||
this.oauth2Service.getOAuth2Template().subscribe(templates => { |
|||
this.initTemplates(templates); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
super.ngOnDestroy(); |
|||
this.subscriptions.forEach((subscription) => { |
|||
subscription.unsubscribe(); |
|||
}); |
|||
} |
|||
|
|||
buildForm(entity: OAuth2Client): UntypedFormGroup { |
|||
return this.fb.group({ |
|||
title: [entity?.title ? entity.title : '', [Validators.required]], |
|||
additionalInfo: this.fb.group({ |
|||
providerName: [entity?.additionalInfo?.providerName ? entity?.additionalInfo?.providerName : '', Validators.required] |
|||
}), |
|||
platforms: [entity?.platforms ? entity.platforms : []], |
|||
clientId: [entity?.clientId ? entity.clientId : '', [Validators.required, Validators.maxLength(255)]], |
|||
clientSecret: [entity?.clientSecret ? entity.clientSecret : '', [Validators.required, Validators.maxLength(2048)]], |
|||
accessTokenUri: [entity?.accessTokenUri ? entity.accessTokenUri : '', |
|||
[Validators.required, Validators.pattern(this.URL_REGEXP)]], |
|||
authorizationUri: [entity?.authorizationUri ? entity.authorizationUri : '', |
|||
[Validators.required, Validators.pattern(this.URL_REGEXP)]], |
|||
jwkSetUri: [entity?.jwkSetUri ? entity.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], |
|||
userInfoUri: [entity?.userInfoUri ? entity.userInfoUri : '', [Validators.pattern(this.URL_REGEXP)]], |
|||
clientAuthenticationMethod: [entity?.clientAuthenticationMethod ? |
|||
entity.clientAuthenticationMethod : ClientAuthenticationMethod.POST, Validators.required], |
|||
loginButtonLabel: [entity?.loginButtonLabel ? entity.loginButtonLabel : null, Validators.required], |
|||
loginButtonIcon: [entity?.loginButtonIcon ? entity.loginButtonIcon : null], |
|||
userNameAttributeName: [entity?.userNameAttributeName ? entity.userNameAttributeName : 'email', Validators.required], |
|||
scope: this.fb.array(entity?.scope ? entity.scope : [], ClientComponent.validateScope), |
|||
mapperConfig: this.fb.group({ |
|||
allowUserCreation: [isDefinedAndNotNull(entity?.mapperConfig?.allowUserCreation) ? |
|||
entity.mapperConfig.allowUserCreation : true], |
|||
activateUser: [isDefinedAndNotNull(entity?.mapperConfig?.activateUser) ? entity.mapperConfig.activateUser : false], |
|||
type: [entity?.mapperConfig?.type ? entity.mapperConfig.type : MapperType.BASIC, Validators.required] |
|||
}) |
|||
}); |
|||
} |
|||
|
|||
updateForm(entity: OAuth2Client) { |
|||
this.entityForm.patchValue({ |
|||
title: entity.title, |
|||
additionalInfo: { |
|||
providerName: entity.additionalInfo.providerName |
|||
}, |
|||
platforms: entity.platforms, |
|||
clientId: entity.clientId, |
|||
clientSecret: entity.clientSecret, |
|||
accessTokenUri: entity.accessTokenUri, |
|||
authorizationUri: entity.authorizationUri, |
|||
jwkSetUri: entity.jwkSetUri, |
|||
userInfoUri: entity.userInfoUri, |
|||
clientAuthenticationMethod: entity.clientAuthenticationMethod, |
|||
loginButtonLabel: entity.loginButtonLabel, |
|||
loginButtonIcon: entity.loginButtonIcon, |
|||
userNameAttributeName: entity.userNameAttributeName, |
|||
mapperConfig: { |
|||
allowUserCreation: entity.mapperConfig.allowUserCreation, |
|||
activateUser: entity.mapperConfig.activateUser, |
|||
type: entity.mapperConfig.type |
|||
} |
|||
}, {emitEvent: false}); |
|||
|
|||
const mapperConfig = this.entityForm.get('mapperConfig') as UntypedFormGroup; |
|||
if (entity.mapperConfig.basic) { |
|||
mapperConfig.removeControl('custom', {emitEvent: false}); |
|||
if (mapperConfig.get('basic')) { |
|||
this.entityForm.get('mapperConfig').patchValue({ |
|||
basic: entity.mapperConfig.basic |
|||
}); |
|||
} else { |
|||
mapperConfig.addControl('basic', this.formBasicGroup(entity.mapperConfig.basic)); |
|||
} |
|||
} else { |
|||
mapperConfig.removeControl('basic', {emitEvent: false}); |
|||
if (mapperConfig.get('custom')) { |
|||
this.entityForm.get('mapperConfig').patchValue({ |
|||
custom: entity.mapperConfig.custom |
|||
}); |
|||
} else { |
|||
mapperConfig.addControl('custom', this.formCustomGroup(entity.mapperConfig.custom)); |
|||
} |
|||
} |
|||
|
|||
const scopeControls = this.entityForm.get('scope') as UntypedFormArray; |
|||
if (entity.scope.length === scopeControls.length) { |
|||
scopeControls.patchValue(entity.scope, {emitEvent: false}); |
|||
} else { |
|||
const scopeControls: Array<AbstractControl> = []; |
|||
if (entity.scope) { |
|||
for (const scope of entity.scope) { |
|||
scopeControls.push(this.fb.control(scope, [Validators.required])); |
|||
} |
|||
} |
|||
this.entityForm.setControl('scope', this.fb.array(scopeControls)); |
|||
} |
|||
} |
|||
|
|||
getProviderName(): string { |
|||
return this.entityForm.get('additionalInfo.providerName').value; |
|||
} |
|||
|
|||
isCustomProvider(): boolean { |
|||
return this.getProviderName() === 'Custom'; |
|||
} |
|||
|
|||
trackByParams(index: number): number { |
|||
return index; |
|||
} |
|||
|
|||
trackByItem(i, item) { |
|||
return item; |
|||
} |
|||
|
|||
addScope(event: MatChipInputEvent, control: AbstractControl): void { |
|||
const input = event.chipInput.inputElement; |
|||
const value = event.value; |
|||
const controller = control.get('scope') as UntypedFormArray; |
|||
if ((value.trim() !== '')) { |
|||
controller.push(this.fb.control(value.trim())); |
|||
controller.markAsDirty(); |
|||
} |
|||
|
|||
if (input) { |
|||
input.value = ''; |
|||
} |
|||
} |
|||
|
|||
removeScope(i: number, control: AbstractControl): void { |
|||
const controller = control.get('scope') as UntypedFormArray; |
|||
controller.removeAt(i); |
|||
controller.markAsTouched(); |
|||
controller.markAsDirty(); |
|||
} |
|||
|
|||
private initTemplates(templates: OAuth2ClientRegistrationTemplate[]): void { |
|||
templates.map(provider => { |
|||
delete provider.additionalInfo; |
|||
this.templates.set(provider.name, provider); |
|||
}); |
|||
this.templateProvider.push(...Array.from(this.templates.keys())); |
|||
this.templateProvider.sort(); |
|||
|
|||
let additionalInfo = null; |
|||
if (isDefinedAndNotNull(this.entityValue?.additionalInfo)) { |
|||
additionalInfo = this.entityValue.additionalInfo; |
|||
if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) { |
|||
additionalInfo.providerName = 'Custom'; |
|||
} |
|||
} |
|||
let defaultProviderName = 'Custom'; |
|||
if (this.templateProvider.indexOf('Google')) { |
|||
defaultProviderName = 'Google'; |
|||
} |
|||
|
|||
this.entityForm.get('additionalInfo.providerName').setValue(additionalInfo?.providerName ? |
|||
additionalInfo?.providerName : defaultProviderName); |
|||
|
|||
if (this.entityValue?.mapperConfig) { |
|||
this.changeMapperConfigType(this.entityForm, this.entityValue.mapperConfig.type, this.entityValue.mapperConfig); |
|||
} else { |
|||
this.changeMapperConfigType(this.entityForm, MapperType.BASIC); |
|||
this.setProviderDefaultValue(defaultProviderName, this.entityForm); |
|||
} |
|||
|
|||
this.subscriptions.push(this.entityForm.get('mapperConfig.type').valueChanges.subscribe((value) => { |
|||
this.changeMapperConfigType(this.entityForm, value); |
|||
})); |
|||
|
|||
this.subscriptions.push(this.entityForm.get('additionalInfo.providerName').valueChanges.subscribe((provider) => { |
|||
(this.entityForm.get('scope') as UntypedFormArray).clear(); |
|||
this.setProviderDefaultValue(provider, this.entityForm); |
|||
})); |
|||
} |
|||
|
|||
private changeMapperConfigType(control: AbstractControl, type: MapperType, predefinedValue?: OAuth2MapperConfig) { |
|||
const mapperConfig = control.get('mapperConfig') as UntypedFormGroup; |
|||
if (type === MapperType.CUSTOM) { |
|||
mapperConfig.removeControl('basic'); |
|||
mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom)); |
|||
} else { |
|||
mapperConfig.removeControl('custom'); |
|||
if (!mapperConfig.get('basic')) { |
|||
mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic)); |
|||
} |
|||
if (type === MapperType.GITHUB) { |
|||
mapperConfig.get('basic.emailAttributeKey').disable(); |
|||
mapperConfig.get('basic.emailAttributeKey').patchValue(null, {emitEvent: false}); |
|||
} else { |
|||
if (this.isEdit) { |
|||
mapperConfig.get('basic.emailAttributeKey').enable(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private formCustomGroup(mapperConfigCustom?: OAuth2CustomMapperConfig): UntypedFormGroup { |
|||
const customGroup = this.fb.group({ |
|||
url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, |
|||
[Validators.required, Validators.pattern(this.URL_REGEXP), Validators.maxLength(255)]], |
|||
username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null, Validators.maxLength(255)], |
|||
password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null, Validators.maxLength(255)], |
|||
sendToken: [isDefinedAndNotNull(mapperConfigCustom?.sendToken) ? mapperConfigCustom.sendToken : false] |
|||
}); |
|||
if (!this.isEdit) { |
|||
customGroup.disable(); |
|||
} |
|||
return customGroup; |
|||
} |
|||
|
|||
private formBasicGroup(mapperConfigBasic?: OAuth2BasicMapperConfig): UntypedFormGroup { |
|||
let tenantNamePattern; |
|||
if (mapperConfigBasic?.tenantNamePattern) { |
|||
tenantNamePattern = mapperConfigBasic.tenantNamePattern; |
|||
} else { |
|||
tenantNamePattern = {value: null, disabled: true}; |
|||
} |
|||
const basicGroup = this.fb.group({ |
|||
emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email', |
|||
[Validators.required, Validators.maxLength(31)]], |
|||
firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : '', |
|||
Validators.maxLength(31)], |
|||
lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : '', |
|||
Validators.maxLength(31)], |
|||
tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategyType.DOMAIN], |
|||
tenantNamePattern: [tenantNamePattern, [Validators.required, Validators.maxLength(255)]], |
|||
customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null, |
|||
Validators.maxLength(255)], |
|||
defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null, |
|||
Validators.maxLength(255)], |
|||
alwaysFullScreen: [isDefinedAndNotNull(mapperConfigBasic?.alwaysFullScreen) ? mapperConfigBasic.alwaysFullScreen : false] |
|||
}); |
|||
|
|||
if (!this.isEdit) { |
|||
basicGroup.disable(); |
|||
} |
|||
|
|||
this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => { |
|||
if (domain === 'CUSTOM') { |
|||
basicGroup.get('tenantNamePattern').enable(); |
|||
} else { |
|||
basicGroup.get('tenantNamePattern').disable(); |
|||
} |
|||
})); |
|||
|
|||
return basicGroup; |
|||
} |
|||
|
|||
private setProviderDefaultValue(provider: string, clientRegistration: UntypedFormGroup) { |
|||
if (provider === 'Custom') { |
|||
clientRegistration.reset(this.defaultProvider, {emitEvent: false}); |
|||
} else { |
|||
const template = this.templates.get(provider); |
|||
template.clientId = ''; |
|||
template.clientSecret = ''; |
|||
template.scope.forEach(() => { |
|||
(clientRegistration.get('scope') as UntypedFormArray).push(this.fb.control('')); |
|||
}); |
|||
clientRegistration.patchValue(template); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { Resolve } from '@angular/router'; |
|||
import { |
|||
DateEntityTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@home/models/entity/entities-table-config.models'; |
|||
import { |
|||
getProviderHelpLink, |
|||
OAuth2Client, |
|||
OAuth2ClientInfo, |
|||
platformTypeTranslations |
|||
} from '@shared/models/oauth2.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import { OAuth2Service } from '@core/http/oauth2.service'; |
|||
import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component'; |
|||
import { ClientTableHeaderComponent } from '@home/pages/admin/oauth2/clients/client-table-header.component'; |
|||
import { Direction } from '@shared/models/page/sort-order'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
|
|||
@Injectable() |
|||
export class ClientsTableConfigResolver implements Resolve<EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo>> { |
|||
|
|||
private readonly config: EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo> = new EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo>(); |
|||
|
|||
constructor(private translate: TranslateService, |
|||
private datePipe: DatePipe, |
|||
private utilsService: UtilsService, |
|||
private oauth2Service: OAuth2Service) { |
|||
this.config.tableTitle = this.translate.instant('admin.oauth2.clients'); |
|||
this.config.hideTableTitle = true; |
|||
this.config.selectionEnabled = false; |
|||
this.config.entityType = EntityType.OAUTH2_CLIENT; |
|||
this.config.rowPointer = true; |
|||
this.config.entityTranslations = entityTypeTranslations.get(EntityType.OAUTH2_CLIENT); |
|||
this.config.entityResources = { |
|||
helpLinkId: null, |
|||
helpLinkIdForEntity(entity: OAuth2Client): string { |
|||
return getProviderHelpLink(entity.additionalInfo.providerName); |
|||
} |
|||
}; |
|||
this.config.entityComponent = ClientComponent; |
|||
this.config.headerComponent = ClientTableHeaderComponent; |
|||
this.config.hideDetailsTabs = true; |
|||
this.config.addDialogStyle = {width: '850px', maxHeight: '100vh'}; |
|||
this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; |
|||
this.config.displayPagination = false; |
|||
this.config.pageMode = false; |
|||
|
|||
this.config.columns.push( |
|||
new DateEntityTableColumn<OAuth2ClientInfo>('createdTime', 'common.created-time', this.datePipe, '170px'), |
|||
new EntityTableColumn<OAuth2ClientInfo>('title', 'admin.oauth2.provider', '170px'), |
|||
new EntityTableColumn<OAuth2ClientInfo>('platforms', 'admin.oauth2.allowed-platforms', '100%', |
|||
(clientInfo) => { |
|||
return clientInfo.platforms && clientInfo.platforms.length ? |
|||
clientInfo.platforms.map(platform => this.translate.instant(platformTypeTranslations.get(platform))).join(', ') : |
|||
this.translate.instant('admin.oauth2.all-platforms'); |
|||
}, () => ({}), false) |
|||
); |
|||
|
|||
this.config.deleteEntityTitle = (client) => this.translate.instant('admin.oauth2.delete-client-title', {clientName: client.title}); |
|||
this.config.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-client-text'); |
|||
this.config.entitiesFetchFunction = pageLink => this.oauth2Service.findTenantOAuth2ClientInfos(pageLink); |
|||
this.config.loadEntity = id => this.oauth2Service.getOAuth2ClientById(id.id); |
|||
this.config.saveEntity = client => { |
|||
return this.oauth2Service.saveOAuth2Client(client as OAuth2Client); |
|||
} |
|||
this.config.deleteEntity = id => this.oauth2Service.deleteOauth2Client(id.id); |
|||
} |
|||
|
|||
resolve(): EntityTableConfig<OAuth2Client, PageLink, OAuth2ClientInfo> { |
|||
return this.config; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { ActivatedRouteSnapshot, Resolve } from '@angular/router'; |
|||
import { |
|||
ClientChipsEntityTableColumn, |
|||
DateEntityTableColumn, |
|||
EntityActionTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@home/models/entity/entities-table-config.models'; |
|||
import { Domain, DomainInfo } from '@shared/models/oauth2.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import { DomainService } from '@app/core/http/domain.service'; |
|||
import { DomainComponent } from '@home/pages/admin/oauth2/domains/domain.component'; |
|||
import { isEqual } from '@core/utils'; |
|||
import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component'; |
|||
import { Direction } from '@app/shared/models/page/sort-order'; |
|||
|
|||
@Injectable() |
|||
export class DomainTableConfigResolver implements Resolve<EntityTableConfig<DomainInfo>> { |
|||
|
|||
private readonly config: EntityTableConfig<DomainInfo> = new EntityTableConfig<DomainInfo>(); |
|||
|
|||
constructor(private translate: TranslateService, |
|||
private datePipe: DatePipe, |
|||
private utilsService: UtilsService, |
|||
private domainService: DomainService) { |
|||
this.config.tableTitle = this.translate.instant('admin.oauth2.domains'); |
|||
this.config.hideTableTitle = true; |
|||
this.config.selectionEnabled = false; |
|||
this.config.entityType = EntityType.DOMAIN; |
|||
this.config.rowPointer = true; |
|||
this.config.entityTranslations = entityTypeTranslations.get(EntityType.DOMAIN); |
|||
this.config.entityResources = entityTypeResources.get(EntityType.DOMAIN); |
|||
this.config.entityComponent = DomainComponent; |
|||
this.config.headerComponent = DomainTableHeaderComponent; |
|||
this.config.hideDetailsTabs = true; |
|||
this.config.addDialogStyle = {width: '850px', height: '688px'}; |
|||
this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; |
|||
this.config.displayPagination = false; |
|||
this.config.pageMode = false; |
|||
|
|||
this.config.columns.push( |
|||
new DateEntityTableColumn<DomainInfo>('createdTime', 'common.created-time', this.datePipe, '170px'), |
|||
new EntityTableColumn<DomainInfo>('name', 'admin.oauth2.domain-name', '170px'), |
|||
new ClientChipsEntityTableColumn<DomainInfo>('oauth2ClientInfos', 'admin.oauth2.clients', '40%'), |
|||
new EntityActionTableColumn('oauth2Enabled', 'admin.oauth2.enable', |
|||
{ |
|||
name: '', |
|||
nameFunction: (domain) => |
|||
this.translate.instant(domain.oauth2Enabled ? 'admin.oauth2.disable' : 'admin.oauth2.enable'), |
|||
icon: 'mdi:toggle-switch', |
|||
iconFunction: (entity) => entity.oauth2Enabled ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', |
|||
isEnabled: () => true, |
|||
onAction: ($event, entity) => this.toggleEnableOAuth($event, entity) |
|||
}), |
|||
new EntityActionTableColumn('propagateToEdge', 'admin.oauth2.edge', |
|||
{ |
|||
name: '', |
|||
nameFunction: (domain) => |
|||
this.translate.instant(domain.propagateToEdge ? 'admin.oauth2.edge-disable' : 'admin.oauth2.edge-enable'), |
|||
icon: 'mdi:toggle-switch', |
|||
iconFunction: (entity) => entity.propagateToEdge ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', |
|||
isEnabled: () => true, |
|||
onAction: ($event, entity) => this.togglePropagateToEdge($event, entity) |
|||
}) |
|||
); |
|||
|
|||
this.config.deleteEntityTitle = (domain) => this.translate.instant('admin.oauth2.delete-domain-title', {domainName: domain.name}); |
|||
this.config.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-domain-text'); |
|||
this.config.entitiesFetchFunction = pageLink => this.domainService.getTenantDomainInfos(pageLink); |
|||
this.config.loadEntity = id => this.domainService.getDomainInfoById(id.id); |
|||
this.config.saveEntity = (domain, originalEntity) => { |
|||
const clientsIds = domain.oauth2ClientInfos ? domain.oauth2ClientInfos.map(clientInfo => clientInfo.id.id) : []; |
|||
if (domain.id && !isEqual(domain.oauth2ClientInfos, originalEntity.oauth2ClientInfos)) { |
|||
this.domainService.updateOauth2Clients(domain.id.id, clientsIds).subscribe(); |
|||
} |
|||
delete domain.oauth2ClientInfos; |
|||
return this.domainService.saveDomain(domain as Domain, domain.id ? [] : clientsIds); |
|||
} |
|||
this.config.deleteEntity = id => this.domainService.deleteDomain(id.id); |
|||
} |
|||
|
|||
resolve(route: ActivatedRouteSnapshot): EntityTableConfig<DomainInfo> { |
|||
return this.config; |
|||
} |
|||
|
|||
private toggleEnableOAuth($event: Event, domain: DomainInfo): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
|
|||
const modifiedDomain: DomainInfo = { |
|||
...domain, |
|||
oauth2Enabled: !domain.oauth2Enabled |
|||
}; |
|||
|
|||
this.domainService.saveDomain(modifiedDomain, domain.oauth2ClientInfos.map(clientInfo => clientInfo.id.id), |
|||
{ignoreLoading: true}) |
|||
.subscribe((result) => { |
|||
domain.oauth2Enabled = result.oauth2Enabled; |
|||
this.config.getTable().detectChanges(); |
|||
}); |
|||
} |
|||
|
|||
private togglePropagateToEdge($event: Event, domain: DomainInfo): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
|
|||
const modifiedDomain: DomainInfo = { |
|||
...domain, |
|||
propagateToEdge: !domain.propagateToEdge |
|||
}; |
|||
|
|||
this.domainService.saveDomain(modifiedDomain, domain.oauth2ClientInfos.map(clientInfo => clientInfo.id.id), |
|||
{ignoreLoading: true}) |
|||
.subscribe((result) => { |
|||
domain.propagateToEdge = result.propagateToEdge; |
|||
this.config.getTable().detectChanges(); |
|||
}); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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 fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> |
|||
<span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span> |
|||
<div tb-help="oauth2Settings"></div> |
|||
</div> |
|||
@ -0,0 +1,33 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 } from '@angular/core'; |
|||
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { DomainInfo } from '@shared/models/oauth2.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-domain-table-header', |
|||
templateUrl: './domain-table-header.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class DomainTableHeaderComponent extends EntityTableHeaderComponent<DomainInfo> { |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
<section class="tb-form-panel no-gap no-border" [class.no-padding]="isAdd" [formGroup]="entityForm"> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.domain-name</mat-label> |
|||
<input matInput formControlName="name"> |
|||
<mat-error *ngIf="entityForm.get('name').hasError('required')"> |
|||
{{ 'admin.oauth2.domain-name-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('name').hasError('maxlength')"> |
|||
{{ 'admin.domain-name-max-length' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('name').hasError('pattern')"> |
|||
{{ 'admin.error-verification-url' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex"> |
|||
<mat-label translate>admin.oauth2.redirect-url-template</mat-label> |
|||
<input matInput [value]="redirectURI()" readonly> |
|||
<tb-copy-button |
|||
matSuffix |
|||
miniButton="false" |
|||
color="primary" |
|||
[copyText]="redirectURI()" |
|||
tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}" |
|||
tooltipPosition="above" |
|||
icon="content_copy"> |
|||
</tb-copy-button> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row no-border no-padding"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="oauth2Enabled"> |
|||
{{ 'admin.oauth2.enable' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<tb-client-list formControlName="oauth2ClientInfos"> |
|||
<button #createTargetButton |
|||
mat-button color="primary" matSuffix |
|||
[disabled]="!isEdit" |
|||
(click)="createClient($event, createTargetButton)"> |
|||
<span style="white-space: nowrap">{{ 'notification.create-new' | translate }}</span> |
|||
</button> |
|||
</tb-client-list> |
|||
<div class="tb-form-row no-border no-padding"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="propagateToEdge"> |
|||
{{ 'admin.oauth2.edge' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
</section> |
|||
|
|||
@ -0,0 +1,105 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
|||
import { EntityComponent } from '@home/components/entity/entity.component'; |
|||
import { DomainInfo } from '@shared/models/oauth2.models'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { OAuth2Service } from '@core/http/oauth2.service'; |
|||
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { WINDOW } from '@core/services/window.service'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { MatButton } from '@angular/material/button'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-domain', |
|||
templateUrl: './domain.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class DomainComponent extends EntityComponent<DomainInfo> { |
|||
|
|||
private loginProcessingUrl: string = ''; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected translate: TranslateService, |
|||
private oauth2Service: OAuth2Service, |
|||
@Inject('entity') protected entityValue: DomainInfo, |
|||
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DomainInfo>, |
|||
protected cd: ChangeDetectorRef, |
|||
public fb: UntypedFormBuilder, |
|||
@Inject(WINDOW) private window: Window, |
|||
private dialog: MatDialog) { |
|||
super(store, fb, entityValue, entitiesTableConfigValue, cd); |
|||
this.entityForm.get('name').setValue(this.window.location.hostname); |
|||
this.oauth2Service.getLoginProcessingUrl().subscribe(url => { |
|||
this.loginProcessingUrl = url; |
|||
}); |
|||
} |
|||
|
|||
buildForm(entity: DomainInfo): UntypedFormGroup { |
|||
return this.fb.group({ |
|||
name: [entity?.name ? entity.name : '', [ |
|||
Validators.required, Validators.maxLength(255), Validators.pattern('^(?:\\w+(?::\\w+)?@)?[^\\s/]+(?::\\d+)?$')]], |
|||
oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : false, |
|||
oauth2ClientInfos: entity?.oauth2ClientInfos ? entity.oauth2ClientInfos : [], |
|||
propagateToEdge: isDefinedAndNotNull(entity?.propagateToEdge) ? entity.propagateToEdge : false |
|||
}); |
|||
} |
|||
|
|||
updateForm(entity: DomainInfo) { |
|||
this.entityForm.patchValue({ |
|||
name: entity.name, |
|||
oauth2Enabled: entity.oauth2Enabled, |
|||
oauth2ClientInfos: entity.oauth2ClientInfos, |
|||
propagateToEdge: entity.propagateToEdge |
|||
}) |
|||
} |
|||
|
|||
redirectURI(): string { |
|||
const domainName = this.entityForm.get('name').value; |
|||
return domainName !== '' ? `${domainName}${this.loginProcessingUrl}` : ''; |
|||
} |
|||
|
|||
createClient($event: Event, button: MatButton) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
$event.preventDefault(); |
|||
} |
|||
button._elementRef.nativeElement.blur(); |
|||
this.dialog.open<ClientDialogComponent>(ClientDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: {} |
|||
}).afterClosed() |
|||
.subscribe((res) => { |
|||
if (res) { |
|||
const formValue = this.entityForm.get('oauth2ClientInfos').value ? |
|||
[...this.entityForm.get('oauth2ClientInfos').value] : []; |
|||
formValue.push({id: res.id, title: res.title}); |
|||
this.entityForm.get('oauth2ClientInfos').patchValue(formValue); |
|||
this.entityForm.get('oauth2ClientInfos').markAsDirty(); |
|||
this.entityForm.updateValueAndValidity(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { ActivatedRouteSnapshot, Resolve } from '@angular/router'; |
|||
import { |
|||
CellActionDescriptorType, |
|||
ClientChipsEntityTableColumn, |
|||
DateEntityTableColumn, |
|||
EntityActionTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@home/models/entity/entities-table-config.models'; |
|||
import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import { isEqual } from '@core/utils'; |
|||
import { Direction } from '@app/shared/models/page/sort-order'; |
|||
import { MobileAppService } from '@core/http/mobile-app.service'; |
|||
import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; |
|||
import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component'; |
|||
|
|||
@Injectable() |
|||
export class MobileAppTableConfigResolver implements Resolve<EntityTableConfig<MobileAppInfo>> { |
|||
|
|||
private readonly config: EntityTableConfig<MobileAppInfo> = new EntityTableConfig<MobileAppInfo>(); |
|||
|
|||
constructor(private translate: TranslateService, |
|||
private datePipe: DatePipe, |
|||
private utilsService: UtilsService, |
|||
private mobileAppService: MobileAppService) { |
|||
this.config.tableTitle = this.translate.instant('admin.oauth2.mobile-apps'); |
|||
this.config.hideTableTitle = true; |
|||
this.config.selectionEnabled = false; |
|||
this.config.entityType = EntityType.MOBILE_APP; |
|||
this.config.rowPointer = true; |
|||
this.config.entityTranslations = entityTypeTranslations.get(EntityType.MOBILE_APP); |
|||
this.config.entityResources = entityTypeResources.get(EntityType.MOBILE_APP); |
|||
this.config.entityComponent = MobileAppComponent; |
|||
this.config.headerComponent = MobileAppTableHeaderComponent; |
|||
this.config.hideDetailsTabs = true; |
|||
this.config.addDialogStyle = {width: '850px', height: '688px'}; |
|||
this.config.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; |
|||
this.config.displayPagination = false; |
|||
this.config.pageMode = false; |
|||
|
|||
this.config.columns.push( |
|||
new DateEntityTableColumn<MobileAppInfo>('createdTime', 'common.created-time', this.datePipe, '170px'), |
|||
new EntityTableColumn<MobileAppInfo>('pkgName', 'admin.oauth2.mobile-package', '170px'), |
|||
new EntityTableColumn<MobileAppInfo>('appSecret', 'admin.oauth2.mobile-app-secret', '350px', |
|||
(entity) => entity.appSecret ? this.appSecretText(entity) : '', () => ({}), |
|||
false, () => ({}), () => undefined, false, |
|||
{ |
|||
name: this.translate.instant('admin.oauth2.copy-mobile-app-secret'), |
|||
icon: 'content_copy', |
|||
style: { |
|||
'padding-top': '1px', |
|||
'font-size': '16px', |
|||
color: 'rgba(0,0,0,.87)' |
|||
}, |
|||
isEnabled: (entity) => !!entity.appSecret, |
|||
onAction: ($event, entity) => entity.appSecret, |
|||
type: CellActionDescriptorType.COPY_BUTTON |
|||
}), |
|||
new ClientChipsEntityTableColumn<MobileAppInfo>('oauth2ClientInfos', 'admin.oauth2.clients', '20%'), |
|||
new EntityActionTableColumn('oauth2Enabled', 'admin.oauth2.enable', |
|||
{ |
|||
name: '', |
|||
nameFunction: (app) => |
|||
this.translate.instant(app.oauth2Enabled ? 'admin.oauth2.disable' : 'admin.oauth2.enable'), |
|||
icon: 'mdi:toggle-switch', |
|||
iconFunction: (entity) => entity.oauth2Enabled ? 'mdi:toggle-switch' : 'mdi:toggle-switch-off-outline', |
|||
isEnabled: () => true, |
|||
onAction: ($event, entity) => this.toggleEnableOAuth($event, entity) |
|||
}) |
|||
); |
|||
|
|||
this.config.deleteEntityTitle = (app) => this.translate.instant('admin.oauth2.delete-mobile-app-title', {applicationName: app.pkgName}); |
|||
this.config.deleteEntityContent = () => this.translate.instant('admin.oauth2.delete-mobile-app-text'); |
|||
this.config.entitiesFetchFunction = pageLink => this.mobileAppService.getTenantMobileAppInfos(pageLink); |
|||
this.config.loadEntity = id => this.mobileAppService.getMobileAppInfoById(id.id); |
|||
this.config.saveEntity = (mobileApp, originalEntity) => { |
|||
const clientsIds = mobileApp.oauth2ClientInfos ? mobileApp.oauth2ClientInfos.map(clientInfo => clientInfo.id.id) : []; |
|||
if (mobileApp.id && !isEqual(mobileApp.oauth2ClientInfos, originalEntity.oauth2ClientInfos)) { |
|||
this.mobileAppService.updateOauth2Clients(mobileApp.id.id, clientsIds).subscribe(); |
|||
} |
|||
delete mobileApp.oauth2ClientInfos; |
|||
return this.mobileAppService.saveMobileApp(mobileApp as MobileApp, mobileApp.id ? [] : clientsIds); |
|||
} |
|||
this.config.deleteEntity = id => this.mobileAppService.deleteMobileApp(id.id); |
|||
} |
|||
|
|||
resolve(route: ActivatedRouteSnapshot): EntityTableConfig<MobileAppInfo> { |
|||
return this.config; |
|||
} |
|||
|
|||
private toggleEnableOAuth($event: Event, mobileApp: MobileAppInfo): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
|
|||
const modifiedMobileApp: MobileAppInfo = { |
|||
...mobileApp, |
|||
oauth2Enabled: !mobileApp.oauth2Enabled |
|||
}; |
|||
|
|||
this.mobileAppService.saveMobileApp(modifiedMobileApp, mobileApp.oauth2ClientInfos.map(clientInfo => clientInfo.id.id), |
|||
{ignoreLoading: true}) |
|||
.subscribe((result) => { |
|||
mobileApp.oauth2Enabled = result.oauth2Enabled; |
|||
this.config.getTable().detectChanges(); |
|||
}); |
|||
} |
|||
|
|||
private appSecretText(entity): string { |
|||
let text = entity.appSecret; |
|||
if (text.length > 40) { |
|||
text = `${text.slice(0, 40)}…`; |
|||
} |
|||
return text; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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 fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> |
|||
<span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span> |
|||
<div tb-help="oauth2Settings"></div> |
|||
</div> |
|||
@ -0,0 +1,33 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 } from '@angular/core'; |
|||
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { MobileAppInfo } from '@shared/models/oauth2.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mobile-app-table-header', |
|||
templateUrl: './mobile-app-table-header.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class MobileAppTableHeaderComponent extends EntityTableHeaderComponent<MobileAppInfo> { |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 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. |
|||
|
|||
--> |
|||
<section class="tb-form-panel no-border" [class.no-padding]="isAdd" [formGroup]="entityForm"> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex" floatLabel="always" subscriptSizing="dynamic"> |
|||
<mat-label translate>admin.oauth2.mobile-package</mat-label> |
|||
<input matInput formControlName="pkgName" |
|||
placeholder="{{ 'admin.oauth2.mobile-package-placeholder' | translate }}" required> |
|||
<mat-hint translate>admin.oauth2.mobile-package-hint</mat-hint> |
|||
<mat-error *ngIf="entityForm.hasError('unique')"> |
|||
{{ 'admin.oauth2.mobile-package-unique' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row tb-standard-fields no-border no-padding"> |
|||
<mat-form-field class="flex" floatLabel="always" subscriptSizing="dynamic"> |
|||
<mat-label translate>admin.oauth2.mobile-app-secret</mat-label> |
|||
<input matInput formControlName="appSecret" required> |
|||
<tb-copy-button |
|||
matSuffix |
|||
miniButton="false" |
|||
color="primary" |
|||
[copyText]="entityForm.get('appSecret').value" |
|||
tooltipText="{{ 'admin.oauth2.copy-mobile-app-secret' | translate }}" |
|||
tooltipPosition="above" |
|||
icon="content_copy"> |
|||
</tb-copy-button> |
|||
<mat-hint translate>admin.oauth2.mobile-app-secret-hint</mat-hint> |
|||
<mat-error *ngIf="entityForm.get('appSecret').hasError('required')"> |
|||
{{ 'admin.oauth2.mobile-app-secret-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('appSecret').hasError('base64')"> |
|||
{{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('appSecret').hasError('minLength')"> |
|||
{{ 'admin.oauth2.mobile-app-secret-base64' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="tb-form-row no-border no-padding"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="oauth2Enabled"> |
|||
{{ 'admin.oauth2.enable' | translate }} |
|||
</mat-slide-toggle> |
|||
</div> |
|||
<tb-client-list formControlName="oauth2ClientInfos"> |
|||
<button #createTargetButton |
|||
mat-button color="primary" matSuffix |
|||
[disabled]="!isEdit" |
|||
(click)="createClient($event, createTargetButton)"> |
|||
<span style="white-space: nowrap">{{ 'notification.create-new' | translate }}</span> |
|||
</button> |
|||
</tb-client-list> |
|||
</section> |
|||
|
|||
@ -0,0 +1,108 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
|||
import { EntityComponent } from '@home/components/entity/entity.component'; |
|||
import { MobileAppInfo } from '@shared/models/oauth2.models'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { OAuth2Service } from '@core/http/oauth2.service'; |
|||
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { WINDOW } from '@core/services/window.service'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { MatButton } from '@angular/material/button'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mobile-app', |
|||
templateUrl: './mobile-app.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class MobileAppComponent extends EntityComponent<MobileAppInfo> { |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected translate: TranslateService, |
|||
private oauth2Service: OAuth2Service, |
|||
@Inject('entity') protected entityValue: MobileAppInfo, |
|||
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<MobileAppInfo>, |
|||
protected cd: ChangeDetectorRef, |
|||
public fb: UntypedFormBuilder, |
|||
@Inject(WINDOW) private window: Window, |
|||
private dialog: MatDialog) { |
|||
super(store, fb, entityValue, entitiesTableConfigValue, cd); |
|||
} |
|||
|
|||
buildForm(entity: MobileAppInfo): UntypedFormGroup { |
|||
return this.fb.group({ |
|||
pkgName: [entity?.pkgName ? entity.pkgName : '', [Validators.required]], |
|||
appSecret: [entity?.appSecret ? entity.appSecret : '', [Validators.required, this.base64Format]], |
|||
oauth2Enabled: isDefinedAndNotNull(entity?.oauth2Enabled) ? entity.oauth2Enabled : false, |
|||
oauth2ClientInfos: entity?.oauth2ClientInfos ? entity.oauth2ClientInfos : [] |
|||
}); |
|||
} |
|||
|
|||
updateForm(entity: MobileAppInfo) { |
|||
this.entityForm.patchValue({ |
|||
pkgName: entity.pkgName, |
|||
appSecret: entity.appSecret, |
|||
oauth2Enabled: entity.oauth2Enabled, |
|||
oauth2ClientInfos: entity.oauth2ClientInfos |
|||
}) |
|||
} |
|||
|
|||
createClient($event: Event, button: MatButton) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
$event.preventDefault(); |
|||
} |
|||
button._elementRef.nativeElement.blur(); |
|||
this.dialog.open<ClientDialogComponent>(ClientDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: {} |
|||
}).afterClosed() |
|||
.subscribe((res) => { |
|||
if (res) { |
|||
const formValue = this.entityForm.get('oauth2ClientInfos').value ? |
|||
[...this.entityForm.get('oauth2ClientInfos').value] : []; |
|||
formValue.push({id: res.id, title: res.title}); |
|||
this.entityForm.get('oauth2ClientInfos').patchValue(formValue); |
|||
this.entityForm.get('oauth2ClientInfos').markAsDirty(); |
|||
this.entityForm.updateValueAndValidity(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private base64Format(control: UntypedFormControl): { [key: string]: boolean } | null { |
|||
if (control.value === '') { |
|||
return null; |
|||
} |
|||
try { |
|||
const value = atob(control.value); |
|||
if (value.length < 64) { |
|||
return {minLength: true}; |
|||
} |
|||
return null; |
|||
} catch (e) { |
|||
return {base64: true}; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,152 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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, NgModule } from '@angular/core'; |
|||
import { Resolve, RouterModule, Routes } from '@angular/router'; |
|||
import { RouterTabsComponent } from '@home/components/router-tabs.component'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; |
|||
import { OAuth2Service } from '@core/http/oauth2.service'; |
|||
import { Observable } from 'rxjs'; |
|||
import { ClientsTableConfigResolver } from './clients/clients-table-config.resolver'; |
|||
import { DomainTableConfigResolver } from '@home/pages/admin/oauth2/domains/domain-table-config.resolver'; |
|||
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; |
|||
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; |
|||
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; |
|||
import { MobileAppTableConfigResolver } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver'; |
|||
|
|||
@Injectable() |
|||
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { |
|||
|
|||
constructor(private oauth2Service: OAuth2Service) { |
|||
} |
|||
|
|||
resolve(): Observable<string> { |
|||
return this.oauth2Service.getLoginProcessingUrl(); |
|||
} |
|||
} |
|||
|
|||
export const oAuth2Routes: Routes = [ |
|||
{ |
|||
path: 'oauth2', |
|||
component: RouterTabsComponent, |
|||
data: { |
|||
auth: [Authority.SYS_ADMIN], |
|||
breadcrumb: { |
|||
label: 'admin.oauth2.oauth2', |
|||
icon: 'mdi:shield-account' |
|||
} |
|||
}, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
children: [], |
|||
data: { |
|||
auth: [Authority.SYS_ADMIN], |
|||
redirectTo: '/security-settings/oauth2/domains' |
|||
} |
|||
}, |
|||
{ |
|||
path: 'domains', |
|||
component: EntitiesTableComponent, |
|||
data: { |
|||
auth: [Authority.SYS_ADMIN], |
|||
title: 'admin.oauth2.domains', |
|||
breadcrumb: { |
|||
label: 'admin.oauth2.domains', |
|||
icon: 'domain' |
|||
} |
|||
}, |
|||
resolve: { |
|||
entitiesTableConfig: DomainTableConfigResolver |
|||
} |
|||
}, |
|||
{ |
|||
path: 'mobile-applications', |
|||
component: EntitiesTableComponent, |
|||
data: { |
|||
auth: [Authority.SYS_ADMIN], |
|||
title: 'admin.oauth2.mobile-apps', |
|||
breadcrumb: { |
|||
label: 'admin.oauth2.mobile-apps', |
|||
icon: 'smartphone' |
|||
} |
|||
}, |
|||
resolve: { |
|||
entitiesTableConfig: MobileAppTableConfigResolver |
|||
} |
|||
}, |
|||
{ |
|||
path: 'clients', |
|||
data: { |
|||
title: 'admin.oauth2.clients', |
|||
breadcrumb: { |
|||
label: 'admin.oauth2.clients', |
|||
icon: 'public' |
|||
} |
|||
}, |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
component: EntitiesTableComponent, |
|||
data: { |
|||
auth: [Authority.SYS_ADMIN], |
|||
title: 'admin.oauth2.clients' |
|||
}, |
|||
resolve: { |
|||
entitiesTableConfig: ClientsTableConfigResolver |
|||
} |
|||
}, |
|||
{ |
|||
path: 'details', |
|||
children: [ |
|||
{ |
|||
path: ':entityId', |
|||
component: EntityDetailsPageComponent, |
|||
data: { |
|||
breadcrumb: { |
|||
labelFunction: entityDetailsPageBreadcrumbLabelFunction, |
|||
icon: 'public' |
|||
} as BreadCrumbConfig<EntityDetailsPageComponent>, |
|||
auth: [Authority.SYS_ADMIN], |
|||
title: 'admin.oauth2.clients', |
|||
hideTabs: true, |
|||
backNavigationCommands: ['../..'] |
|||
}, |
|||
resolve: { |
|||
entitiesTableConfig: ClientsTableConfigResolver |
|||
} |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
] |
|||
} |
|||
]; |
|||
|
|||
@NgModule({ |
|||
providers: [ |
|||
OAuth2LoginProcessingUrlResolver, |
|||
ClientsTableConfigResolver, |
|||
DomainTableConfigResolver, |
|||
MobileAppTableConfigResolver |
|||
], |
|||
imports: [RouterModule.forChild(oAuth2Routes)], |
|||
exports: [RouterModule] |
|||
}) |
|||
export class Oauth2RoutingModule { |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { NgModule } from '@angular/core'; |
|||
import { ClientComponent } from '@home/pages/admin/oauth2/clients/client.component'; |
|||
import { Oauth2RoutingModule } from '@home/pages/admin/oauth2/oauth2-routing.module'; |
|||
import { SharedModule } from '@shared/shared.module'; |
|||
import { HomeComponentsModule } from '@home/components/home-components.module'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { ClientTableHeaderComponent } from '@home/pages/admin/oauth2/clients/client-table-header.component'; |
|||
import { DomainComponent } from '@home/pages/admin/oauth2/domains/domain.component'; |
|||
import { ClientListComponent } from '@home/pages/admin/oauth2/clients/client-list.component'; |
|||
import { ClientDialogComponent } from '@home/pages/admin/oauth2/clients/client-dialog.component'; |
|||
import { DomainTableHeaderComponent } from '@home/pages/admin/oauth2/domains/domain-table-header.component'; |
|||
import { MobileAppComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app.component'; |
|||
import { MobileAppTableHeaderComponent } from '@home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [ |
|||
ClientComponent, |
|||
ClientDialogComponent, |
|||
ClientListComponent, |
|||
ClientTableHeaderComponent, |
|||
DomainComponent, |
|||
DomainTableHeaderComponent, |
|||
MobileAppComponent, |
|||
MobileAppTableHeaderComponent |
|||
], |
|||
imports: [ |
|||
Oauth2RoutingModule, |
|||
CommonModule, |
|||
SharedModule, |
|||
HomeComponentsModule |
|||
] |
|||
}) |
|||
export class OAuth2Module { |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { EntityId } from '@shared/models/id/entity-id'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
|
|||
export class DomainId implements EntityId { |
|||
entityType = EntityType.DOMAIN |
|||
id: string; |
|||
|
|||
constructor(id: string) { |
|||
this.id = id; |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { EntityId } from '@shared/models/id/entity-id'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
|
|||
export class MobileAppId implements EntityId { |
|||
entityType = EntityType.MOBILE_APP |
|||
id: string; |
|||
|
|||
constructor(id: string) { |
|||
this.id = id; |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 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 { EntityId } from '@shared/models/id/entity-id'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
|
|||
export class OAuth2ClientId implements EntityId { |
|||
entityType = EntityType.OAUTH2_CLIENT |
|||
id: string; |
|||
|
|||
constructor(id: string) { |
|||
this.id = id; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue