diff --git a/docs/access_control.md b/docs/access_control.md
index 264a75248dbd81625c0a82376aa725b349910893..1a3c07fcd899e876074181c5746fcf9419d382d6 100644
--- a/docs/access_control.md
+++ b/docs/access_control.md
@@ -17,6 +17,8 @@ The table below lists all potential capabilities associated to a user:
 | USER_CREATE       | Can create a user                                 |
 | USER_DESTROY      | Can destroy a user                                |
 | USER_SET_CAPS     | Can change a user's capabilities                  |
+| USER_UNLOCK       | Can unlock a user's account                       |
+| USER_RESETPWD     | Can reset a user's password                       |
 | USER_LIST         | Can list all users                                |
 | VM_CREATE         | Can create a VM                                   |
 | VM_DESTROY_ANY    | Can destoy **ANY** VM                             |
diff --git a/docs/rest_api.md b/docs/rest_api.md
index 43e344652101da2a1189769e1199b3b80f1a52a1..d7086082cac5a4eb2c7faef413c09c1471039d98 100644
--- a/docs/rest_api.md
+++ b/docs/rest_api.md
@@ -2,69 +2,71 @@
 
 ### Login and version management
 
-| Route            | Description                 | Method | Parameters |
-|---               |---                          |---     |---         |
-| `/login`         | login (return access token) | POST   | email,pwd  |
-| `/token/refresh` | obtain a new access token   | GET    |            |
-| `/version`       | obtain version number       | GET    |            |
+| Route            | Description                 | Method | Parameters | Output              |
+|---               |---                          |---     |---         |---                  |
+| `/login`         | login (return access token) | POST   | email,pwd  | common.params.Login |
+| `/token/refresh` | obtain a new access token   | GET    |            | common.params.Token |
+| `/version`       | obtain version number       | GET    |            | version number      |
 
 ### User management
 
-| Route                 | Description            | Method | Parameters                        | Req. user cap.   |
-|---                    |---                     |---     |---                                |---               |
-| `/users`              | create a user          | POST   | email,firstname,lastname,pwd,caps | `USER_CREATE`    |
-| `/users/{email}`      | delete a user          | DELETE |                                   | `USER_DESTROY`   |
-| `/users/{email}/caps` | set caps for a user    | PUT    | caps                              | `USER_SET_CAPS`  |
-| `/users/pwd`          | set current user's pwd | PUT    | pwd                               |                  |
-| `/users`              | list users             | GET    |                                   | `USER_LIST`      |
-| `/users/{email}`      | list a user            | GET    |                                   | `USER_LIST`      |
-| `/users/whoami`       | list current user      | GET    |                                   |                  |
+| Route                     | Description            | Method | Parameters                        | Req. user cap.   | Output  |
+|---                        |---                     |---     |---                                |---               |---      |
+| `/users`                  | create a user          | POST   | email,firstname,lastname,pwd,caps | `USER_CREATE`    |         |
+| `/users/{email}`          | delete a user          | DELETE |                                   | `USER_DESTROY`   |         |
+| `/users/{email}/caps`     | set caps for a user    | PUT    | caps                              | `USER_SET_CAPS`  |         |
+| `/users/{email}/unlock`   | unlock a user          | PUT    |                                   | `USER_UNLOCK`    |         |
+| `/users/{email}/resetpwd` | reset a user's pwd     | PUT    |                                   | `USER_RESETPWD`  | new pwd |
+| `/users/pwd`              | set current user's pwd | PUT    | pwd                               |                  |         |
+| `/users`                  | list users             | GET    |                                   | `USER_LIST`      |         |
+| `/users/{email}`          | list a user            | GET    |                                   | `USER_LIST`      |         |
+| `/users/whoami`           | list current user      | GET    |                                   |                  |         |
      
 - Open question: shall we forbid the deletion of a user if they still own templates or VMs?
 
 ### VM management
 
-| Route              | Description                                           | Method | Parameters | Req. user cap.   | Op. | Req. VM access cap. |
-|---                 |---                                                    |---     |---         |---               |---  |---                  |
-| `/vms`             | returns VMs that can be listed                        | GET    |            | `VM_LIST_ANY`    | OR  | `VM_LIST`           |
-| `/vms/{id}`        | returns the specified VM                              | GET    |            | `VM_LIST_ANY`    | OR  | `VM_LIST`           |
-| `/vms/start`       | returns VMs that can be started                       | GET    |            | `VM_START_ANY`   | OR  | `VM_START`          |
-| `/vms/attach`      | returns VM creds info for VMs that can be attached to | GET    |            | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         |
-| `/vms/{id}/attach` | returns VM creds info for the specified VM            | GET    |            | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         |
-| `/vms/stop`        | returns VMs that can be killed/shutdown               | GET    |            | `VM_STOP_ANY`    | OR  | `VM_STOP`           |
-| `/vms/reboot`      | returns VMs that can be rebooted                      | GET    |            | `VM_REBOOT_ANY`  | OR  | `VM_REBOOT`         |
-| `/vms/edit`        | returns VMs that can be edited                        | GET    |            | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           |
-| `/vms/editaccess`  | returns VMs that can have their access changed        | GET    |            | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |
-| `/vms/del`         | returns VMs that can be deleted                       | GET    |            | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        |
-| `/vms/exportdir`   | returns VMs that can have a dir downloaded            | GET    |            | `VM_READFS_ANY`  | OR  | `VM_READFS`         |
-| `/vms/importfiles` | returns VMs allowing files upload                     | GET    |            | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |
+| Route              | Description                                           | Method | Parameters | Req. user cap.   | Op. | Req. VM access cap. | Output  |
+|---                 |---                                                    |---     |---         |---               |---  |---                  |---      |
+| `/vms`             | returns VMs that can be listed                        | GET    |            | `VM_LIST_ANY`    | OR  | `VM_LIST`           |         |
+| `/vms/{id}`        | returns the specified VM                              | GET    |            | `VM_LIST_ANY`    | OR  | `VM_LIST`           |         |
+| `/vms/start`       | returns VMs that can be started                       | GET    |            | `VM_START_ANY`   | OR  | `VM_START`          |         |
+| `/vms/attach`      | returns VM creds info for VMs that can be attached to | GET    |            | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         |         |
+| `/vms/{id}/attach` | returns VM creds info for the specified VM            | GET    |            | `VM_ATTACH_ANY`  | OR  | `VM_ATTACH`         |         |
+| `/vms/stop`        | returns VMs that can be killed/shutdown               | GET    |            | `VM_STOP_ANY`    | OR  | `VM_STOP`           |         |
+| `/vms/reboot`      | returns VMs that can be rebooted                      | GET    |            | `VM_REBOOT_ANY`  | OR  | `VM_REBOOT`         |         |
+| `/vms/edit`        | returns VMs that can be edited                        | GET    |            | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           |         |
+| `/vms/editaccess`  | returns VMs that can have their access changed        | GET    |            | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |         |
+| `/vms/del`         | returns VMs that can be deleted                       | GET    |            | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        |         |
+| `/vms/exportdir`   | returns VMs that can have a dir downloaded            | GET    |            | `VM_READFS_ANY`  | OR  | `VM_READFS`         |         |
+| `/vms/importfiles` | returns VMs allowing files upload                     | GET    |            | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |         |
 
