2 Commits a9d3ac3c0f ... 8894ffe205

Autore SHA1 Messaggio Data
  Toby Chui 8894ffe205 Add multi user delete 4 giorni fa
  Toby Chui c8a0667292 Fixed #206 4 giorni fa

+ 26 - 13
src/file_system.go

@@ -1274,10 +1274,17 @@ func system_fs_handleNewObjects(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
+		//Check if the filename contains web-unsafe characters
+		if !utils.FilenameIsWebSafe(filename) {
+			utils.SendErrorResponse(w, "Filename contains illegal characters")
+			return
+		}
+
 		//Check if the file already exists. If yes, fix its filename.
 		newfilePath := filepath.ToSlash(filepath.Join(rpath, filename))
 
-		if fileType == "file" {
+		switch fileType {
+		case "file":
 			for fshAbs.FileExists(newfilePath) {
 				utils.SendErrorResponse(w, "Given filename already exists")
 				return
@@ -1300,7 +1307,7 @@ func system_fs_handleNewObjects(w http.ResponseWriter, r *http.Request) {
 				return
 			}
 
-		} else if fileType == "folder" {
+		case "folder":
 			if fshAbs.FileExists(newfilePath) {
 				utils.SendErrorResponse(w, "Given folder already exists")
 				return
@@ -1914,15 +1921,22 @@ func system_fs_handleOpr(w http.ResponseWriter, r *http.Request) {
 
 				//Check if the target dir is not readonly
 				accmode := userinfo.GetPathAccessPermission(string(vsrcFile))
-				if accmode == arozfs.FsReadOnly {
+				switch accmode {
+				case arozfs.FsReadOnly:
 					utils.SendErrorResponse(w, "This directory is Read Only")
 					return
-				} else if accmode == arozfs.FsDenied {
+				case arozfs.FsDenied:
 					utils.SendErrorResponse(w, "Access Denied")
 					return
 				}
 
 				thisFilename := filepath.Base(newFilenames[i])
+
+				//Check if the new filename contains web-unsafe characters
+				if !utils.FilenameIsWebSafe(thisFilename) {
+					utils.SendErrorResponse(w, "Filename contains illegal characters")
+					return
+				}
 				//Check if the name already exists. If yes, return false
 				if srcFshAbs.FileExists(filepath.Join(filepath.Dir(rsrcFile), thisFilename)) {
 					utils.SendErrorResponse(w, "File already exists")
@@ -2516,8 +2530,6 @@ func system_fs_getFileProperties(w http.ResponseWriter, r *http.Request) {
 	Usage: Pass in dir like the following examples:
 	AOR:/Desktop	<= Open /user/{username}/Desktop
 	S1:/			<= Open {uuid=S1}/
-
-
 */
 
 func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
@@ -2526,8 +2538,6 @@ func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
 		utils.SendErrorResponse(w, err.Error())
 		return
 	}
-	//Commented this line to handle dirname that contains "+" sign
-	//currentDir, _ = url.QueryUnescape(currentDir)
 	sortMode, _ := utils.PostPara(r, "sort")
 	showHidden, _ := utils.PostPara(r, "showHidden")
 
@@ -2543,8 +2553,8 @@ func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	//Pad a slash at the end of currentDir if not exists
-	if currentDir[len(currentDir)-1:] != "/" {
+	// Pad a slash at the end of currentDir if not exists
+	if !strings.HasSuffix(currentDir, "/") {
 		currentDir = currentDir + "/"
 	}
 
@@ -2556,12 +2566,12 @@ func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
 
 	fshAbs := fsh.FileSystemAbstraction
 
-	//Normal file systems
 	realpath, err := fshAbs.VirtualPathToRealPath(subpath, userinfo.Username)
 	if err != nil {
 		utils.SendErrorResponse(w, err.Error())
 		return
 	}
+
 	if !fshAbs.FileExists(realpath) {
 		//Path not exists
 		userRoot, _ := fshAbs.VirtualPathToRealPath("/", userinfo.Username)
@@ -2599,7 +2609,6 @@ func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
 	//Sorting use list
 	realpathList := []string{}
 	fileInfoList := []fs.FileInfo{}
-
 	for _, f := range files {
 		//Check if it is hidden file
 		isHidden, _ := hidden.IsHidden(f.Name(), false)
@@ -2608,6 +2617,11 @@ func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
 			continue
 		}
 
+		//Check if this file contains invalid characters
+		if !utils.FilenameIsWebSafe(f.Name()) {
+			continue
+		}
+
 		//Check if this is an aodb file
 		if f.Name() == "aofs.db" || f.Name() == "aofs.db.lock" {
 			//Database file (reserved)
@@ -2666,7 +2680,6 @@ func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
 
 	jsonString, _ := json.Marshal(results)
 	utils.SendJSONResponse(w, string(jsonString))
-
 }
 
 // Handle getting a hash from a given contents in the given path

+ 10 - 0
src/mod/utils/utils.go

@@ -226,3 +226,13 @@ func TemplateApply(templateString string, data map[string]string) string {
 
 	return string(content)
 }
+
+func FilenameIsWebSafe(filename string) bool {
+	unsafeChars := []string{"/", "\\", "?", "%", "*", ":", "|", "\"", "<", ">"}
+	for _, char := range unsafeChars {
+		if strings.Contains(filename, char) {
+			return false
+		}
+	}
+	return true
+}

+ 71 - 21
src/user.go

@@ -9,6 +9,7 @@ package main
 import (
 	"encoding/base64"
 	"encoding/json"
+	"fmt"
 	"image"
 	"image/gif"
 	"image/jpeg"
@@ -100,20 +101,29 @@ func UserSystemInit() {
 
 // Remove a user from the system
 func user_handleUserRemove(w http.ResponseWriter, r *http.Request) {
-	username, err := utils.PostPara(r, "username")
-	if err != nil {
-		utils.SendErrorResponse(w, "Username not defined")
-		return
-	}
+	// Check if multiple usernames are provided (new format)
+	usernamesJSON, err := utils.PostPara(r, "usernames")
+	var usernames []string
 
-	if !authAgent.UserExists(username) {
-		utils.SendErrorResponse(w, "User not exists")
-		return
+	if err == nil && usernamesJSON != "" {
+		// New format: multiple usernames as JSON array
+		err = json.Unmarshal([]byte(usernamesJSON), &usernames)
+		if err != nil {
+			utils.SendErrorResponse(w, "Invalid usernames format")
+			return
+		}
+	} else {
+		// Old format: single username (for backward compatibility)
+		username, err := utils.PostPara(r, "username")
+		if err != nil {
+			utils.SendErrorResponse(w, "Username not defined")
+			return
+		}
+		usernames = []string{username}
 	}
 
-	userinfo, err := userHandler.GetUserInfoFromUsername(username)
-	if err != nil {
-		utils.SendErrorResponse(w, err.Error())
+	if len(usernames) == 0 {
+		utils.SendErrorResponse(w, "No usernames provided")
 		return
 	}
 
@@ -124,18 +134,58 @@ func user_handleUserRemove(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if currentUserinfo.Username == userinfo.Username {
-		//This user has not logged in
-		utils.SendErrorResponse(w, "You can't remove yourself")
-		return
-	}
+	// Process each user for removal
+	var errors []string
+	var successCount int
 
-	//Clear Core User Data
-	userinfo.RemoveUser()
+	for _, username := range usernames {
+		// Check if user exists
+		if !authAgent.UserExists(username) {
+			errors = append(errors, username+": User not exists")
+			continue
+		}
+
+		// Get user info
+		userinfo, err := userHandler.GetUserInfoFromUsername(username)
+		if err != nil {
+			errors = append(errors, username+": "+err.Error())
+			continue
+		}
+
+		// Check if user is trying to remove themselves
+		if currentUserinfo.Username == userinfo.Username {
+			errors = append(errors, username+": You can't remove yourself")
+			continue
+		}
+
+		// Remove the user
+		userinfo.RemoveUser()
+
+		// Clean up FileSystem preferences
+		system_fs_removeUserPreferences(username)
 
-	//Clearn Up FileSystem preferences
-	system_fs_removeUserPreferences(username)
-	utils.SendOK(w)
+		successCount++
+	}
+
+	// Send response
+	if len(errors) > 0 {
+		if successCount == 0 {
+			// All removals failed
+			utils.SendErrorResponse(w, strings.Join(errors, "; "))
+		} else {
+			// Partial success
+			response := map[string]interface{}{
+				"success": successCount,
+				"errors":  errors,
+				"message": fmt.Sprintf("%d user(s) removed successfully, %d failed", successCount, len(errors)),
+			}
+			js, _ := json.Marshal(response)
+			utils.SendJSONResponse(w, string(js))
+		}
+	} else {
+		// All successful
+		utils.SendOK(w)
+	}
 }
 
 func user_handleUserEdit(w http.ResponseWriter, r *http.Request) {

+ 124 - 91
src/web/SystemAO/file_system/file_explorer.html

@@ -596,17 +596,98 @@
             //Intiiation functions
             $(document).ready(function(){
                 $("#contextmenu").css("display", "hidden");
+                
+                //Function to complete initialization after applocale is ready
+                function completeInitialization(){
+                    initRootDirs();
+                    initSystemInfo();
+                    initUploadMode();
+                    $(".dropdown").dropdown();
+                    $("#sortingMethodSelector").dropdown("set selected", sortMode);
+                    updateSelectedObjectsCount();
+                    initWindowSizes(false);
+                    
+                    //Initialize view mode buttons
+                    updateViewmodeButtons();
+                    
+                    //Initialize system theme
+                    loadPreference("file_explorer/theme",function(data){
+                        if (data.error === undefined){
+                            if (data == "darkTheme"){
+                                toggleDarkTheme();
+                            }else{
+                                //White theme
+                            
+                            }
+                        }
+                    });
+
+                    //Initialize properties view
+                    if (localStorage.getItem("file_explorer/viewProperties") == "true"){
+                        $("#togglePropertiesViewBtn").click();
+                    }
+
+                    //Initialize directory views based on hash
+                    if (window.location.hash != ""){
+                        //Check if the hash is standard open protocol. If yes, translate it
+                        if (ao_module_loadInputFiles() === null){
+                            //Window location hash set. List the desire directory
+                            currentPath = window.location.hash.substring(1,window.location.hash.length);
+                            if (currentPath.substring(currentPath.length -1) != "/"){
+                                currentPath = currentPath + "/";
+                            }
+                            currentPath = decodeURIComponent(currentPath);
+                            loadListModeFromDB(function(){
+                                listDirectory(currentPath);
+                            });
+                            
+                        }else{
+                            //This is ao_module load file input. Handle the file opening
+                            var filelist = ao_module_loadInputFiles();
+                            if (filelist.length > 0){
+                                filelist = filelist[0];
+                                //Check if this is folder or file. Only opendir when it is folder
+                                //Updates 27-12-2020: Open folder and highlight the file if it is file
+                                if (filelist.filename.includes(".") == false){
+                                    //Try to open it and overwrite the hash to filesystem hash
+                                    loadListModeFromDB(function(){
+                                        listDirectory(filelist.filepath);
+                                    });
+                                }else{
+                                    //File. Open its parent folder and highlight the target file if exists
+                                    var parentdir = filelist.filepath.split("/");
+                                    let focusFilename = JSON.parse(JSON.stringify(filelist.filename));
+                                    parentdir.pop();
+                                    parentdir = parentdir.join("/");
+                                    loadListModeFromDB(function(){
+                                        listDirectory(parentdir, function(){
+                                            if (focusFilename != ""){
+                                                //Timeout to give the DOM time to render
+                                                //DO NOT REPLACE THIS WITH listDirectoryAndHighlight
+                                                //Additional delay are required on page load
+                                                setTimeout(function(){
+                                                    focusFileObject(focusFilename);
+                                                }, 300);
+                                                
+                                            }
+                                        })
+                                    });
+                                }
+                            }
+                        }
+                    }else{
+                        //Initialized directory views
+                        loadListModeFromDB(function(){
+                            listDirectory(currentPath);
+                        });
+                    }
+                }
+                
                 if (applocale){
                     //Applocale found. Do localization
                     applocale.init("../locale/file_explorer.json", function(){
                         applocale.translate();
-                        initRootDirs();
-                        initSystemInfo();
-                        initUploadMode();
-                        $(".dropdown").dropdown();
-                        $("#sortingMethodSelector").dropdown("set selected", sortMode);
-                        updateSelectedObjectsCount();
-                        initWindowSizes(false);
+                        completeInitialization();
                     });
                 }else{
                     //Applocale not found. Is this a trim down version of ArozOS?
@@ -615,10 +696,7 @@
                             return original;
                         }
                     }
-                    initRootDirs();
-                    initSystemInfo();
-                    initUploadMode();
-                    $(".dropdown").dropdown();
+                    completeInitialization();
                 }
            
                 if (isMobile){
@@ -648,82 +726,6 @@
                     $(".mobileOnly").hide();
                     $(".desktopOnly").show();
                 }
-                //Initialize view mode buttons
-                updateViewmodeButtons();
-                loadListModeFromDB();
-
-                //Initialize system theme
-                loadPreference("file_explorer/theme",function(data){
-                    if (data.error === undefined){
-                        if (data == "darkTheme"){
-                            toggleDarkTheme();
-                        }else{
-                            //White theme
-                        
-                        }
-                    }
-                });
-
-                //Initialize properties view
-                if (localStorage.getItem("file_explorer/viewProperties") == "true"){
-                    $("#togglePropertiesViewBtn").click();
-                }
-
-
-                if (window.location.hash != ""){
-                    //Check if the hash is standard open protocol. If yes, translate it
-                    if (ao_module_loadInputFiles() === null){
-                        //Window location hash set. List the desire directory
-                        currentPath = window.location.hash.substring(1,window.location.hash.length);
-                        if (currentPath.substring(currentPath.length -1) != "/"){
-                            currentPath = currentPath + "/";
-                        }
-                        currentPath = decodeURIComponent(currentPath);
-                        loadListModeFromDB(function(){
-                            listDirectory(currentPath);
-                        });
-                        
-                    }else{
-                        //This is ao_module load file input. Handle the file opening
-                        var filelist = ao_module_loadInputFiles();
-                        if (filelist.length > 0){
-                            filelist = filelist[0];
-                            //Check if this is folder or file. Only opendir when it is folder
-                            //Updates 27-12-2020: Open folder and highlight the file if it is file
-                            if (filelist.filename.includes(".") == false){
-                                //Try to open it and overwrite the hash to filesystem hash
-                                loadListModeFromDB(function(){
-                                    listDirectory(filelist.filepath);
-                                });
-                            }else{
-                                //File. Open its parent folder and highlight the target file if exists
-                                var parentdir = filelist.filepath.split("/");
-                                let focusFilename = JSON.parse(JSON.stringify(filelist.filename));
-                                parentdir.pop();
-                                parentdir = parentdir.join("/");
-                                loadListModeFromDB(function(){
-                                    listDirectory(parentdir, function(){
-                                        if (focusFilename != ""){
-                                            //Timeout to give the DOM time to render
-                                            //DO NOT REPLACE THIS WITH listDirectoryAndHighlight
-                                            //Additional delay are required on page load
-                                            setTimeout(function(){
-                                                focusFileObject(focusFilename);
-                                            }, 300);
-                                            
-                                        }
-                                    })
-                                });
-                            }
-                        }
-                    }
-                }else{
-
-                    //Initialized directory views
-                    loadListModeFromDB(function(){
-                        listDirectory(currentPath);
-                    });
-                }
                 
 
                 
@@ -909,8 +911,12 @@
                 if (recordPreviousPage){
                     viewHistory.push(currentPath);
                 }
-               
-                window.location.hash = path;
+                
+                if (!ao_module_virtualDesktop){
+                    //Update the window hash
+                    window.location.hash = encodeURIComponent(path);
+                }
+                
                 updatePathDisplay(path);
                 currentPath = path;
 
@@ -2363,6 +2369,17 @@
             }
             
 
+            //Check if filename contains web-unsafe characters
+            function filenameContainsIllegalCharacters(filename){
+                var illegalChars = ['/', '\\', '?', '%', '*', ':', '|', '"', '<', '>'];
+                for (var i = 0; i < illegalChars.length; i++){
+                    if (filename.includes(illegalChars[i])){
+                        return true;
+                    }
+                }
+                return false;
+            }
+
             function confirmRename(oldName=undefined, newName=undefined){
                 renameMode = false;
                 if (oldName == undefined){
@@ -2373,6 +2390,12 @@
                     newName = $("#renameBox").find(".newfn").val().trim();
                 }
 
+                //Check for illegal characters
+                if (filenameContainsIllegalCharacters(newName)){
+                    msgbox("red remove", applocale.getString("message/illegalCharacters", "Filename contains illegal characters"));
+                    return;
+                }
+
                 if (newName == oldName){
                     msgbox("red remove",applocale.getString("message/newFilenameIdentical", "New filename is identical to the original filename."));
                     hideAllPopupWindows();
@@ -3208,8 +3231,9 @@
                     }
 
                     //Check for invalid characters
-                    if (newFoldername.includes("%")){
+                    if (filenameContainsIllegalCharacters(newFoldername)){
                         $("#createNewFolder").parent().addClass("error");
+                        msgbox("red remove", applocale.getString("message/illegalCharacters", "Folder name contains illegal characters"));
                         return;
                     }
 
@@ -3253,7 +3277,16 @@
                     //Filename not set
                     $("#createNewFileName").parent().addClass("error");
                     return;
-                }else if(currentFilelist.includes(filename)){
+                }
+                
+                //Check for illegal characters
+                if (filenameContainsIllegalCharacters(filename)){
+                    $("#createNewFileName").parent().addClass("error");
+                    msgbox("red remove", applocale.getString("message/illegalCharacters", "Filename contains illegal characters"));
+                    return;
+                }
+                
+                if(currentFilelist.includes(filename)){
                     //File with this name already exists
                     $("#createNewFileName").parent().addClass("error");
                     $("#newFile").find(".duplicateWarning").show();

+ 10 - 4
src/web/SystemAO/locale/file_explorer.json

@@ -244,7 +244,8 @@
                 "message/destIdentical": "檔案來源與目的地相同",
                 "message/decodeFilelistFail": "載案置入失敗:無法讀取檔案列表",
                 "message/uploadFailed": "載案上載失敗:檔案太大或目標儲存裝置已滿",
-                "message/newFilenameIdentical": "重新命名失敗:新舊檔案名稱相同"
+                "message/newFilenameIdentical": "重新命名失敗:新舊檔案名稱相同",
+                "message/illegalCharacters": "名稱包含非法字元(不可使用:/ \\ ? % * : | \" < >)"
             },
             "titles": {
                 "Back": "上一頁",
@@ -528,7 +529,8 @@
                 "message/destIdentical": "檔案來源與目的地相同",
                 "message/decodeFilelistFail": "載案置入失敗:無法讀取檔案列表",
                 "message/uploadFailed": "載案上載失敗:檔案太大或目標儲存裝置已滿",
-                "message/newFilenameIdentical": "重新命名失敗:新舊檔案名稱相同"
+                "message/newFilenameIdentical": "重新命名失敗:新舊檔案名稱相同",
+                "message/illegalCharacters": "名稱包含非法字元(不可使用:/ \\ ? % * : | \" < >)"
             },
             "titles": {
                 "Back": "上一頁",
@@ -811,7 +813,8 @@
                 "message/destIdentical": "文件来源及目标相同",
                 "message/decodeFilelistFail": "文件加载失败:无法读取文件列表",
                 "message/uploadFailed": "文件上传失败:文件太大或目标存储设备已满",
-                "message/newFilenameIdentical": "重命名失败:新旧文件名称相同"
+                "message/newFilenameIdentical": "重命名失败:新旧文件名称相同",
+                "message/illegalCharacters": "名称包含非法字符(不可使用:/ \\ ? % * : | \" < >)"
             },
             "titles": {
                 "Back": "上一页",
@@ -1097,6 +1100,7 @@
                 "message/decodeFilelistFail": "Failed to load folder: could not read the file list.",
                 "message/uploadFailed": "File upload failed: file is too large or the storage device is full.",
                 "message/newFilenameIdentical": "Failed to rename file: new name is identical to the old name.",
+                "message/illegalCharacters": "Name contains illegal characters (cannot use: / \\ ? % * : | \" < >)",
                 "":""
             },
             "titles": {
@@ -1378,6 +1382,7 @@
                 "message/decodeFilelistFail": "フォルダを読み込めませんでした:ファイルリストを読み込めませんでした",
                 "message/uploadFailed": "ファイルのアップロードに失敗しました:ファイルが大きすぎるか、ストレージデバイスがいっぱいです。",
                 "message/newFilenameIdentical": "ファイル名の変更に失敗しました:新しい名前が古い名前と同じです。",
+                "message/illegalCharacters": "名前に使用できない文字が含まれています(使用不可:/ \\ ? % * : | \" < >)",
                 "":""
             },
             "titles": {
@@ -1664,7 +1669,8 @@
                 "message/destIdentical": "소스 및 대상 경로가 동일합니다",
                 "message/decodeFilelistFail": "파일 목록을 읽을 수 없어 파일 로드 실패",
                 "message/uploadFailed": "파일 업로드 실패: 파일이 너무 크거나 대상 저장 장치가 가득 찼습니다",
-                "message/newFilenameIdentical": "이름 변경 실패: 새로운 파일 이름이 기존과 동일합니다"
+                "message/newFilenameIdentical": "이름 변경 실패: 새로운 파일 이름이 기존과 동일합니다",
+                "message/illegalCharacters": "이름에 사용할 수 없는 문자가 포함되어 있습니다 (사용 불가: / \\ ? % * : | \" < >)"
             },
             "titles": {
                 "Back": "뒤로",

+ 105 - 33
src/web/SystemAO/users/userList.html

@@ -29,6 +29,9 @@
                 <button id="editUserButton" class="ui small negative disabled right floated button" onclick="removeUser();">
                     Remove
                 </button>
+                <button id="deselectAllBtn" class="ui small right floated basic button" onclick="deselectAll();" style="display: none;">
+                    <i class="remove icon"></i>Deselect All
+                </button>
             </div>
             <div class="ui styled fluid accordion">
                 <div class="title">
@@ -177,45 +180,62 @@
 
 
             //Init User List
+            var validGroups = [];
             initUserList();
             function initUserList(){
                 $("#userTable").html("");
                 $("#editbtn").addClass('disabled');
-                $.ajax({
-                    url: "../../system/users/list",
-                    success: function(data){
-                        for (var i =0; i < data.length; i++){
-                            var username = data[i][0];
-                            var group = data[i][1];
-                            var profilePic = data[i][2];
-                            if (profilePic == ""){
-                                profilePic = "../users/img/noprofileicon.png"
-                            }
-                            var accountStatus = data[i][3];
-                            $("#userTable").append(`<tr>
-                                <td>
-                                    <h4 class="ui image header">
-                                        <img src="${profilePic}" class="ui mini rounded image">
-                                        <div class="content">
-                                        ${username}
+                
+                // First, fetch the valid groups list
+                $.get("../../system/permission/listgroup", function(groupData){
+                    validGroups = groupData;
+                    
+                    // Then load the user list
+                    $.ajax({
+                        url: "../../system/users/list",
+                        success: function(data){
+                            for (var i =0; i < data.length; i++){
+                                var username = data[i][0];
+                                var group = data[i][1];
+                                var profilePic = data[i][2];
+                                if (profilePic == ""){
+                                    profilePic = "../users/img/noprofileicon.png"
+                                }
+                                var accountStatus = data[i][3];
+                                
+                                // Check each group and add invalid label if needed
+                                var groupDisplay = group.map(function(g) {
+                                    if (validGroups.indexOf(g) === -1) {
+                                        return g + ' <span class="ui mini red label">Invalid</span>';
+                                    }
+                                    return g;
+                                }).join("/");
+                                
+                                $("#userTable").append(`<tr>
+                                    <td>
+                                        <h4 class="ui image header">
+                                            <img src="${profilePic}" class="ui mini rounded image">
+                                            <div class="content">
+                                            ${username}
                                         </div>
                                     </h4>
                                 </td>
                                 <td>
-                                    ${group.join("/")}
+                                    ${groupDisplay}
                                 </td>
                                 <td>
                                     <div class="ui checkbox">
-                                        <input type="radio" name="selectUser" value="${username}" self="${data[i][3]}" onchange="enableEdit(this);">
+                                        <input type="checkbox" class="selectUser" value="${username}" self="${data[i][3]}" onchange="handleUserSelection();">
                                         <label></label>
                                     </div>
                                 </td>
                             </tr>`);
+                            }
+                        },
+                        error: function(data){
+                            $("#userTable").html("<p> Failed to load user database. </p>");
                         }
-                    },
-                    error: function(data){
-                        $("#userTable").html("<p> Failed to load user database. </p>");
-                    }
+                    });
                 });
             }
 
@@ -333,7 +353,7 @@
             }
 
             function showEditUI(){
-                var username = $("input[name='selectUser']:checked").val();
+                var username = $(".selectUser:checked").first().val();
                 ao_module_newfw({
                     url:"SystemAO/users/editUser.html#" + encodeURIComponent(username),
                     width: 530,
@@ -346,11 +366,25 @@
             }
 
             function removeUser(){
-                var username = $("input[name='selectUser']:checked").val();
-                if (confirm("Remove " + username + " from the system PERMANENTLY?")){
+                var selectedUsers = $(".selectUser:checked");
+                var usernames = [];
+                
+                selectedUsers.each(function(){
+                    usernames.push($(this).val());
+                });
+                
+                if (usernames.length === 0) {
+                    return;
+                }
+                
+                var confirmMessage = usernames.length === 1 
+                    ? "Remove " + usernames[0] + " from the system PERMANENTLY?"
+                    : "Remove " + usernames.length + " users from the system PERMANENTLY?";
+                    
+                if (confirm(confirmMessage)){
                     $.ajax({
                         url: "../../system/users/removeUser",
-                        data: {username: username},
+                        data: {usernames: JSON.stringify(usernames)},
                         method: "POST",
                         success: function(data){
                             if (data.error !== undefined){
@@ -359,6 +393,9 @@
                                 //Reload the list
                                 initUserList();
                             }
+                        },
+                        error: function(xhr, status, error){
+                            alert("Failed to remove user(s): " + error);
                         }
                     });
                 }
@@ -370,15 +407,50 @@
                 }
             }
 
-            function enableEdit(object){
-                if ($(object).attr("self") != "true"){
-                    $("#editUserButton").removeClass("disabled");
-                    $("#editbtn").removeClass("disabled");
-                }else{
+            function handleUserSelection(){
+                var selectedUsers = $(".selectUser:checked");
+                var selectedCount = selectedUsers.length;
+                
+                // Check if the current user is in the selection
+                var currentUserSelected = false;
+                selectedUsers.each(function(){
+                    if ($(this).attr("self") === "true") {
+                        currentUserSelected = true;
+                        return false; // break the loop
+                    }
+                });
+                
+                if (selectedCount === 0) {
+                    // No users selected
                     $("#editUserButton").addClass("disabled");
+                    $("#editbtn").addClass("disabled");
+                    $("#deselectAllBtn").hide();
+                } else if (selectedCount === 1) {
+                    // Single user selected
+                    var isSelf = selectedUsers.first().attr("self") === "true";
+                    if (isSelf) {
+                        $("#editUserButton").addClass("disabled");
+                    } else {
+                        $("#editUserButton").removeClass("disabled");
+                    }
                     $("#editbtn").removeClass("disabled");
+                    $("#deselectAllBtn").hide();
+                } else {
+                    // Multiple users selected
+                    if (currentUserSelected) {
+                        // Current user is in the selection, disable delete button
+                        $("#editUserButton").addClass("disabled");
+                    } else {
+                        $("#editUserButton").removeClass("disabled");
+                    }
+                    $("#editbtn").addClass("disabled"); // Disable edit for multiple selection
+                    $("#deselectAllBtn").show();
                 }
-               
+            }
+            
+            function deselectAll(){
+                $(".selectUser:checked").prop('checked', false);
+                handleUserSelection();
             }
         </script>
     </body>