diff --git a/backend/internal/handler/connection_tester.go b/backend/internal/handler/connection_tester.go index 37abe03..6720870 100644 --- a/backend/internal/handler/connection_tester.go +++ b/backend/internal/handler/connection_tester.go @@ -42,7 +42,7 @@ func testDBConnection(instance *model.Instance) (bool, string) { } return testPostgreSQL(ctx, instance.Host, instance.Port, instance.Username, password, dbname) case "redis": - ok, msg, _ := testRedis(ctx, instance.Host, instance.Port, password) + ok, msg, _ := testRedis(ctx, instance.Host, instance.Port, instance.Username, password) return ok, msg default: return false, fmt.Sprintf("Unsupported database type: %s", instance.DBType) @@ -71,7 +71,7 @@ func testDBConnectionFull(instance *model.Instance) *ConnectionResult { ok, msg := testPostgreSQL(ctx, instance.Host, instance.Port, instance.Username, password, dbname) return &ConnectionResult{Success: ok, Message: msg, Version: extractVersion(msg, "version: ")} case "redis": - ok, msg, isCluster := testRedis(ctx, instance.Host, instance.Port, password) + ok, msg, isCluster := testRedis(ctx, instance.Host, instance.Port, instance.Username, password) return &ConnectionResult{Success: ok, Message: msg, Version: extractVersion(msg, "version: "), ClusterMode: isCluster} default: return &ConnectionResult{Success: false, Message: fmt.Sprintf("Unsupported database type: %s", instance.DBType)} @@ -132,10 +132,16 @@ func testPostgreSQL(ctx context.Context, host string, port int, user, pass, dbna return true, fmt.Sprintf("PostgreSQL connection successful (version: %s)", ver) } -func testRedis(ctx context.Context, host string, port int, pass string) (bool, string, bool) { +func testRedis(ctx context.Context, host string, port int, username, pass string) (bool, string, bool) { addr := fmt.Sprintf("%s:%d", host, port) + // Redis 6.0+ ACL: empty username = traditional password-only auth + redisUsername := username + if redisUsername == "default" { + redisUsername = "" + } rdb := redis.NewClient(&redis.Options{ Addr: addr, + Username: redisUsername, Password: pass, DB: 0, DialTimeout: 10 * time.Second, @@ -199,11 +205,17 @@ func getRedisMetrics(instance *model.Instance) map[string]interface{} { "cluster_mode": isCluster, } + // Redis 6.0+ ACL support + redisUsername := instance.Username + if redisUsername == "default" { + redisUsername = "" + } var info string // For metrics collection, always use single-node connection even for clusters. // ClusterClient does topology discovery which can timeout if some nodes are unreachable. rdb := redis.NewClient(&redis.Options{ Addr: addr, + Username: redisUsername, Password: password, DB: 0, DialTimeout: 5 * time.Second, diff --git a/backend/internal/handler/instance.go b/backend/internal/handler/instance.go index af65dab..2f261a1 100644 --- a/backend/internal/handler/instance.go +++ b/backend/internal/handler/instance.go @@ -108,15 +108,16 @@ func UpdateInstance(c *gin.Context) { return } - // if password is masked, don't update it - if pw, ok := input["password"].(string); ok && pw == "******" { - delete(input, "password") - } - // Encrypt password if provided and not masked - if pw, ok := input["password"].(string); ok && pw != "" { - encPw, err := encryptPassword(pw) - if err == nil { - input["password"] = "enc:" + encPw + // if password is empty or masked placeholder, don't update it + if pw, ok := input["password"].(string); ok { + if pw == "" || pw == "******" { + delete(input, "password") + } else { + // Encrypt new password + encPw, err := encryptPassword(pw) + if err == nil { + input["password"] = "enc:" + encPw + } } } diff --git a/backend/internal/handler/redis.go b/backend/internal/handler/redis.go index 3678e4f..dbcdf94 100644 --- a/backend/internal/handler/redis.go +++ b/backend/internal/handler/redis.go @@ -156,12 +156,19 @@ func getRedisClient(instance *model.Instance, db int) (*redisClientWrapper, erro } addr := fmt.Sprintf("%s:%d", instance.Host, instance.Port) + // Redis 6.0+ ACL: username is optional (empty = use "default" traditional auth) + redisUsername := instance.Username + if redisUsername == "default" || redisUsername == "" { + redisUsername = "" // go-redis treats empty Username as no ACL auth + } + // Always use single-node connection for query execution. // ClusterClient does topology discovery (CLUSTER SLOTS) which fails when // internal cluster IPs are unreachable from the client (e.g., Docker networks). // Single-node mode works for all read-only commands on cluster nodes. client := redis.NewClient(&redis.Options{ Addr: addr, + Username: redisUsername, Password: password, DB: db, }) diff --git a/frontend/src/components/EditInstanceDialog.tsx b/frontend/src/components/EditInstanceDialog.tsx index 31b9442..867f4f8 100644 --- a/frontend/src/components/EditInstanceDialog.tsx +++ b/frontend/src/components/EditInstanceDialog.tsx @@ -172,12 +172,15 @@ export default function EditInstanceDialog({ instance, open, onClose, onSaved, m
+