Browse Source

Merge with develop/3.4

pull/6713/head
Igor Kulikov 4 years ago
parent
commit
18ec32f092
  1. 27
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
  2. 7
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java
  3. 6
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  4. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java
  5. 7
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java
  6. 3
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java
  7. 4
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java
  8. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java
  9. 4
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
  10. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  11. 41
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java
  12. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java
  13. 177
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java
  14. 11
      ui-ngx/src/app/core/auth/auth.service.ts
  15. 7
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  16. 2
      ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts
  17. 6
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  18. 4
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  19. 55
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.html
  20. 99
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.ts
  21. 3
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html
  22. 11
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
  23. 4
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.html
  24. 10
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html
  25. 11
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts
  26. 15
      ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss
  27. 64
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html
  28. 17
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.scss
  29. 70
      ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts
  30. 18
      ui-ngx/src/app/modules/home/pages/profile/profile.component.html
  31. 6
      ui-ngx/src/app/modules/home/pages/profile/profile.component.scss
  32. 55
      ui-ngx/src/app/modules/home/pages/profile/profile.component.ts
  33. 4
      ui-ngx/src/app/modules/home/pages/profile/profile.module.ts
  34. 115
      ui-ngx/src/app/modules/home/pages/security/security.component.html
  35. 59
      ui-ngx/src/app/modules/home/pages/security/security.component.scss
  36. 141
      ui-ngx/src/app/modules/home/pages/security/security.component.ts
  37. 8
      ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts
  38. 4
      ui-ngx/src/app/shared/models/device.models.ts
  39. 7
      ui-ngx/src/app/shared/models/two-factor-auth.models.ts
  40. 79
      ui-ngx/src/assets/help/en_US/device-profile/alarm_custom_schedule_format.md
  41. 31
      ui-ngx/src/assets/help/en_US/device-profile/alarm_specific_schedule_format.md
  42. 24
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  43. 3
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  44. 3
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  45. 1
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  46. 7
      ui-ngx/src/theme.scss

27
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java

@ -56,12 +56,31 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
@Override @Override
public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId) { public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId) {
PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null);
return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
.flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaSettings())) .map(userAuthSettings -> {
.map(twoFaSettings -> { AccountTwoFaSettings twoFaSettings = userAuthSettings.getTwoFaSettings();
twoFaSettings.getConfigs().keySet().removeIf(providerType -> { if (twoFaSettings == null) return null;
return getTwoFaProviderConfig(tenantId, providerType).isEmpty(); boolean updateNeeded;
Map<TwoFaProviderType, TwoFaAccountConfig> configs = twoFaSettings.getConfigs();
updateNeeded = configs.keySet().removeIf(providerType -> {
return platformTwoFaSettings == null || platformTwoFaSettings.getProviderConfig(providerType).isEmpty();
}); });
if (configs.size() == 1 && configs.containsKey(TwoFaProviderType.BACKUP_CODE)) {
configs.remove(TwoFaProviderType.BACKUP_CODE);
updateNeeded = true;
}
if (!configs.isEmpty() && configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) {
configs.values().stream()
.filter(config -> config.getProviderType() != TwoFaProviderType.BACKUP_CODE)
.findFirst().ifPresent(config -> config.setUseByDefault(true));
updateNeeded = true;
}
if (updateNeeded) {
twoFaSettings = saveAccountTwoFaSettings(tenantId, userId, twoFaSettings);
}
return twoFaSettings; return twoFaSettings;
}); });
} }

7
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java

@ -96,6 +96,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig)); twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig));
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
twoFaSettings.setTotalAllowedTimeForVerification(3600); twoFaSettings.setTotalAllowedTimeForVerification(3600);
@ -117,6 +118,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
twoFaSettings.setVerificationCodeCheckRateLimit("0:12"); twoFaSettings.setVerificationCodeCheckRateLimit("0:12");
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(-1); twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(-1);
twoFaSettings.setTotalAllowedTimeForVerification(0); twoFaSettings.setTotalAllowedTimeForVerification(0);
twoFaSettings.setMinVerificationCodeSendPeriod(5);
String errorMessage = getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) String errorMessage = getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
.andExpect(status().isBadRequest())); .andExpect(status().isBadRequest()));
@ -156,6 +158,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100);
return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
.andExpect(status().isBadRequest())); .andExpect(status().isBadRequest()));
@ -432,8 +436,9 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
private void saveProvidersConfigs(TwoFaProviderConfig... providerConfigs) throws Exception { private void saveProvidersConfigs(TwoFaProviderConfig... providerConfigs) throws Exception {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList()));
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100);
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
} }

6
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java

@ -355,6 +355,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
emailTwoFaProviderConfig.setVerificationCodeLifetime(60); emailTwoFaProviderConfig.setVerificationCodeLifetime(60);
platformTwoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig, emailTwoFaProviderConfig)); platformTwoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig, emailTwoFaProviderConfig));
platformTwoFaSettings.setMinVerificationCodeSendPeriod(5);
platformTwoFaSettings.setTotalAllowedTimeForVerification(100);
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, platformTwoFaSettings); twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, platformTwoFaSettings);
User twoFaUser = new User(); User twoFaUser = new User();
@ -409,6 +411,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100);
Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings)); Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings));
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
@ -425,6 +429,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList())); twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList()));
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100);
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();

3
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmSchedule.java

@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.common.data.query.DynamicValue;
import java.io.Serializable; import java.io.Serializable;
@ -34,4 +35,6 @@ public interface AlarmSchedule extends Serializable {
AlarmScheduleType getType(); AlarmScheduleType getType();
DynamicValue<String> getDynamicValue();
} }

7
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AnyTimeSchedule.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.server.common.data.device.profile; package org.thingsboard.server.common.data.device.profile;
import org.thingsboard.server.common.data.query.DynamicValue;
public class AnyTimeSchedule implements AlarmSchedule { public class AnyTimeSchedule implements AlarmSchedule {
@Override @Override
@ -22,4 +24,9 @@ public class AnyTimeSchedule implements AlarmSchedule {
return AlarmScheduleType.ANY_TIME; return AlarmScheduleType.ANY_TIME;
} }
@Override
public DynamicValue<String> getDynamicValue() {
return null;
}
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CustomTimeSchedule.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.device.profile; package org.thingsboard.server.common.data.device.profile;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.query.DynamicValue;
import java.util.List; import java.util.List;
@ -25,6 +26,8 @@ public class CustomTimeSchedule implements AlarmSchedule {
private String timezone; private String timezone;
private List<CustomTimeScheduleItem> items; private List<CustomTimeScheduleItem> items;
private DynamicValue<String> dynamicValue;
@Override @Override
public AlarmScheduleType getType() { public AlarmScheduleType getType() {
return AlarmScheduleType.CUSTOM; return AlarmScheduleType.CUSTOM;

4
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SpecificTimeSchedule.java

@ -16,8 +16,8 @@
package org.thingsboard.server.common.data.device.profile; package org.thingsboard.server.common.data.device.profile;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.query.DynamicValue;
import java.util.List;
import java.util.Set; import java.util.Set;
@Data @Data
@ -28,6 +28,8 @@ public class SpecificTimeSchedule implements AlarmSchedule {
private long startsOn; private long startsOn;
private long endsOn; private long endsOn;
private DynamicValue<String> dynamicValue;
@Override @Override
public AlarmScheduleType getType() { public AlarmScheduleType getType() {
return AlarmScheduleType.SPECIFIC_TIME; return AlarmScheduleType.SPECIFIC_TIME;

2
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java

@ -35,12 +35,14 @@ public class PlatformTwoFaSettings {
@NotNull @NotNull
private List<TwoFaProviderConfig> providers; private List<TwoFaProviderConfig> providers;
@NotNull
@Min(value = 5, message = "minimum verification code sent period must be greater than or equal 5") @Min(value = 5, message = "minimum verification code sent period must be greater than or equal 5")
private Integer minVerificationCodeSendPeriod; private Integer minVerificationCodeSendPeriod;
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid") @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
private String verificationCodeCheckRateLimit; private String verificationCodeCheckRateLimit;
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive") @Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
private Integer maxVerificationFailuresBeforeUserLockout; private Integer maxVerificationFailuresBeforeUserLockout;
@NotNull
@Min(value = 60, message = "total amount of time allotted for verification must be greater than or equal 60") @Min(value = 60, message = "total amount of time allotted for verification must be greater than or equal 60")
private Integer totalAllowedTimeForVerification; private Integer totalAllowedTimeForVerification;

4
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java

@ -575,6 +575,10 @@ public class JsonConverter {
return JSON_PARSER.parse(json); return JSON_PARSER.parse(json);
} }
public static <T> T parse(String json, Class<T> clazz) {
return fromJson(parse(json), clazz);
}
public static String toJson(JsonElement element) { public static String toJson(JsonElement element) {
return GSON.toJson(element); return GSON.toJson(element);
} }

4
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -371,7 +371,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
try { try {
return jdbcTemplate.queryForObject(countQuery, ctx, Long.class); return jdbcTemplate.queryForObject(countQuery, ctx, Long.class);
} finally { } finally {
queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs); queryLog.logQuery(ctx, countQuery, System.currentTimeMillis() - startTs);
} }
}); });
} }
@ -483,7 +483,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
try { try {
rows = jdbcTemplate.queryForList(dataQuery, ctx); rows = jdbcTemplate.queryForList(dataQuery, ctx);
} finally { } finally {
queryLog.logQuery(ctx, countQuery, System.currentTimeMillis() - startTs); queryLog.logQuery(ctx, dataQuery, System.currentTimeMillis() - startTs);
} }
return EntityDataAdapter.createEntityData(pageLink, selectionMapping, rows, totalElements); return EntityDataAdapter.createEntityData(pageLink, selectionMapping, rows, totalElements);
}); });

