← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 13850: Merged in user account invite branch

 

Merge authors:
  Jim Grace (jimgrace)
------------------------------------------------------------
revno: 13850 [merge]
committer: Jim Grace <jimgrace@xxxxxxxxx>
branch nick: dhis2
timestamp: Sat 2014-01-25 14:03:56 -0500
message:
  Merged in user account invite branch
added:
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/RestoreType.java
  dhis-2/dhis-services/dhis-service-core/src/main/resources/invite_message1.vm
  dhis-2/dhis-services/dhis-service-core/src/main/resources/invite_message2.vm
  dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/IsInviteTokenValidAction.java
modified:
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/setting/SystemSettingManager.java
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/setting/DefaultSystemSettingManager.java
  dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java
  dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AccountController.java
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/account.js
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/updateUserAccountForm.js
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/validationRules.js
  dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/useraccount/account.vm
  dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/interceptor/SystemSettingInterceptor.java
  dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/IsRestoreTokenValidAction.java
  dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/beans.xml
  dhis-2/dhis-web/dhis-web-commons/src/main/resources/dhis-web-commons.xml
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/java/org/hisp/dhis/settings/action/system/SetAccessSettingsAction.java
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/resources/org/hisp/dhis/settings/i18n_module.properties
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/webapp/dhis-web-maintenance-settings/systemAccessSettings.vm
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/pom.xml
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/META-INF/dhis/beans.xml
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/org/hisp/dhis/user/i18n_module.properties
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/struts.xml
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/addUserForm.vm
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/javascript/user.js
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/updateUserForm.vm


--
lp:dhis2
https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk

Your team DHIS 2 developers is subscribed to branch lp:dhis2.
To unsubscribe from this branch go to https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk/+edit-subscription
=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/setting/SystemSettingManager.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/setting/SystemSettingManager.java	2013-12-25 15:01:48 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/setting/SystemSettingManager.java	2014-01-23 15:04:07 +0000
@@ -79,6 +79,7 @@
     final String KEY_SCHEDULE_AGGREGATE_QUERY_BUILDER_TASK_STRATEGY = "scheduleAggregateQueryBuilderTackStrategy";
     final String KEY_CONFIGURATION = "keyConfig";
     final String KEY_ACCOUNT_RECOVERY = "keyAccountRecovery";
+    final String KEY_ACCOUNT_INVITE = "keyAccountInvite";
     final String KEY_LAST_MONITORING_RUN = "keyLastMonitoringRun";
     final String KEY_GOOGLE_ANALYTICS_UA = "googleAnalyticsUA";
     final String KEY_CREDENTIALS_EXPIRES = "credentialsExpires";
@@ -135,6 +136,8 @@
 
     boolean accountRecoveryEnabled();
 