-| Route                      | Description                  | Method | Parameters                     | Req. user cap.   | Op. | Req. VM access cap. |
-|---                         |---                           |---     |---                             |---               |---  |---                  |
-| `/vms`                     | create a VM                  | POST   | name,cpus,ram,nic,usb,template | `VM_CREATE`      |     |                     |
-| `/vms/{id}`                | delete a VM                  | DELETE |                                | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        |
-| `/vms/{id}`                | edit a VM                    | PUT    | name,cpus,ram,nic,usb          | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           |
-| `/vms/{id}/start`          | start a VM                   | PUT    |                                | `VM_START_ANY`   | OR  | `VM_START`          |
-| `/vms/{id}/startwithcreds` | start a VM with credentials  | PUT    | port,pwd                       | `VM_START_ANY`   | OR  | `VM_START`          |
-| `/vms/{id}/stop`           | kill a VM                    | PUT    |                                | `VM_STOP_ANY`    | OR  | `VM_STOP`           |
-| `/vms/{id}/reboot`         | reboot a VM                  | PUT    |                                | `VM_REBOOT_ANY`  | OR  | `VM_REBOOT`         |
-| `/vms/{id}/shutdown`       | gracefully shutdown a VM     | PUT    |                                | `VM_STOP_ANY`    | OR  | `VM_STOP`           |
-| `/vms/{id}/access/{email}` | set VM access for a user     | PUT    | caps                           | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |
-| `/vms/{id}/access/{email}` | del VM access for a user     | DELETE |                                | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |
-| `/vms/{id}/exportdir`      | download a VM's dir          | GET    | dir                            | `VM_READFS_ANY`  | OR  | `VM_READFS`         |
-| `/vms/{id}/importfiles`    | upload files into a VM's dir | POST   | files (.tar archive),dir       | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |
+| Route                      | Description                  | Method | Parameters                     | Req. user cap.   | Op. | Req. VM access cap. | Output  |
+|---                         |---                           |---     |---                             |---               |---  |---                  |---      |
+| `/vms`                     | create a VM                  | POST   | name,cpus,ram,nic,usb,template | `VM_CREATE`      |     |                     |         |
+| `/vms/{id}`                | delete a VM                  | DELETE |                                | `VM_DESTROY_ANY` | OR  | `VM_DESTROY`        |         |
+| `/vms/{id}`                | edit a VM                    | PUT    | name,cpus,ram,nic,usb          | `VM_EDIT_ANY`    | OR  | `VM_EDIT`           |         |
+| `/vms/{id}/start`          | start a VM                   | PUT    |                                | `VM_START_ANY`   | OR  | `VM_START`          |         |
+| `/vms/{id}/startwithcreds` | start a VM with credentials  | PUT    | port,pwd                       | `VM_START_ANY`   | OR  | `VM_START`          |         |
+| `/vms/{id}/stop`           | kill a VM                    | PUT    |                                | `VM_STOP_ANY`    | OR  | `VM_STOP`           |         |
+| `/vms/{id}/reboot`         | reboot a VM                  | PUT    |                                | `VM_REBOOT_ANY`  | OR  | `VM_REBOOT`         |         |
+| `/vms/{id}/shutdown`       | gracefully shutdown a VM     | PUT    |                                | `VM_STOP_ANY`    | OR  | `VM_STOP`           |         |
+| `/vms/{id}/access/{email}` | set VM access for a user     | PUT    | caps                           | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |         |
+| `/vms/{id}/access/{email}` | del VM access for a user     | DELETE |                                | `VM_SET_ACCESS`  | AND | `VM_SET_ACCESS`     |         |
+| `/vms/{id}/exportdir`      | download a VM's dir          | GET    | dir                            | `VM_READFS_ANY`  | OR  | `VM_READFS`         |         |
+| `/vms/{id}/importfiles`    | upload files into a VM's dir | POST   | files (.tar archive),dir       | `VM_WRITEFS_ANY` | OR  | `VM_WRITEFS`        |         |
 
 ### Template management
 