41
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmRuleState.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType; import org.thingsboard.server.common.data.device.profile.AlarmConditionSpecType;
import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule; import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule;
import org.thingsboard.server.common.data.device.profile.AlarmSchedule;
import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem; import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem;
import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec;
@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.query.KeyFilterPredicate;
import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.StringFilterPredicate; import org.thingsboard.server.common.data.query.StringFilterPredicate;
import org.thingsboard.server.common.msg.tools.SchedulerUtils; import org.thingsboard.server.common.msg.tools.SchedulerUtils;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
@ -115,7 +117,7 @@ class AlarmRuleState {
} }
public AlarmEvalResult eval(DataSnapshot data) { public AlarmEvalResult eval(DataSnapshot data) {
boolean active = isActive(data.getTs()); boolean active = isActive(data, data.getTs());
switch (spec.getType()) { switch (spec.getType()) {
case SIMPLE: case SIMPLE:
return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE; return (active && eval(alarmRule.getCondition(), data)) ? AlarmEvalResult.TRUE : AlarmEvalResult.FALSE;
@ -128,7 +130,7 @@ class AlarmRuleState {
} }
} }
private boolean isActive(long eventTs) { private boolean isActive(DataSnapshot data, long eventTs) {
if (eventTs == 0L) { if (eventTs == 0L) {
eventTs = System.currentTimeMillis(); eventTs = System.currentTimeMillis();
} }
@ -139,14 +141,28 @@ class AlarmRuleState {
case ANY_TIME: case ANY_TIME:
return true; return true;
case SPECIFIC_TIME: case SPECIFIC_TIME:
return isActiveSpecific((SpecificTimeSchedule) alarmRule.getSchedule(), eventTs); return isActiveSpecific((SpecificTimeSchedule) getSchedule(data, alarmRule), eventTs);
case CUSTOM: case CUSTOM:
return isActiveCustom((CustomTimeSchedule) alarmRule.getSchedule(), eventTs); return isActiveCustom((CustomTimeSchedule) getSchedule(data, alarmRule), eventTs);
default: default:
throw new RuntimeException("Unsupported schedule type: " + alarmRule.getSchedule().getType()); throw new RuntimeException("Unsupported schedule type: " + alarmRule.getSchedule().getType());
} }
} }
private AlarmSchedule getSchedule(DataSnapshot data, AlarmRule alarmRule) {
AlarmSchedule schedule = alarmRule.getSchedule();
EntityKeyValue dynamicValue = getDynamicPredicateValue(data, schedule.getDynamicValue());
if (dynamicValue != null) {
try {
return JsonConverter.parse(dynamicValue.getJsonValue(), alarmRule.getSchedule().getClass());
} catch (Exception e) {
log.trace("Failed to parse AlarmSchedule from dynamicValue: {}", dynamicValue.getJsonValue(), e);
}
}
return schedule;
}
private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) { private boolean isActiveSpecific(SpecificTimeSchedule schedule, long eventTs) {
ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone()); ZoneId zoneId = SchedulerUtils.getZoneId(schedule.getTimezone());
ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId); ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(eventTs), zoneId);
@ -156,7 +172,13 @@ class AlarmRuleState {
return false; return false;
} }
} }
return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), schedule.getEndsOn()); long endsOn = schedule.getEndsOn();
if (endsOn == 0) {
// 24 hours in milliseconds
endsOn = 86400000;
}
return isActive(eventTs, zoneId, zdt, schedule.getStartsOn(), endsOn);
} }
private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) { private boolean isActiveCustom(CustomTimeSchedule schedule, long eventTs) {
@ -166,7 +188,12 @@ class AlarmRuleState {
for (CustomTimeScheduleItem item : schedule.getItems()) { for (CustomTimeScheduleItem item : schedule.getItems()) {
if (item.getDayOfWeek() == dayOfWeek) { if (item.getDayOfWeek() == dayOfWeek) {
if (item.isEnabled()) { if (item.isEnabled()) {
return isActive(eventTs, zoneId, zdt, item.getStartsOn(), item.getEndsOn()); long endsOn = item.getEndsOn();
if (endsOn == 0) {
// 24 hours in milliseconds
endsOn = 86400000;
}
return isActive(eventTs, zoneId, zdt, item.getStartsOn(), endsOn);
} else { } else {
return false; return false;
} }
@ -279,7 +306,7 @@ class AlarmRuleState {
long requiredDurationInMs = resolveRequiredDurationInMs(dataSnapshot); long requiredDurationInMs = resolveRequiredDurationInMs(dataSnapshot);
if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) { if (requiredDurationInMs > 0 && state.getLastEventTs() > 0 && ts > state.getLastEventTs()) {
long duration = state.getDuration() + (ts - state.getLastEventTs()); long duration = state.getDuration() + (ts - state.getLastEventTs());
if (isActive(ts)) { if (isActive(dataSnapshot, ts)) {
return duration > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE; return duration > requiredDurationInMs ? AlarmEvalResult.TRUE : AlarmEvalResult.NOT_YET_TRUE;
} else { } else {
return AlarmEvalResult.FALSE; return AlarmEvalResult.FALSE;

15
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/ProfileState.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.AlarmSchedule;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValue;
@ -77,6 +78,10 @@ class ProfileState {
addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys); addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys);
} }
addEntityKeysFromAlarmConditionSpec(alarmRule); addEntityKeysFromAlarmConditionSpec(alarmRule);
AlarmSchedule schedule = alarmRule.getSchedule();
if (schedule != null) {
addScheduleDynamicValues(schedule);
}
})); }));
if (alarm.getClearRule() != null) { if (alarm.getClearRule() != null) {
var clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>()); var clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>());
@ -91,6 +96,16 @@ class ProfileState {
} }
} }
private void addScheduleDynamicValues(AlarmSchedule schedule) {
DynamicValue<String> dynamicValue = schedule.getDynamicValue();
if (dynamicValue != null) {
entityKeys.add(
new AlarmConditionFilterKey(AlarmConditionKeyType.ATTRIBUTE,
dynamicValue.getSourceAttribute())
);
}
}
private void addEntityKeysFromAlarmConditionSpec(AlarmRule alarmRule) { private void addEntityKeysFromAlarmConditionSpec(AlarmRule alarmRule) {
AlarmConditionSpec spec = alarmRule.getCondition().getSpec(); AlarmConditionSpec spec = alarmRule.getCondition().getSpec();
if (spec == null) { if (spec == null) {

177
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java

@ -44,9 +44,12 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec;
import org.thingsboard.server.common.data.device.profile.CustomTimeSchedule;
import org.thingsboard.server.common.data.device.profile.CustomTimeScheduleItem;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
@ -71,6 +74,7 @@ import java.math.RoundingMode;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.ArrayList;
import java.util.Optional; import java.util.Optional;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
@ -1086,6 +1090,179 @@ public class TbDeviceProfileNodeTest {
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
} }
@Test
public void testActiveAlarmScheduleFromDynamicValuesWhenDefaultScheduleIsInactive() throws Exception {
init();
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setId(deviceProfileId);
DeviceProfileData deviceProfileData = new DeviceProfileData();
Device device = new Device();
device.setId(deviceId);
device.setCustomerId(customerId);
AttributeKvCompositeKey compositeKeyActiveSchedule = new AttributeKvCompositeKey(
EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "dynamicValueActiveSchedule"
);
AttributeKvEntity attributeKvEntityActiveSchedule = new AttributeKvEntity();
attributeKvEntityActiveSchedule.setId(compositeKeyActiveSchedule);
attributeKvEntityActiveSchedule.setJsonValue(
"{\"timezone\":\"Europe/Kiev\",\"items\":[{\"enabled\":true,\"dayOfWeek\":1,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":2,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":3,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":4,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":5,\"startsOn\":0,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":6,\"startsOn\":8.64e+7,\"endsOn\":8.64e+7},{\"enabled\":true,\"dayOfWeek\":7,\"startsOn\":0,\"endsOn\":8.64e+7}],\"dynamicValue\":null}"
);
attributeKvEntityActiveSchedule.setLastUpdateTs(0L);
AttributeKvEntry entryActiveSchedule = attributeKvEntityActiveSchedule.toData();
ListenableFuture<List<AttributeKvEntry>> listListenableFutureActiveSchedule =
Futures.immediateFuture(Collections.singletonList(entryActiveSchedule));
AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
highTemperaturePredicate.setValue(new FilterPredicateValue<>(
0.0,
null,
null
));
highTempFilter.setPredicate(highTemperaturePredicate);
AlarmCondition alarmCondition = new AlarmCondition();
alarmCondition.setCondition(Collections.singletonList(highTempFilter));
CustomTimeSchedule schedule = new CustomTimeSchedule();
schedule.setItems(Collections.emptyList());
schedule.setDynamicValue(new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "dynamicValueActiveSchedule", false));
AlarmRule alarmRule = new AlarmRule();
alarmRule.setCondition(alarmCondition);
alarmRule.setSchedule(schedule);
DeviceProfileAlarm deviceProfileAlarmActiveSchedule = new DeviceProfileAlarm();
deviceProfileAlarmActiveSchedule.setId("highTemperatureAlarmID");
deviceProfileAlarmActiveSchedule.setAlarmType("highTemperatureAlarm");
deviceProfileAlarmActiveSchedule.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
deviceProfileData.setAlarms(Collections.singletonList(deviceProfileAlarmActiveSchedule));
deviceProfile.setProfileData(deviceProfileData);
Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
.thenReturn(Futures.immediateFuture(Collections.emptyList()));
Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
.thenReturn(Futures.immediateFuture(null));
Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg());
Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
.thenReturn(listListenableFutureActiveSchedule);
TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString()))
.thenReturn(theMsg);
ObjectNode data = mapper.createObjectNode();
data.put("temperature", 35);
TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg);
verify(ctx).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
}
@Test
public void testInactiveAlarmScheduleFromDynamicValuesWhenDefaultScheduleIsActive() throws Exception {
init();
DeviceProfile deviceProfile = new DeviceProfile();
deviceProfile.setId(deviceProfileId);
DeviceProfileData deviceProfileData = new DeviceProfileData();
Device device = new Device();
device.setId(deviceId);
device.setCustomerId(customerId);
AttributeKvCompositeKey compositeKeyInactiveSchedule = new AttributeKvCompositeKey(
EntityType.TENANT, deviceId.getId(), "SERVER_SCOPE", "dynamicValueInactiveSchedule"
);
AttributeKvEntity attributeKvEntityInactiveSchedule = new AttributeKvEntity();
attributeKvEntityInactiveSchedule.setId(compositeKeyInactiveSchedule);
attributeKvEntityInactiveSchedule.setJsonValue(
"{\"timezone\":\"Europe/Kiev\",\"items\":[{\"enabled\":false,\"dayOfWeek\":1,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":2,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":3,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":4,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":5,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":6,\"startsOn\":0,\"endsOn\":0},{\"enabled\":false,\"dayOfWeek\":7,\"startsOn\":0,\"endsOn\":0}],\"dynamicValue\":null}"
);
attributeKvEntityInactiveSchedule.setLastUpdateTs(0L);
AttributeKvEntry entryInactiveSchedule = attributeKvEntityInactiveSchedule.toData();
ListenableFuture<List<AttributeKvEntry>> listListenableFutureInactiveSchedule =
Futures.immediateFuture(Collections.singletonList(entryInactiveSchedule));
AlarmConditionFilter highTempFilter = new AlarmConditionFilter();
highTempFilter.setKey(new AlarmConditionFilterKey(AlarmConditionKeyType.TIME_SERIES, "temperature"));
highTempFilter.setValueType(EntityKeyValueType.NUMERIC);
NumericFilterPredicate highTemperaturePredicate = new NumericFilterPredicate();
highTemperaturePredicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
highTemperaturePredicate.setValue(new FilterPredicateValue<>(
0.0,
null,
null
));
highTempFilter.setPredicate(highTemperaturePredicate);
AlarmCondition alarmCondition = new AlarmCondition();
alarmCondition.setCondition(Collections.singletonList(highTempFilter));
CustomTimeSchedule schedule = new CustomTimeSchedule();
List<CustomTimeScheduleItem> items = new ArrayList<>();
for (int i = 0; i < 7; i++) {
CustomTimeScheduleItem item = new CustomTimeScheduleItem();
item.setEnabled(true);
item.setDayOfWeek(i + 1);
item.setEndsOn(0);
item.setStartsOn(0);
items.add(item);
}
schedule.setItems(items);
schedule.setDynamicValue(new DynamicValue<>(DynamicValueSourceType.CURRENT_DEVICE, "dynamicValueInactiveSchedule", false));
AlarmRule alarmRule = new AlarmRule();
alarmRule.setCondition(alarmCondition);
alarmRule.setSchedule(schedule);
DeviceProfileAlarm deviceProfileAlarmNonactiveSchedule = new DeviceProfileAlarm();
deviceProfileAlarmNonactiveSchedule.setId("highTemperatureAlarmID");
deviceProfileAlarmNonactiveSchedule.setAlarmType("highTemperatureAlarm");
deviceProfileAlarmNonactiveSchedule.setCreateRules(new TreeMap<>(Collections.singletonMap(AlarmSeverity.CRITICAL, alarmRule)));
deviceProfileData.setAlarms(Collections.singletonList(deviceProfileAlarmNonactiveSchedule));
deviceProfile.setProfileData(deviceProfileData);
Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile);
Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature")))
.thenReturn(Futures.immediateFuture(Collections.emptyList()));
Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm"))
.thenReturn(Futures.immediateFuture(null));
Mockito.when(ctx.getAttributesService()).thenReturn(attributesService);
Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet()))
.thenReturn(listListenableFutureInactiveSchedule);
TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "");
ObjectNode data = mapper.createObjectNode();
data.put("temperature", 35);
TbMsg msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(),
TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null);
node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg);
verify(ctx, Mockito.never()).enqueueForTellNext(theMsg, "Alarm Created");
verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any());
}
@Test @Test
public void testCurrentCustomersAttributeForDynamicValue() throws Exception { public void testCurrentCustomersAttributeForDynamicValue() throws Exception {

11
ui-ngx/src/app/core/auth/auth.service.ts

@ -23,7 +23,7 @@ import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models'; import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models';
import { ActivatedRoute, Router, UrlTree } from '@angular/router'; import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { defaultHttpOptions } from '../http/http-utils'; import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from '../http/http-utils';
import { UserService } from '../http/user.service'; import { UserService } from '../http/user.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '../core.state'; import { AppState } from '../core.state';
@ -47,6 +47,7 @@ import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.com
import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models'; import { OAuth2ClientInfo, PlatformType } from '@shared/models/oauth2.models';
import { isMobileApp } from '@core/utils'; import { isMobileApp } from '@core/utils';
import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models'; import { TwoFactorAuthProviderType, TwoFaProviderInfo } from '@shared/models/two-factor-auth.models';
import { UserPasswordPolicy } from '@shared/models/settings.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -163,14 +164,18 @@ export class AuthService {
)); ));
} }
public changePassword(currentPassword: string, newPassword: string) { public changePassword(currentPassword: string, newPassword: string, config?: RequestConfig) {
return this.http.post('/api/auth/changePassword', {currentPassword, newPassword}, defaultHttpOptions()).pipe( return this.http.post('/api/auth/changePassword', {currentPassword, newPassword}, defaultHttpOptionsFromConfig(config)).pipe(
tap((loginResponse: LoginResponse) => { tap((loginResponse: LoginResponse) => {
this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, false); this.setUserFromJwtToken(loginResponse.token, loginResponse.refreshToken, false);
} }
)); ));
} }
public getUserPasswordPolicy() {
return this.http.get<UserPasswordPolicy>(`/api/noauth/userPasswordPolicy`, defaultHttpOptions());
}
public activateByEmailCode(emailCode: string): Observable<LoginResponse> { public activateByEmailCode(emailCode: string): Observable<LoginResponse> {
return this.http.post<LoginResponse>(`/api/noauth/activateByEmailCode?emailCode=${emailCode}`, return this.http.post<LoginResponse>(`/api/noauth/activateByEmailCode?emailCode=${emailCode}`,
null, defaultHttpOptions()); null, defaultHttpOptions());

7
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -1135,6 +1135,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
$event.stopPropagation(); $event.stopPropagation();
if (this.isAddingWidget) {
this.onAddWidgetClosed();
this.isAddingWidgetClosed = true;
this.isEditingWidgetClosed = false;
}
if (this.editingWidgetOriginal === widget) { if (this.editingWidgetOriginal === widget) {
this.onEditWidgetClosed(); this.onEditWidgetClosed();
} else { } else {

2
ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts

@ -138,7 +138,7 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig<EdgeEvent, TimePa
} }
private updateEdgeEventStatus(createdTime: number): string { private updateEdgeEventStatus(createdTime: number): string {
if (this.queueStartTs && createdTime < this.queueStartTs) { if (this.queueStartTs && createdTime <= this.queueStartTs) {
return this.translate.instant('edge.deployed'); return this.translate.instant('edge.deployed');
} else { } else {
return this.translate.instant('edge.pending'); return this.translate.instant('edge.pending');

6
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -295,8 +295,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
if (this.pageMode) { if (this.pageMode) {
this.route.queryParams.pipe(skip(1)).subscribe((params: PageQueryParam) => { this.route.queryParams.pipe(skip(1)).subscribe((params: PageQueryParam) => {
this.paginator.pageIndex = Number(params.page) || 0; if (this.displayPagination) {
this.paginator.pageSize = Number(params.pageSize) || this.defaultPageSize; this.paginator.pageIndex = Number(params.page) || 0;
this.paginator.pageSize = Number(params.pageSize) || this.defaultPageSize;
}
this.sort.active = params.property || this.entitiesTableConfig.defaultSortOrder.property; this.sort.active = params.property || this.entitiesTableConfig.defaultSortOrder.property;
this.sort.direction = (params.direction || this.entitiesTableConfig.defaultSortOrder.direction).toLowerCase() as SortDirection; this.sort.direction = (params.direction || this.entitiesTableConfig.defaultSortOrder.direction).toLowerCase() as SortDirection;
if (params.hasOwnProperty('textSearch') && !isEmptyStr(params.textSearch)) { if (params.hasOwnProperty('textSearch') && !isEmptyStr(params.textSearch)) {

4
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -148,6 +148,7 @@ import {
HOME_COMPONENTS_MODULE_TOKEN HOME_COMPONENTS_MODULE_TOKEN
} from '@home/components/tokens'; } from '@home/components/tokens';
import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component'; import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component';
import { AlarmDynamicValue } from '@home/components/profile/alarm/alarm-dynamic-value.component';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component'; import { TenantProfileQueuesComponent } from '@home/components/profile/queue/tenant-profile-queues.component';
import { QueueFormComponent } from '@home/components/queue/queue-form.component'; import { QueueFormComponent } from '@home/components/queue/queue-form.component';
@ -265,6 +266,7 @@ import { AutoCommitSettingsComponent } from '@home/components/vc/auto-commit-set
AlarmScheduleInfoComponent, AlarmScheduleInfoComponent,
DeviceProfileProvisionConfigurationComponent, DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent, AlarmScheduleComponent,
AlarmDynamicValue,
AlarmDurationPredicateValueComponent, AlarmDurationPredicateValueComponent,
DeviceWizardDialogComponent, DeviceWizardDialogComponent,
AlarmScheduleDialogComponent, AlarmScheduleDialogComponent,
@ -394,11 +396,11 @@ import { AutoCommitSettingsComponent } from '@home/components/vc/auto-commit-set
DeviceWizardDialogComponent, DeviceWizardDialogComponent,
AlarmScheduleInfoComponent, AlarmScheduleInfoComponent,
AlarmScheduleComponent, AlarmScheduleComponent,
AlarmDynamicValue,
AlarmScheduleDialogComponent, AlarmScheduleDialogComponent,
AlarmDurationPredicateValueComponent, AlarmDurationPredicateValueComponent,
EditAlarmDetailsDialogComponent, EditAlarmDetailsDialogComponent,
DeviceProfileProvisionConfigurationComponent, DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent,
SmsProviderConfigurationComponent, SmsProviderConfigurationComponent,
AwsSnsProviderConfigurationComponent, AwsSnsProviderConfigurationComponent,
SmppSmsProviderConfigurationComponent, SmppSmsProviderConfigurationComponent,

55
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.html

@ -0,0 +1,55 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-expansion-panel [formGroup] = "dynamicValue" class="device-profile-alarm" style = "margin-bottom: 26px;" fxFlex>
<mat-expansion-panel-header>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-panel-title>
<div fxLayout="row" fxFlex fxLayoutAlign="start center">
{{'filter.dynamic-value' | translate}}
</div>
</mat-panel-title>
<span fxFlex></span>
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxFlex fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<div fxFlex="40" fxLayout="column">
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block">
<mat-label></mat-label>
<mat-select formControlName="sourceType" placeholder="{{'filter.dynamic-source-type' | translate}}">
<mat-option [value]="null">
{{'filter.no-dynamic-value' | translate}}
</mat-option>
<mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType">
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex fxLayout="column">
<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block source-attribute">
<mat-label></mat-label>
<input matInput formControlName="sourceAttribute" placeholder="{{'filter.source-attribute' | translate}}">
</mat-form-field>
</div>
<div [tb-help-popup]="helpId"></div>
</div>
</div>
</ng-template>
</mat-expansion-panel>

99
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.ts

@ -0,0 +1,99 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import {
DynamicValueSourceType,
dynamicValueSourceTypeTranslationMap,
getDynamicSourcesForAllowUser
} from '@shared/models/query/query.models';
@Component({
selector: 'tb-alarm-dynamic-value',
templateUrl: './alarm-dynamic-value.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmDynamicValue),
multi: true
}]
})
export class AlarmDynamicValue implements ControlValueAccessor, OnInit{
public dynamicValue: FormGroup;
public dynamicValueSourceTypes: DynamicValueSourceType[] = getDynamicSourcesForAllowUser(false);
public dynamicValueSourceTypeTranslations = dynamicValueSourceTypeTranslationMap;
private propagateChange = (v: any) => { };
@Input()
helpId: string;
@Input()
disabled: boolean;
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.dynamicValue = this.fb.group({
sourceType: [null, []],
sourceAttribute: [null]
})
this.dynamicValue.get('sourceType').valueChanges.subscribe(
(sourceType) => {
if (!sourceType) {
this.dynamicValue.get('sourceAttribute').patchValue(null, {emitEvent: false});
}
}
);
this.dynamicValue.valueChanges.subscribe(() => {
this.updateModel();
})
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
writeValue(dynamicValue: {sourceType: string, sourceAttribute: string}): void {
if(dynamicValue) {
this.dynamicValue.patchValue(dynamicValue, {emitEvent: false});
}
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.dynamicValue.disable({emitEvent: false});
} else {
this.dynamicValue.enable({emitEvent: false});
}
}
private updateModel() {
this.propagateChange(this.dynamicValue.value);
}
}

3
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html

@ -34,6 +34,7 @@
formControlName="timezone"> formControlName="timezone">
</tb-timezone-select> </tb-timezone-select>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME"> <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME">
<tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_specific_schedule_format'></tb-alarm-dynamic-value>
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px"> <div fxLayout="column" fxLayout.gt-md="row" fxLayoutGap="16px">
<div fxLayout="row" fxLayoutGap="16px"> <div fxLayout="row" fxLayoutGap="16px">
@ -73,8 +74,8 @@
</div> </div>
</section> </section>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM"> <section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM">
<tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_custom_schedule_format'></tb-alarm-dynamic-value>
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div *ngFor="let day of allDays" fxLayout="column" formArrayName="items" fxLayoutGap="1em"> <div *ngFor="let day of allDays" fxLayout="column" formArrayName="items" fxLayoutGap="1em">
<div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" [formGroupName]="''+day" fxLayoutAlign="start center" fxLayoutAlign.xs="center start"> <div fxLayout.xs="column" fxLayout="row" fxLayoutGap="8px" [formGroupName]="''+day" fxLayoutAlign="start center" fxLayoutAlign.xs="center start">
<mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, day)"> <mat-checkbox formControlName="enabled" fxFlex="17" (change)="changeCustomScheduler($event, day)">

11
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts

@ -64,7 +64,6 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
alarmScheduleTypes = Object.keys(AlarmScheduleType); alarmScheduleTypes = Object.keys(AlarmScheduleType);
alarmScheduleType = AlarmScheduleType; alarmScheduleType = AlarmScheduleType;
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
dayOfWeekTranslationsArray = dayOfWeekTranslations; dayOfWeekTranslationsArray = dayOfWeekTranslations;
allDays = Array(7).fill(0).map((x, i) => i); allDays = Array(7).fill(0).map((x, i) => i);
@ -91,8 +90,10 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
daysOfWeek: this.fb.array(new Array(7).fill(false), this.validateDayOfWeeks), daysOfWeek: this.fb.array(new Array(7).fill(false), this.validateDayOfWeeks),
startsOn: [0, Validators.required], startsOn: [0, Validators.required],
endsOn: [0, Validators.required], endsOn: [0, Validators.required],
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems) items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems),
dynamicValue: [null]
}); });
this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => { this.alarmScheduleForm.get('type').valueChanges.subscribe((type) => {
const defaultTimezone = getDefaultTimezone(); const defaultTimezone = getDefaultTimezone();
this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false}); this.alarmScheduleForm.reset({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false});
@ -158,7 +159,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
timezone: this.modelValue.timezone, timezone: this.modelValue.timezone,
daysOfWeek, daysOfWeek,
startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn), startsOn: utcTimestampToTimeOfDay(this.modelValue.startsOn),
endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn) endsOn: utcTimestampToTimeOfDay(this.modelValue.endsOn),
dynamicValue: this.modelValue.dynamicValue
}, {emitEvent: false}); }, {emitEvent: false});
break; break;
case AlarmScheduleType.CUSTOM: case AlarmScheduleType.CUSTOM:
@ -177,7 +179,8 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
this.alarmScheduleForm.patchValue({ this.alarmScheduleForm.patchValue({
type: this.modelValue.type, type: this.modelValue.type,
timezone: this.modelValue.timezone, timezone: this.modelValue.timezone,
items: alarmDays items: alarmDays,
dynamicValue: this.modelValue.dynamicValue
}, {emitEvent: false}); }, {emitEvent: false});
} }
break; break;

