diff --git a/jmix-core/core/src/main/java/io/jmix/core/security/UserManager.java b/jmix-core/core/src/main/java/io/jmix/core/security/UserManager.java
index d0f9de3b18..f928d725db 100644
--- a/jmix-core/core/src/main/java/io/jmix/core/security/UserManager.java
+++ b/jmix-core/core/src/main/java/io/jmix/core/security/UserManager.java
@@ -61,22 +61,46 @@ UserDetails changePassword(String userName, @Nullable String oldPassword, @Nulla
/**
* Generates new passwords for passed users and saves changes to the database immediately.
+ *
+ * If the user entity has a field marked with
+ * {@code @io.jmix.security.user.PasswordChangeRequired}, the field is set to
+ * {@code true} so that the user is required to change the password at the next logon.
*
* @param users users which need reset passwords
* @return map which contains new passwords for the passed users
*/
default Map resetPasswords(Set users) {
- return resetPasswords(users, true);
+ return resetPasswords(users, true, true);
}
/**
* Generates new passwords for passed users.
+ *
+ * If the user entity has a field marked with
+ * {@code @io.jmix.security.user.PasswordChangeRequired}, the field is set to
+ * {@code true} so that the user is required to change the password at the next logon.
*
* @param users users which need reset passwords
* @param saveChanges whether to save changes to the database
* @return map which contains new passwords for the passed users
*/
- Map resetPasswords(Set users, boolean saveChanges);
+ default Map resetPasswords(Set users, boolean saveChanges) {
+ return resetPasswords(users, saveChanges, true);
+ }
+
+ /**
+ * Generates new passwords for passed users.
+ *
+ * @param users users which need reset passwords
+ * @param saveChanges whether to save changes to the database
+ * @param requireChangeAtNextLogon if {@code true} and the user entity has a field marked with
+ * {@code @io.jmix.security.user.PasswordChangeRequired}, the field is set
+ * to {@code true} so that the user is required to change the password at
+ * the next logon
+ * @return map which contains new passwords for the passed users
+ */
+ Map resetPasswords(Set users, boolean saveChanges,
+ boolean requireChangeAtNextLogon);
/**
* Resets 'remember me' token for the specific user.
diff --git a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/change-password-view.css b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/change-password-view.css
new file mode 100644
index 0000000000..83b2f7cdc9
--- /dev/null
+++ b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/change-password-view.css
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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.
+ */
+
+.force-change-password-view .dialog-close-button {
+ display: none;
+}
diff --git a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/index.css b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/index.css
index 76ecf81b6c..31f5d1e737 100644
--- a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/index.css
+++ b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/index.css
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+@import './change-password-view.css';
@import './reset-password-view.css';
@import './role-assignment-view.css';
@import './substitute-user-view.css';
\ No newline at end of file
diff --git a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/reset-password-view.css b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/reset-password-view.css
index 478948344f..9ad84ae280 100644
--- a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/reset-password-view.css
+++ b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-aura/src/addons/security/reset-password-view.css
@@ -23,6 +23,10 @@
margin-top: var(--vaadin-padding-l);
}
+.reset-password-view .required-change-checkbox {
+ margin-top: var(--vaadin-padding-m);
+}
+
.reset-password-view vaadin-password-field[readonly][class~='reset-password-field']::part(input-field) {
--vaadin-input-field-border-color: transparent;
}
diff --git a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/change-password-view.css b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/change-password-view.css
new file mode 100644
index 0000000000..83b2f7cdc9
--- /dev/null
+++ b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/change-password-view.css
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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.
+ */
+
+.force-change-password-view .dialog-close-button {
+ display: none;
+}
diff --git a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/index.css b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/index.css
index 76ecf81b6c..31f5d1e737 100644
--- a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/index.css
+++ b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/index.css
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+@import './change-password-view.css';
@import './reset-password-view.css';
@import './role-assignment-view.css';
@import './substitute-user-view.css';
\ No newline at end of file
diff --git a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/reset-password-view.css b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/reset-password-view.css
index dbf291276b..e613c39296 100644
--- a/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/reset-password-view.css
+++ b/jmix-flowui/flowui-themes/src/main/resources/META-INF/resources/themes/jmix-lumo/src/addons/security/reset-password-view.css
@@ -23,6 +23,10 @@
margin-top: var(--lumo-space-l);
}
+.reset-password-view .required-change-checkbox {
+ margin-top: var(--lumo-space-m);
+}
+
.reset-password-view .warning-icon {
padding: var(--lumo-space-xs);
}
diff --git a/jmix-security/security-data/src/main/java/io/jmix/securitydata/user/AbstractDatabaseUserRepository.java b/jmix-security/security-data/src/main/java/io/jmix/securitydata/user/AbstractDatabaseUserRepository.java
index af9da3b77c..b5d44714f0 100644
--- a/jmix-security/security-data/src/main/java/io/jmix/securitydata/user/AbstractDatabaseUserRepository.java
+++ b/jmix-security/security-data/src/main/java/io/jmix/securitydata/user/AbstractDatabaseUserRepository.java
@@ -36,6 +36,7 @@
import io.jmix.security.role.assignment.RoleAssignment;
import io.jmix.security.role.assignment.RoleAssignmentRepository;
import io.jmix.security.role.assignment.RoleAssignmentRoleType;
+import io.jmix.security.user.PasswordChangeRequiredSupport;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
@@ -79,6 +80,8 @@ public abstract class AbstractDatabaseUserRepository impl
protected ApplicationEventPublisher eventPublisher;
@Autowired
protected RoleGrantedAuthorityUtils roleGrantedAuthorityUtils;
+ @Autowired
+ protected PasswordChangeRequiredSupport passwordChangeRequiredSupport;
/**
* Helps create authorities from roles.
@@ -204,6 +207,7 @@ public T changePassword(String userName, @Nullable String oldPassword, @Nullable
Preconditions.checkNotNullArgument(newPassword, "Null new password");
T userDetails = loadUserByUsername(userName);
changePassword(userDetails, oldPassword, newPassword);
+ passwordChangeRequiredSupport.setPasswordChangeRequired(userDetails, false);
if (saveChanges) {
userDetails = dataManager.save(userDetails);
@@ -225,7 +229,8 @@ private void changePassword(T userDetails, @Nullable String oldPassword, @Nullab
}
@Override
- public Map resetPasswords(Set users, boolean saveChanges) {
+ public Map resetPasswords(Set users, boolean saveChanges,
+ boolean requireChangeAtNextLogon) {
Map usernamePasswordMap = new LinkedHashMap<>();
SaveContext saveContext = new SaveContext();
@@ -244,6 +249,7 @@ public Map resetPasswords(Set users, boolean s
success = true;
} while (!success);
+ passwordChangeRequiredSupport.setPasswordChangeRequired(userDetails, requireChangeAtNextLogon);
saveContext.saving(userDetails);
usernamePasswordMap.put(userDetails, newPassword);
}
diff --git a/jmix-security/security-data/src/test/groovy/password_change_required/PasswordChangeRequiredTest.groovy b/jmix-security/security-data/src/test/groovy/password_change_required/PasswordChangeRequiredTest.groovy
new file mode 100644
index 0000000000..0f4d2f5daa
--- /dev/null
+++ b/jmix-security/security-data/src/test/groovy/password_change_required/PasswordChangeRequiredTest.groovy
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 password_change_required
+
+import io.jmix.core.CoreConfiguration
+import io.jmix.core.UnconstrainedDataManager
+import io.jmix.core.security.PasswordNotMatchException
+import io.jmix.core.security.UserManager
+import io.jmix.data.DataConfiguration
+import io.jmix.eclipselink.EclipselinkConfiguration
+import io.jmix.security.SecurityConfiguration
+import io.jmix.securitydata.SecurityDataConfiguration
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.jdbc.core.JdbcTemplate
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.test.context.ContextConfiguration
+import password_change_required.test_support.PasswordChangeRequiredTestConfiguration
+import spock.lang.Specification
+import test_support.TestContextInititalizer
+import test_support.entity.TestUser
+
+@ContextConfiguration(
+ classes = [CoreConfiguration, DataConfiguration, EclipselinkConfiguration,
+ SecurityConfiguration, SecurityDataConfiguration, PasswordChangeRequiredTestConfiguration],
+ initializers = [TestContextInititalizer]
+)
+class PasswordChangeRequiredTest extends Specification {
+
+ @Autowired
+ UserManager userManager
+ @Autowired
+ UnconstrainedDataManager dataManager
+ @Autowired
+ PasswordEncoder passwordEncoder
+ @Autowired
+ JdbcTemplate jdbcTemplate
+
+ def cleanup() {
+ jdbcTemplate.update('delete from TEST_USER')
+ }
+
+ private TestUser createUser(String username, String rawPassword, boolean changePasswordFlag) {
+ def user = dataManager.create(TestUser)
+ user.username = username
+ user.password = passwordEncoder.encode(rawPassword)
+ user.changePasswordAtNextLogon = changePasswordFlag
+ return dataManager.save(user)
+ }
+
+ private TestUser reload(String username) {
+ return dataManager.load(TestUser).query('e.username = :u').parameter('u', username).one()
+ }
+
+ def "changePassword resets the changePasswordAtNextLogon flag"() {
+ given:
+ createUser('john', 'oldPwd', true)
+
+ when:
+ userManager.changePassword('john', 'oldPwd', 'newPwd')
+
+ then:
+ def reloaded = reload('john')
+ reloaded.changePasswordAtNextLogon == false
+ passwordEncoder.matches('newPwd', reloaded.password)
+ }
+
+ def "changePassword in-memory updates the flag when saveChanges is false"() {
+ given:
+ createUser('alice', 'oldPwd', true)
+
+ when:
+ def result = userManager.changePassword('alice', 'oldPwd', 'newPwd', false) as TestUser
+
+ then:
+ result.changePasswordAtNextLogon == false
+
+ and: 'database still has the original flag value because changes were not saved'
+ reload('alice').changePasswordAtNextLogon == true
+ }
+
+ def "changePassword keeps the flag when PasswordNotMatchException is thrown"() {
+ given:
+ createUser('mary', 'samePwd', true)
+
+ when:
+ userManager.changePassword('mary', 'samePwd', 'samePwd')
+
+ then:
+ thrown(PasswordNotMatchException)
+
+ and:
+ reload('mary').changePasswordAtNextLogon == true
+ }
+
+ def "resetPasswords with requireChangeAtNextLogon=true sets the flag to true"() {
+ given:
+ def user = createUser('bob', 'oldPwd', false)
+
+ when:
+ def passwords = userManager.resetPasswords([user] as Set, true, true)
+
+ then:
+ passwords.size() == 1
+ def reloaded = reload('bob')
+ reloaded.changePasswordAtNextLogon == true
+ !passwordEncoder.matches('oldPwd', reloaded.password)
+ }
+
+ def "resetPasswords with requireChangeAtNextLogon=false keeps the flag false"() {
+ given:
+ def user = createUser('eve', 'oldPwd', false)
+
+ when:
+ userManager.resetPasswords([user] as Set, true, false)
+
+ then:
+ reload('eve').changePasswordAtNextLogon == false
+ }
+
+ def "default resetPasswords overload defaults to requireChangeAtNextLogon=true"() {
+ given:
+ def user = createUser('carol', 'oldPwd', false)
+
+ when:
+ userManager.resetPasswords([user] as Set)
+
+ then:
+ reload('carol').changePasswordAtNextLogon == true
+ }
+}
diff --git a/jmix-security/security-data/src/test/java/password_change_required/test_support/PasswordChangeRequiredTestConfiguration.java b/jmix-security/security-data/src/test/java/password_change_required/test_support/PasswordChangeRequiredTestConfiguration.java
new file mode 100644
index 0000000000..472001317f
--- /dev/null
+++ b/jmix-security/security-data/src/test/java/password_change_required/test_support/PasswordChangeRequiredTestConfiguration.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 password_change_required.test_support;
+
+import io.jmix.core.security.UserRepository;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import test_support.SecurityDataTestConfiguration;
+import test_support.repository.TestUserRepository;
+
+/**
+ * Test configuration for the {@link io.jmix.securitydata.user.AbstractDatabaseUserRepository} integration test.
+ * Imports the common security-data test infrastructure and overrides the default in-memory user repository
+ * with {@link TestUserRepository} based on the {@code TEST_USER} table.
+ */
+@Configuration
+@Import(SecurityDataTestConfiguration.class)
+public class PasswordChangeRequiredTestConfiguration {
+
+ @Bean("test_userRepository")
+ @Primary
+ public UserRepository testUserRepository() {
+ return new TestUserRepository();
+ }
+}
diff --git a/jmix-security/security-data/src/test/java/test_support/entity/TestUser.java b/jmix-security/security-data/src/test/java/test_support/entity/TestUser.java
new file mode 100644
index 0000000000..8ff74f67b2
--- /dev/null
+++ b/jmix-security/security-data/src/test/java/test_support/entity/TestUser.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 test_support.entity;
+
+import io.jmix.core.entity.annotation.JmixGeneratedValue;
+import io.jmix.core.entity.annotation.SystemLevel;
+import io.jmix.core.metamodel.annotation.JmixEntity;
+import io.jmix.security.user.PasswordChangeRequired;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.Transient;
+import jakarta.persistence.Version;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.UUID;
+
+@Entity(name = "test_User")
+@JmixEntity
+@Table(name = "TEST_USER")
+public class TestUser implements UserDetails {
+
+ @Id
+ @Column(name = "ID")
+ @JmixGeneratedValue
+ private UUID id;
+
+ @Version
+ @Column(name = "VERSION", nullable = false)
+ private Integer version;
+
+ @Column(name = "USERNAME", nullable = false)
+ private String username;
+
+ @SystemLevel
+ @Column(name = "PASSWORD")
+ private String password;
+
+ @PasswordChangeRequired
+ @Column(name = "CHANGE_PASSWORD_AT_NEXT_LOGON")
+ private Boolean changePasswordAtNextLogon = false;
+
+ @Transient
+ private Collection extends GrantedAuthority> authorities;
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public Integer getVersion() {
+ return version;
+ }
+
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ @Override
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public Boolean getChangePasswordAtNextLogon() {
+ return changePasswordAtNextLogon;
+ }
+
+ public void setChangePasswordAtNextLogon(Boolean changePasswordAtNextLogon) {
+ this.changePasswordAtNextLogon = changePasswordAtNextLogon;
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return authorities != null ? authorities : Collections.emptyList();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+}
diff --git a/jmix-security/security-data/src/test/java/test_support/repository/TestUserRepository.java b/jmix-security/security-data/src/test/java/test_support/repository/TestUserRepository.java
new file mode 100644
index 0000000000..caed0b4efe
--- /dev/null
+++ b/jmix-security/security-data/src/test/java/test_support/repository/TestUserRepository.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 test_support.repository;
+
+import io.jmix.securitydata.user.AbstractDatabaseUserRepository;
+import test_support.entity.TestUser;
+
+public class TestUserRepository extends AbstractDatabaseUserRepository {
+
+ @Override
+ protected Class getUserClass() {
+ return TestUser.class;
+ }
+}
diff --git a/jmix-security/security-data/src/test/resources/test_support/liquibase/test-changelog.xml b/jmix-security/security-data/src/test/resources/test_support/liquibase/test-changelog.xml
index 5e4c7d324f..ac422ec229 100644
--- a/jmix-security/security-data/src/test/resources/test_support/liquibase/test-changelog.xml
+++ b/jmix-security/security-data/src/test/resources/test_support/liquibase/test-changelog.xml
@@ -126,6 +126,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/authentication/PasswordChangeRequiredViewListener.java b/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/authentication/PasswordChangeRequiredViewListener.java
new file mode 100644
index 0000000000..f4aa4debb1
--- /dev/null
+++ b/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/authentication/PasswordChangeRequiredViewListener.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 io.jmix.securityflowui.authentication;
+
+import com.google.common.base.Strings;
+import io.jmix.core.security.CurrentAuthentication;
+import io.jmix.flowui.DialogWindows;
+import io.jmix.flowui.UiProperties;
+import io.jmix.flowui.event.view.ViewOpenedEvent;
+import io.jmix.flowui.view.DialogWindow;
+import io.jmix.flowui.view.View;
+import io.jmix.flowui.view.ViewInfo;
+import io.jmix.flowui.view.ViewRegistry;
+import io.jmix.security.user.PasswordChangeRequiredSupport;
+import io.jmix.securityflowui.view.changepassword.ChangePasswordView;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+/**
+ * Opens a forced {@link ChangePasswordView} dialog on the main view if the currently logged in user is required
+ * to change the password at the next logon.
+ *
+ * Activates only when the user entity has a field marked with
+ * {@link io.jmix.security.user.PasswordChangeRequired @PasswordChangeRequired} and the value is {@code true}.
+ * Otherwise, this listener does nothing.
+ *
+ * @see PasswordChangeRequiredSupport
+ */
+@Component("sec_PasswordChangeRequiredViewListener")
+public class PasswordChangeRequiredViewListener {
+
+ @Autowired
+ protected CurrentAuthentication currentAuthentication;
+ @Autowired
+ protected DialogWindows dialogWindows;
+ @Autowired
+ protected PasswordChangeRequiredSupport passwordChangeRequiredSupport;
+ @Autowired
+ protected UiProperties uiProperties;
+ @Autowired
+ protected ViewRegistry viewRegistry;
+
+ @EventListener
+ public void onViewOpened(ViewOpenedEvent event) {
+ View> view = event.getSource();
+ if (!shouldOpenDialog(view)) {
+ return;
+ }
+
+ if (!currentAuthentication.isSet()) {
+ return;
+ }
+
+ UserDetails user = currentAuthentication.getUser();
+ if (!passwordChangeRequiredSupport.isPasswordChangeRequired(user)) {
+ return;
+ }
+
+ // A single navigation may open more than one view (e.g. main view and the default view inside it),
+ // each firing a ViewOpenedEvent. Avoid stacking several forced dialogs on top of each other.
+ if (isChangePasswordDialogOpened()) {
+ return;
+ }
+
+ openForcedChangePasswordDialog(view, user.getUsername());
+ }
+
+ protected boolean shouldOpenDialog(View> view) {
+ Class> viewClass = view.getClass();
+ // Skip the ChangePasswordView itself to avoid recursion.
+ if (isViewOfId(viewClass, "changePasswordView")) {
+ return false;
+ }
+
+ String loginViewId = uiProperties.getLoginViewId();
+ // Skip the login view so the dialog is not opened before authentication.
+ // Any other view (main, default, or one navigated to via bookmark/URL) is a valid target.
+ return Strings.isNullOrEmpty(loginViewId) || !isViewOfId(viewClass, loginViewId);
+
+ }
+
+ protected boolean isViewOfId(Class> viewClass, String viewId) {
+ try {
+ ViewInfo viewInfo = viewRegistry.getViewInfo(viewId);
+ return viewInfo.getControllerClass().isAssignableFrom(viewClass);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ protected void openForcedChangePasswordDialog(View> origin, String username) {
+ DialogWindow dialog = dialogWindows.view(origin, ChangePasswordView.class)
+ .build();
+
+ ChangePasswordView view = dialog.getView();
+ view.setUsername(username);
+ view.setCurrentPasswordRequired(false);
+ view.setForced(true);
+
+ dialog.addClassName("force-change-password-view");
+
+ dialog.setCloseOnEsc(false);
+ dialog.setCloseOnOutsideClick(false);
+
+ dialog.open();
+ }
+
+ protected boolean isChangePasswordDialogOpened() {
+ return dialogWindows.getOpenedDialogWindows().getDialogs().stream()
+ .anyMatch(ChangePasswordView.class::isInstance);
+ }
+}
diff --git a/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/role/UiMinimalPolicies.java b/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/role/UiMinimalPolicies.java
index d9370a9be6..f683c89f4b 100644
--- a/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/role/UiMinimalPolicies.java
+++ b/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/role/UiMinimalPolicies.java
@@ -26,7 +26,7 @@
public interface UiMinimalPolicies {
- @ViewPolicy(viewIds = {"inputDialog", "multiValueSelectDialog", "sec_SubstituteUserView"})
+ @ViewPolicy(viewIds = {"inputDialog", "multiValueSelectDialog", "sec_SubstituteUserView", "changePasswordView"})
void systemDialogs();
@EntityPolicy(entityClass = KeyValueEntity.class, actions = EntityPolicyAction.READ)
diff --git a/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/view/changepassword/ChangePasswordView.java b/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/view/changepassword/ChangePasswordView.java
index 8a0fe3de9c..7bbb8de023 100644
--- a/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/view/changepassword/ChangePasswordView.java
+++ b/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/view/changepassword/ChangePasswordView.java
@@ -21,28 +21,23 @@
import com.vaadin.flow.component.textfield.PasswordField;
import io.jmix.core.MetadataTools;
import io.jmix.core.common.util.Preconditions;
+import io.jmix.core.security.CurrentAuthentication;
import io.jmix.core.security.PasswordNotMatchException;
import io.jmix.core.security.UserManager;
import io.jmix.core.security.UserRepository;
import io.jmix.flowui.Notifications;
import io.jmix.flowui.component.validation.ValidationErrors;
+import io.jmix.flowui.kit.action.Action;
import io.jmix.flowui.kit.action.ActionPerformedEvent;
import io.jmix.flowui.util.OperationResult;
-import io.jmix.flowui.view.DialogMode;
-import io.jmix.flowui.view.MessageBundle;
-import io.jmix.flowui.view.StandardOutcome;
-import io.jmix.flowui.view.StandardView;
-import io.jmix.flowui.view.Subscribe;
-import io.jmix.flowui.view.ViewComponent;
-import io.jmix.flowui.view.ViewController;
-import io.jmix.flowui.view.ViewDescriptor;
-import io.jmix.flowui.view.ViewValidation;
+import io.jmix.flowui.view.*;
+import io.jmix.security.user.PasswordChangeRequiredSupport;
import io.jmix.securityflowui.password.PasswordValidation;
+import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
-import org.jspecify.annotations.Nullable;
import java.util.Objects;
@ViewController("changePasswordView")
@@ -56,6 +51,9 @@ public class ChangePasswordView extends StandardView {
@ViewComponent
protected PasswordField currentPasswordField;
+ @ViewComponent
+ protected Action closeAction;
+
@ViewComponent
protected MessageBundle messageBundle;
@Autowired
@@ -72,9 +70,14 @@ public class ChangePasswordView extends StandardView {
protected UserRepository userRepository;
@Autowired
protected MetadataTools metadataTools;
+ @Autowired
+ protected CurrentAuthentication currentAuthentication;
+ @Autowired
+ protected PasswordChangeRequiredSupport passwordChangeRequiredSupport;
protected String username;
protected UserDetails user;
+ protected boolean forced = false;
/**
* @return username for which should be changed password
@@ -110,17 +113,49 @@ public void setCurrentPasswordRequired(boolean required) {
currentPasswordField.setVisible(required);
}
+ /**
+ * @return {@code true} if the dialog is opened in the forced mode
+ */
+ public boolean isForced() {
+ return forced;
+ }
+
+ /**
+ * Sets the forced mode. In the forced mode the user cannot close the dialog without changing the password.
+ *
+ * Default value is {@code false}.
+ *
+ * @param forced forced mode flag
+ */
+ public void setForced(boolean forced) {
+ this.forced = forced;
+ }
+
@Subscribe
public void onBeforeShow(BeforeShowEvent event) {
Preconditions.checkNotNullArgument(username,
messageBundle.getMessage("changePasswordView.emptyUsernameMessage"));
user = userRepository.loadUserByUsername(username);
+
+ closeAction.setVisible(!forced);
+
if (currentPasswordField.isVisible())
currentPasswordField.focus();
}
+ @Subscribe
+ public void onBeforeClose(BeforeCloseEvent event) {
+ if (forced && !event.closedWith(StandardOutcome.SAVE)) {
+ event.preventClose();
+ }
+ }
+
@Override
public String getPageTitle() {
+ if (forced) {
+ return messageBundle.getMessage("changePasswordView.forcedTitle");
+ }
+
return username != null
? String.format(messageBundle.formatMessage("changePasswordView.title", username))
: super.getPageTitle();
@@ -134,17 +169,22 @@ public void onSaveActionPerformed(ActionPerformedEvent event) {
changePassword(username, getPassword(), null)
.then(() -> {
+ refreshCurrentUserPasswordChangeRequiredFlag();
notifications.create(messageBundle.getMessage("changePasswordView.passwordChanged"))
.withType(Notifications.Type.SUCCESS)
.withPosition(Notification.Position.MIDDLE)
.show();
close(StandardOutcome.SAVE);
- }).otherwise(() -> {
- viewValidation.showValidationErrors(ValidationErrors.of(
- messageBundle.getMessage("changePasswordView.currentPasswordWarning")));
- });
+ }).otherwise(() -> viewValidation.showValidationErrors(ValidationErrors.of(
+ messageBundle.getMessage("changePasswordView.currentPasswordWarning"))));
+ }
- close(StandardOutcome.SAVE);
+ protected void refreshCurrentUserPasswordChangeRequiredFlag() {
+ UserDetails currentUser = currentAuthentication.getUser();
+ if (Objects.equals(currentUser.getUsername(), username)) {
+ // Sync the principal cached in SecurityContext so that the view listener does not reopen the dialog.
+ passwordChangeRequiredSupport.setPasswordChangeRequired(currentUser, false);
+ }
}
@Subscribe("closeAction")
diff --git a/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/view/resetpassword/ResetPasswordView.java b/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/view/resetpassword/ResetPasswordView.java
index 7217efd465..0dd6664fe5 100644
--- a/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/view/resetpassword/ResetPasswordView.java
+++ b/jmix-security/security-flowui/src/main/java/io/jmix/securityflowui/view/resetpassword/ResetPasswordView.java
@@ -41,6 +41,7 @@
import io.jmix.flowui.backgroundtask.BackgroundWorker;
import io.jmix.flowui.backgroundtask.TaskLifeCycle;
import io.jmix.flowui.component.UiComponentUtils;
+import io.jmix.flowui.component.checkbox.JmixCheckbox;
import io.jmix.flowui.component.grid.DataGrid;
import io.jmix.flowui.component.textfield.JmixPasswordField;
import io.jmix.flowui.icon.Icons;
@@ -50,6 +51,7 @@
import io.jmix.flowui.model.CollectionContainer;
import io.jmix.flowui.util.UnknownOperationResult;
import io.jmix.flowui.view.*;
+import io.jmix.security.user.PasswordChangeRequiredSupport;
import io.jmix.securityflowui.view.resetpassword.model.UserPasswordValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -80,6 +82,8 @@ public class ResetPasswordView extends StandardView {
@Autowired
protected ApplicationEventPublisher applicationEventPublisher;
@Autowired
+ protected PasswordChangeRequiredSupport passwordChangeRequiredSupport;
+ @Autowired
protected UiAsyncTasks uiAsyncTasks;
@Autowired
protected BackgroundWorker backgroundWorker;
@@ -109,6 +113,8 @@ public class ResetPasswordView extends StandardView {
protected DataGrid passwordsDataGrid;
@ViewComponent
protected JmixButton closeBtn;
+ @ViewComponent
+ protected JmixCheckbox requireChangeAtNextLogonField;
@ViewComponent
protected VerticalLayout progressBarLayout;
@@ -130,6 +136,7 @@ public class ResetPasswordView extends StandardView {
protected Set extends UserDetails> users;
protected Dialog cancelDialog;
+ protected boolean requireChangeAtNextLogon = true;
@Subscribe
public void onInit(InitEvent event) {
@@ -141,6 +148,18 @@ public void onReady(ReadyEvent event) {
if (!isSingleSelected()) {
updateMultiSelectComponentsLabels();
}
+
+ updateRequireChangeAtNextLogonFieldVisibility();
+ }
+
+ protected void updateRequireChangeAtNextLogonFieldVisibility() {
+ if (users == null || users.isEmpty()) {
+ return;
+ }
+
+ Class> userClass = users.iterator().next().getClass();
+ boolean supported = passwordChangeRequiredSupport.findFlagProperty(userClass) != null;
+ requireChangeAtNextLogonField.setVisible(supported);
}
@Subscribe
@@ -198,6 +217,7 @@ public String getPageTitle() {
@Subscribe("generateBtn")
protected void onGenerateBtnClick(ClickEvent event) {
+ requireChangeAtNextLogon = Boolean.TRUE.equals(requireChangeAtNextLogonField.getValue());
configureComponentsBeforeGeneration();
generationTaskHandler = backgroundWorker.handle(createBackgroundTask());
@@ -362,15 +382,16 @@ public List run(TaskLifeCycle taskLifeCycle) throws
int i = 0;
for (UserDetails userDetails : users) {
- Map userPasswordMap =
- userManager.resetPasswords(Collections.singleton(userDetails), false);
+ Map userPasswordMap = userManager.resetPasswords(
+ Collections.singleton(userDetails), false, requireChangeAtNextLogon);
Map.Entry userPassword = userPasswordMap.entrySet().iterator().next();
- String username = userPassword.getKey().getUsername();
+ UserDetails refreshedUser = userPassword.getKey();
+ String username = refreshedUser.getUsername();
String password = userPassword.getValue();
usernamePasswordMap.put(username, password);
- saveContext.saving(userPassword.getKey());
+ saveContext.saving(refreshedUser);
result.add(createPasswordValue(username, password));
taskLifeCycle.publish(++i);
diff --git a/jmix-security/security-flowui/src/main/resources/io/jmix/securityflowui/messages.properties b/jmix-security/security-flowui/src/main/resources/io/jmix/securityflowui/messages.properties
index 8afe5e1edc..1938ec964d 100644
--- a/jmix-security/security-flowui/src/main/resources/io/jmix/securityflowui/messages.properties
+++ b/jmix-security/security-flowui/src/main/resources/io/jmix/securityflowui/messages.properties
@@ -213,6 +213,7 @@ io.jmix.securityflowui.view.changepassword/changePasswordView.passwordsDoNotMatc
io.jmix.securityflowui.view.changepassword/changePasswordView.passwordChanged=Password successfully changed
io.jmix.securityflowui.view.changepassword/changePasswordView.emptyUsernameMessage=Dialog cannot be opened without username
io.jmix.securityflowui.view.changepassword/changePasswordView.title=Change password for [%s]
+io.jmix.securityflowui.view.changepassword/changePasswordView.forcedTitle=Password change required
io.jmix.securityflowui.view.resetpassword/resetPasswordView.resetPasswordsTitle = Generate new passwords for selected users
io.jmix.securityflowui.view.resetpassword/resetPasswordView.resetSinglePasswordTitle = Generate a new password for [%s]
@@ -230,6 +231,7 @@ io.jmix.securityflowui.view.resetpassword/resetPasswordView.cancelDialog.title =
io.jmix.securityflowui.view.resetpassword/resetPasswordView.cancelDialog.message = Do you want to cancel passwords generation?
io.jmix.securityflowui.view.resetpassword/resetPasswordView.exportActionButton.text = Excel
io.jmix.securityflowui.view.resetpassword/resetPasswordView.copyButton.tooltip = Copy to clipboard
+io.jmix.securityflowui.view.resetpassword/resetPasswordView.requireChangeAtNextLogon = Require change at next logon
io.jmix.securityflowui.view.resetpassword.model/UserPasswordValue.username = Username
io.jmix.securityflowui.view.resetpassword.model/UserPasswordValue.password = Password
diff --git a/jmix-security/security-flowui/src/main/resources/io/jmix/securityflowui/view/resetpassword/reset-password-view.xml b/jmix-security/security-flowui/src/main/resources/io/jmix/securityflowui/view/resetpassword/reset-password-view.xml
index 6ca8328af3..e843280e79 100644
--- a/jmix-security/security-flowui/src/main/resources/io/jmix/securityflowui/view/resetpassword/reset-password-view.xml
+++ b/jmix-security/security-flowui/src/main/resources/io/jmix/securityflowui/view/resetpassword/reset-password-view.xml
@@ -33,6 +33,10 @@
+
diff --git a/jmix-security/security/src/main/java/io/jmix/security/user/PasswordChangeRequired.java b/jmix-security/security/src/main/java/io/jmix/security/user/PasswordChangeRequired.java
new file mode 100644
index 0000000000..069d51467a
--- /dev/null
+++ b/jmix-security/security/src/main/java/io/jmix/security/user/PasswordChangeRequired.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 io.jmix.security.user;
+
+import io.jmix.core.entity.annotation.MetaAnnotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Marks a Boolean field of a user entity that indicates whether the user is required to change
+ * the password at the next logon.
+ *
+ * When this annotation is present on a field:
+ *
+ * - {@code AbstractDatabaseUserRepository} resets the field to {@code false} when the user changes
+ * the password, and sets it to {@code true} when the administrator resets the password.
+ * - UI shows a modal forced change-password dialog after the user logs in.
+ *
+ * Only one field of a class can have this annotation.
+ */
+@Target({FIELD})
+@Retention(RUNTIME)
+@MetaAnnotation
+public @interface PasswordChangeRequired {
+}
diff --git a/jmix-security/security/src/main/java/io/jmix/security/user/PasswordChangeRequiredSupport.java b/jmix-security/security/src/main/java/io/jmix/security/user/PasswordChangeRequiredSupport.java
new file mode 100644
index 0000000000..93c736a5a3
--- /dev/null
+++ b/jmix-security/security/src/main/java/io/jmix/security/user/PasswordChangeRequiredSupport.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 io.jmix.security.user;
+
+import io.jmix.core.Metadata;
+import io.jmix.core.entity.EntityValues;
+import io.jmix.core.metamodel.model.MetaClass;
+import io.jmix.core.metamodel.model.MetaProperty;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.jspecify.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * Helper for working with the user entity field marked with the {@link PasswordChangeRequired} annotation.
+ *
+ * If the user entity does not have such a field, all methods of this class behave as no-ops:
+ * {@link #isPasswordChangeRequired(Object)} returns {@code false} and {@link #setPasswordChangeRequired(Object, boolean)}
+ * does nothing. This guarantees that the feature is fully optional and does not affect projects whose
+ * user entity is not annotated.
+ */
+@Component("sec_PasswordChangeRequiredSupport")
+public class PasswordChangeRequiredSupport {
+
+ @Autowired
+ protected Metadata metadata;
+
+ protected final ConcurrentHashMap, Optional> propertyCache = new ConcurrentHashMap<>();
+
+ /**
+ * @return the {@link MetaProperty} corresponding to the field marked with {@link PasswordChangeRequired}
+ * in the given user class, or {@code null} if the class does not have such a field, or the field is not
+ * registered in the metamodel
+ */
+ @Nullable
+ public MetaProperty findFlagProperty(Class> userClass) {
+ return propertyCache.computeIfAbsent(userClass, this::lookupFlagProperty)
+ .orElse(null);
+ }
+
+ /**
+ * @return {@code true} if the given user has the field marked with {@link PasswordChangeRequired} and its
+ * value is {@code true}
+ */
+ public boolean isPasswordChangeRequired(@Nullable Object user) {
+ if (!EntityValues.isEntity(user)) {
+ return false;
+ }
+
+ MetaProperty property = findFlagProperty(user.getClass());
+ if (property == null) {
+ return false;
+ }
+
+ Boolean value = EntityValues.getValue(user, property.getName());
+ return Boolean.TRUE.equals(value);
+ }
+
+ /**
+ * Sets the value of the field marked with {@link PasswordChangeRequired} on the given user.
+ * Does nothing if the user is not a Jmix entity or does not have such a field.
+ */
+ public void setPasswordChangeRequired(Object user, boolean value) {
+ if (!EntityValues.isEntity(user)) {
+ return;
+ }
+
+ MetaProperty property = findFlagProperty(user.getClass());
+ if (property == null) {
+ return;
+ }
+
+ EntityValues.setValue(user, property.getName(), value);
+ }
+
+ protected Optional lookupFlagProperty(Class> userClass) {
+ List annotatedFields = Arrays.stream(FieldUtils.getAllFields(userClass))
+ .filter(f -> f.isAnnotationPresent(PasswordChangeRequired.class))
+ .toList();
+ if (annotatedFields.isEmpty()) {
+ return Optional.empty();
+ }
+
+ if (annotatedFields.size() > 1) {
+ String names = annotatedFields.stream()
+ .map(Field::getName)
+ .collect(Collectors.joining(", "));
+ throw new IllegalStateException(
+ "Class '%s' has multiple fields annotated with @PasswordChangeRequired: [%s]. Only one is allowed."
+ .formatted(userClass.getName(), names));
+ }
+
+ Field field = annotatedFields.get(0);
+ Class> fieldType = field.getType();
+ if (fieldType != Boolean.class && fieldType != boolean.class) {
+ throw new IllegalStateException(("Field '%s' in class '%s' annotated with @PasswordChangeRequired"
+ + " must be of type Boolean or boolean, but is '%s'.")
+ .formatted(field.getName(), userClass.getName(), fieldType.getName()));
+ }
+
+ MetaClass metaClass = metadata.findClass(userClass);
+ if (metaClass == null) {
+ return Optional.empty();
+ }
+
+ return Optional.ofNullable(metaClass.findProperty(field.getName()));
+ }
+}
diff --git a/jmix-security/security/src/main/java/io/jmix/security/user/package-info.java b/jmix-security/security/src/main/java/io/jmix/security/user/package-info.java
new file mode 100644
index 0000000000..22b39becc9
--- /dev/null
+++ b/jmix-security/security/src/main/java/io/jmix/security/user/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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.
+ */
+
+@NullMarked
+package io.jmix.security.user;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/jmix-security/security/src/test/groovy/password_change_required/PasswordChangeRequiredSupportTest.groovy b/jmix-security/security/src/test/groovy/password_change_required/PasswordChangeRequiredSupportTest.groovy
new file mode 100644
index 0000000000..ba48f1133b
--- /dev/null
+++ b/jmix-security/security/src/test/groovy/password_change_required/PasswordChangeRequiredSupportTest.groovy
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 password_change_required
+
+import io.jmix.core.Metadata
+import io.jmix.security.user.PasswordChangeRequiredSupport
+import org.springframework.beans.factory.annotation.Autowired
+import test_support.SecuritySpecification
+import test_support.entity.TestUserWithFlag
+import test_support.entity.TestUserWithTwoFlags
+import test_support.entity.TestUserWithWrongFieldType
+import test_support.entity.TestUserWithoutFlag
+
+class PasswordChangeRequiredSupportTest extends SecuritySpecification {
+
+ @Autowired
+ PasswordChangeRequiredSupport support
+
+ @Autowired
+ Metadata metadata
+
+ def "flag property is found on entity annotated with @PasswordChangeRequired"() {
+ when:
+ def property = support.findFlagProperty(TestUserWithFlag)
+
+ then:
+ property != null
+ property.name == 'changePasswordAtNextLogon'
+ }
+
+ def "flag property is not found on entity without @PasswordChangeRequired"() {
+ expect:
+ support.findFlagProperty(TestUserWithoutFlag) == null
+ }
+
+ def "isPasswordChangeRequired returns false for entity without the flag field"() {
+ given:
+ def user = metadata.create(TestUserWithoutFlag)
+
+ expect:
+ !support.isPasswordChangeRequired(user)
+ }
+
+ def "isPasswordChangeRequired reflects the value of the annotated field"() {
+ given:
+ def user = metadata.create(TestUserWithFlag)
+
+ expect: 'default value is false'
+ !support.isPasswordChangeRequired(user)
+
+ when:
+ user.setChangePasswordAtNextLogon(true)
+
+ then:
+ support.isPasswordChangeRequired(user)
+ }
+
+ def "isPasswordChangeRequired returns false for null user"() {
+ expect:
+ !support.isPasswordChangeRequired(null)
+ }
+
+ def "isPasswordChangeRequired returns false for non-entity object"() {
+ expect:
+ !support.isPasswordChangeRequired('not an entity')
+ }
+
+ def "setPasswordChangeRequired updates the field"() {
+ given:
+ def user = metadata.create(TestUserWithFlag)
+ user.setChangePasswordAtNextLogon(true)
+
+ when:
+ support.setPasswordChangeRequired(user, false)
+
+ then:
+ user.changePasswordAtNextLogon == false
+
+ when:
+ support.setPasswordChangeRequired(user, true)
+
+ then:
+ user.changePasswordAtNextLogon == true
+ }
+
+ def "setPasswordChangeRequired is a no-op for entity without the flag field"() {
+ given:
+ def user = metadata.create(TestUserWithoutFlag)
+
+ when:
+ support.setPasswordChangeRequired(user, true)
+
+ then:
+ noExceptionThrown()
+ }
+
+ def "findFlagProperty fails with a clear error when more than one field is annotated"() {
+ when:
+ support.findFlagProperty(TestUserWithTwoFlags)
+
+ then:
+ def ex = thrown(IllegalStateException)
+ ex.message.contains(TestUserWithTwoFlags.name)
+ ex.message.contains('flagOne')
+ ex.message.contains('flagTwo')
+ }
+
+ def "findFlagProperty fails with a clear error when the annotated field has a wrong type"() {
+ when:
+ support.findFlagProperty(TestUserWithWrongFieldType)
+
+ then:
+ def ex = thrown(IllegalStateException)
+ ex.message.contains(TestUserWithWrongFieldType.name)
+ ex.message.contains('flagAsString')
+ ex.message.contains('Boolean')
+ }
+}
diff --git a/jmix-security/security/src/test/java/test_support/entity/TestUserWithFlag.java b/jmix-security/security/src/test/java/test_support/entity/TestUserWithFlag.java
new file mode 100644
index 0000000000..38b09cb10f
--- /dev/null
+++ b/jmix-security/security/src/test/java/test_support/entity/TestUserWithFlag.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 test_support.entity;
+
+import io.jmix.core.metamodel.annotation.JmixEntity;
+import io.jmix.security.user.PasswordChangeRequired;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+
+@Entity(name = "test_UserWithFlag")
+@JmixEntity
+@Table(name = "TEST_USER_WITH_FLAG")
+public class TestUserWithFlag extends BaseEntity {
+
+ @Column(name = "USERNAME")
+ private String username;
+
+ @PasswordChangeRequired
+ @Column(name = "CHANGE_PASSWORD_AT_NEXT_LOGON")
+ private Boolean changePasswordAtNextLogon = false;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Boolean getChangePasswordAtNextLogon() {
+ return changePasswordAtNextLogon;
+ }
+
+ public void setChangePasswordAtNextLogon(Boolean changePasswordAtNextLogon) {
+ this.changePasswordAtNextLogon = changePasswordAtNextLogon;
+ }
+}
diff --git a/jmix-security/security/src/test/java/test_support/entity/TestUserWithTwoFlags.java b/jmix-security/security/src/test/java/test_support/entity/TestUserWithTwoFlags.java
new file mode 100644
index 0000000000..6816c4dc5d
--- /dev/null
+++ b/jmix-security/security/src/test/java/test_support/entity/TestUserWithTwoFlags.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 test_support.entity;
+
+import io.jmix.core.metamodel.annotation.JmixEntity;
+import io.jmix.security.user.PasswordChangeRequired;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+
+@Entity(name = "test_UserWithTwoFlags")
+@JmixEntity
+@Table(name = "TEST_USER_WITH_TWO_FLAGS")
+public class TestUserWithTwoFlags extends BaseEntity {
+
+ @PasswordChangeRequired
+ @Column(name = "FLAG_ONE")
+ private Boolean flagOne = false;
+
+ @PasswordChangeRequired
+ @Column(name = "FLAG_TWO")
+ private Boolean flagTwo = false;
+}
diff --git a/jmix-security/security/src/test/java/test_support/entity/TestUserWithWrongFieldType.java b/jmix-security/security/src/test/java/test_support/entity/TestUserWithWrongFieldType.java
new file mode 100644
index 0000000000..f9938f6589
--- /dev/null
+++ b/jmix-security/security/src/test/java/test_support/entity/TestUserWithWrongFieldType.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 test_support.entity;
+
+import io.jmix.core.metamodel.annotation.JmixEntity;
+import io.jmix.security.user.PasswordChangeRequired;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+
+@Entity(name = "test_UserWithWrongFieldType")
+@JmixEntity
+@Table(name = "TEST_USER_WITH_WRONG_FIELD_TYPE")
+public class TestUserWithWrongFieldType extends BaseEntity {
+
+ @Column(name = "USERNAME")
+ private String username;
+
+ @PasswordChangeRequired
+ @Column(name = "FLAG_AS_STRING")
+ private String flagAsString;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getFlagAsString() {
+ return flagAsString;
+ }
+
+ public void setFlagAsString(String flagAsString) {
+ this.flagAsString = flagAsString;
+ }
+}
diff --git a/jmix-security/security/src/test/java/test_support/entity/TestUserWithoutFlag.java b/jmix-security/security/src/test/java/test_support/entity/TestUserWithoutFlag.java
new file mode 100644
index 0000000000..6898c476ae
--- /dev/null
+++ b/jmix-security/security/src/test/java/test_support/entity/TestUserWithoutFlag.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * 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 test_support.entity;
+
+import io.jmix.core.metamodel.annotation.JmixEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+
+@Entity(name = "test_UserWithoutFlag")
+@JmixEntity
+@Table(name = "TEST_USER_WITHOUT_FLAG")
+public class TestUserWithoutFlag extends BaseEntity {
+
+ @Column(name = "USERNAME")
+ private String username;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+}
diff --git a/jmix-templates/content/project/application-kotlin/src/main/kotlin/${project_rootPath}/entity/User.kt b/jmix-templates/content/project/application-kotlin/src/main/kotlin/${project_rootPath}/entity/User.kt
index 768e355229..d65de55649 100644
--- a/jmix-templates/content/project/application-kotlin/src/main/kotlin/${project_rootPath}/entity/User.kt
+++ b/jmix-templates/content/project/application-kotlin/src/main/kotlin/${project_rootPath}/entity/User.kt
@@ -8,6 +8,7 @@ import io.jmix.core.metamodel.annotation.DependsOnProperties
import io.jmix.core.metamodel.annotation.InstanceName
import io.jmix.core.metamodel.annotation.JmixEntity
import io.jmix.security.authentication.JmixUserDetails
+import io.jmix.security.user.PasswordChangeRequired
import jakarta.persistence.*
import jakarta.validation.constraints.Email
import org.springframework.security.core.GrantedAuthority
@@ -56,6 +57,10 @@ open class User : JmixUserDetails, HasTimeZone {
@get:JvmName("getTimeZoneId_")
var timeZoneId: String? = null
+ @PasswordChangeRequired
+ @Column(name = "PASSWORD_CHANGE_REQUIRED")
+ var passwordChangeRequired: Boolean? = false
+
@Transient
private var userAuthorities: Collection = emptyList()
diff --git a/jmix-templates/content/project/application-kotlin/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml b/jmix-templates/content/project/application-kotlin/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml
index 7788c05590..2efdc8d53b 100644
--- a/jmix-templates/content/project/application-kotlin/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml
+++ b/jmix-templates/content/project/application-kotlin/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml
@@ -25,6 +25,7 @@
+
diff --git a/jmix-templates/content/project/application-kotlin/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties b/jmix-templates/content/project/application-kotlin/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties
index 328a35f23f..e8cd3847a1 100644
--- a/jmix-templates/content/project/application-kotlin/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties
+++ b/jmix-templates/content/project/application-kotlin/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties
@@ -9,6 +9,7 @@ ${project_rootPackage}.entity/User.password=Password
${project_rootPackage}.entity/User.email=Email
${project_rootPackage}.entity/User.timeZoneId=Time zone
${project_rootPackage}.entity/User.active=Active
+${project_rootPackage}.entity/User.passwordChangeRequired=Password change required
${project_rootPackage}.entity/User.version=Version
${project_rootPackage}.view.main/MainView.title=${project_projectPrintableName}
diff --git a/jmix-templates/content/project/application-tabmod/src/main/java/${project_rootPath}/entity/User.java b/jmix-templates/content/project/application-tabmod/src/main/java/${project_rootPath}/entity/User.java
index 2c5a451b9f..ca87cb7a25 100644
--- a/jmix-templates/content/project/application-tabmod/src/main/java/${project_rootPath}/entity/User.java
+++ b/jmix-templates/content/project/application-tabmod/src/main/java/${project_rootPath}/entity/User.java
@@ -8,6 +8,7 @@
import io.jmix.core.metamodel.annotation.InstanceName;
import io.jmix.core.metamodel.annotation.JmixEntity;
import io.jmix.security.authentication.JmixUserDetails;
+import io.jmix.security.user.PasswordChangeRequired;
import org.springframework.security.core.GrantedAuthority;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
@@ -55,6 +56,10 @@ public class User implements JmixUserDetails, HasTimeZone {
@Column(name = "TIME_ZONE_ID")
private String timeZoneId;
+ @PasswordChangeRequired
+ @Column(name = "PASSWORD_CHANGE_REQUIRED")
+ private Boolean passwordChangeRequired = false;
+
@Transient
private Collection extends GrantedAuthority> authorities;
@@ -123,6 +128,14 @@ public void setLastName(final String lastName) {
this.lastName = lastName;
}
+ public Boolean getPasswordChangeRequired() {
+ return passwordChangeRequired;
+ }
+
+ public void setPasswordChangeRequired(final Boolean passwordChangeRequired) {
+ this.passwordChangeRequired = passwordChangeRequired;
+ }
+
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities != null ? authorities : Collections.emptyList();
diff --git a/jmix-templates/content/project/application-tabmod/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml b/jmix-templates/content/project/application-tabmod/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml
index 7788c05590..2efdc8d53b 100644
--- a/jmix-templates/content/project/application-tabmod/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml
+++ b/jmix-templates/content/project/application-tabmod/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml
@@ -25,6 +25,7 @@
+
diff --git a/jmix-templates/content/project/application-tabmod/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties b/jmix-templates/content/project/application-tabmod/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties
index 328a35f23f..e8cd3847a1 100644
--- a/jmix-templates/content/project/application-tabmod/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties
+++ b/jmix-templates/content/project/application-tabmod/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties
@@ -9,6 +9,7 @@ ${project_rootPackage}.entity/User.password=Password
${project_rootPackage}.entity/User.email=Email
${project_rootPackage}.entity/User.timeZoneId=Time zone
${project_rootPackage}.entity/User.active=Active
+${project_rootPackage}.entity/User.passwordChangeRequired=Password change required
${project_rootPackage}.entity/User.version=Version
${project_rootPackage}.view.main/MainView.title=${project_projectPrintableName}
diff --git a/jmix-templates/content/project/application/src/main/java/${project_rootPath}/entity/User.java b/jmix-templates/content/project/application/src/main/java/${project_rootPath}/entity/User.java
index 2c5a451b9f..ca87cb7a25 100644
--- a/jmix-templates/content/project/application/src/main/java/${project_rootPath}/entity/User.java
+++ b/jmix-templates/content/project/application/src/main/java/${project_rootPath}/entity/User.java
@@ -8,6 +8,7 @@
import io.jmix.core.metamodel.annotation.InstanceName;
import io.jmix.core.metamodel.annotation.JmixEntity;
import io.jmix.security.authentication.JmixUserDetails;
+import io.jmix.security.user.PasswordChangeRequired;
import org.springframework.security.core.GrantedAuthority;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
@@ -55,6 +56,10 @@ public class User implements JmixUserDetails, HasTimeZone {
@Column(name = "TIME_ZONE_ID")
private String timeZoneId;
+ @PasswordChangeRequired
+ @Column(name = "PASSWORD_CHANGE_REQUIRED")
+ private Boolean passwordChangeRequired = false;
+
@Transient
private Collection extends GrantedAuthority> authorities;
@@ -123,6 +128,14 @@ public void setLastName(final String lastName) {
this.lastName = lastName;
}
+ public Boolean getPasswordChangeRequired() {
+ return passwordChangeRequired;
+ }
+
+ public void setPasswordChangeRequired(final Boolean passwordChangeRequired) {
+ this.passwordChangeRequired = passwordChangeRequired;
+ }
+
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities != null ? authorities : Collections.emptyList();
diff --git a/jmix-templates/content/project/application/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml b/jmix-templates/content/project/application/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml
index 7788c05590..2efdc8d53b 100644
--- a/jmix-templates/content/project/application/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml
+++ b/jmix-templates/content/project/application/src/main/resources/${project_rootPath}/liquibase/changelog/010-init-user.xml
@@ -25,6 +25,7 @@
+
diff --git a/jmix-templates/content/project/application/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties b/jmix-templates/content/project/application/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties
index 328a35f23f..e8cd3847a1 100644
--- a/jmix-templates/content/project/application/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties
+++ b/jmix-templates/content/project/application/src/main/resources/${project_rootPath}/messages_${current_locale.code}.properties
@@ -9,6 +9,7 @@ ${project_rootPackage}.entity/User.password=Password
${project_rootPackage}.entity/User.email=Email
${project_rootPackage}.entity/User.timeZoneId=Time zone
${project_rootPackage}.entity/User.active=Active
+${project_rootPackage}.entity/User.passwordChangeRequired=Password change required
${project_rootPackage}.entity/User.version=Version
${project_rootPackage}.view.main/MainView.title=${project_projectPrintableName}
diff --git a/jmix-translations/content/io/jmix/securityflowui/messages.properties b/jmix-translations/content/io/jmix/securityflowui/messages.properties
index 8afe5e1edc..1938ec964d 100644
--- a/jmix-translations/content/io/jmix/securityflowui/messages.properties
+++ b/jmix-translations/content/io/jmix/securityflowui/messages.properties
@@ -213,6 +213,7 @@ io.jmix.securityflowui.view.changepassword/changePasswordView.passwordsDoNotMatc
io.jmix.securityflowui.view.changepassword/changePasswordView.passwordChanged=Password successfully changed
io.jmix.securityflowui.view.changepassword/changePasswordView.emptyUsernameMessage=Dialog cannot be opened without username
io.jmix.securityflowui.view.changepassword/changePasswordView.title=Change password for [%s]
+io.jmix.securityflowui.view.changepassword/changePasswordView.forcedTitle=Password change required
io.jmix.securityflowui.view.resetpassword/resetPasswordView.resetPasswordsTitle = Generate new passwords for selected users
io.jmix.securityflowui.view.resetpassword/resetPasswordView.resetSinglePasswordTitle = Generate a new password for [%s]
@@ -230,6 +231,7 @@ io.jmix.securityflowui.view.resetpassword/resetPasswordView.cancelDialog.title =
io.jmix.securityflowui.view.resetpassword/resetPasswordView.cancelDialog.message = Do you want to cancel passwords generation?
io.jmix.securityflowui.view.resetpassword/resetPasswordView.exportActionButton.text = Excel
io.jmix.securityflowui.view.resetpassword/resetPasswordView.copyButton.tooltip = Copy to clipboard
+io.jmix.securityflowui.view.resetpassword/resetPasswordView.requireChangeAtNextLogon = Require change at next logon
io.jmix.securityflowui.view.resetpassword.model/UserPasswordValue.username = Username
io.jmix.securityflowui.view.resetpassword.model/UserPasswordValue.password = Password
diff --git a/jmix-translations/content/io/jmix/securityflowui/messages_ru.properties b/jmix-translations/content/io/jmix/securityflowui/messages_ru.properties
index 774b2f5b8b..8843f41017 100644
--- a/jmix-translations/content/io/jmix/securityflowui/messages_ru.properties
+++ b/jmix-translations/content/io/jmix/securityflowui/messages_ru.properties
@@ -213,6 +213,7 @@ io.jmix.securityflowui.view.changepassword/changePasswordView.passwordsDoNotMatc
io.jmix.securityflowui.view.changepassword/changePasswordView.passwordChanged=Пароль успешно изменен
io.jmix.securityflowui.view.changepassword/changePasswordView.emptyUsernameMessage=Диалог не может быть открыт без имени пользователя
io.jmix.securityflowui.view.changepassword/changePasswordView.title=Сменить пароль для пользователя [%s]
+io.jmix.securityflowui.view.changepassword/changePasswordView.forcedTitle=Обязательная смена пароля
io.jmix.securityflowui.view.resetpassword/resetPasswordView.resetPasswordsTitle = Генерация нового пароля выбранным пользователям
io.jmix.securityflowui.view.resetpassword/resetPasswordView.resetSinglePasswordTitle = Генерация нового пароля для [%s]
@@ -230,6 +231,7 @@ io.jmix.securityflowui.view.resetpassword/resetPasswordView.cancelDialog.title =
io.jmix.securityflowui.view.resetpassword/resetPasswordView.cancelDialog.message = Вы действительно хотите отменить генерацию паролей?
io.jmix.securityflowui.view.resetpassword/resetPasswordView.exportActionButton.text = Excel
io.jmix.securityflowui.view.resetpassword/resetPasswordView.copyButton.tooltip = Копировать в буфер обмена
+io.jmix.securityflowui.view.resetpassword/resetPasswordView.requireChangeAtNextLogon = Требовать смены пароля при следующем входе в систему
io.jmix.securityflowui.view.resetpassword.model/UserPasswordValue.username = Имя пользователя
io.jmix.securityflowui.view.resetpassword.model/UserPasswordValue.password = Пароль