]> git.mxchange.org Git - jjobs-war.git/commitdiff
Continued: (please cherry-pick)
authorRoland Häder <roland@mxchange.org>
Wed, 3 Aug 2016 14:39:23 +0000 (16:39 +0200)
committerRoland Haeder <roland@mxchange.org>
Sun, 7 Aug 2016 10:25:29 +0000 (12:25 +0200)
- rewrote fetching context parameter with protected methods to have this code encapsulated
- used this for feature controller
- added new user password-change controller (+ interface) to change user's password
- used it in template to avoid monolithic super controllers ...
- added missing i18n strings
- used password-change event for updating user's password history
- added new context parameter for "change_user_password" feature
- added new context parameter for maximum checked passwords
- fixed redirect outcomes

Signed-off-by: Roland Häder <roland@mxchange.org>
13 files changed:
nbproject/faces-config.NavData
src/java/org/mxchange/jjobs/beans/BaseJobsController.java
src/java/org/mxchange/jjobs/beans/features/JobsFeatureWebApplicationBean.java [new file with mode: 0644]
src/java/org/mxchange/jjobs/beans/login/JobsUserLoginWebSessionBean.java
src/java/org/mxchange/jjobs/beans/login/JobsUserLoginWebSessionController.java
src/java/org/mxchange/jjobs/beans/user/JobsUserWebSessionBean.java
src/java/org/mxchange/jjobs/beans/user/JobsUserWebSessionController.java
src/java/org/mxchange/jjobs/beans/user/password/JobsUserPasswordWebRequestBean.java [new file with mode: 0644]
src/java/org/mxchange/jjobs/beans/user/password/JobsUserPasswordWebRequestController.java [new file with mode: 0644]
src/java/org/mxchange/localization/bundle_de_DE.properties
src/java/org/mxchange/localization/bundle_en_US.properties
web/WEB-INF/web.xml
web/user/login_change_password.xhtml

index 28fad671b29c447f6e6e672e4244ea2a35a5fcc1..00c68c878797b8c07893fa6ff8c0b41f4efd1a05 100644 (file)
@@ -2,69 +2,70 @@
 <Scene Scope="Project" version="2">
     <Scope Scope="Faces Configuration Only"/>
     <Scope Scope="Project">