4
ui-ngx/src/app/modules/home/components/widget/lib/rpc/knob.component.html

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-knob" fxLayout="column" [ngStyle]="{'pointerEvents': ctx.isEdit ? 'none' : 'all'}"> <div #knobContainer class="tb-knob" fxLayout="column" [ngStyle]="{'pointerEvents': ctx.isEdit ? 'none' : 'all'}">
<div #knobContainer id="knob-container" fxFlex fxLayout="column" fxLayoutAlign="center center"> <div fxFlex fxLayout="column" fxLayoutAlign="center center">
<div #knob class="knob"> <div #knob class="knob">
<div #knobValueContainer class="value-container" fxLayout="row" fxLayoutAlign="center center"> <div #knobValueContainer class="value-container" fxLayout="row" fxLayoutAlign="center center">
<span #knobValue class="knob-value">{{ value }}</span> <span #knobValue class="knob-value">{{ value }}</span>

10
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html

@ -66,6 +66,16 @@
{{'dashboard.delete' | translate }} {{'dashboard.delete' | translate }}
</button> </button>
</div> </div>
<div fxLayout="row">
<button mat-raised-button
ngxClipboard
(cbOnSuccess)="onDashboardIdCopied($event)"
[cbContent]="entity?.id?.id"
[fxShow]="!isEdit">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>dashboard.copyId</span>
</button>
</div>
<div class="mat-padding" fxLayout="column"> <div class="mat-padding" fxLayout="column">
<mat-form-field class="mat-block" <mat-form-field class="mat-block"
[fxShow]="!isEdit && assignedCustomersText?.length [fxShow]="!isEdit && assignedCustomersText?.length

