Expand financial providers (#1407)

* Initial implementation

* Tiingo fixes

* Adds 2 providers, remove 2

* Add  extra checks

* FIX a big hotwire race condition

// Fix hotwire_combobox race condition: when typing quickly, a slow response for
// an early query (e.g. "A") can overwrite the correct results for the final query
// (e.g. "AAPL"). We abort the previous in-flight request whenever a new one fires,
// so stale Turbo Stream responses never reach the DOM.

* pipelock

* Update price_test.rb

* Reviews

* i8n

* fixes

* fixes

* Update tiingo.rb

* fixes

* Improvements

* Big revamp

* optimisations

* Update 20260408151837_add_offline_reason_to_securities.rb

* Add missing tests, fixes

* small rank tests

* FIX tests

* Update show.html.erb

* Update resolver.rb

* Update usd_converter.rb

* Update holdings_controller.rb

* Update holdings_controller.rb

* Update holdings_controller.rb

* Update holdings_controller.rb

* Update holdings_controller.rb

* Update _yahoo_finance_settings.html.erb
This commit is contained in:
soky srm
2026-04-09 18:33:59 +02:00
committed by GitHub
parent ab13093634
commit 7908f7d8a4
50 changed files with 2553 additions and 206 deletions

View File

@@ -36,6 +36,83 @@ class Setting < RailsSettings::Base
field :exchange_rate_provider, type: :string, default: ENV.fetch("EXCHANGE_RATE_PROVIDER", "twelve_data")
field :securities_provider, type: :string, default: ENV.fetch("SECURITIES_PROVIDER", "twelve_data")
# Multi-provider: comma-separated list of enabled securities providers
field :securities_providers, type: :string, default: ENV.fetch("SECURITIES_PROVIDERS", "")
# New provider API keys (encrypted at rest — see EncryptedSettingFields below)
field :tiingo_api_key, type: :string, default: ENV["TIINGO_API_KEY"]
field :eodhd_api_key, type: :string, default: ENV["EODHD_API_KEY"]
field :alpha_vantage_api_key, type: :string, default: ENV["ALPHA_VANTAGE_API_KEY"]
# Transparent encryption for API key fields. The `field` macro defines the
# raw getter/setter on the class. By prepending this module we intercept
# reads (decrypt) and writes (encrypt) while `super` delegates to the
# original getter/setter generated by rails-settings-cached.
#
# Backward-compatible: if decryption fails (e.g. the value was stored before
# encryption was enabled) the raw value is returned as-is.
module EncryptedSettingFields
ENCRYPTED_FIELDS = %i[
twelve_data_api_key
tiingo_api_key
eodhd_api_key
alpha_vantage_api_key
openai_access_token
external_assistant_token
].freeze
ENCRYPTED_FIELDS.each do |field_name|
define_method(field_name) do
raw = super()
decrypt_setting(raw)
end
define_method(:"#{field_name}=") do |value|
super(encrypt_setting(value))
end
end
private
def setting_encryptor
@setting_encryptor ||= begin
key = ActiveSupport::KeyGenerator.new(
Rails.application.secret_key_base
).generate_key("setting_encryption", 32)
ActiveSupport::MessageEncryptor.new(key)
end
end
def encrypt_setting(value)
return value if value.blank?
setting_encryptor.encrypt_and_sign(value)
end
def decrypt_setting(value)
return value if value.blank?
setting_encryptor.decrypt_and_verify(value)
rescue ActiveSupport::MessageVerifier::InvalidSignature,
ActiveSupport::MessageEncryptor::InvalidMessage
# Value was stored before encryption was enabled — return as-is.
# It will be re-encrypted on next write.
value
end
end
class << self
prepend EncryptedSettingFields
end
def self.enabled_securities_providers
plural = ENV["SECURITIES_PROVIDERS"].presence || securities_providers.presence
if plural.present?
plural.to_s.split(",").map(&:strip).reject(&:blank?)
else
# Backward compat: fall back to singular setting
[ ENV["SECURITIES_PROVIDER"].presence || securities_provider ].compact
end
end
# Sync settings - check both provider env vars for default
# Only defaults to true if neither provider explicitly disables pending
SYNCS_INCLUDE_PENDING_DEFAULT = begin