-| Route                  | Description                          | Method | Parameters       | Req. user cap.                     |
-|---                     |---                                   |---     |---               |---                                 |
-| `/templates`           | returns templates that can be listed | GET    |                  | `TPL_LIST_ANY` OR `TPL_LIST`       |
-| `/templates/{id}`      | returns a template                   | GET    |                  | `TPL_LIST_ANY` OR `TPL_LIST`       |
-| `/templates/vm`        | create a template                    | POST   | vmID,name,access | `TPL_CREATE`                       |
-| `/templates/qcow`      | create a template                    | POST   | qcow,name,access | `TPL_CREATE`                       |
-| `/templates/{id}`      | edit a template                      | PUT    | name,access      | `TPL_EDIT_ANY` OR `TPL_EDIT`       |
-| `/templates/{id}`      | delete a template                    | DELETE |                  | `TPL_DESTROY_ANY` OR `TPL_DESTROY` |
-| `/templates/{id}/disk` | download a template's disk           | GET    |                  | `TPL_READFS_ANY` OR `TPL_READFS`   |
+| Route                  | Description                          | Method | Parameters       | Req. user cap.                     | Output  |
+|---                     |---                                   |---     |---               |---                                 |---      |
+| `/templates`           | returns templates that can be listed | GET    |                  | `TPL_LIST_ANY` OR `TPL_LIST`       |         |
+| `/templates/{id}`      | returns a template                   | GET    |                  | `TPL_LIST_ANY` OR `TPL_LIST`       |         |
+| `/templates/vm`        | create a template                    | POST   | vmID,name,access | `TPL_CREATE`                       |         |
+| `/templates/qcow`      | create a template                    | POST   | qcow,name,access | `TPL_CREATE`                       |         |
+| `/templates/{id}`      | edit a template                      | PUT    | name,access      | `TPL_EDIT_ANY` OR `TPL_EDIT`       |         |
+| `/templates/{id}`      | delete a template                    | DELETE |                  | `TPL_DESTROY_ANY` OR `TPL_DESTROY` |         |
+| `/templates/{id}/disk` | download a template's disk           | GET    |                  | `TPL_READFS_ANY` OR `TPL_READFS`   |         |
 
 Remarks:
 
diff --git a/docs/server.md b/docs/server.md
index dbb41a6fd131db7b3438d4e384a1b85749d85404..3877e6405e52286dd03aa1b31cf82499aa4be887 100644
--- a/docs/server.md
+++ b/docs/server.md
@@ -273,21 +273,27 @@ The host machine on which **nexus-server** will be running requires the followin
 - `guestfish` to export/import files from VM disk images (`guestfish` Ubuntu package)
 - `uuidgen` to generate UUIDs from bash (`uuid-runtime` Ubuntu package)
 