11
ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts

@ -119,6 +119,17 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> {
})); }));
} }
onDashboardIdCopied($event) {
this.store.dispatch(new ActionNotificationShow(
{
message: this.translate.instant('dashboard.idCopiedMessage'),
type: 'success',
duration: 750,
verticalPosition: 'bottom',
horizontalPosition: 'right'
}));
}
private updateFields(entity: Dashboard): void { private updateFields(entity: Dashboard): void {
if (entity && !isEqual(entity, {})) { if (entity && !isEqual(entity, {})) {
this.assignedCustomersText = getDashboardAssignedCustomersText(entity); this.assignedCustomersText = getDashboardAssignedCustomersText(entity);

15
ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss

@ -1,15 +0,0 @@
/**
* Copyright © 2016-2022 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.
*/

64
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.html

@ -1,64 +0,0 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="changePassword" (ngSubmit)="onChangePassword()">
<mat-toolbar fxLayout="row" color="primary">
<h2 translate>profile.change-password</h2>
<span fxFlex></span>
<button mat-icon-button
[mat-dialog-close]="false"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-form-field class="mat-block">
<mat-label translate>profile.current-password</mat-label>
<input matInput type="password" formControlName="currentPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>login.new-password</mat-label>
<input matInput type="password" formControlName="newPassword"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>login.new-password-again</mat-label>
<input matInput type="password" formControlName="newPassword2"/>
<mat-icon class="material-icons" matPrefix>lock</mat-icon>
<tb-toggle-password matSuffix></tb-toggle-password>
</mat-form-field>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
[mat-dialog-close]="false" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || changePassword.invalid">
{{ 'profile.change-password' | translate }}
</button>
</div>
</form>

17
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.scss

@ -1,17 +0,0 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
}

70
ui-ngx/src/app/modules/home/pages/profile/change-password-dialog.component.ts

@ -1,70 +0,0 @@
///
/// Copyright © 2016-2022 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
@Component({
selector: 'tb-change-password-dialog',
templateUrl: './change-password-dialog.component.html',
styleUrls: ['./change-password-dialog.component.scss']
})
export class ChangePasswordDialogComponent extends DialogComponent<ChangePasswordDialogComponent> implements OnInit {
changePassword: FormGroup;
constructor(protected store: Store<AppState>,
protected router: Router,
private translate: TranslateService,
private authService: AuthService,
public dialogRef: MatDialogRef<ChangePasswordDialogComponent>,
public fb: FormBuilder) {
super(store, router, dialogRef);
}
ngOnInit(): void {
this.buildChangePasswordForm();
}
buildChangePasswordForm() {
this.changePassword = this.fb.group({
currentPassword: [''],
newPassword: [''],
newPassword2: ['']
});
}
onChangePassword(): void {
if (this.changePassword.get('newPassword').value !== this.changePassword.get('newPassword2').value) {
this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('login.passwords-mismatch-error'),
type: 'error' }));
} else {
this.authService.changePassword(
this.changePassword.get('currentPassword').value,
this.changePassword.get('newPassword').value).subscribe(() => {
this.dialogRef.close(true);
});
}
}
}

