Skip to content

Commit 17b1588

Browse files
committed
chore: add configuration module
1 parent b8e57bf commit 17b1588

7 files changed

Lines changed: 282 additions & 137 deletions

File tree

api/routes_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestIsLocalhost(t *testing.T) {
4242

4343
func TestPushTokenReport(t *testing.T) {
4444
e := echo.New()
45-
svc := service.NewMessageService(nil, nil, "")
45+
svc := service.NewMessageService(nil, nil, service.NewTestConfig())
4646

4747
t.Run("valid push token report", func(t *testing.T) {
4848
reqBody := models.PushTokenReportRequest{
@@ -130,7 +130,7 @@ func TestGetPushTokens(t *testing.T) {
130130
require.NoError(t, err)
131131

132132
e := echo.New()
133-
svc := service.NewMessageService(nil, pushTokenDB, "")
133+
svc := service.NewMessageService(nil, pushTokenDB, service.NewTestConfig())
134134

135135
t.Run("get all push tokens with valid token", func(t *testing.T) {
136136
req := httptest.NewRequest(http.MethodGet, "/api/internal/push_tokens", nil)
@@ -276,7 +276,7 @@ func TestResetPushTokens(t *testing.T) {
276276
require.NoError(t, err)
277277

278278
e := echo.New()
279-
svc := service.NewMessageService(nil, pushTokenDB, "")
279+
svc := service.NewMessageService(nil, pushTokenDB, service.NewTestConfig())
280280

281281
t.Run("reset push tokens with valid token", func(t *testing.T) {
282282
req := httptest.NewRequest(http.MethodDelete, "/api/internal/push_tokens", nil)

main.go

Lines changed: 21 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,25 @@
11
package main
22

33
import (
4-
"os"
5-
64
"github.com/labstack/echo/v4"
75
"github.com/labstack/echo/v4/middleware"
86
"github.com/nethesis/matrix2acrobits/api"
97
"github.com/nethesis/matrix2acrobits/db"
108
"github.com/nethesis/matrix2acrobits/logger"
119
"github.com/nethesis/matrix2acrobits/matrix"
1210
"github.com/nethesis/matrix2acrobits/service"
13-
"maunium.net/go/mautrix/id"
1411
)
1512

16-
const defaultPort = "8080"
17-
1813
func main() {
19-
// Initialize logger from LOGLEVEL env var (default: INFO)
20-
logLevel := os.Getenv("LOGLEVEL")
21-
if logLevel == "" {
22-
logLevel = string(logger.LevelInfo)
14+
// Load configuration from environment variables
15+
cfg, err := service.NewConfig()
16+
if err != nil {
17+
logger.Fatal().Err(err).Msg("failed to load configuration")
2318
}
24-
logger.Init(logger.Level(logLevel))
25-
logger.Info().Str("level", logLevel).Msg("logger initialized")
2619

27-
port := os.Getenv("PROXY_PORT")
28-
if port == "" {
29-
port = defaultPort
30-
}
20+
// Initialize logger
21+
logger.Init(logger.Level(cfg.LogLevel))
22+
logger.Info().Str("level", cfg.LogLevel).Msg("logger initialized")
3123

3224
e := echo.New()
3325
e.HideBanner = true
@@ -40,71 +32,43 @@ func main() {
4032
return c.JSON(200, map[string]string{"status": "ok"})
4133
})
4234

43-
homeserver := os.Getenv("MATRIX_HOMESERVER_URL")
44-
if homeserver == "" {
45-
logger.Fatal().Msg("MATRIX_HOMESERVER_URL is required")
46-
}
47-
48-
adminToken := os.Getenv("SUPER_ADMIN_TOKEN")
49-
if adminToken == "" {
50-
logger.Fatal().Msg("SUPER_ADMIN_TOKEN is required (must be the Application Service as_token)")
51-
}
52-
53-
asUserID := os.Getenv("AS_USER_ID")
54-
if asUserID == "" {
55-
logger.Fatal().Msg("AS_USER_ID is required (e.g., '@_acrobits_proxy:your.server.com')")
56-
}
57-
58-
logger.Info().Str("homeserver", homeserver).Str("as_user_id", asUserID).Msg("initializing matrix client")
35+
logger.Info().Str("homeserver", cfg.MatrixHomeserverURL).Str("as_user_id", cfg.MatrixAsUserID.String()).Msg("initializing matrix client")
5936

6037
matrixClient, err := matrix.NewClient(matrix.Config{
61-
HomeserverURL: homeserver,
62-
AsToken: adminToken,
63-
AsUserID: id.UserID(asUserID),
38+
HomeserverURL: cfg.MatrixHomeserverURL,
39+
AsToken: cfg.MatrixAsToken,
40+
AsUserID: cfg.MatrixAsUserID,
6441
})
6542
if err != nil {
6643
logger.Fatal().Err(err).Msg("failed to initialize matrix client")
6744
}
6845

6946
// Initialize push token database
70-
pushTokenDBPath := os.Getenv("PUSH_TOKEN_DB_PATH")
71-
if pushTokenDBPath == "" {
72-
pushTokenDBPath = "/tmp/push_tokens.db"
73-
}
74-
75-
pushTokenDB, err := db.NewDatabase(pushTokenDBPath)
47+
pushTokenDB, err := db.NewDatabase(cfg.PushTokenDBPath)
7648
if err != nil {
77-
logger.Fatal().Err(err).Str("path", pushTokenDBPath).Msg("failed to initialize push token database")
49+
logger.Fatal().Err(err).Str("path", cfg.PushTokenDBPath).Msg("failed to initialize push token database")
7850
}
7951
defer func() {
8052
if err := pushTokenDB.Close(); err != nil {
8153
logger.Warn().Err(err).Msg("failed to close push token database")
8254
}
8355
}()
8456

85-
// Get proxy URL for pusher registration
86-
proxyURL := os.Getenv("PROXY_URL")
87-
if proxyURL == "" {
88-
proxyURL = homeserver
89-
logger.Info().Msg("PROXY_URL not configured, assuming same as MATRIX_HOMESERVER_URL")
90-
} else {
91-
logger.Info().Str("proxy_url", proxyURL).Msg("proxy URL configured for pusher registration")
92-
}
57+
logger.Info().Str("proxy_url", cfg.ProxyURL).Msg("proxy URL configured for pusher registration")
9358

94-
svc := service.NewMessageService(matrixClient, pushTokenDB, proxyURL)
59+
svc := service.NewMessageService(matrixClient, pushTokenDB, cfg)
9560
pushSvc := service.NewPushService(pushTokenDB)
96-
api.RegisterRoutes(e, svc, pushSvc, adminToken, pushTokenDB)
61+
api.RegisterRoutes(e, svc, pushSvc, cfg.MatrixAsToken, pushTokenDB)
9762

9863
// Load mappings from file if MAPPING_FILE env var is set
99-
mappingFile := os.Getenv("MAPPING_FILE")
100-
if mappingFile != "" {
101-
if err := svc.LoadMappingsFromFile(mappingFile); err != nil {
102-
logger.Error().Err(err).Str("file", mappingFile).Msg("failed to load mappings from file")
64+
if cfg.MappingFile != "" {
65+
if err := svc.LoadMappingsFromFile(cfg.MappingFile); err != nil {
66+
logger.Error().Err(err).Str("file", cfg.MappingFile).Msg("failed to load mappings from file")
10367
}
10468
}
10569

106-
logger.Info().Str("port", port).Msg("starting server")
107-
if err := e.Start(":" + port); err != nil {
70+
logger.Info().Str("port", cfg.ProxyPort).Msg("starting server")
71+
if err := e.Start(":" + cfg.ProxyPort); err != nil {
10872
logger.Fatal().Err(err).Msg("server stopped")
10973
}
11074
}

main_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,23 @@ func startTestServer(cfg *testConfig) (*echo.Echo, error) {
147147
return nil, fmt.Errorf("initialize matrix client: %w", err)
148148
}
149149

150-
svc := service.NewMessageService(matrixClient, nil, "")
150+
// Create a Config from the test configuration
151+
serviceCfg := &service.Config{
152+
ProxyPort: "18080",
153+
LogLevel: "INFO",
154+
MatrixHomeserverURL: cfg.homeserverURL,
155+
MatrixAsToken: cfg.adminToken,
156+
MatrixAsUserID: id.UserID(cfg.asUser),
157+
MatrixHomeserverHost: cfg.serverName,
158+
PushTokenDBPath: "/tmp/push_tokens_test.db",
159+
ProxyURL: cfg.homeserverURL,
160+
CacheTTLSeconds: 3600,
161+
CacheTTL: 3600 * time.Second,
162+
ExtAuthTimeoutS: 5,
163+
ExtAuthTimeout: 5 * time.Second,
164+
}
165+
166+
svc := service.NewMessageService(matrixClient, nil, serviceCfg)
151167
pushSvc := service.NewPushService(nil)
152168
api.RegisterRoutes(e, svc, pushSvc, cfg.adminToken, nil)
153169

service/config.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package service
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"os"
7+
"strconv"
8+
"time"
9+
10+
"github.com/nethesis/matrix2acrobits/logger"
11+
"maunium.net/go/mautrix/id"
12+
)
13+
14+
const (
15+
defaultPort = "8080"
16+
defaultCacheTTLSeconds = 3600
17+
defaultPushTokenDBPath = "/tmp/push_tokens.db"
18+
defaultExtAuthTimeoutS = 5
19+
defaultLogLevel = "INFO"
20+
)
21+
22+
// Config holds all configuration loaded from environment variables
23+
type Config struct {
24+
// Server configuration
25+
ProxyPort string
26+
LogLevel string
27+
28+
// Matrix configuration
29+
MatrixHomeserverURL string
30+
MatrixAsToken string
31+
MatrixAsUserID id.UserID
32+
MatrixHomeserverHost string
33+
34+
// Push tokens database
35+
PushTokenDBPath string
36+
37+
// Proxy configuration for push registration
38+
ProxyURL string
39+
40+
// Message service configuration
41+
CacheTTLSeconds int
42+
CacheTTL time.Duration
43+
44+
// External authentication configuration
45+
ExtAuthURL string
46+
ExtAuthTimeoutS int
47+
ExtAuthTimeout time.Duration
48+
49+
// Mapping file
50+
MappingFile string
51+
}
52+
53+
// NewConfig loads all configuration from environment variables with validation
54+
func NewConfig() (*Config, error) {
55+
cfg := &Config{}
56+
57+
logger.Debug().Msg("starting configuration loading from environment variables")
58+
59+
// Load server configuration
60+
cfg.LogLevel = os.Getenv("LOGLEVEL")
61+
if cfg.LogLevel == "" {
62+
cfg.LogLevel = defaultLogLevel
63+
logger.Debug().Str("LOGLEVEL", cfg.LogLevel).Msg("using default log level")
64+
} else {
65+
logger.Debug().Str("LOGLEVEL", cfg.LogLevel).Msg("log level loaded from environment")
66+
}
67+
68+
cfg.ProxyPort = os.Getenv("PROXY_PORT")
69+
if cfg.ProxyPort == "" {
70+
cfg.ProxyPort = defaultPort
71+
logger.Debug().Str("PROXY_PORT", cfg.ProxyPort).Msg("using default proxy port")
72+
} else {
73+
logger.Debug().Str("PROXY_PORT", cfg.ProxyPort).Msg("proxy port loaded from environment")
74+
}
75+
76+
// Load Matrix configuration (required)
77+
cfg.MatrixHomeserverURL = os.Getenv("MATRIX_HOMESERVER_URL")
78+
if cfg.MatrixHomeserverURL == "" {
79+
logger.Error().Msg("MATRIX_HOMESERVER_URL environment variable is missing")
80+
return nil, fmt.Errorf("MATRIX_HOMESERVER_URL is required")
81+
}
82+
logger.Debug().Str("MATRIX_HOMESERVER_URL", cfg.MatrixHomeserverURL).Msg("matrix homeserver URL loaded from environment")
83+
84+
cfg.MatrixAsToken = os.Getenv("SUPER_ADMIN_TOKEN")
85+
if cfg.MatrixAsToken == "" {
86+
logger.Error().Msg("SUPER_ADMIN_TOKEN environment variable is missing")
87+
return nil, fmt.Errorf("SUPER_ADMIN_TOKEN is required (must be the Application Service as_token)")
88+
}
89+
logger.Debug().Msg("SUPER_ADMIN_TOKEN loaded from environment")
90+
91+
asUserIDStr := os.Getenv("AS_USER_ID")
92+
if asUserIDStr == "" {
93+
logger.Error().Msg("AS_USER_ID environment variable is missing")
94+
return nil, fmt.Errorf("AS_USER_ID is required (e.g., '@_acrobits_proxy:your.server.com')")
95+
}
96+
cfg.MatrixAsUserID = id.UserID(asUserIDStr)
97+
logger.Debug().Str("AS_USER_ID", asUserIDStr).Msg("application service user ID loaded from environment")
98+
99+
// Derive homeserver host from MATRIX_HOMESERVER_URL
100+
if u, err := url.Parse(cfg.MatrixHomeserverURL); err == nil {
101+
cfg.MatrixHomeserverHost = u.Hostname()
102+
logger.Debug().Str("homeserver_host", cfg.MatrixHomeserverHost).Msg("extracted homeserver host from URL")
103+
} else {
104+
logger.Warn().Err(err).Str("MATRIX_HOMESERVER_URL", cfg.MatrixHomeserverURL).Msg("failed to parse homeserver URL")
105+
}
106+
107+
// Load push tokens database configuration
108+
cfg.PushTokenDBPath = os.Getenv("PUSH_TOKEN_DB_PATH")
109+
if cfg.PushTokenDBPath == "" {
110+
cfg.PushTokenDBPath = defaultPushTokenDBPath
111+
logger.Debug().Str("PUSH_TOKEN_DB_PATH", cfg.PushTokenDBPath).Msg("using default push token database path")
112+
} else {
113+
logger.Debug().Str("PUSH_TOKEN_DB_PATH", cfg.PushTokenDBPath).Msg("push token database path loaded from environment")
114+
}
115+
116+
// Load proxy configuration
117+
cfg.ProxyURL = os.Getenv("PROXY_URL")
118+
if cfg.ProxyURL == "" {
119+
cfg.ProxyURL = cfg.MatrixHomeserverURL
120+
logger.Info().Str("PROXY_URL", cfg.ProxyURL).Msg("PROXY_URL not configured, assuming same as MATRIX_HOMESERVER_URL")
121+
} else {
122+
logger.Debug().Str("PROXY_URL", cfg.ProxyURL).Msg("proxy URL loaded from environment")
123+
}
124+
125+
// Load cache configuration
126+
cacheTTLStr := os.Getenv("CACHE_TTL_SECONDS")
127+
cfg.CacheTTLSeconds = defaultCacheTTLSeconds
128+
if cacheTTLStr != "" {
129+
if parsed, err := strconv.Atoi(cacheTTLStr); err == nil && parsed > 0 {
130+
cfg.CacheTTLSeconds = parsed
131+
logger.Debug().Int("CACHE_TTL_SECONDS", cfg.CacheTTLSeconds).Msg("cache TTL loaded from environment")
132+
} else {
133+
logger.Warn().Str("CACHE_TTL_SECONDS", cacheTTLStr).Err(err).Int("default", defaultCacheTTLSeconds).Msg("invalid cache TTL value, using default")
134+
}
135+
} else {
136+
logger.Debug().Int("CACHE_TTL_SECONDS", cfg.CacheTTLSeconds).Msg("using default cache TTL")
137+
}
138+
cfg.CacheTTL = time.Duration(cfg.CacheTTLSeconds) * time.Second
139+
140+
// Load external authentication configuration
141+
cfg.ExtAuthURL = os.Getenv("EXT_AUTH_URL")
142+
if cfg.ExtAuthURL == "" {
143+
logger.Warn().Msg("EXT_AUTH_URL not set - external authentication will not be available")
144+
} else {
145+
logger.Debug().Str("EXT_AUTH_URL", cfg.ExtAuthURL).Msg("external authentication URL loaded from environment")
146+
}
147+
148+
cfg.ExtAuthTimeoutS = defaultExtAuthTimeoutS
149+
if v := os.Getenv("EXT_AUTH_TIMEOUT_S"); v != "" {
150+
if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 {
151+
cfg.ExtAuthTimeoutS = parsed
152+
logger.Debug().Int("EXT_AUTH_TIMEOUT_S", cfg.ExtAuthTimeoutS).Msg("external auth timeout loaded from environment")
153+
} else {
154+
logger.Warn().Str("EXT_AUTH_TIMEOUT_S", v).Err(err).Int("default", defaultExtAuthTimeoutS).Msg("invalid external auth timeout value, using default")
155+
}
156+
} else {
157+
logger.Debug().Int("EXT_AUTH_TIMEOUT_S", cfg.ExtAuthTimeoutS).Msg("using default external auth timeout")
158+
}
159+
cfg.ExtAuthTimeout = time.Duration(cfg.ExtAuthTimeoutS) * time.Second
160+
161+
// Load mapping file configuration
162+
cfg.MappingFile = os.Getenv("MAPPING_FILE")
163+
if cfg.MappingFile != "" {
164+
logger.Debug().Str("MAPPING_FILE", cfg.MappingFile).Msg("mapping file path loaded from environment")
165+
} else {
166+
logger.Debug().Msg("MAPPING_FILE not set - no mapping file will be loaded at startup")
167+
}
168+
169+
logger.Debug().Msg("configuration loading completed successfully")
170+
171+
return cfg, nil
172+
}
173+
174+
// NewTestConfig creates a minimal Config for testing purposes
175+
func NewTestConfig() *Config {
176+
return &Config{
177+
ProxyPort: defaultPort,
178+
LogLevel: defaultLogLevel,
179+
MatrixHomeserverURL: "https://example.com",
180+
MatrixAsToken: "test_token",
181+
MatrixAsUserID: "@test:example.com",
182+
MatrixHomeserverHost: "example.com",
183+
PushTokenDBPath: defaultPushTokenDBPath,
184+
ProxyURL: "https://example.com",
185+
CacheTTLSeconds: defaultCacheTTLSeconds,
186+
CacheTTL: time.Duration(defaultCacheTTLSeconds) * time.Second,
187+
ExtAuthTimeoutS: defaultExtAuthTimeoutS,
188+
ExtAuthTimeout: time.Duration(defaultExtAuthTimeoutS) * time.Second,
189+
}
190+
}
191+
192+
// NewTestConfigWithAuth creates a Config for testing with custom auth URL
193+
func NewTestConfigWithAuth(extAuthURL string) *Config {
194+
cfg := NewTestConfig()
195+
cfg.ExtAuthURL = extAuthURL
196+
return cfg
197+
}

0 commit comments

Comments
 (0)