user can change his username
authorPhiTux <redacted>
Tue, 9 Sep 2025 17:07:45 +0000 (19:07 +0200)
committerPhiTux <redacted>
Tue, 9 Sep 2025 17:07:45 +0000 (19:07 +0200)
backend/handlers/users.go
backend/main.go
frontend/src/routes/(authed)/+layout.svelte

index 47b85bbd45bb20f3c584a88ac063f43d46d9f043..b1d3bedb563444e514279b94fc952f0c06c6210d 100644 (file)
@@ -1029,3 +1029,112 @@ func CreateBackupCodes(w http.ResponseWriter, r *http.Request) {
                "available_backup_codes": available_backup_codes,
        })
 }
+
+// ChangeUsername handles changing a user's username
+func ChangeUsername(w http.ResponseWriter, r *http.Request) {
+       // Get user info from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request
+       var req struct {
+               NewUsername string `json:"new_username"`
+               Password    string `json:"password"`
+       }
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Validate input
+       req.NewUsername = strings.TrimSpace(req.NewUsername)
+       if req.NewUsername == "" {
+               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+                       "success": false,
+                       "message": "Username cannot be empty",
+               })
+               return
+       }
+
+       // Get users
+       users, err := utils.GetUsers()
+       if err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       usersList, ok := users["users"].([]any)
+       if !ok {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       // Check if new username is already taken (case-insensitive)
+       for _, u := range usersList {
+               user, ok := u.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               existingUsername, ok := user["username"].(string)
+               if !ok {
+                       continue
+               }
+
+               // Skip current user
+               if int(user["user_id"].(float64)) == userID {
+                       continue
+               }
+
+               // Case-insensitive comparison
+               if strings.EqualFold(existingUsername, req.NewUsername) {
+                       utils.JSONResponse(w, http.StatusOK, map[string]any{
+                               "success":        false,
+                               "username_taken": true,
+                       })
+                       return
+               }
+       }
+
+       // check password
+       derivedKey, availableBackupCodes, err := utils.CheckPasswordForUser(userID, req.Password)
+       if err != nil || len(derivedKey) == 0 {
+               utils.JSONResponse(w, http.StatusOK, map[string]any{
+                       "success":            false,
+                       "password_incorrect": true,
+               })
+               return
+       }
+
+       // Update username
+       for _, u := range usersList {
+               user, ok := u.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               if int(user["user_id"].(float64)) == userID {
+                       user["username"] = req.NewUsername
+                       //usersList[currentUserIndex] = user
+                       users["users"] = usersList
+                       break
+               }
+       }
+
+       // Save users file
+       if err := utils.WriteUsers(users); err != nil {
+               utils.Logger.Printf("Error saving users after username change: %v", err)
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       utils.Logger.Printf("Username changed for user ID %d to '%s'", userID, req.NewUsername)
+
+       utils.JSONResponse(w, http.StatusOK, map[string]any{
+               "success":                true,
+               "available_backup_codes": availableBackupCodes,
+       })
+}
index ed9bbc633cccead8de8d9eda65ab1fc22dac70ec..46ba82281ab7717ee9789d56d2b3c92a993a8042 100644 (file)
@@ -63,6 +63,7 @@ func main() {
        mux.HandleFunc("GET /users/getUserSettings", middleware.RequireAuth(handlers.GetUserSettings))
        mux.HandleFunc("POST /users/saveUserSettings", middleware.RequireAuth(handlers.SaveUserSettings))
        mux.HandleFunc("POST /users/changePassword", middleware.RequireAuth(handlers.ChangePassword))
+       mux.HandleFunc("POST /users/changeUsername", middleware.RequireAuth(handlers.ChangeUsername))
        mux.HandleFunc("POST /users/deleteAccount", middleware.RequireAuth(handlers.DeleteAccount))
        mux.HandleFunc("POST /users/createBackupCodes", middleware.RequireAuth(handlers.CreateBackupCodes))
        mux.HandleFunc("GET /users/statistics", middleware.RequireAuth(handlers.GetStatistics))
index dc0d1c5ae3a6e9e34c739166818365c03c4748c5..f873263041654eda9c7c653e431a8bafbfb5f89c 100644 (file)
        let deleteAccountPasswordIncorrect = $state(false);
        let showDeleteAccountSuccess = $state(false);
 
+       let newUsername = $state('');
+       let changeUsernamePassword = $state('');
+       let isChangingUsername = $state(false);
+       let changeUsernameSuccess = $state(false);
+       let changeUsernameError = $state('');
+       let changeUsernamePasswordIncorrect = $state(false);
+
        function deleteAccount() {
                if (isDeletingAccount) return;
                isDeletingAccount = true;
                        });
        }
 