+    boolean accountInviteEnabled();
+
     boolean selfRegistrationNoRecaptcha();
 
     boolean emailEnabled();

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java	2014-01-13 15:16:22 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java	2014-01-25 18:46:36 +0000
@@ -46,7 +46,6 @@
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.util.Arrays;
-import java.util.Calendar;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -61,10 +60,13 @@
 {
     private static final Log log = LogFactory.getLog( DefaultSecurityService.class );
 
-    private static final String RESTORE_PATH = "/dhis-web-commons/security/restore.action";
-
-    private static final int TOKEN_LENGTH = 50;
-    private static final int CODE_LENGTH = 15;
+    private static final String RESTORE_PATH = "/dhis-web-commons/security/";
+
+    private static final int INVITED_USERNAME_UNIQUE_LENGTH = 15;
+    private static final int INVITED_USER_PASSWORD_LENGTH = 40;
+
+    private static final int RESTORE_TOKEN_LENGTH = 50;
+    private static final int RESTORE_CODE_LENGTH = 15;
 
     // -------------------------------------------------------------------------
     // Dependencies
@@ -105,7 +107,25 @@
     // SecurityService implementation
     // -------------------------------------------------------------------------
 
-    public boolean sendRestoreMessage( UserCredentials credentials, String rootPath )
+    public boolean prepareUserForInvite( UserCredentials credentials )
+    {
+        if ( credentials == null || credentials.getUser() == null )
+        {
+            return false;
+        }
+
+        String username = "invitedUser_" + CodeGenerator.generateCode( INVITED_USERNAME_UNIQUE_LENGTH );
+        String rawPassword = CodeGenerator.generateCode( INVITED_USER_PASSWORD_LENGTH );
+
+        credentials.getUser().setSurname( "(TBD)" );
+        credentials.getUser().setFirstName( "(TBD)" );
+        credentials.setUsername( username );
+        credentials.setPassword( passwordManager.encodePassword( username, rawPassword ) );
+
+        return true;
+    }
+
+    public boolean sendRestoreMessage( UserCredentials credentials, String rootPath, RestoreType restoreType )
     {
         if ( credentials == null || rootPath == null )
         {
@@ -114,58 +134,58 @@
 
         if ( credentials.getUser() == null || credentials.getUser().getEmail() == null )
         {
-            log.info( "Could not send message as user does not exist or has no email: " + credentials );
+            log.info( "Could not send " + restoreType.name() + " message as user does not exist or has no email: " + credentials );
             return false;
         }
 
         if ( !ValidationUtils.emailIsValid( credentials.getUser().getEmail() ) )
         {
-            log.info( "Could not send message as email is invalid" );
+            log.info( "Could not send " + restoreType.name() + " message as email is invalid" );
             return false;
         }
 
         if ( !systemSettingManager.emailEnabled() )
         {
-            log.info( "Could not send message as email is not configured" );
+            log.info( "Could not send " + restoreType.name() + " message as email is not configured" );
             return false;
         }
 
         if ( credentials.hasAnyAuthority( Arrays.asList( UserAuthorityGroup.CRITICAL_AUTHS ) ) )
         {
-            log.info( "Not allowed to recover credentials with critical authorities" );
+            log.info( "Not allowed to  " + restoreType.name() + " users with critical authorities" );
             return false;
         }
 
-        String[] result = initRestore( credentials );
+        String[] result = initRestore( credentials, restoreType );
 
         Set<User> users = new HashSet<User>();
         users.add( credentials.getUser() );
 
         Map<String, String> vars = new HashMap<String, String>();
         vars.put( "rootPath", rootPath );
-        vars.put( "restorePath", rootPath + RESTORE_PATH );
+        vars.put( "restorePath", rootPath + RESTORE_PATH + restoreType.getAction() );
         vars.put( "token", result[0] );
         vars.put( "code", result[1] );
         vars.put( "username", credentials.getUsername() );
 
-        String text1 = new VelocityManager().render( vars, "restore_message1" );
-        String text2 = new VelocityManager().render( vars, "restore_message2" );
+        String text1 = new VelocityManager().render( vars, restoreType.getEmailTemplate() + "1" );
+        String text2 = new VelocityManager().render( vars, restoreType.getEmailTemplate() + "2" );
 
-        emailMessageSender.sendMessage( "User account restore confirmation (message 1 of 2)", text1, null, users, true );
-        emailMessageSender.sendMessage( "User account restore confirmation (message 2 of 2)", text2, null, users, true );
+        emailMessageSender.sendMessage( restoreType.getEmailSubject() + " (message 1 of 2)", text1, null, users, true );
+        emailMessageSender.sendMessage( restoreType.getEmailSubject() + " (message 2 of 2)", text2, null, users, true );
 
         return true;
     }
 
-    public String[] initRestore( UserCredentials credentials )
+    public String[] initRestore( UserCredentials credentials, RestoreType restoreType )
     {
-        String token = CodeGenerator.generateCode( TOKEN_LENGTH );
-        String code = CodeGenerator.generateCode( CODE_LENGTH );
+        String token = restoreType.getTokenPrefix() + CodeGenerator.generateCode( RESTORE_TOKEN_LENGTH );
+        String code = CodeGenerator.generateCode( RESTORE_CODE_LENGTH );
 
         String hashedToken = passwordManager.encodePassword( credentials.getUsername(), token );
         String hashedCode = passwordManager.encodePassword( credentials.getUsername(), code );
 
-        Date expiry = new Cal().now().add( Calendar.HOUR_OF_DAY, 1 ).time();
+        Date expiry = new Cal().now().add( restoreType.getExpiryIntervalType(), restoreType.getExpiryIntervalCount() ).time();
 
         credentials.setRestoreToken( hashedToken );
         credentials.setRestoreCode( hashedCode );
@@ -177,24 +197,15 @@
         return result;
     }
 
-    public boolean restore( UserCredentials credentials, String token, String code, String newPassword )
+    public boolean restore( UserCredentials credentials, String token, String code, String newPassword, RestoreType restoreType )
     {
-        if ( credentials == null || token == null || code == null || newPassword == null )
+        if ( credentials == null || token == null || code == null || newPassword == null
+                || !canRestoreNow( credentials, token, code, restoreType ) )
         {
             return false;
         }
 
         String username = credentials.getUsername();
-        
-        token = passwordManager.encodePassword( username, token );
-        code = passwordManager.encodePassword( username, code );
-
-        Date date = new Cal().now().time();
-
-        if ( !credentials.canRestore( token, code, date ) )
-        {
-            return false;
-        }
 
         newPassword = passwordManager.encodePassword( username, newPassword );
 
@@ -209,13 +220,36 @@
         return true;
     }
 
-    public boolean verifyToken( UserCredentials credentials, String token )
+    public boolean canRestoreNow( UserCredentials credentials, String token, String code, RestoreType restoreType )
+    {
+        if ( !verifyToken ( credentials, token, restoreType ) )
+        {
+            return false;
+        }
+
+        String username = credentials.getUsername();
+
+        String encodedToken = passwordManager.encodePassword( username, token );
+        String encodedCode = passwordManager.encodePassword( username, code );
+
+        Date date = new Cal().now().time();
+
+        return credentials.canRestore( encodedToken, encodedCode, date );
+    }
+
+    public boolean verifyToken( UserCredentials credentials, String token, RestoreType restoreType )
     {
         if ( credentials == null || token == null )
         {
             return false;
         }
 
+        if ( !token.startsWith( restoreType.getTokenPrefix() ) )
+        {
+            log.info( "Wrong prefix for restore type " + restoreType.name() + " on token: " + token );
+            return false;
+        }
+
         if ( credentials.getRestoreToken() == null )
         {
             log.info( "Could not verify token as user has no token: " + credentials );

=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/RestoreType.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/RestoreType.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/RestoreType.java	2014-01-17 03:48:57 +0000
@@ -0,0 +1,122 @@
+package org.hisp.dhis.security;
+/*
+ * Copyright (c) 2004-2013, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import java.util.Calendar;
+
+/**
+ * Type of user account restore operation.
+ *
+ * @author Jim Grace
+ */
+
+public enum RestoreType
+{
+    RECOVER_PASSWORD ("R", Calendar.HOUR_OF_DAY, 1, "restore_message", "User account restore confirmation", "restore.action" ),
+    INVITE ("I", Calendar.MONTH, 3, "invite_message", "Create DHIS 2 user account invitation", "invite.action" );
+
+    /**
+     * Prefix to be used on restore token. This prevents one type of restore
+     * URL from being hacked and used for a different type of restore.
+     */
+    private final String tokenPrefix;
+
+    /**
+     * Type of Calendar interval before the restore expires.
+     */
+    private final int expiryIntervalType;
+
+    /**
+     * Count of Calendar intervals before the restore expires.
+     */
+    private final int expiryIntervalCount;
+
+    /**
+     * Name of the email template for this restore action type.
+     */
+    private final String emailTemplate;
+
+    /**
+     * Subject line of the email for this restore action type.
+     */
+    private final String emailSubject;
+
+    /**
+     * Return web action to put in the email message.
+     */
+    private final String action;
+
+    // -------------------------------------------------------------------------
+    // Constructor
+    // -------------------------------------------------------------------------
+
+    RestoreType( String tokenPrefix, int expiryIntervalType, int expiryIntervalCount,
+                 String emailTemplate, String emailSubject, String action )
+    {
+        this.tokenPrefix = tokenPrefix;
+        this.expiryIntervalType = expiryIntervalType;
+        this.expiryIntervalCount = expiryIntervalCount;
+        this.emailTemplate = emailTemplate;
+        this.emailSubject = emailSubject;
+        this.action = action;
+    }
+
+    // -------------------------------------------------------------------------
+    // Getters
+    // -------------------------------------------------------------------------
+
+    public String getTokenPrefix()
+    {
+        return tokenPrefix;
+    }
+
+    public int getExpiryIntervalType()
+    {
+        return expiryIntervalType;
+    }
+
+    public int getExpiryIntervalCount()
+    {
+        return expiryIntervalCount;
+    }
+
+    public String getEmailTemplate()
+    {
+        return emailTemplate;
+    }
+
+    public String getEmailSubject()
+    {
+        return emailSubject;
+    }
+
+    public String getAction()
+    {
+        return action;
+    }
+}

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java	2014-01-13 15:16:22 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java	2014-01-25 18:46:36 +0000
@@ -37,27 +37,42 @@
 public interface SecurityService
 {
     /**
-     * Will invoke the initiateRestore method and dispatch email messages with
+     * Sets information for a user who will be invited by email to finish
+     * setting up their user account.
+     *
+     * @param credentials the credentials of the user to invite.
+     * @return true if the invitation was sent, otherwise false.
+     */
+    boolean prepareUserForInvite( UserCredentials credentials );
+
+    /**
+     * Invokes the initRestore method and dispatches email messages with
      * restore information to the user.
+     * <p>
+     * In the case of inviting a user to finish setting up an account,
+     * the user account must already be configured with the profile desired
+     * for the user (e.g., locale, organisation unit(s), role(s), etc.)
      *
      * @param credentials the credentials for the user to send restore message.
      * @param rootPath the root path of the request.
+     * @param restoreType type of restore operation (e.g. pw recovery, invite).
      * @return false if any of the arguments are null or if the user credentials
      *         identified by the user name does not exist, true otherwise.
      */
-    boolean sendRestoreMessage( UserCredentials credentials, String rootPath );
+    boolean sendRestoreMessage( UserCredentials credentials, String rootPath, RestoreType restoreType );
 
     /**
-     * Will populate the restoreToken and restoreCode property of the given
-     * credentials with a hashed version of auto-generated values. Will set the
-     * restoreExpiry property with a date time one hour from now. Changes will be
-     * persisted.
+     * Populates the restoreToken and restoreCode property of the given
+     * credentials with a hashed version of auto-generated values. Sets the
+     * restoreExpiry property with a date time some interval from now depending
+     * on the restore type. Changes are persisted.
      *
      * @param credentials the user credentials.
+     * @param restoreType type of restore operation (e.g. pw recovery, invite).
      * @return an array where index 0 is the clear-text token and index 1 the
      *         clear-text code.
      */
-    String[] initRestore( UserCredentials credentials );
+    String[] initRestore( UserCredentials credentials, RestoreType restoreType );
 
     /**
      * Tests whether the given token and code are valid for the given user name.
@@ -70,9 +85,24 @@
      * @param token the token.
      * @param code the code.
      * @param newPassword the proposed new password.
-     * @return true or false.
-     */
-    boolean restore( UserCredentials credentials, String token, String code, String newPassword );
+     * @param restoreType type of restore operation (e.g. pw recovery, invite).
+     * @return true or false.
+     */
+    boolean restore( UserCredentials credentials, String token, String code, String newPassword, RestoreType restoreType );
+
+    /**
+     * Tests whether the given token and code are valid for the given user name.
+     * In order to succeed, the given token and code must match the ones on the
+     * credentials, and the current date must be before the expiry date time of
+     * the credentials.
+     *
+     * @param credentials the user credentials.
+     * @param token the token.
+     * @param code the code.
+     * @param restoreType type of restore operation (e.g. pw recovery, invite).
+     * @return true or false.
+     */
+    boolean canRestoreNow( UserCredentials credentials, String token, String code, RestoreType restoreType );
 
     /**
      * Tests whether the given token in combination with the given user name is
@@ -85,7 +115,7 @@
      *         identified by the user name does not exist, true if the arguments
      *         are valid.
      */
-    boolean verifyToken( UserCredentials credentials, String token );
+    boolean verifyToken( UserCredentials credentials, String token, RestoreType restoreType );
 
     /**
      * Checks whether current user has read access to object.

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/setting/DefaultSystemSettingManager.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/setting/DefaultSystemSettingManager.java	2013-12-19 13:25:04 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/setting/DefaultSystemSettingManager.java	2014-01-23 15:04:07 +0000
@@ -163,6 +163,11 @@
         return (Boolean) getSystemSetting( KEY_ACCOUNT_RECOVERY, false );
     }
 
+    public boolean accountInviteEnabled()
+    {
+        return (Boolean) getSystemSetting( KEY_ACCOUNT_INVITE, false );
+    }
+
     @Override
     public boolean selfRegistrationNoRecaptcha()
     {

=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/invite_message1.vm'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/invite_message1.vm	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/invite_message1.vm	2014-01-17 03:48:57 +0000
@@ -0,0 +1,11 @@
+This is an invitation to create a user account on the DHIS 2 system at ${object.rootPath}.
+You have been sent two emails, where this is the first one. Please follow the
+link below. In the next step you will be asked to enter a code which has been
+sent to you in the other email.
+
+
+${object.restorePath}?username=${object.username}&token=${object.token}
+
+
+You must respond to this invitation within 3 months. If you take no action,
+the invitation will expire at that time.

=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/invite_message2.vm'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/invite_message2.vm	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/invite_message2.vm	2014-01-23 15:04:07 +0000
@@ -0,0 +1,11 @@
+This is an invitation to create a user account on the DHIS 2 system at ${object.rootPath}.
+You have been sent two emails, where this is the second one. Please read the
+first email and follow the instructions. Please use the code below to complete
+the new account form.
+
+
+${object.code}
+
+
+You must respond to this invitation within 3 months. If you take no action,
+the invitation will expire at that time.

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java'
--- dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java	2014-01-13 15:16:22 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java	2014-01-25 18:46:36 +0000
@@ -44,7 +44,9 @@
     extends DhisSpringTest
 {
     private UserCredentials credentials;
-    
+
+    private UserCredentials otherCredentials;
+
     @Autowired
     private UserService userService; 
     
@@ -60,37 +62,142 @@
         credentials = new UserCredentials();
         credentials.setUsername( "johndoe" );
         credentials.setPassword( "" );
-        
-        User user = createUser( 'A' );
-        user.setEmail( "valid@xxxxxxxxx" );
-        user.setUserCredentials( credentials );
-        credentials.setUser( user );
+
+        User userA = createUser( 'A' );
+        userA.setEmail( "validA@xxxxxxxxx" );
+        userA.setUserCredentials( credentials );
+        credentials.setUser( userA );
         userService.addUserCredentials( credentials );
-    }
-    
-    @Test
-    public void testRestore()
-    {
-        String[] result = securityService.initRestore( credentials );
-        
-        assertNotNull( result[0] );
-        assertNotNull( result[1] );
-        assertNotNull( credentials.getRestoreToken() );
-        assertNotNull( credentials.getRestoreCode() );
-        assertNotNull( credentials.getRestoreExpiry() );
-        
-        boolean verified = securityService.verifyToken( credentials, result[0] );
-        
-        assertTrue( verified );
-        
-        String password = "NewPassword1";
-        
-        boolean restored = securityService.restore( credentials, result[0], result[1], password );
-        
-        assertTrue( restored );
-        
-        String hashedPassword = passwordManager.encodePassword( credentials.getUsername(), password );
-        
+
+        otherCredentials = new UserCredentials();
+        otherCredentials.setUsername( "janesmith" );
+        otherCredentials.setPassword( "" );
+
+        User userB = createUser( 'B' );
+        userB.setEmail( "validB@xxxxxxxxx" );
+        userB.setUserCredentials( otherCredentials );
+        otherCredentials.setUser( userB );
+        userService.addUserCredentials( otherCredentials );
+    }
+
+    @Test
+    public void testRestoreRecoverPassword()
+    {
+        String[] result = securityService.initRestore( credentials, RestoreType.RECOVER_PASSWORD );
+        
+        String token = result[0];
+        String code = result[1];
+
+        assertNotNull( token );
+        assertNotNull( code );
+        assertNotNull( credentials.getRestoreToken() );
+        assertNotNull( credentials.getRestoreCode() );
+        assertNotNull( credentials.getRestoreExpiry() );
+
+        //
+        // verifyToken()
+        //
+        assertFalse( securityService.verifyToken( otherCredentials, token, RestoreType.RECOVER_PASSWORD ) );
+
+        assertFalse( securityService.verifyToken( credentials, "wrongToken", RestoreType.RECOVER_PASSWORD ) );
+
+        assertFalse( securityService.verifyToken( credentials, token, RestoreType.INVITE ) );
+
+        assertTrue( securityService.verifyToken( credentials, token, RestoreType.RECOVER_PASSWORD ) );
+
+        //
+        // canRestoreNow()
+        //
+        assertFalse( securityService.canRestoreNow( otherCredentials, token, code, RestoreType.RECOVER_PASSWORD ) );
+
+        assertFalse( securityService.canRestoreNow( credentials, "wrongToken", code, RestoreType.RECOVER_PASSWORD ) );
+
+        assertFalse( securityService.canRestoreNow( credentials, token, "wrongCode", RestoreType.RECOVER_PASSWORD ) );
+
+        assertFalse( securityService.canRestoreNow( credentials, token, code, RestoreType.INVITE ) );
+
+        assertTrue( securityService.canRestoreNow( credentials, token, code, RestoreType.RECOVER_PASSWORD ) );
+
+        //
+        // restore()
+        //
+        String password = "NewPassword1";
+
+        assertFalse( securityService.restore( otherCredentials, token, code, password, RestoreType.RECOVER_PASSWORD ) );
+
+        assertFalse( securityService.restore( credentials, "wrongToken", code, password, RestoreType.RECOVER_PASSWORD ) );
+
+        assertFalse( securityService.restore( credentials, token, "wrongCode", password, RestoreType.RECOVER_PASSWORD ) );
+
+        assertFalse( securityService.restore( credentials, token, code, password, RestoreType.INVITE ) );
+
+        assertTrue( securityService.restore( credentials, token, code, password, RestoreType.RECOVER_PASSWORD ) );
+
+        //
+        // check password
+        //
+        String hashedPassword = passwordManager.encodePassword( credentials.getUsername(), password );
+
+        assertEquals( hashedPassword, credentials.getPassword() );
+    }
+
+    @Test
+    public void testRestoreInvite()
+    {
+        String[] result = securityService.initRestore( credentials, RestoreType.INVITE );
+        String token = result[0];
+        String code = result[1];
+
+        assertNotNull( token );
+        assertNotNull( code );
+        assertNotNull( credentials.getRestoreToken() );
+        assertNotNull( credentials.getRestoreCode() );
+        assertNotNull( credentials.getRestoreExpiry() );
+
+        //
+        // verifyToken()
+        //
+        assertFalse( securityService.verifyToken( otherCredentials, token, RestoreType.INVITE ) );
+
+        assertFalse( securityService.verifyToken( credentials, "wrongToken", RestoreType.INVITE ) );
+
+        assertFalse( securityService.verifyToken( credentials, token, RestoreType.RECOVER_PASSWORD ) );
+
+        assertTrue( securityService.verifyToken( credentials, token, RestoreType.INVITE ) );
+
+        //
+        // canRestoreNow()
+        //
+        assertFalse( securityService.canRestoreNow( otherCredentials, token, code, RestoreType.INVITE ) );
+
+        assertFalse( securityService.canRestoreNow( credentials, "wrongToken", code, RestoreType.INVITE ) );
+
+        assertFalse( securityService.canRestoreNow( credentials, token, "wrongCode", RestoreType.INVITE ) );
+
+        assertFalse( securityService.canRestoreNow( credentials, token, code, RestoreType.RECOVER_PASSWORD ) );
+
+        assertTrue( securityService.canRestoreNow( credentials, token, code, RestoreType.INVITE ) );
+
+        //
+        // restore()
+        //
+        String password = "NewPassword1";
+
+        assertFalse( securityService.restore( otherCredentials, token, code, password, RestoreType.INVITE ) );
+
+        assertFalse( securityService.restore( credentials, "wrongToken", code, password, RestoreType.INVITE ) );
+
+        assertFalse( securityService.restore( credentials, token, "wrongCode", password, RestoreType.INVITE ) );
+
+        assertFalse( securityService.restore( credentials, token, code, password, RestoreType.RECOVER_PASSWORD ) );
+
+        assertTrue( securityService.restore( credentials, token, code, password, RestoreType.INVITE ) );
+
+        //
+        // check password
+        //
+        String hashedPassword = passwordManager.encodePassword( credentials.getUsername(), password );
+
         assertEquals( hashedPassword, credentials.getPassword() );
     }
 }

=== modified file 'dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AccountController.java'
--- dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AccountController.java	2014-01-13 15:16:22 +0000
+++ dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AccountController.java	2014-01-23 15:04:07 +0000
@@ -35,7 +35,9 @@
 import org.hisp.dhis.api.utils.ContextUtils;
 import org.hisp.dhis.configuration.ConfigurationService;
 import org.hisp.dhis.organisationunit.OrganisationUnit;
+import org.hisp.dhis.period.Cal;
 import org.hisp.dhis.security.PasswordManager;
+import org.hisp.dhis.security.RestoreType;
 import org.hisp.dhis.security.SecurityService;
 import org.hisp.dhis.setting.SystemSettingManager;
 import org.hisp.dhis.system.util.ValidationUtils;
@@ -131,7 +133,7 @@
             return "User does not exist: " + username;
         }
         
-        boolean recover = securityService.sendRestoreMessage( credentials, rootPath );
+        boolean recover = securityService.sendRestoreMessage( credentials, rootPath, RestoreType.RECOVER_PASSWORD );
 
         if ( !recover )
         {
@@ -180,7 +182,7 @@
             return "User does not exist: " + username;
         }
         
-        boolean restore = securityService.restore( credentials, token, code, password );
+        boolean restore = securityService.restore( credentials, token, code, password, RestoreType.RECOVER_PASSWORD );
 
         if ( !restore )
         {
@@ -203,17 +205,51 @@
         @RequestParam String email,
         @RequestParam String phoneNumber,
         @RequestParam String employer,
+        @RequestParam String inviteUsername,
+        @RequestParam String inviteToken,
+        @RequestParam String inviteCode,
         @RequestParam( value = "recaptcha_challenge_field", required = false ) String recapChallenge,
         @RequestParam( value = "recaptcha_response_field", required = false ) String recapResponse,
         HttpServletRequest request,
         HttpServletResponse response )
     {
-        boolean allowed = configurationService.getConfiguration().selfRegistrationAllowed();
-
-        if ( !allowed )
-        {
-            response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
-            return "User self registration is not allowed";
+        UserCredentials credentials = null;
+
+        boolean invitedByEmail = !inviteUsername.isEmpty();
+
+        if ( invitedByEmail )
+        {
+            if ( !systemSettingManager.accountInviteEnabled() )
+            {
+                response.setStatus( HttpServletResponse.SC_CONFLICT );
+                return "Account invite is not enabled";
+            }
+
+            credentials = userService.getUserCredentialsByUsername( inviteUsername );
+
+            if ( credentials == null )
+            {
+                response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
+                return "Invitation link not valid";
+            }
+
+            boolean canRestore = securityService.canRestoreNow( credentials, inviteToken, inviteCode, RestoreType.INVITE );
+
+            if ( !canRestore )
+            {
+                response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
+                return "Invitation code not valid";
+            }
+        }
+        else
+        {
+            boolean allowed = configurationService.getConfiguration().selfRegistrationAllowed();
+
+            if ( !allowed )
+            {
+                response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
+                return "User self registration is not allowed";
+            }
         }
 
         // ---------------------------------------------------------------------
@@ -240,9 +276,9 @@
             return "User name is not specified or invalid";
         }
 
-        UserCredentials credentials = userService.getUserCredentialsByUsername( username );
+        UserCredentials usernameAlreadyTakenCredentials = userService.getUserCredentialsByUsername( username );
 
-        if ( credentials != null )
+        if ( usernameAlreadyTakenCredentials != null )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "User name is already taken";
@@ -333,32 +369,64 @@
         // Create and save user, return 201
         // ---------------------------------------------------------------------
 
-        UserAuthorityGroup userRole = configurationService.getConfiguration().getSelfRegistrationRole();
-        OrganisationUnit orgUnit = configurationService.getConfiguration().getSelfRegistrationOrgUnit();
-
-        User user = new User();
-        user.setFirstName( firstName );
-        user.setSurname( surname );
-        user.setEmail( email );
-        user.setPhoneNumber( phoneNumber );
-        user.setEmployer( employer );
-        user.getOrganisationUnits().add( orgUnit );
-
-        credentials = new UserCredentials();
-        credentials.setUsername( username );
-        credentials.setPassword( passwordManager.encodePassword( username, password ) );
-        credentials.setSelfRegistered( true );
-        credentials.setUser( user );
-        credentials.getUserAuthorityGroups().add( userRole );
-
-        user.setUserCredentials( credentials );
-
-        userService.addUser( user );
-        userService.addUserCredentials( credentials );
-
-        authenticate( username, password, userRole, request );
-
-        log.info( "Created user with username: " + username );
+        if ( invitedByEmail )
+        {
+            boolean restored = securityService.restore( credentials, inviteToken, inviteCode, password, RestoreType.INVITE );
+
+            if ( !restored )
+            {
+                log.info( "Invite restore failed for: " + inviteUsername );
+
+                response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
+                return "Unable to create invited user account";
+            }
+
+            User user = credentials.getUser();
+            user.setFirstName( firstName );
+            user.setSurname( surname );
+            user.setEmail( email );
+            user.setPhoneNumber( phoneNumber );
+            user.setEmployer( employer );
+
+            credentials.setUsername( username );
+            credentials.setPassword( passwordManager.encodePassword( username, password ) );
+
+            userService.updateUser( user );
+            userService.updateUserCredentials( credentials );
+
+            log.info( "User " + username + " accepted invitation for " + inviteUsername );
+        }
+        else
+        {
+            UserAuthorityGroup userRole = configurationService.getConfiguration().getSelfRegistrationRole();
+            OrganisationUnit orgUnit = configurationService.getConfiguration().getSelfRegistrationOrgUnit();
+
+            User user = new User();
+            user.setFirstName( firstName );
+            user.setSurname( surname );
+            user.setEmail( email );
+            user.setPhoneNumber( phoneNumber );
+            user.setEmployer( employer );
+            user.getOrganisationUnits().add( orgUnit );
+
+            credentials = new UserCredentials();
+            credentials.setUsername( username );
+            credentials.setPassword( passwordManager.encodePassword( username, password ) );
+            credentials.setSelfRegistered( true );
+            credentials.setUser( user );
+            credentials.getUserAuthorityGroups().add( userRole );
+
+            user.setUserCredentials( credentials );
+
+            userService.addUser( user );
+            userService.addUserCredentials( credentials );
+
+            log.info( "Created user with username: " + username );
+        }
+
+        Set<GrantedAuthority> authorities = getAuthorities( credentials.getUserAuthorityGroups() );
+
+        authenticate( username, password, authorities, request );
 
         response.setStatus( HttpServletResponse.SC_CREATED );
         return "Account created";
@@ -473,23 +541,9 @@
         session.setAttribute( "SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext() );
     }
 
-    private void authenticate( String username, String rawPassword, UserAuthorityGroup userRole, HttpServletRequest request )
-    {
-        UsernamePasswordAuthenticationToken token =
-            new UsernamePasswordAuthenticationToken( username, rawPassword, getAuthorities( userRole ) );
-
-        Authentication auth = authenticationManager.authenticate( token );
-
-        SecurityContextHolder.getContext().setAuthentication( auth );
-
-        HttpSession session = request.getSession();
-
-        session.setAttribute( "SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext() );
-    }
-
-    private Collection<GrantedAuthority> getAuthorities( Set<UserAuthorityGroup> userRoles )
-    {
-        Collection<GrantedAuthority> auths = new HashSet<GrantedAuthority>();
+    private Set<GrantedAuthority> getAuthorities( Set<UserAuthorityGroup> userRoles )
+    {
+        Set<GrantedAuthority> auths = new HashSet<GrantedAuthority>();
 
         for ( UserAuthorityGroup userRole : userRoles )
         {
@@ -499,9 +553,9 @@
         return auths;
     }
 
-    private Collection<GrantedAuthority> getAuthorities( UserAuthorityGroup userRole )
+    private Set<GrantedAuthority> getAuthorities( UserAuthorityGroup userRole )
     {
-        Collection<GrantedAuthority> auths = new HashSet<GrantedAuthority>();
+        Set<GrantedAuthority> auths = new HashSet<GrantedAuthority>();
 
         for ( String auth : userRole.getAuthorities() )
         {

=== modified file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/account.js'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/account.js	2013-12-19 13:25:04 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/account.js	2014-01-17 03:48:57 +0000
@@ -28,6 +28,11 @@
             email: true,
             rangelength: [ 4, 80 ]
         },
+        inviteEmail : {
+            required : true,
+            email : true,
+            rangelength : [ 4, 80 ]
+        },
         phoneNumber: {
             required: true,
             rangelength: [ 6, 30 ]

=== modified file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/updateUserAccountForm.js'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/updateUserAccountForm.js	2012-10-11 17:21:32 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/useraccount/updateUserAccountForm.js	2014-01-17 03:48:57 +0000
@@ -5,6 +5,7 @@
 	/* some customization is needed for the updateUserAccount validation rules */
     rules["rawPassword"].required = false;
     rules["retypePassword"].required = false;
+    rules["inviteEmail"].required = false;
 
     rules["oldPassword"] = {
 			required: true

=== modified file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/validationRules.js'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/validationRules.js	2014-01-14 13:07:02 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/validationRules.js	2014-01-17 03:48:57 +0000
@@ -31,6 +31,11 @@
             "email" : true,
             "rangelength" : [ 0, 160 ]
         },
+        "inviteEmail" : {
+            "required" : true,
+            "email" : true,
+            "rangelength" : [ 4, 160 ]
+        },
         "phoneNumber" : {
             "rangelength" : [ 0, 80 ]
         },

=== modified file 'dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/useraccount/account.vm'
--- dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/useraccount/account.vm	2013-12-19 13:25:04 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/useraccount/account.vm	2014-01-23 15:04:07 +0000
@@ -27,6 +27,16 @@
 <form id="accountForm">
 
 <table>
+
+    <tr #if( $accountAction != "invited" ) style="display:none" #end>
+        <td style="width:140px"><label for="code">$i18n.getString( "code_from_email" )</label></td>
+        <td>
+            <input type="text" id="inviteCode" name="inviteCode" autocomplete="off">
+            <input type="hidden" id="inviteUsername" name="inviteUsername" #if( $accountAction == "invited" ) value="$username" #end>
+            <input type="hidden" id="inviteToken" name="inviteToken" #if( $accountAction == "invited" ) value="$token" #end>
+        </td>
+    </tr>
+
     <tr>
         <td style="width:140px"><label id="label_firstName" for="firstName">$i18n.getString( "name" )</label></td>
         <td><input type="text" id="firstName" name="firstName" autocomplete="off" style="width:11.7em; margin-right:7px;" placeholder="First">
@@ -46,7 +56,7 @@
     </tr>
     <tr>
         <td><label id="label_email" for="email">$i18n.getString( "email" )</label></td>
-        <td><input type="text" id="email" name="email"></td>
+        <td><input type="text" id="email" name="email" #if( $accountAction == "invited" )value="$email" #end></td>
     </tr>
     <tr>
         <td><label id="label_phoneNumber" for="phoneNumber">$i18n.getString( "mobile_phone" )</label></td>

=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/interceptor/SystemSettingInterceptor.java'
--- dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/interceptor/SystemSettingInterceptor.java	2013-12-25 15:01:48 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/interceptor/SystemSettingInterceptor.java	2014-01-23 15:04:07 +0000
@@ -94,6 +94,7 @@
         map.put( KEY_PHONE_NUMBER_AREA_CODE, systemSettingManager.getSystemSetting( KEY_PHONE_NUMBER_AREA_CODE, "" ) );
         map.put( KEY_MULTI_ORGANISATION_UNIT_FORMS, systemSettingManager.getSystemSetting( KEY_MULTI_ORGANISATION_UNIT_FORMS, false ) );
         map.put( KEY_ACCOUNT_RECOVERY, systemSettingManager.getSystemSetting( KEY_ACCOUNT_RECOVERY, false ) );
+        map.put( KEY_ACCOUNT_INVITE, systemSettingManager.getSystemSetting( KEY_ACCOUNT_INVITE, false ) );
         map.put( KEY_CONFIGURATION, configurationService.getConfiguration() );
         map.put( KEY_APP_BASE_URL, systemSettingManager.getSystemSetting( KEY_APP_BASE_URL ) );
         map.put( KEY_GOOGLE_ANALYTICS_UA, systemSettingManager.getSystemSetting( KEY_GOOGLE_ANALYTICS_UA, "" ) );

=== added file 'dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/IsInviteTokenValidAction.java'
--- dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/IsInviteTokenValidAction.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/IsInviteTokenValidAction.java	2014-01-23 15:04:07 +0000
@@ -0,0 +1,132 @@
+package org.hisp.dhis.useraccount.action;
+
+/*
+ * Copyright (c) 2004-2013, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import org.hisp.dhis.security.RestoreType;
+import org.hisp.dhis.security.SecurityService;
+import org.hisp.dhis.setting.SystemSettingManager;
+import org.hisp.dhis.user.UserCredentials;
+import org.hisp.dhis.user.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.opensymphony.xwork2.Action;
+
+/**
+ * @author Jim Grace
+ */
+public class IsInviteTokenValidAction
+        implements Action
+{
+    @Autowired
+    private SystemSettingManager systemSettingManager;
+
+    @Autowired
+    private SecurityService securityService;
+
+    @Autowired
+    private UserService userService;
+
+    // -------------------------------------------------------------------------
+    // Input
+    // -------------------------------------------------------------------------
+
+    private String username;
+
+    public String getUsername()
+    {
+        return username;
+    }
+
+    public void setUsername( String username )
+    {
+        this.username = username;
+    }
+
+    private String token;
+
+    public String getToken()
+    {
+        return token;
+    }
+
+    public void setToken( String token )
+    {
+        this.token = token;
+    }
+
+    // -------------------------------------------------------------------------
+    // Output
+    // -------------------------------------------------------------------------
+
+    private UserCredentials userCredentials;
+
+    public UserCredentials getUserCredentials()
+    {
+        return userCredentials;
+    }
+
+    private final String accountAction = "invited";
+
+    public String getAccountAction()
+    {
+        return accountAction;
+    }
+
+    private String email;
+
+    public String getEmail()
+    {
+        return email;
+    }
+
+    // -------------------------------------------------------------------------
+    // Action implementation
+    // -------------------------------------------------------------------------
+
+    public String execute()
+    {
+        if ( !systemSettingManager.accountInviteEnabled() )
+        {
+            return ERROR;
+        }
+
+        userCredentials = userService.getUserCredentialsByUsername( username );
+
+        if ( userCredentials == null )
+        {
+            return ERROR;
+        }
+
+        email = userCredentials.getUser().getEmail();
+
+        boolean verified = securityService.verifyToken( userCredentials, token, RestoreType.INVITE );
+
+        return verified ? SUCCESS : ERROR;
+    }
+}

=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/IsRestoreTokenValidAction.java'
--- dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/IsRestoreTokenValidAction.java	2014-01-13 15:44:28 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/IsRestoreTokenValidAction.java	2014-01-17 03:48:57 +0000
@@ -28,6 +28,7 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import org.hisp.dhis.security.RestoreType;
 import org.hisp.dhis.security.SecurityService;
 import org.hisp.dhis.user.UserCredentials;
 import org.hisp.dhis.user.UserService;
@@ -88,7 +89,7 @@
             return ERROR;
         }
         
-        boolean verified = securityService.verifyToken( credentials, token );
+        boolean verified = securityService.verifyToken( credentials, token, RestoreType.RECOVER_PASSWORD );
         
         return verified ? SUCCESS : ERROR;
     }

=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/beans.xml'
--- dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/beans.xml	2014-01-08 17:41:22 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/resources/META-INF/dhis/beans.xml	2014-01-23 15:04:07 +0000
@@ -577,6 +577,9 @@
   <bean id="org.hisp.dhis.useraccount.action.IsRestoreTokenValidAction" class="org.hisp.dhis.useraccount.action.IsRestoreTokenValidAction"
     scope="prototype" />
 
+  <bean id="org.hisp.dhis.useraccount.action.IsInviteTokenValidAction" class="org.hisp.dhis.useraccount.action.IsInviteTokenValidAction"
+    scope="prototype" />
+
   <bean id="org.hisp.dhis.useraccount.action.IsAccountRecoveryAllowedAction"
     class="org.hisp.dhis.useraccount.action.IsAccountRecoveryAllowedAction"
     scope="prototype" />

=== modified file 'dhis-2/dhis-web/dhis-web-commons/src/main/resources/dhis-web-commons.xml'
--- dhis-2/dhis-web/dhis-web-commons/src/main/resources/dhis-web-commons.xml	2014-01-08 17:41:22 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/resources/dhis-web-commons.xml	2014-01-23 15:04:07 +0000
@@ -146,10 +146,15 @@
     </action>
 
     <action name="restore" class="org.hisp.dhis.useraccount.action.IsRestoreTokenValidAction">
-      <result name="success" type="velocity">/dhis-web-commons/useraccount/restore.vm</result>
-      <result name="error" type="redirect">login.action</result>
-    </action>
-    
+        <result name="success" type="velocity">/dhis-web-commons/useraccount/restore.vm</result>
+        <result name="error" type="redirect">login.action</result>
+    </action>
+
+    <action name="invite" class="org.hisp.dhis.useraccount.action.IsInviteTokenValidAction">
+        <result name="success" type="velocity">/dhis-web-commons/useraccount/account.vm</result>
+        <result name="error" type="redirect">login.action</result>
+    </action>
+
   </package>
 
   <!-- Organisation Unit Selection Tree -->

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/java/org/hisp/dhis/settings/action/system/SetAccessSettingsAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/java/org/hisp/dhis/settings/action/system/SetAccessSettingsAction.java	2013-12-19 13:25:04 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/java/org/hisp/dhis/settings/action/system/SetAccessSettingsAction.java	2014-01-23 15:04:07 +0000
@@ -91,6 +91,13 @@
         this.accountRecovery = accountRecovery;
     }
 
+    private Boolean accountInvite;
+
+    public void setAccountInvite( Boolean accountInvite )
+    {
+        this.accountInvite = accountInvite;
+    }
+
     private Integer credentialsExpires;
 
     public void setCredentialsExpires( Integer credentialsExpires )
@@ -141,6 +148,7 @@
         configurationService.setConfiguration( config );
 
         systemSettingManager.saveSystemSetting( KEY_ACCOUNT_RECOVERY, accountRecovery );
+        systemSettingManager.saveSystemSetting( KEY_ACCOUNT_INVITE, accountInvite );
         systemSettingManager.saveSystemSetting( KEY_SELF_REGISTRATION_NO_RECAPTCHA, selfRegistrationNoRecaptcha );
 
         if ( credentialsExpires != null )

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/resources/org/hisp/dhis/settings/i18n_module.properties'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/resources/org/hisp/dhis/settings/i18n_module.properties	2013-12-30 13:02:41 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/resources/org/hisp/dhis/settings/i18n_module.properties	2014-01-23 15:04:07 +0000
@@ -56,6 +56,7 @@
 self_registration_account_organisation_unit=Self registration account organisation unit
 access=Access
 enable_user_account_recovery=Enable user account recovery
+enable_user_account_invite=Enable user account invite
 select_organisation_unit=Select organisation unit
 application_notification=Application notification
 multi_organisation_unit_forms=Enable multi-organisation unit forms

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/webapp/dhis-web-maintenance-settings/systemAccessSettings.vm'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/webapp/dhis-web-maintenance-settings/systemAccessSettings.vm	2013-12-30 13:02:41 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-settings/src/main/webapp/dhis-web-maintenance-settings/systemAccessSettings.vm	2014-01-23 15:04:07 +0000
@@ -6,6 +6,7 @@
             selfRegistrationOrgUnit: jQuery( "#selfRegistrationOrgUnit" ).val(),
             selfRegistrationNoRecaptcha: jQuery( '#selfRegistrationNoRecaptcha' ).is( ':checked' ),
             accountRecovery: jQuery( '#accountRecovery' ).is( ':checked' ),
+            accountInvite: jQuery( '#accountInvite' ).is( ':checked' ),
             credentialsExpires: jQuery( '#credentialsExpires' ).val()
         }, function( json ) {
             if ( json.response == "success" ) {
@@ -50,8 +51,13 @@
 </div>
 
 <div class="setting">
-	<input type="checkbox" id="accountRecovery" name="accountRecovery"#if( $keyAccountRecovery ) checked="checked"#end>
-	<label for="accountRecovery">$i18n.getString( "enable_user_account_recovery" )</label>
+    <input type="checkbox" id="accountRecovery" name="accountRecovery"#if( $keyAccountRecovery ) checked="checked"#end>
+    <label for="accountRecovery">$i18n.getString( "enable_user_account_recovery" )</label>
+</div>
+
+<div class="setting">
+    <input type="checkbox" id="accountInvite" name="accountInvite"#if( $keyAccountInvite ) checked="checked"#end>
+    <label for="accountInvite">$i18n.getString( "enable_user_account_invite" )</label>
 </div>
 
 <div class="settingLabel">$i18n.getString( "user_credentials_expires" )</div>

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/pom.xml'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/pom.xml	2014-01-09 21:56:49 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/pom.xml	2014-01-17 03:48:57 +0000
@@ -18,6 +18,11 @@
 
   <dependencies>
 
+      <dependency>
+          <groupId>javax.servlet</groupId>
+          <artifactId>servlet-api</artifactId>
+      </dependency>
+
     <!-- DHIS -->
 
     <dependency>
@@ -51,7 +56,7 @@
     
   </dependencies>
   
-  <properties>
-    <rootDir>../../../</rootDir>
-  </properties>
+  <properties>
+    <rootDir>../../../</rootDir>
+  </properties>
 </project>

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java	2013-09-16 17:07:25 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java	2014-01-25 18:46:36 +0000
@@ -33,11 +33,17 @@
 import java.util.HashSet;
 import java.util.List;
 
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.struts2.ServletActionContext;
+import org.hisp.dhis.api.utils.ContextUtils;
 import org.hisp.dhis.attribute.AttributeService;
 import org.hisp.dhis.organisationunit.OrganisationUnit;
 import org.hisp.dhis.oust.manager.SelectionTreeManager;
 import org.hisp.dhis.ouwt.manager.OrganisationUnitSelectionManager;
 import org.hisp.dhis.security.PasswordManager;
+import org.hisp.dhis.security.RestoreType;
+import org.hisp.dhis.security.SecurityService;
 import org.hisp.dhis.system.util.AttributeUtils;
 import org.hisp.dhis.system.util.LocaleUtils;
 import org.hisp.dhis.user.CurrentUserService;
@@ -54,8 +60,10 @@
  * @author Torgeir Lorange Ostby
  */
 public class AddUserAction
-    implements Action
+        implements Action
 {
+    private String ACCOUNT_ACTION_INVITE = "invite";
+
     // -------------------------------------------------------------------------
     // Dependencies
     // -------------------------------------------------------------------------
@@ -81,6 +89,13 @@
         this.userService = userService;
     }
 
+    private SecurityService securityService;
+
+    public void setSecurityService( SecurityService securityService )
+    {
+        this.securityService = securityService;
+    }
+
     private PasswordManager passwordManager;
 
     public void setPasswordManager( PasswordManager passwordManager )
@@ -106,6 +121,13 @@
     // Input & Output
     // -------------------------------------------------------------------------
 
+    private String accountAction;
+
+    public void setAccountAction( String accountAction )
+    {
+        this.accountAction = accountAction;
+    }
+
     private String username;
 
     public void setUsername( String username )
@@ -141,6 +163,13 @@
         this.email = email;
     }
 
+    private String inviteEmail;
+
+    public void setInviteEmail( String inviteEmail )
+    {
+        this.inviteEmail = inviteEmail;
+    }
+
     private String phoneNumber;
 
     public void setPhoneNumber( String phoneNumber )
@@ -161,7 +190,7 @@
     }
 
     private String localeUi;
-    
+
     public void setLocaleUi( String localeUi )
     {
         this.localeUi = localeUi;
@@ -187,16 +216,16 @@
     {
         this.jsonAttributeValues = jsonAttributeValues;
     }
-    
+
     // -------------------------------------------------------------------------
     // Action implementation
     // -------------------------------------------------------------------------
 
     public String execute()
-        throws Exception
+            throws Exception
     {
         UserCredentials currentUserCredentials = currentUserService.getCurrentUser() != null ? currentUserService
-            .getCurrentUser().getUserCredentials() : null;
+                .getCurrentUser().getUserCredentials() : null;
 
         // ---------------------------------------------------------------------
         // Prepare values
@@ -208,6 +237,7 @@
         }
 
         username = username.trim();
+        inviteEmail = inviteEmail.trim();
 
         // ---------------------------------------------------------------------
         // Create userCredentials and user
@@ -215,18 +245,31 @@
 
         Collection<OrganisationUnit> orgUnits = selectionTreeManager.getReloadedSelectedOrganisationUnits();
 
+        UserCredentials userCredentials = new UserCredentials();
         User user = new User();
-        user.setSurname( surname );
-        user.setFirstName( firstName );
-        user.setEmail( email );
-        user.setPhoneNumber( phoneNumber );
+
+        userCredentials.setUser( user );
+        user.setUserCredentials( userCredentials );
+
+        if ( ACCOUNT_ACTION_INVITE.equals( accountAction ) )
+        {
+            user.setEmail( inviteEmail );
+
+            securityService.prepareUserForInvite ( userCredentials );
+        }
+        else
+        {
+            user.setSurname( surname );
+            user.setFirstName( firstName );
+            user.setEmail( email );
+            user.setPhoneNumber( phoneNumber );
+
+            userCredentials.setUsername( username );
+            userCredentials.setPassword( passwordManager.encodePassword( username, rawPassword ) );
+        }
+
         user.updateOrganisationUnits( new HashSet<OrganisationUnit>( orgUnits ) );
 
-        UserCredentials userCredentials = new UserCredentials();
-        userCredentials.setUser( user );
-        userCredentials.setUsername( username );
-        userCredentials.setPassword( passwordManager.encodePassword( username, rawPassword ) );
-
         for ( String id : selectedList )
         {
             UserAuthorityGroup group = userService.getUserAuthorityGroup( Integer.parseInt( id ) );
@@ -237,12 +280,10 @@
             }
         }
 
-        user.setUserCredentials( userCredentials );
-
         if ( jsonAttributeValues != null )
         {
             AttributeUtils.updateAttributeValuesFromJson( user.getAttributeValues(), jsonAttributeValues,
-                attributeService );
+                    attributeService );
         }
 
         userService.addUser( user );
