diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go index 0891917b3..dceb50c94 100644 --- a/server/middleware/jwt.go +++ b/server/middleware/jwt.go @@ -11,19 +11,51 @@ import ( "NanoKVM-Server/config" ) +// Role constants mirrored here to avoid circular imports. +const ( + RoleAdmin = "admin" + RoleOperator = "operator" + RoleViewer = "viewer" +) + type Token struct { Username string `json:"username"` + Role string `json:"role"` jwt.RegisteredClaims } +// CheckToken allows any authenticated user. func CheckToken() gin.HandlerFunc { return func(c *gin.Context) { - if allowByToken(c) { - c.Next() + token, ok := parseTokenFromContext(c) + if !ok { + abortUnauthorized(c) return } + // Store username and role for downstream handlers. + c.Set("username", token.Username) + c.Set("role", token.Role) + c.Next() + } +} - abortUnauthorized(c) +// RequireRole returns a middleware that only allows users with one of the given roles. +func RequireRole(roles ...string) gin.HandlerFunc { + allowed := make(map[string]bool, len(roles)) + for _, r := range roles { + allowed[r] = true + } + return func(c *gin.Context) { + role, exists := c.Get("role") + if !exists { + abortForbidden(c) + return + } + if !allowed[role.(string)] { + abortForbidden(c) + return + } + c.Next() } } @@ -33,36 +65,43 @@ func CheckLoopbackInternalToken() gin.HandlerFunc { c.Next() return } - abortUnauthorized(c) } } func CheckTokenOrLoopbackInternalToken() gin.HandlerFunc { return func(c *gin.Context) { - if allowByToken(c) || allowByLoopbackInternalToken(c.Request) { + token, ok := parseTokenFromContext(c) + if ok { + c.Set("username", token.Username) + c.Set("role", token.Role) + c.Next() + return + } + if allowByLoopbackInternalToken(c.Request) { c.Next() return } - abortUnauthorized(c) } } -func allowByToken(c *gin.Context) bool { +func parseTokenFromContext(c *gin.Context) (*Token, bool) { conf := config.GetInstance() - if conf.Authentication == "disable" { - return true + c.Set("username", "admin") + c.Set("role", RoleAdmin) + return &Token{Username: "admin", Role: RoleAdmin}, true } - cookie, err := c.Cookie("nano-kvm-token") if err != nil { - return false + return nil, false } - - _, err = ParseJWT(cookie) - return err == nil + token, err := ParseJWT(cookie) + if err != nil { + return nil, false + } + return token, true } func abortUnauthorized(c *gin.Context) { @@ -70,26 +109,27 @@ func abortUnauthorized(c *gin.Context) { c.Abort() } -func GenerateJWT(username string) (string, error) { - conf := config.GetInstance() +func abortForbidden(c *gin.Context) { + c.JSON(http.StatusForbidden, "forbidden: insufficient permissions") + c.Abort() +} +func GenerateJWT(username, role string) (string, error) { + conf := config.GetInstance() expireDuration := time.Duration(conf.JWT.RefreshTokenDuration) * time.Second - claims := Token{ Username: username, + Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireDuration)), }, } - t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return t.SignedString([]byte(conf.JWT.SecretKey)) } func ParseJWT(jwtToken string) (*Token, error) { conf := config.GetInstance() - t, err := jwt.ParseWithClaims(jwtToken, &Token{}, func(token *jwt.Token) (interface{}, error) { return []byte(conf.JWT.SecretKey), nil }) @@ -97,10 +137,8 @@ func ParseJWT(jwtToken string) (*Token, error) { log.Debugf("parse jwt error: %s", err) return nil, err } - if claims, ok := t.Claims.(*Token); ok && t.Valid { return claims, nil - } else { - return nil, err } + return nil, err } diff --git a/server/proto/auth.go b/server/proto/auth.go index dcf92cff0..828799b44 100644 --- a/server/proto/auth.go +++ b/server/proto/auth.go @@ -11,6 +11,7 @@ type LoginRsp struct { type GetAccountRsp struct { Username string `json:"username"` + Role string `json:"role"` } type ChangePasswordReq struct { @@ -21,3 +22,26 @@ type ChangePasswordReq struct { type IsPasswordUpdatedRsp struct { IsUpdated bool `json:"isUpdated"` } + +// --- Multi-user management --- + +type UserInfo struct { + Username string `json:"username"` + Role string `json:"role"` + Enabled bool `json:"enabled"` +} + +type ListUsersRsp struct { + Users []UserInfo `json:"users"` +} + +type CreateUserReq struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + Role string `json:"role" validate:"required"` +} + +type UpdateUserReq struct { + Role string `json:"role"` + Enabled *bool `json:"enabled"` +} diff --git a/server/router/auth.go b/server/router/auth.go index aa237fa0f..8b0acda30 100644 --- a/server/router/auth.go +++ b/server/router/auth.go @@ -10,12 +10,27 @@ import ( func authRouter(r *gin.Engine) { service := auth.NewService() - r.POST("/api/auth/login", service.Login) // login + // Public – no token required + r.POST("/api/auth/login", service.Login) + // Any authenticated user api := r.Group("/api").Use(middleware.CheckToken()) + api.GET("/auth/password", service.IsPasswordUpdated) + api.POST("/auth/password", service.ChangePassword) + api.GET("/auth/account", service.GetAccount) + api.POST("/auth/logout", service.Logout) - api.GET("/auth/password", service.IsPasswordUpdated) // is password updated - api.GET("/auth/account", service.GetAccount) // get account - api.POST("/auth/password", service.ChangePassword) // change password - api.POST("/auth/logout", service.Logout) // logout + // Any authenticated user may change their own password; + // admin may change any user's password (enforced inside handler). + api.POST("/auth/users/:username/password", service.ChangeUserPassword) + + // Admin-only: full user management + adminAPI := r.Group("/api").Use( + middleware.CheckToken(), + middleware.RequireRole(middleware.RoleAdmin), + ) + adminAPI.GET("/auth/users", service.ListUsers) + adminAPI.POST("/auth/users", service.CreateUser) + adminAPI.PUT("/auth/users/:username", service.UpdateUser) + adminAPI.DELETE("/auth/users/:username", service.DeleteUser) } diff --git a/server/router/hid.go b/server/router/hid.go index ac4e897ce..e7efcc98b 100644 --- a/server/router/hid.go +++ b/server/router/hid.go @@ -15,22 +15,36 @@ func HIDLoopbackHTTPAllowedPaths() []string { func hidRouter(r *gin.Engine) { service := hid.NewService() - api := r.Group("/api").Use(middleware.CheckToken()) - localAPI := r.Group("/api/internal").Use(middleware.CheckLoopbackInternalToken()) - api.POST("/hid/paste", service.Paste) // paste + // Operator and admin may send inputs (paste, shortcuts, read/use keyboard) + opAPI := r.Group("/api").Use( + middleware.CheckToken(), + middleware.RequireRole(middleware.RoleAdmin, middleware.RoleOperator), + ) + + opAPI.POST("/hid/paste", service.Paste) // paste + + opAPI.GET("/hid/shortcuts", service.GetShortcuts) // get shortcuts + opAPI.POST("/hid/shortcut", service.AddShortcut) // add shortcut + opAPI.DELETE("/hid/shortcut", service.DeleteShortcut) // delete shortcut + + opAPI.GET("/hid/shortcut/leader-key", service.GetLeaderKey) // get shortcut leader key + opAPI.POST("/hid/shortcut/leader-key", service.SetLeaderKey) // set shortcut leader key - api.GET("/hid/shortcuts", service.GetShortcuts) // get shortcuts - api.POST("/hid/shortcut", service.AddShortcut) // add shortcut - api.DELETE("/hid/shortcut", service.DeleteShortcut) // delete shortcut + opAPI.GET("/hid/mode", service.GetHidMode) // get hid mode - api.GET("/hid/shortcut/leader-key", service.GetLeaderKey) // set shortcut leader key - api.POST("/hid/shortcut/leader-key", service.SetLeaderKey) // set shortcut leader key + opAPI.GET("/hid/leds", service.GetKeyboardLedStatus) // get keyboard led status - api.GET("/hid/mode", service.GetHidMode) // get hid mode - api.POST("/hid/mode", service.SetHidMode) // set hid mode - api.POST("/hid/reset", service.ResetHid) // reset hid - api.GET("/hid/leds", service.GetKeyboardLedStatus) + // Admin only: HID hardware configuration + adminAPI := r.Group("/api").Use( + middleware.CheckToken(), + middleware.RequireRole(middleware.RoleAdmin), + ) + adminAPI.POST("/hid/mode", service.SetHidMode) // set hid mode + adminAPI.POST("/hid/reset", service.ResetHid) // reset hid + + // Internal loopback (for kvm_system / picoclaw): no JWT, only loopback token + localAPI := r.Group("/api/internal").Use(middleware.CheckLoopbackInternalToken()) localAPI.POST("/usb/recover", service.RecoverUSB) -} +} \ No newline at end of file diff --git a/server/router/network.go b/server/router/network.go index 9aeb4b6af..f148d730c 100644 --- a/server/router/network.go +++ b/server/router/network.go @@ -10,20 +10,28 @@ import ( func networkRouter(r *gin.Engine) { service := network.NewService() + // Unauthenticated endpoints (only meaningful in AP/setup mode) r.POST("/api/network/wifi", service.ConnectWifiNoAuth) // connect Wi-Fi without auth (only available in ap mode) r.POST("/api/network/wifi/verify", service.VerifyApLogin) // verify ap login - api := r.Group("/api").Use(middleware.CheckToken()) - - api.POST("/network/wol", service.WakeOnLAN) // wake on lan - api.GET("/network/wol/mac", service.GetMac) // get mac list - api.DELETE("/network/wol/mac", service.DeleteMac) // delete mac - api.POST("/network/wol/mac/name", service.SetMacName) // set mac name - - api.GET("/network/wifi", service.GetWifi) // get Wi-Fi information - api.POST("/network/wifi/connect", service.ConnectWifi) // connect Wi-Fi - api.POST("/network/wifi/disconnect", service.DisconnectWifi) // disconnect Wi-Fi - - api.GET("/network/dns", service.GetDNS) // get DNS configuration - api.POST("/network/dns", service.SetDNS) // set DNS configuration + // Operator and admin: read network state, perform Wake-on-LAN + opAPI := r.Group("/api").Use( + middleware.CheckToken(), + middleware.RequireRole(middleware.RoleAdmin, middleware.RoleOperator), + ) + opAPI.POST("/network/wol", service.WakeOnLAN) // wake on lan + opAPI.GET("/network/wol/mac", service.GetMac) // get mac list + opAPI.GET("/network/wifi", service.GetWifi) // get Wi-Fi information + opAPI.GET("/network/dns", service.GetDNS) // get DNS configuration + + // Admin only: network configuration + adminAPI := r.Group("/api").Use( + middleware.CheckToken(), + middleware.RequireRole(middleware.RoleAdmin), + ) + adminAPI.DELETE("/network/wol/mac", service.DeleteMac) // delete mac + adminAPI.POST("/network/wol/mac/name", service.SetMacName) // set mac name + adminAPI.POST("/network/wifi/connect", service.ConnectWifi) // connect Wi-Fi + adminAPI.POST("/network/wifi/disconnect", service.DisconnectWifi) // disconnect Wi-Fi + adminAPI.POST("/network/dns", service.SetDNS) // set DNS configuration } diff --git a/server/router/vm.go b/server/router/vm.go index d9fc1c3d5..3df5dd69d 100644 --- a/server/router/vm.go +++ b/server/router/vm.go @@ -10,64 +10,59 @@ import ( func vmRouter(r *gin.Engine) { service := vm.NewService() - api := r.Group("/api").Use(middleware.CheckToken()) - - api.GET("/vm/info", service.GetInfo) // get device information - api.GET("/vm/hardware", service.GetHardware) // get hardware version - - api.POST("/vm/gpio", service.SetGpio) // update gpio - api.GET("/vm/gpio", service.GetGpio) // get gpio - api.POST("/vm/screen", service.SetScreen) // update screen - - api.GET("/vm/terminal", service.Terminal) // web terminal - - api.GET("/vm/script", service.GetScripts) // get script - api.POST("/vm/script/upload", service.UploadScript) // upload script - api.POST("/vm/script/run", service.RunScript) // run script - api.DELETE("/vm/script", service.DeleteScript) // delete script - - api.GET("/vm/device/virtual", service.GetVirtualDevice) // get virtual device - api.POST("/vm/device/virtual", service.UpdateVirtualDevice) // update virtual device - - api.GET("/vm/memory/limit", service.GetMemoryLimit) // get memory limit - api.POST("/vm/memory/limit", service.SetMemoryLimit) // set memory limit - - api.GET("/vm/oled", service.GetOLED) // get OLED configuration - api.POST("/vm/oled", service.SetOLED) // set OLED configuration - - // Only supported by PCIe version - api.GET("/vm/hdmi", service.GetHdmiState) // get HDMI state - api.POST("/vm/hdmi/reset", service.ResetHdmi) // reset hdmi - api.POST("/vm/hdmi/enable", service.EnableHdmi) // enable hdmi - api.POST("/vm/hdmi/disable", service.DisableHdmi) // disable hdmi - api.POST("/vm/hdmi/timeout", service.SetHdmiIdleTimeout) - - api.GET("/vm/ssh", service.GetSSHState) // get SSH state - api.POST("/vm/ssh/enable", service.EnableSSH) // enable SSH - api.POST("/vm/ssh/disable", service.DisableSSH) // disable SSH - - api.GET("/vm/swap", service.GetSwap) // get swap file size - api.POST("/vm/swap", service.SetSwap) // set swap file size - - api.GET("/vm/mouse-jiggler", service.GetMouseJiggler) // get mouse jiggler - api.POST("/vm/mouse-jiggler/", service.SetMouseJiggler) // set mouse jiggler - - api.GET("/vm/hostname", service.GetHostname) // Get Hostname - api.POST("/vm/hostname", service.SetHostname) // Set Hostname - - api.GET("/vm/web-title", service.GetWebTitle) // Get web title - api.POST("/vm/web-title", service.SetWebTitle) // Set web title - - api.GET("/vm/mdns", service.GetMdnsState) // get mDNS state - api.POST("/vm/mdns/enable", service.EnableMdns) // enable mDNS - api.POST("/vm/mdns/disable", service.DisableMdns) // disable mDNS - - api.POST("/vm/tls", service.SetTls) // enable/disable TLS - - api.GET("/vm/autostart", service.GetAutostart) // get autostart list - api.GET("/vm/autostart/:name", service.GetAutostartContent) // get autostart content - api.DELETE("/vm/autostart/:name", service.DeleteAutostart) // delete autostart script - api.POST("/vm/autostart/:name", service.UploadAutostart) // upload autostart script - - api.POST("/vm/system/reboot", service.Reboot) // reboot system + // All authenticated users (viewer, operator, admin) may read basic info + anyAPI := r.Group("/api").Use(middleware.CheckToken()) + anyAPI.GET("/vm/info", service.GetInfo) + anyAPI.GET("/vm/hardware", service.GetHardware) + anyAPI.GET("/vm/gpio", service.GetGpio) + anyAPI.GET("/vm/device/virtual", service.GetVirtualDevice) + anyAPI.GET("/vm/memory/limit", service.GetMemoryLimit) + anyAPI.GET("/vm/oled", service.GetOLED) + anyAPI.GET("/vm/hdmi", service.GetHdmiState) + anyAPI.GET("/vm/ssh", service.GetSSHState) + anyAPI.GET("/vm/swap", service.GetSwap) + anyAPI.GET("/vm/mouse-jiggler", service.GetMouseJiggler) + anyAPI.GET("/vm/hostname", service.GetHostname) + anyAPI.GET("/vm/web-title", service.GetWebTitle) + anyAPI.GET("/vm/mdns", service.GetMdnsState) + anyAPI.GET("/vm/autostart", service.GetAutostart) + anyAPI.GET("/vm/autostart/:name", service.GetAutostartContent) + + // Operator and admin may interact with the machine + opAPI := r.Group("/api").Use( + middleware.CheckToken(), + middleware.RequireRole(middleware.RoleAdmin, middleware.RoleOperator), + ) + opAPI.POST("/vm/gpio", service.SetGpio) // power/reset buttons + opAPI.GET("/vm/terminal", service.Terminal) // web terminal + opAPI.GET("/vm/script", service.GetScripts) + opAPI.POST("/vm/script/run", service.RunScript) + opAPI.POST("/vm/mouse-jiggler/", service.SetMouseJiggler) + + // Admin only: system configuration + adminAPI := r.Group("/api").Use( + middleware.CheckToken(), + middleware.RequireRole(middleware.RoleAdmin), + ) + adminAPI.POST("/vm/screen", service.SetScreen) + adminAPI.POST("/vm/script/upload", service.UploadScript) + adminAPI.DELETE("/vm/script", service.DeleteScript) + adminAPI.POST("/vm/device/virtual", service.UpdateVirtualDevice) + adminAPI.POST("/vm/memory/limit", service.SetMemoryLimit) + adminAPI.POST("/vm/oled", service.SetOLED) + adminAPI.POST("/vm/hdmi/reset", service.ResetHdmi) + adminAPI.POST("/vm/hdmi/enable", service.EnableHdmi) + adminAPI.POST("/vm/hdmi/disable", service.DisableHdmi) + adminAPI.POST("/vm/hdmi/timeout", service.SetHdmiIdleTimeout) + adminAPI.POST("/vm/ssh/enable", service.EnableSSH) + adminAPI.POST("/vm/ssh/disable", service.DisableSSH) + adminAPI.POST("/vm/swap", service.SetSwap) + adminAPI.POST("/vm/hostname", service.SetHostname) + adminAPI.POST("/vm/web-title", service.SetWebTitle) + adminAPI.POST("/vm/mdns/enable", service.EnableMdns) + adminAPI.POST("/vm/mdns/disable", service.DisableMdns) + adminAPI.POST("/vm/tls", service.SetTls) + adminAPI.DELETE("/vm/autostart/:name", service.DeleteAutostart) + adminAPI.POST("/vm/autostart/:name", service.UploadAutostart) + adminAPI.POST("/vm/system/reboot", service.Reboot) } diff --git a/server/service/auth/account.go b/server/service/auth/account.go index 0305965e8..76c7f0632 100644 --- a/server/service/auth/account.go +++ b/server/service/auth/account.go @@ -1,7 +1,6 @@ package auth import ( - "NanoKVM-Server/utils" "encoding/json" "errors" "os" @@ -9,105 +8,248 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" + + "NanoKVM-Server/utils" ) -const AccountFile = "/etc/kvm/pwd" +const AccountFile = "/etc/kvm/accounts.json" +const LegacyAccountFile = "/etc/kvm/pwd" +// Role defines the permission level of a user. +type Role string + +const ( + RoleAdmin Role = "admin" // Full access including user management + RoleOperator Role = "operator" // KVM control: stream, keyboard, mouse, GPIO + RoleViewer Role = "viewer" // View-only: stream access +) + +// Account represents a single user. type Account struct { Username string `json:"username"` - Password string `json:"password"` // should be named HashedPassword for clarity + Password string `json:"password"` // bcrypt hash + Role Role `json:"role"` + Enabled bool `json:"enabled"` } -func GetAccount() (*Account, error) { - if _, err := os.Stat(AccountFile); err != nil { - if errors.Is(err, os.ErrNotExist) { - return getDefaultAccount(), nil - } - return nil, err +// legacyAccount mirrors the old single-user format for migration. +type legacyAccount struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// GetAccounts returns all accounts, migrating from legacy format if needed. +func GetAccounts() ([]Account, error) { + if _, err := os.Stat(AccountFile); err == nil { + return readAccountsFile() + } + if _, err := os.Stat(LegacyAccountFile); err == nil { + return migrateLegacyAccount() } + return []Account{defaultAdminAccount()}, nil +} - content, err := os.ReadFile(AccountFile) +// GetAccountByUsername returns a specific account or an error if not found. +func GetAccountByUsername(username string) (*Account, error) { + accounts, err := GetAccounts() if err != nil { return nil, err } - - var account Account - if err = json.Unmarshal(content, &account); err != nil { - log.Errorf("unmarshal account failed: %s", err) - return nil, err + for _, a := range accounts { + if a.Username == username { + acc := a + return &acc, nil + } } - - return &account, nil + return nil, errors.New("user not found") } -func SetAccount(username string, hashedPassword string) error { - account, err := json.Marshal(&Account{ - Username: username, - Password: hashedPassword, - }) +// SaveAccounts writes the full account list to disk. +func SaveAccounts(accounts []Account) error { + data, err := json.MarshalIndent(accounts, "", " ") if err != nil { - log.Errorf("failed to marshal account information to json: %s", err) return err } + if err = os.MkdirAll(filepath.Dir(AccountFile), 0o755); err != nil { + return err + } + return os.WriteFile(AccountFile, data, 0o600) +} - err = os.MkdirAll(filepath.Dir(AccountFile), 0o644) +// AddAccount appends a new user. Returns error if username exists. +func AddAccount(username, plainPassword string, role Role) error { + accounts, err := GetAccounts() if err != nil { - log.Errorf("create directory %s failed: %s", AccountFile, err) return err } - - err = os.WriteFile(AccountFile, account, 0o644) + for _, a := range accounts { + if a.Username == username { + return errors.New("username already exists") + } + } + hashed, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost) if err != nil { - log.Errorf("write password failed: %s", err) return err } - - return nil + accounts = append(accounts, Account{ + Username: username, + Password: string(hashed), + Role: role, + Enabled: true, + }) + return SaveAccounts(accounts) } -func CompareAccount(username string, plainPassword string) bool { - account, err := GetAccount() +// UpdateAccountPassword changes a user's password (expects bcrypt hash). +func UpdateAccountPassword(username, hashedPassword string) error { + accounts, err := GetAccounts() if err != nil { - return false + return err + } + for i, a := range accounts { + if a.Username == username { + accounts[i].Password = hashedPassword + return SaveAccounts(accounts) + } } + return errors.New("user not found") +} - if username != account.Username { - return false +// UpdateAccountRole changes a user's role. +func UpdateAccountRole(username string, role Role) error { + accounts, err := GetAccounts() + if err != nil { + return err } + for i, a := range accounts { + if a.Username == username { + accounts[i].Role = role + return SaveAccounts(accounts) + } + } + return errors.New("user not found") +} - hashedPassword, err := utils.DecodeDecrypt(plainPassword) - if err != nil || hashedPassword == "" { - return false +// SetAccountEnabled enables or disables a user account. +func SetAccountEnabled(username string, enabled bool) error { + accounts, err := GetAccounts() + if err != nil { + return err } + for i, a := range accounts { + if a.Username == username { + accounts[i].Enabled = enabled + return SaveAccounts(accounts) + } + } + return errors.New("user not found") +} - err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(hashedPassword)) +// DeleteAccount removes a user. The last admin account cannot be deleted. +func DeleteAccount(username string) error { + accounts, err := GetAccounts() if err != nil { - // Compatible with old versions - accountHashedPassword, _ := utils.DecodeDecrypt(account.Password) - if accountHashedPassword == hashedPassword { - return true + return err + } + var target *Account + for _, a := range accounts { + if a.Username == username { + acc := a + target = &acc + break } + } + if target == nil { + return errors.New("user not found") + } + if target.Role == RoleAdmin { + adminCount := 0 + for _, a := range accounts { + if a.Role == RoleAdmin && a.Enabled { + adminCount++ + } + } + if adminCount <= 1 { + return errors.New("cannot delete the last admin account") + } + } + filtered := make([]Account, 0, len(accounts)-1) + for _, a := range accounts { + if a.Username != username { + filtered = append(filtered, a) + } + } + return SaveAccounts(filtered) +} - return false +// CompareAccount checks credentials and returns the account on success. +func CompareAccount(username, plainPassword string) (*Account, bool) { + account, err := GetAccountByUsername(username) + if err != nil || account == nil || !account.Enabled { + return nil, false } + decoded, err := utils.DecodeDecrypt(plainPassword) + if err != nil || decoded == "" { + return nil, false + } + if err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(decoded)); err != nil { + // Compatibility with old plain-hashed storage + oldHash, _ := utils.DecodeDecrypt(account.Password) + if oldHash != decoded { + return nil, false + } + } + return account, true +} - return true +// IsValidRole checks whether a role string is valid. +func IsValidRole(r Role) bool { + return r == RoleAdmin || r == RoleOperator || r == RoleViewer } -func DelAccount() error { - if err := os.Remove(AccountFile); err != nil { - log.Errorf("failed to delete password: %s", err) - return err +func readAccountsFile() ([]Account, error) { + data, err := os.ReadFile(AccountFile) + if err != nil { + return nil, err } - - return nil + var accounts []Account + if err = json.Unmarshal(data, &accounts); err != nil { + log.Errorf("failed to unmarshal accounts: %s", err) + return nil, err + } + return accounts, nil } -func getDefaultAccount() *Account { - hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost) +func migrateLegacyAccount() ([]Account, error) { + data, err := os.ReadFile(LegacyAccountFile) + if err != nil { + return nil, err + } + var legacy legacyAccount + if err = json.Unmarshal(data, &legacy); err != nil { + log.Errorf("failed to unmarshal legacy account: %s", err) + return []Account{defaultAdminAccount()}, nil + } + account := Account{ + Username: legacy.Username, + Password: legacy.Password, + Role: RoleAdmin, + Enabled: true, + } + accounts := []Account{account} + if saveErr := SaveAccounts(accounts); saveErr == nil { + _ = os.Remove(LegacyAccountFile) + log.Infof("migrated legacy account '%s' to multi-user format", legacy.Username) + } + return accounts, nil +} - return &Account{ +func defaultAdminAccount() Account { + hashed, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost) + return Account{ Username: "admin", - Password: string(hashedPassword), + Password: string(hashed), + Role: RoleAdmin, + Enabled: true, } } diff --git a/server/service/auth/login.go b/server/service/auth/login.go index 9e566d660..48344cc84 100644 --- a/server/service/auth/login.go +++ b/server/service/auth/login.go @@ -15,12 +15,9 @@ func (s *Service) Login(c *gin.Context) { var req proto.LoginReq var rsp proto.Response - // authentication disabled conf := config.GetInstance() if conf.Authentication == "disable" { - rsp.OkRspWithData(c, &proto.LoginRsp{ - Token: "disabled", - }) + rsp.OkRspWithData(c, &proto.LoginRsp{Token: "disabled"}) return } @@ -37,41 +34,35 @@ func (s *Service) Login(c *gin.Context) { return } - if ok := CompareAccount(req.Username, req.Password); !ok { + account, ok := CompareAccount(req.Username, req.Password) + if !ok { time.Sleep(2 * time.Second) - if locked, code, msg := RecordLoginFailure(clientIP); locked { rsp.ErrRsp(c, code, msg) return } - rsp.ErrRsp(c, -2, "invalid username or password") return } ClearLoginAttempt(clientIP) - token, err := middleware.GenerateJWT(req.Username) + token, err := middleware.GenerateJWT(account.Username, string(account.Role)) if err != nil { time.Sleep(1 * time.Second) rsp.ErrRsp(c, -3, "generate token failed") return } - rsp.OkRspWithData(c, &proto.LoginRsp{ - Token: token, - }) - - log.Debugf("login success, username: %s", req.Username) + rsp.OkRspWithData(c, &proto.LoginRsp{Token: token}) + log.Debugf("login success, username: %s, role: %s", account.Username, account.Role) } func (s *Service) Logout(c *gin.Context) { conf := config.GetInstance() - if conf.JWT.RevokeTokensOnLogout { config.RegenerateSecretKey() } - var rsp proto.Response rsp.OkRsp(c) } @@ -79,14 +70,12 @@ func (s *Service) Logout(c *gin.Context) { func (s *Service) GetAccount(c *gin.Context) { var rsp proto.Response - account, err := GetAccount() - if err != nil { - rsp.ErrRsp(c, -1, "get account failed") - return - } + username, _ := c.Get("username") + role, _ := c.Get("role") rsp.OkRspWithData(c, &proto.GetAccountRsp{ - Username: account.Username, + Username: username.(string), + Role: role.(string), }) log.Debugf("get account successful") } diff --git a/server/service/auth/password.go b/server/service/auth/password.go index 22c601613..c9461b993 100644 --- a/server/service/auth/password.go +++ b/server/service/auth/password.go @@ -1,19 +1,22 @@ package auth import ( - "NanoKVM-Server/proto" - "NanoKVM-Server/utils" "errors" "io" "os" "os/exec" "time" + "NanoKVM-Server/proto" + "NanoKVM-Server/utils" + "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" ) +// ChangePassword allows a user to change their own password (legacy endpoint). +// If the request contains no username, the current logged-in user is used. func (s *Service) ChangePassword(c *gin.Context) { var req proto.ChangePasswordReq var rsp proto.Response @@ -23,57 +26,61 @@ func (s *Service) ChangePassword(c *gin.Context) { return } + selfUsername, _ := c.Get("username") + selfRole, _ := c.Get("role") + + // If no username given, default to the current user. + if req.Username == "" { + req.Username = selfUsername.(string) + } + + // Only admins may change other accounts; others can only change their own. + if selfRole.(string) != "admin" && selfUsername.(string) != req.Username { + rsp.ErrRsp(c, -2, "permission denied") + return + } + password, err := utils.DecodeDecrypt(req.Password) if err != nil || password == "" { - rsp.ErrRsp(c, -2, "invalid password") + rsp.ErrRsp(c, -3, "invalid password") return } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { - rsp.ErrRsp(c, -3, "failed to hash password") + rsp.ErrRsp(c, -4, "failed to hash password") return } - if err = SetAccount(req.Username, string(hashedPassword)); err != nil { - rsp.ErrRsp(c, -4, "failed to save password") + if err = UpdateAccountPassword(req.Username, string(hashedPassword)); err != nil { + rsp.ErrRsp(c, -5, "failed to save password") return } - // change root password - err = changeRootPassword(password) - if err != nil { - _ = DelAccount() - rsp.ErrRsp(c, -5, "failed to change password") - return + // Only change the root system password when the admin changes their own password. + if req.Username == "admin" { + if err = changeRootPassword(password); err != nil { + log.Warnf("failed to change root password: %s", err) + } } rsp.OkRsp(c) - log.Debugf("change password success, username: %s", req.Username) + log.Debugf("password changed for user: %s", req.Username) } +// IsPasswordUpdated reports whether the admin password has been changed from the default. func (s *Service) IsPasswordUpdated(c *gin.Context) { var rsp proto.Response - if _, err := os.Stat(AccountFile); err != nil { - rsp.OkRspWithData(c, &proto.IsPasswordUpdatedRsp{ - IsUpdated: false, - }) - return - } - - account, err := GetAccount() - if err != nil || account == nil { - rsp.ErrRsp(c, -1, "failed to get password") + account, err := GetAccountByUsername("admin") + if err != nil { + rsp.OkRspWithData(c, &proto.IsPasswordUpdatedRsp{IsUpdated: false}) return } - err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte("admin")) - + checkErr := bcrypt.CompareHashAndPassword([]byte(account.Password), []byte("admin")) rsp.OkRspWithData(c, &proto.IsPasswordUpdatedRsp{ - // If the hash is not valid, still assume it's not updated - // The error we want to see is password and hash not matching - IsUpdated: errors.Is(err, bcrypt.ErrMismatchedHashAndPassword), + IsUpdated: errors.Is(checkErr, bcrypt.ErrMismatchedHashAndPassword), }) } @@ -83,42 +90,28 @@ func changeRootPassword(password string) error { log.Errorf("failed to change root password: %s", err) return err } - - log.Debugf("change root password successful.") + log.Debugf("root password changed successfully") return nil } func passwd(password string) error { cmd := exec.Command("passwd", "root") - stdin, err := cmd.StdinPipe() if err != nil { return err } - defer func() { - _ = stdin.Close() - }() - + defer func() { _ = stdin.Close() }() cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if err = cmd.Start(); err != nil { return err } - if _, err = io.WriteString(stdin, password+"\n"); err != nil { return err } - time.Sleep(100 * time.Millisecond) - if _, err = io.WriteString(stdin, password+"\n"); err != nil { return err } - - if err = cmd.Wait(); err != nil { - return err - } - - return nil + return cmd.Wait() } diff --git a/server/service/auth/users.go b/server/service/auth/users.go new file mode 100644 index 000000000..e252d4571 --- /dev/null +++ b/server/service/auth/users.go @@ -0,0 +1,176 @@ +package auth + +import ( + "NanoKVM-Server/proto" + "NanoKVM-Server/utils" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" +) + +// ListUsers returns all user accounts (passwords excluded). +func (s *Service) ListUsers(c *gin.Context) { + var rsp proto.Response + + accounts, err := GetAccounts() + if err != nil { + rsp.ErrRsp(c, -1, "failed to load users") + return + } + + users := make([]proto.UserInfo, 0, len(accounts)) + for _, a := range accounts { + users = append(users, proto.UserInfo{ + Username: a.Username, + Role: string(a.Role), + Enabled: a.Enabled, + }) + } + + rsp.OkRspWithData(c, &proto.ListUsersRsp{Users: users}) +} + +// CreateUser adds a new user (admin only). +func (s *Service) CreateUser(c *gin.Context) { + var req proto.CreateUserReq + var rsp proto.Response + + if err := proto.ParseFormRequest(c, &req); err != nil { + rsp.ErrRsp(c, -1, "invalid parameters") + return + } + + role := Role(req.Role) + if !IsValidRole(role) { + rsp.ErrRsp(c, -2, "invalid role; must be admin, operator, or viewer") + return + } + + password, err := utils.DecodeDecrypt(req.Password) + if err != nil || password == "" { + rsp.ErrRsp(c, -3, "invalid password") + return + } + + if err = AddAccount(req.Username, password, role); err != nil { + rsp.ErrRsp(c, -4, err.Error()) + return + } + + rsp.OkRsp(c) + log.Infof("user created: %s (role: %s)", req.Username, role) +} + +// UpdateUser changes a user's role or enabled status (admin only). +func (s *Service) UpdateUser(c *gin.Context) { + var req proto.UpdateUserReq + var rsp proto.Response + + username := c.Param("username") + if username == "" { + rsp.ErrRsp(c, -1, "username is required") + return + } + + if err := proto.ParseFormRequest(c, &req); err != nil { + rsp.ErrRsp(c, -2, "invalid parameters") + return + } + + if req.Role != "" { + role := Role(req.Role) + if !IsValidRole(role) { + rsp.ErrRsp(c, -3, "invalid role; must be admin, operator, or viewer") + return + } + if err := UpdateAccountRole(username, role); err != nil { + rsp.ErrRsp(c, -4, err.Error()) + return + } + log.Infof("user role updated: %s -> %s", username, role) + } + + if req.Enabled != nil { + if err := SetAccountEnabled(username, *req.Enabled); err != nil { + rsp.ErrRsp(c, -5, err.Error()) + return + } + log.Infof("user enabled state updated: %s -> %v", username, *req.Enabled) + } + + rsp.OkRsp(c) +} + +// DeleteUser removes a user account (admin only). +func (s *Service) DeleteUser(c *gin.Context) { + var rsp proto.Response + + username := c.Param("username") + if username == "" { + rsp.ErrRsp(c, -1, "username is required") + return + } + + // Prevent an admin from deleting themselves. + selfUsername, _ := c.Get("username") + if selfUsername.(string) == username { + rsp.ErrRsp(c, -2, "cannot delete your own account") + return + } + + if err := DeleteAccount(username); err != nil { + rsp.ErrRsp(c, -3, err.Error()) + return + } + + rsp.OkRsp(c) + log.Infof("user deleted: %s", username) +} + +// ChangeUserPassword allows an admin to set any user's password, +// or a user to change their own password. +func (s *Service) ChangeUserPassword(c *gin.Context) { + var req proto.ChangePasswordReq + var rsp proto.Response + + username := c.Param("username") + if username == "" { + rsp.ErrRsp(c, -1, "username is required") + return + } + + selfUsername, _ := c.Get("username") + selfRole, _ := c.Get("role") + + // Only admins may change other users' passwords. + if selfRole.(string) != "admin" && selfUsername.(string) != username { + rsp.ErrRsp(c, -2, "permission denied") + return + } + + if err := proto.ParseFormRequest(c, &req); err != nil { + rsp.ErrRsp(c, -3, "invalid parameters") + return + } + + password, err := utils.DecodeDecrypt(req.Password) + if err != nil || password == "" { + rsp.ErrRsp(c, -4, "invalid password") + return + } + + hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + rsp.ErrRsp(c, -5, "failed to hash password") + return + } + + if err = UpdateAccountPassword(username, string(hashed)); err != nil { + rsp.ErrRsp(c, -6, err.Error()) + return + } + + rsp.OkRsp(c) + log.Infof("password changed for user: %s", username) +} diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts index 3657ce4c5..132b4efde 100644 --- a/web/src/api/auth.ts +++ b/web/src/api/auth.ts @@ -1,10 +1,7 @@ import { http } from '@/lib/http'; export function login(username: string, password: string) { - const data = { - username, - password - }; + const data = { username, password }; return http.post('/api/auth/login', data); } @@ -17,13 +14,31 @@ export function getAccount() { } export function changePassword(username: string, password: string) { - const data = { - username, - password - }; + const data = { username, password }; return http.post('/api/auth/password', data); } export function isPasswordUpdated() { return http.get('/api/auth/password'); } + +// Multi-user management +export function listUsers() { + return http.get('/api/auth/users'); +} + +export function createUser(username: string, password: string, role: string) { + return http.post('/api/auth/users', { username, password, role }); +} + +export function updateUser(username: string, data: { role?: string; enabled?: boolean }) { + return http.put(`/api/auth/users/${username}`, data); +} + +export function deleteUser(username: string) { + return http.delete(`/api/auth/users/${username}`); +} + +export function changeUserPassword(username: string, password: string) { + return http.post(`/api/auth/users/${username}/password`, { username, password }); +} diff --git a/web/src/hooks/useRole.ts b/web/src/hooks/useRole.ts new file mode 100644 index 000000000..1affd41d8 --- /dev/null +++ b/web/src/hooks/useRole.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; +import * as api from '@/api/auth.ts'; + +export type Role = 'admin' | 'operator' | 'viewer' | 'loading'; + +export function useRole() { + const [role, setRole] = useState('loading'); + + useEffect(() => { + api.getAccount().then((rsp: any) => { + if (rsp.code === 0 && rsp.data?.role) { + setRole(rsp.data.role as Role); + } else { + // Fallback: alles anzeigen wenn Rolle nicht ermittelt werden kann + setRole('admin'); + } + }).catch(() => { + setRole('admin'); + }); + }, []); + + // Während Ladezeit alles anzeigen + const loaded = role !== 'loading'; + + return { + role, + isAdmin: !loaded || role === 'admin', + isOperator: !loaded || role === 'admin' || role === 'operator', + isViewer: loaded && role === 'viewer', + }; +} diff --git a/web/src/i18n/locales/ca.ts b/web/src/i18n/locales/ca.ts index d0f9d0d02..965de7734 100644 --- a/web/src/i18n/locales/ca.ts +++ b/web/src/i18n/locales/ca.ts @@ -547,6 +547,38 @@ const ca = { updateFailed: 'Error en actualitzar. Torna-ho a intentar.' } }, + users: { + title: 'Gestió d\'usuaris', + addUser: 'Afegir usuari', + colUsername: 'Nom d\'usuari', + colRole: 'Rol', + colEnabled: 'Actiu', + colActions: 'Accions', + rolesTitle: 'Visió general dels rols', + roleAdmin: 'Accés complet + gestió d\'usuaris', + roleOperator: 'Ús del KVM: stream, teclat, ratolí, botons d\'engegada', + roleViewer: 'Només visualització de l\'stream', + changePassword: 'Canviar contrasenya', + newPassword: 'Nova contrasenya', + confirmPassword: 'Confirmar contrasenya', + pwdMismatch: 'Les contrasenyes no coincideixen', + pwdSuccess: 'Contrasenya canviada correctament', + pwdFailed: 'No s\'ha pogut canviar la contrasenya', + password: 'Contrasenya', + delete: 'Eliminar', + deleteConfirm: 'Segur que voleu eliminar aquest usuari?', + createSuccess: 'Usuari creat', + createFailed: 'No s\'ha pogut crear l\'usuari', + deleteSuccess: 'Usuari eliminat', + deleteFailed: 'No s\'ha pogut eliminar l\'usuari', + updateSuccess: 'Actualitzat', + updateFailed: 'Actualització fallida', + loadFailed: 'No s\'han pogut carregar els usuaris', + usernameRequired: 'Introduïu el nom d\'usuari', + passwordRequired: 'Introduïu la contrasenya', + okBtn: 'D\'acord', + cancelBtn: 'Cancel·lar' + }, account: { title: 'Compte', webAccount: 'Nom del compte web', diff --git a/web/src/i18n/locales/cz.ts b/web/src/i18n/locales/cz.ts index 6e2f094e1..37ac2118b 100644 --- a/web/src/i18n/locales/cz.ts +++ b/web/src/i18n/locales/cz.ts @@ -549,6 +549,38 @@ const cz = { updateFailed: 'Aktualizace se nezdařila. Zkuste to prosím znovu.' } }, + users: { + title: 'Správa uživatelů', + addUser: 'Přidat uživatele', + colUsername: 'Uživatelské jméno', + colRole: 'Role', + colEnabled: 'Aktivní', + colActions: 'Akce', + rolesTitle: 'Přehled rolí', + roleAdmin: 'Plný přístup + správa uživatelů', + roleOperator: 'Použití KVM: stream, klávesnice, myš, tlačítka napájení', + roleViewer: 'Pouze zobrazení streamu', + changePassword: 'Změnit heslo', + newPassword: 'Nové heslo', + confirmPassword: 'Potvrďte heslo', + pwdMismatch: 'Hesla se neshodují', + pwdSuccess: 'Heslo úspěšně změněno', + pwdFailed: 'Nepodařilo se změnit heslo', + password: 'Heslo', + delete: 'Smazat', + deleteConfirm: 'Opravdu chcete smazat tohoto uživatele?', + createSuccess: 'Uživatel vytvořen', + createFailed: 'Vytvoření uživatele se nezdařilo', + deleteSuccess: 'Uživatel smazán', + deleteFailed: 'Smazání se nezdařilo', + updateSuccess: 'Aktualizováno', + updateFailed: 'Aktualizace se nezdařila', + loadFailed: 'Načtení uživatelů se nezdařilo', + usernameRequired: 'Zadejte uživatelské jméno', + passwordRequired: 'Zadejte heslo', + okBtn: 'OK', + cancelBtn: 'Zrušit' + }, account: { title: 'Účet', webAccount: 'Název webového účtu', diff --git a/web/src/i18n/locales/da.ts b/web/src/i18n/locales/da.ts index ac4260728..c22c3f8af 100644 --- a/web/src/i18n/locales/da.ts +++ b/web/src/i18n/locales/da.ts @@ -547,6 +547,38 @@ const da = { updateFailed: 'Opdatering fejlede. Prøv igen.' } }, + users: { + title: 'Brugeradministration', + addUser: 'Tilføj bruger', + colUsername: 'Brugernavn', + colRole: 'Rolle', + colEnabled: 'Aktiv', + colActions: 'Handlinger', + rolesTitle: 'Rolleoversigt', + roleAdmin: 'Fuld adgang + brugeradministration', + roleOperator: 'KVM-brug: stream, tastatur, mus, tænd-knapper', + roleViewer: 'Kun stream-visning', + changePassword: 'Skift adgangskode', + newPassword: 'Ny adgangskode', + confirmPassword: 'Bekræft adgangskode', + pwdMismatch: 'Adgangskoder stemmer ikke overens', + pwdSuccess: 'Adgangskode ændret', + pwdFailed: 'Kunne ikke ændre adgangskode', + password: 'Adgangskode', + delete: 'Slet', + deleteConfirm: 'Er du sikker på at du vil slette denne bruger?', + createSuccess: 'Bruger oprettet', + createFailed: 'Kunne ikke oprette bruger', + deleteSuccess: 'Bruger slettet', + deleteFailed: 'Sletning mislykkedes', + updateSuccess: 'Opdateret', + updateFailed: 'Opdatering mislykkedes', + loadFailed: 'Kunne ikke indlæse brugere', + usernameRequired: 'Indtast brugernavn', + passwordRequired: 'Indtast adgangskode', + okBtn: 'OK', + cancelBtn: 'Annuller' + }, account: { title: 'Konto', webAccount: 'Navn på webkonto', diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index f0937647d..96ed4afdb 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -556,6 +556,38 @@ const de = { updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.' } }, + users: { + title: 'Benutzerverwaltung', + addUser: 'Benutzer hinzufügen', + colUsername: 'Benutzername', + colRole: 'Rolle', + colEnabled: 'Aktiv', + colActions: 'Aktionen', + rolesTitle: 'Rollen-Übersicht', + roleAdmin: 'Vollzugriff + Benutzerverwaltung', + roleOperator: 'KVM nutzen: Stream, Tastatur, Maus, Power-Buttons', + roleViewer: 'Nur Stream anschauen', + changePassword: 'Passwort ändern', + newPassword: 'Neues Passwort', + confirmPassword: 'Passwort bestätigen', + pwdMismatch: 'Passwörter stimmen nicht überein', + pwdSuccess: 'Passwort erfolgreich geändert', + pwdFailed: 'Passwort ändern fehlgeschlagen', + password: 'Passwort', + delete: 'Löschen', + deleteConfirm: 'Benutzer wirklich löschen?', + createSuccess: 'Benutzer erstellt', + createFailed: 'Erstellen fehlgeschlagen', + deleteSuccess: 'Benutzer gelöscht', + deleteFailed: 'Löschen fehlgeschlagen', + updateSuccess: 'Aktualisiert', + updateFailed: 'Aktualisierung fehlgeschlagen', + loadFailed: 'Benutzer laden fehlgeschlagen', + usernameRequired: 'Benutzername eingeben', + passwordRequired: 'Passwort eingeben', + okBtn: 'OK', + cancelBtn: 'Abbrechen' + }, account: { title: 'Konto', webAccount: 'Web Konto Name', diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 087ad344f..701b9e968 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -543,6 +543,38 @@ const en = { updateFailed: 'Update failed. Please retry.' } }, + users: { + title: 'User Management', + addUser: 'Add User', + colUsername: 'Username', + colRole: 'Role', + colEnabled: 'Enabled', + colActions: 'Actions', + rolesTitle: 'Role Overview', + roleAdmin: 'Full access + user management', + roleOperator: 'KVM control: stream, keyboard, mouse, power buttons', + roleViewer: 'View-only: watch stream', + changePassword: 'Change Password', + newPassword: 'New Password', + confirmPassword: 'Confirm Password', + pwdMismatch: 'Passwords do not match', + pwdSuccess: 'Password changed successfully', + pwdFailed: 'Failed to change password', + password: 'Password', + delete: 'Delete', + deleteConfirm: 'Are you sure you want to delete this user?', + createSuccess: 'User created', + createFailed: 'Failed to create user', + deleteSuccess: 'User deleted', + deleteFailed: 'Failed to delete user', + updateSuccess: 'Updated', + updateFailed: 'Update failed', + loadFailed: 'Failed to load users', + usernameRequired: 'Please enter a username', + passwordRequired: 'Please enter a password', + okBtn: 'OK', + cancelBtn: 'Cancel' + }, account: { title: 'Account', webAccount: 'Web Account Name', diff --git a/web/src/i18n/locales/es.ts b/web/src/i18n/locales/es.ts index 8cc94f500..a96f08c47 100644 --- a/web/src/i18n/locales/es.ts +++ b/web/src/i18n/locales/es.ts @@ -555,6 +555,38 @@ const es = { updateFailed: 'La actualización falló. Por favor, inténtalo de nuevo.' } }, + users: { + title: 'Gestión de usuarios', + addUser: 'Agregar usuario', + colUsername: 'Nombre de usuario', + colRole: 'Rol', + colEnabled: 'Activo', + colActions: 'Acciones', + rolesTitle: 'Resumen de roles', + roleAdmin: 'Acceso completo + gestión de usuarios', + roleOperator: 'Uso del KVM: stream, teclado, ratón, botones de encendido', + roleViewer: 'Solo visualización del stream', + changePassword: 'Cambiar contraseña', + newPassword: 'Nueva contraseña', + confirmPassword: 'Confirmar contraseña', + pwdMismatch: 'Las contraseñas no coinciden', + pwdSuccess: 'Contraseña cambiada con éxito', + pwdFailed: 'Error al cambiar la contraseña', + password: 'Contraseña', + delete: 'Eliminar', + deleteConfirm: '¿Está seguro de que desea eliminar este usuario?', + createSuccess: 'Usuario creado', + createFailed: 'Error al crear el usuario', + deleteSuccess: 'Usuario eliminado', + deleteFailed: 'Error al eliminar', + updateSuccess: 'Actualizado', + updateFailed: 'Error de actualización', + loadFailed: 'Error al cargar usuarios', + usernameRequired: 'Ingrese el nombre de usuario', + passwordRequired: 'Ingrese la contraseña', + okBtn: 'Aceptar', + cancelBtn: 'Cancelar' + }, account: { title: 'Cuenta', webAccount: 'Nombre de la cuenta web', diff --git a/web/src/i18n/locales/fr.ts b/web/src/i18n/locales/fr.ts index c7b8e6fca..e058402f2 100644 --- a/web/src/i18n/locales/fr.ts +++ b/web/src/i18n/locales/fr.ts @@ -555,6 +555,38 @@ const fr = { updateFailed: 'Mise à jour échouée. Veuillez réessayer.' } }, + users: { + title: 'Gestion des utilisateurs', + addUser: 'Ajouter un utilisateur', + colUsername: 'Nom d\'utilisateur', + colRole: 'Rôle', + colEnabled: 'Actif', + colActions: 'Actions', + rolesTitle: 'Aperçu des rôles', + roleAdmin: 'Accès complet + gestion des utilisateurs', + roleOperator: 'Utiliser le KVM : flux, clavier, souris, boutons d\'alimentation', + roleViewer: 'Visualisation du flux uniquement', + changePassword: 'Changer le mot de passe', + newPassword: 'Nouveau mot de passe', + confirmPassword: 'Confirmer le mot de passe', + pwdMismatch: 'Les mots de passe ne correspondent pas', + pwdSuccess: 'Mot de passe modifié avec succès', + pwdFailed: 'Échec du changement de mot de passe', + password: 'Mot de passe', + delete: 'Supprimer', + deleteConfirm: 'Êtes-vous sûr de vouloir supprimer cet utilisateur ?', + createSuccess: 'Utilisateur créé', + createFailed: 'Échec de la création', + deleteSuccess: 'Utilisateur supprimé', + deleteFailed: 'Échec de la suppression', + updateSuccess: 'Mis à jour', + updateFailed: 'Échec de la mise à jour', + loadFailed: 'Échec du chargement des utilisateurs', + usernameRequired: 'Saisissez le nom d\'utilisateur', + passwordRequired: 'Saisissez le mot de passe', + okBtn: 'OK', + cancelBtn: 'Annuler' + }, account: { title: 'Compte', webAccount: 'Nom du compte Web', diff --git a/web/src/i18n/locales/hu.ts b/web/src/i18n/locales/hu.ts index a2ab1d13f..6b46907f3 100644 --- a/web/src/i18n/locales/hu.ts +++ b/web/src/i18n/locales/hu.ts @@ -551,6 +551,38 @@ const hu = { updateFailed: 'Frissítés sikertelen. Kérem, próbálja újra.' } }, + users: { + title: 'Felhasználókezelés', + addUser: 'Felhasználó hozzáadása', + colUsername: 'Felhasználónév', + colRole: 'Szerepkör', + colEnabled: 'Aktív', + colActions: 'Műveletek', + rolesTitle: 'Szerepkörök áttekintése', + roleAdmin: 'Teljes hozzáférés + felhasználókezelés', + roleOperator: 'KVM használata: stream, billentyűzet, egér, bekapcsoló gombok', + roleViewer: 'Csak stream megtekintés', + changePassword: 'Jelszó módosítása', + newPassword: 'Új jelszó', + confirmPassword: 'Jelszó megerősítése', + pwdMismatch: 'A jelszavak nem egyeznek', + pwdSuccess: 'Jelszó sikeresen módosítva', + pwdFailed: 'Jelszó módosítása sikertelen', + password: 'Jelszó', + delete: 'Törlés', + deleteConfirm: 'Biztosan törölni szeretné ezt a felhasználót?', + createSuccess: 'Felhasználó létrehozva', + createFailed: 'Létrehozás sikertelen', + deleteSuccess: 'Felhasználó törölve', + deleteFailed: 'Törlés sikertelen', + updateSuccess: 'Frissítve', + updateFailed: 'Frissítés sikertelen', + loadFailed: 'Felhasználók betöltése sikertelen', + usernameRequired: 'Adjon meg felhasználónevet', + passwordRequired: 'Adjon meg jelszót', + okBtn: 'OK', + cancelBtn: 'Mégse' + }, account: { title: 'Fiók', webAccount: 'Webes fiók neve', diff --git a/web/src/i18n/locales/id.ts b/web/src/i18n/locales/id.ts index f2e63eb7a..128ca13f3 100644 --- a/web/src/i18n/locales/id.ts +++ b/web/src/i18n/locales/id.ts @@ -548,6 +548,38 @@ const id = { updateFailed: 'Gagal memperbarui, tolong coba lagi.' } }, + users: { + title: 'Manajemen Pengguna', + addUser: 'Tambah Pengguna', + colUsername: 'Nama Pengguna', + colRole: 'Peran', + colEnabled: 'Aktif', + colActions: 'Tindakan', + rolesTitle: 'Ikhtisar Peran', + roleAdmin: 'Akses penuh + manajemen pengguna', + roleOperator: 'Penggunaan KVM: stream, keyboard, mouse, tombol daya', + roleViewer: 'Hanya melihat stream', + changePassword: 'Ubah Kata Sandi', + newPassword: 'Kata Sandi Baru', + confirmPassword: 'Konfirmasi Kata Sandi', + pwdMismatch: 'Kata sandi tidak cocok', + pwdSuccess: 'Kata sandi berhasil diubah', + pwdFailed: 'Gagal mengubah kata sandi', + password: 'Kata Sandi', + delete: 'Hapus', + deleteConfirm: 'Anda yakin ingin menghapus pengguna ini?', + createSuccess: 'Pengguna dibuat', + createFailed: 'Gagal membuat pengguna', + deleteSuccess: 'Pengguna dihapus', + deleteFailed: 'Gagal menghapus', + updateSuccess: 'Diperbarui', + updateFailed: 'Pembaruan gagal', + loadFailed: 'Gagal memuat pengguna', + usernameRequired: 'Masukkan nama pengguna', + passwordRequired: 'Masukkan kata sandi', + okBtn: 'OK', + cancelBtn: 'Batal' + }, account: { title: 'Akun', webAccount: 'Nama akun web', diff --git a/web/src/i18n/locales/it.ts b/web/src/i18n/locales/it.ts index 8a65b9f4e..365aeccfa 100644 --- a/web/src/i18n/locales/it.ts +++ b/web/src/i18n/locales/it.ts @@ -554,6 +554,38 @@ const it = { updateFailed: 'Aggiornamento fallito. Riprova.' } }, + users: { + title: 'Gestione utenti', + addUser: 'Aggiungi utente', + colUsername: 'Nome utente', + colRole: 'Ruolo', + colEnabled: 'Attivo', + colActions: 'Azioni', + rolesTitle: 'Panoramica dei ruoli', + roleAdmin: 'Accesso completo + gestione utenti', + roleOperator: 'Uso KVM: stream, tastiera, mouse, pulsanti di accensione', + roleViewer: 'Solo visualizzazione stream', + changePassword: 'Cambia password', + newPassword: 'Nuova password', + confirmPassword: 'Conferma password', + pwdMismatch: 'Le password non corrispondono', + pwdSuccess: 'Password modificata con successo', + pwdFailed: 'Impossibile cambiare la password', + password: 'Password', + delete: 'Elimina', + deleteConfirm: 'Sei sicuro di voler eliminare questo utente?', + createSuccess: 'Utente creato', + createFailed: 'Creazione fallita', + deleteSuccess: 'Utente eliminato', + deleteFailed: 'Eliminazione fallita', + updateSuccess: 'Aggiornato', + updateFailed: 'Aggiornamento fallito', + loadFailed: 'Caricamento utenti fallito', + usernameRequired: 'Inserisci il nome utente', + passwordRequired: 'Inserisci la password', + okBtn: 'OK', + cancelBtn: 'Annulla' + }, account: { title: 'Account', webAccount: 'Nome account web', diff --git a/web/src/i18n/locales/ja.ts b/web/src/i18n/locales/ja.ts index f3a0594e4..c903d5e6b 100644 --- a/web/src/i18n/locales/ja.ts +++ b/web/src/i18n/locales/ja.ts @@ -553,6 +553,38 @@ const ja = { updateFailed: 'アップデートに失敗しました。もう一度お試しください。' } }, + users: { + title: 'ユーザー管理', + addUser: 'ユーザーを追加', + colUsername: 'ユーザー名', + colRole: 'ロール', + colEnabled: '有効', + colActions: '操作', + rolesTitle: 'ロール概要', + roleAdmin: 'フルアクセス + ユーザー管理', + roleOperator: 'KVM使用: ストリーム、キーボード、マウス、電源ボタン', + roleViewer: 'ストリーム閲覧のみ', + changePassword: 'パスワード変更', + newPassword: '新しいパスワード', + confirmPassword: 'パスワード確認', + pwdMismatch: 'パスワードが一致しません', + pwdSuccess: 'パスワードが変更されました', + pwdFailed: 'パスワードの変更に失敗しました', + password: 'パスワード', + delete: '削除', + deleteConfirm: 'このユーザーを削除しますか?', + createSuccess: 'ユーザーが作成されました', + createFailed: '作成に失敗しました', + deleteSuccess: 'ユーザーが削除されました', + deleteFailed: '削除に失敗しました', + updateSuccess: '更新しました', + updateFailed: '更新に失敗しました', + loadFailed: 'ユーザーの読み込みに失敗しました', + usernameRequired: 'ユーザー名を入力してください', + passwordRequired: 'パスワードを入力してください', + okBtn: 'OK', + cancelBtn: 'キャンセル' + }, account: { title: 'アカウント', webAccount: 'ウェブアカウント名', diff --git a/web/src/i18n/locales/ko.ts b/web/src/i18n/locales/ko.ts index cf3685125..1412cb3d3 100644 --- a/web/src/i18n/locales/ko.ts +++ b/web/src/i18n/locales/ko.ts @@ -542,6 +542,38 @@ const ko = { updateFailed: '업데이트에 실패했습니다. 재시도하세요.' } }, + users: { + title: '사용자 관리', + addUser: '사용자 추가', + colUsername: '사용자 이름', + colRole: '역할', + colEnabled: '활성', + colActions: '작업', + rolesTitle: '역할 개요', + roleAdmin: '전체 액세스 + 사용자 관리', + roleOperator: 'KVM 사용: 스트림, 키보드, 마우스, 전원 버튼', + roleViewer: '스트림 보기 전용', + changePassword: '비밀번호 변경', + newPassword: '새 비밀번호', + confirmPassword: '비밀번호 확인', + pwdMismatch: '비밀번호가 일치하지 않습니다', + pwdSuccess: '비밀번호가 변경되었습니다', + pwdFailed: '비밀번호 변경에 실패했습니다', + password: '비밀번호', + delete: '삭제', + deleteConfirm: '이 사용자를 삭제하시겠습니까?', + createSuccess: '사용자가 생성되었습니다', + createFailed: '생성에 실패했습니다', + deleteSuccess: '사용자가 삭제되었습니다', + deleteFailed: '삭제에 실패했습니다', + updateSuccess: '업데이트됨', + updateFailed: '업데이트 실패', + loadFailed: '사용자 로드 실패', + usernameRequired: '사용자 이름을 입력하세요', + passwordRequired: '비밀번호를 입력하세요', + okBtn: '확인', + cancelBtn: '취소' + }, account: { title: '계정', webAccount: '웹 계정', diff --git a/web/src/i18n/locales/nb.ts b/web/src/i18n/locales/nb.ts index 9a2231856..ee808ad41 100644 --- a/web/src/i18n/locales/nb.ts +++ b/web/src/i18n/locales/nb.ts @@ -547,6 +547,38 @@ const nb = { updateFailed: 'En feil oppstod under oppdatering. Vennligst forsøk igjen.' } }, + users: { + title: 'Brukerhåndtering', + addUser: 'Legg til bruker', + colUsername: 'Brukernavn', + colRole: 'Rolle', + colEnabled: 'Aktiv', + colActions: 'Handlinger', + rolesTitle: 'Rolleoversikt', + roleAdmin: 'Full tilgang + brukerhåndtering', + roleOperator: 'KVM-bruk: stream, tastatur, mus, av/på-knapper', + roleViewer: 'Kun stream-visning', + changePassword: 'Endre passord', + newPassword: 'Nytt passord', + confirmPassword: 'Bekreft passord', + pwdMismatch: 'Passordene stemmer ikke', + pwdSuccess: 'Passord endret', + pwdFailed: 'Kunne ikke endre passord', + password: 'Passord', + delete: 'Slett', + deleteConfirm: 'Er du sikker på at du vil slette denne brukeren?', + createSuccess: 'Bruker opprettet', + createFailed: 'Kunne ikke opprette bruker', + deleteSuccess: 'Bruker slettet', + deleteFailed: 'Sletting mislyktes', + updateSuccess: 'Oppdatert', + updateFailed: 'Oppdatering mislyktes', + loadFailed: 'Kunne ikke laste brukere', + usernameRequired: 'Skriv inn brukernavn', + passwordRequired: 'Skriv inn passord', + okBtn: 'OK', + cancelBtn: 'Avbryt' + }, account: { title: 'Konto', webAccount: 'Navn på webkonto', diff --git a/web/src/i18n/locales/nl.ts b/web/src/i18n/locales/nl.ts index 5652acc7e..79d550745 100644 --- a/web/src/i18n/locales/nl.ts +++ b/web/src/i18n/locales/nl.ts @@ -553,6 +553,38 @@ const nl = { updateFailed: 'Update mislukt. Probeer het opnieuw.' } }, + users: { + title: 'Gebruikersbeheer', + addUser: 'Gebruiker toevoegen', + colUsername: 'Gebruikersnaam', + colRole: 'Rol', + colEnabled: 'Actief', + colActions: 'Acties', + rolesTitle: 'Rollenoverzicht', + roleAdmin: 'Volledige toegang + gebruikersbeheer', + roleOperator: 'KVM-gebruik: stream, toetsenbord, muis, aan/uit-knoppen', + roleViewer: 'Alleen stream bekijken', + changePassword: 'Wachtwoord wijzigen', + newPassword: 'Nieuw wachtwoord', + confirmPassword: 'Wachtwoord bevestigen', + pwdMismatch: 'Wachtwoorden komen niet overeen', + pwdSuccess: 'Wachtwoord gewijzigd', + pwdFailed: 'Wachtwoord wijzigen mislukt', + password: 'Wachtwoord', + delete: 'Verwijderen', + deleteConfirm: 'Weet u zeker dat u deze gebruiker wilt verwijderen?', + createSuccess: 'Gebruiker aangemaakt', + createFailed: 'Aanmaken mislukt', + deleteSuccess: 'Gebruiker verwijderd', + deleteFailed: 'Verwijderen mislukt', + updateSuccess: 'Bijgewerkt', + updateFailed: 'Bijwerken mislukt', + loadFailed: 'Gebruikers laden mislukt', + usernameRequired: 'Gebruikersnaam invoeren', + passwordRequired: 'Wachtwoord invoeren', + okBtn: 'OK', + cancelBtn: 'Annuleren' + }, account: { title: 'Account', webAccount: 'Web Account Naam', diff --git a/web/src/i18n/locales/pl.ts b/web/src/i18n/locales/pl.ts index a13a10d9d..d40f8c339 100644 --- a/web/src/i18n/locales/pl.ts +++ b/web/src/i18n/locales/pl.ts @@ -551,6 +551,38 @@ const pl = { updateFailed: 'Aktualizacja nie powiodła się. Spróbuj ponownie.' } }, + users: { + title: 'Zarządzanie użytkownikami', + addUser: 'Dodaj użytkownika', + colUsername: 'Nazwa użytkownika', + colRole: 'Rola', + colEnabled: 'Aktywny', + colActions: 'Akcje', + rolesTitle: 'Przegląd ról', + roleAdmin: 'Pełny dostęp + zarządzanie użytkownikami', + roleOperator: 'Korzystanie z KVM: stream, klawiatura, mysz, przyciski zasilania', + roleViewer: 'Tylko podgląd streamu', + changePassword: 'Zmień hasło', + newPassword: 'Nowe hasło', + confirmPassword: 'Potwierdź hasło', + pwdMismatch: 'Hasła nie pasują do siebie', + pwdSuccess: 'Hasło zostało zmienione', + pwdFailed: 'Nie udało się zmienić hasła', + password: 'Hasło', + delete: 'Usuń', + deleteConfirm: 'Czy na pewno chcesz usunąć tego użytkownika?', + createSuccess: 'Użytkownik utworzony', + createFailed: 'Utworzenie nie powiodło się', + deleteSuccess: 'Użytkownik usunięty', + deleteFailed: 'Usunięcie nie powiodło się', + updateSuccess: 'Zaktualizowano', + updateFailed: 'Aktualizacja nie powiodła się', + loadFailed: 'Nie udało się wczytać użytkowników', + usernameRequired: 'Wprowadź nazwę użytkownika', + passwordRequired: 'Wprowadź hasło', + okBtn: 'OK', + cancelBtn: 'Anuluj' + }, account: { title: 'Konto', webAccount: 'Nazwa konta web', diff --git a/web/src/i18n/locales/pt_br.ts b/web/src/i18n/locales/pt_br.ts index 161c2930e..e696e95cc 100644 --- a/web/src/i18n/locales/pt_br.ts +++ b/web/src/i18n/locales/pt_br.ts @@ -549,6 +549,38 @@ const pt_br = { updateFailed: 'Falha na atualização. Por favor, tente novamente.' } }, + users: { + title: 'Gerenciamento de usuários', + addUser: 'Adicionar usuário', + colUsername: 'Nome de usuário', + colRole: 'Função', + colEnabled: 'Ativo', + colActions: 'Ações', + rolesTitle: 'Visão geral das funções', + roleAdmin: 'Acesso total + gerenciamento de usuários', + roleOperator: 'Uso do KVM: stream, teclado, mouse, botões de energia', + roleViewer: 'Apenas visualização do stream', + changePassword: 'Alterar senha', + newPassword: 'Nova senha', + confirmPassword: 'Confirmar senha', + pwdMismatch: 'As senhas não coincidem', + pwdSuccess: 'Senha alterada com sucesso', + pwdFailed: 'Falha ao alterar a senha', + password: 'Senha', + delete: 'Excluir', + deleteConfirm: 'Tem certeza que deseja excluir este usuário?', + createSuccess: 'Usuário criado', + createFailed: 'Falha ao criar usuário', + deleteSuccess: 'Usuário excluído', + deleteFailed: 'Falha ao excluir', + updateSuccess: 'Atualizado', + updateFailed: 'Falha na atualização', + loadFailed: 'Falha ao carregar usuários', + usernameRequired: 'Digite o nome de usuário', + passwordRequired: 'Digite a senha', + okBtn: 'OK', + cancelBtn: 'Cancelar' + }, account: { title: 'Conta', webAccount: 'Nome da Conta Web', diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts index f1928fc6a..18a786eb1 100644 --- a/web/src/i18n/locales/ru.ts +++ b/web/src/i18n/locales/ru.ts @@ -551,6 +551,38 @@ const ru = { updateFailed: 'Обновление не удалось. Пожалуйста, попробуйте еще раз.' } }, + users: { + title: 'Управление пользователями', + addUser: 'Добавить пользователя', + colUsername: 'Имя пользователя', + colRole: 'Роль', + colEnabled: 'Активен', + colActions: 'Действия', + rolesTitle: 'Обзор ролей', + roleAdmin: 'Полный доступ + управление пользователями', + roleOperator: 'Использование KVM: поток, клавиатура, мышь, кнопки питания', + roleViewer: 'Только просмотр потока', + changePassword: 'Сменить пароль', + newPassword: 'Новый пароль', + confirmPassword: 'Подтвердите пароль', + pwdMismatch: 'Пароли не совпадают', + pwdSuccess: 'Пароль успешно изменен', + pwdFailed: 'Не удалось изменить пароль', + password: 'Пароль', + delete: 'Удалить', + deleteConfirm: 'Вы уверены, что хотите удалить этого пользователя?', + createSuccess: 'Пользователь создан', + createFailed: 'Не удалось создать', + deleteSuccess: 'Пользователь удален', + deleteFailed: 'Не удалось удалить', + updateSuccess: 'Обновлено', + updateFailed: 'Ошибка обновления', + loadFailed: 'Не удалось загрузить пользователей', + usernameRequired: 'Введите имя пользователя', + passwordRequired: 'Введите пароль', + okBtn: 'OK', + cancelBtn: 'Отмена' + }, account: { title: 'Аккаунт', webAccount: 'Имя веб-аккаунта', diff --git a/web/src/i18n/locales/se.ts b/web/src/i18n/locales/se.ts index 53a778de7..327f384fc 100644 --- a/web/src/i18n/locales/se.ts +++ b/web/src/i18n/locales/se.ts @@ -545,6 +545,38 @@ const se = { updateFailed: 'Uppdatering misslyckades. Försök igen.' } }, + users: { + title: 'Användarhantering', + addUser: 'Lägg till användare', + colUsername: 'Användarnamn', + colRole: 'Roll', + colEnabled: 'Aktiv', + colActions: 'Åtgärder', + rolesTitle: 'Rollöversikt', + roleAdmin: 'Full åtkomst + användarhantering', + roleOperator: 'KVM-användning: ström, tangentbord, mus, strömbrytare', + roleViewer: 'Endast strömvisning', + changePassword: 'Ändra lösenord', + newPassword: 'Nytt lösenord', + confirmPassword: 'Bekräfta lösenord', + pwdMismatch: 'Lösenorden matchar inte', + pwdSuccess: 'Lösenord ändrat', + pwdFailed: 'Det gick inte att ändra lösenordet', + password: 'Lösenord', + delete: 'Ta bort', + deleteConfirm: 'Är du säker på att du vill ta bort denna användare?', + createSuccess: 'Användare skapad', + createFailed: 'Kunde inte skapa användare', + deleteSuccess: 'Användare borttagen', + deleteFailed: 'Borttagning misslyckades', + updateSuccess: 'Uppdaterad', + updateFailed: 'Uppdatering misslyckades', + loadFailed: 'Det gick inte att läsa in användare', + usernameRequired: 'Ange användarnamn', + passwordRequired: 'Ange lösenord', + okBtn: 'OK', + cancelBtn: 'Avbryt' + }, account: { title: 'Konto', webAccount: 'Webbkonto-namn', diff --git a/web/src/i18n/locales/th.ts b/web/src/i18n/locales/th.ts index dd7aae89d..a69f0cd1c 100644 --- a/web/src/i18n/locales/th.ts +++ b/web/src/i18n/locales/th.ts @@ -540,6 +540,38 @@ const th = { updateFailed: 'การอัปเดตล้มเหลว กรุณาลองใหม่' } }, + users: { + title: 'การจัดการผู้ใช้', + addUser: 'เพิ่มผู้ใช้', + colUsername: 'ชื่อผู้ใช้', + colRole: 'บทบาท', + colEnabled: 'เปิดใช้งาน', + colActions: 'การดำเนินการ', + rolesTitle: 'ภาพรวมบทบาท', + roleAdmin: 'เข้าถึงเต็มรูปแบบ + การจัดการผู้ใช้', + roleOperator: 'การใช้ KVM: สตรีม คีย์บอร์ด เมาส์ ปุ่มเปิด/ปิด', + roleViewer: 'ดูสตรีมเท่านั้น', + changePassword: 'เปลี่ยนรหัสผ่าน', + newPassword: 'รหัสผ่านใหม่', + confirmPassword: 'ยืนยันรหัสผ่าน', + pwdMismatch: 'รหัสผ่านไม่ตรงกัน', + pwdSuccess: 'เปลี่ยนรหัสผ่านสำเร็จ', + pwdFailed: 'ไม่สามารถเปลี่ยนรหัสผ่านได้', + password: 'รหัสผ่าน', + delete: 'ลบ', + deleteConfirm: 'คุณแน่ใจหรือไม่ว่าต้องการลบผู้ใช้นี้?', + createSuccess: 'สร้างผู้ใช้แล้ว', + createFailed: 'การสร้างล้มเหลว', + deleteSuccess: 'ลบผู้ใช้แล้ว', + deleteFailed: 'การลบล้มเหลว', + updateSuccess: 'อัปเดตแล้ว', + updateFailed: 'การอัปเดตล้มเหลว', + loadFailed: 'โหลดผู้ใช้ไม่สำเร็จ', + usernameRequired: 'กรุณากรอกชื่อผู้ใช้', + passwordRequired: 'กรุณากรอกรหัสผ่าน', + okBtn: 'ตกลง', + cancelBtn: 'ยกเลิก' + }, account: { title: 'บัญชี', webAccount: 'ชื่อผู้ใช้', diff --git a/web/src/i18n/locales/tr.ts b/web/src/i18n/locales/tr.ts index cee7d3eff..f70537e7d 100644 --- a/web/src/i18n/locales/tr.ts +++ b/web/src/i18n/locales/tr.ts @@ -549,6 +549,38 @@ const tr = { updateFailed: 'Güncelleme başarısız oldu. Lütfen tekrar deneyin.' } }, + users: { + title: 'Kullanıcı Yönetimi', + addUser: 'Kullanıcı Ekle', + colUsername: 'Kullanıcı Adı', + colRole: 'Rol', + colEnabled: 'Aktif', + colActions: 'Eylemler', + rolesTitle: 'Roller Genel Bakış', + roleAdmin: 'Tam erişim + kullanıcı yönetimi', + roleOperator: 'KVM kullanımı: yayın, klavye, fare, güç düğmeleri', + roleViewer: 'Yalnızca yayın görüntüleme', + changePassword: 'Şifre Değiştir', + newPassword: 'Yeni Şifre', + confirmPassword: 'Şifreyi Onayla', + pwdMismatch: 'Şifreler eşleşmiyor', + pwdSuccess: 'Şifre başarıyla değiştirildi', + pwdFailed: 'Şifre değiştirilemedi', + password: 'Şifre', + delete: 'Sil', + deleteConfirm: 'Bu kullanıcıyı silmek istediğinizden emin misiniz?', + createSuccess: 'Kullanıcı oluşturuldu', + createFailed: 'Oluşturma başarısız', + deleteSuccess: 'Kullanıcı silindi', + deleteFailed: 'Silme başarısız', + updateSuccess: 'Güncellendi', + updateFailed: 'Güncelleme başarısız', + loadFailed: 'Kullanıcılar yüklenemedi', + usernameRequired: 'Kullanıcı adı girin', + passwordRequired: 'Şifre girin', + okBtn: 'Tamam', + cancelBtn: 'İptal' + }, account: { title: 'Hesap', webAccount: 'Web Hesap Adı', diff --git a/web/src/i18n/locales/uk.ts b/web/src/i18n/locales/uk.ts index f09dd8038..d0d0abadf 100644 --- a/web/src/i18n/locales/uk.ts +++ b/web/src/i18n/locales/uk.ts @@ -548,6 +548,38 @@ const uk = { updateFailed: 'Оновлення не вдалося. Будь ласка, спробуйте ще раз.' } }, + users: { + title: 'Керування користувачами', + addUser: 'Додати користувача', + colUsername: 'Ім\'я користувача', + colRole: 'Роль', + colEnabled: 'Активний', + colActions: 'Дії', + rolesTitle: 'Огляд ролей', + roleAdmin: 'Повний доступ + керування користувачами', + roleOperator: 'Використання KVM: потік, клавіатура, миша, кнопки живлення', + roleViewer: 'Лише перегляд потоку', + changePassword: 'Змінити пароль', + newPassword: 'Новий пароль', + confirmPassword: 'Підтвердити пароль', + pwdMismatch: 'Паролі не збігаються', + pwdSuccess: 'Пароль успішно змінено', + pwdFailed: 'Не вдалося змінити пароль', + password: 'Пароль', + delete: 'Видалити', + deleteConfirm: 'Ви впевнені, що хочете видалити цього користувача?', + createSuccess: 'Користувача створено', + createFailed: 'Помилка створення', + deleteSuccess: 'Користувача видалено', + deleteFailed: 'Помилка видалення', + updateSuccess: 'Оновлено', + updateFailed: 'Помилка оновлення', + loadFailed: 'Не вдалося завантажити користувачів', + usernameRequired: 'Введіть ім\'я користувача', + passwordRequired: 'Введіть пароль', + okBtn: 'OK', + cancelBtn: 'Скасувати' + }, account: { title: 'Обліковий запис', webAccount: "Ім'я облыкового запису у веб-інтерфейсі", diff --git a/web/src/i18n/locales/vi.ts b/web/src/i18n/locales/vi.ts index f0ec1e762..0f8359500 100644 --- a/web/src/i18n/locales/vi.ts +++ b/web/src/i18n/locales/vi.ts @@ -547,6 +547,38 @@ const vi = { updateFailed: 'Cập nhật thất bại. Vui lòng thử lại.' } }, + users: { + title: 'Quản lý người dùng', + addUser: 'Thêm người dùng', + colUsername: 'Tên người dùng', + colRole: 'Vai trò', + colEnabled: 'Hoạt động', + colActions: 'Hành động', + rolesTitle: 'Tổng quan vai trò', + roleAdmin: 'Truy cập đầy đủ + quản lý người dùng', + roleOperator: 'Sử dụng KVM: luồng, bàn phím, chuột, nút nguồn', + roleViewer: 'Chỉ xem luồng', + changePassword: 'Đổi mật khẩu', + newPassword: 'Mật khẩu mới', + confirmPassword: 'Xác nhận mật khẩu', + pwdMismatch: 'Mật khẩu không khớp', + pwdSuccess: 'Đổi mật khẩu thành công', + pwdFailed: 'Không thể đổi mật khẩu', + password: 'Mật khẩu', + delete: 'Xóa', + deleteConfirm: 'Bạn có chắc muốn xóa người dùng này?', + createSuccess: 'Đã tạo người dùng', + createFailed: 'Tạo thất bại', + deleteSuccess: 'Đã xóa người dùng', + deleteFailed: 'Xóa thất bại', + updateSuccess: 'Đã cập nhật', + updateFailed: 'Cập nhật thất bại', + loadFailed: 'Tải người dùng thất bại', + usernameRequired: 'Nhập tên người dùng', + passwordRequired: 'Nhập mật khẩu', + okBtn: 'OK', + cancelBtn: 'Hủy' + }, account: { title: 'Tài khoản', webAccount: 'Tên tài khoản web', diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index b7a997df4..4d3c820d2 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -532,6 +532,38 @@ const zh = { updateFailed: '更新失败,请重试' } }, + users: { + title: '用户管理', + addUser: '添加用户', + colUsername: '用户名', + colRole: '角色', + colEnabled: '启用', + colActions: '操作', + rolesTitle: '角色概览', + roleAdmin: '完全访问 + 用户管理', + roleOperator: 'KVM 使用:流、键盘、鼠标、电源按钮', + roleViewer: '仅查看流', + changePassword: '修改密码', + newPassword: '新密码', + confirmPassword: '确认密码', + pwdMismatch: '密码不匹配', + pwdSuccess: '密码修改成功', + pwdFailed: '修改密码失败', + password: '密码', + delete: '删除', + deleteConfirm: '确定要删除此用户吗?', + createSuccess: '用户已创建', + createFailed: '创建失败', + deleteSuccess: '用户已删除', + deleteFailed: '删除失败', + updateSuccess: '已更新', + updateFailed: '更新失败', + loadFailed: '加载用户失败', + usernameRequired: '请输入用户名', + passwordRequired: '请输入密码', + okBtn: '确定', + cancelBtn: '取消' + }, account: { title: '帐号', webAccount: '网页帐号', diff --git a/web/src/i18n/locales/zh_tw.ts b/web/src/i18n/locales/zh_tw.ts index 1d12530ab..a4e52a1d7 100644 --- a/web/src/i18n/locales/zh_tw.ts +++ b/web/src/i18n/locales/zh_tw.ts @@ -532,6 +532,38 @@ const zh_tw = { updateFailed: '更新失敗,請重試' } }, + users: { + title: '使用者管理', + addUser: '新增使用者', + colUsername: '使用者名稱', + colRole: '角色', + colEnabled: '啟用', + colActions: '操作', + rolesTitle: '角色概覽', + roleAdmin: '完全存取 + 使用者管理', + roleOperator: 'KVM 使用:串流、鍵盤、滑鼠、電源按鈕', + roleViewer: '僅檢視串流', + changePassword: '變更密碼', + newPassword: '新密碼', + confirmPassword: '確認密碼', + pwdMismatch: '密碼不相符', + pwdSuccess: '密碼變更成功', + pwdFailed: '變更密碼失敗', + password: '密碼', + delete: '刪除', + deleteConfirm: '確定要刪除此使用者嗎?', + createSuccess: '使用者已建立', + createFailed: '建立失敗', + deleteSuccess: '使用者已刪除', + deleteFailed: '刪除失敗', + updateSuccess: '已更新', + updateFailed: '更新失敗', + loadFailed: '載入使用者失敗', + usernameRequired: '請輸入使用者名稱', + passwordRequired: '請輸入密碼', + okBtn: '確定', + cancelBtn: '取消' + }, account: { title: '帳號', webAccount: '網頁帳號', diff --git a/web/src/lib/http.ts b/web/src/lib/http.ts index 38802d9e7..0002eface 100644 --- a/web/src/lib/http.ts +++ b/web/src/lib/http.ts @@ -67,6 +67,15 @@ class Http { }); } + public put(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return this.instance.request({ + method: 'put', + url, + data, + ...config + }); + } + public delete(url: string, data?: any): Promise { return this.instance.request({ method: 'delete', diff --git a/web/src/pages/auth/password/index.tsx b/web/src/pages/auth/password/index.tsx index a75457a55..117db4af3 100644 --- a/web/src/pages/auth/password/index.tsx +++ b/web/src/pages/auth/password/index.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { LockOutlined, UserOutlined } from '@ant-design/icons'; +import { LockOutlined } from '@ant-design/icons'; import { Button, Card, Form, Input } from 'antd'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -25,20 +25,16 @@ export const Password = () => { setMsg(t('auth.differentPassword')); return; } - if (!validateString(values.username)) { - setMsg(t('auth.illegalUsername')); - return; - } if (!validateString(values.password)) { setMsg('auth.illegalPassword'); return; } - const username = values.username; + // initial password change is always for "admin" const password = encrypt(values.password); api - .changePassword(username, password) + .changePassword('admin', password) .then((rsp: any) => { if (rsp.code !== 0) { setMsg(t('auth.error')); @@ -74,13 +70,6 @@ export const Password = () => { initialValues={{ remember: true }} onFinish={changePassword} > - - } placeholder={t('auth.placeholderUsername')} /> - - { const nodeRef = useRef(null); - const menuDisabledItems = useAtomValue(menuDisabledItemsAtom); + const { isOperator, isAdmin } = useRole(); const isKeyboardLedStatusVisible = useAtomValue(keyboardLedStatusVisibleAtom); const { @@ -68,12 +69,10 @@ export const Menu = () => { onMouseLeave={() => handleHovered(false)} onBlur={() => handleHovered(false)} > - {/* Trigger area for auto-show when hidden */} {isMenuExpanded && (
)} - {/* Menubar */}
{ + {/* Screen immer sichtbar (nur Anzeige) */} - - + + {/* Tastatur & Maus: nur operator+ */} + {isOperator && } + {isOperator && } + + {/* Image/Download: alle */} {isEnabled('image') && } {isEnabled('download') && } - {isEnabled('terminal') && } - {isEnabled('script') &&