diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 48018e9317..02133c4ca5 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -42,7 +42,7 @@ import { TimeService } from '@core/services/time.service'; import { UtilsService } from '@core/services/utils.service'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; -import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models'; +import { OAuth2ClientLoginInfo, PlatformType } from '@shared/models/oauth2.models'; import { isMobileApp } from '@core/utils'; import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models'; import { UserPasswordPolicy } from '@shared/models/settings.models'; @@ -66,7 +66,7 @@ export class AuthService { } redirectUrl: string; - oauth2Clients: Array = null; + oauth2Clients: Array = null; twoFactorAuthProviders: Array = null; private refreshTokenSubject: ReplaySubject = null; @@ -223,9 +223,9 @@ export class AuthService { } } - public loadOAuth2Clients(): Observable> { - const url = '/api/noauth/oauth2Clients?platform=' + PlatformType.WEB; - return this.http.post>(url, + public loadOAuth2Clients(): Observable> { + const url = '/api/noauth/oauth2/client?platform=' + PlatformType.WEB; + return this.http.post>(url, null, defaultHttpOptions()).pipe( catchError(err => of([])), tap((OAuth2Clients) => { diff --git a/ui-ngx/src/app/core/http/domain.service.ts b/ui-ngx/src/app/core/http/domain.service.ts new file mode 100644 index 0000000000..206748378e --- /dev/null +++ b/ui-ngx/src/app/core/http/domain.service.ts @@ -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, config?: RequestConfig): Observable { + return this.http.post(`/api/domain?oauth2ClientIds=${oauth2ClientIds.join(',')}`, + domain, defaultHttpOptionsFromConfig(config)); + } + + public updateOauth2Clients(id: string, oauth2ClientRegistrationIds: Array, config?: RequestConfig): Observable { + return this.http.put(`/api/domain/${id}/oauth2Clients`, oauth2ClientRegistrationIds, defaultHttpOptionsFromConfig(config)); + } + + public getTenantDomainInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/domain/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getDomainInfoById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/domain/info/${id}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteDomain(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/domain/${id}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/mobile-app.service.ts b/ui-ngx/src/app/core/http/mobile-app.service.ts index 7b108e487c..a506d2805e 100644 --- a/ui-ngx/src/app/core/http/mobile-app.service.ts +++ b/ui-ngx/src/app/core/http/mobile-app.service.ts @@ -14,11 +14,13 @@ /// limitations under the License. /// -import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { MobileAppSettings } from '@shared/models/mobile-app.models'; +import { MobileApp, MobileAppInfo } from '@shared/models/oauth2.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; @Injectable({ providedIn: 'root' @@ -30,16 +32,25 @@ export class MobileAppService { ) { } - public getMobileAppSettings(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + public saveMobileApp(mobileApp: MobileApp, oauth2ClientIds: Array, config?: RequestConfig): Observable { + return this.http.post(`/api/mobileApp?oauth2ClientIds=${oauth2ClientIds.join(',')}`, + mobileApp, defaultHttpOptionsFromConfig(config)); + } + + public updateOauth2Clients(id: string, oauth2ClientRegistrationIds: Array, config?: RequestConfig): Observable { + return this.http.put(`/api/mobileApp/${id}/oauth2Clients`, oauth2ClientRegistrationIds, defaultHttpOptionsFromConfig(config)); + } + + public getTenantMobileAppInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/mobileApp/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); } - public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { - return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + public getMobileAppInfoById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/mobileApp/info/${id}`, defaultHttpOptionsFromConfig(config)); } - public getMobileAppDeepLink(config?: RequestConfig): Observable { - return this.http.get(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); + public deleteMobileApp(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/mobileApp/${id}`, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/core/http/mobile-application.service.ts b/ui-ngx/src/app/core/http/mobile-application.service.ts new file mode 100644 index 0000000000..17ebdd3fef --- /dev/null +++ b/ui-ngx/src/app/core/http/mobile-application.service.ts @@ -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 { + return this.http.get(`/api/mobile/app/settings`, defaultHttpOptionsFromConfig(config)); + } + + public saveMobileAppSettings(mobileAppSettings: MobileAppSettings, config?: RequestConfig): Observable { + return this.http.post(`/api/mobile/app/settings`, mobileAppSettings, defaultHttpOptionsFromConfig(config)); + } + + public getMobileAppDeepLink(config?: RequestConfig): Observable { + return this.http.get(`/api/mobile/deepLink`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/oauth2.service.ts b/ui-ngx/src/app/core/http/oauth2.service.ts index a0ac0d5126..14b853eb93 100644 --- a/ui-ngx/src/app/core/http/oauth2.service.ts +++ b/ui-ngx/src/app/core/http/oauth2.service.ts @@ -18,7 +18,9 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { OAuth2ClientRegistrationTemplate, OAuth2Info } from '@shared/models/oauth2.models'; +import { OAuth2Client, OAuth2ClientInfo, OAuth2ClientRegistrationTemplate } from '@shared/models/oauth2.models'; +import { PageData } from '@shared/models/page/page-data'; +import { PageLink } from '@shared/models/page/page-link'; @Injectable({ providedIn: 'root' @@ -27,22 +29,31 @@ export class OAuth2Service { constructor( private http: HttpClient - ) { } - - public getOAuth2Settings(config?: RequestConfig): Observable { - return this.http.get(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config)); + ) { } public getOAuth2Template(config?: RequestConfig): Observable> { return this.http.get>(`/api/oauth2/config/template`, defaultHttpOptionsFromConfig(config)); } - public saveOAuth2Settings(OAuth2Setting: OAuth2Info, config?: RequestConfig): Observable { - return this.http.post('/api/oauth2/config', OAuth2Setting, - defaultHttpOptionsFromConfig(config)); + public saveOAuth2Client(oAuth2Client: OAuth2Client, config?: RequestConfig): Observable { + return this.http.post('/api/oauth2/client', oAuth2Client, defaultHttpOptionsFromConfig(config)); + } + + public findTenantOAuth2ClientInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/oauth2/client/infos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getOAuth2ClientById(id: string, config?: RequestConfig): Observable { + return this.http.get(`/api/oauth2/client/${id}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteOauth2Client(id: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/oauth2/client/${id}`, defaultHttpOptionsFromConfig(config)); } public getLoginProcessingUrl(config?: RequestConfig): Observable { - return this.http.get(`/api/oauth2/loginProcessingUrl`, defaultHttpOptionsFromConfig(config)); + return this.http.get('/api/oauth2/loginProcessingUrl', defaultHttpOptionsFromConfig(config)); } + } diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index ffb5b5f0a6..2fb0f17623 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -282,7 +282,30 @@ export class MenuService { name: 'admin.oauth2.oauth2', type: 'link', path: '/security-settings/oauth2', - icon: 'mdi:shield-account' + icon: 'mdi:shield-account', + pages: [ + { + id: 'domains', + name: 'admin.oauth2.domains', + type: 'link', + path: '/security-settings/oauth2/domains', + icon: 'domain' + }, + { + id: 'mobile_app', + name: 'admin.oauth2.mobile-apps', + type: 'link', + path: '/security-settings/oauth2/mobile-applications', + icon: 'smartphone' + }, + { + id: 'clients', + name: 'admin.oauth2.clients', + type: 'link', + path: '/security-settings/oauth2/clients', + icon: 'public' + } + ] } ] } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index 5aec80aacb..952e117539 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -38,7 +38,9 @@
- {{ entitiesTableConfig.tableTitle }} + + {{ entitiesTableConfig.tableTitle }} + - - + + + + + + + + + + - - - @@ -209,7 +218,7 @@ matTooltipPosition="above" (click)="column.actionDescriptor.onAction($event, entity)"> - {{column.actionDescriptor.icon}} + {{column.actionDescriptor.iconFunction ? column.actionDescriptor.iconFunction(entity) : column.actionDescriptor.icon}} diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index dd3e846d92..a37b22826b 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -46,6 +46,7 @@ import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router'; import { CellActionDescriptor, CellActionDescriptorType, + ClientChipsEntityTableColumn, EntityActionTableColumn, EntityColumn, EntityLinkTableColumn, @@ -614,7 +615,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa columnsUpdated(resetData: boolean = false) { this.entityColumns = this.entitiesTableConfig.columns.filter( - (column) => column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) + (column) => column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn || + column instanceof ClientChipsEntityTableColumn) .map(column => column as EntityTableColumn>); this.actionColumns = this.entitiesTableConfig.columns.filter( (column) => column instanceof EntityActionTableColumn) diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index ea1dfbf46d..7afbeafad4 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -228,7 +228,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV } hideDetailsTabs(): boolean { - return this.isEditValue && this.entitiesTableConfig.hideDetailsTabsOnEdit; + return this.entitiesTableConfig.hideDetailsTabs || this.isEditValue && this.entitiesTableConfig.hideDetailsTabsOnEdit; } reloadEntity(): Observable> { diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 5390a915d3..74fb826715 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -171,6 +171,7 @@ import { import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; +import { ClientChipsComponent } from '@home/pages/admin/oauth2/clients/client-chips.component'; @NgModule({ declarations: @@ -308,7 +309,8 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet RateLimitsComponent, RateLimitsTextComponent, RateLimitsDetailsDialogComponent, - SendNotificationButtonComponent + SendNotificationButtonComponent, + ClientChipsComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts index b484b9eed1..afc508d3d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/mobile-app-qrcode-widget.component.ts @@ -19,7 +19,7 @@ import { PageComponent } from '@shared/components/page.component'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; import { BadgePosition, MobileAppSettings } from '@shared/models/mobile-app.models'; -import { MobileAppService } from '@core/http/mobile-app.service'; +import { MobileApplicationService } from '@core/http/mobile-application.service'; import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; import { Observable, Subject } from 'rxjs'; @@ -78,7 +78,7 @@ export class MobileAppQrcodeWidgetComponent extends PageComponent implements OnI constructor(protected store: Store, protected cd: ChangeDetectorRef, - private mobileAppService: MobileAppService, + private mobileAppService: MobileApplicationService, private utilsService: UtilsService, private elementRef: ElementRef, private imagePipe: ImagePipe, diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index 9dd434101d..b342fd2e06 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -78,7 +78,7 @@ export interface HeaderActionDescriptor { onAction: ($event: MouseEvent) => void; } -export type EntityTableColumnType = 'content' | 'action' | 'link'; +export type EntityTableColumnType = 'content' | 'action' | 'link' | 'clients'; export class BaseEntityTableColumn> { constructor(public type: EntityTableColumnType, @@ -141,7 +141,15 @@ export class DateEntityTableColumn> extends EntityTabl } } -export type EntityColumn> = EntityTableColumn | EntityActionTableColumn | EntityLinkTableColumn; +export class ClientChipsEntityTableColumn> extends BaseEntityTableColumn { + constructor(public key: string, + public title: string, + public width: string = '0px') { + super('clients', key, title, width, false); + } +} + +export type EntityColumn> = EntityTableColumn | EntityActionTableColumn | EntityLinkTableColumn | ClientChipsEntityTableColumn; export class EntityTableConfig, P extends PageLink = PageLink, L extends BaseData = T> { @@ -159,11 +167,13 @@ export class EntityTableConfig, P extends PageLink = P defaultTimewindowInterval = historyInterval(DAY); entityType: EntityType = null; tableTitle = ''; + hideTableTitle = false; selectionEnabled = true; searchEnabled = true; addEnabled = true; entitiesDeleteEnabled = true; detailsPanelEnabled = true; + hideDetailsTabs = false; hideDetailsTabsOnEdit = true; rowPointer = false; actionsColumnTitle = null; diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index f05fc05a25..5101a3b50a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -14,17 +14,13 @@ /// limitations under the License. /// -import { Injectable, NgModule } from '@angular/core'; -import { Resolve, RouterModule, Routes } from '@angular/router'; - +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; import { MailServerComponent } from '@modules/home/pages/admin/mail-server.component'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { Authority } from '@shared/models/authority.enum'; import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; -import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component'; -import { Observable } from 'rxjs'; -import { OAuth2Service } from '@core/http/oauth2.service'; import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; @@ -41,17 +37,7 @@ import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; import { ImageGalleryComponent } from '@shared/components/image/image-gallery.component'; import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; - -@Injectable() -export class OAuth2LoginProcessingUrlResolver implements Resolve { - - constructor(private oauth2Service: OAuth2Service) { - } - - resolve(): Observable { - return this.oauth2Service.getLoginProcessingUrl(); - } -} +import { oAuth2Routes } from '@home/pages/admin/oauth2/oauth2-routing.module'; const routes: Routes = [ { @@ -358,22 +344,7 @@ const routes: Routes = [ } } }, - { - path: 'oauth2', - component: OAuth2SettingsComponent, - canDeactivate: [ConfirmOnExitGuard], - data: { - auth: [Authority.SYS_ADMIN], - title: 'admin.oauth2.oauth2', - breadcrumb: { - label: 'admin.oauth2.oauth2', - icon: 'mdi:shield-account' - } - }, - resolve: { - loginProcessingUrl: OAuth2LoginProcessingUrlResolver - } - }, + ...oAuth2Routes, ...auditLogsRoutes ] } @@ -383,7 +354,6 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], providers: [ - OAuth2LoginProcessingUrlResolver, ResourcesLibraryTableConfigResolver, QueuesTableConfigResolver ] diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index f44782e814..1904738299 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -23,7 +23,6 @@ import { MailServerComponent } from '@modules/home/pages/admin/mail-server.compo import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; -import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component'; import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; @@ -35,6 +34,7 @@ import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit- import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; import { MobileAppSettingsComponent } from '@home/pages/admin/mobile-app-settings.component'; import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; +import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; @NgModule({ declarations: @@ -44,7 +44,6 @@ import { WidgetComponentsModule } from '@home/components/widget/widget-component SmsProviderComponent, SendTestSmsDialogComponent, SecuritySettingsComponent, - OAuth2SettingsComponent, HomeSettingsComponent, ResourcesLibraryComponent, ResourcesTableHeaderComponent, @@ -59,7 +58,8 @@ import { WidgetComponentsModule } from '@home/components/widget/widget-component SharedModule, HomeComponentsModule, AdminRoutingModule, - WidgetComponentsModule + WidgetComponentsModule, + OAuth2Module ] }) export class AdminModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts index 0f2527d833..7f2a48253f 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/mobile-app-settings.component.ts @@ -21,7 +21,7 @@ import { PageComponent } from '@shared/components/page.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { Subject, takeUntil } from 'rxjs'; -import { MobileAppService } from '@core/http/mobile-app.service'; +import { MobileApplicationService } from '@core/http/mobile-application.service'; import { BadgePosition, badgePositionTranslationsMap, @@ -45,7 +45,7 @@ export class MobileAppSettingsComponent extends PageComponent implements HasConf badgePositionTranslationsMap = badgePositionTranslationsMap; constructor(protected store: Store, - private mobileAppService: MobileAppService, + private mobileAppService: MobileApplicationService, private fb: FormBuilder) { super(store); this.buildMobileAppSettingsForm(); diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html deleted file mode 100644 index 2ea96f6387..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html +++ /dev/null @@ -1,614 +0,0 @@ - -
- - - - admin.oauth2.oauth2 - - -
-
- - -
- -
-
-
- - {{ 'admin.oauth2.enable' | translate }} - - - {{ 'admin.oauth2.edge-enable' | translate }} - -
-
- -
- - - - - - {{ domainListTittle(oauth2ParamsInfo) }} - - - - - - - - - - -
-
-
-
-
-
- - admin.oauth2.protocol - - - {{ domainSchemaTranslations.get(protocol) | translate | uppercase }} - - - - - admin.domain-name - - - {{ 'admin.error-verification-url' | translate }} - - - {{ 'admin.domain-name-max-length' | translate }} - - -
- - {{ 'admin.domain-name-unique' | translate }} - -
- -
- - admin.oauth2.redirect-uri-template - - - - - - - - - -
-
- -
- -
-
-
-
- -
-
-
- - -
-
- admin.oauth2.no-mobile-apps -
-
-
-
-
- - admin.oauth2.mobile-package - - admin.oauth2.mobile-package-hint - - - {{ 'admin.oauth2.mobile-package-unique' | translate }} - -
-
- - admin.oauth2.mobile-app-secret - - - - admin.oauth2.mobile-app-secret-hint - - {{ 'admin.oauth2.mobile-app-secret-required' | translate }} - - - {{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} - - - {{ 'admin.oauth2.mobile-app-secret-base64' | translate }} - - -
-
-
- -
-
-
-
- -
-
-
-
-
admin.oauth2.providers
- -
- - - - {{ getProviderName(registration) }} - - - - - - - -
-
-
- - admin.oauth2.login-provider - - - {{ provider }} - - - -
-
- - admin.oauth2.allowed-platforms - - - {{ platformTypeTranslations.get(platform) | translate }} - - - -
-
- - admin.oauth2.client-id - - - {{ 'admin.oauth2.client-id-required' | translate }} - - - {{ 'admin.oauth2.client-id-max-length' | translate }} - - - - - admin.oauth2.client-secret - - - {{ 'admin.oauth2.client-secret-required' | translate }} - - - {{ 'admin.oauth2.client-secret-max-length' | translate }} - - -
- - - - - {{ 'admin.oauth2.custom-setting' | translate }} - - - - - -
- - admin.oauth2.access-token-uri - - - - {{ 'admin.oauth2.access-token-uri-required' | translate }} - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - - - - admin.oauth2.authorization-uri - - - - {{ 'admin.oauth2.authorization-uri-required' | translate }} - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - -
- -
- - admin.oauth2.jwk-set-uri - - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - - - - admin.oauth2.user-info-uri - - - - {{ 'admin.oauth2.uri-pattern-error' | translate }} - - -
- - - admin.oauth2.client-authentication-method - - - {{ clientAuthenticationMethod | uppercase }} - - - - -
- - admin.oauth2.login-button-label - - - {{ 'admin.oauth2.login-button-label-required' | translate }} - - - - - admin.oauth2.login-button-icon - - -
- -
-
- - {{ 'admin.oauth2.allow-user-creation' | translate }} - - - {{ 'admin.oauth2.activate-user' | translate }} - -
-
- - - admin.oauth2.scope - - - {{scope}} - cancel - - - - - {{ 'admin.oauth2.scope-required' | translate }} - - - - - -
- - - admin.oauth2.user-name-attribute-name - - - {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} - - - -
- - admin.oauth2.type - - - {{ mapperConfigType }} - - - - -
- - admin.oauth2.email-attribute-key - - - {{ 'admin.oauth2.email-attribute-key-required' | translate }} - - - {{ 'admin.oauth2.email-attribute-key-max-length' | translate }} - - - -
- - admin.oauth2.first-name-attribute-key - - - {{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} - - - - - admin.oauth2.last-name-attribute-key - - - {{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} - - -
- -
- - admin.oauth2.tenant-name-strategy - - - {{ tenantNameStrategy }} - - - - - - admin.oauth2.tenant-name-pattern - - - {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} - - - {{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} - - -
- - - admin.oauth2.customer-name-pattern - - - {{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} - - - -
- - admin.oauth2.default-dashboard-name - - - {{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} - - - - - {{ 'admin.oauth2.always-fullscreen' | translate}} - -
-
- -
- - admin.oauth2.url - - - {{ 'admin.oauth2.url-required' | translate }} - - - {{ 'admin.oauth2.url-pattern' | translate }} - - - {{ 'admin.oauth2.url-max-length' | translate }} - - - -
- - common.username - - - {{ 'admin.oauth2.username-max-length' | translate }} - - - - - common.password - - - - {{ 'admin.oauth2.password-max-length' | translate }} - - -
-
-
-
-
-
-
- -
-
-
-
-
- -
- -
-
- -
-
-
-
-
-
-
-
- - -
-
-
-
-
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss deleted file mode 100644 index 639fb877f2..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.scss +++ /dev/null @@ -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; - } -} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts deleted file mode 100644 index f1eabf2d0c..0000000000 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ /dev/null @@ -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, - 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(); - 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(); - 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; - } -} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-chips.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-chips.component.html new file mode 100644 index 0000000000..5a1c87b29e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-chips.component.html @@ -0,0 +1,22 @@ + + + + {{ client.title }} + diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-chips.component.scss b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-chips.component.scss new file mode 100644 index 0000000000..6c489effd0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-chips.component.scss @@ -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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-chips.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-chips.component.ts new file mode 100644 index 0000000000..24611fa581 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-chips.component.ts @@ -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; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html new file mode 100644 index 0000000000..1a002020a1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.html @@ -0,0 +1,49 @@ + +
+ +

{{ 'admin.oauth2.add-client' | translate }}

+ +
+ +
+ + +
+
+ +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts new file mode 100644 index 0000000000..4bd0aa7f28 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-dialog.component.ts @@ -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 implements OnDestroy { + + submitted = false; + + @ViewChild('clientComponent', {static: true}) clientComponent: ClientComponent; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + 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); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-list.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-list.component.html new file mode 100644 index 0000000000..0de423aded --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-list.component.html @@ -0,0 +1,53 @@ + + + admin.oauth2.clients + + + {{entity.title}} + close + + + + + + + + + + {{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }} + + + +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-list.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-list.component.ts new file mode 100644 index 0000000000..94320a9d28 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-list.component.ts @@ -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 | null; + + @Input() + disabled: boolean; + + @Input() + subscriptSizing: SubscriptSizing = 'fixed'; + + @ViewChild('entityInput') entityInput: ElementRef; + @ViewChild('entityAutocomplete') matAutocomplete: MatAutocomplete; + @ViewChild('chipList', {static: true}) chipList: MatChipGrid; + + entities: Array> = []; + filteredEntities: Observable>>; + + searchText = ''; + + private dirty = false; + + private propagateChange = (v: any) => { + }; + + constructor(private store: Store, + 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 | 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> { + 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); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.html new file mode 100644 index 0000000000..304d1fdec1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.html @@ -0,0 +1,22 @@ + +
+ {{ entitiesTableConfig.tableTitle }} +
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts new file mode 100644 index 0000000000..891285d4fd --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client-table-header.component.ts @@ -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 { + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html new file mode 100644 index 0000000000..ccf88d4782 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.html @@ -0,0 +1,331 @@ + +
+
+ + admin.oauth2.title + + + {{ 'admin.oauth2.client-title-required' | translate }} + + +
+
+
+ + admin.oauth2.provider + + + {{ provider }} + + + +
+ + admin.oauth2.allowed-platforms + + + {{ platformTypeTranslations.get(platform) | translate }} + + + +
+
+ + admin.oauth2.client-id + + + {{ 'admin.oauth2.client-id-required' | translate }} + + + {{ 'admin.oauth2.client-id-max-length' | translate }} + + + + admin.oauth2.client-secret + + + {{ 'admin.oauth2.client-secret-required' | translate }} + + + {{ 'admin.oauth2.client-secret-max-length' | translate }} + + +
+ + + + admin.oauth2.advanced-settings + + + +
+
+ + + {{ "admin.oauth2.general" | translate }} + {{ 'admin.oauth2.mapper' | translate }} + +
+
+
+ + admin.oauth2.access-token-uri + + + {{ 'admin.oauth2.access-token-uri-required' | translate }} + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + + + admin.oauth2.authorization-uri + + + {{ 'admin.oauth2.authorization-uri-required' | translate }} + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + +
+
+ + admin.oauth2.jwk-set-uri + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + + + admin.oauth2.user-info-uri + + + {{ 'admin.oauth2.uri-pattern-error' | translate }} + + +
+
+ + admin.oauth2.client-authentication-method + + + {{ clientAuthenticationMethod | uppercase }} + + + +
+
+ + admin.oauth2.login-button-label + + + {{ 'admin.oauth2.login-button-label-required' | translate }} + + + + admin.oauth2.login-button-icon + + +
+
+
+ + {{ 'admin.oauth2.allow-user-creation' | translate }} + +
+
+ + {{ 'admin.oauth2.activate-user' | translate }} + +
+
+
+ + admin.oauth2.scope + + + {{scope}} + cancel + + + + + {{ 'admin.oauth2.scope-required' | translate }} + + +
+ + +
+
+
+ + admin.oauth2.user-name-attribute-name + + + {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} + + +
+
+
+ + admin.oauth2.type + + + {{ mapperType }} + + + +
+
+
+ + admin.oauth2.email-attribute-key + + + {{ 'admin.oauth2.email-attribute-key-required' | translate }} + + + {{ 'admin.oauth2.email-attribute-key-max-length' | translate }} + + +
+
+ + admin.oauth2.first-name-attribute-key + + + {{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} + + + + admin.oauth2.last-name-attribute-key + + + {{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} + + +
+
+ + admin.oauth2.first-name-attribute-key + + + {{ tenantNameStrategy }} + + + + + admin.oauth2.tenant-name-pattern + + + {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} + + + {{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} + + +
+
+ + admin.oauth2.customer-name-pattern + + + {{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} + + +
+
+ + admin.oauth2.default-dashboard-name + + + {{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} + + + + {{ 'admin.oauth2.always-fullscreen' | translate}} + +
+
+
+
+ + admin.oauth2.url + + + {{ 'admin.oauth2.url-required' | translate }} + + + {{ 'admin.oauth2.url-pattern' | translate }} + + + {{ 'admin.oauth2.url-max-length' | translate }} + + +
+
+ + common.username + + + {{ 'admin.oauth2.username-max-length' | translate }} + + + + common.password + + + {{ 'admin.oauth2.password-max-length' | translate }} + + +
+ + {{ 'admin.oauth2.send-token' | translate}} + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.scss b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.scss new file mode 100644 index 0000000000..b7dcc9050d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.scss @@ -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); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts new file mode 100644 index 0000000000..ef95423be8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/client.component.ts @@ -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 { + + templateProvider = ['Custom']; + private templates = new Map(); + 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 = []; + + 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, + protected translate: TranslateService, + private oauth2Service: OAuth2Service, + @Optional() @Inject('entity') protected entityValue: OAuth2Client, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + 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 = []; + 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); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts new file mode 100644 index 0000000000..3f07ef82e1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/clients/clients-table-config.resolver.ts @@ -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> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + 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('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('title', 'admin.oauth2.provider', '170px'), + new EntityTableColumn('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 { + return this.config; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts new file mode 100644 index 0000000000..4c59bf336f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-config.resolver.ts @@ -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> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + 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('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('name', 'admin.oauth2.domain-name', '170px'), + new ClientChipsEntityTableColumn('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 { + 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(); + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.html new file mode 100644 index 0000000000..f78a483eb0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.html @@ -0,0 +1,21 @@ + +
+ {{ entitiesTableConfig.tableTitle }} +
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts new file mode 100644 index 0000000000..3b9f7c6a3c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain-table-header.component.ts @@ -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 { + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html new file mode 100644 index 0000000000..5b4bef660e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html @@ -0,0 +1,68 @@ + +
+
+ + admin.oauth2.domain-name + + + {{ 'admin.oauth2.domain-name-required' | translate }} + + + {{ 'admin.domain-name-max-length' | translate }} + + + {{ 'admin.error-verification-url' | translate }} + + +
+
+ + admin.oauth2.redirect-url-template + + + + +
+
+ + {{ 'admin.oauth2.enable' | translate }} + +
+ + + +
+ + {{ 'admin.oauth2.edge' | translate }} + +
+
+ diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts new file mode 100644 index 0000000000..d41e63fb11 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.ts @@ -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 { + + private loginProcessingUrl: string = ''; + + constructor(protected store: Store, + protected translate: TranslateService, + private oauth2Service: OAuth2Service, + @Inject('entity') protected entityValue: DomainInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + 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, { + 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(); + } + }); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts new file mode 100644 index 0000000000..c7d5fa0be0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-config.resolver.ts @@ -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> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + 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('createdTime', 'common.created-time', this.datePipe, '170px'), + new EntityTableColumn('pkgName', 'admin.oauth2.mobile-package', '170px'), + new EntityTableColumn('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('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 { + 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; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html new file mode 100644 index 0000000000..f78a483eb0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.html @@ -0,0 +1,21 @@ + +
+ {{ entitiesTableConfig.tableTitle }} +
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts new file mode 100644 index 0000000000..ca4bbaf74b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app-table-header.component.ts @@ -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 { + + constructor(protected store: Store) { + super(store); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html new file mode 100644 index 0000000000..d5146098fa --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.html @@ -0,0 +1,69 @@ + +
+
+ + admin.oauth2.mobile-package + + admin.oauth2.mobile-package-hint + + {{ 'admin.oauth2.mobile-package-unique' | translate }} + + +
+
+ + admin.oauth2.mobile-app-secret + + + + admin.oauth2.mobile-app-secret-hint + + {{ 'admin.oauth2.mobile-app-secret-required' | translate }} + + + {{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} + + + {{ 'admin.oauth2.mobile-app-secret-base64' | translate }} + + +
+
+ + {{ 'admin.oauth2.enable' | translate }} + +
+ + + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts new file mode 100644 index 0000000000..d46ac8da57 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/mobile-apps/mobile-app.component.ts @@ -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 { + + constructor(protected store: Store, + protected translate: TranslateService, + private oauth2Service: OAuth2Service, + @Inject('entity') protected entityValue: MobileAppInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + 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, { + 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}; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts new file mode 100644 index 0000000000..b1e18ed502 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2-routing.module.ts @@ -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 { + + constructor(private oauth2Service: OAuth2Service) { + } + + resolve(): Observable { + 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, + 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 { +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts new file mode 100644 index 0000000000..8cd1083155 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2/oauth2.module.ts @@ -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 { +} diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts index 9b0a32baed..83ab1bbe4b 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -23,7 +23,7 @@ import { UntypedFormBuilder } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { Constants } from '@shared/models/constants'; import { Router } from '@angular/router'; -import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; +import { OAuth2ClientLoginInfo } from '@shared/models/oauth2.models'; @Component({ selector: 'tb-login', @@ -38,7 +38,7 @@ export class LoginComponent extends PageComponent implements OnInit { username: '', password: '' }); - oauth2Clients: Array = null; + oauth2Clients: Array = null; constructor(protected store: Store, private authService: AuthService, @@ -73,7 +73,7 @@ export class LoginComponent extends PageComponent implements OnInit { } } - getOAuth2Uri(oauth2Client: OAuth2ClientInfo): string { + getOAuth2Uri(oauth2Client: OAuth2ClientLoginInfo): string { let result = ""; if (this.authService.redirectUrl) { result += "?prevUri=" + this.authService.redirectUrl; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index ec6aa1c9e7..34d2d06dd2 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -91,6 +91,10 @@ export const HelpLinks = { slackSettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/slack-settings`, securitySettings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/security-settings`, oauth2Settings: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/oauth-2-support/`, + oauth2Apple: 'https://developer.apple.com/sign-in-with-apple/get-started/', + oauth2Facebook: 'https://developers.facebook.com/docs/facebook-login/web#logindialog', + oauth2Github: 'https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app', + oauth2Google: 'https://developers.google.com/google-ads/api/docs/start', ruleEngine: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/overview/`, ruleNodeCheckRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node`, ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node`, diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 045e95983c..80194ac843 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -44,7 +44,10 @@ export enum EntityType { NOTIFICATION_REQUEST = 'NOTIFICATION_REQUEST', NOTIFICATION_RULE = 'NOTIFICATION_RULE', NOTIFICATION_TARGET = 'NOTIFICATION_TARGET', - NOTIFICATION_TEMPLATE = 'NOTIFICATION_TEMPLATE' + NOTIFICATION_TEMPLATE = 'NOTIFICATION_TEMPLATE', + OAUTH2_CLIENT = 'OAUTH2_CLIENT', + DOMAIN = 'DOMAIN', + MOBILE_APP = 'MOBILE_APP' } export enum AliasEntityType { @@ -423,6 +426,42 @@ export const entityTypeTranslations = new Map([ [EntityType.OTA_PACKAGE, '/features/otaUpdates'], [EntityType.QUEUE, '/settings/queues'], [EntityType.WIDGETS_BUNDLE, '/resources/widgets-library/widgets-bundles/details'], - [EntityType.WIDGET_TYPE, '/resources/widgets-library/widget-types/details'] + [EntityType.WIDGET_TYPE, '/resources/widgets-library/widget-types/details'], + [EntityType.OAUTH2_CLIENT, '/security-settings/oauth2/clients/details'] ]); export interface EntitySubtype { diff --git a/ui-ngx/src/app/shared/models/id/domain-id.ts b/ui-ngx/src/app/shared/models/id/domain-id.ts new file mode 100644 index 0000000000..69d592d342 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/domain-id.ts @@ -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; + } +} diff --git a/ui-ngx/src/app/shared/models/id/mobile-app-id.ts b/ui-ngx/src/app/shared/models/id/mobile-app-id.ts new file mode 100644 index 0000000000..2148bc2f5c --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/mobile-app-id.ts @@ -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; + } +} diff --git a/ui-ngx/src/app/shared/models/id/oauth2-client-id.ts b/ui-ngx/src/app/shared/models/id/oauth2-client-id.ts new file mode 100644 index 0000000000..6e69a663ac --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/oauth2-client-id.ts @@ -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; + } +} diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts index 9e3d3fa1c5..6ebd12be0e 100644 --- a/ui-ngx/src/app/shared/models/oauth2.models.ts +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -14,31 +14,15 @@ /// limitations under the License. /// +import { OAuth2ClientId } from '@shared/models/id/oauth2-client-id'; +import { BaseData } from '@shared/models/base-data'; +import { TenantId } from '@shared/models/id/tenant-id'; +import { HasTenantId } from './entity.models'; +import { DomainId } from './id/domain-id'; import { HasUUID } from '@shared/models/id/has-uuid'; +import { MobileAppId } from '@shared/models/id/mobile-app-id'; -export interface OAuth2Info { - enabled: boolean; - edgeEnabled: boolean; - oauth2ParamsInfos: OAuth2ParamsInfo[]; -} - -export interface OAuth2ParamsInfo { - clientRegistrations: OAuth2RegistrationInfo[]; - domainInfos: OAuth2DomainInfo[]; - mobileInfos: OAuth2MobileInfo[]; -} - -export interface OAuth2DomainInfo { - name: string; - scheme: DomainSchema; -} - -export interface OAuth2MobileInfo { - pkgName: string; - appSecret: string; -} - -export enum DomainSchema{ +export enum DomainSchema { HTTP = 'HTTP', HTTPS = 'HTTPS', MIXED = 'MIXED' @@ -52,34 +36,13 @@ export const domainSchemaTranslations = new Map( ] ); -export enum MapperConfigType{ - BASIC = 'BASIC', - CUSTOM = 'CUSTOM', - GITHUB = 'GITHUB', - APPLE = 'APPLE' -} - -export enum TenantNameStrategy{ - DOMAIN = 'DOMAIN', - EMAIL = 'EMAIL', - CUSTOM = 'CUSTOM' -} - export enum PlatformType { WEB = 'WEB', ANDROID = 'ANDROID', IOS = 'IOS' } -export const platformTypeTranslations = new Map( - [ - [PlatformType.WEB, 'admin.oauth2.platform-web'], - [PlatformType.ANDROID, 'admin.oauth2.platform-android'], - [PlatformType.IOS, 'admin.oauth2.platform-ios'] - ] -); - -export interface OAuth2ClientRegistrationTemplate extends OAuth2RegistrationInfo{ +export interface OAuth2ClientRegistrationTemplate extends OAuth2RegistrationInfo { comment: string; createdTime: number; helpLink: string; @@ -101,7 +64,7 @@ export interface OAuth2RegistrationInfo { userInfoUri: string; clientAuthenticationMethod: ClientAuthenticationMethod; userNameAttributeName: string; - mapperConfig: MapperConfig; + mapperConfig: OAuth2MapperConfig; additionalInfo: string; } @@ -110,33 +73,132 @@ export enum ClientAuthenticationMethod { POST = 'POST' } -export interface MapperConfig { +export interface Domain extends BaseData, HasTenantId { + tenantId?: TenantId; + name: string; + oauth2Enabled: boolean; + propagateToEdge: boolean; +} + +export interface HasOauth2Clients { + oauth2ClientInfos?: Array; +} + +export interface DomainInfo extends Domain, HasOauth2Clients { + oauth2ClientInfos?: Array; +} + +export interface MobileApp extends BaseData, HasTenantId { + tenantId?: TenantId; + pkgName: string; + appSecret: string; + oauth2Enabled: boolean; +} + +export interface MobileAppInfo extends MobileApp, HasOauth2Clients { + oauth2ClientInfos?: Array; +} + +export interface OAuth2Client extends BaseData, HasTenantId { + tenantId?: TenantId; + title: string; + mapperConfig: OAuth2MapperConfig; + clientId: string; + clientSecret: string; + authorizationUri: string; + accessTokenUri: string; + scope: Array; + userInfoUri?: string; + userNameAttributeName: string; + jwkSetUri?: string; + clientAuthenticationMethod: ClientAuthenticationMethod; + loginButtonLabel: string; + loginButtonIcon?: string; + platforms?: Array; + additionalInfo: any; +} + +export interface OAuth2MapperConfig { allowUserCreation: boolean; activateUser: boolean; - type: MapperConfigType; - basic?: MapperConfigBasic; - custom?: MapperConfigCustom; + type: MapperType; + basic?: OAuth2BasicMapperConfig; + custom?: OAuth2CustomMapperConfig +} + +export enum MapperType { + BASIC = 'BASIC', + CUSTOM = 'CUSTOM', + GITHUB = 'GITHUB', + APPLE = 'APPLE' } -export interface MapperConfigBasic { - emailAttributeKey: string; +export interface OAuth2BasicMapperConfig { + emailAttributeKey?: string; firstNameAttributeKey?: string; lastNameAttributeKey?: string; - tenantNameStrategy: TenantNameStrategy; + tenantNameStrategy?: TenantNameStrategyType; tenantNamePattern?: string; customerNamePattern?: string; defaultDashboardName?: string; alwaysFullScreen?: boolean; } -export interface MapperConfigCustom { - url: string; +export enum TenantNameStrategyType { + DOMAIN = 'DOMAIN', + EMAIL = 'EMAIL', + CUSTOM = 'CUSTOM' +} + +export interface OAuth2CustomMapperConfig { + url?: string; username?: string; password?: string; + sendToken: boolean; +} + +export const platformTypeTranslations = new Map( + [ + [PlatformType.WEB, 'admin.oauth2.platform-web'], + [PlatformType.ANDROID, 'admin.oauth2.platform-android'], + [PlatformType.IOS, 'admin.oauth2.platform-ios'] + ] +); + +export interface OAuth2ClientInfo extends BaseData { + title: string; + platforms?: Array; } -export interface OAuth2ClientInfo { +export interface OAuth2ClientLoginInfo { name: string; - icon?: string; + icon: string; url: string; } + +export function getProviderHelpLink(provider: Provider): string { + if (provider) { + if (providerHelpLinkMap.has(provider)) { + return providerHelpLinkMap.get(provider); + } + } + return 'oauth2Settings'; +} + +export enum Provider { + CUSTOM = 'Custom', + FACEBOOK = 'Facebook', + GOOGLE = 'Google', + GITHUB = 'Github', + APPLE = 'Apple' +} + +const providerHelpLinkMap = new Map( + [ + [Provider.CUSTOM, 'oauth2Settings'], + [Provider.APPLE, 'oauth2Apple'], + [Provider.FACEBOOK, 'oauth2Facebook'], + [Provider.GITHUB, 'oauth2Github'], + [Provider.GOOGLE, 'oauth2Google'], + ] +) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4eb0679b92..1c6a5248b3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -222,6 +222,27 @@ "always-fullscreen": "Always fullscreen", "authorization-uri": "Authorization URI", "authorization-uri-required": "Authorization URI is required.", + "add-client": "Add OAuth 2.0 client", + "client-details": "OAuth 2.0 details", + "client": "OAuth 2.0 client", + "clients": "OAuth 2.0 clients", + "no-oauth2-clients": "No OAuth 2.0 clients found", + "search-oauth2-clients": "Search OAuth 2.0 clients", + "delete-client-title": "Are you sure you want to delete the OAuth 2.0 client '{{clientName}}'?", + "delete-client-text": "Be careful, after the confirmation the client and all related data will become unrecoverable.", + "delete-mobile-app-title": "Are you sure you want to delete the mobile application '{{applicationName}}'?", + "delete-mobile-app-text": "Be careful, after the confirmation the mobile application and all related data will become unrecoverable.", + "title": "Title", + "client-title-required": "Title is required", + "advanced-settings": "Advanced settings", + "domain-details": "Domain details", + "no-domains": "No domains found", + "search-domains": "Search domains", + "mobile-app-details": "Mobile application details", + "add-mobile-app": "Add mobile application", + "no-mobile-apps": "Search mobile applications", + "search-mobile-apps": "No mobile applications found", + "send-token": "Send token", "client-authentication-method": "Client authentication method", "client-id": "Client ID", "client-id-required": "Client ID is required.", @@ -254,7 +275,7 @@ "login-provider": "Login provider", "mapper": "Mapper", "new-domain": "New domain", - "oauth2": "OAuth2", + "oauth2": "OAuth 2.0", "password-max-length": "Password should be less than 256", "redirect-uri-template": "Redirect URI template", "copy-redirect-uri": "Copy redirect URI", @@ -282,11 +303,13 @@ "domain-schema-http": "HTTP", "domain-schema-https": "HTTPS", "domain-schema-mixed": "HTTP+HTTPS", - "enable": "Enable OAuth2 settings", - "edge-enable": "Propagate to Edge", + "enable": "Enable OAuth 2.0 settings", + "disable": "Disable OAuth 2.0 settings", + "edge": "Propagate to Edge", + "edge-enable": "Enable propagation to Edge", + "edge-disable": "Disable propagation to Edge", "domains": "Domains", "mobile-apps": "Mobile applications", - "no-mobile-apps": "No applications configured", "mobile-package": "Application package", "mobile-package-placeholder": "Ex.: my.example.app", "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", @@ -298,7 +321,6 @@ "mobile-app-secret-base64": "Application secret must be base64 format.", "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", "copy-mobile-app-secret": "Copy application secret", - "add-mobile-app": "Add application", "delete-mobile-app": "Delete application info", "providers": "Providers", "platform-web": "Web", @@ -312,6 +334,7 @@ "provider": "Provider", "redirect-url": "Redirect URI", "domain-name": "Domain name", + "domain-name-required": "Domain name is required", "redirect-url-template": "Redirect URI template", "microsoft-tenant-id": "Directory (tenant) Id", "microsoft-tenant-id-required": "Directory (tenant) Id is required", @@ -2300,7 +2323,16 @@ "type-notification-request": "Notification request", "type-notification-template": "Notification template", "type-notification-templates": "Notification templates", - "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }" + "list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }", + "type-oauth2-client": "OAuth 2.0 client", + "type-oauth2-clients": "OAuth 2.0 clients", + "list-of-oauth2-clients": "{ count, plural, =1 {One OAuth 2.0 client} other {List of # OAuth 2.0 clients} }", + "type-domain": "Domain", + "type-domains": "Domains", + "list-of-domains": "{ count, plural, =1 {One Domain} other {List of # Domains} }", + "type-mobile-app": "Mobile application", + "type-mobile-apps": "Mobile applications", + "list-of-mobile-apps": "{ count, plural, =1 {One Mobile application} other {List of # Mobile applications} }" }, "entity-field": { "created-time": "Created time",