44 changed files with 973 additions and 60 deletions
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
package org.thingsboard.server.exception; |
|||
|
|||
import org.springframework.http.HttpStatus; |
|||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; |
|||
|
|||
public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse { |
|||
|
|||
private final String resetToken; |
|||
|
|||
protected ThingsboardCredentialsExpiredResponse(String message, String resetToken) { |
|||
super(message, ThingsboardErrorCode.CREDENTIALS_EXPIRED, HttpStatus.UNAUTHORIZED); |
|||
this.resetToken = resetToken; |
|||
} |
|||
|
|||
public static ThingsboardCredentialsExpiredResponse of(final String message, final String resetToken) { |
|||
return new ThingsboardCredentialsExpiredResponse(message, resetToken); |
|||
} |
|||
|
|||
public String getResetToken() { |
|||
return resetToken; |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
|
|||
package org.thingsboard.server.service.security.auth.rest; |
|||
|
|||
import lombok.Data; |
|||
import ua_parser.Client; |
|||
import ua_parser.Parser; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
import java.io.IOException; |
|||
import java.io.Serializable; |
|||
|
|||
@Data |
|||
public class RestAuthenticationDetails implements Serializable { |
|||
|
|||
private final String clientAddress; |
|||
private final Client userAgent; |
|||
|
|||
public RestAuthenticationDetails(HttpServletRequest request) { |
|||
this.clientAddress = getClientIP(request); |
|||
this.userAgent = getUserAgent(request); |
|||
} |
|||
|
|||
private static String getClientIP(HttpServletRequest request) { |
|||
String xfHeader = request.getHeader("X-Forwarded-For"); |
|||
if (xfHeader == null) { |
|||
return request.getRemoteAddr(); |
|||
} |
|||
return xfHeader.split(",")[0]; |
|||
} |
|||
|
|||
private static Client getUserAgent(HttpServletRequest request) { |
|||
try { |
|||
Parser uaParser = new Parser(); |
|||
return uaParser.parse(request.getHeader("User-Agent")); |
|||
} catch (IOException e) { |
|||
return new Client(null, null, null); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
|
|||
package org.thingsboard.server.service.security.auth.rest; |
|||
|
|||
import org.springframework.security.authentication.AuthenticationDetailsSource; |
|||
|
|||
import javax.servlet.http.HttpServletRequest; |
|||
|
|||
public class RestAuthenticationDetailsSource implements |
|||
AuthenticationDetailsSource<HttpServletRequest, RestAuthenticationDetails> { |
|||
|
|||
public RestAuthenticationDetails buildDetails(HttpServletRequest context) { |
|||
return new RestAuthenticationDetails(context); |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
package org.thingsboard.server.service.security.exception; |
|||
|
|||
import org.springframework.security.authentication.CredentialsExpiredException; |
|||
|
|||
public class UserPasswordExpiredException extends CredentialsExpiredException { |
|||
|
|||
private final String resetToken; |
|||
|
|||
public UserPasswordExpiredException(String msg, String resetToken) { |
|||
super(msg); |
|||
this.resetToken = resetToken; |
|||
} |
|||
|
|||
public String getResetToken() { |
|||
return resetToken; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
package org.thingsboard.server.service.security.model; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class SecuritySettings { |
|||
|
|||
private UserPasswordPolicy passwordPolicy; |
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
package org.thingsboard.server.service.security.model; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class UserPasswordPolicy { |
|||
|
|||
private Integer minimumLength; |
|||
private Integer minimumUppercaseLetters; |
|||
private Integer minimumLowercaseLetters; |
|||
private Integer minimumDigits; |
|||
private Integer minimumSpecialCharacters; |
|||
|
|||
private Integer passwordExpirationPeriodDays; |
|||
|
|||
} |
|||
@ -0,0 +1,155 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
package org.thingsboard.server.service.security.system; |
|||
|
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.passay.*; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.cache.annotation.CacheEvict; |
|||
import org.springframework.cache.annotation.Cacheable; |
|||
import org.springframework.security.authentication.BadCredentialsException; |
|||
import org.springframework.security.authentication.CredentialsExpiredException; |
|||
import org.springframework.security.authentication.DisabledException; |
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.AdminSettings; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.security.UserCredentials; |
|||
import org.thingsboard.server.dao.exception.DataValidationException; |
|||
import org.thingsboard.server.dao.settings.AdminSettingsService; |
|||
import org.thingsboard.server.dao.user.UserService; |
|||
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; |
|||
import org.thingsboard.server.service.security.model.SecuritySettings; |
|||
import org.thingsboard.server.service.security.model.UserPasswordPolicy; |
|||
|
|||
import javax.annotation.Resource; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE; |
|||
import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTINGS_CACHE; |
|||
|
|||
@Service |
|||
@Slf4j |
|||
public class DefaultSystemSecurityService implements SystemSecurityService { |
|||
|
|||
private static final ObjectMapper objectMapper = new ObjectMapper(); |
|||
|
|||
@Autowired |
|||
private AdminSettingsService adminSettingsService; |
|||
|
|||
@Autowired |
|||
private BCryptPasswordEncoder encoder; |
|||
|
|||
@Autowired |
|||
private UserService userService; |
|||
|
|||
@Resource |
|||
private SystemSecurityService self; |
|||
|
|||
@Cacheable(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'") |
|||
@Override |
|||
public SecuritySettings getSecuritySettings(TenantId tenantId) { |
|||
SecuritySettings securitySettings = null; |
|||
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, "securitySettings"); |
|||
if (adminSettings != null) { |
|||
try { |
|||
securitySettings = objectMapper.treeToValue(adminSettings.getJsonValue(), SecuritySettings.class); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException("Failed to load security settings!", e); |
|||
} |
|||
} else { |
|||
securitySettings = new SecuritySettings(); |
|||
securitySettings.setPasswordPolicy(new UserPasswordPolicy()); |
|||
securitySettings.getPasswordPolicy().setMinimumLength(6); |
|||
} |
|||
return securitySettings; |
|||
} |
|||
|
|||
@CacheEvict(cacheNames = SECURITY_SETTINGS_CACHE, key = "'securitySettings'") |
|||
@Override |
|||
public SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings) { |
|||
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(tenantId, "securitySettings"); |
|||
if (adminSettings == null) { |
|||
adminSettings = new AdminSettings(); |
|||
adminSettings.setKey("securitySettings"); |
|||
} |
|||
adminSettings.setJsonValue(objectMapper.valueToTree(securitySettings)); |
|||
AdminSettings savedAdminSettings = adminSettingsService.saveAdminSettings(tenantId, adminSettings); |
|||
try { |
|||
return objectMapper.treeToValue(savedAdminSettings.getJsonValue(), SecuritySettings.class); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException("Failed to load security settings!", e); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String password) throws AuthenticationException { |
|||
|
|||
if (!encoder.matches(password, userCredentials.getPassword())) { |
|||
throw new BadCredentialsException("Authentication Failed. Username or Password not valid."); |
|||
} |
|||
|
|||
if (!userCredentials.isEnabled()) { |
|||
throw new DisabledException("User is not active"); |
|||
} |
|||
|
|||
SecuritySettings securitySettings = self.getSecuritySettings(tenantId); |
|||
if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) { |
|||
if ((userCredentials.getCreatedTime() |
|||
+ TimeUnit.DAYS.toMillis(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) |
|||
< System.currentTimeMillis()) { |
|||
userCredentials = userService.requestExpiredPasswordReset(tenantId, userCredentials.getId()); |
|||
throw new UserPasswordExpiredException("User password expired!", userCredentials.getResetToken()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void validatePassword(TenantId tenantId, String password) throws DataValidationException { |
|||
SecuritySettings securitySettings = self.getSecuritySettings(tenantId); |
|||
UserPasswordPolicy passwordPolicy = securitySettings.getPasswordPolicy(); |
|||
|
|||
List<Rule> passwordRules = new ArrayList<>(); |
|||
passwordRules.add(new LengthRule(passwordPolicy.getMinimumLength(), Integer.MAX_VALUE)); |
|||
if (isPositiveInteger(passwordPolicy.getMinimumUppercaseLetters())) { |
|||
passwordRules.add(new CharacterRule(EnglishCharacterData.UpperCase, passwordPolicy.getMinimumUppercaseLetters())); |
|||
} |
|||
if (isPositiveInteger(passwordPolicy.getMinimumLowercaseLetters())) { |
|||
passwordRules.add(new CharacterRule(EnglishCharacterData.LowerCase, passwordPolicy.getMinimumLowercaseLetters())); |
|||
} |
|||
if (isPositiveInteger(passwordPolicy.getMinimumDigits())) { |
|||
passwordRules.add(new CharacterRule(EnglishCharacterData.Digit, passwordPolicy.getMinimumDigits())); |
|||
} |
|||
if (isPositiveInteger(passwordPolicy.getMinimumSpecialCharacters())) { |
|||
passwordRules.add(new CharacterRule(EnglishCharacterData.Special, passwordPolicy.getMinimumSpecialCharacters())); |
|||
} |
|||
PasswordValidator validator = new PasswordValidator(passwordRules); |
|||
PasswordData passwordData = new PasswordData(password); |
|||
RuleResult result = validator.validate(passwordData); |
|||
if (!result.isValid()) { |
|||
String message = String.join("\n", validator.getMessages(result)); |
|||
throw new DataValidationException(message); |
|||
} |
|||
} |
|||
|
|||
private static boolean isPositiveInteger(Integer val) { |
|||
return val != null && val.intValue() > 0; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
/** |
|||
* Copyright © 2016-2019 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. |
|||
*/ |
|||
package org.thingsboard.server.service.security.system; |
|||
|
|||
import org.springframework.security.core.AuthenticationException; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.security.UserCredentials; |
|||
import org.thingsboard.server.dao.exception.DataValidationException; |
|||
import org.thingsboard.server.service.security.model.SecuritySettings; |
|||
|
|||
public interface SystemSecurityService { |
|||
|
|||
SecuritySettings getSecuritySettings(TenantId tenantId); |
|||
|
|||
SecuritySettings saveSecuritySettings(TenantId tenantId, SecuritySettings securitySettings); |
|||
|
|||
void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String password) throws AuthenticationException; |
|||
|
|||
void validatePassword(TenantId tenantId, String password) throws DataValidationException; |
|||
|
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/* |
|||
* Copyright © 2016-2019 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 './settings-card.scss'; |
|||
|
|||
/*@ngInject*/ |
|||
export default function SecuritySettingsController(adminService, $mdExpansionPanel) { |
|||
|
|||
var vm = this; |
|||
vm.$mdExpansionPanel = $mdExpansionPanel; |
|||
|
|||
vm.save = save; |
|||
|
|||
loadSettings(); |
|||
|
|||
function loadSettings() { |
|||
adminService.getSecuritySettings().then(function success(securitySettings) { |
|||
vm.securitySettings = securitySettings; |
|||
}); |
|||
} |
|||
|
|||
function save() { |
|||
adminService.saveSecuritySettings(vm.securitySettings).then(function success(securitySettings) { |
|||
vm.securitySettings = securitySettings; |
|||
vm.settingsForm.$setPristine(); |
|||
}); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,125 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2019 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. |
|||
|
|||
--> |
|||
<div tb-help="'securitySettings'" help-container-id="help-container"> |
|||
<md-card class="settings-card"> |
|||
<md-card-title> |
|||
<md-card-title-text layout="row"> |
|||
<span translate class="md-headline">admin.security-settings</span> |
|||
<span flex></span> |
|||
<div id="help-container"></div> |
|||
</md-card-title-text> |
|||
</md-card-title> |
|||
<md-progress-linear md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear> |
|||
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span> |
|||
<md-card-content> |
|||
<form name="vm.settingsForm" ng-submit="vm.save()" tb-confirm-on-exit confirm-form="vm.settingsForm"> |
|||
<fieldset ng-disabled="$root.loading"> |
|||
<md-expansion-panel-group md-component-id="securitySettingsPanelGroup" auto-expand="true" multiple> |
|||
<md-expansion-panel md-component-id="passwordPolicyPanel" id="passwordPolicyPanel"> |
|||
<md-expansion-panel-collapsed> |
|||
<div class="tb-panel-title" translate>admin.password-policy</div> |
|||
<md-expansion-panel-icon></md-expansion-panel-icon> |
|||
</md-expansion-panel-collapsed> |
|||
<md-expansion-panel-expanded> |
|||
<md-expansion-panel-header ng-click="vm.$mdExpansionPanel('passwordPolicyPanel').collapse()"> |
|||
<div class="tb-panel-title" translate>admin.password-policy</div> |
|||
<md-expansion-panel-icon></md-expansion-panel-icon> |
|||
</md-expansion-panel-header> |
|||
<md-expansion-panel-content> |
|||
<md-input-container class="md-block"> |
|||
<label translate>admin.minimum-password-length</label> |
|||
<input type="number" |
|||
step="1" |
|||
min="5" |
|||
max="50" |
|||
required |
|||
name="minimumPasswordLength" |
|||
ng-model="vm.securitySettings.passwordPolicy.minimumLength"> |
|||
<div ng-messages="vm.settingsForm.minimumPasswordLength.$error"> |
|||
<div translate ng-message="required">admin.minimum-password-length-required</div> |
|||
<div translate ng-message="min">admin.minimum-password-length-range</div> |
|||
<div translate ng-message="max">admin.minimum-password-length-range</div> |
|||
</div> |
|||
</md-input-container> |
|||
<md-input-container class="md-block"> |
|||
<label translate>admin.minimum-uppercase-letters</label> |
|||
<input type="number" |
|||
step="1" |
|||
min="0" |
|||
name="minimumUppercaseLetters" |
|||
ng-model="vm.securitySettings.passwordPolicy.minimumUppercaseLetters"> |
|||
<div ng-messages="vm.settingsForm.minimumUppercaseLetters.$error"> |
|||
<div translate ng-message="min">admin.minimum-uppercase-letters-range</div> |
|||
</div> |
|||
</md-input-container> |
|||
<md-input-container class="md-block"> |
|||
<label translate>admin.minimum-lowercase-letters</label> |
|||
<input type="number" |
|||
step="1" |
|||
min="0" |
|||
name="minimumLowercaseLetters" |
|||
ng-model="vm.securitySettings.passwordPolicy.minimumLowercaseLetters"> |
|||
<div ng-messages="vm.settingsForm.minimumLowercaseLetters.$error"> |
|||
<div translate ng-message="min">admin.minimum-lowercase-letters-range</div> |
|||
</div> |
|||
</md-input-container> |
|||
<md-input-container class="md-block"> |
|||
<label translate>admin.minimum-digits</label> |
|||
<input type="number" |
|||
step="1" |
|||
min="0" |
|||
name="minimumDigits" |
|||
ng-model="vm.securitySettings.passwordPolicy.minimumDigits"> |
|||
<div ng-messages="vm.settingsForm.minimumDigits.$error"> |
|||
<div translate ng-message="min">admin.minimum-digits-range</div> |
|||
</div> |
|||
</md-input-container> |
|||
<md-input-container class="md-block"> |
|||
<label translate>admin.minimum-special-characters</label> |
|||
<input type="number" |
|||
step="1" |
|||
min="0" |
|||
name="minimumSpecialCharacters" |
|||
ng-model="vm.securitySettings.passwordPolicy.minimumSpecialCharacters"> |
|||
<div ng-messages="vm.settingsForm.minimumSpecialCharacters.$error"> |
|||
<div translate ng-message="min">admin.minimum-special-characters-range</div> |
|||
</div> |
|||
</md-input-container> |
|||
<md-input-container class="md-block"> |
|||
<label translate>admin.password-expiration-period-days</label> |
|||
<input type="number" |
|||
step="1" |
|||
min="0" |
|||
name="passwordExpirationPeriodDays" |
|||
ng-model="vm.securitySettings.passwordPolicy.passwordExpirationPeriodDays"> |
|||
<div ng-messages="vm.settingsForm.passwordExpirationPeriodDays.$error"> |
|||
<div translate ng-message="min">admin.password-expiration-period-days-range</div> |
|||
</div> |
|||
</md-input-container> |
|||
</md-expansion-panel-content> |
|||
</md-expansion-panel-expanded> |
|||
</md-expansion-panel> |
|||
</md-expansion-panel-group> |
|||
<div layout="row" layout-align="end center" width="100%" layout-wrap> |
|||
<md-button ng-disabled="$root.loading || vm.settingsForm.$invalid || !vm.settingsForm.$dirty" type="submit" class="md-raised md-primary">{{'action.save' | translate}}</md-button> |
|||
</div> |
|||
</fieldset> |
|||
</form> |
|||
</md-card-content> |
|||
</md-card> |
|||
</div> |
|||
Loading…
Reference in new issue