-Note that Ubuntu 20.04 ships with QEMU 4.2.1 which is too old, but Ubuntu 22.04 ships with QEMU 6.2.
+Note that Ubuntu 20.04 ships with QEMU 4.2.1 which is too old, but Ubuntu 22.04 ships with QEMU 6.2 and Ubuntu 24.04 ships with QEMU 8.2.2.
+
 The latest stable version of QEMU can be downloaded from [https://www.qemu.org/download/](https://www.qemu.org/download/). To compile QEMU from source, read the [Compiling QEMU from source](##Compiling-QEMU-from-source) section at the end of this document.
 
 To get, build, and install **nexus-server**, these additionnal software are required:
 
 - git (`git` Ubuntu package)
-- golang 1.18 or newer (`golang-go` Ubuntu 22.04 packages)
+- golang 1.18 or newer (`golang-go` Ubuntu package)
 - GNU make (`make` Ubuntu package)
 - certtool (`gnutls-bin` Ubuntu package)
 
-To install all the above-mentioned packages on a Ubuntu 22.04 system:
+To install all the above-mentioned packages on a Ubuntu system:
 ```
 sudo apt-get install -y qemu-system-x86 qemu-utils guestfish uuid-runtime git golang-go make gnutls-bin
 ```
 
+On Ubuntu 24.04, an additionnal package must be installed to support SPICE:
+```
+sudo apt-get install -y qemu-system-modules-spice
+```
+
 To make sure the KVM module is loaded into the kernel, run:
 ```
 lsmod|grep kvm
diff --git a/src/client/cmdUser/userResetPwd.go b/src/client/cmdUser/userResetPwd.go
new file mode 100644
index 0000000000000000000000000000000000000000..661a1d034e4208c057343cec18d9038ee1208997
--- /dev/null
+++ b/src/client/cmdUser/userResetPwd.go
@@ -0,0 +1,70 @@
+package cmdUser
+
+import (
+    "errors"
+    u "nexus-client/utils"
+    g "nexus-client/globals"
+)
+
+type ResetPwd struct {
+    Name string
+}
+
+func (cmd *ResetPwd)GetName() string {
+    return cmd.Name
+}
+
+func (cmd *ResetPwd)GetDesc() []string {
+    return []string{
+        "Reset password for one or more users.",
+        "Requires USER_RESETPWD user capability."}
+}
+
+func (cmd *ResetPwd)PrintUsage() {
+    for _, desc := range cmd.GetDesc() {
+        u.PrintlnErr(desc)
+    }
+    u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
+    u.PrintlnErr("USAGE: "+cmd.GetName()+" email [email ...]")
+    u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
+}
+
+func (cmd *ResetPwd)Run(args []string) int {
+    argc := len(args)
+    if argc < 1 {
+        cmd.PrintUsage()
+        return 1
+    }
+
+    statusCode := 0
+
+    // Iterates through each user (email) and reset their password
+    for i:= range(args) {
+        email := args[i]
+        resp, err := cmd.runRequest(email)
+        if err != nil {
+            u.PrintlnErr(err.Error())
+            statusCode = 1
+        } else {
+            u.Println("New password for user "+email+": "+resp)
+        }
+    }
+
+    return statusCode
+}
+
+func (cmd *ResetPwd)runRequest(email string) (string, error) {
+    client := g.GetInstance().Client
+    host := g.GetInstance().Host
+
+    resp, err := client.R().Put(host+"/users/"+email+"/resetpwd")
+    if err != nil {
+        return "", errors.New("Error: "+err.Error())
+    }
+
+    if resp.IsSuccess() {
+        return resp.String(), nil
+    } else {
+        return "", errors.New("Error: "+resp.Status()+": "+resp.String())
+    }
+}
diff --git a/src/client/cmdUser/userUnlock.go b/src/client/cmdUser/userUnlock.go
new file mode 100644
index 0000000000000000000000000000000000000000..1d3845c24d55ab2aa2db29b75b0f1e03a1b139bd
--- /dev/null
+++ b/src/client/cmdUser/userUnlock.go
@@ -0,0 +1,69 @@
+package cmdUser
+
+import (
+    "errors"
+    u "nexus-client/utils"
+    g "nexus-client/globals"
+)
+
+type Unlock struct {
+    Name string
+}
+
+func (cmd *Unlock)GetName() string {
+    return cmd.Name
+}
+
+func (cmd *Unlock)GetDesc() []string {
+    return []string{
+        "Unlocks one or more users.",
+        "Requires USER_UNLOCK user capability."}
+}
+
+func (cmd *Unlock)PrintUsage() {
+    for _, desc := range cmd.GetDesc() {
+        u.PrintlnErr(desc)
+    }
+    u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
+    u.PrintlnErr("USAGE: "+cmd.GetName()+" email [email ...]")
+    u.PrintlnErr("―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――")
+}
+
+func (cmd *Unlock)Run(args []string) int {
+    argc := len(args)
+    if argc < 1 {
+        cmd.PrintUsage()
+        return 1
+    }
+
+    statusCode := 0
+
+    // Iterates through each user (email) and unlock them
+    for i:= range(args) {
+        email := args[i]
+        if err := cmd.runRequest(email); err != nil {
+            u.PrintlnErr(err.Error())
+            statusCode = 1
+        } else {
+            u.Println("Successfully unlocked user "+email)
+        }
+    }
+
+    return statusCode
+}
+
+func (cmd *Unlock)runRequest(email string) error {
+    client := g.GetInstance().Client
+    host := g.GetInstance().Host
+
+    resp, err := client.R().Put(host+"/users/"+email+"/unlock")
+    if err != nil {
+        return errors.New("Error: "+err.Error())
+    }
+
+    if resp.IsSuccess() {
+        return nil
+    } else {
+        return errors.New("Error: "+resp.Status()+": "+resp.String())
+    }
+}
diff --git a/src/client/cmdUser/userUpdatePwd.go b/src/client/cmdUser/userUpdatePwd.go
index 7d045e33a7a673572251d0248e9ae85e1880781d..172cac8db6be53e2db859881ad2b868cbf2fe006 100644
--- a/src/client/cmdUser/userUpdatePwd.go
+++ b/src/client/cmdUser/userUpdatePwd.go
@@ -1,16 +1,13 @@
 package cmdUser
 
 import (
+    "bytes"
+    "golang.org/x/crypto/ssh/terminal"
     "nexus-common/params"
     u "nexus-client/utils"
     g "nexus-client/globals"
 )
 
-import (
-    "bytes"
-    "golang.org/x/crypto/ssh/terminal"
-)
-
 type UpdatePwd struct {
     Name string
 }
diff --git a/src/client/nexus-cli/nexus-cli.go b/src/client/nexus-cli/nexus-cli.go
index 292b18a354960c48212efb3d1eb2fe3d2a465110..bc8a52aa7e3a06ab12ad20252ccc5994b1817833 100644
--- a/src/client/nexus-cli/nexus-cli.go
+++ b/src/client/nexus-cli/nexus-cli.go
@@ -37,7 +37,9 @@ var cmdList = []cmd.Command {
     &cmdUser.List{"userlist"},
     &cmdUser.Add{"usercreate"},
     &cmdUser.Del{"userdel"},
+    &cmdUser.ResetPwd{"userresetpwd"},
     &cmdUser.SetCaps{"usersetcaps"},
+    &cmdUser.Unlock{"userunlock"},
 
     &cmdMisc.HelpHeader{"!═════╡ TEMPLATE commands ╞═════════════════════════════════════════════════════════════════"},
     &cmdTemplate.Create{"tplcreate"},
diff --git a/src/client/nexush/nexush.go b/src/client/nexush/nexush.go
index 7a71505ebc4ec1b2306230cb1da324805eb9275b..ece4816d39430955607f9a0189d8681ff1d41b6e 100644
--- a/src/client/nexush/nexush.go
+++ b/src/client/nexush/nexush.go
@@ -42,7 +42,9 @@ var cmdList = []cmd.Command {
     &cmdUser.List{"userlist"},
     &cmdUser.Add{"usercreate"},
     &cmdUser.Del{"userdel"},
+    &cmdUser.ResetPwd{"userresetpwd"},
     &cmdUser.SetCaps{"usersetcaps"},
+    &cmdUser.Unlock{"userunlock"},
 
     &cmdMisc.HelpHeader{"!═════╡ TEMPLATE commands ╞═════════════════════════════════════════════════════════════════"},
     &cmdTemplate.Create{"tplcreate"},
diff --git a/src/client/version/version.go b/src/client/version/version.go
index 5aa7a035938a6c5e25bdf20943d95fb3697c8e09..4faa88c3d2ceb44019cfe6e18ebcf1e878e78e78 100644
--- a/src/client/version/version.go
+++ b/src/client/version/version.go
@@ -7,8 +7,8 @@ import (
 
 const (
     major  = 1
-    minor  = 9
-    bugfix = 4
+    minor  = 10
+    bugfix = 0
 )
 
 type Version struct {
diff --git a/src/common/caps/caps.go b/src/common/caps/caps.go
index 94b66d061a6f7e598e02b2a25300cf16b71517ad..f4173606612292915c66975a726108e988761047 100644
--- a/src/common/caps/caps.go
+++ b/src/common/caps/caps.go
@@ -10,7 +10,8 @@ const (
     CAP_USER_CREATE string   = "USER_CREATE"
     CAP_USER_DESTROY         = "USER_DESTROY"
     CAP_USER_SET_CAPS        = "USER_SET_CAPS"
-    CAP_USER_SET_PWD         = "USER_SET_PWD"
+    CAP_USER_UNLOCK          = "USER_UNLOCK"
+    CAP_USER_RESETPWD        = "USER_RESETPWD"
     CAP_USER_LIST            = "USER_LIST"
 
     CAP_VM_LIST              = "VM_LIST"
@@ -51,6 +52,8 @@ var userCaps = Capabilities {
     CAP_USER_CREATE: 1,
     CAP_USER_DESTROY: 1,
     CAP_USER_SET_CAPS: 1,
+    CAP_USER_UNLOCK: 1,
+    CAP_USER_RESETPWD: 1,
     CAP_USER_LIST: 1,
 
     CAP_VM_CREATE: 1,
diff --git a/src/common/params/login.go b/src/common/params/login.go
index 1463ddcf3123aa445dcaff2c0754a692041e7144..9161bea0a94ca27988b12198ab9f6a757afe666a 100644
--- a/src/common/params/login.go
+++ b/src/common/params/login.go
@@ -5,3 +5,7 @@ type Login struct {
     Email string   `json:"email"  validate:"required,email"`
     Pwd string     `json:"pwd"    validate:"required,min=8"`
 }
+
+type Token struct {
+    Token string   `json:"token"  validate:"required"`
+}
\ No newline at end of file
diff --git a/src/server/exec/QemuSystem.go b/src/server/exec/QemuSystem.go
index 9f1e082a7e83ce1e27d93771d82ce0883e5d08e7..3084252229240b303792aa5d30f6a6bb04c73072 100644
--- a/src/server/exec/QemuSystem.go
+++ b/src/server/exec/QemuSystem.go
@@ -118,9 +118,9 @@ func NewQemuSystem(qgaSock string, cpus, ram int, nic string, usbDevs []string,
     args = append(args, "-device", "virtio-serial", "-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0", "-chardev", "socket,path="+qgaSock+",server=on,wait=off,id=qga0")
     // USB redirection
     args = append(args, usb...)
-    // Sound support
-    args = append(args, "-device", "intel-hda", "-device", "hda-duplex")
-
+    // No sound support for now as we want to keep compatibility with recent and older qemu versions without introducing audio backends, etc.
+    // args = append(args, "-device", "intel-hda", "-device", "hda-duplex")
+    
     // To share a folder with the host:
     // - Add QEMU args: virtfs local,path=/tmp/pipo,mount_tag=sharedfs,security_model=none
     // - In the guest: mkdir shared && sudo mount -t 9p sharedfs shared
diff --git a/src/server/router/auth.go b/src/server/router/auth.go
index 72975fc3d5cf69297e2c2cb43f9fd11d040fafcf..465ac426170b206f00f575a04ebed226718449af 100644
--- a/src/server/router/auth.go
+++ b/src/server/router/auth.go
@@ -15,24 +15,24 @@ import (
 )
 
 // jwt.StandardClaims is added as an embedded type to provide fields like user email and expiration time.
-type CustomClaims struct {
+type customClaims struct {
     Email string
     jwt.StandardClaims
 }
 
-type Auth struct {
+type auth struct {
     users *users.Users
     tokenAccess middleware.JWTConfig
 }
 
-var loginAttempts map[string]int  // Keeps track of the number of wrong logins.
-var loginLocked map[string]time.Time   // Keeps track of when an account was locked.
+var loginAttempts map[string]int  // Number of consecutive wrong logins for the same user
+var lockedUsers map[string]time.Time   // Locked users and timestamps when their accounts were locked
 var jwtSecretKey string
 
-// Creates an Auth object.
-func NewAuth(u *users.Users) (*Auth, error) {
+// Creates an auth object.
+func NewAuth(u *users.Users) (*auth, error) {
     loginAttempts = make(map[string]int)
-    loginLocked = make(map[string]time.Time)
+    lockedUsers = make(map[string]time.Time)
 
     // TODO: better to store jwtSecretKey in an env. variable?
     // Generates a 64 characters long key with 20 digits, 0 symbols,
@@ -43,12 +43,12 @@ func NewAuth(u *users.Users) (*Auth, error) {
     }
     jwtSecretKey = secret
 
-    return &Auth {
+    return &auth {
         users : u,
         tokenAccess : middleware.JWTConfig {
             ContextKey: "user",
             SigningKey: []byte(jwtSecretKey),
-            Claims:     &CustomClaims{},
+            Claims:     &customClaims{},
             ErrorHandlerWithContext: func(err error, c echo.Context) error {
                 return echo.NewHTTPError(http.StatusUnauthorized, "access denied")
             },
@@ -57,14 +57,14 @@ func NewAuth(u *users.Users) (*Auth, error) {
 }
 
 // Token used by the JWT middleware to protect access to the specified group of routes.
-func (auth *Auth)GetTokenAccess() middleware.JWTConfig {
+func (auth *auth)GetTokenAccess() middleware.JWTConfig {
     return auth.tokenAccess
 }
 
 // curl --cacert ca.pem -X POST https://localhost:1077/login -H 'Content-Type: application/json' -d '{"email": "johndoe@nexus.org", "pwd":"the_user_password"}'
 // Returns a jwt token if authentication was successful:
 // {"token":"<jwt token>"}
-func (auth *Auth)Login(c echo.Context) error {
+func (auth *auth)Login(c echo.Context) error {
     // Deserializes the JSON body and checks its validity.
     reqUser := new(params.Login)
     if err := decodeJson(c, &reqUser); err != nil {
@@ -77,59 +77,60 @@ func (auth *Auth)Login(c echo.Context) error {
     // User exists?
     user, err := auth.users.GetUserByEmail(reqUser.Email)
     if err != nil {
+        // return echo.NewHTTPError(http.StatusUnauthorized, "User not found")
         return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
     }
 
     // Is user locked?
-    lockedTime, exists := loginLocked[user.Email]
-    if exists {
-        // If locked time has elapsed, unlock the account.
+    lockedTime, found := lockedUsers[user.Email]
+    if found {
+        // If locked time has elapsed, unlock the user.
         t := time.Now()
         elapsedTime := t.Sub(lockedTime)
         if elapsedTime.Hours() > conf.Auth.LockedTimeInHours {
-            delete(loginLocked, user.Email)
-            delete(loginAttempts, user.Email)
+            if err := UnlockUser(c, user.Email); err != nil {
+                return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+            }
         } else {
-            log.Info("Account ", user.Email ," is still locked")
-            return echo.NewHTTPError(http.StatusUnauthorized, "Account is locked")
+            log.Info("User ", user.Email ," is locked")
+            return echo.NewHTTPError(http.StatusUnauthorized, "User is locked")
         }
     }
 
     // Checks the user password is correct.
-    // If a user enters the wrong password too many times, the account will be locked for a number of hours.
-    if err := bcrypt.CompareHashAndPassword([]byte(user.Pwd), []byte(reqUser.Pwd)); err != nil {
+    // If a user enters the wrong password too many times, the user will be locked for a number of hours.
+    err = bcrypt.CompareHashAndPassword([]byte(user.Pwd), []byte(reqUser.Pwd))
+    // If err == nil, passwords match!
+    if (err == nil) {
+        loginAttempts[user.Email] = 0
+    } else {
         // Stores the number of attempted logins.
-        count, exists := loginAttempts[user.Email]
-        if exists {
-            loginAttempts[user.Email] = count+1
-            if count >= conf.Auth.MaxLoginAttempts {
-                // Locks the account.
-                loginLocked[user.Email] = time.Now()
-                log.Info("Account ", user.Email ," is locked: too many wrong password attempts!")
-                return echo.NewHTTPError(http.StatusUnauthorized, "Account is locked")
-            }
+        count, found := loginAttempts[user.Email]
+        if found {
+            count = count+1
         } else {
-            loginAttempts[user.Email] = 1
+            count = 1
+        }
+        loginAttempts[user.Email] = count
+        log.Info("Login attempt ", count, " for user ", user.Email)
+        if count > conf.Auth.MaxLoginAttempts {
+            // Locks user
+            lockedUsers[user.Email] = time.Now()
+            log.Info("User ", user.Email ," is locked: too many wrong password attempts!")
+            return echo.NewHTTPError(http.StatusUnauthorized, "User is locked")
         }
 
-        // If the two passwords don't match, return a 401 status.
+        // If passwords don't match, return a 401 status.
         return echo.NewHTTPError(http.StatusUnauthorized, "Invalid password")
-    } else {
-        loginAttempts[user.Email] = 0
-    }
-
-    // Retrieves user from its email.
-    user, err = auth.users.GetUserByEmail(reqUser.Email)
-    if err != nil {
-        return echo.NewHTTPError(http.StatusUnauthorized, "User not found")
     }
 
-    token, err := createToken(user.Email)
+    tok, err := createToken(user.Email)
     if err != nil {
         return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
     }
 
-    return c.JSON(http.StatusOK, echo.Map{"token": token})
+    tokenSerialized := params.Token{ Token: tok }
+    return c.JSON(http.StatusOK, tokenSerialized)
 }
 
 // Creates a JWT token and claims with the expiration time for the given user.
@@ -137,7 +138,7 @@ func createToken(userEmail string) (string, error) {
     expirationTime := time.Now().Add(time.Duration(conf.Auth.AccessTokenExpirationTimeInHours)*time.Hour)
 
     // Creates the claims
-    claims := &CustomClaims{
+    claims := &customClaims{
         Email: userEmail,
         StandardClaims: jwt.StandardClaims {
             // JWT expresses expiration time in Unix milliseconds
@@ -166,11 +167,30 @@ func GetUserFromContext(c echo.Context) (string, error) {
     }
 
     user := c.Get("user").(*jwt.Token)
-    claims := user.Claims.(*CustomClaims)
+    claims := user.Claims.(*customClaims)
 
     return claims.Email, nil
 }
 
+func UnlockUser(c echo.Context, emailToUnlock string) error {
+    userEmail, err := GetUserFromContext(c)
+    if err != nil {
+        return err
+    }
+
+    _, found := lockedUsers[emailToUnlock]
+    if found {
+        // unlock user by removing it from the locked map
+        delete(lockedUsers, emailToUnlock)
+        loginAttempts[emailToUnlock] = 0  // reset the number of attempts
+        log.Info("User ", userEmail, " unlocked user ", emailToUnlock)
+        return nil
+    } else {
+        return errors.New("User not locked")
+    }
+
+}
+
 /*
 // Example of middleware
 // Such a middleware could be used to handle a logout feature.
@@ -187,7 +207,7 @@ func OnlyJoe(next echo.HandlerFunc) echo.HandlerFunc {
             return echo.NewHTTPError(http.StatusUnauthorized, "invalid user token\n")
         }
         user := c.Get("user").(*jwt.Token)
-        claims := user.Claims.(*CustomClaims)
+        claims := user.Claims.(*customClaims)
         username := claims.Username
         if username != "joe" {
             return echo.ErrUnauthorized
diff --git a/src/server/router/router.go b/src/server/router/router.go
index 24b5516730c41fd5c760318907032861d94c8be4..6c62576c98c5522f51bb00590b5c079f1f39f84c 100644
--- a/src/server/router/router.go
+++ b/src/server/router/router.go
@@ -66,6 +66,8 @@ func (router *Router)Start(port int) {
     usersGroup.POST("", router.users.CreateUser)
     usersGroup.DELETE("/:email", router.users.DeleteUserByEmail)
     usersGroup.PUT("/:email/caps", router.users.SetUserCaps)
+    usersGroup.PUT("/:email/unlock", router.users.UnlockUser)
+    usersGroup.PUT("/:email/resetpwd", router.users.ResetPwd)
     usersGroup.PUT("/pwd", router.users.SetUserPwd)
 
     // VM management.
diff --git a/src/server/router/routerUsers.go b/src/server/router/routerUsers.go
index 05b6144516c74c69a6e7c02ec68c3a0d9826cd96..0f367b5a0cd7bacad38ef7ab69f540837149f40c 100644
--- a/src/server/router/routerUsers.go
+++ b/src/server/router/routerUsers.go
@@ -21,7 +21,7 @@ func NewRouterUsers() *RouterUsers {
 // Lists users.
 // Requires CAP_USER_LIST.
 // curl --cacert ca.pem -X GET http://localhost:1077/users -H "Authorization: Bearer <AccessToken>"
-func (r *RouterUsers) GetUsers(c echo.Context) error {
+func (r *RouterUsers)GetUsers(c echo.Context) error {
     // Retrieves logged user from context.
     user, err := getLoggedUser(r.users, c)
     if err != nil {
@@ -43,7 +43,7 @@ func (r *RouterUsers) GetUsers(c echo.Context) error {
 
 // Retrieves the logged (authententicated) user.
 // curl --cacert ca.pem -X GET http://localhost:1077/users/me -H "Authorization: Bearer <AccessToken>"
-func (r *RouterUsers) GetLoggedUser(c echo.Context) error {
+func (r *RouterUsers)GetLoggedUser(c echo.Context) error {
     user, err := getLoggedUser(r.users, c)
     if err != nil {
         return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
@@ -55,7 +55,7 @@ func (r *RouterUsers) GetLoggedUser(c echo.Context) error {
 // Deletes a user by its ID.
 // Requires CAP_USER_DESTROY.
 // curl --cacert ca.pem -X DELETE http://localhost:1077/users/janedoes@nexus.org -H "Authorization: Bearer <AccessToken>"
-func (r *RouterUsers) DeleteUserByEmail(c echo.Context) error {
+func (r *RouterUsers)DeleteUserByEmail(c echo.Context) error {
     // Retrieves logged user from context.
     user, err := getLoggedUser(r.users, c)
     if err != nil {
@@ -81,7 +81,7 @@ func (r *RouterUsers) DeleteUserByEmail(c echo.Context) error {
 // Creates a user.
 // Requires CAP_USER_CREATE.
 // curl --cacert ca.pem -X POST http://localhost:1077/users -H 'Content-Type: application/json' -d '{"email":"johndoe@nexus.org","firstName":"John","lastName":"Doe","pwd":"pipomolo","caps":{"USER_LIST":1,"VM_CREATE":1, "VM_SET_ACCESS":1}}' -H "Authorization: Bearer <AccessToken>"
-func (r *RouterUsers) CreateUser(c echo.Context) error {
+func (r *RouterUsers)CreateUser(c echo.Context) error {
     // Retrieves logged user from context.
     user, err := getLoggedUser(r.users, c)
     if err != nil {
@@ -118,7 +118,7 @@ func (r *RouterUsers) CreateUser(c echo.Context) error {
 // Update a user's capabilities.
 // Requires CAP_USER_SET_CAPS.
 // curl --cacert ca.pem -X PUT http://localhost:1077/users/janedoes@nexus.org/caps -H 'Content-Type: application/json' -d '{"caps":{"USER_LIST":1,"VM_CREATE":1, "VM_SET_ACCESS":1}}' -H "Authorization: Bearer <AccessToken>"
-func (r *RouterUsers) SetUserCaps(c echo.Context) error {
+func (r *RouterUsers)SetUserCaps(c echo.Context) error {
     // Retrieves logged user from context.
     user, err := getLoggedUser(r.users, c)
     if err != nil {
@@ -159,7 +159,7 @@ func (r *RouterUsers) SetUserCaps(c echo.Context) error {
 
 // Update the logged user's password.
 // curl --cacert ca.pem -X PUT http://localhost:1077/users/pwd -H 'Content-Type: application/json' -d '{"pwd": "new_password"}' -H "Authorization: Bearer <AccessToken>"
-func (r *RouterUsers) SetUserPwd(c echo.Context) error {
+func (r *RouterUsers)SetUserPwd(c echo.Context) error {
     // Deserializes and validates client's parameters.
     p := new(params.UserSetPwd)
     if err := decodeJson(c, &p); err != nil {
@@ -187,3 +187,71 @@ func (r *RouterUsers) SetUserPwd(c echo.Context) error {
 
     return c.JSONPretty(http.StatusOK, jsonMsg("OK"), "    ")
 }
+
+// Unlock a user.
+// Requires CAP_USER_UNLOCK.
+// curl --cacert ca.pem -X PUT http://localhost:1077/users/janedoes@nexus.org/unlock -H 'Content-Type: application/json' -H "Authorization: Bearer <AccessToken>"
+func (r *RouterUsers)UnlockUser(c echo.Context) error {
+    // Retrieves logged user from context.
+    user, err := getLoggedUser(r.users, c)
+    if err != nil {
+        return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
+    }
+
+    if !user.HasCapability(caps.CAP_USER_UNLOCK) {
+        return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
+    }
+
+    userToUnlock, err := r.users.GetUserByEmail(c.Param("email"))
+    if err != nil {
+        return echo.NewHTTPError(http.StatusNotFound, err.Error())
+    }
+
+    if err = UnlockUser(c, userToUnlock.Email); err != nil {
+        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+    }
+
+    return c.JSONPretty(http.StatusOK, jsonMsg("OK"), "    ")
+}
+
+// Reset a user's password.
+// Requires CAP_USER_RESETPWD.
+// curl --cacert ca.pem -X PUT http://localhost:1077/users/janedoes@nexus.org/resetpwd -H 'Content-Type: application/json' -H "Authorization: Bearer <AccessToken>"
+func (r *RouterUsers)ResetPwd(c echo.Context) error {
+    // Retrieves logged user from context.
+    user, err := getLoggedUser(r.users, c)
+    if err != nil {
+        return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
+    }
+
+    if !user.HasCapability(caps.CAP_USER_RESETPWD) {
+        return echo.NewHTTPError(http.StatusUnauthorized, msgInsufficientCaps)
+    }
+
+    userToResetPwd, err := r.users.GetUserByEmail(c.Param("email"))
+    if err != nil {
+        return echo.NewHTTPError(http.StatusNotFound, err.Error())
+    }
+
+    pwdLength := 8
+    pwdDigitCount := 4
+    pwdSymbolCount := 0
+    pwdRepeatChars := false
+    randomPwd, err := utils.CustomPwd.Generate(pwdLength, pwdDigitCount, pwdSymbolCount, false, pwdRepeatChars)
+    if err != nil {
+        msg := "Failed to reset password for user "+userToResetPwd.Email+": "+err.Error()
+        log.Error(msg)
+        return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+    }
+
+    userToResetPwd.Pwd = utils.HashPassword(randomPwd)
+
+    // Updates user and saves the new user file.
+    if err = r.users.UpdateUser(&userToResetPwd); err != nil {
+        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
+    }
+
+    log.Info("User ", user.Email, " reset password for user ", userToResetPwd.Email)
+
+    return c.JSON(http.StatusOK, &params.UserSetPwd{randomPwd})
+}
diff --git a/src/server/utils/password.go b/src/server/utils/password.go
new file mode 100644
index 0000000000000000000000000000000000000000..bffa2fda5ec1476e0528cc56c9f537250bed0a11
--- /dev/null
+++ b/src/server/utils/password.go
@@ -0,0 +1,13 @@
+package utils
+
+import (
+    "github.com/sethvargo/go-password/password"
+)
+
+// Custom password generator with the following characters removed: 0, O, l, I
+var CustomPwd, _ = password.NewGenerator(&password.GeneratorInput{
+    LowerLetters: "abcdefghijkmnopqrstuvwxyz",
+    UpperLetters: "ABCDEFGHJKLMNPQRSTUVWXYZ",
+    Digits: "123456789",
+})
+
diff --git a/src/server/version/version.go b/src/server/version/version.go
index 96c9c92bfc1670831212e2d061e57403528e2bdb..e842cbab98a6c78c06ec3e4d0cc592c71c42f242 100644
--- a/src/server/version/version.go
+++ b/src/server/version/version.go
@@ -6,8 +6,8 @@ import (
 
 const (
     major  = 1
-    minor  = 9
-    bugfix = 3
+    minor  = 10
+    bugfix = 0
 )
 
 type Version struct {
diff --git a/src/server/vms/vm.go b/src/server/vms/vm.go
index 1aca6263aff0f3450507b91d9f3534f9185dce6c..681e35db4e4dca7ea2273168d7abec51c1a1b2ac 100644
--- a/src/server/vms/vm.go
+++ b/src/server/vms/vm.go
@@ -15,7 +15,6 @@ import (
     "nexus-server/exec"
     "github.com/google/uuid"
     "github.com/go-playground/validator/v10"
-    "github.com/sethvargo/go-password/password"
 )
 
 type (
@@ -49,13 +48,6 @@ const (
     vmQGASockFile  = "qga.sock"
 )
 
-// Custom password generator with the following characters removed: 0, O, l, I
-var passwordGen, _ = password.NewGenerator(&password.GeneratorInput{
-    LowerLetters: "abcdefghijkmnopqrstuvwxyz",
-    UpperLetters: "ABCDEFGHJKLMNPQRSTUVWXYZ",
-    Digits: "123456789",
-})
-
 // Creates a VM and returns a pointer to it.
 // Concurrency: safe
 func NewVM(creatorEmail string, name string, cpus, ram int, nic vmc.NicType, usbDevs []string, templateID uuid.UUID, owner string) (*VM, error) {
diff --git a/src/server/vms/vms.go b/src/server/vms/vms.go
index ba927da2b0b6b42be7a9a8bf4b2152221930f587..00b7c99caa4acc96cdffb3f289feaa2d717c7539 100644
--- a/src/server/vms/vms.go
+++ b/src/server/vms/vms.go
@@ -403,7 +403,7 @@ func (vms *VMs)StartVM(vmID uuid.UUID) (int, string, error) {
 
     // Randomly generates a 8 characters long password with 4 digits, 0 symbols,
     // allowing upper and lower case letters, disallowing repeat characters.
-    pwd, err := passwordGen.Generate(conf.VMPwdLength, conf.VMPwdDigitCount, conf.VMPwdSymbolCount, false, conf.VMPwdRepeatChars)
+    pwd, err := utils.CustomPwd.Generate(conf.VMPwdLength, conf.VMPwdDigitCount, conf.VMPwdSymbolCount, false, conf.VMPwdRepeatChars)
     if err != nil {
         msg := "Failed starting VM "+vmID.String()+": password generation error: "+err.Error()
         log.Error(msg)