-        <Node id="admin/cellphone/admin_contact_cellphone_list.xhtml" x="1150" y="900" zoom="true"/>
-        <Node id="admin/contact/admin_contact_show.xhtml" x="900" y="750" zoom="true"/>
-        <Node id="privacy.xhtml" x="900" y="150" zoom="true"/>
-        <Node id="guest/user/show_addressbook.xhtml" x="1900" y="300" zoom="true"/>
-        <Node id="admin/mobile_provider/admin_mobile_provider_show.xhtml" x="900" y="900" zoom="true"/>
-        <Node id="admin/user/admin_user_show.xhtml" x="400" y="600" zoom="true"/>
-        <Node id="admin/index.xhtml" x="400" y="150" zoom="true"/>
-        <Node id="user/login_own_addressbooks.xhtml" x="650" y="750" zoom="true"/>
-        <Node id="user/login_user_data_saved.xhtml" x="650" y="1050" zoom="true"/>
-        <Node id="admin/admin_logout.xhtml" x="1400" y="150" zoom="true"/>
-        <Node id="user/login_data_saved.xhtml" x="2150" y="150" zoom="true"/>
-        <Node id="admin/user/admin_user_unlock.xhtml" x="150" y="750" zoom="true"/>
-        <Node id="guest/user/user_list.xhtml" x="650" y="300" zoom="true"/>
-        <Node id="index.xhtml" x="400" y="1050" zoom="true"/>
-        <Node id="user/login_edit_user_data.xhtml" x="1900" y="600" zoom="true"/>
-        <Node id="*" x="1900" y="150" zoom="true"/>
-        <Node id="admin/admin_category_delete.xhtml" x="1150" y="600" zoom="true"/>
-        <Node id="user/login_index.xhtml" x="1150" y="150" zoom="true"/>
-        <Node id="admin/cellphone/admin_contact_cellphone_edit.xhtml" x="1650" y="600" zoom="true"/>
-        <Node id="guest/user/lost_passwd.xhtml" x="650" y="1200" zoom="true"/>
-        <Node id="guest/user/register_done.xhtml" x="1150" y="300" zoom="true"/>
-        <Node id="guest/user/show_addressbook_entries.xhtml" x="1650" y="450" zoom="true"/>
-        <Node id="user/login.xhtml" x="900" y="300" zoom="true"/>
-        <Node id="admin/cellphone/admin_contact_cellphone_unlink.xhtml" x="1650" y="300" zoom="true"/>
-        <Node id="admin/country/admin_country_list.xhtml" x="650" y="900" zoom="true"/>
-        <Node id="login/login_edit_user_data.xhtml" x="2400" y="150" zoom="true"/>
-        <Node id="guest/user/login_error.xhtml" x="400" y="1350" zoom="true"/>
-        <Node id="admin/user/admin_user_export.xhtml" x="400" y="900" zoom="true"/>
-        <Node id="admin/mobile_provider/admin_mobile_provider_delete.xhtml" x="1650" y="150" zoom="true"/>
-        <Node id="guest/user/resend_done.xhtml" x="900" y="1050" zoom="true"/>
-        <Node id="user/login_change_password.xhtml" x="650" y="450" zoom="true"/>
-        <Node id="admin/mobile_provider/admin_mobile_provider_list.xhtml" x="2150" y="450" zoom="true"/>
-        <Node id="admin/contact/admin_contact_export.xhtml" x="1400" y="300" zoom="true"/>
-        <Node id="guest/user/user_profile.xhtml" x="650" y="150" zoom="true"/>
-        <Node id="guest/user/confirm_account.xhtml" x="1400" y="900" zoom="true"/>
-        <Node id="admin/user/admin_user_delete.xhtml" x="1400" y="450" zoom="true"/>
-        <Node id="admin/cellphone/admin_contact_cellphone_delete.xhtml" x="1150" y="750" zoom="true"/>
-        <Node id="exception.xhtml" x="900" y="1200" zoom="true"/>
-        <Node id="login/login_index.xhtml" x="150" y="450" zoom="true"/>
-        <Node id="guest/user/register_page2.xhtml" x="150" y="150" zoom="true"/>
-        <Node id="admin/user/admin_user_edit.xhtml" x="900" y="600" zoom="true"/>
-        <Node id="terms.xhtml" x="2400" y="300" zoom="true"/>
-        <Node id="admin/contact/admin_contact_edit.xhtml" x="1150" y="450" zoom="true"/>
-        <Node id="admin/contact/admin_contact_list.xhtml" x="400" y="1200" zoom="true"/>
-        <Node id="admin/user/admin_user_list.xhtml" x="1900" y="450" zoom="true"/>
-        <Node id="admin/admin_user_add.xhtml" x="1150" y="1050" zoom="true"/>
-        <Node id="admin/country/admin_country_delete.xhtml" x="150" y="300" zoom="true"/>
-        <Node id="user/login_change_personal_data.xhtml" x="150" y="1050" zoom="true"/>
-        <Node id="logout.xhtml" x="400" y="450" zoom="true"/>
-        <Node id="admin/country/admin_country_edit.xhtml" x="150" y="1200" zoom="true"/>
-        <Node id="user/login_add_addressbook.xhtml" x="150" y="900" zoom="true"/>
-        <Node id="guest/user/resend_link.xhtml" x="150" y="1350" zoom="true"/>
-        <Node id="user/login_contact_data_saved.xhtml" x="400" y="300" zoom="true"/>
-        <Node id="user/user_profile.xhtml" x="650" y="600" zoom="true"/>
-        <Node id="admin/contact/admin_contact_delete.xhtml" x="2150" y="300" zoom="true"/>
-        <Node id="imprint.xhtml" x="900" y="450" zoom="true"/>
-        <Node id="admin/mobile_provider/admin_mobile_provider_edit.xhtml" x="2650" y="150" zoom="true"/>
-        <Node id="admin/cellphone/admin_contact_cellphone_show.xhtml" x="150" y="1500" zoom="true"/>
-        <Node id="admin/admin_product_delete.xhtml" x="1400" y="600" zoom="true"/>
-        <Node id="guest/user/login.xhtml" x="1650" y="750" zoom="true"/>
-        <Node id="guest/user/register.xhtml" x="1400" y="750" zoom="true"/>
-        <Node id="user/login_change_email_address.xhtml" x="400" y="750" zoom="true"/>
-        <Node id="user/login/login_data_saved.xhtml" x="150" y="600" zoom="true"/>
+        <Node id="admin/cellphone/admin_contact_cellphone_list.xhtml" x="150" y="1050" zoom="true"/>
+        <Node id="user/login_logout.xhtml" x="1650" y="300" zoom="true"/>
+        <Node id="admin/contact/admin_contact_show.xhtml" x="400" y="750" zoom="true"/>
+        <Node id="privacy.xhtml" x="650" y="900" zoom="true"/>
+        <Node id="admin/mobile_provider/admin_mobile_provider_show.xhtml" x="650" y="750" zoom="true"/>
+        <Node id="guest/user/show_addressbook.xhtml" x="650" y="1350" zoom="true"/>
+        <Node id="admin/user/admin_user_show.xhtml" x="900" y="600" zoom="true"/>
+        <Node id="admin/index.xhtml" x="1150" y="450" zoom="true"/>
+        <Node id="user/login_own_addressbooks.xhtml" x="900" y="300" zoom="true"/>
+        <Node id="user/login_user_data_saved.xhtml" x="400" y="900" zoom="true"/>
+        <Node id="admin/admin_logout.xhtml" x="400" y="1200" zoom="true"/>
+        <Node id="user/login_data_saved.xhtml" x="150" y="450" zoom="true"/>
+        <Node id="admin/user/admin_user_unlock.xhtml" x="1400" y="750" zoom="true"/>
+        <Node id="guest/user/user_list.xhtml" x="1150" y="750" zoom="true"/>
+        <Node id="index.xhtml" x="1150" y="900" zoom="true"/>
+        <Node id="user/login_edit_user_data.xhtml" x="1900" y="150" zoom="true"/>
+        <Node id="*" x="650" y="450" zoom="true"/>
+        <Node id="admin/admin_category_delete.xhtml" x="900" y="750" zoom="true"/>
+        <Node id="admin/cellphone/admin_contact_cellphone_edit.xhtml" x="1150" y="600" zoom="true"/>
+        <Node id="user/login_index.xhtml" x="1650" y="450" zoom="true"/>
+        <Node id="guest/user/lost_passwd.xhtml" x="1900" y="450" zoom="true"/>
+        <Node id="guest/user/register_done.xhtml" x="1650" y="150" zoom="true"/>
+        <Node id="guest/user/show_addressbook_entries.xhtml" x="400" y="1350" zoom="true"/>
+        <Node id="user/login.xhtml" x="150" y="1350" zoom="true"/>
+        <Node id="login/login_edit_user_data.xhtml" x="150" y="300" zoom="true"/>
+        <Node id="admin/cellphone/admin_contact_cellphone_unlink.xhtml" x="2400" y="150" zoom="true"/>
+        <Node id="admin/country/admin_country_list.xhtml" x="1900" y="600" zoom="true"/>
+        <Node id="guest/user/login_error.xhtml" x="650" y="150" zoom="true"/>
+        <Node id="admin/user/admin_user_export.xhtml" x="900" y="1050" zoom="true"/>
+        <Node id="admin/mobile_provider/admin_mobile_provider_delete.xhtml" x="400" y="300" zoom="true"/>
+        <Node id="guest/user/resend_done.xhtml" x="1150" y="300" zoom="true"/>
+        <Node id="user/login_change_password.xhtml" x="1400" y="600" zoom="true"/>
+        <Node id="admin/mobile_provider/admin_mobile_provider_list.xhtml" x="150" y="750" zoom="true"/>
+        <Node id="admin/contact/admin_contact_export.xhtml" x="1900" y="300" zoom="true"/>
+        <Node id="guest/user/user_profile.xhtml" x="1150" y="1050" zoom="true"/>
+        <Node id="guest/user/confirm_account.xhtml" x="2150" y="150" zoom="true"/>
+        <Node id="exception.xhtml" x="650" y="600" zoom="true"/>
+        <Node id="admin/user/admin_user_delete.xhtml" x="1650" y="750" zoom="true"/>
+        <Node id="admin/cellphone/admin_contact_cellphone_delete.xhtml" x="900" y="1200" zoom="true"/>
+        <Node id="login/login_index.xhtml" x="2150" y="300" zoom="true"/>
+        <Node id="guest/user/register_page2.xhtml" x="1150" y="150" zoom="true"/>
+        <Node id="admin/user/admin_user_edit.xhtml" x="900" y="900" zoom="true"/>
+        <Node id="terms.xhtml" x="900" y="150" zoom="true"/>
+        <Node id="admin/contact/admin_contact_list.xhtml" x="400" y="450" zoom="true"/>
+        <Node id="admin/contact/admin_contact_edit.xhtml" x="150" y="600" zoom="true"/>
+        <Node id="admin/user/admin_user_list.xhtml" x="1400" y="450" zoom="true"/>
+        <Node id="admin/admin_user_add.xhtml" x="650" y="1050" zoom="true"/>
+        <Node id="admin/country/admin_country_delete.xhtml" x="2400" y="300" zoom="true"/>
+        <Node id="user/login_change_personal_data.xhtml" x="1400" y="300" zoom="true"/>
+        <Node id="logout.xhtml" x="400" y="150" zoom="true"/>
+        <Node id="admin/country/admin_country_edit.xhtml" x="400" y="1050" zoom="true"/>
+        <Node id="guest/user/resend_link.xhtml" x="150" y="150" zoom="true"/>
+        <Node id="user/login_add_addressbook.xhtml" x="1400" y="150" zoom="true"/>
+        <Node id="user/login_contact_data_saved.xhtml" x="400" y="600" zoom="true"/>
+        <Node id="user/user_profile.xhtml" x="150" y="1200" zoom="true"/>
+        <Node id="admin/contact/admin_contact_delete.xhtml" x="1400" y="900" zoom="true"/>
+        <Node id="imprint.xhtml" x="1650" y="600" zoom="true"/>
+        <Node id="admin/mobile_provider/admin_mobile_provider_edit.xhtml" x="2150" y="450" zoom="true"/>
+        <Node id="admin/cellphone/admin_contact_cellphone_show.xhtml" x="2650" y="150" zoom="true"/>
+        <Node id="admin/admin_product_delete.xhtml" x="650" y="1200" zoom="true"/>
+        <Node id="guest/user/login.xhtml" x="150" y="1500" zoom="true"/>
+        <Node id="guest/user/register.xhtml" x="900" y="450" zoom="true"/>
+        <Node id="user/login_change_email_address.xhtml" x="150" y="900" zoom="true"/>
+        <Node id="user/login/login_data_saved.xhtml" x="650" y="300" zoom="true"/>
     </Scope>
     <Scope Scope="All Faces Configurations"/>
 </Scene>