@@ -252,10 +293,22 @@
         {
             selectionManager.setSelectedOrganisationUnits( orgUnits );
         }
-        
+
         userService.addUserSetting( new UserSetting( user, UserSettingService.KEY_UI_LOCALE, LocaleUtils.getLocale( localeUi ) ) );
         userService.addUserSetting( new UserSetting( user, UserSettingService.KEY_DB_LOCALE, LocaleUtils.getLocale( localeDb ) ) );
-        
+
+        if ( ACCOUNT_ACTION_INVITE.equals( accountAction ) )
+        {
+            securityService.sendRestoreMessage( userCredentials, getRootPath(), RestoreType.INVITE );
+        }
+
         return SUCCESS;
     }
+
+    private String getRootPath()
+    {
+        HttpServletRequest request = ServletActionContext.getRequest();
+
+        return ContextUtils.getContextPath( request );
+    }
 }

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/META-INF/dhis/beans.xml'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/META-INF/dhis/beans.xml	2013-10-13 16:19:17 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/META-INF/dhis/beans.xml	2014-01-17 03:48:57 +0000
@@ -8,6 +8,7 @@
 
   <bean id="org.hisp.dhis.user.action.AddUserAction" class="org.hisp.dhis.user.action.AddUserAction" scope="prototype">
     <property name="userService" ref="org.hisp.dhis.user.UserService" />
