← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 14031: Support account invite with predefined username

 

------------------------------------------------------------
revno: 14031
committer: Jim Grace <jimgrace@xxxxxxxxx>
branch nick: dhis2
timestamp: Fri 2014-02-14 09:09:37 -0500
message:
  Support account invite with predefined username
added:
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/RestoreOptions.java
modified:
  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/RestoreType.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/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/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/useraccount/action/IsInviteTokenValidAction.java
  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/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


--
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-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-02-04 11:02:32 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/DefaultSecurityService.java	2014-02-14 14:09:37 +0000
@@ -114,24 +114,31 @@
             return false;
         }
 
-        String username = "invitedUser_" + CodeGenerator.generateCode( INVITED_USERNAME_UNIQUE_LENGTH );
+        if ( credentials.getUsername().isEmpty() )
+        {
+            String username = "user_invitation_" + CodeGenerator.generateCode( INVITED_USERNAME_UNIQUE_LENGTH );
+
+            credentials.setUsername( username );
+        }
+
         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 ) );
+        credentials.setPassword( passwordManager.encodePassword( credentials.getUsername(), rawPassword ) );
 
         return true;
     }
 
-    public boolean sendRestoreMessage( UserCredentials credentials, String rootPath, RestoreType restoreType )
+    public boolean sendRestoreMessage( UserCredentials credentials, String rootPath, RestoreOptions restoreOptions )
     {
         if ( credentials == null || rootPath == null )
         {
             return false;
         }
 
+        RestoreType restoreType = restoreOptions.getRestoreType();
+
         if ( credentials.getUser() == null || credentials.getUser().getEmail() == null )
         {
             log.info( "Could not send " + restoreType.name() + " message as user does not exist or has no email: " + credentials );
@@ -156,7 +163,7 @@
             return false;
         }
 
-        String[] result = initRestore( credentials, restoreType );
+        String[] result = initRestore( credentials, restoreOptions );
 
         Set<User> users = new HashSet<User>();
         users.add( credentials.getUser() );
@@ -177,14 +184,16 @@
         return true;
     }
 
-    public String[] initRestore( UserCredentials credentials, RestoreType restoreType )
+    public String[] initRestore( UserCredentials credentials, RestoreOptions restoreOptions )
     {
-        String token = restoreType.getTokenPrefix() + CodeGenerator.generateCode( RESTORE_TOKEN_LENGTH );
+        String token = restoreOptions.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 );
 
+        RestoreType restoreType = restoreOptions.getRestoreType();
+
         Date expiry = new Cal().now().add( restoreType.getExpiryIntervalType(), restoreType.getExpiryIntervalCount() ).time();
 
         credentials.setRestoreToken( hashedToken );
@@ -197,6 +206,11 @@
         return result;
     }
 
