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