+    <property name="securityService" ref="org.hisp.dhis.security.SecurityService" />
     <property name="passwordManager" ref="org.hisp.dhis.security.PasswordManager" />
     <property name="selectionTreeManager" ref="org.hisp.dhis.oust.manager.SelectionTreeManager" />
     <property name="selectionManager" ref="org.hisp.dhis.ouwt.manager.OrganisationUnitSelectionManager" />

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/org/hisp/dhis/user/i18n_module.properties'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/org/hisp/dhis/user/i18n_module.properties	2014-01-22 09:14:39 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/org/hisp/dhis/user/i18n_module.properties	2014-01-23 15:03:16 +0000
@@ -252,6 +252,9 @@
 #-- User module ---------------------------------------------------------------#
 
 user_management=User management
+action=Action
+create_account_with_user_details=Create account with user details
+email_invitation_to_create_account=Email invitation to create account
 username=Username
 fullname=Fullname
 role=Role

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/struts.xml'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/struts.xml	2014-01-22 09:14:39 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/resources/struts.xml	2014-01-23 15:04:07 +0000
@@ -55,6 +55,7 @@
     <action name="addUser" class="org.hisp.dhis.user.action.AddUserAction">
       <result name="success" type="redirect">user.action?currentPage=${keyCurrentPage}&amp;key=${keyCurrentKey}</result>
       <result name="error" type="redirect">showAddUserForm.action</result>