18
ui-ngx/src/app/modules/home/pages/profile/profile.component.html

@ -78,24 +78,6 @@
{{ 'dashboard.home-dashboard-hide-toolbar' | translate }} {{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
</mat-checkbox> </mat-checkbox>
</section> </section>
<div fxLayout="row" fxLayoutGap="16px" style="padding-bottom: 16px; margin-top: 20px;">
<div>
<button mat-button mat-raised-button color="primary"
type="button"
[disabled]="(isLoading$ | async)" (click)="changePassword()">
{{'profile.change-password' | translate}}
</button>
</div>
<div>
<button mat-raised-button
type="button"
(click)="copyToken()">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span>{{ 'profile.copy-jwt-token' | translate }}</span>
</button>
<div class="profile-btn-subtext">{{ expirationJwtData }}</div>
</div>
</div>
<div fxLayout="row" fxLayoutAlign="end start"> <div fxLayout="row" fxLayoutAlign="end start">
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
type="submit" type="submit"

6
ui-ngx/src/app/modules/home/pages/profile/profile.component.scss

@ -38,12 +38,6 @@
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
} }
.profile-btn-subtext {
font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif;
letter-spacing: 0.25px;
opacity: 0.6;
padding: 8px 0;
}
.tb-home-dashboard { .tb-home-dashboard {
tb-dashboard-autocomplete { tb-dashboard-autocomplete {
@media #{$mat-gt-sm} { @media #{$mat-gt-sm} {

55
ui-ngx/src/app/modules/home/pages/profile/profile.component.ts

@ -27,16 +27,9 @@ import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions';
import { environment as env } from '@env/environment'; import { environment as env } from '@env/environment';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions';
import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DatePipe } from '@angular/common';
import { ClipboardService } from 'ngx-clipboard';
@Component({ @Component({
selector: 'tb-profile', selector: 'tb-profile',
@ -51,29 +44,11 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
languageList = env.supportedLangs; languageList = env.supportedLangs;
private readonly authUser: AuthUser; private readonly authUser: AuthUser;
get jwtToken(): string {
return `Bearer ${localStorage.getItem('jwt_token')}`;
}
get jwtTokenExpiration(): string {
return localStorage.getItem('jwt_token_expiration');
}
get expirationJwtData(): string {
const expirationData = this.datePipe.transform(this.jwtTokenExpiration, 'yyyy-MM-dd HH:mm:ss');
return this.translate.instant('profile.valid-till', { expirationData });
}
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private route: ActivatedRoute, private route: ActivatedRoute,
private userService: UserService, private userService: UserService,
private authService: AuthService,
private translate: TranslateService, private translate: TranslateService,
public dialog: MatDialog, public fb: FormBuilder) {
public dialogService: DialogService,
public fb: FormBuilder,
private datePipe: DatePipe,
private clipboardService: ClipboardService) {
super(store); super(store);
this.authUser = getCurrentAuthUser(this.store); this.authUser = getCurrentAuthUser(this.store);
} }
@ -121,13 +96,6 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
); );
} }
changePassword(): void {
this.dialog.open(ChangePasswordDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
});
}
private userLoaded(user: User) { private userLoaded(user: User) {
this.user = user; this.user = user;
this.profile.reset(user); this.profile.reset(user);
@ -158,25 +126,4 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
isSysAdmin(): boolean { isSysAdmin(): boolean {
return this.authUser.authority === Authority.SYS_ADMIN; return this.authUser.authority === Authority.SYS_ADMIN;
} }
copyToken() {
if (+this.jwtTokenExpiration < Date.now()) {
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('profile.tokenCopiedWarnMessage'),
type: 'warn',
duration: 1500,
verticalPosition: 'bottom',
horizontalPosition: 'right'
}));
} else {
this.clipboardService.copyFromContent(this.jwtToken);
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('profile.tokenCopiedSuccessMessage'),
type: 'success',
duration: 750,
verticalPosition: 'bottom',
horizontalPosition: 'right'
}));
}
}
} }

4
ui-ngx/src/app/modules/home/pages/profile/profile.module.ts

@ -19,12 +19,10 @@ import { CommonModule } from '@angular/common';
import { ProfileComponent } from './profile.component'; import { ProfileComponent } from './profile.component';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { ProfileRoutingModule } from './profile-routing.module'; import { ProfileRoutingModule } from './profile-routing.module';
import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
ProfileComponent, ProfileComponent
ChangePasswordDialogComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,

115
ui-ngx/src/app/modules/home/pages/security/security.component.html

