// Package configprovider implements ports.ConfigProvider with Redis-backed // dynamic auth/session configuration. package configprovider import ( "context" "crypto/tls" "errors" "fmt" "strconv" "strings" "time" "galaxy/authsession/internal/ports" "github.com/redis/go-redis/v9" ) // Config configures one Redis-backed config provider instance. type Config struct { // Addr is the Redis network address in host:port form. Addr string // Username is the optional Redis ACL username. Username string // Password is the optional Redis ACL password. Password string // DB is the Redis logical database index. DB int // TLSEnabled enables TLS with a conservative minimum protocol version. TLSEnabled bool // SessionLimitKey identifies the single Redis string key that stores the // active-session-limit configuration value. SessionLimitKey string // OperationTimeout bounds each Redis round trip performed by the adapter. OperationTimeout time.Duration } // Store reads dynamic auth/session configuration from Redis. type Store struct { client *redis.Client sessionLimitKey string operationTimeout time.Duration } // New constructs a Redis-backed config provider from cfg. func New(cfg Config) (*Store, error) { switch { case strings.TrimSpace(cfg.Addr) == "": return nil, errors.New("new redis config provider: redis addr must not be empty") case cfg.DB < 0: return nil, errors.New("new redis config provider: redis db must not be negative") case strings.TrimSpace(cfg.SessionLimitKey) == "": return nil, errors.New("new redis config provider: session limit key must not be empty") case cfg.OperationTimeout <= 0: return nil, errors.New("new redis config provider: operation timeout must be positive") } options := &redis.Options{ Addr: cfg.Addr, Username: cfg.Username, Password: cfg.Password, DB: cfg.DB, Protocol: 2, DisableIdentity: true, } if cfg.TLSEnabled { options.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12} } return &Store{ client: redis.NewClient(options), sessionLimitKey: cfg.SessionLimitKey, operationTimeout: cfg.OperationTimeout, }, nil } // Close releases the underlying Redis client resources. func (s *Store) Close() error { if s == nil || s.client == nil { return nil } return s.client.Close() } // Ping verifies that the configured Redis backend is reachable within the // adapter operation timeout budget. func (s *Store) Ping(ctx context.Context) error { operationCtx, cancel, err := s.operationContext(ctx, "ping redis config provider") if err != nil { return err } defer cancel() if err := s.client.Ping(operationCtx).Err(); err != nil { return fmt.Errorf("ping redis config provider: %w", err) } return nil } // LoadSessionLimit returns the current active-session-limit configuration. // Missing or invalid Redis values are treated as “limit absent” by policy. func (s *Store) LoadSessionLimit(ctx context.Context) (ports.SessionLimitConfig, error) { operationCtx, cancel, err := s.operationContext(ctx, "load session limit from redis") if err != nil { return ports.SessionLimitConfig{}, err } defer cancel() value, err := s.client.Get(operationCtx, s.sessionLimitKey).Result() switch { case errors.Is(err, redis.Nil): return ports.SessionLimitConfig{}, nil case err != nil: return ports.SessionLimitConfig{}, fmt.Errorf("load session limit from redis: %w", err) } config, valid := parseSessionLimitConfig(value) if !valid { return ports.SessionLimitConfig{}, nil } if err := config.Validate(); err != nil { return ports.SessionLimitConfig{}, nil } return config, nil } func (s *Store) operationContext(ctx context.Context, operation string) (context.Context, context.CancelFunc, error) { if s == nil || s.client == nil { return nil, nil, fmt.Errorf("%s: nil store", operation) } if ctx == nil { return nil, nil, fmt.Errorf("%s: nil context", operation) } operationCtx, cancel := context.WithTimeout(ctx, s.operationTimeout) return operationCtx, cancel, nil } func parseSessionLimitConfig(raw string) (ports.SessionLimitConfig, bool) { if strings.TrimSpace(raw) == "" || strings.TrimSpace(raw) != raw { return ports.SessionLimitConfig{}, false } for _, symbol := range raw { if symbol < '0' || symbol > '9' { return ports.SessionLimitConfig{}, false } } parsed, err := strconv.ParseInt(raw, 10, strconv.IntSize) if err != nil || parsed <= 0 { return ports.SessionLimitConfig{}, false } limit := int(parsed) return ports.SessionLimitConfig{ ActiveSessionLimit: &limit, }, true } var _ ports.ConfigProvider = (*Store)(nil)