index 6bb15383995f88b13107a329b7d17f04206b5eb3..229c64bbe6acc1d6fc3a8ddf13081bdc052f0be9 100644 (file)
@@ -75,4 +75,45 @@ public abstract class BaseJobsController implements Serializable {
                FacesContext.getCurrentInstance().addMessage(clientId, new FacesMessage(cause.getMessage()));
        }
 
+       /**
+        * Returns given property key or throws an exception if not found.
+        * <p>
+        * @param parameterKey Property key
+        * <p>
+        * @return Property value
+        * <p>
+        * @throws NullPointerException If given key is not found
+        * @throws NumberFormatException If no number is given in context parameter
+        */
+       protected int getIntegerContextParameter (final String parameterKey) throws NullPointerException, NumberFormatException {
+               // Get context parameter
+               Integer contextValue = Integer.parseInt(this.getStringContextParameter(parameterKey));
+
+               // Return it
+               return contextValue;
+       }
+
+       /**
+        * Returns given property key or throws an exception if not found.
+        * <p>
+        * @param parameterKey Property key
+        * <p>
+        * @return Property value
+        * <p>
+        * @throws NullPointerException If given key is not found
+        */
+       protected String getStringContextParameter (final String parameterKey) throws NullPointerException {
+               // Get context parameter
+               String contextValue = FacesContext.getCurrentInstance().getExternalContext().getInitParameter(parameterKey);
+
+               // Is it null?
+               if (null == contextValue) {
+                       // Throw NPE
+                       throw new NullPointerException("parameterKey=" + parameterKey + " is not set.");
+               }
+
+               // Return it
+               return contextValue;
+       }
+
 }
diff --git a/src/java/org/mxchange/jjobs/beans/features/JobsFeatureWebApplicationBean.java b/src/java/org/mxchange/jjobs/beans/features/JobsFeatureWebApplicationBean.java
new file mode 100644 (file)
index 0000000..6727e65
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 Roland Haeder
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.mxchange.jjobs.beans.features;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Named;
+import org.mxchange.jjobs.beans.BaseJobsController;
+
+/**
+ * A feature bean
+ * <p>
+ * @author Roland Haeder<rhaeder@cho-time.de>
+ */
+@Named ("featureController")
+@ApplicationScoped
+public class JobsFeatureWebApplicationBean extends BaseJobsController implements JobsFeaturesWebApplicationController {
+
+       /**
+        * Serial number
+        */
+       private static final long serialVersionUID = 64_237_512_690_168_674L;
+
+       @Override
+       public boolean isFeatureEnabled (final String feature) {
+               // The parameter must be set
+               if (null == feature) {
+                       // Throw NPE
+                       throw new NullPointerException("feature is null"); //NOI18N
+               } else if (feature.isEmpty()) {
+                       // Is empty
+                       throw new IllegalArgumentException("feature is empty"); //NOI18N
+               }
+
+               // Default is not enabled
+               boolean isEnabled = false;
+
+               // Try it as an NPE may come
+               try {
+                       // Get value from property
+                       String value = this.getStringContextParameter(String.format("is_feature_%s_enabled", feature)); //NOI18N
+
+                       // Is it set?
+                       isEnabled = (value.toLowerCase().equals("true")); //NOI18N
+               } catch (final NullPointerException ex) {
+                       // Ignored
+               }
+
+               // Return value
+               return isEnabled;
+       }
+
+}
index 41d2d2e5e2f96b71fdb19d476bbe1f5eddb694f1..a0c6aba1acdcd76a8860ea6456e7f361d9f9f6d5 100644 (file)
  */
 package org.mxchange.jjobs.beans.login;
 
+import java.text.MessageFormat;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import javax.enterprise.context.SessionScoped;
 import javax.enterprise.event.Event;