@ -17,34 +17,117 @@
--> -->
<div class="profile-container" fxLayout="column" fxLayoutGap="8px"> <div class="profile-container" fxLayout="column" fxLayoutGap="8px">
<mat-card class="profile-card" fxLayout="column"> <mat-card class="profile-card" fxLayout="column">
<mat-card-title> <mat-card-title style="margin-bottom: 8px;">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.xs="8px" <span class="mat-headline card-title" translate>profile.jwt-token</span>
fxLayoutAlign="space-between start" fxLayoutAlign.xs="start start">
<div fxFlex class="mat-headline" translate>
security.security
</div>
<div fxLayout="column">
<span class="mat-subheader" translate>profile.last-login-time</span>
<span class="profile-last-login-ts" style='opacity: 0.7;'>{{ user?.additionalInfo?.lastLoginTs | date:'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
</div>
</mat-card-title> </mat-card-title>
<mat-card-content> <mat-card-content>
<div> <div fxLayout="row" fxLayoutAlign="space-between center">
<button mat-stroked-button <div class="token-text">{{ 'profile.token-valid-till' | translate }} <span class="date">{{ jwtTokenExpiration | date: 'yyyy-MM-dd HH:mm:ss' }}</span></div>
<button mat-raised-button
color="primary" color="primary"
type="button" type="button"
(click)="copyToken()"> (click)="copyToken()">
<mat-icon class="material-icons">add_circle_outline</mat-icon>
<span>{{ 'profile.copy-jwt-token' | translate }}</span> <span>{{ 'profile.copy-jwt-token' | translate }}</span>
</button> </button>
<div class="profile-btn-subtext">{{ expirationJwtData }}</div>
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card class="profile-card" fxLayout="column">
<mat-card-content class="change-password" tb-toast toastTarget="changePassword">
<form #changePasswordForm="ngForm" [formGroup]="changePassword" (ngSubmit)="onChangePassword(changePasswordForm)">
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap="25px" fxLayoutGap.xs="0">
<div fxFlex="290px" fxFlex.sm="250px" fxFlex.xs="100">
<h3 class="card-title" translate>profile.change-password</h3>
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary">
<mat-label translate>profile.current-password</mat-label>
<input matInput type="password" name="current-password" formControlName="currentPassword" autocomplete="current-password" required/>
<tb-toggle-password [fxShow]="changePassword.get('currentPassword').dirty || changePassword.get('currentPassword').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('currentPassword').hasError('differencePassword')">
{{ 'security.password-requirement.incorrect-password-try-again' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary">
<mat-label translate>login.new-password</mat-label>
<input matInput type="password" name="new-password" formControlName="newPassword" autocomplete="new-password" required/>
<tb-toggle-password [fxShow]="changePassword.get('newPassword').dirty || changePassword.get('newPassword').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('newPassword').errors
&& !changePassword.get('newPassword').hasError('alreadyUsed')
&& !changePassword.get('newPassword').hasError('hasWhitespaces')
&& !changePassword.get('newPassword').hasError('samePassword')">
{{ 'security.password-requirement.password-not-meet-requirements' | translate }}
</mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('alreadyUsed')">
{{ changePassword.get('newPassword').getError('alreadyUsed') }}
</mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('samePassword')">
{{ 'security.password-requirement.password-should-difference' | translate }}
</mat-error>
<mat-error *ngIf="changePassword.get('newPassword').hasError('hasWhitespaces')">
{{ 'security.password-requirement.password-should-not-contain-spaces' | translate }}
</mat-error>
</mat-form-field>
<div fxFlex fxHide fxShow.xs fxLayoutAlign="start center">
<ng-container *ngTemplateOutlet="passwordRequirements"></ng-container>
</div>
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary">
<mat-label translate>login.new-password-again</mat-label>
<input matInput type="password" name="new-password" formControlName="newPassword2" autocomplete="new-password" required/>
<tb-toggle-password [fxShow]="changePassword.get('newPassword2').dirty || changePassword.get('newPassword2').touched" matSuffix></tb-toggle-password>
<mat-error *ngIf="changePassword.get('newPassword2').hasError('differencePassword')">
{{ 'security.password-requirement.new-passwords-not-match' | translate }}
</mat-error>
</mat-form-field>
</div>
<mat-divider [vertical]="true"></mat-divider>
<div fxFlex fxHide.xs fxLayoutAlign="start start">
<ng-container *ngTemplateOutlet="passwordRequirements"></ng-container>
</div>
</div>
<ng-template #passwordRequirements>
<div class="password-requirements" *ngIf="passwordPolicy">
<h3 class="card-title" translate>security.password-requirement.password-requirements</h3>
<h4 class="mat-h4" translate>security.password-requirement.at-least</h4>
<p class="mat-body" *ngIf="passwordPolicy.minimumUppercaseLetters > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('notUpperCase') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.uppercase-letter' | translate : {count: passwordPolicy.minimumUppercaseLetters} }}
</p>
<p class="mat-body" *ngIf="passwordPolicy.minimumLowercaseLetters > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('notLowerCase') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.lowercase-letter' | translate : {count: passwordPolicy.minimumLowercaseLetters} }}
</p>
<p class="mat-body" *ngIf="passwordPolicy.minimumDigits > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('notNumeric') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.digit' | translate : {count: passwordPolicy.minimumDigits} }}
</p>
<p class="mat-body" *ngIf="passwordPolicy.minimumSpecialCharacters > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('notSpecial') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.special-character' | translate : {count: passwordPolicy.minimumSpecialCharacters} }}
</p>
<p class="mat-body" *ngIf="passwordPolicy.minimumLength > 0">
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('minLength') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
{{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }}
</p>
</div>
</ng-template>
<div fxLayout="row" fxLayoutGap="8px" style="margin-top: 18px;" [fxShow]="changePassword.dirty || changePassword.touched">
<button mat-button color="primary"
type="button"
(click)="discardChanges(changePasswordForm, $event)"
[disabled]="(isLoading$ | async)">
{{ 'action.discard-changes' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async)">
{{ 'profile.change-password' | translate }}
</button>
</div>
</form>
</mat-card-content>
</mat-card>
<mat-card class="profile-card" *ngIf="allowTwoFactorProviders.length"> <mat-card class="profile-card" *ngIf="allowTwoFactorProviders.length">
<mat-card-title style="margin-bottom: 20px;"> <mat-card-title style="margin-bottom: 20px;">
<span class="mat-headline" translate>admin.2fa.2fa</span> <span class="mat-headline card-title" translate>admin.2fa.2fa</span>
</mat-card-title> </mat-card-title>
<mat-card-subtitle style="margin-bottom: 40px;"> <mat-card-subtitle style="margin-bottom: 40px;">
<div class="mat-body-1 description" translate>security.2fa.2fa-description</div> <div class="mat-body-1 description" translate>security.2fa.2fa-description</div>

59
ui-ngx/src/app/modules/home/pages/security/security.component.scss

@ -21,33 +21,65 @@
} }
mat-card.profile-card { mat-card.profile-card {
padding: 24px;
@media #{$mat-gt-sm} { @media #{$mat-gt-sm} {
width: 70%; width: 80%;
} }
@media #{$mat-gt-md} { @media #{$mat-gt-md} {
width: 50%; width: 55%;
} }
@media #{$mat-gt-xl} { @media #{$mat-gt-xl} {
width: 45%; width: 45%;
} }
.mat-subheader { .card-title {
line-height: 24px; font: 500 18px / 24px Roboto, "Helvetica Neue", sans-serif;
color: rgba(0, 0, 0, 0.54); letter-spacing: 0.15px;
margin-top: 0;
}
.mat-h4 {
font-weight: 500;
font-size: 14px; font-size: 14px;
font-weight: 400; letter-spacing: 0.25px;
margin: 0 0 4px;
}
.change-password {
margin: 0;
.mat-divider.mat-divider-vertical {
margin-bottom: 25px;
}
.mat-form-field {
margin-bottom: 4px;
}
.password-requirements > p {
margin: 0 0 8px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.mat-icon[data-mat-icon-name="check"] {
color: #24A148;
}
} }
.profile-last-login-ts { .auth-title {
font-size: 16px; font-weight: 500;
font-weight: 400; margin: 0;
} }
.profile-btn-subtext { .token-text {
font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif; font: 400 14px / 16px Roboto, "Helvetica Neue", sans-serif;
letter-spacing: 0.25px; letter-spacing: 0.25px;
opacity: 0.6;
padding: 8px 0; padding: 8px 0;
> .date {
opacity: .7;
}
} }
} }
@ -91,3 +123,8 @@
} }
} }
} }
:host ::ng-deep {
.mat-form-field-appearance-fill .mat-form-field-underline::before {
background-color: transparent;
}
}

141
ui-ngx/src/app/modules/home/pages/security/security.component.ts

