@@ -27,16 +27,17 @@ const (
2727// Fields with `options:"file"` support Docker secrets via the _FILE suffix.
2828// Available options: file, toLower, trimTrailingSlash
2929type Config struct {
30- AppUrl string `env:"APP_URL" default:"http://localhost:3552"`
31- DatabaseURL string `env:"DATABASE_URL" default:"file:data/arcane.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate" options:"file"`
32- Port string `env:"PORT" default:"3552"`
33- Listen string `env:"LISTEN" default:""`
34- TLSEnabled bool `env:"TLS_ENABLED" default:"false"`
35- TLSCertFile string `env:"TLS_CERT_FILE" default:""`
36- TLSKeyFile string `env:"TLS_KEY_FILE" default:""`
37- Environment AppEnvironment `env:"ENVIRONMENT" default:"production"`
38- JWTSecret string `env:"JWT_SECRET" default:"default-jwt-secret-change-me" options:"file"` //nolint:gosec // configuration field name is part of stable config API
39- EncryptionKey string `env:"ENCRYPTION_KEY" default:"arcane-dev-key-32-characters!!!" options:"file"`
30+ AppUrl string `env:"APP_URL" default:"http://localhost:3552"`
31+ DatabaseURL string `env:"DATABASE_URL" default:"file:data/arcane.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate" options:"file"`
32+ Port string `env:"PORT" default:"3552"`
33+ Listen string `env:"LISTEN" default:""`
34+ TLSEnabled bool `env:"TLS_ENABLED" default:"false"`
35+ TLSCertFile string `env:"TLS_CERT_FILE" default:""`
36+ TLSKeyFile string `env:"TLS_KEY_FILE" default:""`
37+ Environment AppEnvironment `env:"ENVIRONMENT" default:"production"`
38+ JWTSecret string `env:"JWT_SECRET" default:"default-jwt-secret-change-me" options:"file"` //nolint:gosec // configuration field name is part of stable config API
39+ JWTRefreshExpiry time.Duration `env:"JWT_REFRESH_EXPIRY" default:"168h"`
40+ EncryptionKey string `env:"ENCRYPTION_KEY" default:"arcane-dev-key-32-characters!!!" options:"file"`
4041
4142 OidcEnabled bool `env:"OIDC_ENABLED" default:"false"`
4243 OidcClientID string `env:"OIDC_CLIENT_ID" default:"" options:"file"`
@@ -123,7 +124,7 @@ func loadFromEnv(cfg *Config) {
123124 envValue = defaultValue
124125 }
125126
126- setFieldValue (field , envValue )
127+ setFieldValueInternal (field , fieldType , envValue )
127128 })
128129}
129130
@@ -241,8 +242,8 @@ func resolveFileBasedEnvVariable(field reflect.Value, fieldType reflect.StructFi
241242 }
242243}
243244
244- // setFieldValue sets a reflect.Value from a string based on the field's type.
245- func setFieldValue (field reflect.Value , value string ) {
245+ // setFieldValueInternal sets a reflect.Value from a string based on the field's type.
246+ func setFieldValueInternal (field reflect.Value , fieldType reflect. StructField , value string ) {
246247 if ! field .CanSet () {
247248 return
248249 }
@@ -274,6 +275,37 @@ func setFieldValue(field reflect.Value, value string) {
274275 return
275276 }
276277
278+ if field .Type () == reflect .TypeFor [time.Duration ]() {
279+ applyDurationDefault := func (reason string ) {
280+ envTag := fieldType .Tag .Get ("env" )
281+ defaultValue := fieldType .Tag .Get ("default" )
282+
283+ if fallback , fallbackErr := time .ParseDuration (defaultValue ); fallbackErr == nil {
284+ slog .Warn (reason + ", using tagged default" ,
285+ "field" , envTag ,
286+ "value" , value ,
287+ "default" , defaultValue )
288+ field .SetInt (int64 (fallback ))
289+ } else {
290+ slog .Warn (reason + ", and invalid tagged default" ,
291+ "field" , envTag ,
292+ "value" , value ,
293+ "default" , defaultValue )
294+ }
295+ }
296+
297+ if d , err := time .ParseDuration (value ); err == nil {
298+ if d > 0 {
299+ field .SetInt (int64 (d ))
300+ } else {
301+ applyDurationDefault ("Non-positive duration for config field" )
302+ }
303+ } else {
304+ applyDurationDefault ("Invalid duration for config field" )
305+ }
306+ return
307+ }
308+
277309 // Handle custom types based on underlying kind
278310 if field .Type ().ConvertibleTo (reflect .TypeFor [string ]()) {
279311 // String-based types like AppEnvironment
0 commit comments