+    public RestoreOptions getRestoreOptions( String token )
+    {
+        return RestoreOptions.getRestoreOptions( token );
+    }
+
     public boolean restore( UserCredentials credentials, String token, String code, String newPassword, RestoreType restoreType )
     {
         if ( credentials == null || token == null || code == null || newPassword == null
@@ -239,20 +253,28 @@
 
     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 );
+        if ( credentials == null || token == null || restoreType == null )
+        {
+            return false;
+        }
+
+        RestoreOptions restoreOptions = RestoreOptions.getRestoreOptions( token );
+
+        if ( restoreOptions == null )
+        {
+            log.info( "Can't parse restore options for " + restoreType.name() + " from token " + token + " for user " + credentials );
+            return false;
+        }
+
+        if ( restoreType != restoreOptions.getRestoreType() )
+        {
+            log.info( "Wrong prefix for restore type " + restoreType.name() + " on token " + token + " for user " + credentials );
             return false;
         }
 
         if ( credentials.getRestoreToken() == null )
         {
-            log.info( "Could not verify token as user has no token: " + credentials );
+            log.info( "Could not verify token for " + restoreType.name() + " as user has no token: " + credentials );
             return false;
         }
 

=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/RestoreOptions.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/RestoreOptions.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/RestoreOptions.java	2014-02-14 14:09:37 +0000
@@ -0,0 +1,108 @@
+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.
+ */
+
+/**
+ * Options for user account restore operation. These options are represented
+ * in the user account restore email as a prefix to the restore token.
+ * This token is hashed, and the hash is stored in the database. This means
+ * that the options cannot be hacked to change them, because then the token
+ * would no longer match the saved hash in the database.
+ *
+ * @author Jim Grace
+ */
+
+public enum RestoreOptions
+{
+    RECOVER_PASSWORD_OPTION ( "R", RestoreType.RECOVER_PASSWORD, false ),
+    INVITE_WITH_USERNAME_CHOICE ( "IC", RestoreType.INVITE, true ),
+    INVITE_WITH_DEFINED_USERNAME ( "ID", RestoreType.INVITE, false );
+
+    /**
+     * Prefix to be used on restore token, to represent this set of options.
+     */
+    private final String tokenPrefix;
+
+    /**
+     * The type of restore operation to perform (i.e. password recovery
+     * or invite to create account.)
+     */
+    private final RestoreType restoreType;
+
+    /**
+     * Defines whether the user can choose a username at the time of restore.
+     */
+    private final boolean usernameChoice;
+
+    // -------------------------------------------------------------------------
+    // Constructor
+    // -------------------------------------------------------------------------
+
+    RestoreOptions( String tokenPrefix, RestoreType restoreType, boolean usernameChoice )
+    {
+        this.tokenPrefix = tokenPrefix;
+        this.restoreType = restoreType;
+        this.usernameChoice = usernameChoice;
+    }
+
+    // -------------------------------------------------------------------------
+    // Get Restore Options from a token string
+    // -------------------------------------------------------------------------
+
+    static public RestoreOptions getRestoreOptions( String token )
+    {
+        for (RestoreOptions ro : RestoreOptions.values())
+        {
+            if ( token.startsWith( ro.getTokenPrefix() ) )
+            {
+                return ro;
+            }
+        }
+
+        return null;
+    }
+
+    // -------------------------------------------------------------------------
+    // Getters
+    // -------------------------------------------------------------------------
+
+    public String getTokenPrefix()
+    {
+        return tokenPrefix;
+    }
+
+    public RestoreType getRestoreType()
+    {
+        return restoreType;
+    }
+
+    public boolean isUsernameChoice()
+    {
+        return usernameChoice;
+    }
+}

=== modified 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	2014-01-17 03:48:57 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/RestoreType.java	2014-02-14 14:09:37 +0000
@@ -37,14 +37,8 @@
 
 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;
+    RECOVER_PASSWORD( Calendar.HOUR_OF_DAY, 1, "restore_message", "User account restore confirmation", "restore.action" ),
+    INVITE( Calendar.MONTH, 3, "invite_message", "Create DHIS 2 user account invitation", "invite.action" );
 
     /**
      * Type of Calendar interval before the restore expires.
@@ -75,10 +69,9 @@
     // Constructor
     // -------------------------------------------------------------------------
 
-    RestoreType( String tokenPrefix, int expiryIntervalType, int expiryIntervalCount,
+    RestoreType( int expiryIntervalType, int expiryIntervalCount,
                  String emailTemplate, String emailSubject, String action )
     {
-        this.tokenPrefix = tokenPrefix;
         this.expiryIntervalType = expiryIntervalType;
         this.expiryIntervalCount = expiryIntervalCount;
         this.emailTemplate = emailTemplate;
@@ -90,11 +83,6 @@
     // Getters
     // -------------------------------------------------------------------------
 
-    public String getTokenPrefix()
-    {
-        return tokenPrefix;
-    }
-
     public int getExpiryIntervalType()
     {
         return expiryIntervalType;

=== 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-25 18:46:36 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/security/SecurityService.java	2014-02-14 14:09:37 +0000
@@ -55,11 +55,11 @@
      *
      * @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).
+     * @param restoreOptions restore options, including type of restore.
      * @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, RestoreType restoreType );
+    boolean sendRestoreMessage( UserCredentials credentials, String rootPath, RestoreOptions restoreOptions );
 
     /**
      * Populates the restoreToken and restoreCode property of the given
@@ -68,11 +68,19 @@
      * on the restore type. Changes are persisted.
      *
      * @param credentials the user credentials.
-     * @param restoreType type of restore operation (e.g. pw recovery, invite).
+     * @param restoreOptions restore options, including type of restore.
      * @return an array where index 0 is the clear-text token and index 1 the
      *         clear-text code.
      */
-    String[] initRestore( UserCredentials credentials, RestoreType restoreType );
+    String[] initRestore( UserCredentials credentials, RestoreOptions restoreOptions );
+
+    /**
+     * Gets the restore options by parsing them from a restore token string.
+     *
+     * @param token the restore token.
+     * @return the restore options.
+     */
+    RestoreOptions getRestoreOptions( String token );
 
     /**
      * Tests whether the given token and code are valid for the given user name.

=== 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-25 18:46:36 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/security/SecurityServiceTest.java	2014-02-14 14:09:37 +0000
@@ -83,8 +83,10 @@
     @Test
     public void testRestoreRecoverPassword()
     {
-        String[] result = securityService.initRestore( credentials, RestoreType.RECOVER_PASSWORD );
-        
+        String[] result = securityService.initRestore( credentials, RestoreOptions.RECOVER_PASSWORD_OPTION );
+
+        assertEquals( 2, result.length );
+
         String token = result[0];
         String code = result[1];
 
@@ -94,6 +96,12 @@
         assertNotNull( credentials.getRestoreCode() );
         assertNotNull( credentials.getRestoreExpiry() );
 
+        RestoreOptions restoreOptions = securityService.getRestoreOptions( token );
+
+        assertEquals( RestoreOptions.RECOVER_PASSWORD_OPTION, restoreOptions );
+        assertEquals( RestoreType.RECOVER_PASSWORD, restoreOptions.getRestoreType() );
+        assertEquals( false, restoreOptions.isUsernameChoice() );
+
         //
         // verifyToken()
         //
@@ -144,7 +152,10 @@
     @Test
     public void testRestoreInvite()
     {
-        String[] result = securityService.initRestore( credentials, RestoreType.INVITE );
+        String[] result = securityService.initRestore( credentials, RestoreOptions.INVITE_WITH_DEFINED_USERNAME );
+
+        assertEquals( 2, result.length );
+
         String token = result[0];
         String code = result[1];
 
@@ -154,6 +165,12 @@
         assertNotNull( credentials.getRestoreCode() );
         assertNotNull( credentials.getRestoreExpiry() );
 
+        RestoreOptions restoreOptions = securityService.getRestoreOptions( token );
+
+        assertEquals( RestoreOptions.INVITE_WITH_DEFINED_USERNAME, restoreOptions );
+        assertEquals( RestoreType.INVITE, restoreOptions.getRestoreType() );
+        assertEquals( false, restoreOptions.isUsernameChoice() );
+
         //
         // verifyToken()
         //
@@ -200,4 +217,20 @@
 
         assertEquals( hashedPassword, credentials.getPassword() );
     }
+
+    @Test
+    public void testRestoreInviteWithUsernameChoice()
+    {
+        String[] result = securityService.initRestore( credentials, RestoreOptions.INVITE_WITH_USERNAME_CHOICE );
+
+        assertEquals( 2, result.length );
+
+        String token = result[0];
+
+        RestoreOptions restoreOptions = securityService.getRestoreOptions( token );
+
+        assertEquals( RestoreOptions.INVITE_WITH_USERNAME_CHOICE, restoreOptions );
+        assertEquals( RestoreType.INVITE, restoreOptions.getRestoreType() );
+        assertEquals( true, restoreOptions.isUsernameChoice() );
+    }
 }

=== 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-27 17:20:36 +0000
+++ dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/api/controller/AccountController.java	2014-02-14 14:09:37 +0000
@@ -36,6 +36,7 @@
 import org.hisp.dhis.configuration.ConfigurationService;
 import org.hisp.dhis.organisationunit.OrganisationUnit;
 import org.hisp.dhis.security.PasswordManager;
+import org.hisp.dhis.security.RestoreOptions;
 import org.hisp.dhis.security.RestoreType;
 import org.hisp.dhis.security.SecurityService;
 import org.hisp.dhis.setting.SystemSettingManager;
@@ -132,7 +133,7 @@
             return "User does not exist: " + username;
         }
         
-        boolean recover = securityService.sendRestoreMessage( credentials, rootPath, RestoreType.RECOVER_PASSWORD );
+        boolean recover = securityService.sendRestoreMessage( credentials, rootPath, RestoreOptions.RECOVER_PASSWORD_OPTION );
 
         if ( !recover )
         {
@@ -204,9 +205,9 @@
         @RequestParam String email,
         @RequestParam String phoneNumber,
         @RequestParam String employer,
-        @RequestParam String inviteUsername,
-        @RequestParam String inviteToken,
-        @RequestParam String inviteCode,
+        @RequestParam( required = false ) String inviteUsername,
+        @RequestParam( required = false ) String inviteToken,
+        @RequestParam( required = false ) String inviteCode,
         @RequestParam( value = "recaptcha_challenge_field", required = false ) String recapChallenge,
         @RequestParam( value = "recaptcha_response_field", required = false ) String recapResponse,
         HttpServletRequest request,
@@ -214,7 +215,11 @@
     {
         UserCredentials credentials = null;
 
-        boolean invitedByEmail = !inviteUsername.isEmpty();
+        boolean invitedByEmail = ( inviteUsername != null && !inviteUsername.isEmpty() );
+
+        log.info( "AccountController: inviteUsername = " + inviteUsername );
+
+        boolean canChooseUsername = true;
 
         if ( invitedByEmail )
         {
@@ -239,6 +244,10 @@
                 response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
                 return "Invitation code not valid";
             }
+
+            RestoreOptions restoreOptions = securityService.getRestoreOptions( inviteToken );
+
+            canChooseUsername = restoreOptions.isUsernameChoice();
         }
         else
         {
@@ -277,7 +286,7 @@
 
         UserCredentials usernameAlreadyTakenCredentials = userService.getUserCredentialsByUsername( username );
 
-        if ( usernameAlreadyTakenCredentials != null )
+        if ( canChooseUsername && usernameAlreadyTakenCredentials != null )
         {
             response.setStatus( HttpServletResponse.SC_BAD_REQUEST );
             return "User name is already taken";
@@ -387,7 +396,15 @@
             user.setPhoneNumber( phoneNumber );
             user.setEmployer( employer );
 
-            credentials.setUsername( username );
+            if ( canChooseUsername )
+            {
+                credentials.setUsername( username );
+            }
+            else
+            {
+                username = credentials.getUsername();
+            }
+
             credentials.setPassword( passwordManager.encodePassword( username, password ) );
 
             userService.updateUser( user );

=== 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-17 03:48:57 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/javascripts/validationRules.js	2014-02-14 14:09:37 +0000
@@ -4,6 +4,9 @@
             "required" : true,
             "rangelength" : [ 2, 140 ]
         },
+        "inviteUsername" : {
+            "rangelength" : [ 2, 140 ]
+        },
         "firstName" : {
             "required" : true,
             "rangelength" : [ 2, 140 ]

=== 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	2014-01-23 15:04:07 +0000
+++ dhis-2/dhis-web/dhis-web-commons-resources/src/main/webapp/dhis-web-commons/useraccount/account.vm	2014-02-14 14:09:37 +0000
@@ -28,23 +28,34 @@
 
 <table>
 
-    <tr #if( $accountAction != "invited" ) style="display:none" #end>
+#if( $accountAction == "invited" )
+    <tr>
         <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>
+            <input type="hidden" id="inviteUsername" name="inviteUsername" value="$username">
+            <input type="hidden" id="inviteToken" name="inviteToken" value="$token">
         </td>
     </tr>
+#end
 
     <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">
-            <input type="text" id="surname" name="surname" autocomplete="off" style="width:11.7em" placeholder="Last"></td>
+        <td>
+            <input type="text" id="firstName" name="firstName" autocomplete="off" style="width:11.7em; margin-right:7px;" placeholder="First">
+            <input type="text" id="surname" name="surname" autocomplete="off" style="width:11.7em" placeholder="Last">
+        </td>
     </tr>
     <tr>
         <td><label id="label_username" for="username">$i18n.getString( "user_name" )</label></td>
-        <td><input type="text" id="username" name="username" autocomplete="off"></td>
+        <td>
+            <input type="text" id="username" name="username" autocomplete="off" #if( $usernameChoice == "false" ) style="display:none" value="nonExistingUserName_RpuECtIlVoRKTpYmEkYrAHmPtX4m1U" #end>
+
+#if( $usernameChoice == "false" )
+            <input type="text" id="assignedUsername" name="assignedUsername" disabled="disabled" value="$username">
+#end
+
+        </td>
     </tr>
     <tr>
         <td><label id="label_password" for="password">$i18n.getString( "password" )</label></td>

=== modified 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	2014-01-23 15:04:07 +0000
+++ dhis-2/dhis-web/dhis-web-commons/src/main/java/org/hisp/dhis/useraccount/action/IsInviteTokenValidAction.java	2014-02-14 14:09:37 +0000
@@ -28,6 +28,7 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import org.hisp.dhis.security.RestoreOptions;
 import org.hisp.dhis.security.RestoreType;
 import org.hisp.dhis.security.SecurityService;
 import org.hisp.dhis.setting.SystemSettingManager;
@@ -98,6 +99,13 @@
         return accountAction;
     }
 
+    private String usernameChoice;
+
+    public String getUsernameChoice()
+    {
+        return usernameChoice;
+    }
+
     private String email;
 
     public String getEmail()
@@ -125,6 +133,13 @@
 
         email = userCredentials.getUser().getEmail();
 
+        RestoreOptions restoreOptions = securityService.getRestoreOptions( token );
+
+        if ( restoreOptions != null )
+        {
+            usernameChoice = Boolean.toString( restoreOptions.isUsernameChoice() );
+        }
+
         boolean verified = securityService.verifyToken( userCredentials, token, RestoreType.INVITE );
 
         return verified ? SUCCESS : ERROR;

=== 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	2014-01-25 18:46:36 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/java/org/hisp/dhis/user/action/AddUserAction.java	2014-02-14 14:09:37 +0000
@@ -42,7 +42,7 @@
 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.RestoreOptions;
 import org.hisp.dhis.security.SecurityService;
 import org.hisp.dhis.system.util.AttributeUtils;
 import org.hisp.dhis.system.util.LocaleUtils;
@@ -135,6 +135,13 @@
         this.username = username;
     }
 
+    private String inviteUsername;
+
+    public void setInviteUsername( String inviteUsername )
+    {
+        this.inviteUsername = inviteUsername;
+    }
+
     private String rawPassword;
 
     public void setRawPassword( String rawPassword )
@@ -237,6 +244,7 @@
         }
 
         username = username.trim();
+        inviteUsername = inviteUsername.trim();
         inviteEmail = inviteEmail.trim();
 
         // ---------------------------------------------------------------------
@@ -251,8 +259,11 @@
         userCredentials.setUser( user );
         user.setUserCredentials( userCredentials );
 
+        userCredentials.setUsername( username );
+
         if ( ACCOUNT_ACTION_INVITE.equals( accountAction ) )
         {
+            userCredentials.setUsername( inviteUsername );
             user.setEmail( inviteEmail );
 
             securityService.prepareUserForInvite ( userCredentials );
@@ -264,7 +275,6 @@
             user.setEmail( email );
             user.setPhoneNumber( phoneNumber );
 
-            userCredentials.setUsername( username );
             userCredentials.setPassword( passwordManager.encodePassword( username, rawPassword ) );
         }
 
@@ -299,7 +309,9 @@
 
         if ( ACCOUNT_ACTION_INVITE.equals( accountAction ) )
         {
-            securityService.sendRestoreMessage( userCredentials, getRootPath(), RestoreType.INVITE );
+            RestoreOptions restoreOptions = inviteUsername.isEmpty() ? RestoreOptions.INVITE_WITH_USERNAME_CHOICE : RestoreOptions.INVITE_WITH_DEFINED_USERNAME;
+
+            securityService.sendRestoreMessage( userCredentials, getRootPath(), restoreOptions );
         }
 
         return SUCCESS;

=== 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-02-04 09:38:44 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/addUserForm.vm	2014-02-14 14:09:37 +0000
@@ -63,12 +63,22 @@
 		<th colspan="4">$i18n.getString( "details" )</th>
 	</tr>
 
-	<tr class="account">
+    <tr class="invite" 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 class="invite" style="display:none">
+        <td><label for="inviteUsername">$i18n.getString( "username" )</label></td>
+        <td colspan="3"><input type="text" id="inviteUsername" name="inviteUsername"></td>
+    </tr>
+
+    <tr class="account">
 		<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 class="account">
+    <tr class="account">
 		<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>
@@ -90,12 +100,7 @@
 
 	<tr class="account">
 		<td><label for="email">$i18n.getString( "email" )</label></td>
-		<td colspan="3"><input type="text" id="email" name="email" ></td>
-	</tr>
-
-	<tr class="invite" 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>
+		<td colspan="3"><input type="text" id="email" name="email"></td>
 	</tr>
 
     <tr class="account">

=== 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	2014-02-04 09:38:44 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-user/src/main/webapp/dhis-web-maintenance-user/javascript/user.js	2014-02-14 14:09:37 +0000
@@ -85,7 +85,8 @@
 {
     if( $('#accountAction').val() == 'create' )
     {
-        $('#username').val( saved["username"] );
+        $('#username').val( $('#inviteUsername').val() );
+        $('#inviteUsername').val( 'nonExistingUserName_RpuECtIlVoRKTpYmEkYrAHmPtX4m1U' );
         $('#rawPassword').val( saved["rawPassword"] );
         $('#retypePassword').val( saved["retypePassword"] );
         $('#surname').val( saved["surname"] );
@@ -102,13 +103,13 @@
         $('.account').hide();
         $('.invite').show();
 
-        saved["username"] = $('#username').val();
         saved["rawPassword"] = $('#rawPassword').val();
         saved["retypePassword"] = $('#retypePassword').val();
         saved["surname"] = $('#surname').val();
         saved["firstName"] = $('#firstName').val();
         saved["phoneNumber"] = $('#phoneNumber').val();
 
+        $('#inviteUsername').val( $('#username').val() );
         $('#username').val( 'nonExistingUserName_RpuECtIlVoRKTpYmEkYrAHmPtX4m1U' );
         $('#rawPassword').val( 'validPassword_123' );
         $('#retypePassword').val( 'validPassword_123' );