@ -19,7 +19,15 @@ import { User } from '@shared/models/user.model';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup } from '@angular/forms'; import {
AbstractControl,
FormBuilder,
FormGroup, FormGroupDirective,
NgForm,
ValidationErrors,
ValidatorFn,
Validators
} from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
@ -29,6 +37,7 @@ import { DatePipe } from '@angular/common';
import { ClipboardService } from 'ngx-clipboard'; import { ClipboardService } from 'ngx-clipboard';
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
import { import {
AccountTwoFaSettingProviders,
AccountTwoFaSettings, AccountTwoFaSettings,
BackupCodeTwoFactorAuthAccountConfig, BackupCodeTwoFactorAuthAccountConfig,
EmailTwoFactorAuthAccountConfig, EmailTwoFactorAuthAccountConfig,
@ -39,7 +48,9 @@ import {
import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map'; import { authenticationDialogMap } from '@home/pages/security/authentication-dialog/authentication-dialog.map';
import { takeUntil, tap } from 'rxjs/operators'; import { takeUntil, tap } from 'rxjs/operators';
import { Observable, of, Subject } from 'rxjs'; import { Observable, of, Subject } from 'rxjs';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull, isEqual } from '@core/utils';
import { AuthService } from '@core/auth/auth.service';
import { UserPasswordPolicy } from '@shared/models/settings.models';
@Component({ @Component({
selector: 'tb-security', selector: 'tb-security',
@ -49,10 +60,14 @@ import { isDefinedAndNotNull } from '@core/utils';
export class SecurityComponent extends PageComponent implements OnInit, OnDestroy { export class SecurityComponent extends PageComponent implements OnInit, OnDestroy {
private readonly destroy$ = new Subject<void>(); private readonly destroy$ = new Subject<void>();
private accountConfig: AccountTwoFaSettings; private accountConfig: AccountTwoFaSettingProviders;
twoFactorAuth: FormGroup; twoFactorAuth: FormGroup;
changePassword: FormGroup;
user: User; user: User;
passwordPolicy: UserPasswordPolicy;
allowTwoFactorProviders: TwoFactorAuthProviderType[] = []; allowTwoFactorProviders: TwoFactorAuthProviderType[] = [];
providersData = twoFactorAuthProvidersData; providersData = twoFactorAuthProvidersData;
twoFactorAuthProviderType = TwoFactorAuthProviderType; twoFactorAuthProviderType = TwoFactorAuthProviderType;
@ -80,6 +95,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
public dialogService: DialogService, public dialogService: DialogService,
public fb: FormBuilder, public fb: FormBuilder,
private datePipe: DatePipe, private datePipe: DatePipe,
private authService: AuthService,
private clipboardService: ClipboardService) { private clipboardService: ClipboardService) {
super(store); super(store);
} }
@ -88,6 +104,8 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
this.buildTwoFactorForm(); this.buildTwoFactorForm();
this.user = this.route.snapshot.data.user; this.user = this.route.snapshot.data.user;
this.twoFactorLoad(this.route.snapshot.data.providers); this.twoFactorLoad(this.route.snapshot.data.providers);
this.buildChangePasswordForm();
this.loadPasswordPolicy();
} }
ngOnDestroy() { ngOnDestroy() {
@ -128,12 +146,11 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
} }
private processTwoFactorAuthConfig(setting: AccountTwoFaSettings) { private processTwoFactorAuthConfig(setting: AccountTwoFaSettings) {
this.accountConfig = setting; this.accountConfig = setting?.configs || {};
const configs = this.accountConfig.configs;
Object.values(TwoFactorAuthProviderType).forEach(provider => { Object.values(TwoFactorAuthProviderType).forEach(provider => {
if (configs[provider]) { if (this.accountConfig[provider]) {
this.twoFactorAuth.get(provider).setValue(true); this.twoFactorAuth.get(provider).setValue(true);
if (configs[provider].useByDefault) { if (this.accountConfig[provider].useByDefault) {
this.useByDefault = provider; this.useByDefault = provider;
} }
} else { } else {
@ -142,6 +159,75 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
}); });
} }
private buildChangePasswordForm() {
this.changePassword = this.fb.group({
currentPassword: [''],
newPassword: ['', Validators.required],
newPassword2: ['', this.samePasswordValidation(false, 'newPassword')]
});
}
private loadPasswordPolicy() {
this.authService.getUserPasswordPolicy().subscribe(policy => {
this.passwordPolicy = policy;
this.changePassword.get('newPassword').setValidators([
this.passwordStrengthValidator(),
this.samePasswordValidation(true, 'currentPassword'),
Validators.required
]);
this.changePassword.get('newPassword').updateValueAndValidity({emitEvent: false});
});
}
private passwordStrengthValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value: string = control.value;
const errors: any = {};
if (this.passwordPolicy.minimumUppercaseLetters > 0 &&
!new RegExp(`(?:.*?[A-Z]){${this.passwordPolicy.minimumUppercaseLetters}}`).test(value)) {
errors.notUpperCase = true;
}
if (this.passwordPolicy.minimumLowercaseLetters > 0 &&
!new RegExp(`(?:.*?[a-z]){${this.passwordPolicy.minimumLowercaseLetters}}`).test(value)) {
errors.notLowerCase = true;
}
if (this.passwordPolicy.minimumDigits > 0
&& !new RegExp(`(?:.*?\\d){${this.passwordPolicy.minimumDigits}}`).test(value)) {
errors.notNumeric = true;
}
if (this.passwordPolicy.minimumSpecialCharacters > 0 &&
!new RegExp(`(?:.*?[\\W_]){${this.passwordPolicy.minimumSpecialCharacters}}`).test(value)) {
errors.notSpecial = true;
}
if (!this.passwordPolicy.allowWhitespaces && /\s/.test(value)) {
errors.hasWhitespaces = true;
}
if (this.passwordPolicy.minimumLength > 0 && value.length < this.passwordPolicy.minimumLength) {
errors.minLength = true;
}
return isEqual(errors, {}) ? null : errors;
};
}
private samePasswordValidation(isSame: boolean, key: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value: string = control.value;
const keyValue = control.parent?.value[key];
if (isSame) {
return value === keyValue ? {samePassword: true} : null;
}
return value !== keyValue ? {differencePassword: true} : null;
};
}
trackByProvider(i: number, provider: TwoFactorAuthProviderType) { trackByProvider(i: number, provider: TwoFactorAuthProviderType) {
return provider; return provider;
} }
@ -216,7 +302,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
} }
generateNewBackupCode() { generateNewBackupCode() {
const codeLeft = this.accountConfig.configs[TwoFactorAuthProviderType.BACKUP_CODE].codesLeft; const codeLeft = (this.accountConfig[TwoFactorAuthProviderType.BACKUP_CODE] as BackupCodeTwoFactorAuthAccountConfig).codesLeft;
let subscription: Observable<boolean>; let subscription: Observable<boolean>;
if (codeLeft) { if (codeLeft) {
subscription = this.dialogService.confirm( subscription = this.dialogService.confirm(
@ -240,7 +326,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
providerDataInfo(provider: TwoFactorAuthProviderType) { providerDataInfo(provider: TwoFactorAuthProviderType) {
const info = {info: null}; const info = {info: null};
const providerConfig = this.accountConfig.configs[provider]; const providerConfig = this.accountConfig[provider];
if (isDefinedAndNotNull(providerConfig)) { if (isDefinedAndNotNull(providerConfig)) {
switch (provider) { switch (provider) {
case TwoFactorAuthProviderType.EMAIL: case TwoFactorAuthProviderType.EMAIL:
@ -256,4 +342,41 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
} }
return info; return info;
} }
onChangePassword(form: FormGroupDirective): void {
if (this.changePassword.valid) {
this.authService.changePassword(this.changePassword.get('currentPassword').value,
this.changePassword.get('newPassword').value, {ignoreErrors: true}).subscribe(() => {
this.discardChanges(form);
},
(error) => {
if (error.status === 400 && error.error.message === 'Current password doesn\'t match!') {
this.changePassword.get('currentPassword').setErrors({differencePassword: true});
} else if (error.status === 400 && error.error.message.startsWith('Password must')) {
this.loadPasswordPolicy();
} else if (error.status === 400 && error.error.message.startsWith('Password was already used')) {
this.changePassword.get('newPassword').setErrors({alreadyUsed: error.error.message});
} else {
this.store.dispatch(new ActionNotificationShow({
message: error.error.message,
type: 'error',
target: 'changePassword'
}));
}
});
} else {
this.changePassword.markAllAsTouched();
}
}
discardChanges(form: FormGroupDirective, event?: MouseEvent) {
if (event) {
event.stopPropagation();
}
form.resetForm({
currentPassword: '',
newPassword: '',
newPassword2: ''
});
}
} }

8
ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts

@ -29,6 +29,7 @@ import {
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { interval, Subscription } from 'rxjs'; import { interval, Subscription } from 'rxjs';
import { isEqual } from '@core/utils'; import { isEqual } from '@core/utils';
import { ActionNotificationShow } from '@core/notification/notification.actions';
@Component({ @Component({
selector: 'tb-two-factor-auth-login', selector: 'tb-two-factor-auth-login',
@ -118,6 +119,13 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit
} }
this.verificationForm.get('verificationCode').setErrors(errors); this.verificationForm.get('verificationCode').setErrors(errors);
}, 5000); }, 5000);
} else {
this.store.dispatch(new ActionNotificationShow({
message: error.error.message,
type: 'error',
verticalPosition: 'top',
horizontalPosition: 'left'
}));
} }
} }
); );

4
ui-ngx/src/app/shared/models/device.models.ts

