package migrations import ( "log" "github.com/jmoiron/sqlx" "github.com/knadh/koanf/v2" "github.com/knadh/stuffbin" ) func V6_2_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error { // Add `msg_retry_delay` and `from_addresses` to each SMTP server entry in the `smtp` // Idempotent: only updates rows where at least one entry is missing the key. if _, err := db.Exec(` UPDATE settings SET value = ( SELECT JSONB_AGG( JSONB_SET( JSONB_SET( v, '{msg_retry_delay}', COALESCE(v->'msg_retry_delay', '"10ms"'::JSONB) ), '{from_addresses}', COALESCE(v->'from_addresses', '[]'::JSONB) ) ORDER BY ord ) FROM JSONB_ARRAY_ELEMENTS(value) WITH ORDINALITY AS t(v, ord) ) WHERE key = 'smtp' AND EXISTS ( SELECT 1 FROM JSONB_ARRAY_ELEMENTS(value) AS v WHERE NOT (v ? 'msg_retry_delay') OR NOT (v ? 'from_addresses') ); `); err != nil { return err } // Update app language settings that used incorrect locale codes. if _, err := db.Exec(` UPDATE settings SET value = langs.new_value FROM (VALUES ('"cs-cz"'::JSONB, '"cs"'::JSONB), ('"jp"'::JSONB, '"ja"'::JSONB), ('"se"'::JSONB, '"sv"'::JSONB) ) AS langs(old_value, new_value) WHERE key = 'app.lang' AND value = langs.old_value; `); err != nil { return err } // Add `bounce.azure` for ACS/Event Grid bounce handling; upsert if the row already exists. if _, err := db.Exec(` INSERT INTO settings (key, value) VALUES('bounce.azure', '{"enabled": false, "shared_secret": "", "shared_secret_header": ""}') ON CONFLICT (key) DO UPDATE SET value = jsonb_build_object( 'enabled', COALESCE((settings.value->>'enabled')::boolean, false), 'shared_secret', COALESCE(settings.value->>'shared_secret', ''), 'shared_secret_header', COALESCE(settings.value->>'shared_secret_header', '') ); `); err != nil { return err } if _, err := db.Exec(`INSERT INTO settings (key, value) VALUES ('app.show_optin_page', 'true') ON CONFLICT (key) DO NOTHING `); err != nil { return err } // Rename `security.cors_origins` to `security.trusted_urls`. if _, err := db.Exec(`UPDATE settings SET key = 'security.trusted_urls' WHERE key = 'security.cors_origins' AND NOT EXISTS (SELECT 1 FROM settings WHERE key = 'security.trusted_urls')`); err != nil { return err } // Hash existing API tokens. This is idempotent by skipping values that // already look like lowercase SHA-256 hex digests. if _, err := db.Exec(` UPDATE users SET password = ENCODE(DIGEST(password, 'sha256'), 'hex') WHERE type = 'api' AND password IS NOT NULL AND password != '' AND password !~ '^[a-f0-9]{64}$'; `); err != nil { return err } return nil }