+      <param name="javascripts">javascript/user.js</param>
       <param name="requiredAuthorities">F_USER_ADD</param>
     </action>
 

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/addUserForm.vm'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/addUserForm.vm	2014-01-13 16:34:27 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/addUserForm.vm	2014-01-23 15:04:07 +0000
@@ -1,5 +1,6 @@
 <script type="text/javascript">
-	jQuery(function() {
+	jQuery(function()
+    {
 	    validation2( 'addUserForm', function( form )
 	    {
 	        jQuery( "#selectedList" ).children().attr( "selected", true );
@@ -46,41 +47,58 @@
 <table>
     <col style="width: 120px"/>
 
+    #if ( $keyAccountInvite )
+    <tr>
+        <td><label>$i18n.getString( "action" )</label></td>
+        <td>
+            <select id="accountAction" name="accountAction" onchange="changeAccountAction()">
+                <option value="create" selected="selected">$i18n.getString( "create_account_with_user_details" )</option>
+                <option value="invite">$i18n.getString( "email_invitation_to_create_account" )</option>
+            </select>
+        </td>
+    </tr>
+    #end
+
 	<tr>
 		<th colspan="4">$i18n.getString( "details" )</th>
 	</tr>
 
-	<tr>
+	<tr id="usernameTR">
 		<td><label for="username">$i18n.getString( "username" ) <em title="$i18n.getString( 'required' )" class="required">*</em></label></td>
 		<td colspan="3"><input type="text" id="username" name="username" autocomplete="off"></td>
 	</tr>
 
-	<tr>
+	<tr id="rawPasswordTR">
 		<td><label for="rawPassword">$i18n.getString( "password" ) <em title="$i18n.getString( 'required' )" class="required">*</em></label></td>
 		<td colspan="3"><input type="password" id="rawPassword" name="rawPassword" autocomplete="off"></td>			
 	</tr>
 
-	<tr>
+    <tr id="retypePasswordTR">
 		<td><label for="retypePassword">$i18n.getString( "retype_password" ) <em title="$i18n.getString( 'required' )" class="required">*</em></label></td>
 		<td colspan="3"><input type="password" id="retypePassword" name="retypePassword" autocomplete="off"></td>		
 	</tr>
 
-	<tr>
+    <tr id="surnameTR">
 		<td><label for="surname">$i18n.getString( "surname" ) <em title="$i18n.getString( 'required' )" class="required">*</em></label></td>
 		<td colspan="3"><input type="text" id="surname" name="surname"></td>
 	</tr>
 
-	<tr>
+    <tr id="firstNameTR">
 		<td><label for="firstName">$i18n.getString( "firstName" ) <em title="$i18n.getString( 'required' )" class="required">*</em></label></td>
 		<td colspan="3"><input type="text" id="firstName" name="firstName"></td>		
 	</tr>
 
-	<tr>
+	<tr id="emailTR">
 		<td><label for="email">$i18n.getString( "email" )</label></td>
-		<td colspan="3"><input type="text" id="email" name="email" ></td>			
-	</tr>
-
-    <tr>
+		<td colspan="3"><input type="text" id="email" name="email" ></td>
+	</tr>
+
+	<tr id="inviteEmailTR" style="display:none">
+		<td><label for="inviteEmail">$i18n.getString( "email" ) <em title="$i18n.getString( 'required' )" class="required">*</em></label></td>
+		<td colspan="3"><input type="text" id="inviteEmail" name="inviteEmail" value="validEmail@xxxxxxxxxx" ></td>
+	</tr>
+
+    <tr id="phoneNumberTR">
         <td><label for="phoneNumber">$i18n.getString( "phone_number" )</label></td>
         <td colspan="3"><input type="text" id="phoneNumber" name="phoneNumber"></td>
     </tr>

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/javascript/user.js'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/javascript/user.js	2013-12-04 14:39:36 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/javascript/user.js	2014-01-23 15:04:07 +0000
@@ -76,6 +76,65 @@
 }
 
 // -----------------------------------------------------------------------------
+// Add user
+// -----------------------------------------------------------------------------
+
+var saved = {};
+
+function changeAccountAction()
+{
+    if( $('#accountAction').val() == 'create' )
+    {
+        $('#username').val( saved["username"] );
+        $('#rawPassword').val( saved["rawPassword"] );
+        $('#retypePassword').val( saved["retypePassword"] );
+        $('#surname').val( saved["surname"] );
+        $('#firstName').val( saved["firstName"] );
+        $('#phoneNumber').val( saved["phoneNumber"] );
+        $('#email').val( $('#inviteEmail').val() );
+        $('#inviteEmail').val( 'validEmail@xxxxxxxxxx' );
+
+        showById('usernameTR');
+        showById('rawPasswordTR');
+        showById('retypePasswordTR');
+        showById('surnameTR');
+        showById('firstNameTR');
+        showById('phoneNumberTR');
+        showById('emailTR');
+
+        hideById('inviteEmailTR');
+    }
+    else
+    {
+        hideById('usernameTR');
+        hideById('rawPasswordTR');
+        hideById('retypePasswordTR');
+        hideById('surnameTR');
+        hideById('firstNameTR');
+        hideById('phoneNumberTR');
+        hideById('emailTR');
+
+        showById('inviteEmailTR');
+
+        saved["username"] = $('#username').val();
+        saved["rawPassword"] = $('#rawPassword').val();
+        saved["retypePassword"] = $('#retypePassword').val();
+        saved["surname"] = $('#surname').val();
+        saved["firstName"] = $('#firstName').val();
+        saved["phoneNumber"] = $('#phoneNumber').val();
+
+        $('#username').val( 'nonExistingUserName_RpuECtIlVoRKTpYmEkYrAHmPtX4m1U' );
+        $('#rawPassword').val( 'validPassword_123' );
+        $('#retypePassword').val( 'validPassword_123' );
+        $('#surname').val( 'validSurname' );
+        $('#firstName').val( 'validFirstName' );
+        $('#phoneNumber').val( '5555555555' );
+        $('#inviteEmail').val( $('#email').val() );
+        $('#email').val( '' );
+    }
+}
+
+// -----------------------------------------------------------------------------
 // Remove user
 // -----------------------------------------------------------------------------
 

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/updateUserForm.vm'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/updateUserForm.vm	2013-10-08 17:16:47 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/updateUserForm.vm	2014-01-17 03:48:57 +0000
@@ -2,8 +2,9 @@
 	jQuery(function() {
 	    var rules = getValidationRules( "user" );
 	    rules["rawPassword"].required = false;
-	    rules["retypePassword"].required = false;
-	
+        rules["retypePassword"].required = false;
+        rules["inviteEmail"].required = false;
+
 	    validation2( 'updateUserForm', function( form )
 	    {
 	        jQuery( "#selectedList" ).children().attr( "selected", true );