@ -480,6 +480,10 @@ export const AlarmScheduleTypeTranslationMap = new Map<AlarmScheduleType, string
); );
export interface AlarmSchedule{ export interface AlarmSchedule{
dynamicValue?: {
sourceAttribute: string,
sourceType: string;
};
type: AlarmScheduleType; type: AlarmScheduleType;
timezone?: string; timezone?: string;
daysOfWeek?: number[]; daysOfWeek?: number[];

7
ui-ngx/src/app/shared/models/two-factor-auth.models.ts

@ -88,11 +88,14 @@ export interface BackupCodeTwoFactorAuthAccountConfig extends GeneralTwoFactorAu
export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwoFactorAuthAccountConfig | export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwoFactorAuthAccountConfig |
EmailTwoFactorAuthAccountConfig | BackupCodeTwoFactorAuthAccountConfig; EmailTwoFactorAuthAccountConfig | BackupCodeTwoFactorAuthAccountConfig;
export interface AccountTwoFaSettings { export interface AccountTwoFaSettings {
configs: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig}; configs: AccountTwoFaSettingProviders;
} }
export type AccountTwoFaSettingProviders = {
[key in TwoFactorAuthProviderType]?: TwoFactorAuthAccountConfig;
};
export interface TwoFaProviderInfo { export interface TwoFaProviderInfo {
type: TwoFactorAuthProviderType; type: TwoFactorAuthProviderType;
default: boolean; default: boolean;

79
ui-ngx/src/assets/help/en_US/device-profile/alarm_custom_schedule_format.md

@ -0,0 +1,79 @@
#### Custom schedule format
An attribute with a dynamic value for a custom schedule format must have JSON in the following format:
```javascript
{
"timezone": "Europe/Kiev",
"items": [
{
"dayOfWeek": 1,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 2,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 3,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 4,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 5,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 6,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 7,
"enabled": true,
"endsOn": 0,
"startsOn": 0
}
]
}
```
<ul>
<li>
<b>timezone:</b> this value is used to designate the timezone you are using.
</li>
<li>
<b>items:</b> the array of values representing the days on which the schedule will be active.
</li>
</ul>
One array item contains such fields:
<ul>
<li>
<b>dayOfWeek:</b> this value is used to designate the specified day in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active.
</li>
<li>
<b>enabled:</b> this <code>boolean</code> value, used to designate that the specified day in the schedule will be enabled.
</li>
<li>
<b>startsOn:</b> this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated day.
</li>
<li>
<b>endsOn:</b> this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified day.
</li>
</ul>
When <b>startsOn</b> and <b>endsOn</b> equals 0 it's means that the schedule will be active the whole day.

31
ui-ngx/src/assets/help/en_US/device-profile/alarm_specific_schedule_format.md

@ -0,0 +1,31 @@
#### Specific schedule format
An attribute with a dynamic value for a specific schedule format must have JSON in the following format:
```javascript
{
"daysOfWeek": [
2,
4
],
"endsOn": 0,
"startsOn": 0,
"timezone": "Europe/Kiev"
}
```
<ul>
<li>
<b>timezone:</b> this value is used to designate the timezone you are using.
</li>
<li>
<b>daysOfWeek:</b> this value is used to designate the days in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active.
</li>
<li>
<b>startsOn:</b> this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated days.
</li>
<li>
<b>endsOn:</b> this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified days.
</li>
</ul>
When <b>startsOn</b> and <b>endsOn</b> equals 0 it's means that the schedule will be active the whole day.

24
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -963,6 +963,8 @@
"assignedToCustomer": "Assigned to customer", "assignedToCustomer": "Assigned to customer",
"assignedToCustomers": "Assigned to customers", "assignedToCustomers": "Assigned to customers",
"public": "Public", "public": "Public",
"copyId": "Copy dashboard id",
"idCopiedMessage": "Dashboard Id has been copied to clipboard",
"public-link": "Public link", "public-link": "Public link",
"copy-public-link": "Copy public link", "copy-public-link": "Copy public link",
"public-link-copied-message": "Dashboard public link has been copied to clipboard", "public-link-copied-message": "Dashboard public link has been copied to clipboard",
@ -2275,6 +2277,7 @@
"current-device": "Current device", "current-device": "Current device",
"default-value": "Default value", "default-value": "Default value",
"dynamic-source-type": "Dynamic source type", "dynamic-source-type": "Dynamic source type",
"dynamic-value": "Dynamic value",
"no-dynamic-value": "No dynamic value", "no-dynamic-value": "No dynamic value",
"source-attribute": "Source attribute", "source-attribute": "Source attribute",
"switch-to-dynamic-value": "Switch to dynamic value", "switch-to-dynamic-value": "Switch to dynamic value",
@ -2527,7 +2530,7 @@
"password-reset": "Password reset", "password-reset": "Password reset",
"expired-password-reset-message": "Your credentials has been expired! Please create new password.", "expired-password-reset-message": "Your credentials has been expired! Please create new password.",
"new-password": "New password", "new-password": "New password",
"new-password-again": "New password again", "new-password-again": "Confirm new password",
"password-link-sent-message": "Reset link has been sent", "password-link-sent-message": "Reset link has been sent",
"email": "Email", "email": "Email",
"login-with": "Login with {{name}}", "login-with": "Login with {{name}}",
@ -2630,12 +2633,13 @@
"change-password": "Change Password", "change-password": "Change Password",
"current-password": "Current password", "current-password": "Current password",
"copy-jwt-token": "Copy JWT token", "copy-jwt-token": "Copy JWT token",
"valid-till": "Valid till {{expirationData}}", "jwt-token": "JWT token",
"token-valid-till": "Token is valid till",
"tokenCopiedSuccessMessage": "JWT token has been copied to clipboard", "tokenCopiedSuccessMessage": "JWT token has been copied to clipboard",
"tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page." "tokenCopiedWarnMessage": "JWT token is expired! Please, refresh the page."
}, },
"security": { "security": {
"security": "Security", "security": "Password and authentication",
"2fa": { "2fa": {
"2fa": "Two-factor authentication", "2fa": "Two-factor authentication",
"2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.", "2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.",
@ -2689,6 +2693,20 @@
"backup-code-description": "These printable one-time passcodes allow you to sign in when away from your phone, like when you’re traveling.", "backup-code-description": "These printable one-time passcodes allow you to sign in when away from your phone, like when you’re traveling.",
"backup-code-hint": "{{ info }} single-use codes are active at this time" "backup-code-hint": "{{ info }} single-use codes are active at this time"
} }
},
"password-requirement": {
"at-least": "At least:",
"character": "{ count, plural, 1 {1 character} other {# characters} }",
"digit": "{ count, plural, 1 {1 digit} other {# digits} }",
"incorrect-password-try-again": "Incorrect password. Try again",
"lowercase-letter": "{ count, plural, 1 {1 lowercase letter} other {# lowercase letters} }",
"new-passwords-not-match": "New password didn't match",
"password-should-not-contain-spaces": "Your password should not contain spaces",
"password-not-meet-requirements": "Password didn't meet requirements",
"password-requirements": "Password requirements",
"password-should-difference": "New password should be different from current",
"special-character": "{ count, plural, 1 {1 special character} other {# special characters} }",
"uppercase-letter": "{ count, plural, 1 {1 uppercase letter} other {# uppercase letters} }"
} }
}, },
"relation": { "relation": {

3
ui-ngx/src/assets/locale/locale.constant-ru_RU.json

@ -483,6 +483,8 @@
"add-widget": "Добавить новый виджет", "add-widget": "Добавить новый виджет",
"title": "Название", "title": "Название",
"select-widget-title": "Выберите виджет", "select-widget-title": "Выберите виджет",
"copyId": "Копировать идентификатор дашборда",
"idCopiedMessage": "Идентификатор дашборда скопирован в буфер обмена",
"select-widget-subtitle": "Список доступных виджетов", "select-widget-subtitle": "Список доступных виджетов",
"delete": "Удалить дашборд", "delete": "Удалить дашборд",
"title-required": "Название обязательно.", "title-required": "Название обязательно.",
@ -1289,7 +1291,6 @@
"change-password": "Изменить пароль", "change-password": "Изменить пароль",
"current-password": "Текущий пароль", "current-password": "Текущий пароль",
"copy-jwt-token": "Копировать JWT токен", "copy-jwt-token": "Копировать JWT токен",
"valid-till": "Действителен до {{expirationData}}",
"tokenCopiedMessage": "JWT токен скопирован в буфер обмена", "tokenCopiedMessage": "JWT токен скопирован в буфер обмена",
"tokenCopiedWarnMessage": "JWT токен недействителен! Перезагрузите страницу." "tokenCopiedWarnMessage": "JWT токен недействителен! Перезагрузите страницу."
}, },

3
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

@ -594,6 +594,8 @@
"add-widget": "Додати новий віджет", "add-widget": "Додати новий віджет",
"title": "Назва", "title": "Назва",
"select-widget-title": "Вибрати віджет", "select-widget-title": "Вибрати віджет",
"copyId": "Копіювати ідентифікатор панелі приладів",
"idCopiedMessage": "Ідентифікатор панелі приладів скопійовано в буфер обміну",
"select-widget-subtitle": "Список доступних типів віджетів", "select-widget-subtitle": "Список доступних типів віджетів",
"delete": "Видалити панель приладів", "delete": "Видалити панель приладів",
"title-required": "Необхідно задати назву.", "title-required": "Необхідно задати назву.",
@ -1704,7 +1706,6 @@
"change-password": "Змінити пароль", "change-password": "Змінити пароль",
"current-password": "Поточний пароль", "current-password": "Поточний пароль",
"copy-jwt-token": "Копіювати JWT токен", "copy-jwt-token": "Копіювати JWT токен",
"valid-till": "Дійсний до {{expirationData}}",
"tokenCopiedMessage": "JWT токен скопійовано в буфер обміну", "tokenCopiedMessage": "JWT токен скопійовано в буфер обміну",
"tokenCopiedWarnMessage": "JWT токен не є дійсним! Перезавантажте сторінку." "tokenCopiedWarnMessage": "JWT токен не є дійсним! Перезавантажте сторінку."
}, },

1
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -2184,7 +2184,6 @@
"last-login-time": "最后登录", "last-login-time": "最后登录",
"profile": "属性", "profile": "属性",
"copy-jwt-token": "复制 JWT 令牌", "copy-jwt-token": "复制 JWT 令牌",
"valid-till": "有效期至 {{expirationData}}",
"tokenCopiedSuccessMessage": "JWT 令牌已复制到剪贴板", "tokenCopiedSuccessMessage": "JWT 令牌已复制到剪贴板",
"tokenCopiedWarnMessage": "JWT 令牌已过期!请刷新页面。" "tokenCopiedWarnMessage": "JWT 令牌已过期!请刷新页面。"
}, },

7
ui-ngx/src/theme.scss

@ -221,6 +221,7 @@ $tb-dark-theme: get-tb-dark-theme(
@mixin tb-components-theme($theme) { @mixin tb-components-theme($theme) {
$primary: map-get($theme, primary); $primary: map-get($theme, primary);
$warn: map-get($theme, warn);
mat-toolbar{ mat-toolbar{
&.mat-hue-3 { &.mat-hue-3 {
@ -233,6 +234,12 @@ $tb-dark-theme: get-tb-dark-theme(
div.tb-dashboard-page.mobile-app { div.tb-dashboard-page.mobile-app {
@include mat-fab-toolbar-inverse-theme($tb-theme); @include mat-fab-toolbar-inverse-theme($tb-theme);
} }
.same-color.mat-form-field-invalid {
.mat-form-field-suffix {
color: mat.get-color-from-palette($warn, text);
}
}
} }
.tb-default { .tb-default {

Loading…
Cancel
Save