Showing
12 changed files
with
845 additions
and
9 deletions
... | ... | @@ -18,7 +18,13 @@ import { Injectable } from '@angular/core'; |
18 | 18 | import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; |
19 | 19 | import { Observable } from 'rxjs'; |
20 | 20 | import { HttpClient } from '@angular/common/http'; |
21 | -import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models'; | |
21 | +import { | |
22 | + AdminSettings, | |
23 | + MailServerSettings, | |
24 | + OAuth2Settings, | |
25 | + SecuritySettings, | |
26 | + UpdateMessage | |
27 | +} from '@shared/models/settings.models'; | |
22 | 28 | |
23 | 29 | @Injectable({ |
24 | 30 | providedIn: 'root' |
... | ... | @@ -53,6 +59,16 @@ export class AdminService { |
53 | 59 | defaultHttpOptionsFromConfig(config)); |
54 | 60 | } |
55 | 61 | |
62 | + public getOAuth2Settings(config?: RequestConfig): Observable<OAuth2Settings> { | |
63 | + return this.http.get<OAuth2Settings>(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config)); | |
64 | + } | |
65 | + | |
66 | + public saveOAuth2Settings(OAuth2Setting: OAuth2Settings, | |
67 | + config?: RequestConfig): Observable<OAuth2Settings> { | |
68 | + return this.http.post<OAuth2Settings>('/api/oauth2/config', OAuth2Setting, | |
69 | + defaultHttpOptionsFromConfig(config)); | |
70 | + } | |
71 | + | |
56 | 72 | public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> { |
57 | 73 | return this.http.get<UpdateMessage>(`/api/admin/updates`, defaultHttpOptionsFromConfig(config)); |
58 | 74 | } | ... | ... |
... | ... | @@ -95,7 +95,7 @@ export class MenuService { |
95 | 95 | name: 'admin.system-settings', |
96 | 96 | type: 'toggle', |
97 | 97 | path: '/settings', |
98 | - height: '120px', | |
98 | + height: '160px', | |
99 | 99 | icon: 'settings', |
100 | 100 | pages: [ |
101 | 101 | { |
... | ... | @@ -115,6 +115,12 @@ export class MenuService { |
115 | 115 | type: 'link', |
116 | 116 | path: '/settings/security-settings', |
117 | 117 | icon: 'security' |
118 | + }, | |
119 | + { | |
120 | + name: 'admin.oauth2.settings', | |
121 | + type: 'link', | |
122 | + path: '/settings/oauth2-settings', | |
123 | + icon: 'security' | |
118 | 124 | } |
119 | 125 | ] |
120 | 126 | } | ... | ... |
... | ... | @@ -22,6 +22,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; |
22 | 22 | import { Authority } from '@shared/models/authority.enum'; |
23 | 23 | import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; |
24 | 24 | import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; |
25 | +import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component'; | |
25 | 26 | |
26 | 27 | const routes: Routes = [ |
27 | 28 | { |
... | ... | @@ -77,6 +78,19 @@ const routes: Routes = [ |
77 | 78 | icon: 'security' |
78 | 79 | } |
79 | 80 | } |
81 | + }, | |
82 | + { | |
83 | + path: 'oauth2-settings', | |
84 | + component: OAuth2SettingsComponent, | |
85 | + canDeactivate: [ConfirmOnExitGuard], | |
86 | + data: { | |
87 | + auth: [Authority.SYS_ADMIN], | |
88 | + title: 'admin.oauth2.settings', | |
89 | + breadcrumb: { | |
90 | + label: 'admin.oauth2.settings', | |
91 | + icon: 'security' | |
92 | + } | |
93 | + } | |
80 | 94 | } |
81 | 95 | ] |
82 | 96 | } | ... | ... |
... | ... | @@ -23,13 +23,15 @@ import { MailServerComponent } from '@modules/home/pages/admin/mail-server.compo |
23 | 23 | import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component'; |
24 | 24 | import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component'; |
25 | 25 | import { HomeComponentsModule } from '@modules/home/components/home-components.module'; |
26 | +import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component'; | |
26 | 27 | |
27 | 28 | @NgModule({ |
28 | 29 | declarations: |
29 | 30 | [ |
30 | 31 | GeneralSettingsComponent, |
31 | 32 | MailServerComponent, |
32 | - SecuritySettingsComponent | |
33 | + SecuritySettingsComponent, | |
34 | + OAuth2SettingsComponent | |
33 | 35 | ], |
34 | 36 | imports: [ |
35 | 37 | CommonModule, | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2020 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<div> | |
19 | + <mat-card class="settings-card"> | |
20 | + <mat-card-title> | |
21 | + <div fxLayout="row"> | |
22 | + <span class="mat-headline" translate>admin.oauth2.settings</span> | |
23 | + <span fxFlex></span> | |
24 | + <div tb-help="oauth2Settings"></div> | |
25 | + </div> | |
26 | + </mat-card-title> | |
27 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
28 | + </mat-progress-bar> | |
29 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | |
30 | + <mat-card-content style="padding-top: 16px;"> | |
31 | + <form [formGroup]="oauth2SettingsForm" (ngSubmit)="save()"> | |
32 | + <fieldset [disabled]="isLoading$ | async"> | |
33 | + <ng-container formArrayName="clientsDomainsParams"> | |
34 | + <div class="container"> | |
35 | + <mat-accordion multi> | |
36 | + <ng-container *ngFor="let domain of clientsDomainsParams.controls; let i = index; let last = last;"> | |
37 | + <mat-expansion-panel [formGroupName]="i" [expanded]="last"> | |
38 | + <mat-expansion-panel-header> | |
39 | + <mat-panel-title fxLayoutAlign="start center"> | |
40 | + {{ domain.get('domainName').value ? domain.get('domainName').value : ("admin.new-domain" | translate) }} | |
41 | + </mat-panel-title> | |
42 | + <mat-panel-description fxLayoutAlign="end center"> | |
43 | + <button mat-icon-button (click)="deleteDomain($event, i)"> | |
44 | + <mat-icon>delete</mat-icon> | |
45 | + </button> | |
46 | + </mat-panel-description> | |
47 | + </mat-expansion-panel-header> | |
48 | + | |
49 | + <mat-form-field class="mat-block"> | |
50 | + <mat-label translate>admin.domain-name</mat-label> | |
51 | + <input matInput formControlName="domainName" required> | |
52 | + <mat-error *ngIf="domain.get('domainName').hasError('pattern')"> | |
53 | + {{ 'admin.error-verification-url' | translate }} | |
54 | + </mat-error> | |
55 | + </mat-form-field> | |
56 | + | |
57 | + <mat-form-field fxFlex class="mat-block"> | |
58 | + <mat-label translate>admin.oauth2.redirect-uri-template</mat-label> | |
59 | + <input matInput formControlName="redirectUriTemplate" required> | |
60 | + <mat-error | |
61 | + *ngIf="domain.get('redirectUriTemplate').hasError('required')"> | |
62 | + {{ 'admin.oauth2.redirect-uri-template-required' | translate }} | |
63 | + </mat-error> | |
64 | + <mat-error | |
65 | + *ngIf="domain.get('redirectUriTemplate').hasError('pattern')"> | |
66 | + {{ 'admin.oauth2.uri-pattern-error' | translate }} | |
67 | + </mat-error> | |
68 | + </mat-form-field> | |
69 | + | |
70 | + <ng-container formArrayName="clientRegistrations"> | |
71 | + <div class="container"> | |
72 | + <mat-card *ngFor="let registration of clientDomainRegistrations(domain).controls; let j = index;" | |
73 | + class="registration-card"> | |
74 | + <section [formGroupName]="j"> | |
75 | + <div fxLayoutAlign="end center"> | |
76 | + <button mat-icon-button (click)="deleteRegistration($event, domain, j)"> | |
77 | + <mat-icon>delete</mat-icon> | |
78 | + </button> | |
79 | + </div> | |
80 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | |
81 | + <mat-form-field fxFlex class="mat-block"> | |
82 | + <mat-label translate>admin.oauth2.registration-id</mat-label> | |
83 | + <input matInput formControlName="registrationId" required> | |
84 | + <mat-error *ngIf="registration.get('registrationId').hasError('required')"> | |
85 | + {{ 'admin.oauth2.registration-id-required' | translate }} | |
86 | + </mat-error> | |
87 | + </mat-form-field> | |
88 | + | |
89 | + <mat-form-field fxFlex class="mat-block"> | |
90 | + <mat-label translate>admin.oauth2.client-name</mat-label> | |
91 | + <input matInput formControlName="clientName" required> | |
92 | + <mat-error *ngIf="registration.get('clientName').hasError('required')"> | |
93 | + {{ 'admin.oauth2.client-name-required' | translate }} | |
94 | + </mat-error> | |
95 | + </mat-form-field> | |
96 | + </div> | |
97 | + | |
98 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | |
99 | + <mat-form-field fxFlex class="mat-block"> | |
100 | + <mat-label translate>admin.oauth2.client-id</mat-label> | |
101 | + <input matInput formControlName="clientId" required> | |
102 | + <mat-error *ngIf="registration.get('clientId').hasError('required')"> | |
103 | + {{ 'admin.oauth2.client-id-required' | translate }} | |
104 | + </mat-error> | |
105 | + </mat-form-field> | |
106 | + | |
107 | + <mat-form-field fxFlex class="mat-block"> | |
108 | + <mat-label translate>admin.oauth2.client-secret</mat-label> | |
109 | + <input matInput formControlName="clientSecret" required> | |
110 | + <mat-error *ngIf="registration.get('clientSecret').hasError('required')"> | |
111 | + {{ 'admin.oauth2.client-secret-required' | translate }} | |
112 | + </mat-error> | |
113 | + </mat-form-field> | |
114 | + </div> | |
115 | + | |
116 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | |
117 | + <mat-form-field fxFlex class="mat-block"> | |
118 | + <mat-label translate>admin.oauth2.access-token-uri</mat-label> | |
119 | + <input matInput formControlName="accessTokenUri" required> | |
120 | + <mat-error *ngIf="registration.get('accessTokenUri').hasError('required')"> | |
121 | + {{ 'admin.oauth2.access-token-uri-required' | translate }} | |
122 | + </mat-error> | |
123 | + <mat-error *ngIf="registration.get('accessTokenUri').hasError('pattern')"> | |
124 | + {{ 'admin.oauth2.uri-pattern-error' | translate }} | |
125 | + </mat-error> | |
126 | + </mat-form-field> | |
127 | + | |
128 | + <mat-form-field fxFlex class="mat-block"> | |
129 | + <mat-label translate>admin.oauth2.authorization-uri</mat-label> | |
130 | + <input matInput formControlName="authorizationUri" required> | |
131 | + <mat-error *ngIf="registration.get('authorizationUri').hasError('required')"> | |
132 | + {{ 'admin.oauth2.authorization-uri-required' | translate }} | |
133 | + </mat-error> | |
134 | + <mat-error *ngIf="registration.get('authorizationUri').hasError('pattern')"> | |
135 | + {{ 'admin.oauth2.uri-pattern-error' | translate }} | |
136 | + </mat-error> | |
137 | + </mat-form-field> | |
138 | + </div> | |
139 | + | |
140 | + <mat-form-field fxFlex class="mat-block"> | |
141 | + <mat-label translate>admin.oauth2.scope</mat-label> | |
142 | + <mat-chip-list #scopeList> | |
143 | + <mat-chip | |
144 | + *ngFor="let scope of registration.get('scope').value; let k = index;" | |
145 | + removable | |
146 | + (removed)="removeScope(k, registration)"> | |
147 | + {{scope}} | |
148 | + <mat-icon matChipRemove>cancel</mat-icon> | |
149 | + </mat-chip> | |
150 | + <input [matChipInputFor]="scopeList" | |
151 | + [matChipInputSeparatorKeyCodes]="separatorKeysCodes" | |
152 | + matChipInputAddOnBlur | |
153 | + (matChipInputTokenEnd)="addScope($event, registration)"> | |
154 | + </mat-chip-list> | |
155 | + </mat-form-field> | |
156 | + | |
157 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | |
158 | + <mat-form-field fxFlex class="mat-block"> | |
159 | + <mat-label translate>admin.oauth2.jwk-set-uri</mat-label> | |
160 | + <input matInput formControlName="jwkSetUri" required> | |
161 | + <mat-error *ngIf="registration.get('jwkSetUri').hasError('required')"> | |
162 | + {{ 'admin.oauth2.jwk-set-uri-required' | translate }} | |
163 | + </mat-error> | |
164 | + <mat-error *ngIf="registration.get('jwkSetUri').hasError('pattern')"> | |
165 | + {{ 'admin.oauth2.uri-pattern-error' | translate }} | |
166 | + </mat-error> | |
167 | + </mat-form-field> | |
168 | + | |
169 | + <mat-form-field fxFlex class="mat-block"> | |
170 | + <mat-label translate>admin.oauth2.user-info-uri</mat-label> | |
171 | + <input matInput formControlName="userInfoUri" required> | |
172 | + <mat-error *ngIf="registration.get('userInfoUri').hasError('required')"> | |
173 | + {{ 'admin.oauth2.user-info-uri-required' | translate }} | |
174 | + </mat-error> | |
175 | + <mat-error *ngIf="registration.get('userInfoUri').hasError('pattern')"> | |
176 | + {{ 'admin.oauth2.uri-pattern-error' | translate }} | |
177 | + </mat-error> | |
178 | + </mat-form-field> | |
179 | + </div> | |
180 | + | |
181 | + <mat-form-field fxFlex class="mat-block"> | |
182 | + <mat-label translate>admin.oauth2.client-authentication-method</mat-label> | |
183 | + <mat-select formControlName="clientAuthenticationMethod"> | |
184 | + <mat-option *ngFor="let clientAuthenticationMethod of clientAuthenticationMethods" | |
185 | + [value]="clientAuthenticationMethod"> | |
186 | + {{ clientAuthenticationMethod | uppercase }} | |
187 | + </mat-option> | |
188 | + </mat-select> | |
189 | + </mat-form-field> | |
190 | + | |
191 | + <mat-form-field class="mat-block"> | |
192 | + <mat-label translate>admin.oauth2.user-name-attribute-name</mat-label> | |
193 | + <input matInput formControlName="userNameAttributeName" required> | |
194 | + <mat-error *ngIf="registration.get('userNameAttributeName').hasError('required')"> | |
195 | + {{ 'admin.oauth2.user-name-attribute-name-required' | translate }} | |
196 | + </mat-error> | |
197 | + </mat-form-field> | |
198 | + | |
199 | + <section formGroupName="mapperConfig"> | |
200 | + <div fxLayout="column" fxLayoutGap="8px"> | |
201 | + <mat-checkbox formControlName="allowUserCreation"> | |
202 | + {{ 'admin.oauth2.allow-user-creation' | translate }} | |
203 | + </mat-checkbox> | |
204 | + <mat-checkbox formControlName="activateUser"> | |
205 | + {{ 'admin.oauth2.activate-user' | translate }} | |
206 | + </mat-checkbox> | |
207 | + </div> | |
208 | + | |
209 | + <mat-form-field fxFlex class="mat-block"> | |
210 | + <mat-label translate>admin.oauth2.type</mat-label> | |
211 | + <mat-select formControlName="type"> | |
212 | + <mat-option *ngFor="let converterTypeExternalUser of converterTypesExternalUser" | |
213 | + [value]="converterTypeExternalUser"> | |
214 | + {{ converterTypeExternalUser }} | |
215 | + </mat-option> | |
216 | + </mat-select> | |
217 | + </mat-form-field> | |
218 | + | |
219 | + <section formGroupName="basic" | |
220 | + *ngIf="registration.get('mapperConfig.type').value === 'BASIC'"> | |
221 | + <mat-form-field class="mat-block"> | |
222 | + <mat-label translate>admin.oauth2.email-attribute-key</mat-label> | |
223 | + <input matInput formControlName="emailAttributeKey" required> | |
224 | + <mat-error | |
225 | + *ngIf="registration.get('mapperConfig.basic.emailAttributeKey').hasError('required')"> | |
226 | + {{ 'admin.oauth2.email-attribute-key-required' | translate }} | |
227 | + </mat-error> | |
228 | + </mat-form-field> | |
229 | + | |
230 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | |
231 | + <mat-form-field fxFlex class="mat-block"> | |
232 | + <mat-label translate>admin.oauth2.first-name-attribute-key</mat-label> | |
233 | + <input matInput formControlName="firstNameAttributeKey"> | |
234 | + </mat-form-field> | |
235 | + | |
236 | + <mat-form-field fxFlex class="mat-block"> | |
237 | + <mat-label translate>admin.oauth2.last-name-attribute-key</mat-label> | |
238 | + <input matInput formControlName="lastNameAttributeKey"> | |
239 | + </mat-form-field> | |
240 | + </div> | |
241 | + | |
242 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | |
243 | + <mat-form-field fxFlex class="mat-block"> | |
244 | + <mat-label translate>admin.oauth2.tenant-name-strategy</mat-label> | |
245 | + <mat-select formControlName="tenantNameStrategy"> | |
246 | + <mat-option *ngFor="let tenantNameStrategy of tenantNameStrategies" | |
247 | + [value]="tenantNameStrategy"> | |
248 | + {{ tenantNameStrategy }} | |
249 | + </mat-option> | |
250 | + </mat-select> | |
251 | + </mat-form-field> | |
252 | + | |
253 | + <mat-form-field fxFlex class="mat-block"> | |
254 | + <mat-label translate>admin.oauth2.tenant-name-pattern</mat-label> | |
255 | + <input matInput | |
256 | + formControlName="tenantNamePattern" | |
257 | + [required]="registration.get('mapperConfig.basic.tenantNameStrategy').value === 'CUSTOM'"> | |
258 | + <mat-error | |
259 | + *ngIf="registration.get('mapperConfig.basic.tenantNamePattern').hasError('required')"> | |
260 | + {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} | |
261 | + </mat-error> | |
262 | + </mat-form-field> | |
263 | + </div> | |
264 | + | |
265 | + <mat-form-field fxFlex class="mat-block"> | |
266 | + <mat-label translate>admin.oauth2.customer-name-pattern</mat-label> | |
267 | + <input matInput formControlName="customerNamePattern"> | |
268 | + </mat-form-field> | |
269 | + | |
270 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | |
271 | + <mat-form-field fxFlex class="mat-block"> | |
272 | + <mat-label translate>admin.oauth2.default-dashboard-name</mat-label> | |
273 | + <input matInput formControlName="defaultDashboardName"> | |
274 | + </mat-form-field> | |
275 | + | |
276 | + <mat-checkbox fxFlex formControlName="alwaysFullScreen" class="checkbox-row"> | |
277 | + {{ 'admin.oauth2.always-fullscreen' | translate}} | |
278 | + </mat-checkbox> | |
279 | + </div> | |
280 | + | |
281 | + </section> | |
282 | + | |
283 | + <section formGroupName="custom" | |
284 | + *ngIf="registration.get('mapperConfig.type').value === 'CUSTOM'"> | |
285 | + <mat-form-field class="mat-block"> | |
286 | + <mat-label translate>admin.oauth2.url</mat-label> | |
287 | + <input matInput formControlName="url" required> | |
288 | + <mat-error | |
289 | + *ngIf="registration.get('mapperConfig.custom.url').hasError('required')"> | |
290 | + {{ 'admin.oauth2.url-required' | translate }} | |
291 | + </mat-error> | |
292 | + <mat-error | |
293 | + *ngIf="registration.get('mapperConfig.custom.url').hasError('pattern')"> | |
294 | + {{ 'admin.oauth2.url-pattern' | translate }} | |
295 | + </mat-error> | |
296 | + </mat-form-field> | |
297 | + | |
298 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | |
299 | + <mat-form-field fxFlex class="mat-block"> | |
300 | + <mat-label translate>common.username</mat-label> | |
301 | + <input matInput formControlName="username"> | |
302 | + </mat-form-field> | |
303 | + | |
304 | + <mat-form-field fxFlex class="mat-block"> | |
305 | + <mat-label translate>common.password</mat-label> | |
306 | + <input matInput formControlName="password"> | |
307 | + </mat-form-field> | |
308 | + </div> | |
309 | + </section> | |
310 | + </section> | |
311 | + | |
312 | + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | |
313 | + <mat-form-field fxFlex class="mat-block"> | |
314 | + <mat-label translate>admin.oauth2.login-button-label</mat-label> | |
315 | + <input matInput formControlName="loginButtonLabel" required> | |
316 | + <mat-error | |
317 | + *ngIf="registration.get('loginButtonLabel').hasError('required')"> | |
318 | + {{ 'admin.oauth2.login-button-label-required' | translate }} | |
319 | + </mat-error> | |
320 | + </mat-form-field> | |
321 | + | |
322 | + <mat-form-field fxFlex class="mat-block"> | |
323 | + <mat-label translate>admin.oauth2.login-button-icon</mat-label> | |
324 | + <input matInput formControlName="loginButtonIcon"> | |
325 | + </mat-form-field> | |
326 | + </div> | |
327 | + </section> | |
328 | + </mat-card> | |
329 | + </div> | |
330 | + </ng-container> | |
331 | + | |
332 | + <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> | |
333 | + <button mat-button mat-raised-button color="primary" | |
334 | + [disabled]="(isLoading$ | async)" | |
335 | + (click)="addRegistration(domain)" | |
336 | + type="button"> | |
337 | + {{'admin.add-registration' | translate}} | |
338 | + </button> | |
339 | + </div> | |
340 | + | |
341 | + </mat-expansion-panel> | |
342 | + </ng-container> | |
343 | + </mat-accordion> | |
344 | + </div> | |
345 | + </ng-container> | |
346 | + | |
347 | + <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px"> | |
348 | + <button mat-button mat-raised-button color="primary" | |
349 | + [disabled]="(isLoading$ | async)" | |
350 | + (click)="addDomain()" | |
351 | + type="button"> | |
352 | + {{'admin.add-domain' | translate}} | |
353 | + </button> | |
354 | + <button mat-button mat-raised-button color="primary" | |
355 | + [disabled]="(isLoading$ | async) || oauth2SettingsForm.invalid || !oauth2SettingsForm.dirty" | |
356 | + type="submit"> | |
357 | + {{'action.save' | translate}} | |
358 | + </button> | |
359 | + </div> | |
360 | + </fieldset> | |
361 | + </form> | |
362 | + </mat-card-content> | |
363 | + </mat-card> | |
364 | +</div> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +:host{ | |
17 | + .checkbox-row { | |
18 | + margin-top: 16px; | |
19 | + } | |
20 | + | |
21 | + .registration-card{ | |
22 | + margin-bottom: 8px; | |
23 | + } | |
24 | + | |
25 | + .container{ | |
26 | + margin-bottom: 16px; | |
27 | + } | |
28 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2020 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; | |
18 | +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; | |
19 | +import { ClientRegistration, DomainParams, OAuth2Settings } from '@shared/models/settings.models'; | |
20 | +import { Store } from '@ngrx/store'; | |
21 | +import { AppState } from '@core/core.state'; | |
22 | +import { AdminService } from '@core/http/admin.service'; | |
23 | +import { PageComponent } from '@shared/components/page.component'; | |
24 | +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; | |
25 | +import { COMMA, ENTER } from '@angular/cdk/keycodes'; | |
26 | +import { MatChipInputEvent } from '@angular/material/chips'; | |
27 | +import { WINDOW } from '@core/services/window.service'; | |
28 | +import { Subscription } from 'rxjs'; | |
29 | +import { DialogService } from '@core/services/dialog.service'; | |
30 | +import { TranslateService } from '@ngx-translate/core'; | |
31 | + | |
32 | +@Component({ | |
33 | + selector: 'tb-oauth2-settings', | |
34 | + templateUrl: './oauth2-settings.component.html', | |
35 | + styleUrls: ['./oauth2-settings.component.scss', './settings-card.scss'] | |
36 | +}) | |
37 | +export class OAuth2SettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy { | |
38 | + | |
39 | + private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/; | |
40 | + private subscriptions: Subscription[] = []; | |
41 | + | |
42 | + readonly separatorKeysCodes: number[] = [ENTER, COMMA]; | |
43 | + | |
44 | + oauth2SettingsForm: FormGroup; | |
45 | + oauth2Settings: OAuth2Settings; | |
46 | + | |
47 | + clientAuthenticationMethods = ['basic', 'post']; | |
48 | + converterTypesExternalUser = ['BASIC', 'CUSTOM']; | |
49 | + tenantNameStrategies = ['DOMAIN', 'EMAIL', 'CUSTOM']; | |
50 | + | |
51 | + constructor(protected store: Store<AppState>, | |
52 | + private adminService: AdminService, | |
53 | + private fb: FormBuilder, | |
54 | + private dialogService: DialogService, | |
55 | + private translate: TranslateService, | |
56 | + @Inject(WINDOW) private window: Window) { | |
57 | + super(store); | |
58 | + } | |
59 | + | |
60 | + ngOnInit(): void { | |
61 | + this.buildOAuth2SettingsForm(); | |
62 | + this.adminService.getOAuth2Settings().subscribe( | |
63 | + (oauth2Settings) => { | |
64 | + this.oauth2Settings = oauth2Settings; | |
65 | + this.initOAuth2Settings(this.oauth2Settings); | |
66 | + this.oauth2SettingsForm.reset(this.oauth2Settings); | |
67 | + } | |
68 | + ); | |
69 | + } | |
70 | + | |
71 | + ngOnDestroy() { | |
72 | + super.ngOnDestroy(); | |
73 | + this.subscriptions.forEach((subscription) => { | |
74 | + subscription.unsubscribe(); | |
75 | + }) | |
76 | + } | |
77 | + | |
78 | + get clientsDomainsParams(): FormArray { | |
79 | + return this.oauth2SettingsForm.get('clientsDomainsParams') as FormArray; | |
80 | + } | |
81 | + | |
82 | + private get formBasicGroup(): FormGroup { | |
83 | + const basicGroup = this.fb.group({ | |
84 | + emailAttributeKey: ['email', [Validators.required]], | |
85 | + firstNameAttributeKey: [''], | |
86 | + lastNameAttributeKey: [''], | |
87 | + tenantNameStrategy: ['DOMAIN'], | |
88 | + tenantNamePattern: [null], | |
89 | + customerNamePattern: [null], | |
90 | + defaultDashboardName: [null], | |
91 | + alwaysFullScreen: [false], | |
92 | + }); | |
93 | + | |
94 | + this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => { | |
95 | + if (domain === 'CUSTOM') { | |
96 | + basicGroup.get('tenantNamePattern').setValidators(Validators.required); | |
97 | + } else { | |
98 | + basicGroup.get('tenantNamePattern').clearValidators(); | |
99 | + } | |
100 | + })); | |
101 | + | |
102 | + return basicGroup; | |
103 | + } | |
104 | + | |
105 | + get formCustomGroup(): FormGroup { | |
106 | + return this.fb.group({ | |
107 | + url: [null, [Validators.required, Validators.pattern(this.URL_REGEXP)]], | |
108 | + username: [null], | |
109 | + password: [null] | |
110 | + }) | |
111 | + } | |
112 | + | |
113 | + private buildOAuth2SettingsForm(): void { | |
114 | + this.oauth2SettingsForm = this.fb.group({ | |
115 | + clientsDomainsParams: this.fb.array([]) | |
116 | + }); | |
117 | + } | |
118 | + | |
119 | + private initOAuth2Settings(oauth2Settings: OAuth2Settings): void { | |
120 | + oauth2Settings.clientsDomainsParams.forEach((domaindomain) => { | |
121 | + this.clientsDomainsParams.push(this.buildSettingsDomain(domaindomain)); | |
122 | + }); | |
123 | + } | |
124 | + | |
125 | + private buildSettingsDomain(domainParams?: DomainParams): FormGroup { | |
126 | + let url = this.window.location.protocol + '//' + this.window.location.hostname; | |
127 | + const port = this.window.location.port; | |
128 | + if (port !== '80' && port !== '443') { | |
129 | + url += ':' + port; | |
130 | + } | |
131 | + url += '/login/oauth2/code/'; | |
132 | + const formDomain = this.fb.group({ | |
133 | + domainName: ['', [Validators.required, Validators.pattern('((?![:/]).)*$')]], | |
134 | + redirectUriTemplate: [url, [Validators.required, Validators.pattern(this.URL_REGEXP)]], | |
135 | + clientRegistrations: this.fb.array([]) | |
136 | + }); | |
137 | + | |
138 | + this.subscriptions.push(formDomain.get('domainName').valueChanges.subscribe((domain) => { | |
139 | + if (!domain) { | |
140 | + domain = this.window.location.hostname | |
141 | + } | |
142 | + const uri = this.window.location.protocol + `//${domain}/login/oauth2/code/`; | |
143 | + formDomain.get('redirectUriTemplate').patchValue(uri); | |
144 | + })); | |
145 | + | |
146 | + if(domainParams){ | |
147 | + domainParams.clientRegistrations.forEach((registration) => { | |
148 | + this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration(registration)); | |
149 | + }) | |
150 | + } else { | |
151 | + this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration()); | |
152 | + } | |
153 | + | |
154 | + return formDomain; | |
155 | + } | |
156 | + | |
157 | + private buildSettingsRegistration(registrationData?: ClientRegistration): FormGroup { | |
158 | + const clientRegistration = this.fb.group({ | |
159 | + registrationId: [null, [Validators.required]], | |
160 | + clientName: [null, [Validators.required]], | |
161 | + loginButtonLabel: [null, [Validators.required]], | |
162 | + loginButtonIcon: [null], | |
163 | + clientId: ['', [Validators.required]], | |
164 | + clientSecret: ['', [Validators.required]], | |
165 | + accessTokenUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]], | |
166 | + authorizationUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]], | |
167 | + scope: this.fb.array([], [Validators.required]), | |
168 | + jwkSetUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]], | |
169 | + userInfoUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]], | |
170 | + clientAuthenticationMethod: ['post', [Validators.required]], | |
171 | + userNameAttributeName: ['email', [Validators.required]], | |
172 | + mapperConfig: this.fb.group({ | |
173 | + allowUserCreation: [true], | |
174 | + activateUser: [false], | |
175 | + type: ['BASIC', [Validators.required]], | |
176 | + basic: this.formBasicGroup | |
177 | + } | |
178 | + ) | |
179 | + }); | |
180 | + | |
181 | + this.subscriptions.push(clientRegistration.get('mapperConfig.type').valueChanges.subscribe((value) => { | |
182 | + const mapperConfig = clientRegistration.get('mapperConfig') as FormGroup; | |
183 | + if (value === 'BASIC') { | |
184 | + mapperConfig.removeControl('custom'); | |
185 | + mapperConfig.addControl('basic', this.formBasicGroup); | |
186 | + } else { | |
187 | + mapperConfig.removeControl('basic'); | |
188 | + mapperConfig.addControl('custom', this.formCustomGroup); | |
189 | + } | |
190 | + })); | |
191 | + | |
192 | + if(registrationData){ | |
193 | + registrationData.scope.forEach(() => { | |
194 | + (clientRegistration.get('scope') as FormArray).push(this.fb.control('')) | |
195 | + }) | |
196 | + if(registrationData.mapperConfig.type !== 'BASIC'){ | |
197 | + clientRegistration.get('mapperConfig.type').patchValue('CUSTOM'); | |
198 | + } | |
199 | + } | |
200 | + | |
201 | + return clientRegistration; | |
202 | + } | |
203 | + | |
204 | + save(): void { | |
205 | + console.log(this.oauth2SettingsForm.value); | |
206 | + this.adminService.saveOAuth2Settings(this.oauth2SettingsForm.value).subscribe( | |
207 | + (oauth2Settings) => { | |
208 | + this.oauth2Settings = oauth2Settings; | |
209 | + this.oauth2SettingsForm.markAsPristine(); | |
210 | + this.oauth2SettingsForm.markAsUntouched(); | |
211 | + } | |
212 | + ); | |
213 | + } | |
214 | + | |
215 | + confirmForm(): FormGroup { | |
216 | + return this.oauth2SettingsForm; | |
217 | + } | |
218 | + | |
219 | + addScope(event: MatChipInputEvent, control: AbstractControl): void { | |
220 | + const input = event.input; | |
221 | + const value = event.value; | |
222 | + const controller = control.get('scope') as FormArray; | |
223 | + if ((value.trim() !== '')) { | |
224 | + controller.push(this.fb.control(value.trim())); | |
225 | + } | |
226 | + | |
227 | + if (input) { | |
228 | + input.value = ''; | |
229 | + } | |
230 | + } | |
231 | + | |
232 | + removeScope(i: number, control: AbstractControl): void { | |
233 | + const controller = control.get('scope') as FormArray; | |
234 | + controller.removeAt(i); | |
235 | + } | |
236 | + | |
237 | + addDomain(): void { | |
238 | + this.clientsDomainsParams.push(this.buildSettingsDomain()); | |
239 | + } | |
240 | + | |
241 | + deleteDomain($event: Event, index: number): void { | |
242 | + if ($event) { | |
243 | + $event.stopPropagation(); | |
244 | + $event.preventDefault(); | |
245 | + } | |
246 | + | |
247 | + const domainName = this.clientsDomainsParams.at(index).get('domainName').value; | |
248 | + this.dialogService.confirm( | |
249 | + this.translate.instant('admin.oauth2.delete-domain-title', {domainName}), | |
250 | + this.translate.instant('admin.oauth2.delete-domain-text'), null, | |
251 | + this.translate.instant('action.delete') | |
252 | + ).subscribe((data) => { | |
253 | + if (data) { | |
254 | + this.clientsDomainsParams.removeAt(index); | |
255 | + } | |
256 | + }) | |
257 | + } | |
258 | + | |
259 | + clientDomainRegistrations(control: AbstractControl): FormArray { | |
260 | + return control.get('clientRegistrations') as FormArray; | |
261 | + } | |
262 | + | |
263 | + addRegistration(control: AbstractControl): void { | |
264 | + this.clientDomainRegistrations(control).push(this.buildSettingsRegistration()); | |
265 | + } | |
266 | + | |
267 | + deleteRegistration($event: Event, controler: AbstractControl, index: number): void { | |
268 | + if ($event) { | |
269 | + $event.stopPropagation(); | |
270 | + $event.preventDefault(); | |
271 | + } | |
272 | + | |
273 | + const registrationId = this.clientDomainRegistrations(controler).at(index).get('registrationId').value; | |
274 | + this.dialogService.confirm( | |
275 | + this.translate.instant('admin.oauth2.delete-registration-title', {name: registrationId}), | |
276 | + this.translate.instant('admin.oauth2.delete-registration-text'), null, | |
277 | + this.translate.instant('action.delete') | |
278 | + ).subscribe((data) => { | |
279 | + if (data) { | |
280 | + this.clientDomainRegistrations(controler).removeAt(index); | |
281 | + } | |
282 | + }) | |
283 | + } | |
284 | +} | ... | ... |
... | ... | @@ -54,6 +54,9 @@ |
54 | 54 | <mat-label translate>tenant.description</mat-label> |
55 | 55 | <textarea matInput formControlName="description" rows="2"></textarea> |
56 | 56 | </mat-form-field> |
57 | + <mat-checkbox fxFlex formControlName="allowOAuth2Configuration" style="padding-bottom: 16px;"> | |
58 | + {{ 'tenant.allow-oauth2-configuration' | translate }} | |
59 | + </mat-checkbox> | |
57 | 60 | </div> |
58 | 61 | <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact> |
59 | 62 | <div fxLayout="column"> | ... | ... |
... | ... | @@ -23,6 +23,7 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti |
23 | 23 | import { TranslateService } from '@ngx-translate/core'; |
24 | 24 | import { ContactBasedComponent } from '../../components/entity/contact-based.component'; |
25 | 25 | import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; |
26 | +import { isDefined } from '@core/utils'; | |
26 | 27 | |
27 | 28 | @Component({ |
28 | 29 | selector: 'tb-tenant', |
... | ... | @@ -55,7 +56,9 @@ export class TenantComponent extends ContactBasedComponent<Tenant> { |
55 | 56 | isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], |
56 | 57 | additionalInfo: this.fb.group( |
57 | 58 | { |
58 | - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''] | |
59 | + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], | |
60 | + allowOAuth2Configuration: [isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ? | |
61 | + entity.additionalInfo.allowOAuth2Configuration : true] | |
59 | 62 | } |
60 | 63 | ) |
61 | 64 | } |
... | ... | @@ -66,7 +69,11 @@ export class TenantComponent extends ContactBasedComponent<Tenant> { |
66 | 69 | this.entityForm.patchValue({title: entity.title}); |
67 | 70 | this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); |
68 | 71 | this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); |
69 | - this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); | |
72 | + this.entityForm.patchValue({additionalInfo: { | |
73 | + description: entity.additionalInfo ? entity.additionalInfo.description : '', | |
74 | + allowOAuth2Configuration: isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ? | |
75 | + entity.additionalInfo.allowOAuth2Configuration : true | |
76 | + }}); | |
70 | 77 | } |
71 | 78 | |
72 | 79 | updateFormState() { | ... | ... |
... | ... | @@ -58,6 +58,7 @@ export const HelpLinks = { |
58 | 58 | linksMap: { |
59 | 59 | outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings', |
60 | 60 | securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings', |
61 | + oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/', | |
61 | 62 | ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/', |
62 | 63 | ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node', |
63 | 64 | ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node', |
... | ... | @@ -108,7 +109,7 @@ export const HelpLinks = { |
108 | 109 | widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest', |
109 | 110 | widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc', |
110 | 111 | widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm', |
111 | - widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static' | |
112 | + widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static', | |
112 | 113 | } |
113 | 114 | }; |
114 | 115 | ... | ... |
... | ... | @@ -23,6 +23,10 @@ export interface AdminSettings<T> { |
23 | 23 | |
24 | 24 | export declare type SmtpProtocol = 'smtp' | 'smtps'; |
25 | 25 | |
26 | +export declare type ClientAuthenticationMethod = 'basic' | 'post'; | |
27 | +export declare type MapperConfigType = 'BASIC' | 'CUSTOM'; | |
28 | +export declare type TenantNameStrategy = 'DOMAIN' | 'EMAIL' | 'CUSTOM'; | |
29 | + | |
26 | 30 | export interface MailServerSettings { |
27 | 31 | mailFrom: string; |
28 | 32 | smtpProtocol: SmtpProtocol; |
... | ... | @@ -60,3 +64,55 @@ export interface UpdateMessage { |
60 | 64 | message: string; |
61 | 65 | updateAvailable: boolean; |
62 | 66 | } |
67 | + | |
68 | +export interface OAuth2Settings { | |
69 | + clientsDomainsParams: DomainParams[]; | |
70 | +} | |
71 | + | |
72 | +export interface DomainParams { | |
73 | + domainName: string; | |
74 | + redirectUriTemplate: string; | |
75 | + clientRegistrations: ClientRegistration[]; | |
76 | +} | |
77 | + | |
78 | +export interface ClientRegistration { | |
79 | + registrationId: string; | |
80 | + clientName: string; | |
81 | + loginButtonLabel: string; | |
82 | + loginButtonIcon: string; | |
83 | + clientId: string; | |
84 | + clientSecret: string; | |
85 | + accessTokenUri: string; | |
86 | + authorizationUri: string; | |
87 | + scope: string[]; | |
88 | + jwkSetUri: string; | |
89 | + userInfoUri: string; | |
90 | + clientAuthenticationMethod: ClientAuthenticationMethod | |
91 | + userNameAttributeName: string; | |
92 | + mapperConfig: MapperConfig | |
93 | +} | |
94 | + | |
95 | +export interface MapperConfig { | |
96 | + allowUserCreation: boolean; | |
97 | + activateUser: boolean; | |
98 | + type: MapperConfigType; | |
99 | + basic?: MapperConfigBasic; | |
100 | + custom?: MapperConfigCustom; | |
101 | +} | |
102 | + | |
103 | +export interface MapperConfigBasic { | |
104 | + emailAttributeKey: string; | |
105 | + firstNameAttributeKey?: string; | |
106 | + lastNameAttributeKey?: string; | |
107 | + tenantNameStrategy: TenantNameStrategy; | |
108 | + tenantNamePattern?: string; | |
109 | + customerNamePattern?: string; | |
110 | + defaultDashboardName?: string; | |
111 | + alwaysFullScreen?: boolean; | |
112 | +} | |
113 | + | |
114 | +export interface MapperConfigCustom { | |
115 | + url: string; | |
116 | + username?: string; | |
117 | + password?: string; | |
118 | +} | ... | ... |
... | ... | @@ -119,8 +119,62 @@ |
119 | 119 | "general-policy": "General policy", |
120 | 120 | "max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked", |
121 | 121 | "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative", |
122 | - "user-lockout-notification-email": "In case user account lockout, send notification to email" | |
123 | - }, | |
122 | + "user-lockout-notification-email": "In case user account lockout, send notification to email", | |
123 | + "domain-name": "Domain name", | |
124 | + "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", | |
125 | + "add-domain": "Add domain", | |
126 | + "new-domain": "New domain", | |
127 | + "add-registration": "Add registration", | |
128 | + "oauth2": { | |
129 | + "settings": "OAuth2 settings", | |
130 | + "registration-id": "Registration ID", | |
131 | + "registration-id-required": "Registration ID is required.", | |
132 | + "client-name": "Client name", | |
133 | + "client-name-required": "Client name is required.", | |
134 | + "client-id": "Client ID", | |
135 | + "client-id-required": "Client ID is required.", | |
136 | + "client-secret": "Client secret", | |
137 | + "client-secret-required": "Client secret is required.", | |
138 | + "access-token-uri": "Access token URI", | |
139 | + "access-token-uri-required": "Access token URI is required.", | |
140 | + "authorization-uri": "Authorization URI", | |
141 | + "authorization-uri-required": "Authorization URI is required.", | |
142 | + "uri-pattern-error": "Invalid URI format.", | |
143 | + "scope": "Scope", | |
144 | + "redirect-uri-template": "Redirect URI template", | |
145 | + "redirect-uri-template-required": "Redirect URI template is required.", | |
146 | + "jwk-set-uri": "JSON Web Key URI", | |
147 | + "jwk-set-uri-required": "JSON Web Key URI is required.", | |
148 | + "user-info-uri": "User info URI", | |
149 | + "user-info-uri-required": "User info URI is required.", | |
150 | + "client-authentication-method": "Client authentication method", | |
151 | + "user-name-attribute-name": "User name attribute key", | |
152 | + "user-name-attribute-name-required": "User name attribute key is required", | |
153 | + "allow-user-creation": "Allow user creation", | |
154 | + "activate-user": "Activate user", | |
155 | + "type": "Mapper type", | |
156 | + "email-attribute-key": "Email attribute key", | |
157 | + "email-attribute-key-required": "Email attribute key is required.", | |
158 | + "first-name-attribute-key": "First name attribute key", | |
159 | + "last-name-attribute-key": "Last name attribute key", | |
160 | + "tenant-name-strategy": "Tenant name strategy", | |
161 | + "tenant-name-pattern": "Tenant name pattern", | |
162 | + "tenant-name-pattern-required": "Tenant name pattern is required.", | |
163 | + "customer-name-pattern": "Customer name pattern", | |
164 | + "default-dashboard-name": "Default dashboard name", | |
165 | + "always-fullscreen": "Always fullscreen", | |
166 | + "url": "URL", | |
167 | + "url-required": "URL is required.", | |
168 | + "url-pattern": "Invalid URL format.", | |
169 | + "login-button-label": "Login button label", | |
170 | + "login-button-label-required": "Login button label is required.", | |
171 | + "login-button-icon": "Login button icon", | |
172 | + "delete-domain-title": "Are you sure you want to delete the domain '{{domainName}}'?", | |
173 | + "delete-domain-text": "Be careful, after the confirmation a domain and all registration data will be unavailable.", | |
174 | + "delete-registration-title": "Are you sure you want to delete the registration '{{name}}'?", | |
175 | + "delete-registration-text": "Be careful, after the confirmation a registration data will be unavailable." | |
176 | + } | |
177 | + }, | |
124 | 178 | "alarm": { |
125 | 179 | "alarm": "Alarm", |
126 | 180 | "alarms": "Alarms", |
... | ... | @@ -1561,7 +1615,8 @@ |
1561 | 1615 | "isolated-tb-core": "Processing in isolated ThingsBoard Core container", |
1562 | 1616 | "isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container", |
1563 | 1617 | "isolated-tb-core-details": "Requires separate microservice(s) per isolated Tenant", |
1564 | - "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant" | |
1618 | + "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant", | |
1619 | + "allow-oauth2-configuration": "Allow OAuth2 configuration" | |
1565 | 1620 | }, |
1566 | 1621 | "timeinterval": { |
1567 | 1622 | "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }", | ... | ... |