Skip to content

Commit da12762

Browse files
committed
feat: add jwt refresh interval variable
1 parent ba02546 commit da12762

4 files changed

Lines changed: 48 additions & 11 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ PGID=1000
2727
# IMPORTANT: Generate unique values for production!
2828
ENCRYPTION_KEY=your-32-char-encryption-key-here
2929
JWT_SECRET=your-super-secret-jwt-key-change-this
30+
# JWT refresh token expiry duration (default: 168h = 7 days). Accepts Go duration format (e.g., 72h, 720h, 8760h).
31+
# JWT_REFRESH_EXPIRY=168h
3032

3133
# Database Configuration
3234
DATABASE_URL=file:data/arcane.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate

backend/internal/config/config.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,17 @@ const (
2727
// Fields with `options:"file"` support Docker secrets via the _FILE suffix.
2828
// Available options: file, toLower, trimTrailingSlash
2929
type 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"`
@@ -274,6 +275,13 @@ func setFieldValue(field reflect.Value, value string) {
274275
return
275276
}
276277

278+
if field.Type() == reflect.TypeFor[time.Duration]() {
279+
if d, err := time.ParseDuration(value); err == nil {
280+
field.SetInt(int64(d))
281+
}
282+
return
283+
}
284+
277285
// Handle custom types based on underlying kind
278286
if field.Type().ConvertibleTo(reflect.TypeFor[string]()) {
279287
// String-based types like AppEnvironment

backend/internal/config/config_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"os"
55
"path/filepath"
66
"testing"
7+
"time"
78

89
"github.com/getarcaneapp/arcane/backend/internal/common"
910
"github.com/stretchr/testify/assert"
@@ -353,6 +354,32 @@ func TestConfig_GetManagerBaseURL(t *testing.T) {
353354
})
354355
}
355356

357+
func TestConfig_JWTRefreshExpiry(t *testing.T) {
358+
origJWTRefreshExpiry := os.Getenv("JWT_REFRESH_EXPIRY")
359+
defer restoreEnv("JWT_REFRESH_EXPIRY", origJWTRefreshExpiry)
360+
361+
t.Run("defaults to 7 days when not set", func(t *testing.T) {
362+
unsetEnv(t, "JWT_REFRESH_EXPIRY")
363+
364+
cfg := Load()
365+
assert.Equal(t, 7*24*time.Hour, cfg.JWTRefreshExpiry)
366+
})
367+
368+
t.Run("parses custom duration from env var", func(t *testing.T) {
369+
setEnv(t, "JWT_REFRESH_EXPIRY", "48h")
370+
371+
cfg := Load()
372+
assert.Equal(t, 48*time.Hour, cfg.JWTRefreshExpiry)
373+
})
374+
375+
t.Run("supports compound duration format", func(t *testing.T) {
376+
setEnv(t, "JWT_REFRESH_EXPIRY", "24h30m")
377+
378+
cfg := Load()
379+
assert.Equal(t, 24*time.Hour+30*time.Minute, cfg.JWTRefreshExpiry)
380+
})
381+
}
382+
356383
func restoreEnv(key, value string) {
357384
if value == "" {
358385
_ = os.Unsetenv(key)

backend/internal/services/auth_service.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func NewAuthService(userService *UserService, settingsService *SettingsService,
6464
settingsService: settingsService,
6565
eventService: eventService,
6666
jwtSecret: crypto.CheckOrGenerateJwtSecret(jwtSecret),
67-
refreshExpiry: 7 * 24 * time.Hour,
67+
refreshExpiry: cfg.JWTRefreshExpiry,
6868
config: cfg,
6969
}
7070
}

0 commit comments

Comments
 (0)