diff --git a/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json b/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json index 9b059e81c1..04847a9da6 100644 --- a/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json +++ b/application/src/main/data/json/system/oauth2_config_templates/facebook_config.json @@ -7,14 +7,17 @@ "userInfoUri": "https://graph.facebook.com/me?fields=id,name,first_name,last_name,email", "clientAuthenticationMethod": "BASIC", "userNameAttributeName": "email", - "basic": { - "emailAttributeKey": "email", - "firstNameAttributeKey": "first_name", - "lastNameAttributeKey": "last_name", - "tenantNameStrategy": "DOMAIN" + "mapperConfig": { + "type": "BASIC", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "first_name", + "lastNameAttributeKey": "last_name", + "tenantNameStrategy": "DOMAIN" + } }, "comment": null, - "loginButtonIcon": "mdi:facebook", + "loginButtonIcon": "facebook-logo", "loginButtonLabel": "Facebook", "helpLink": "https://developers.facebook.com/docs/facebook-login/web#logindialog" } diff --git a/application/src/main/data/json/system/oauth2_config_templates/github_config.json b/application/src/main/data/json/system/oauth2_config_templates/github_config.json index 6c3a51723f..0a1ae5779b 100644 --- a/application/src/main/data/json/system/oauth2_config_templates/github_config.json +++ b/application/src/main/data/json/system/oauth2_config_templates/github_config.json @@ -7,11 +7,14 @@ "userInfoUri": "https://api.github.com/user", "clientAuthenticationMethod": "BASIC", "userNameAttributeName": "login", - "basic": { - "tenantNameStrategy": "DOMAIN" + "mapperConfig": { + "type": "GITHUB", + "basic": { + "tenantNameStrategy": "DOMAIN" + } }, "comment": "In order to log into ThingsBoard you need to have user's email. You may configure and use Custom OAuth2 Mapper to get email information. Please refer to Github Documentation", - "loginButtonIcon": "mdi:github", + "loginButtonIcon": "github-logo", "loginButtonLabel": "Github", "helpLink": "https://docs.github.com/en/developers/apps/creating-an-oauth-app" } diff --git a/application/src/main/data/json/system/oauth2_config_templates/google_config.json b/application/src/main/data/json/system/oauth2_config_templates/google_config.json index 2756bcc957..f8626439ba 100644 --- a/application/src/main/data/json/system/oauth2_config_templates/google_config.json +++ b/application/src/main/data/json/system/oauth2_config_templates/google_config.json @@ -8,14 +8,17 @@ "userInfoUri": "https://openidconnect.googleapis.com/v1/userinfo", "clientAuthenticationMethod": "BASIC", "userNameAttributeName": "email", - "basic": { - "emailAttributeKey": "email", - "firstNameAttributeKey": "given_name", - "lastNameAttributeKey": "family_name", - "tenantNameStrategy": "DOMAIN" + "mapperConfig": { + "type": "BASIC", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "given_name", + "lastNameAttributeKey": "family_name", + "tenantNameStrategy": "DOMAIN" + } }, "comment": null, - "loginButtonIcon": "mdi:google", + "loginButtonIcon": "google-logo", "loginButtonLabel": "Google", "helpLink": "https://developers.google.com/adwords/api/docs/guides/authentication" } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java index 897472f154..30c6e1c4a9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java @@ -34,8 +34,7 @@ import java.util.List; public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo implements HasName { private String providerId; - private MapperType mapperType; - private OAuth2BasicMapperConfig basic; + private OAuth2MapperConfig mapperConfig; private String authorizationUri; private String accessTokenUri; private List scope; @@ -51,8 +50,7 @@ public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditio public OAuth2ClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { super(clientRegistrationTemplate); this.providerId = clientRegistrationTemplate.providerId; - this.mapperType = clientRegistrationTemplate.mapperType; - this.basic = clientRegistrationTemplate.basic; + this.mapperConfig = clientRegistrationTemplate.mapperConfig; this.authorizationUri = clientRegistrationTemplate.authorizationUri; this.accessTokenUri = clientRegistrationTemplate.accessTokenUri; this.scope = clientRegistrationTemplate.scope; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java index e7973dcd5d..d4fb6bfebe 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientRegistrationTemplateEntity.java @@ -109,17 +109,20 @@ public class OAuth2ClientRegistrationTemplateEntity extends BaseSqlEntity' + ) + ); + + this.matIconRegistry.addSvgIconLiteral( + 'github-logo', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + + this.matIconRegistry.addSvgIconLiteral( + 'facebook-logo', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + this.storageService.testLocalStorage(); this.setupTranslate(); diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 32733ac139..1f8788d821 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http'; import { forkJoin, Observable, of, ReplaySubject, throwError } from 'rxjs'; import { catchError, map, mergeMap, tap } from 'rxjs/operators'; -import { LoginRequest, LoginResponse, OAuth2Client, PublicLoginRequest } from '@shared/models/login.models'; +import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models'; import { ActivatedRoute, Router, UrlTree } from '@angular/router'; import { defaultHttpOptions } from '../http/http-utils'; import { UserService } from '../http/user.service'; @@ -44,6 +44,7 @@ import { AdminService } from '@core/http/admin.service'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; +import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; @Injectable({ providedIn: 'root' @@ -67,7 +68,7 @@ export class AuthService { } redirectUrl: string; - oauth2Clients: Array = null; + oauth2Clients: Array = null; private refreshTokenSubject: ReplaySubject = null; private jwtHelper = new JwtHelperService(); @@ -197,13 +198,14 @@ export class AuthService { }); } - public loadOAuth2Clients(): Observable> { - return this.http.post>(`/api/noauth/oauth2Clients`, + public loadOAuth2Clients(): Observable> { + return this.http.post>(`/api/noauth/oauth2Clients`, null, defaultHttpOptions()).pipe( - tap((OAuth2Clients) => { - this.oauth2Clients = OAuth2Clients; - }) - ); + catchError(err => of([])), + tap((OAuth2Clients) => { + this.oauth2Clients = OAuth2Clients; + }) + ); } private forceDefaultPlace(authState?: AuthState, path?: string, params?: any): boolean { diff --git a/ui-ngx/src/app/core/http/admin.service.ts b/ui-ngx/src/app/core/http/admin.service.ts index 9423392773..f575f56906 100644 --- a/ui-ngx/src/app/core/http/admin.service.ts +++ b/ui-ngx/src/app/core/http/admin.service.ts @@ -18,14 +18,7 @@ import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; -import { - AdminSettings, - ClientProviderTemplated, - MailServerSettings, - OAuth2Settings, - SecuritySettings, - UpdateMessage -} from '@shared/models/settings.models'; +import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models'; @Injectable({ providedIn: 'root' @@ -60,19 +53,6 @@ export class AdminService { defaultHttpOptionsFromConfig(config)); } - 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: OAuth2Settings, config?: RequestConfig): Observable { - return this.http.post('/api/oauth2/config', OAuth2Setting, - defaultHttpOptionsFromConfig(config)); - } - public checkUpdates(config?: RequestConfig): Observable { return this.http.get(`/api/admin/updates`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/oauth2.service.ts b/ui-ngx/src/app/core/http/oauth2.service.ts new file mode 100644 index 0000000000..a8f890223d --- /dev/null +++ b/ui-ngx/src/app/core/http/oauth2.service.ts @@ -0,0 +1,44 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { OAuth2ClientRegistrationTemplate, OAuth2ClientsParams } from '@shared/models/oauth2.models'; + +@Injectable({ + providedIn: 'root' +}) +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: OAuth2ClientsParams, config?: RequestConfig): Observable { + return this.http.post('/api/oauth2/config', OAuth2Setting, + defaultHttpOptionsFromConfig(config)); + } +} diff --git a/ui-ngx/src/app/core/http/public-api.ts b/ui-ngx/src/app/core/http/public-api.ts index db477c3a8c..10c6b5429e 100644 --- a/ui-ngx/src/app/core/http/public-api.ts +++ b/ui-ngx/src/app/core/http/public-api.ts @@ -28,6 +28,7 @@ export * from './entity-relation.service'; export * from './entity-view.service'; export * from './event.service'; export * from './http-utils'; +export * from './oauth2.service'; export * from './queue.service'; export * from './rule-chain.service'; export * from './tenant.service'; 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 index 30564e9306..e9d3030019 100644 --- 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 @@ -33,7 +33,7 @@ {{ 'admin.oauth2.enable' | translate }} - + @@ -59,7 +59,7 @@ - + @@ -160,7 +160,7 @@ - + @@ -182,9 +182,9 @@ - + [disabled]="isCustomProvider(registration)" + [expanded]="isCustomProvider(registration)"> + {{ 'admin.oauth2.custom-setting' | translate }} @@ -199,7 +199,7 @@ + *ngIf="!isCustomProvider(registration)"> create @@ -216,7 +216,7 @@ + *ngIf="!isCustomProvider(registration)"> create @@ -235,7 +235,7 @@ + *ngIf="!isCustomProvider(registration)"> create @@ -249,7 +249,7 @@ + *ngIf="!isCustomProvider(registration)"> create @@ -271,11 +271,11 @@ - + admin.oauth2.login-button-label {{ 'admin.oauth2.login-button-label-required' | translate }} @@ -316,6 +316,10 @@ {{ 'admin.oauth2.scope-required' | translate }} + + @@ -332,16 +336,17 @@ admin.oauth2.type - - {{ converterTypeExternalUser }} + + {{ mapperConfigType }} - + *ngIf="registration.get('mapperConfig.type').value !== mapperConfigType.CUSTOM"> + admin.oauth2.email-attribute-key + *ngIf="registration.get('mapperConfig.type').value === mapperConfigType.CUSTOM"> admin.oauth2.url 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 index 0ad644f61b..0358badb1c 100644 --- 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 @@ -41,6 +41,14 @@ padding-bottom: 0; } } + + .mat-expansion-panel { + .mat-expansion-panel-header { + &.mat-expanded { + height: 48px; + } + } + } } :host ::ng-deep{ @@ -49,9 +57,6 @@ .mat-expansion-panel-body{ padding: 0 2px 1em; } - .mat-tab-label{ - text-transform: none; - } .mat-form-field-suffix .mat-icon-button .mat-icon{ font-size: 18px; } 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 index 11b39468e9..400b2d3fc2 100644 --- 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 @@ -15,25 +15,24 @@ /// import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormArray, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms'; import { ClientAuthenticationMethod, - ClientProviderTemplated, ClientRegistration, DomainInfo, DomainSchema, domainSchemaTranslations, - DomainsParam, MapperConfig, MapperConfigBasic, MapperConfigCustom, MapperConfigType, - OAuth2Settings, + OAuth2ClientRegistrationTemplate, + OAuth2ClientsDomainParams, + OAuth2ClientsParams, TenantNameStrategy -} from '@shared/models/settings.models'; +} from '@shared/models/oauth2.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { AdminService } from '@core/http/admin.service'; import { PageComponent } from '@shared/components/page.component'; import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; @@ -42,7 +41,8 @@ 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 } from '@core/utils'; +import { isDefined, isDefinedAndNotNull } from '@core/utils'; +import { OAuth2Service } from '@core/http/oauth2.service'; @Component({ selector: 'tb-oauth2-settings', @@ -53,7 +53,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.,?+=&%@\-/]*)?$/; private subscriptions: Subscription[] = []; - private templates = new Map(); + private templates = new Map(); private defaultProvider = { additionalInfo: { providerName: 'Custom' @@ -75,10 +75,11 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha readonly separatorKeysCodes: number[] = [ENTER, COMMA]; oauth2SettingsForm: FormGroup; - oauth2Settings: OAuth2Settings; + auth2ClientsParams: OAuth2ClientsParams; clientAuthenticationMethods = Object.keys(ClientAuthenticationMethod); - converterTypesExternalUser = Object.keys(MapperConfigType); + mapperConfigType = MapperConfigType; + mapperConfigTypes = Object.keys(MapperConfigType); tenantNameStrategies = Object.keys(TenantNameStrategy); protocols = Object.keys(DomainSchema); domainSchemaTranslations = domainSchemaTranslations; @@ -86,7 +87,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha templateProvider = ['Custom']; constructor(protected store: Store, - private adminService: AdminService, + private oauth2Service: OAuth2Service, private fb: FormBuilder, private dialogService: DialogService, private translate: TranslateService, @@ -97,13 +98,13 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha ngOnInit(): void { this.buildOAuth2SettingsForm(); forkJoin([ - this.adminService.getOAuth2Template(), - this.adminService.getOAuth2Settings() + this.oauth2Service.getOAuth2Template(), + this.oauth2Service.getOAuth2Settings() ]).subscribe( - ([templates, oauth2Settings]) => { + ([templates, auth2ClientsParams]) => { this.initTemplates(templates); - this.oauth2Settings = oauth2Settings; - this.initOAuth2Settings(this.oauth2Settings); + this.auth2ClientsParams = auth2ClientsParams; + this.initOAuth2Settings(this.auth2ClientsParams); } ); } @@ -115,8 +116,11 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha }); } - private initTemplates(templates: ClientProviderTemplated[]): void { - templates.map(provider => this.templates.set(provider.name, provider)); + 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(); } @@ -169,10 +173,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha }); } - private initOAuth2Settings(oauth2Settings: OAuth2Settings): void { - if (oauth2Settings) { - this.oauth2SettingsForm.patchValue({enabled: oauth2Settings.enabled}, {emitEvent: false}); - oauth2Settings.domainsParams.forEach((domain) => { + private initOAuth2Settings(auth2ClientsParams: OAuth2ClientsParams): void { + if (auth2ClientsParams) { + this.oauth2SettingsForm.patchValue({enabled: auth2ClientsParams.enabled}, {emitEvent: false}); + auth2ClientsParams.domainsParams.forEach((domain) => { this.domainsParams.push(this.buildDomainsForm(domain)); }); } @@ -204,17 +208,17 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha return this.translate.instant('admin.oauth2.new-domain'); } - private buildDomainsForm(domainParams?: DomainsParam): FormGroup { + private buildDomainsForm(auth2ClientsDomainParams?: OAuth2ClientsDomainParams): FormGroup { const formDomain = this.fb.group({ domainInfos: this.fb.array([], Validators.required), clientRegistrations: this.fb.array([], Validators.required) }); - if (domainParams) { - domainParams.domainInfos.forEach((domain) => { + if (auth2ClientsDomainParams) { + auth2ClientsDomainParams.domainInfos.forEach((domain) => { this.clientDomainInfos(formDomain).push(this.buildDomainForm(domain)); }); - domainParams.clientRegistrations.forEach((registration) => { + auth2ClientsDomainParams.clientRegistrations.forEach((registration) => { this.clientDomainProviders(formDomain).push(this.buildProviderForm(registration)); }); } else { @@ -235,10 +239,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha return domain; } - private buildProviderForm(registrationData?: ClientRegistration): FormGroup { + private buildProviderForm(clientRegistration?: ClientRegistration): FormGroup { let additionalInfo = null; - if (registrationData?.additionalInfo) { - additionalInfo = JSON.parse(registrationData.additionalInfo); + if (isDefinedAndNotNull(clientRegistration?.additionalInfo)) { + additionalInfo = clientRegistration.additionalInfo; if (this.templateProvider.indexOf(additionalInfo.providerName) === -1) { additionalInfo.providerName = 'Custom'; } @@ -248,73 +252,80 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha defaultProviderName = 'Google'; } - const clientRegistration = this.fb.group({ - id: this.fb.group({ - id: [registrationData?.id?.id ? registrationData.id.id : null], - entityType: [registrationData?.id?.entityType ? registrationData.id.entityType : null] - }), + const clientRegistrationFormGroup = this.fb.group({ additionalInfo: this.fb.group({ providerName: [additionalInfo?.providerName ? additionalInfo?.providerName : defaultProviderName, Validators.required] }), - loginButtonLabel: [registrationData?.loginButtonLabel ? registrationData.loginButtonLabel : null, Validators.required], - loginButtonIcon: [registrationData?.loginButtonIcon ? registrationData.loginButtonIcon : null], - clientId: [registrationData?.clientId ? registrationData.clientId : '', Validators.required], - clientSecret: [registrationData?.clientSecret ? registrationData.clientSecret : '', Validators.required], - accessTokenUri: [registrationData?.accessTokenUri ? registrationData.accessTokenUri : '', + loginButtonLabel: [clientRegistration?.loginButtonLabel ? clientRegistration.loginButtonLabel : null, Validators.required], + loginButtonIcon: [clientRegistration?.loginButtonIcon ? clientRegistration.loginButtonIcon : null], + clientId: [clientRegistration?.clientId ? clientRegistration.clientId : '', Validators.required], + clientSecret: [clientRegistration?.clientSecret ? clientRegistration.clientSecret : '', Validators.required], + accessTokenUri: [clientRegistration?.accessTokenUri ? clientRegistration.accessTokenUri : '', [Validators.required, Validators.pattern(this.URL_REGEXP)]], - authorizationUri: [registrationData?.authorizationUri ? registrationData.authorizationUri : '', + authorizationUri: [clientRegistration?.authorizationUri ? clientRegistration.authorizationUri : '', [Validators.required, Validators.pattern(this.URL_REGEXP)]], - scope: this.fb.array(registrationData?.scope ? registrationData.scope : [], Validators.required), - jwkSetUri: [registrationData?.jwkSetUri ? registrationData.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], - userInfoUri: [registrationData?.userInfoUri ? registrationData.userInfoUri : '', + scope: this.fb.array(clientRegistration?.scope ? clientRegistration.scope : [], this.validateScope), + jwkSetUri: [clientRegistration?.jwkSetUri ? clientRegistration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], + userInfoUri: [clientRegistration?.userInfoUri ? clientRegistration.userInfoUri : '', [Validators.required, Validators.pattern(this.URL_REGEXP)]], clientAuthenticationMethod: [ - registrationData?.clientAuthenticationMethod ? registrationData.clientAuthenticationMethod : ClientAuthenticationMethod.POST, + clientRegistration?.clientAuthenticationMethod ? clientRegistration.clientAuthenticationMethod : ClientAuthenticationMethod.POST, Validators.required], userNameAttributeName: [ - registrationData?.userNameAttributeName ? registrationData.userNameAttributeName : 'email', Validators.required], + clientRegistration?.userNameAttributeName ? clientRegistration.userNameAttributeName : 'email', Validators.required], mapperConfig: this.fb.group({ - allowUserCreation: [registrationData?.mapperConfig?.allowUserCreation ? registrationData.mapperConfig.allowUserCreation : true], - activateUser: [registrationData?.mapperConfig?.activateUser ? registrationData.mapperConfig.activateUser : false], - type: [registrationData?.mapperConfig?.type ? registrationData.mapperConfig.type : MapperConfigType.BASIC, Validators.required] + allowUserCreation: [ + clientRegistration?.mapperConfig?.allowUserCreation ? clientRegistration.mapperConfig.allowUserCreation : true + ], + activateUser: [clientRegistration?.mapperConfig?.activateUser ? clientRegistration.mapperConfig.activateUser : false], + type: [ + clientRegistration?.mapperConfig?.type ? clientRegistration.mapperConfig.type : MapperConfigType.BASIC, Validators.required + ] } ) }); - if (registrationData) { - this.changeMapperConfigType(clientRegistration, registrationData.mapperConfig.type, registrationData.mapperConfig); + if (clientRegistration) { + this.changeMapperConfigType(clientRegistrationFormGroup, clientRegistration.mapperConfig.type, clientRegistration.mapperConfig); } else { - this.changeMapperConfigType(clientRegistration, MapperConfigType.BASIC); - this.setProviderDefaultValue(defaultProviderName, clientRegistration); + this.changeMapperConfigType(clientRegistrationFormGroup, MapperConfigType.BASIC); + this.setProviderDefaultValue(defaultProviderName, clientRegistrationFormGroup); } - this.subscriptions.push(clientRegistration.get('mapperConfig.type').valueChanges.subscribe((value) => { - this.changeMapperConfigType(clientRegistration, value); + this.subscriptions.push(clientRegistrationFormGroup.get('mapperConfig.type').valueChanges.subscribe((value) => { + this.changeMapperConfigType(clientRegistrationFormGroup, value); })); - this.subscriptions.push(clientRegistration.get('additionalInfo.providerName').valueChanges.subscribe((provider) => { - (clientRegistration.get('scope') as FormArray).clear(); - this.setProviderDefaultValue(provider, clientRegistration); + this.subscriptions.push(clientRegistrationFormGroup.get('additionalInfo.providerName').valueChanges.subscribe((provider) => { + (clientRegistrationFormGroup.get('scope') as FormArray).clear(); + this.setProviderDefaultValue(provider, clientRegistrationFormGroup); })); - return clientRegistration; + return clientRegistrationFormGroup; + } + + private validateScope (control: AbstractControl): ValidationErrors | null { + const scope: string[] = control.value; + if (!scope || !scope.length) { + return { + required: true + }; + } + return null; } private setProviderDefaultValue(provider: string, clientRegistration: FormGroup) { if (provider === 'Custom') { - const defaultSettings = {...this.defaultProvider, ...{id: clientRegistration.get('id').value}}; - clientRegistration.reset(defaultSettings, {emitEvent: false}); + 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); - delete template.id; - delete template.additionalInfo; template.clientId = ''; template.clientSecret = ''; template.scope.forEach(() => { @@ -324,44 +335,33 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha clientRegistration.get('authorizationUri').disable(); clientRegistration.get('jwkSetUri').disable(); clientRegistration.get('userInfoUri').disable(); - clientRegistration.patchValue(template, {emitEvent: false}); + clientRegistration.patchValue(template); } } private changeMapperConfigType(control: AbstractControl, type: MapperConfigType, predefinedValue?: MapperConfig) { const mapperConfig = control.get('mapperConfig') as FormGroup; - if (type === MapperConfigType.BASIC) { - mapperConfig.removeControl('custom'); - mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic)); - } else { + if (type === MapperConfigType.CUSTOM) { mapperConfig.removeControl('basic'); mapperConfig.addControl('custom', this.formCustomGroup(predefinedValue?.custom)); + } else { + mapperConfig.removeControl('custom'); + mapperConfig.addControl('basic', this.formBasicGroup(predefinedValue?.basic)); } } save(): void { - const setting = this.prepareFormValue(this.oauth2SettingsForm.getRawValue()); - this.adminService.saveOAuth2Settings(setting).subscribe( + const setting = this.oauth2SettingsForm.getRawValue(); + this.oauth2Service.saveOAuth2Settings(setting).subscribe( (oauth2Settings) => { - this.oauth2Settings = oauth2Settings; - this.oauth2SettingsForm.markAsPristine(); + this.auth2ClientsParams = oauth2Settings; + this.oauth2SettingsForm.patchValue(this.oauth2SettingsForm, {emitEvent: false}); this.oauth2SettingsForm.markAsUntouched(); + this.oauth2SettingsForm.markAsPristine(); } ); } - private prepareFormValue(formValue: OAuth2Settings): OAuth2Settings{ - formValue.domainsParams.forEach((setting, index) => { - setting.clientRegistrations.forEach((registration) => { - registration.additionalInfo = JSON.stringify(registration.additionalInfo); - if (registration.id.id === null) { - delete registration.id; - } - }); - }); - return formValue; - } - confirmForm(): FormGroup { return this.oauth2SettingsForm; } @@ -451,6 +451,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha 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') { diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index 3f5438dcd3..c0d4d5ed23 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -28,6 +28,19 @@ + + + + + {{ 'login.login-with' | translate: {name: oauth2Client.name} }} + + + + + {{ "login.or" | translate | uppercase }} + + + login.username @@ -41,7 +54,7 @@ lock - + {{ 'login.forgot-password' | translate }} @@ -49,19 +62,6 @@ {{ 'login.login' | translate }} - - - - {{ "login.or" | translate | uppercase }} - - - - - - {{ 'login.login-with' | translate: {name: oauth2Client.name} }} - - - diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.scss b/ui-ngx/src/app/modules/login/pages/login/login.component.scss index e8e9fc29f2..b8fa052e75 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.scss +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.scss @@ -27,8 +27,11 @@ width: 550px !important; } - .tb-reset-password{ - padding: 0 6px; + .forgot-password { + padding: 0 0.5em 1em; + .tb-reset-password { + padding: 0 6px; + } } .tb-action-button{ @@ -65,9 +68,17 @@ min-width: 20px; } - a.centered { + a.login-with-button { + color: rgba(black, 0.87); + &:hover { - border-bottom: 0px; + border-bottom: 0; + } + + .icon{ + height: 20px; + width: 20px; + vertical-align: sub; } } 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 a9717179d6..30ecafe0b2 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 { FormBuilder } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { Constants } from '@shared/models/constants'; import { Router } from '@angular/router'; -import { OAuth2Client } from '@shared/models/login.models'; +import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; @Component({ selector: 'tb-login', @@ -36,7 +36,7 @@ export class LoginComponent extends PageComponent implements OnInit { username: '', password: '' }); - oauth2Clients: Array = null; + oauth2Clients: Array = null; constructor(protected store: Store, private authService: AuthService, diff --git a/ui-ngx/src/app/shared/models/login.models.ts b/ui-ngx/src/app/shared/models/login.models.ts index 7c22bd3952..19b40fd50e 100644 --- a/ui-ngx/src/app/shared/models/login.models.ts +++ b/ui-ngx/src/app/shared/models/login.models.ts @@ -27,9 +27,3 @@ export interface LoginResponse { token: string; refreshToken: string; } - -export interface OAuth2Client { - name: string; - icon?: string; - url: string; -} diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts new file mode 100644 index 0000000000..54d4d3c8d7 --- /dev/null +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -0,0 +1,119 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HasUUID } from '@shared/models/id/has-uuid'; + +export interface OAuth2ClientsParams { + enabled: boolean; + domainsParams: OAuth2ClientsDomainParams[]; +} + +export interface OAuth2ClientsDomainParams { + clientRegistrations: ClientRegistration[]; + domainInfos: DomainInfo[]; +} + +export interface DomainInfo { + name: string; + scheme: DomainSchema; +} + +export enum DomainSchema{ + HTTP = 'HTTP', + HTTPS = 'HTTPS', + MIXED = 'MIXED' +} + +export const domainSchemaTranslations = new Map( + [ + [DomainSchema.HTTP, 'admin.oauth2.domain-schema-http'], + [DomainSchema.HTTPS, 'admin.oauth2.domain-schema-https'], + [DomainSchema.MIXED, 'admin.oauth2.domain-schema-mixed'] + ] +); + +export enum MapperConfigType{ + BASIC = 'BASIC', + CUSTOM = 'CUSTOM', + GITHUB = 'GITHUB' +} + +export enum TenantNameStrategy{ + DOMAIN = 'DOMAIN', + EMAIL = 'EMAIL', + CUSTOM = 'CUSTOM' +} + +export interface OAuth2ClientRegistrationTemplate extends ClientRegistration{ + comment: string; + createdTime: number; + helpLink: string; + name: string; + providerId: string; + id: HasUUID; +} + +export interface ClientRegistration { + loginButtonLabel: string; + loginButtonIcon: string; + clientId: string; + clientSecret: string; + accessTokenUri: string; + authorizationUri: string; + scope: string[]; + jwkSetUri?: string; + userInfoUri: string; + clientAuthenticationMethod: ClientAuthenticationMethod; + userNameAttributeName: string; + mapperConfig: MapperConfig; + additionalInfo: string; +} + +export enum ClientAuthenticationMethod { + BASIC = 'BASIC', + POST = 'POST' +} + +export interface MapperConfig { + allowUserCreation: boolean; + activateUser: boolean; + type: MapperConfigType; + basic?: MapperConfigBasic; + custom?: MapperConfigCustom; +} + +export interface MapperConfigBasic { + emailAttributeKey: string; + firstNameAttributeKey?: string; + lastNameAttributeKey?: string; + tenantNameStrategy: TenantNameStrategy; + tenantNamePattern?: string; + customerNamePattern?: string; + defaultDashboardName?: string; + alwaysFullScreen?: boolean; +} + +export interface MapperConfigCustom { + url: string; + username?: string; + password?: string; +} + +export interface OAuth2ClientInfo { + name: string; + icon?: string; + url: string; +} diff --git a/ui-ngx/src/app/shared/models/public-api.ts b/ui-ngx/src/app/shared/models/public-api.ts index cbc030a4fa..c2130236c1 100644 --- a/ui-ngx/src/app/shared/models/public-api.ts +++ b/ui-ngx/src/app/shared/models/public-api.ts @@ -36,6 +36,7 @@ export * from './error.models'; export * from './event.models'; export * from './login.models'; export * from './material.models'; +export * from './oauth2.models'; export * from './queue.models'; export * from './relation.models'; export * from './rule-chain.models'; diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts index f31dc5ebe8..f71ac1bb2e 100644 --- a/ui-ngx/src/app/shared/models/settings.models.ts +++ b/ui-ngx/src/app/shared/models/settings.models.ts @@ -14,9 +14,6 @@ /// limitations under the License. /// -import { EntityId } from '@shared/models/id/entity-id'; -import { TenantId } from '@shared/models/id/tenant-id'; - export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/; export interface AdminSettings { @@ -63,99 +60,3 @@ export interface UpdateMessage { message: string; updateAvailable: boolean; } - -export interface OAuth2Settings { - enabled: boolean; - domainsParams: DomainsParam[]; -} - -export interface DomainsParam { - clientRegistrations: ClientRegistration[]; - domainInfos: DomainInfo[]; -} - -export interface DomainInfo { - name: string; - scheme: DomainSchema; -} - -export enum DomainSchema{ - HTTP = 'HTTP', - HTTPS = 'HTTPS', - MIXED = 'MIXED' -} - -export const domainSchemaTranslations = new Map( - [ - [DomainSchema.HTTP, 'admin.oauth2.domain-schema-http'], - [DomainSchema.HTTPS, 'admin.oauth2.domain-schema-https'], - [DomainSchema.MIXED, 'admin.oauth2.domain-schema-mixed'] - ] -); - -export enum MapperConfigType{ - BASIC = 'BASIC', - CUSTOM = 'CUSTOM' -} - -export enum TenantNameStrategy{ - DOMAIN = 'DOMAIN', - EMAIL = 'EMAIL', - CUSTOM = 'CUSTOM' -} - -export interface ClientProviderTemplated extends ClientRegistration{ - comment: string; - createdTime: number; - helpLink: string; - name: string; - providerId: string; - tenantId: TenantId; -} - -export interface ClientRegistration { - loginButtonLabel: string; - loginButtonIcon: string; - clientId: string; - clientSecret: string; - accessTokenUri: string; - authorizationUri: string; - scope: string[]; - jwkSetUri?: string; - userInfoUri: string; - clientAuthenticationMethod: ClientAuthenticationMethod; - userNameAttributeName: string; - mapperConfig: MapperConfig; - id?: EntityId; - additionalInfo: string; -} - -export enum ClientAuthenticationMethod { - BASIC = 'BASIC', - POST = 'POST' -} - -export interface MapperConfig { - allowUserCreation: boolean; - activateUser: boolean; - type: MapperConfigType; - basic?: MapperConfigBasic; - custom?: MapperConfigCustom; -} - -export interface MapperConfigBasic { - emailAttributeKey: string; - firstNameAttributeKey?: string; - lastNameAttributeKey?: string; - tenantNameStrategy: TenantNameStrategy; - tenantNamePattern?: string; - customerNamePattern?: string; - defaultDashboardName?: string; - alwaysFullScreen?: boolean; -} - -export interface MapperConfigCustom { - url: string; - username?: string; - password?: string; -} 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 03e8f4d50c..d4bae7064e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -158,7 +158,7 @@ "last-name-attribute-key": "Last name attribute key", "login-button-icon": "Login button icon", "login-button-label": "Provider label", - "login-button-label-1": "Login with $(Provider label)", + "login-button-label-placeholder": "Login with $(Provider label)", "login-button-label-required": "Label is required.", "login-provider": "Login provider", "mapper": "Mapper",