+import javax.enterprise.event.Observes;
 import javax.enterprise.inject.Any;
+import javax.faces.context.FacesContext;
 import javax.faces.view.facelets.FaceletException;
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -34,6 +37,7 @@ import org.mxchange.jusercore.container.login.LoginContainer;
 import org.mxchange.jusercore.container.login.UserLoginContainer;
 import org.mxchange.jusercore.events.login.UserLoggedInEvent;
 import org.mxchange.jusercore.events.login.UserLoginEvent;
+import org.mxchange.jusercore.events.user.password_change.UpdatedUserPasswordEvent;
 import org.mxchange.jusercore.exceptions.UserNotFoundException;
 import org.mxchange.jusercore.exceptions.UserPasswordMismatchException;
 import org.mxchange.jusercore.exceptions.UserStatusLockedException;
@@ -136,7 +140,46 @@ public class JobsUserLoginWebSessionBean extends BaseJobsController implements J
        }
 
        @Override
-       public String doLogin () {
+       public void afterUserUpdatedPasswordEvent (@Observes final UpdatedUserPasswordEvent event) {
+               // Check parameter
+               if (null == event) {
+                       // Throw NPE
+                       throw new NullPointerException("event is null"); //NOI18N
+               } else if (event.getPasswordHistory() == null) {
+                       // Throw NPE again
+                       throw new NullPointerException("event.passwordHistory is null"); //NOI18N
+               } else if (event.getPasswordHistory().getUserPasswordHistoryId() == null) {
+                       // ... and again
+                       throw new NullPointerException("event.passwordHistory.userPasswordHistoryId is null"); //NOI18N
+               } else if (event.getPasswordHistory().getUserPasswordHistoryId() < 1) {
+                       // Invalid value
+                       throw new IllegalArgumentException(MessageFormat.format("event.passwordHistory.userPasswordHistoryId={0} is in valid", event.getPasswordHistory().getUserPasswordHistoryId())); //NOI18N
+               }
+
+               // All fine, so update list
+               this.updatePasswordHistory(event.getPasswordHistory());
+       }
+
+       @Override
+       public String doAdminLogout () {
+               // Is a user logged-in?
+               if (this.isUserLoggedIn()) {
+                       // Call other logout
+                       return this.doUserLogout();
+               }
+
+               // Invalidate session
+               FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
+
+               // Set template type to guest
+               this.setBaseTemplatePathName(GUEST_BASE_TEMPLATE_NAME); //NOI18N
+
+               // Redirect to index
+               return "index?faces-redirect=true"; //NOI18N
+       }
+
+       @Override
+       public String doUserLogin () {
                // Get user instance
                User user = this.userController.createUserLogin();
 
@@ -232,6 +275,35 @@ public class JobsUserLoginWebSessionBean extends BaseJobsController implements J
                return this.getLoggedInUser().getUserProfileMode().equals(ProfileMode.INVISIBLE);
        }
 
+       @Override
+       public boolean isPasswordInHistory (final String userPassword) {
+               // Default is not found
+               boolean isPasswordInHistory = false;
+
+               // Init variables
+               int count = 1;
+               int maxEntries = this.getIntegerContextParameter("max_user_password_history"); //NOI18N
+
+               // Check all passwords
+               for (final PasswordHistory entry : this.getUserPasswordHistory()) {
+                       // Is password the same?
+                       if (UserUtils.ifPasswordMatches(userPassword, entry.getUserPasswordHistoryUser())) {
+                               // Yes, found it
+                               isPasswordInHistory = true;
+                               break;
+                       } else if (count == maxEntries) {
+                               // Maximum reached
+                               break;
+                       }
+
+                       // Count up
+                       count++;
+               }
+
+               // Return status
+               return isPasswordInHistory;
+       }
+
        @Override
        public boolean isUserLoggedIn () {
                // Trace message
@@ -246,4 +318,49 @@ public class JobsUserLoginWebSessionBean extends BaseJobsController implements J
                return this.userLoggedIn;
        }
 
+       /**
+        * Clears this bean
+        */
+       private void clear () {
+               // Clear all fields
+               this.setCurrentPassword(null);
+       }
+
+       /**
+        * Updates password history by adding given entry to it as long as it is not
+        * there.
+        * <p>
+        * @param passwordHistory Password history entry
+        */
+       private void updatePasswordHistory (final PasswordHistory passwordHistory) {
+               if (null == passwordHistory) {
+                       // Throw NPE
+                       throw new NullPointerException("passwordHistory is null"); //NOI18N
+               } else if (passwordHistory.getUserPasswordHistoryId() == null) {
+                       // Throw NPE again
+                       throw new NullPointerException("passwordHistory.userPasswordHistoryId is null"); //NOI18N
+               } else if (passwordHistory.getUserPasswordHistoryId() < 1) {
+                       // Invalid id
+                       throw new IllegalArgumentException(MessageFormat.format("passwordHistory.userPasswordHistoryId={0} is not valid.", passwordHistory.getUserPasswordHistoryId())); //NOI18N
+               }
+
+               // Is it there?
+               if (this.userPasswordHistory.contains(passwordHistory)) {
+                       // Excact copy found
+                       return;
+               }
+
+               // Check all entries
+               for (final PasswordHistory entry : this.userPasswordHistory) {
+                       // Is same id number?
+                       if (Objects.equals(entry.getUserPasswordHistoryId(), passwordHistory.getUserPasswordHistoryId())) {
+                               // Found it
+                               return;
+                       }
+               }
+
+               // Not found, so add it
+               this.userPasswordHistory.add(passwordHistory);
+       }
+
 }
index fe96c0a0020450c7bad515d3deff54271f70af4a..c99df24576cd212826c78da362e599f284ff003b 100644 (file)
@@ -19,6 +19,7 @@ package org.mxchange.jjobs.beans.login;
 import java.io.Serializable;
 import java.util.List;
 import javax.ejb.Local;
+import org.mxchange.jusercore.events.user.password_change.UpdatedUserPasswordEvent;
 import org.mxchange.jusercore.model.user.User;
 import org.mxchange.jusercore.model.user.password_history.PasswordHistory;
 
