From 49200d47473216e58904d01043ca802d5117215b Mon Sep 17 00:00:00 2001 From: Jingru Shi <58133115+TTopoo@users.noreply.github.com> Date: Tue, 21 Jul 2026 01:31:46 +0800 Subject: [PATCH] fix(config): support Redis ACL username --- README.md | 1 + backend/internal/config/config.go | 2 ++ backend/internal/config/config_test.go | 9 ++++++++ backend/internal/repository/redis.go | 1 + backend/internal/repository/redis_test.go | 2 ++ backend/internal/setup/handler.go | 12 ++++++++++ backend/internal/setup/setup.go | 3 +++ backend/internal/setup/setup_test.go | 23 ++++++++++++++++++++ deploy/.env.example | 2 ++ deploy/config.example.yaml | 3 +++ deploy/docker-compose.dev.yml | 1 + deploy/docker-compose.local.yml | 1 + deploy/docker-compose.standalone.yml | 1 + deploy/docker-compose.yml | 1 + frontend/src/api/setup.ts | 1 + frontend/src/i18n/locales/en/landing.ts | 2 ++ frontend/src/i18n/locales/zh/landing.ts | 2 ++ frontend/src/views/setup/SetupWizardView.vue | 10 +++++++++ 18 files changed, 77 insertions(+) diff --git a/README.md b/README.md index e3c772edb0..f7370cbc62 100644 --- a/README.md +++ b/README.md @@ -544,6 +544,7 @@ database: redis: host: "localhost" port: 6379 + username: "" password: "" jwt: diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index c9f2c02c4a..e9ddb39b01 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -1403,6 +1403,7 @@ func (d *DatabaseConfig) DSNWithTimezone(tz string) string { type RedisConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` + Username string `mapstructure:"username"` Password string `mapstructure:"password"` DB int `mapstructure:"db"` // 连接池与超时配置(性能优化:可配置化连接池参数) @@ -1981,6 +1982,7 @@ func setDefaults() { // Redis viper.SetDefault("redis.host", "localhost") viper.SetDefault("redis.port", 6379) + viper.SetDefault("redis.username", "") viper.SetDefault("redis.password", "") viper.SetDefault("redis.db", 0) viper.SetDefault("redis.dial_timeout_seconds", 5) diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index dece5fd5ae..0c55d2776d 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -37,6 +37,15 @@ func TestLoadServerTimingConfig(t *testing.T) { }) } +func TestLoadRedisUsernameFromEnvironment(t *testing.T) { + resetViperWithJWTSecret(t) + t.Setenv("REDIS_USERNAME", "app-user") + + cfg, err := Load() + require.NoError(t, err) + require.Equal(t, "app-user", cfg.Redis.Username) +} + func TestLoadHTTPIngressSafetyDefaults(t *testing.T) { resetViperWithJWTSecret(t) cfg, err := Load() diff --git a/backend/internal/repository/redis.go b/backend/internal/repository/redis.go index 0ead4644c1..270a150cb2 100644 --- a/backend/internal/repository/redis.go +++ b/backend/internal/repository/redis.go @@ -33,6 +33,7 @@ func InitRedis(cfg *config.Config) *redis.Client { func buildRedisOptions(cfg *config.Config) *redis.Options { opts := &redis.Options{ Addr: cfg.Redis.Address(), + Username: cfg.Redis.Username, Password: cfg.Redis.Password, DB: cfg.Redis.DB, DialTimeout: time.Duration(cfg.Redis.DialTimeoutSeconds) * time.Second, // 建连超时 diff --git a/backend/internal/repository/redis_test.go b/backend/internal/repository/redis_test.go index 7cb31002b3..281f833262 100644 --- a/backend/internal/repository/redis_test.go +++ b/backend/internal/repository/redis_test.go @@ -13,6 +13,7 @@ func TestBuildRedisOptions(t *testing.T) { Redis: config.RedisConfig{ Host: "localhost", Port: 6379, + Username: "app-user", Password: "secret", DB: 2, DialTimeoutSeconds: 5, @@ -25,6 +26,7 @@ func TestBuildRedisOptions(t *testing.T) { opts := buildRedisOptions(cfg) require.Equal(t, "localhost:6379", opts.Addr) + require.Equal(t, "app-user", opts.Username) require.Equal(t, "secret", opts.Password) require.Equal(t, 2, opts.DB) require.Equal(t, 5*time.Second, opts.DialTimeout) diff --git a/backend/internal/setup/handler.go b/backend/internal/setup/handler.go index c2944cedfb..9207b4335c 100644 --- a/backend/internal/setup/handler.go +++ b/backend/internal/setup/handler.go @@ -178,6 +178,7 @@ func testDatabase(c *gin.Context) { type TestRedisRequest struct { Host string `json:"host" binding:"required"` Port int `json:"port" binding:"required"` + Username string `json:"username"` Password string `json:"password"` DB int `json:"db"` EnableTLS bool `json:"enable_tls"` @@ -204,10 +205,16 @@ func testRedis(c *gin.Context) { response.Error(c, http.StatusBadRequest, "Invalid Redis database number (0-15)") return } + req.Username = strings.TrimSpace(req.Username) + if len(req.Username) > 128 { + response.Error(c, http.StatusBadRequest, "Invalid Redis username") + return + } cfg := &RedisConfig{ Host: req.Host, Port: req.Port, + Username: req.Username, Password: req.Password, DB: req.DB, EnableTLS: req.EnableTLS, @@ -252,6 +259,7 @@ func install(c *gin.Context) { req.Database.User = strings.TrimSpace(req.Database.User) req.Database.DBName = strings.TrimSpace(req.Database.DBName) req.Redis.Host = strings.TrimSpace(req.Redis.Host) + req.Redis.Username = strings.TrimSpace(req.Redis.Username) // ========== COMPREHENSIVE INPUT VALIDATION ========== // Database validation @@ -285,6 +293,10 @@ func install(c *gin.Context) { response.Error(c, http.StatusBadRequest, "Invalid Redis database number") return } + if len(req.Redis.Username) > 128 { + response.Error(c, http.StatusBadRequest, "Invalid Redis username") + return + } // Admin validation if !validateEmail(req.Admin.Email) { diff --git a/backend/internal/setup/setup.go b/backend/internal/setup/setup.go index a2c4e2847a..b26acc87ff 100644 --- a/backend/internal/setup/setup.go +++ b/backend/internal/setup/setup.go @@ -95,6 +95,7 @@ type DatabaseConfig struct { type RedisConfig struct { Host string `json:"host" yaml:"host"` Port int `json:"port" yaml:"port"` + Username string `json:"username" yaml:"username"` Password string `json:"password" yaml:"password"` DB int `json:"db" yaml:"db"` EnableTLS bool `json:"enable_tls" yaml:"enable_tls"` @@ -251,6 +252,7 @@ func TestDatabaseConnection(cfg *DatabaseConfig) error { func TestRedisConnection(cfg *RedisConfig) error { opts := &redis.Options{ Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), + Username: cfg.Username, Password: cfg.Password, DB: cfg.DB, } @@ -570,6 +572,7 @@ func AutoSetupFromEnv() error { Redis: RedisConfig{ Host: getEnvOrDefault("REDIS_HOST", "localhost"), Port: getEnvIntOrDefault("REDIS_PORT", 6379), + Username: getEnvOrDefault("REDIS_USERNAME", ""), Password: getEnvOrDefault("REDIS_PASSWORD", ""), DB: getEnvIntOrDefault("REDIS_DB", 0), EnableTLS: getEnvOrDefault("REDIS_ENABLE_TLS", "false") == "true", diff --git a/backend/internal/setup/setup_test.go b/backend/internal/setup/setup_test.go index b95c162bda..d1763d3adc 100644 --- a/backend/internal/setup/setup_test.go +++ b/backend/internal/setup/setup_test.go @@ -105,6 +105,29 @@ func TestWriteConfigFileKeepsDefaultUserConcurrency(t *testing.T) { } } +func TestWriteConfigFileIncludesRedisUsername(t *testing.T) { + t.Setenv("DATA_DIR", t.TempDir()) + + if err := writeConfigFile(&SetupConfig{ + Redis: RedisConfig{ + Host: "redis", + Port: 6379, + Username: "app-user", + }, + }); err != nil { + t.Fatalf("writeConfigFile() error = %v", err) + } + + data, err := os.ReadFile(GetConfigFilePath()) + if err != nil { + t.Fatalf("ReadFile() error = %v", err) + } + + if !strings.Contains(string(data), "username: app-user") { + t.Fatalf("config missing Redis username, got:\n%s", string(data)) + } +} + func TestBuildDatabaseConnectionDSNsUsesPostgresForBootstrap(t *testing.T) { cfg := &DatabaseConfig{ Host: "db", diff --git a/deploy/.env.example b/deploy/.env.example index 89b5ea0e94..4ddac781e8 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -176,6 +176,8 @@ DATABASE_CONN_MAX_IDLE_TIME_MINUTES=5 # ----------------------------------------------------------------------------- # Redis 监听端口(同时用于应用连接和 Redis 服务端,默认 6379) REDIS_PORT=6379 +# Redis ACL username; leave empty for the default user +REDIS_USERNAME= # Leave empty for no password (default for local development) REDIS_PASSWORD= REDIS_DB=0 diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index c0fb225ee9..6bcdefdceb 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -920,6 +920,9 @@ redis: # Redis port # Redis 端口 port: 6379 + # Redis ACL username (leave empty for default user) + # Redis ACL 用户名(使用默认用户时留空) + username: "" # Redis password (leave empty if no password is set) # Redis 密码(如果未设置密码则留空) password: "" diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml index 08e88a64c9..081e1a1830 100644 --- a/deploy/docker-compose.dev.yml +++ b/deploy/docker-compose.dev.yml @@ -36,6 +36,7 @@ services: - DATABASE_SSLMODE=disable - REDIS_HOST=redis - REDIS_PORT=6379 + - REDIS_USERNAME=${REDIS_USERNAME:-} - REDIS_PASSWORD=${REDIS_PASSWORD:-} - REDIS_DB=${REDIS_DB:-0} - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local} diff --git a/deploy/docker-compose.local.yml b/deploy/docker-compose.local.yml index d957d5d813..ea61889913 100644 --- a/deploy/docker-compose.local.yml +++ b/deploy/docker-compose.local.yml @@ -74,6 +74,7 @@ services: # ======================================================================= - REDIS_HOST=redis - REDIS_PORT=6379 + - REDIS_USERNAME=${REDIS_USERNAME:-} - REDIS_PASSWORD=${REDIS_PASSWORD:-} - REDIS_DB=${REDIS_DB:-0} - REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-1024} diff --git a/deploy/docker-compose.standalone.yml b/deploy/docker-compose.standalone.yml index c5f0be7e6f..f7e664ed43 100644 --- a/deploy/docker-compose.standalone.yml +++ b/deploy/docker-compose.standalone.yml @@ -60,6 +60,7 @@ services: # ======================================================================= - REDIS_HOST=${REDIS_HOST:?REDIS_HOST is required} - REDIS_PORT=${REDIS_PORT:-6379} + - REDIS_USERNAME=${REDIS_USERNAME:-} - REDIS_PASSWORD=${REDIS_PASSWORD:-} - REDIS_DB=${REDIS_DB:-0} - REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-1024} diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 0bab288a8e..ab942419c4 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -70,6 +70,7 @@ services: # ======================================================================= - REDIS_HOST=redis - REDIS_PORT=6379 + - REDIS_USERNAME=${REDIS_USERNAME:-} - REDIS_PASSWORD=${REDIS_PASSWORD:-} - REDIS_DB=${REDIS_DB:-0} - REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-1024} diff --git a/frontend/src/api/setup.ts b/frontend/src/api/setup.ts index 70a8a92403..b63865db3e 100644 --- a/frontend/src/api/setup.ts +++ b/frontend/src/api/setup.ts @@ -30,6 +30,7 @@ export interface DatabaseConfig { export interface RedisConfig { host: string port: number + username: string password: string db: number enable_tls: boolean diff --git a/frontend/src/i18n/locales/en/landing.ts b/frontend/src/i18n/locales/en/landing.ts index afbecf4861..c443bfeb0e 100644 --- a/frontend/src/i18n/locales/en/landing.ts +++ b/frontend/src/i18n/locales/en/landing.ts @@ -215,8 +215,10 @@ export default { description: 'Connect to your Redis server', host: 'Host', port: 'Port', + username: 'Username (optional)', password: 'Password (optional)', database: 'Database', + usernamePlaceholder: 'Leave empty for default user', passwordPlaceholder: 'Password', enableTls: 'Enable TLS', enableTlsHint: 'Use TLS when connecting to Redis (public CA certs)' diff --git a/frontend/src/i18n/locales/zh/landing.ts b/frontend/src/i18n/locales/zh/landing.ts index 85b7ef48c9..18a7b25a2d 100644 --- a/frontend/src/i18n/locales/zh/landing.ts +++ b/frontend/src/i18n/locales/zh/landing.ts @@ -215,8 +215,10 @@ export default { description: '连接到您的 Redis 服务器', host: '主机', port: '端口', + username: '用户名(可选)', password: '密码(可选)', database: '数据库', + usernamePlaceholder: '默认用户留空', passwordPlaceholder: '密码', enableTls: '启用 TLS', enableTlsHint: '连接 Redis 时使用 TLS(公共 CA 证书)' diff --git a/frontend/src/views/setup/SetupWizardView.vue b/frontend/src/views/setup/SetupWizardView.vue index 2679c4d4fc..e0267e839f 100644 --- a/frontend/src/views/setup/SetupWizardView.vue +++ b/frontend/src/views/setup/SetupWizardView.vue @@ -217,6 +217,15 @@
+
+ + +
({ redis: { host: 'localhost', port: 6379, + username: '', password: '', db: 0, enable_tls: false