+       let currentUser = $state(localStorage.getItem('user'));
+
+       function changeUsername() {
+               changeUsernameSuccess = false;
+               changeUsernameError = '';
+               changeUsernamePasswordIncorrect = false;
+
+               if (!newUsername.trim()) {
+                       changeUsernameError = $t('settings.change_username.empty_username');
+                       return;
+               }
+
+               if (isChangingUsername) return;
+               isChangingUsername = true;
+
+               axios
+                       .post(API_URL + '/users/changeUsername', {
+                               new_username: newUsername.trim(),
+                               password: changeUsernamePassword
+                       })
+                       .then((response) => {
+                               if (response.data.success) {
+                                       changeUsernameSuccess = true;
+                                       // Update localStorage with new username
+                                       localStorage.setItem('user', newUsername.trim());
+                                       // Clear form
+                                       newUsername = '';
+                                       changeUsernamePassword = '';
+                               } else {
+                                       if (response.data.password_incorrect) {
+                                               changeUsernamePasswordIncorrect = true;
+                                       } else if (response.data.username_taken) {
+                                               changeUsernameError = $t('settings.change_username.username_taken');
+                                       } else {
+                                               changeUsernameError = $t('settings.change_username.error');
+                                       }
+                               }
+                       })
+                       .catch((error) => {
+                               console.error(error);
+                               changeUsernameError = $t('settings.change_username.error');
+                       })
+                       .finally(() => {
+                               isChangingUsername = false;
+                       });
+       }
+
        let backupCodesPassword = $state('');
        let isGeneratingBackupCodes = $state(false);
        let backupCodes = $state([]);
-       let showBackupCodesPasswordIncorrect = $state(false);
        let codesCopiedSuccess = $state(false);
        let showBackupCodesError = $state(false);
 
                if (isGeneratingBackupCodes) return;
                isGeneratingBackupCodes = true;
 
-               showBackupCodesPasswordIncorrect = false;
                showBackupCodesError = false;
                backupCodes = [];
 
                                                                                                class="btn btn-primary"
                                                                                                onclick={createBackupCodes}
                                                                                                data-sveltekit-noscroll
+                                                                                               disabled={isGeneratingBackupCodes || !backupCodesPassword.trim()}
                                                                                        >
                                                                                                {$t('settings.backup_codes.generate_button')}
                                                                                                {#if isGeneratingBackupCodes}
                                                                                        </div>
                                                                                {/if}
                                                                        </div>
-                                                                       <div><h5>Username ändern</h5></div>
+                                                                       <div>
+                                                                               <h5>{$t('settings.change_username')}</h5>
+                                                                               <div class="form-text">
+                                                                                       {@html $t('settings.change_username.description')}
+                                                                               </div>
+                                                                               <div class="mb-3">
+                                                                                       {$t('settings.change_username.current_username')}: {currentUser}
+                                                                               </div>
+
+                                                                               <form onsubmit={changeUsername}>
+                                                                                       <div class="form-floating mb-3">
+                                                                                               <input
+                                                                                                       type="text"
+                                                                                                       class="form-control"
+                                                                                                       id="newUsername"
+                                                                                                       placeholder={$t('settings.change_username.new_username')}
+                                                                                                       bind:value={newUsername}
+                                                                                                       disabled={isChangingUsername}
+                                                                                               />
+                                                                                               <label for="newUsername"
+                                                                                                       >{$t('settings.change_username.new_username')}</label
+                                                                                               >
+                                                                                       </div>
+                                                                                       <div class="form-floating mb-3">
+                                                                                               <input
+                                                                                                       type="password"
+                                                                                                       class="form-control"
+                                                                                                       id="changeUsernamePassword"
+                                                                                                       placeholder={$t('settings.password.current_password')}
+                                                                                                       bind:value={changeUsernamePassword}
+                                                                                                       disabled={isChangingUsername}
+                                                                                               />
+                                                                                               <label for="changeUsernamePassword"
+                                                                                                       >{$t('settings.password.current_password')}</label
+                                                                                               >
+                                                                                       </div>
+                                                                                       <button
+                                                                                               class="btn btn-primary"
+                                                                                               onclick={changeUsername}
+                                                                                               disabled={isChangingUsername ||
+                                                                                                       !newUsername.trim() ||
+                                                                                                       !changeUsernamePassword.trim()}
+                                                                                       >
+                                                                                               {#if isChangingUsername}
+                                                                                                       <span class="spinner-border spinner-border-sm me-2"></span>
+                                                                                               {/if}
+                                                                                               {$t('settings.change_username.button')}
+                                                                                       </button>
+                                                                               </form>
+
+                                                                               {#if changeUsernameSuccess}
+                                                                                       <div class="alert alert-success mt-2" role="alert" transition:slide>
+                                                                                               {$t('settings.change_username.success')}
+                                                                                       </div>
+                                                                               {/if}
+                                                                               {#if changeUsernamePasswordIncorrect}
+                                                                                       <div class="alert alert-danger mt-2" role="alert" transition:slide>
+                                                                                               {$t('settings.password.current_password_incorrect')}
+                                                                                       </div>
+                                                                               {:else if changeUsernameError}
+                                                                                       <div class="alert alert-danger mt-2" role="alert" transition:slide>
+                                                                                               {changeUsernameError}
+                                                                                       </div>
+                                                                               {/if}
+                                                                       </div>
                                                                        <div>
                                                                                <h5>{$t('settings.delete_account')}</h5>
                                                                                <p>
git clone https://git.99rst.org/PROJECT