Skip to content

Commit accecdb

Browse files
committed
Make api_tokens.json authoritative source for API tokens (fixes #685)
This is the proper architectural fix for #685. The previous commit was a bandaid that prevented unnecessary .env writes. This commit addresses the root cause: dual-source-of-truth for API tokens (.env vs api_tokens.json). Changes: 1. Startup Migration (config.go:896-951): - When loading config, if API_TOKEN/API_TOKENS exist in .env but not in api_tokens.json, automatically migrate them - Migrated tokens are named "Migrated from .env (prefix)" for clarity - Logs a deprecation warning: API_TOKEN/API_TOKENS in .env are deprecated - Leaves .env untouched (safe for existing deployments) 2. Config Watcher Changes (watcher.go:338-424): - Only load tokens from .env if api_tokens.json is EMPTY - Once api_tokens.json has records, it becomes the authoritative source - .env changes no longer trigger token overwrites when api_tokens.json exists - Logs debug message when ignoring env tokens Result: - Existing deployments: env tokens automatically migrated to api_tokens.json - UI-created tokens: never overwritten by .env changes - Dark mode toggle: no longer triggers token reload from .env - Backward compatible: fresh installs with API_TOKEN in .env still work - Migration path: users can safely keep API_TOKEN in .env, it will be ignored Future improvement: Add UI warning when API_TOKEN/API_TOKENS still present in .env, prompting users to rotate tokens via the UI.
1 parent 5d99fc2 commit accecdb

File tree

2 files changed

+34
-10
lines changed

2 files changed

+34
-10
lines changed

internal/config/config.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,11 @@ func Load() (*Config, error) {
896896
if len(envTokens) > 0 {
897897
cfg.EnvOverrides["API_TOKEN"] = true
898898
cfg.EnvOverrides["API_TOKENS"] = true
899+
900+
// Track if we migrated any new tokens from env to persistence
901+
migratedCount := 0
902+
needsPersist := false
903+
899904
for _, tokenValue := range envTokens {
900905
if tokenValue == "" {
901906
continue
@@ -909,27 +914,41 @@ func Load() (*Config, error) {
909914
hashed = auth.HashAPIToken(tokenValue)
910915
prefix = tokenPrefix(tokenValue)
911916
suffix = tokenSuffix(tokenValue)
912-
log.Info().Msg("Auto-hashed plain text API token from environment variable")
913-
} else {
914-
log.Debug().Msg("Loaded pre-hashed API token from env var")
917+
log.Debug().Msg("Auto-hashed plain text API token from environment variable")
915918
}
916919

920+
// Check if this token already exists in api_tokens.json
917921
if cfg.HasAPITokenHash(hashed) {
918922
continue
919923
}
920924

925+
// Migrate env token to api_tokens.json
921926
record := APITokenRecord{
922927
ID: uuid.NewString(),
923-
Name: "Environment token",
928+
Name: "Migrated from .env (" + prefix + ")",
924929
Hash: hashed,
925930
Prefix: prefix,
926931
Suffix: suffix,
927932
CreatedAt: time.Now().UTC(),
928933
Scopes: []string{ScopeWildcard},
929934
}
930935
cfg.APITokens = append(cfg.APITokens, record)
936+
migratedCount++
937+
needsPersist = true
931938
}
939+
932940
cfg.SortAPITokens()
941+
942+
// Persist migrated tokens to api_tokens.json
943+
if needsPersist && persistence != nil {
944+
if err := persistence.SaveAPITokens(cfg.APITokens); err != nil {
945+
log.Error().Err(err).Msg("Failed to persist migrated API tokens from environment")
946+
} else {
947+
log.Warn().
948+
Int("count", migratedCount).
949+
Msg("Migrated API tokens from .env to api_tokens.json - API_TOKEN/API_TOKENS in .env are deprecated and will be ignored in future releases. Manage tokens via the UI instead.")
950+
}
951+
}
933952
}
934953

935954
// Check if API token is enabled

internal/config/watcher.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,8 @@ func (cw *ConfigWatcher) reloadConfig() {
335335
}
336336
}
337337

338-
// Apply API tokens if present in .env (legacy support)
338+
// Legacy env token support: only process if api_tokens.json is empty
339+
// This prevents .env changes from overwriting UI-managed tokens (fixes #685)
339340
rawTokens := make([]string, 0, 4)
340341
if raw, ok := envMap["API_TOKENS"]; ok {
341342
raw = strings.Trim(raw, "'\"")
@@ -347,17 +348,19 @@ func (cw *ConfigWatcher) reloadConfig() {
347348
rawTokens = append(rawTokens, token)
348349
}
349350
}
350-
} else {
351-
// Explicit empty list clears tokens
352-
rawTokens = []string{}
353351
}
354352
}
355353
if raw, ok := envMap["API_TOKEN"]; ok {
356354
raw = strings.Trim(raw, "'\"")
357-
rawTokens = append(rawTokens, raw)
355+
if raw != "" {
356+
rawTokens = append(rawTokens, raw)
357+
}
358358
}
359359

360-
if len(rawTokens) > 0 {
360+
// Only reload tokens from .env if NO tokens exist in api_tokens.json
361+
// This makes api_tokens.json the authoritative source once it has records
362+
if len(rawTokens) > 0 && len(cw.config.APITokens) == 0 {
363+
log.Debug().Msg("No existing API tokens found - loading from .env (legacy)")
361364
seen := make(map[string]struct{}, len(rawTokens))
362365
newRecords := make([]APITokenRecord, 0, len(rawTokens))
363366
for _, tokenValue := range rawTokens {
@@ -416,6 +419,8 @@ func (cw *ConfigWatcher) reloadConfig() {
416419
}
417420
}
418421
}
422+
} else if len(rawTokens) > 0 && len(cw.config.APITokens) > 0 {
423+
log.Debug().Msg("Ignoring API_TOKEN/API_TOKENS from .env - api_tokens.json is authoritative")
419424
}
420425

421426
// REMOVED: POLLING_INTERVAL from .env - now ONLY in system.json

0 commit comments

Comments
 (0)