@@ -30,6 +31,23 @@ import org.mxchange.jusercore.model.user.password_history.PasswordHistory;
 @Local
 public interface JobsUserLoginWebSessionController extends Serializable {
 
+       /**
+        * Method being call after user's password has been updated (and history
+        * entry has been created).
+        * <p>
+        * @param event Event being observed
+        */
+       void afterUserUpdatedPasswordEvent (final UpdatedUserPasswordEvent event);
+
+       /**
+        * Checks whether given clear-text password is in user's password history.
+        * <p>
+        * @param userPassword Clear-text password
+        * <p>
+        * @return Whether clear-text password is in user's password history
+        */
+       boolean isPasswordInHistory (final String userPassword);
+
        /**
         * Getter for template type
         * <p>
index 65cbfbbfed553c1d9b56de68bf6c430c3110ecc4..6cd1f020640bb6d84b0c6e19f7ed84607ec37401 100644 (file)
@@ -45,6 +45,7 @@ import org.mxchange.jusercore.events.confirmation.UserConfirmedAccountEvent;
 import org.mxchange.jusercore.events.login.UserLoggedInEvent;
 import org.mxchange.jusercore.events.registration.UserRegisteredEvent;
 import org.mxchange.jusercore.events.user.add.AdminAddedUserEvent;
+import org.mxchange.jusercore.events.user.password_change.UpdatedUserPasswordEvent;
 import org.mxchange.jusercore.events.user.update.AdminUpdatedUserDataEvent;
 import org.mxchange.jusercore.events.user.update.UpdatedUserPersonalDataEvent;
 import org.mxchange.jusercore.events.user.update.UserUpdatedPersonalDataEvent;
@@ -378,6 +379,27 @@ public class JobsUserWebSessionBean extends BaseJobsController implements JobsUs
                }
        }
 
+       @Override
+       public void afterUserUpdatedPasswordEvent (@Observes final UpdatedUserPasswordEvent event) {
+               // Check parameter
+               if (null == event) {
+                       // Throw NPE
+                       throw new NullPointerException("event is null"); //NOI18N
+               } else if (event.getPasswordHistory() == null) {
+                       // Throw NPE again
+                       throw new NullPointerException("event.passwordHistory is null"); //NOI18N
+               } else if (event.getPasswordHistory().getUserPasswordHistoryId() == null) {
+                       // ... and again
+                       throw new NullPointerException("event.passwordHistory.userPasswordHistoryId is null"); //NOI18N
+               } else if (event.getPasswordHistory().getUserPasswordHistoryId() < 1) {
+                       // Invalid value
+                       throw new IllegalArgumentException(MessageFormat.format("event.passwordHistory.userPasswordHistoryId={0} is in valid", event.getPasswordHistory().getUserPasswordHistoryId())); //NOI18N
+               }
+
+               // All fine, so update list
+               this.updateList(event.getPasswordHistory().getUserPasswordHistoryUser());
+       }
+
        @Override
        public void afterUserUpdatedPersonalData (@Observes final UpdatedUserPersonalDataEvent event) {
                // Check parameter
@@ -502,6 +524,9 @@ public class JobsUserWebSessionBean extends BaseJobsController implements JobsUs
                } else if (!this.userLoginController.ifCurrentPasswordMatches()) {
                        // Password not matching
                        throw new FaceletException(new UserPasswordMismatchException(this.userLoginController.getLoggedInUser()));
+               } else if (!this.featureController.isFeatureEnabled("edit_user_data")) {
+                       // Editing is not allowed
+                       throw new IllegalStateException("User tried to edit personal data."); //NOI18N
                }
 
                // Get user instance
@@ -528,7 +553,7 @@ public class JobsUserWebSessionBean extends BaseJobsController implements JobsUs
                this.updatedPersonalDataEvent.fire(new UserUpdatedPersonalDataEvent(updatedUser));
 
                // All fine
-               return "user_data_saved"; //NOI18N
+               return "contact_data_saved"; //NOI18N
        }
 
        @Override
index ba6249832fae7ed4aec189e5c2a3037e891bad6c..c004b24d82e972a88adfc7969a27153a2939bb5e 100644 (file)
@@ -24,6 +24,7 @@ import org.mxchange.jusercore.events.confirmation.UserConfirmedAccountEvent;
 import org.mxchange.jusercore.events.login.UserLoggedInEvent;
 import org.mxchange.jusercore.events.registration.UserRegisteredEvent;
 import org.mxchange.jusercore.events.user.add.AdminAddedUserEvent;
+import org.mxchange.jusercore.events.user.password_change.UpdatedUserPasswordEvent;
 import org.mxchange.jusercore.events.user.update.AdminUpdatedUserDataEvent;
 import org.mxchange.jusercore.events.user.update.UpdatedUserPersonalDataEvent;
 import org.mxchange.jusercore.exceptions.UserEmailAddressNotFoundException;
@@ -72,6 +73,14 @@ public interface JobsUserWebSessionController extends Serializable {
         */
        void afterUserConfirmedAccount (final UserConfirmedAccountEvent event);
 
+       /**
+        * Method being call after user's password has been updated (and history
+        * entry has been created).
+        * <p>
+        * @param event Event being observed
+        */
+       void afterUserUpdatedPasswordEvent (final UpdatedUserPasswordEvent event);
+
        /**
         * Listens to fired event when user updated personal data
         * <p>
diff --git a/src/java/org/mxchange/jjobs/beans/user/password/JobsUserPasswordWebRequestBean.java b/src/java/org/mxchange/jjobs/beans/user/password/JobsUserPasswordWebRequestBean.java
new file mode 100644 (file)
index 0000000..eae3515
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2016 Cho-Time GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.mxchange.jjobs.beans.user.password;
+
+import java.util.Objects;
+import javax.enterprise.context.RequestScoped;
+import javax.enterprise.event.Event;
+import javax.enterprise.inject.Any;
+import javax.faces.view.facelets.FaceletException;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import org.mxchange.addressbook.beans.BaseAddressbookController;
+import org.mxchange.addressbook.beans.features.AddressbookFeaturesWebApplicationController;
+import org.mxchange.addressbook.beans.login.AddressbookUserLoginWebSessionController;
+import org.mxchange.jusercore.events.user.password_change.UpdatedUserPasswordEvent;
+import org.mxchange.jusercore.events.user.password_change.UserUpdatedPasswordEvent;
+import org.mxchange.jusercore.exceptions.UserNotFoundException;
+import org.mxchange.jusercore.exceptions.UserPasswordMismatchException;
+import org.mxchange.jusercore.exceptions.UserStatusLockedException;
+import org.mxchange.jusercore.exceptions.UserStatusUnconfirmedException;
+import org.mxchange.jusercore.model.user.User;
+import org.mxchange.jusercore.model.user.UserSessionBeanRemote;
+import org.mxchange.jusercore.model.user.UserUtils;
+import org.mxchange.jusercore.model.user.password_history.PasswordHistory;
+
+/**
+ * A user password (change) bean (controller)
+ * <p>
+ * @author Roland Haeder<rhaeder@cho-time.de>
+ */
+@Named ("userPasswordController")
+@RequestScoped
+public class JobsUserPasswordWebRequestBean extends BaseAddressbookController implements JobsUserPasswordWebRequestController {
+
+       /**
+        * Serial number
+        */
+       private static final long serialVersionUID = 15_267_867_367_501L;
+
+       /**
+        * Features controller
+        */
+       @Inject
+       private AddressbookFeaturesWebApplicationController featureController;
+
+       /**
+        * Remote user bean
+        */
+       private final UserSessionBeanRemote userBean;
+
+       /**
+        * Current password (for confirmation of password change)
+        */
+       private String userCurrentPassword;
+
+       /**
+        * Login bean (controller)
+        */
+       @Inject
+       private AddressbookUserLoginWebSessionController userLoginController;
+
+       /**
+        * User password (unencrypted from web form)
+        */
+       private String userPassword;
+
+       /**
+        * User password repeated (unencrypted from web form)
+        */
+       private String userPasswordRepeat;
+
+       /**
+        * Event being fired when user's password has been updated
+        */
+       @Any
+       @Inject
+       private Event<UpdatedUserPasswordEvent> userUpdatedPasswordEvent;
+
+       /**
+        * Default constructor
+        */
+       public JobsUserPasswordWebRequestBean () {
+               // Try it
+               try {
+                       // Get initial context
+                       Context context = new InitialContext();
+
+                       // Try to lookup
+                       this.userBean = (UserSessionBeanRemote) context.lookup("java:global/jlandingpage-ejb/user!org.mxchange.jusercore.model.user.UserSessionBeanRemote"); //NOI18N
+               } catch (final NamingException e) {
+                       // Throw again
+                       throw new FaceletException(e);
+               }
+       }
+
+       @Override
+       public String doChangePassword () {
+               // This method shall only be called if the user is logged-in
+               if (!this.userLoginController.isUserLoggedIn()) {
+                       // Not logged-in
+                       throw new IllegalStateException("User is not logged-in"); //NOI18N
+               } else if (!this.isRequiredChangePasswordSet()) {
+                       // Not all required fields are set
+                       throw new FaceletException("Not all required fields are set."); //NOI18N
+               } else if (!this.userLoginController.ifCurrentPasswordMatches()) {
+                       // Password not matching
+                       throw new FaceletException(new UserPasswordMismatchException(this.userLoginController.getLoggedInUser()));
+               } else if (!this.featureController.isFeatureEnabled("change_user_password")) { //NOI18N
+                       // Editing is not allowed
+                       throw new IllegalStateException("User tried to change password."); //NOI18N
+               } else if (!UserUtils.ifPasswordMatches(this.getUserCurrentPassword(), this.userLoginController.getLoggedInUser())) {
+                       // Password mismatches
+                       this.showFacesMessage("form_user_change_password:userCurrentPassword", "Entered current password does not matched stored password."); //NOI18N
+
+                       // Clear bean
+                       this.clear();
+
+                       // No redirect
+                       return ""; //NOI18N
+               } else if (!Objects.equals(this.getUserPassword(), this.getUserPasswordRepeat())) {
+                       // Both entered passwords don't match
+                       this.showFacesMessage("form_user_change_password:userPasswordRepeat", "Entered new passwords mismatch."); //NOI18N
+
+                       // Clear bean
+                       this.clear();
+
+                       // No redirect
+                       return ""; //NOI18N
+               } else if (Objects.equals(this.getUserCurrentPassword(), this.getUserPassword())) {
+                       // New password matches current
+                       this.showFacesMessage("form_user_change_password:userPassword", "Entered new password is same as current password."); //NOI18N
+
+                       // Clear bean
+                       this.clear();
+
+                       // No redirect
+                       return ""; //NOI18N
+               } else if (this.userLoginController.isPasswordInHistory(this.getUserPassword())) {
+                       // Is already in list (to old passwords are ignored)
+                       this.showFacesMessage("form_user_change_password:userPassword", "Entered new password is has already been used some time ago."); //NOI18N
+
+                       // Clear bean
+                       this.clear();
+
+                       // No redirect
+                       return ""; //NOI18N
+               }
+
+               // Get user instance
+               User user = this.userLoginController.getLoggedInUser();
+
+               // Encrypt password
+               String encryptedPassword = UserUtils.encryptPassword(this.getUserPassword());
+
+               // Set it in user
+               user.setUserEncryptedPassword(encryptedPassword);
+
+               try {
+                       // All is set, then update password
+                       PasswordHistory passwordHistory = this.userBean.updateUserPassword(user);
+
+                       // Fire event
+                       this.userUpdatedPasswordEvent.fire(new UserUpdatedPasswordEvent(passwordHistory));
+               } catch (final UserNotFoundException | UserStatusUnconfirmedException | UserStatusLockedException ex) {
+                       // Clear bean
+                       this.clear();
+
+                       // Throw again
+                       throw new FaceletException(ex);
+               }
+
+               // Clear bean
+               this.clear();
+
+               // Return outcome
+               return "login_data_saved"; //NOI18N
+       }
+
+       @Override
+       public String getUserCurrentPassword () {
+               return this.userCurrentPassword;
+       }
+
+       @Override
+       public void setUserCurrentPassword (final String userCurrentPassword) {
+               this.userCurrentPassword = userCurrentPassword;
+       }
+
+       @Override
+       public String getUserPassword () {
+               return this.userPassword;
+       }
+
+       @Override
+       public void setUserPassword (final String userPassword) {
+               this.userPassword = userPassword;
+       }
+
+       @Override
+       public String getUserPasswordRepeat () {
+               return this.userPasswordRepeat;
+       }
+
+       @Override
+       public void setUserPasswordRepeat (final String userPasswordRepeat) {
+               this.userPasswordRepeat = userPasswordRepeat;
+       }
+
+       public boolean isRequiredChangePasswordSet () {
+               // Is all data set?
+               return ((this.getUserCurrentPassword() != null) &&
+                               (!this.getUserCurrentPassword().isEmpty()) &&
+                               (this.getUserPassword() != null) &&
+                               (!this.getUserPassword().isEmpty()) &&
+                               (this.getUserPasswordRepeat() != null) &&
+                               (!this.getUserPasswordRepeat().isEmpty()));
+       }
+
+       /**
+        * Clears this bean
+        */
+       private void clear () {
+               // Clear all data
+               this.setUserPassword(null);
+               this.setUserPasswordRepeat(null);
+       }
+
+}
diff --git a/src/java/org/mxchange/jjobs/beans/user/password/JobsUserPasswordWebRequestController.java b/src/java/org/mxchange/jjobs/beans/user/password/JobsUserPasswordWebRequestController.java
new file mode 100644 (file)
index 0000000..73767ba
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 Cho-Time GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.mxchange.jjobs.beans.user.password;
+
+import java.io.Serializable;
+import javax.ejb.Local;
+
+/**
+ * An interface for user beans
+ * <p>
+ * @author Roland Haeder<rhaeder@cho-time.de>
+ */
+@Local
+public interface JobsUserPasswordWebRequestController extends Serializable {
+
+       /**
+        * Getter for unencrypted user password
+        * <p>
+        * @return Unencrypted user password
+        */
+       String getUserPassword ();
+
+       /**
+        * Setter for unencrypted user password
+        * <p>
+        * @param userPassword Unencrypted user password
+        */
+       void setUserPassword (final String userPassword);
+
+       /**
+        * Getter for current unencrypted user password
+        * <p>
+        * @return Current unencrypted user password
+        */
+       String getUserCurrentPassword ();
+
+       /**
+        * Setter for current unencrypted user password
+        * <p>
+        * @param userCurrentPassword Current unencrypted user password
+        */
+       void setUserCurrentPassword (final String userCurrentPassword);
+
+       /**
+        * Getter for unencrypted user password repeated
+        * <p>
+        * @return Unencrypted user password repeated
+        */
+       String getUserPasswordRepeat ();
+
+       /**
+        * Setter for unencrypted user password repeated
+        * <p>
+        * @param userPasswordRepeat Unencrypted user password repeated
+        */
+       void setUserPasswordRepeat (final String userPasswordRepeat);
+
+       /**
+        * Changes logged-in user's password. It must not match with current password and should not appear in password history list for X (configurable) entries.
+        * <p>
+        * @return Redirect outcome
+        */
+       String doChangePassword ();
+
+}
index dd7188b4c09c12b50eb20a7019213b9cc9a4100e..971babf7456a57276c73348672ff4abb1ff1ccf4 100644 (file)
@@ -584,3 +584,6 @@ ADMIN_MOBILE_PROVIDER_DIAL_PREFIX_REQUIRED=Bitte geben Sie die Vorwahl ohne f\u0
 USER_LOGIN_MUST_CHANGE_PASSWORD=Sie m\u00fcssen Ihr Passwort \u00e4ndern. Dies darf nicht mit dem aktuellen \u00fcbereinstimmen.
 ADMIN_LOGOUT_TITLE=Aus dem Administrationsbereich ausloggen
 ADMIN_LOGOUT_NOTICE=Bitte loggen Sie sich immer aus, damit die Sitzung sauber beendet ist. Andere M\u00f6gliichkeit ist, den Browser zu schhlie\u00dfen, dann wird die Sitzung auch beendet.
+USER_CURRENT_PASSWORD_REQUIRED=Bitte geben Sie Ihr derzeit verwendetes Passwort zur \u00c4nderungsbest\u00e4tigung ein.
+USER_NEW_PASSWORD_REQUIRED=Bitte geben Sie ein neues Passwort zweimal ein. Dieses muss anders als Ihr aktuelles sein.
+USER_NEW_PASSWORD_REPEAT_REQUIRED=Bitte wiederholen Sie das neue Passwort. Dies dient der Reduzierung von Tippfehlern.
index cbf32310098131d75540fdb0861be95e88398911..5f3212469af195f720df015df92f9a607a9cd212 100644 (file)
@@ -584,3 +584,6 @@ ADMIN_MOBILE_PROVIDER_DIAL_PREFIX_REQUIRED=Please enter dial prefix for mobile p
 USER_LOGIN_MUST_CHANGE_PASSWORD=Please change your password. It must not match with your current one.
 ADMIN_LOGOUT_TITLE=Logout from administration area
 ADMIN_LOGOUT_NOTICE=Please always logout from administration area that the session is cleanly closed. An other option is to close the browser, then the session is also closed.
+USER_CURRENT_PASSWORD_REQUIRED=Please enter your currently used password for confirming password change.
+USER_NEW_PASSWORD_REQUIRED=Please a new password twice. This must be different than the current one.
+USER_NEW_PASSWORD_REPEAT_REQUIRED=Please repeat your new password. This is for preventing type mistakes.
index 40fa93ae700b3cbcdfae4657c005e8052218c6fe..da0d52cf9c0d5315e5ff307d5eb3e0b22e7516f9 100644 (file)
         <param-name>is_feature_user_must_change_password_enabled</param-name>
         <param-value>true</param-value>
     </context-param>
+    <context-param>
+        <description>Whether users are allowed to change their login password.</description>
+        <param-name>is_feature_change_user_password_enabled</param-name>
+        <param-value>true</param-value>
+    </context-param>
+    <context-param>
+        <description>Maximum passwords that must be different.</description>
+        <param-name>max_user_password_history</param-name>
+        <param-value>5</param-value>
+    </context-param>
     <servlet>
         <servlet-name>Faces Servlet</servlet-name>
         <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
index ae17f9f97367137c7f3358f9d999fadb9465f364..eb3f15dde426324f29a6fd4c596e2b83a67c8c5a 100644 (file)
 
                <ui:define name="content">
                        <ui:fragment rendered="#{userLoginController.isUserLoggedIn()}">
-                               <div class="table">
-                                       <div class="table_header">
-                                               <h:outputText value="#{msg.LOGIN_CHANGE_PASSWORD_TITLE}" />
-                                       </div>
+                               <ui:fragment rendered="#{featureController.isFeatureEnabled('change_user_password')}">
+                                       <div class="table">
+                                               <div class="table_header">
+                                                       <h:outputText value="#{msg.LOGIN_CHANGE_PASSWORD_TITLE}" />
+                                               </div>
+
+                                               <h:form id="form_user_change_password">
+                                                       <div class="para">
+                                                               <fieldset id="change_password">
+                                                                       <legend title="#{msg.LOGIN_CHANGE_PASSWORD_LEGEND_TITLE}">
+                                                                               <h:outputText value="#{msg.LOGIN_CHANGE_PASSWORD_LEGEND}" />
+                                                                       </legend>
+
+                                                                       <div class="table_row">
+                                                                               <div class="table_left">
+                                                                                       <h:outputLabel for="userCurrentPassword" value="#{msg.GUEST_REGISTRATION_ENTER_CURRENT_PASSWORD}" />
+                                                                               </div>
 
-                                       <h:form id="login_form">
-                                               <div class="para">
-                                                       <fieldset id="change_password">
-                                                               <legend title="#{msg.LOGIN_CHANGE_PASSWORD_LEGEND_TITLE}">
-                                                                       <h:outputText value="#{msg.LOGIN_CHANGE_PASSWORD_LEGEND}" />
-                                                               </legend>
+                                                                               <div class="table_right">
+                                                                                       <h:inputSecret styleClass="input" id="userCurrentPassword" size="10" maxlength="255" value="#{userPasswordController.userCurrentPassword}" required="true" requiredMessage="#{msg.USER_CURRENT_PASSWORD_REQUIRED}" />
+                                                                               </div>
 
-                                                               <div class="table_row">
-                                                                       <div class="table_left">
-                                                                               <h:outputLabel for="userPassword" value="#{msg.GUEST_REGISTRATION_ENTER_PASSWORD}" />
+                                                                               <div class="clear"></div>
                                                                        </div>
 
-                                                                       <div class="table_right">
-                                                                               <h:inputSecret styleClass="input" id="userPassword" size="10" maxlength="255" value="#{userLoginController.userPassword}" required="true" />
+                                                                       <div class="error_container">
+                                                                               <h:message for="userCurrentPassword" errorClass="errors" fatalClass="errors" warnClass="errors" />
                                                                        </div>
 
-                                                                       <div class="clear"></div>
-                                                               </div>
+                                                                       <div class="table_row">
+                                                                               <div class="table_left">
+                                                                                       <h:outputLabel for="userPassword" value="#{msg.GUEST_REGISTRATION_ENTER_PASSWORD}" />
+                                                                               </div>
 
-                                                               <div class="table_row">
-                                                                       <div class="table_left">
-                                                                               <h:outputLabel for="userPasswordRepeat" value="#{msg.GUEST_REGISTRATION_ENTER_PASSWORD_REPEAT}" />
+                                                                               <div class="table_right">
+                                                                                       <h:inputSecret styleClass="input" id="userPassword" size="10" maxlength="255" value="#{userPasswordController.userPassword}" required="true" requiredMessage="#{msg.USER_NEW_PASSWORD_REQUIRED}" />
+                                                                               </div>
                                                                        </div>
 
-                                                                       <div class="table_right">
-                                                                               <h:inputSecret styleClass="input" id="userPasswordRepeat" size="10" maxlength="255" value="#{userLoginController.userPasswordRepeat}" required="true" />
+                                                                       <div class="error_container">
+                                                                               <h:message for="userPassword" errorClass="errors" fatalClass="errors" warnClass="errors" />
                                                                        </div>
 
-                                                                       <div class="clear"></div>
-                                                               </div>
-                                                       </fieldset>
-                                               </div>
+                                                                       <div class="table_row">
+                                                                               <div class="table_left">
+                                                                                       <h:outputLabel for="userPasswordRepeat" value="#{msg.GUEST_REGISTRATION_ENTER_PASSWORD_REPEAT}" />
+                                                                               </div>
 
-                                               <ui:include src="/WEB-INF/templates/login/user/user_enter_current_password.tpl" />
+                                                                               <div class="table_right">
+                                                                                       <h:inputSecret styleClass="input" id="userPasswordRepeat" size="10" maxlength="255" value="#{userPasswordController.userPasswordRepeat}" required="true" requiredMessage="#{msg.USER_NEW_PASSWORD_REPEAT_REQUIRED}" />
+                                                                               </div>
+                                                                       </div>
 
-                                               <div class="table_footer">
-                                                       <h:commandButton styleClass="reset" type="reset" value="#{msg.BUTTON_RESET_FORM}" />
-                                                       <h:commandButton styleClass="submit" type="submit" id="change_password" value="#{msg.BUTTON_CHANGE_PASSWORD}" action="#{userController.changePassword()}" />
-                                               </div>
-                                       </h:form>
-                               </div>
-                       </ui:fragment>
+                                                                       <div class="error_container">
+                                                                               <h:message for="userPasswordRepeat" errorClass="errors" fatalClass="errors" warnClass="errors" />
+                                                                       </div>
+                                                               </fieldset>
+                                                       </div>
+
+                                                       <div class="table_footer">
+                                                               <h:commandButton styleClass="reset" type="reset" value="#{msg.BUTTON_RESET_FORM}" />
+                                                               <h:commandButton styleClass="submit" type="submit" id="change_password" value="#{msg.BUTTON_CHANGE_PASSWORD}" action="#{userPasswordController.doChangePassword()}" />
+                                                       </div>
+                                               </h:form>
+                                       </div>
+
+                                       <ui:include src="/WEB-INF/templates/login/user/user_enter_current_password.tpl" />
+
+                                       <h:outputText styleClass="errors" value="#{msg.ERROR_LOGIN_USER_EDIT_DATA_DISABLED}" rendered="#{not featureController.isFeatureEnabled('change_user_password')}" />
+                               </ui:fragment>
 
-                       <ui:fragment rendered="#{not userLoginController.isUserLoggedIn()}">
-                               <ui:include src="/WEB-INF/templates/user/user_not_logged_in.tpl" />
+                               <ui:fragment rendered="#{not userLoginController.isUserLoggedIn()}">
+                                       <ui:include src="/WEB-INF/templates/user/user_not_logged_in.tpl" />
+                               </ui:fragment>
                        </ui:fragment>
                </ui:define>
        </ui:composition>