@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
490 lines (483 loc) • 394 kB
JavaScript
import * as i0 from '@angular/core';
import { Input, Component, Injectable, EventEmitter, NgModule, Output, inject, ChangeDetectorRef, ElementRef, Renderer2, TemplateRef, ContentChild, ViewChild } from '@angular/core';
import * as i2 from '@c8y/ngx-components';
import { FormGroupComponent, C8yTranslatePipe, Permissions, gettext, C8yTranslateDirective, RequiredInputPlaceholderDirective, MinValidationDirective, MaxValidationDirective, MessagesComponent, MessageDirective, TitleComponent, BreadcrumbComponent, BreadcrumbItemComponent, ActionBarItemComponent, IconDirective, HelpComponent, CoreModule, hookRoute, DefaultValidationDirective, memoize, SelectLegacyComponent, MoNamePipe, EmptyStateComponent, Status, NavigatorNode, hookTab, hookNavigator, hookPatternMessages } from '@c8y/ngx-components';
import * as i1$1 from '@c8y/client';
import { TenantLoginOptionType, TfaStrategy, GrantType, UserManagementSource } from '@c8y/client';
import * as i1 from '@angular/forms';
import { NgForm, ControlContainer, FormsModule } from '@angular/forms';
import * as i1$2 from '@angular/common';
import { NgIf, NgFor, NgSwitch, NgSwitchCase, NgClass, AsyncPipe, KeyValuePipe } from '@angular/common';
import { PopoverDirective, PopoverModule } from 'ngx-bootstrap/popover';
import { map, catchError, tap, switchMap, shareReplay, publishReplay, refCount, take, mapTo, mergeAll, toArray } from 'rxjs/operators';
import { forkJoin, from, of, BehaviorSubject, defer, throwError, Subject, EMPTY, merge } from 'rxjs';
import { isString, omit, defaults, omitBy, isEmpty, cloneDeep, isFinite, pickBy, identity, map as map$1, pick, findKey, has, get, reduce, pull, defaultsDeep, at, head, reject, isUndefined } from 'lodash-es';
import * as i2$1 from '@ngx-translate/core';
import { TooltipDirective, TooltipModule } from 'ngx-bootstrap/tooltip';
import * as i1$4 from 'ngx-bootstrap/collapse';
import { CollapseDirective, CollapseModule } from 'ngx-bootstrap/collapse';
import { HttpStatusCode } from '@angular/common/http';
import { __decorate, __metadata } from 'tslib';
import * as i4 from 'ngx-bootstrap/pagination';
import { PaginationModule } from 'ngx-bootstrap/pagination';
import * as i1$3 from 'ngx-bootstrap/modal';
import { AssetSelectorComponent, AssetSelectorModule } from '@c8y/ngx-components/assets-navigator';
import { BsDatepickerInputDirective, BsDatepickerDirective, BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import * as i1$5 from '@angular/router';
class UserAgent {
constructor(value) {
this._id = this.uniqueId();
this.value = value;
}
get id() {
return this._id;
}
uniqueId() {
const dateString = Date.now().toString(36);
const randomString = Math.random().toString(36).substr(2);
return dateString + randomString;
}
}
function isOauthInternal(tenantLoginOption) {
return tenantLoginOption.type === TenantLoginOptionType.OAUTH2_INTERNAL;
}
function isBasic(tenantLoginOption) {
return tenantLoginOption.type === TenantLoginOptionType.BASIC;
}
class BasicAuthSettingsComponent {
constructor(controlContainer) {
this.controlContainer = controlContainer;
this.preferredLoginOptionType = TenantLoginOptionType.BASIC;
this.tenantLoginOptionTypeEnum = TenantLoginOptionType;
}
ngOnChanges(changes) {
if (changes.authConfiguration && changes.authConfiguration.currentValue) {
this.preferredLoginOptionType =
changes.authConfiguration.currentValue.preferredLoginOptionType;
}
}
ngDoCheck() {
if (this.preferredLoginOptionType !== this.authConfiguration.preferredLoginOptionType) {
this.preferredLoginOptionType = this.authConfiguration.preferredLoginOptionType;
if (this.preferredLoginOptionType === TenantLoginOptionType.BASIC) {
this.forbiddenWebBrowsers = false;
}
}
}
get forbiddenWebBrowsers() {
return this.authenticationRestrictions.forbiddenClients.includes('WEB_BROWSERS');
}
set forbiddenWebBrowsers(value) {
this.authenticationRestrictions.forbiddenClients = value ? ['WEB_BROWSERS'] : [];
}
forbiddenUserAgentsRemove(id) {
this.authenticationRestrictions.forbiddenUserAgents = this.remove(this.authenticationRestrictions.forbiddenUserAgents, id);
this.controlContainer.control.markAsDirty();
}
trustedUserAgentsRemove(id) {
this.authenticationRestrictions.trustedUserAgents = this.remove(this.authenticationRestrictions.trustedUserAgents, id);
this.controlContainer.control.markAsDirty();
}
get authenticationRestrictions() {
return this.authConfiguration.loginOptions.find(isBasic).authenticationRestrictions;
}
add(collection) {
collection.push(new UserAgent(''));
}
remove(collection, id) {
const newArray = collection.filter(obj => obj.id !== id);
if (!newArray.length) {
newArray.push(new UserAgent(''));
}
return newArray;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BasicAuthSettingsComponent, deps: [{ token: i1.ControlContainer }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: BasicAuthSettingsComponent, isStandalone: true, selector: "c8y-basic-auth-settings", inputs: { authConfiguration: "authConfiguration" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"card-block separator-top\"\n *ngIf=\"authConfiguration.preferredLoginOptionType !== 'BASIC'\"\n>\n <div class=\"col-sm-2\">\n <div class=\"h4 text-normal text-right text-left-xs\">\n {{ 'Basic Auth restrictions' | translate }}\n </div>\n </div>\n\n <div class=\"col-sm-9\">\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label class=\"c8y-switch\" title=\"{{ 'Forbidden for web browsers' | translate }}\">\n <input type=\"checkbox\" name=\"forbiddenWebBrowsers\" [(ngModel)]=\"forbiddenWebBrowsers\" />\n <span></span>\n <span>{{ 'Forbidden for web browsers' | translate }}</span>\n </label>\n <div\n class=\"alert alert-warning\"\n *ngIf=\"\n preferredLoginOptionType === tenantLoginOptionTypeEnum.BASIC && forbiddenWebBrowsers\n \"\n >\n {{\n 'You are about to forbid browsers from using Basic authentication. This will prevent users from using web applications on your tenant because you are going to set Basic authentication as the preferred login mode.'\n | translate\n }}\n </div>\n </c8y-form-group>\n </div>\n </div>\n\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <label title=\"{{ 'Forbidden user agents' | translate }}\">\n {{ 'Forbidden user agents' | translate }}\n </label>\n <div\n class=\"input-group m-t-8\"\n *ngFor=\"\n let forbiddenUserAgent of authenticationRestrictions.forbiddenUserAgents;\n last as isLast;\n first as isFirst\n \"\n >\n <input\n type=\"text\"\n [name]=\"'forbiddenUserAgent' + forbiddenUserAgent.id\"\n [(ngModel)]=\"forbiddenUserAgent.value\"\n class=\"form-control\"\n data-cy=\"c8y-basic-auth--forbidden-agent\"\n placeholder=\"{{ 'e.g.' | translate }} forbidden-agent\"\n />\n <div class=\"input-group-btn col-sm-2\">\n <button\n *ngIf=\"!(isFirst && isLast && forbiddenUserAgent.value === '')\"\n title=\"{{ 'Remove' | translate }}\"\n [name]=\"'forbiddenUserAgentRemove' + forbiddenUserAgent.id\"\n type=\"button\"\n class=\"btn btn-dot text-primary\"\n (click)=\"forbiddenUserAgentsRemove(forbiddenUserAgent.id)\"\n >\n <i class=\"dlt-c8y-icon-minus-circle text-danger\"></i>\n </button>\n <button\n title=\"{{ 'Add' | translate }}\"\n type=\"button\"\n class=\"btn btn-dot text-primary\"\n (click)=\"add(authenticationRestrictions.forbiddenUserAgents)\"\n *ngIf=\"isLast\"\n >\n <i class=\"dlt-c8y-icon-plus-circle\"></i>\n </button>\n </div>\n </div>\n </div>\n <div class=\"col-sm-6\">\n <label title=\"{{ 'Trusted user agents' | translate }}\">\n {{ 'Trusted user agents' | translate }}\n </label>\n <div\n class=\"input-group m-t-8\"\n *ngFor=\"\n let trustedUserAgent of authenticationRestrictions.trustedUserAgents;\n last as isLast;\n first as isFirst\n \"\n >\n <input\n type=\"text\"\n [name]=\"'trustedUserAgent' + trustedUserAgent.id\"\n class=\"form-control\"\n placeholder=\"{{ 'e.g.' | translate }} trusted-agent\"\n data-cy=\"c8y-basic-auth--trusted-agent\"\n [(ngModel)]=\"trustedUserAgent.value\"\n />\n <div class=\"input-group-btn col-sm-2\">\n <button\n *ngIf=\"!(isFirst && isLast && trustedUserAgent.value === '')\"\n title=\"{{ 'Remove' | translate }}\"\n [name]=\"'trustedUserAgentRemove' + trustedUserAgent.id\"\n type=\"button\"\n class=\"btn btn-dot btn-dot--danger text-primary\"\n (click)=\"trustedUserAgentsRemove(trustedUserAgent.id)\"\n >\n <i class=\"dlt-c8y-icon-minus-circle\"></i>\n </button>\n <button\n title=\"{{ 'Add' | translate }}\"\n type=\"button\"\n (click)=\"add(authenticationRestrictions.trustedUserAgents)\"\n class=\"btn btn-dot text-primary\"\n *ngIf=\"isLast\"\n >\n <i class=\"dlt-c8y-icon-plus-circle\"></i>\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BasicAuthSettingsComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-basic-auth-settings', viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], imports: [NgIf, FormGroupComponent, FormsModule, NgFor, C8yTranslatePipe], template: "<div\n class=\"card-block separator-top\"\n *ngIf=\"authConfiguration.preferredLoginOptionType !== 'BASIC'\"\n>\n <div class=\"col-sm-2\">\n <div class=\"h4 text-normal text-right text-left-xs\">\n {{ 'Basic Auth restrictions' | translate }}\n </div>\n </div>\n\n <div class=\"col-sm-9\">\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label class=\"c8y-switch\" title=\"{{ 'Forbidden for web browsers' | translate }}\">\n <input type=\"checkbox\" name=\"forbiddenWebBrowsers\" [(ngModel)]=\"forbiddenWebBrowsers\" />\n <span></span>\n <span>{{ 'Forbidden for web browsers' | translate }}</span>\n </label>\n <div\n class=\"alert alert-warning\"\n *ngIf=\"\n preferredLoginOptionType === tenantLoginOptionTypeEnum.BASIC && forbiddenWebBrowsers\n \"\n >\n {{\n 'You are about to forbid browsers from using Basic authentication. This will prevent users from using web applications on your tenant because you are going to set Basic authentication as the preferred login mode.'\n | translate\n }}\n </div>\n </c8y-form-group>\n </div>\n </div>\n\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <label title=\"{{ 'Forbidden user agents' | translate }}\">\n {{ 'Forbidden user agents' | translate }}\n </label>\n <div\n class=\"input-group m-t-8\"\n *ngFor=\"\n let forbiddenUserAgent of authenticationRestrictions.forbiddenUserAgents;\n last as isLast;\n first as isFirst\n \"\n >\n <input\n type=\"text\"\n [name]=\"'forbiddenUserAgent' + forbiddenUserAgent.id\"\n [(ngModel)]=\"forbiddenUserAgent.value\"\n class=\"form-control\"\n data-cy=\"c8y-basic-auth--forbidden-agent\"\n placeholder=\"{{ 'e.g.' | translate }} forbidden-agent\"\n />\n <div class=\"input-group-btn col-sm-2\">\n <button\n *ngIf=\"!(isFirst && isLast && forbiddenUserAgent.value === '')\"\n title=\"{{ 'Remove' | translate }}\"\n [name]=\"'forbiddenUserAgentRemove' + forbiddenUserAgent.id\"\n type=\"button\"\n class=\"btn btn-dot text-primary\"\n (click)=\"forbiddenUserAgentsRemove(forbiddenUserAgent.id)\"\n >\n <i class=\"dlt-c8y-icon-minus-circle text-danger\"></i>\n </button>\n <button\n title=\"{{ 'Add' | translate }}\"\n type=\"button\"\n class=\"btn btn-dot text-primary\"\n (click)=\"add(authenticationRestrictions.forbiddenUserAgents)\"\n *ngIf=\"isLast\"\n >\n <i class=\"dlt-c8y-icon-plus-circle\"></i>\n </button>\n </div>\n </div>\n </div>\n <div class=\"col-sm-6\">\n <label title=\"{{ 'Trusted user agents' | translate }}\">\n {{ 'Trusted user agents' | translate }}\n </label>\n <div\n class=\"input-group m-t-8\"\n *ngFor=\"\n let trustedUserAgent of authenticationRestrictions.trustedUserAgents;\n last as isLast;\n first as isFirst\n \"\n >\n <input\n type=\"text\"\n [name]=\"'trustedUserAgent' + trustedUserAgent.id\"\n class=\"form-control\"\n placeholder=\"{{ 'e.g.' | translate }} trusted-agent\"\n data-cy=\"c8y-basic-auth--trusted-agent\"\n [(ngModel)]=\"trustedUserAgent.value\"\n />\n <div class=\"input-group-btn col-sm-2\">\n <button\n *ngIf=\"!(isFirst && isLast && trustedUserAgent.value === '')\"\n title=\"{{ 'Remove' | translate }}\"\n [name]=\"'trustedUserAgentRemove' + trustedUserAgent.id\"\n type=\"button\"\n class=\"btn btn-dot btn-dot--danger text-primary\"\n (click)=\"trustedUserAgentsRemove(trustedUserAgent.id)\"\n >\n <i class=\"dlt-c8y-icon-minus-circle\"></i>\n </button>\n <button\n title=\"{{ 'Add' | translate }}\"\n type=\"button\"\n (click)=\"add(authenticationRestrictions.trustedUserAgents)\"\n class=\"btn btn-dot text-primary\"\n *ngIf=\"isLast\"\n >\n <i class=\"dlt-c8y-icon-plus-circle\"></i>\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n</div>\n" }]
}], ctorParameters: () => [{ type: i1.ControlContainer }], propDecorators: { authConfiguration: [{
type: Input
}] } });
class AuthConfigurationGuard {
constructor(permissions) {
this.permissions = permissions;
}
canActivate() {
return this.permissions.hasAnyRole([
Permissions.ROLE_TENANT_ADMIN,
Permissions.ROLE_TENANT_MANAGEMENT_ADMIN
]);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthConfigurationGuard, deps: [{ token: i2.Permissions }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthConfigurationGuard }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthConfigurationGuard, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i2.Permissions }] });
class TypedOption {
constructor(category, key, type, defaultValue, value) {
this.category = category;
this.key = key;
this.type = type;
this.defaultValue = defaultValue;
this.value = value;
}
apply(option) {
this.category = option.category;
this.key = option.key;
this.value = option.value;
}
getValue() {
try {
return this.getValueByType();
}
catch (ex) {
return this.defaultValue;
}
}
getValueByType() {
switch (this.type) {
case 'boolean':
return this.value.toLowerCase() === 'true';
case 'number':
return this.parseNumberValue(this.value);
case 'string':
return this.parseStringValue(this.value);
default:
throw new TypeError();
}
}
parseNumberValue(stringValue) {
const value = Number.parseInt(stringValue, 10);
if (typeof value !== 'number' || isNaN(value)) {
throw new Error();
}
return value;
}
parseStringValue(value) {
if (!isString(value)) {
throw new Error();
}
return value;
}
}
class TenantLoginOptionMapper {
mapTo(tenantLoginOption) {
const loginOption = omit(this.prapareTenantLoginOption(tenantLoginOption), 'authenticationRestrictions');
if (isBasic(loginOption)) {
loginOption.authenticationRestrictions = this.mapAuthenticationRestrictionsTo(tenantLoginOption.authenticationRestrictions);
}
return loginOption;
}
mapFrom(originalLoginOption, newLoginOption) {
if (isBasic(originalLoginOption)) {
return this.mapBasicLoginOption(originalLoginOption, newLoginOption);
}
if (isOauthInternal(originalLoginOption)) {
return this.mapOauthInternalLoginOption(originalLoginOption, newLoginOption);
}
throw new Error(`TenantLoginOptionMapper: The tenant login option cannot be mapped. Login option with type: ${originalLoginOption.type} is not supported.`);
}
mapAuthenticationRestrictionsTo(authenticationRestrictions) {
const restrictions = defaults({}, omitBy(authenticationRestrictions, isEmpty), {
forbiddenUserAgents: [''],
trustedUserAgents: [''],
forbiddenClients: []
});
restrictions.forbiddenUserAgents = restrictions.forbiddenUserAgents.map(val => new UserAgent(val));
restrictions.trustedUserAgents = restrictions.trustedUserAgents.map(val => new UserAgent(val));
return restrictions;
}
mapBasicLoginOption(originalLoginOption, newLoginOption) {
const loginOption = omit(originalLoginOption, ['sessionConfiguration']);
loginOption.authenticationRestrictions = this.mapAuthenticationRestrictionsFrom(newLoginOption.authenticationRestrictions);
return loginOption;
}
mapOauthInternalLoginOption(originalLoginOption, newLoginOption) {
const loginOption = omit(originalLoginOption, ['authenticationRestrictions']);
newLoginOption.sessionConfiguration !== null
? (loginOption.sessionConfiguration = newLoginOption.sessionConfiguration)
: delete loginOption.sessionConfiguration;
return loginOption;
}
mapAuthenticationRestrictionsFrom(authenticationRestrictions) {
return {
trustedUserAgents: authenticationRestrictions.trustedUserAgents
.filter(({ value }) => value)
.map(({ value }) => value),
forbiddenUserAgents: authenticationRestrictions.forbiddenUserAgents
.filter(({ value }) => value)
.map(({ value }) => value),
forbiddenClients: authenticationRestrictions.forbiddenClients.filter(value => value)
};
}
prapareTenantLoginOption(tenantLoginOption) {
return omit(tenantLoginOption, [
'self',
'strengthValidity',
'tfaStrategy',
'greenMinLength',
'enforceStrength',
'strengthValidity',
'_type'
]);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TenantLoginOptionMapper, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TenantLoginOptionMapper, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: TenantLoginOptionMapper, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class AuthConfigurationService {
constructor(tenantLoginOptionsService, tenantOptionsService, systemOptionsService, appState, tenantUiService, tenantLoginOptionMapper, tenantService) {
this.tenantLoginOptionsService = tenantLoginOptionsService;
this.tenantOptionsService = tenantOptionsService;
this.systemOptionsService = systemOptionsService;
this.appState = appState;
this.tenantUiService = tenantUiService;
this.tenantLoginOptionMapper = tenantLoginOptionMapper;
this.tenantService = tenantService;
this.systemOptionsWithDefaultValue = [
new TypedOption('password', 'limit.validity', 'number', null),
new TypedOption('password', 'enforce.strength', 'boolean', false),
new TypedOption('two-factor-authentication', 'tenant-scope-settings.enabled', 'boolean', false),
new TypedOption('two-factor-authentication', 'enabled', 'boolean', false),
// note: this definition is inconsistent with backend and is overridden in getSystemOptions$
new TypedOption('two-factor-authentication', 'enforced', 'boolean', false),
new TypedOption('two-factor-authentication', 'enforced.group', 'string', '')
];
this.tenantOptionsWithDefaultValue = [
new TypedOption('password', 'limit.validity', 'number', 0),
new TypedOption('password', 'strength.validity', 'boolean', false),
new TypedOption('two-factor-authentication', 'enabled', 'boolean', false),
new TypedOption('two-factor-authentication', 'token.validity', 'number', 43200), // 30 days
new TypedOption('two-factor-authentication', 'pin.validity', 'number', 30),
new TypedOption('two-factor-authentication', 'enforced', 'boolean', false),
new TypedOption('two-factor-authentication', 'strategy', 'string', 'SMS'),
new TypedOption('oauth.internal', 'basic-token.lifespan.seconds', 'number', null),
new TypedOption('configuration', 'tenant.login.ignore-case', 'boolean', false)
];
}
getAuthConfiguration$() {
const loginOptions$ = this.getLoginOptions$();
return forkJoin({
loginOptions: this.map(loginOptions$),
tenantOptions: this.getTenantOptions$(),
systemOptions: this.getSystemOptions$(),
smsGatewayAvailable: this.isSmsApplicationAvailable$(),
preferredLoginOptionType: this.getPreferredLoginOptionType$(loginOptions$)
});
}
save(newAuthConfiguration, previousAuthConfiguration) {
const tenantOptions = this.prepareTenantOptions(newAuthConfiguration, previousAuthConfiguration);
const updateTenantOptions = tenantOptions.map(tenantOption => {
const fixedOption = this.fixTfaStrategy(tenantOption);
if (fixedOption) {
return fixedOption;
}
return this.tenantOptionsService.create(tenantOption);
});
const basicLoginOption = this.prepareBasicLoginOption(newAuthConfiguration, previousAuthConfiguration);
const oauthInternalLoginOption = this.prepareOauthInternalLoginOption(newAuthConfiguration, previousAuthConfiguration);
return Promise.all([
this.saveOrUpdateLoginOption(basicLoginOption),
this.saveOrUpdateLoginOption(oauthInternalLoginOption),
...updateTenantOptions
]);
}
map(loginOptions$) {
return loginOptions$.pipe(map(loginOptions => loginOptions.map(loginOption => this.tenantLoginOptionMapper.mapTo(loginOption))));
}
saveOrUpdateLoginOption(loginOption) {
return loginOption.id
? this.tenantLoginOptionsService.update(loginOption)
: this.tenantLoginOptionsService.create(loginOption);
}
prepareBasicLoginOption(newAuthConfiguration, previousAuthConfiguration) {
const basicLoginOption = this.originalLoginOptionWithDefaults(previousAuthConfiguration, TenantLoginOptionType.BASIC);
basicLoginOption.visibleOnLoginPage = this.visibleOnLoginPage(newAuthConfiguration, TenantLoginOptionType.BASIC);
return this.tenantLoginOptionMapper.mapFrom(basicLoginOption, this.getLoginOptionFromAuthConfiguration(newAuthConfiguration, TenantLoginOptionType.BASIC));
}
prepareOauthInternalLoginOption(newAuthConfiguration, previousAuthConfiguration) {
const oauthInternalLoginOption = this.originalLoginOptionWithDefaults(previousAuthConfiguration, TenantLoginOptionType.OAUTH2_INTERNAL);
oauthInternalLoginOption.visibleOnLoginPage = this.visibleOnLoginPage(newAuthConfiguration, TenantLoginOptionType.OAUTH2_INTERNAL);
return this.tenantLoginOptionMapper.mapFrom(oauthInternalLoginOption, this.getLoginOptionFromAuthConfiguration(newAuthConfiguration, TenantLoginOptionType.OAUTH2_INTERNAL));
}
originalLoginOptionWithDefaults(previousAuthConfiguration, loginOptionType) {
return defaults({}, this.getLoginOptionFromAuthConfiguration(previousAuthConfiguration, loginOptionType), this.getDefaultLoginOption(loginOptionType));
}
getLoginOptionFromAuthConfiguration(authConfiguration, loginOptionType) {
return authConfiguration.loginOptions.find(loginOption => loginOption.type === loginOptionType);
}
visibleOnLoginPage(authConfiguration, loginOptionType) {
return authConfiguration.preferredLoginOptionType === loginOptionType;
}
prepareTenantOptions(newAuthConfiguration, previousAuthConfiguration) {
const getValue = (authCfg, tenantOption) => authCfg.tenantOptions[tenantOption.category][tenantOption.key];
const hasChanged = tenantOption => getValue(newAuthConfiguration, tenantOption) !==
getValue(previousAuthConfiguration, tenantOption);
return this.tenantOptionsWithDefaultValue
.filter(tenantOption => getValue(newAuthConfiguration, tenantOption) !== null)
.filter(tenantOption => hasChanged(tenantOption))
.map(tenantOption => ({
category: tenantOption.category,
key: tenantOption.key,
value: getValue(newAuthConfiguration, tenantOption).toString()
}));
}
getLoginOptions$() {
return forkJoin([
this.getLoginOption(TenantLoginOptionType.OAUTH2),
this.getLoginOption(TenantLoginOptionType.BASIC),
this.getLoginOption(TenantLoginOptionType.OAUTH2_INTERNAL)
]).pipe(map(loginOptions => loginOptions.filter(loginOption => !!loginOption)));
}
getLoginOption(tenantLoginOptionType) {
return from(this.tenantLoginOptionsService.detail(tenantLoginOptionType)).pipe(map(res => res.data), catchError(() => tenantLoginOptionType !== TenantLoginOptionType.OAUTH2
? of(this.getDefaultLoginOption(tenantLoginOptionType))
: of(null)));
}
getPreferredLoginOptionType$(loginOptions$) {
return loginOptions$.pipe(map(loginOptions => {
return this.tenantUiService.getPreferredLoginOption(loginOptions).type;
}));
}
getTenantOptions$() {
return forkJoin(this.tenantOptionsWithDefaultValue.map((option) => from(this.tenantOptionsService.detail(option)).pipe(map(res => {
option.apply(res.data);
return option;
}), catchError(() => of(option))))).pipe(map(options => this.getOptionsObject(options)));
}
getSystemOptions$() {
return forkJoin(this.systemOptionsWithDefaultValue.map((option) => {
const fixedOption = this.fixTfaEnforcedSystemOption(option);
if (fixedOption) {
return fixedOption;
}
return from(this.systemOptionsService.detail(option)).pipe(map(res => {
option.apply(res.data);
return option;
}), catchError(() => of(option)));
})).pipe(map(options => this.getOptionsObject(options)));
}
/**
* Returns an observable with fixed `two-factor-authentication.enforced` system option or null.
* This method fixes problem with inconsistent value. System option `two-factor-authentication.enforced` is list of tenants when UI using boolean value.
* This part will be removed after implementing new endpoint in MTM-50490.
*/
fixTfaEnforcedSystemOption(option) {
if (option.category === 'two-factor-authentication' && option.key === 'enforced') {
return from(this.tenantService.getTfaSettings(this.tenantUiService.currentTenant)).pipe(map(tfaSettings => {
option.value = tfaSettings.enforcedOnSystemLevel.toString();
return option;
}));
}
return null;
}
/**
* Returns a promise or null.
* This method is needed now, because simply changing TFA strategy tenant option does not trigger all the necessary backend logic to apply the change, therefore, we need to call tenant's `/tfa` endpoint, which applies the change for all users in the tenant.
* Within MTM-50490, we're going to simplify the process further by replacing multiple requests with one new endpoint that will handle saving of all authentication settings.
*/
fixTfaStrategy(option) {
if (option.category === 'two-factor-authentication' && option.key === 'strategy') {
const strategy = option.value === 'SMS' ? TfaStrategy.SMS : TfaStrategy.TOTP;
return this.tenantService.updateTfaStrategy(this.tenantUiService.currentTenant, strategy);
}
return null;
}
isSmsApplicationAvailable$() {
return from(this.appState.isApplicationAvailable('sms-gateway'));
}
getOptionsObject(options) {
return options.reduce((optionsObject, option) => {
optionsObject[option.category] = optionsObject[option.category] || {};
optionsObject[option.category][option.key] = option.getValue();
return optionsObject;
}, {});
}
getDefaultLoginOption(tenantLoginOptionType) {
return {
userManagementSource: UserManagementSource.INTERNAL,
grantType: GrantType.PASSWORD,
providerName: 'Cumulocity',
visibleOnLoginPage: false,
type: tenantLoginOptionType
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthConfigurationService, deps: [{ token: i1$1.TenantLoginOptionsService }, { token: i1$1.TenantOptionsService }, { token: i1$1.SystemOptionsService }, { token: i2.AppStateService }, { token: i2.TenantUiService }, { token: TenantLoginOptionMapper }, { token: i1$1.TenantService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthConfigurationService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: AuthConfigurationService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1$1.TenantLoginOptionsService }, { type: i1$1.TenantOptionsService }, { type: i1$1.SystemOptionsService }, { type: i2.AppStateService }, { type: i2.TenantUiService }, { type: TenantLoginOptionMapper }, { type: i1$1.TenantService }] });
class LoginSettingsComponent {
constructor(tenantUiService) {
this.tenantUiService = tenantUiService;
this.PREFERRED_LOGIN_MODE_POPOVER = gettext('Main difference is the storage of the authentication information. With Basic Auth, it is saved in a session storage and with OAI-Secure in a HttpOnly cookie. OAI-Secure grant is recommended as the authentication information is not accessible via JavaScript. Single sign-on redirect allows a user to login with a single 3rd-party authorization server using the OAuth2 protocol.');
this.ENFORCED_BY_PLATFORM_POPOVER = gettext('The setting is enforced on the platform level.');
this.IGNORE_CASE_SENSITIVITY_POPOVER = gettext('If selected, the letter case of the username does not matter during login.');
this.tenantLoginOptionTypeEnum = TenantLoginOptionType;
this.PASSWORD_CATEGORY = 'password';
this.LIMIT_VALIDITY_KEY = 'limit.validity';
this.TENANT_STRENGTH_VALIDITY_KEY = 'strength.validity';
this.SYSTEM_STRENGTH_VALIDITY_KEY = 'enforce.strength';
}
ngOnChanges(changes) {
if (changes.authConfiguration && changes.authConfiguration.currentValue) {
this.isOauth2 = !!changes.authConfiguration.currentValue.loginOptions.find(this.tenantUiService.isOauth2);
}
}
get systemPasswordLimitValidity() {
return this.authConfiguration.systemOptions[this.PASSWORD_CATEGORY][this.LIMIT_VALIDITY_KEY];
}
get passwordLimitValidity() {
return this.systemPasswordLimitValidity !== null
? this.systemPasswordLimitValidity
: this.authConfiguration.tenantOptions[this.PASSWORD_CATEGORY][this.LIMIT_VALIDITY_KEY];
}
set passwordLimitValidity(value) {
if (this.systemPasswordLimitValidity === null) {
this.authConfiguration.tenantOptions[this.PASSWORD_CATEGORY][this.LIMIT_VALIDITY_KEY] = value;
}
}
get systemPasswordEnforceStrength() {
return this.authConfiguration.systemOptions[this.PASSWORD_CATEGORY][this.SYSTEM_STRENGTH_VALIDITY_KEY];
}
get passwordEnforceStrength() {
return this.systemPasswordEnforceStrength
? this.systemPasswordEnforceStrength
: this.authConfiguration.tenantOptions[this.PASSWORD_CATEGORY][this.TENANT_STRENGTH_VALIDITY_KEY];
}
set passwordEnforceStrength(value) {
if (!this.systemPasswordEnforceStrength) {
this.authConfiguration.tenantOptions[this.PASSWORD_CATEGORY][this.TENANT_STRENGTH_VALIDITY_KEY] = value;
}
}
get tenantLoginIgnoreCase() {
return this.authConfiguration.tenantOptions['configuration']['tenant.login.ignore-case'];
}
set tenantLoginIgnoreCase(value) {
this.authConfiguration.tenantOptions['configuration']['tenant.login.ignore-case'] = value;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoginSettingsComponent, deps: [{ token: i2.TenantUiService }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: LoginSettingsComponent, isStandalone: true, selector: "c8y-login-settings", inputs: { authConfiguration: "authConfiguration" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"card-block separator-top overflow-auto\" *ngIf=\"authConfiguration\">\n <div class=\"col-sm-2\">\n <div class=\"h4 text-normal text-right text-left-xs\">{{ 'Login settings' | translate }}</div>\n </div>\n\n <div class=\"col-sm-9\">\n <div class=\"row m-b-8\">\n <c8y-form-group class=\"col-sm-6\">\n <label>\n {{ 'Preferred login mode' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ PREFERRED_LOGIN_MODE_POPOVER | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n ></button>\n </label>\n <div class=\"c8y-select-wrapper\">\n <select\n [attr.aria-label]=\"'Auth type' | translate\"\n class=\"form-control\"\n id=\"preferredLoginOptionType\"\n name=\"preferredLoginOptionType\"\n [(ngModel)]=\"authConfiguration.preferredLoginOptionType\"\n >\n <option value=\"{{ tenantLoginOptionTypeEnum.BASIC }}\" translate>Basic Auth</option>\n <option value=\"{{ tenantLoginOptionTypeEnum.OAUTH2_INTERNAL }}\" translate>\n OAI-Secure\n </option>\n <option value=\"{{ tenantLoginOptionTypeEnum.OAUTH2 }}\" [disabled]=\"!isOauth2\" translate>\n Single sign-on redirect\n </option>\n </select>\n </div>\n </c8y-form-group>\n </div>\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label title=\"{{ 'Password validity limit' | translate }}\">\n {{ 'Password validity limit' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ ENFORCED_BY_PLATFORM_POPOVER | translate }}\"\n placement=\"bottom\"\n triggers=\"focus\"\n *ngIf=\"systemPasswordLimitValidity\"\n ></button>\n </label>\n <div class=\"input-group\">\n <input\n type=\"number\"\n name=\"passwordLimitValidity\"\n class=\"form-control text-right\"\n [(ngModel)]=\"passwordLimitValidity\"\n min=\"0\"\n max=\"999999\"\n step=\"1\"\n required\n [disabled]=\"systemPasswordLimitValidity\"\n />\n <span class=\"input-group-addon\" translate>days</span>\n </div>\n <p class=\"help-block\">\n {{ 'Default: 0 (unlimited validity)' | translate }}\n </p>\n </c8y-form-group>\n </div>\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label title=\"{{ 'Password strength' | translate }}\">\n {{ 'Password strength' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ ENFORCED_BY_PLATFORM_POPOVER | translate }}\"\n placement=\"bottom\"\n triggers=\"focus\"\n *ngIf=\"systemPasswordEnforceStrength\"\n ></button>\n </label>\n <div>\n <label\n title=\"{{ 'Enforce that all passwords are strong' | translate }}\"\n class=\"c8y-switch\"\n >\n <input\n type=\"checkbox\"\n name=\"passwordEnforceStrength\"\n data-cy=\"c8y-form-group--password-enforce-toggle-btn\"\n [(ngModel)]=\"passwordEnforceStrength\"\n [disabled]=\"systemPasswordEnforceStrength\"\n />\n <span></span>\n <span>{{ 'Enforce strong passwords (green)' | translate }}</span>\n </label>\n </div>\n </c8y-form-group>\n </div>\n </div>\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label\n title=\"{{ 'Ignore case when logging in' | translate }}\"\n data-cy=\"c8y-authentication-configuration--ignore-case-when-logging-in\"\n class=\"c8y-switch\"\n >\n <input\n type=\"checkbox\"\n name=\"tenantLoginIgnoreCase\"\n [(ngModel)]=\"tenantLoginIgnoreCase\"\n />\n <span></span>\n <span>{{ 'Ignore case when logging in' | translate }}</span>\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ IGNORE_CASE_SENSITIVITY_POPOVER | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n ></button>\n </label>\n </c8y-form-group>\n </div>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: FormGroupComponent, selector: "c8y-form-group", inputs: ["hasError", "hasWarning", "hasSuccess", "novalidation", "status"] }, { kind: "directive", type: PopoverDirective, selector: "[popover]", inputs: ["adaptivePosition", "boundariesElement", "popover", "popoverContext", "popoverTitle", "placement", "outsideClick", "triggers", "container", "containerClass", "isOpen", "delay"], outputs: ["onShown", "onHidden"], exportAs: ["bs-popover"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: C8yTranslateDirective, selector: "[translate],[ngx-translate]" }, { kind: "directive", type: RequiredInputPlaceholderDirective, selector: "input[required], input[formControlName]" }, { kind: "directive", type: MinValidationDirective, selector: "[min]", inputs: ["min"] }, { kind: "directive", type: MaxValidationDirective, selector: "[max]", inputs: ["max"] }, { kind: "pipe", type: C8yTranslatePipe, name: "translate" }], viewProviders: [{ provide: ControlContainer, useExisting: NgForm }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: LoginSettingsComponent, decorators: [{
type: Component,
args: [{ selector: 'c8y-login-settings', viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], imports: [
NgIf,
FormGroupComponent,
PopoverDirective,
FormsModule,
C8yTranslateDirective,
RequiredInputPlaceholderDirective,
MinValidationDirective,
MaxValidationDirective,
C8yTranslatePipe
], template: "<div class=\"card-block separator-top overflow-auto\" *ngIf=\"authConfiguration\">\n <div class=\"col-sm-2\">\n <div class=\"h4 text-normal text-right text-left-xs\">{{ 'Login settings' | translate }}</div>\n </div>\n\n <div class=\"col-sm-9\">\n <div class=\"row m-b-8\">\n <c8y-form-group class=\"col-sm-6\">\n <label>\n {{ 'Preferred login mode' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ PREFERRED_LOGIN_MODE_POPOVER | translate }}\"\n placement=\"right\"\n triggers=\"focus\"\n container=\"body\"\n ></button>\n </label>\n <div class=\"c8y-select-wrapper\">\n <select\n [attr.aria-label]=\"'Auth type' | translate\"\n class=\"form-control\"\n id=\"preferredLoginOptionType\"\n name=\"preferredLoginOptionType\"\n [(ngModel)]=\"authConfiguration.preferredLoginOptionType\"\n >\n <option value=\"{{ tenantLoginOptionTypeEnum.BASIC }}\" translate>Basic Auth</option>\n <option value=\"{{ tenantLoginOptionTypeEnum.OAUTH2_INTERNAL }}\" translate>\n OAI-Secure\n </option>\n <option value=\"{{ tenantLoginOptionTypeEnum.OAUTH2 }}\" [disabled]=\"!isOauth2\" translate>\n Single sign-on redirect\n </option>\n </select>\n </div>\n </c8y-form-group>\n </div>\n <div class=\"row\">\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label title=\"{{ 'Password validity limit' | translate }}\">\n {{ 'Password validity limit' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ ENFORCED_BY_PLATFORM_POPOVER | translate }}\"\n placement=\"bottom\"\n triggers=\"focus\"\n *ngIf=\"systemPasswordLimitValidity\"\n ></button>\n </label>\n <div class=\"input-group\">\n <input\n type=\"number\"\n name=\"passwordLimitValidity\"\n class=\"form-control text-right\"\n [(ngModel)]=\"passwordLimitValidity\"\n min=\"0\"\n max=\"999999\"\n step=\"1\"\n required\n [disabled]=\"systemPasswordLimitValidity\"\n />\n <span class=\"input-group-addon\" translate>days</span>\n </div>\n <p class=\"help-block\">\n {{ 'Default: 0 (unlimited validity)' | translate }}\n </p>\n </c8y-form-group>\n </div>\n <div class=\"col-sm-6\">\n <c8y-form-group>\n <label title=\"{{ 'Password strength' | translate }}\">\n {{ 'Password strength' | translate }}\n <button\n class=\"btn-help btn-help--sm\"\n type=\"button\"\n [attr.aria-label]=\"'Help' | translate\"\n popover=\"{{ ENFORCED_BY_PLATFORM_POPOVER | translate }}\"\n placement=\"bottom\"\n triggers=\"focus\"\n *ngIf=\"systemPasswordEnforceStrength\"\n ></button>\n </label>\n <div>\n <label\n title=\"{{ 'Enforce that all passwords are strong' | translate }}\"\n class=\"c8y-switch\"\n >\n <input\n type=\"checkbox\"\n name=\"passwordEnforceStrength\"\n