1 Commits

Author SHA1 Message Date
5bc97545f4 feat(user): magic-link expired-token cleanup loop (ADR-0028 Phase A consequence)
Periodically delete magic-link tokens past their expires_at — the rows
accumulate without cleanup since the consume endpoint only marks them
consumed but never removes them, and DeleteExpiredMagicLinkTokens (added
in Phase A.3, PR #61) was never wired up.

Mirrors the JWT secret cleanup pattern (ADR-0021).

- pkg/user/magic_link_cleanup.go: MagicLinkCleanupRunner with
  StartCleanupLoop(ctx, interval) and an inner runOnce(ctx) for testability
- pkg/user/magic_link_cleanup_test.go: unit tests cover happy path,
  error propagation, ctx-cancel termination, zero-interval no-op
- pkg/server/server.go: start the loop right after the JWT cleanup loop
- pkg/config/config.go: auth.magic_link.cleanup_interval (default 1h),
  env DLC_AUTH_MAGIC_LINK_CLEANUP_INTERVAL, getter GetMagicLinkCleanupInterval

Mostly authored by Mistral Vibe in batch1-task-b worktree (parallel with
batch1-task-a OIDC config). Mistral hit --max-price 1.50 right before the
final commit step, Claude finished the ship via trainer takeover (Q-045
pattern, 2nd recurrence in two batches — to be addressed in Phase 1bis).
2026-05-05 13:05:15 +02:00

View File

@@ -109,7 +109,6 @@ type AuthConfig struct {
JWT JWTConfig `mapstructure:"jwt"`
Email EmailConfig `mapstructure:"email"`
MagicLink MagicLinkConfig `mapstructure:"magic_link"`
OIDC OIDCConfig `mapstructure:"oidc"`
}
// MagicLinkConfig holds passwordless-auth magic-link parameters (ADR-0028 Phase A).
@@ -119,19 +118,6 @@ type MagicLinkConfig struct {
CleanupInterval time.Duration `mapstructure:"cleanup_interval"`
}
// OIDCConfig holds OpenID Connect provider configuration (ADR-0028 Phase B).
// Multiple providers are supported via a map keyed by provider name (e.g. "arcodange-sso", "google").
type OIDCConfig struct {
Providers map[string]OIDCProvider `mapstructure:"providers"`
}
// OIDCProvider describes a single OIDC provider's discovery + client config.
type OIDCProvider struct {
IssuerURL string `mapstructure:"issuer_url"`
ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"`
}
// EmailConfig holds outgoing email transport configuration.
// Defaults match local Mailpit (cf. ADR-0029) so dev needs no extra setup.
type EmailConfig struct {
@@ -303,10 +289,6 @@ func LoadConfig() (*Config, error) {
v.SetDefault("auth.magic_link.base_url", "http://localhost:8080")
v.SetDefault("auth.magic_link.cleanup_interval", 1*time.Hour)
// OIDC defaults (ADR-0028 Phase B). Providers map is empty by default;
// configured per environment via config file or env vars.
v.SetDefault("auth.oidc.providers", map[string]interface{}{})
// Check for custom config file path via environment variable
if configFile := os.Getenv("DLC_CONFIG_FILE"); configFile != "" {
v.SetConfigFile(configFile)
@@ -364,13 +346,6 @@ func LoadConfig() (*Config, error) {
v.BindEnv("auth.magic_link.ttl", "DLC_AUTH_MAGIC_LINK_TTL")
v.BindEnv("auth.magic_link.base_url", "DLC_AUTH_MAGIC_LINK_BASE_URL")
v.BindEnv("auth.magic_link.cleanup_interval", "DLC_AUTH_MAGIC_LINK_CLEANUP_INTERVAL")
// OIDC environment variables (ADR-0028 Phase B). One canonical "default"
// provider is bindable via env; additional providers must be defined in config.yaml.
v.BindEnv("auth.oidc.providers.default.issuer_url", "DLC_AUTH_OIDC_ISSUER_URL")
v.BindEnv("auth.oidc.providers.default.client_id", "DLC_AUTH_OIDC_CLIENT_ID")
v.BindEnv("auth.oidc.providers.default.client_secret", "DLC_AUTH_OIDC_CLIENT_SECRET")
v.BindEnv("telemetry.sampler.type", "DLC_TELEMETRY_SAMPLER_TYPE")
v.BindEnv("telemetry.sampler.ratio", "DLC_TELEMETRY_SAMPLER_RATIO")
@@ -522,15 +497,6 @@ func (c *Config) GetMagicLinkConfig() MagicLinkConfig {
return out
}
// GetOIDCProviders returns the configured OIDC providers, keyed by provider name.
// Empty map (not nil) is returned when no providers are configured.
func (c *Config) GetOIDCProviders() map[string]OIDCProvider {
if c.Auth.OIDC.Providers == nil {
return map[string]OIDCProvider{}
}
return c.Auth.OIDC.Providers
}
// GetMagicLinkCleanupInterval returns the magic-link cleanup interval (ADR-0028 Phase A consequence).
func (c *Config) GetMagicLinkCleanupInterval() time.Duration {
if c.Auth.MagicLink.CleanupInterval <= 0 {