class SophtronItemsController < ApplicationController CONNECTION_STATUS_MAX_POLLS = 6 LOGIN_PROGRESS_CONNECTION_STATUS_MAX_POLLS = 15 POST_MFA_CONNECTION_STATUS_MAX_POLLS = 15 CONNECTION_STATUS_POLL_INTERVAL_MS = 4_000 MAX_SECURITY_ANSWERS = 10 MAX_SECURITY_ANSWER_LENGTH = 256 before_action :set_sophtron_item, only: [ :show, :edit, :update, :destroy, :connect_institution, :sync, :connection_status, :submit_mfa, :setup_accounts, :complete_account_setup ] before_action :require_admin!, only: [ :new, :create, :preload_accounts, :select_accounts, :link_accounts, :select_existing_account, :link_existing_account, :connect_institution, :edit, :update, :destroy, :sync, :connection_status, :submit_mfa, :setup_accounts, :complete_account_setup ] def index @sophtron_items = Current.family.sophtron_items.active.ordered render layout: "settings" end def show end def preload_accounts item = configured_sophtron_item unless item render json: { success: false, error: "no_credentials_configured", has_accounts: false } return end item.ensure_customer! unless item.connected_to_institution? render json: { success: false, error: "no_institution_connected", has_accounts: nil } return end accounts = item.fetch_remote_accounts render json: { success: true, has_accounts: accounts.any?, cached: true } rescue Provider::Sophtron::Error => e Rails.logger.error("Sophtron preload error: #{e.message}") render json: { success: false, error: "api_error", error_message: t(".api_error"), has_accounts: nil } rescue StandardError => e Rails.logger.error("Unexpected error preloading Sophtron accounts: #{e.class}: #{e.message}") render json: { success: false, error: "unexpected_error", error_message: t(".unexpected_error"), has_accounts: nil } end def select_accounts item = configured_sophtron_item unless item render_or_redirect_setup_required return end item.ensure_customer! if connect_new_institution_flow? || !item.connected_to_institution? prepare_connection_form(item) render :connect, layout: false return end @available_accounts = item.reject_already_linked(item.fetch_remote_accounts) @accountable_type = params[:accountable_type] || "Depository" @return_to = safe_return_to_path if @available_accounts.empty? redirect_to new_account_path, alert: t(".no_accounts_found") return end render layout: false rescue Provider::Sophtron::Error => e Rails.logger.error("Sophtron API error in select_accounts: #{e.message}") render_api_error(t(".api_error"), safe_return_to_path) rescue StandardError => e Rails.logger.error("Unexpected error in select_accounts: #{e.class}: #{e.message}") render_api_error(t(".unexpected_error"), safe_return_to_path) end def connect_institution if params[:institution_id].blank? || params[:bank_username].blank? || params[:bank_password].blank? redirect_to select_accounts_sophtron_items_path(connection_context_params), alert: t(".missing_parameters") return end item = item_for_institution_connection(@sophtron_item) item.ensure_customer! response = sophtron_response_data!( item.sophtron_provider.create_user_institution( institution_id: params[:institution_id], username: params[:bank_username], password: params[:bank_password], pin: "" ) ).with_indifferent_access job_id = response[:JobID] || response[:job_id] user_institution_id = response[:UserInstitutionID] || response[:user_institution_id] if job_id.blank? || user_institution_id.blank? raise Provider::Sophtron::Error.new("Sophtron did not return JobID and UserInstitutionID", :invalid_response) end item.update!( name: item.name.presence || t("sophtron_items.defaults.name"), institution_id: params[:institution_id], institution_name: params[:institution_name], user_institution_id: user_institution_id, current_job_id: job_id, raw_job_payload: response, job_status: nil, last_connection_error: nil, status: :good ) redirect_to connection_status_sophtron_item_path(item, connection_context_params) rescue Provider::Sophtron::Error => e Rails.logger.error("Sophtron connect institution error: #{e.message}") redirect_to select_accounts_sophtron_items_path(connection_context_params), alert: t(".api_error", message: e.message) end def connection_status if prefetch_request? head :no_content return end if @sophtron_item.current_job_id.blank? redirect_to select_accounts_sophtron_items_path(connection_context_params) return end @poll_attempt = requested_poll_attempt if @poll_attempt > connection_status_max_polls render_connection_timeout return end job = sophtron_response_data!(@sophtron_item.sophtron_provider.get_job_information(@sophtron_item.current_job_id)) @sophtron_item.upsert_job_snapshot!(job) if Provider::Sophtron.job_success?(job) @sophtron_item.update!( current_job_id: nil, last_connection_error: nil, pending_account_setup: true, status: :good ) render_account_selection(@sophtron_item, force_refresh: true) elsif Provider::Sophtron.job_requires_input?(job) @challenge = @sophtron_item.build_mfa_challenge(job) prepare_connection_status_context render :mfa, layout: false elsif post_mfa_polling? && Provider::Sophtron.job_completed?(job) return if render_account_selection_if_accounts_available(@sophtron_item) render_pending_connection_status elsif Provider::Sophtron.job_failed?(job) failure_message = sophtron_connection_failure_message(job) @sophtron_item.update!( current_job_id: nil, user_institution_id: nil, last_connection_error: failure_message, status: :requires_update ) render_institution_connection_error(failure_message) else render_pending_connection_status end rescue Provider::Sophtron::Error => e Rails.logger.error("Sophtron job polling error: #{e.message}") render_api_error(t(".api_error", message: e.message), accounts_path) end def submit_mfa provider = @sophtron_item.sophtron_provider job_id = @sophtron_item.current_job_id case params[:mfa_type] when "security_answer" security_answers = normalized_security_answers unless security_answers redirect_to connection_status_sophtron_item_path(@sophtron_item, connection_context_params), alert: t(".invalid_security_answers") return end sophtron_response_data!(provider.update_job_security_answer(job_id, security_answers)) when "token_choice" sophtron_response_data!(provider.update_job_token_input(job_id, token_choice: params[:token_choice])) when "token_input" sophtron_response_data!(provider.update_job_token_input(job_id, token_input: params[:token_input])) when "verify_phone" sophtron_response_data!(provider.update_job_token_input(job_id, verify_phone_flag: true)) when "captcha" sophtron_response_data!(provider.update_job_captcha(job_id, params[:captcha_input])) else redirect_to connection_status_sophtron_item_path(@sophtron_item, connection_context_params), alert: t(".unknown_challenge") return end redirect_to connection_status_sophtron_item_path(@sophtron_item, connection_context_params.merge(post_mfa: true)) rescue Provider::Sophtron::Error => e Rails.logger.error("Sophtron MFA submission error: #{e.message}") redirect_to connection_status_sophtron_item_path(@sophtron_item, connection_context_params), alert: t(".api_error", message: e.message) end def link_accounts selected_account_ids = params[:account_ids] || [] accountable_type = params[:accountable_type] || "Depository" return_to = safe_return_to_path if selected_account_ids.empty? redirect_to new_account_path, alert: t(".no_accounts_selected") return end item = configured_sophtron_item unless item&.connected_to_institution? redirect_to select_accounts_sophtron_items_path(accountable_type: accountable_type, return_to: return_to), alert: t(".no_institution_connected") return end accounts_data = item.fetch_remote_accounts(force: true) created_accounts = [] already_linked_accounts = [] invalid_accounts = [] selected_account_ids.each do |account_id| account_data = accounts_data.find { |account| SophtronItem.external_account_id(account).to_s == account_id.to_s } next unless account_data if account_data[:account_name].blank? invalid_accounts << account_id Rails.logger.warn "SophtronItemsController - Skipping account #{account_id} with blank name" next end sophtron_account = item.upsert_sophtron_account(account_data) if sophtron_account.account_provider.present? already_linked_accounts << account_data[:account_name] next end ActiveRecord::Base.transaction do account = Account.create_and_sync( { family: Current.family, name: account_data[:account_name], balance: 0, currency: account_data[:currency] || "USD", accountable_type: accountable_type, accountable_attributes: {} }, skip_initial_sync: true ) AccountProvider.create!(account: account, provider: sophtron_account) created_accounts << account end end item.start_initial_load_later if created_accounts.any? redirect_after_account_link(return_to, created_accounts, already_linked_accounts, invalid_accounts) rescue Provider::Sophtron::Error => e redirect_to new_account_path, alert: t(".api_error", message: e.message) end def select_existing_account unless params[:account_id].present? redirect_to accounts_path, alert: t(".no_account_specified") return end @account = Current.family.accounts.find(params[:account_id]) if @account.account_providers.exists? redirect_to accounts_path, alert: t(".account_already_linked") return end item = configured_sophtron_item unless item render_or_redirect_setup_required return end item.ensure_customer! unless item.connected_to_institution? prepare_connection_form(item, account: @account) render :connect, layout: false return end @available_accounts = item.reject_already_linked(item.fetch_remote_accounts) @return_to = safe_return_to_path if @available_accounts.empty? redirect_to accounts_path, alert: t(".all_accounts_already_linked") return end render layout: false rescue Provider::Sophtron::Error => e Rails.logger.error("Sophtron API error in select_existing_account: #{e.message}") render_api_error(t(".api_error", message: e.message), accounts_path) rescue StandardError => e Rails.logger.error("Unexpected error in select_existing_account: #{e.class}: #{e.message}") render_api_error(t(".unexpected_error"), accounts_path) end def link_existing_account account_id = params[:account_id] sophtron_account_id = params[:sophtron_account_id] return_to = safe_return_to_path unless account_id.present? && sophtron_account_id.present? redirect_to accounts_path, alert: t(".missing_parameters") return end account = Current.family.accounts.find(account_id) if account.account_providers.exists? redirect_to accounts_path, alert: t(".account_already_linked") return end item = configured_sophtron_item unless item&.connected_to_institution? redirect_to accounts_path, alert: t(".no_institution_connected") return end account_data = item.fetch_remote_accounts(force: true).find { |remote_account| SophtronItem.external_account_id(remote_account).to_s == sophtron_account_id.to_s } unless account_data redirect_to accounts_path, alert: t(".sophtron_account_not_found") return end if account_data[:account_name].blank? redirect_to accounts_path, alert: t(".invalid_account_name") return end sophtron_account = item.upsert_sophtron_account(account_data) if sophtron_account.account_provider.present? redirect_to accounts_path, alert: t(".sophtron_account_already_linked") return end AccountProvider.create!(account: account, provider: sophtron_account) item.start_initial_load_later redirect_to return_to || accounts_path, notice: t(".success", account_name: account.name) rescue Provider::Sophtron::Error => e Rails.logger.error("Sophtron API error in link_existing_account: #{e.message}") redirect_to accounts_path, alert: t(".api_error", message: e.message) end def new @sophtron_item = Current.family.sophtron_items.build end def create @sophtron_item = Current.family.sophtron_items.build(sophtron_params) @sophtron_item.name ||= t("sophtron_items.defaults.name") if @sophtron_item.save unless verify_and_provision_customer(@sophtron_item) render_sophtron_panel_error(:new, @sophtron_item.last_connection_error) return end render_sophtron_panel_success(:create) else render_sophtron_panel_error(:new, @sophtron_item.errors.full_messages.join(", ")) end end def edit end def update if @sophtron_item.update(sophtron_params) unless verify_and_provision_customer(@sophtron_item) render_sophtron_panel_error(:edit, @sophtron_item.last_connection_error) return end render_sophtron_panel_success(:update) else render_sophtron_panel_error(:edit, @sophtron_item.errors.full_messages.join(", ")) end end def destroy begin @sophtron_item.unlink_all!(dry_run: false) rescue => e Rails.logger.warn("Sophtron unlink during destroy failed: #{e.class} - #{e.message}") end @sophtron_item.destroy_later redirect_to accounts_path, notice: t(".success") end def sync @sophtron_item.sync_later unless @sophtron_item.syncing? respond_to do |format| format.html { redirect_back_or_to accounts_path } format.json { head :ok } end end def setup_accounts @api_error = fetch_sophtron_accounts_from_api @sophtron_accounts = @sophtron_item.sophtron_accounts .left_joins(:account_provider) .where(account_providers: { id: nil }) supported_types = Provider::SophtronAdapter.supported_account_types account_type_keys = { "depository" => "Depository", "credit_card" => "CreditCard", "investment" => "Investment", "loan" => "Loan", "other_asset" => "OtherAsset" } all_account_type_options = account_type_keys.filter_map do |key, type| next unless supported_types.include?(type) [ t(".account_types.#{key}"), type ] end @account_type_options = [ [ t(".account_types.skip"), "skip" ] ] + all_account_type_options @subtype_options = { "Depository" => { label: "Account Subtype:", options: Depository::SUBTYPES.map { |k, v| [ v[:long], k ] } }, "CreditCard" => { label: "", options: [], message: "Credit cards will be automatically set up as credit card accounts." }, "Investment" => { label: "Investment Type:", options: Investment::SUBTYPES.map { |k, v| [ v[:long], k ] } }, "Loan" => { label: "Loan Type:", options: Loan::SUBTYPES.map { |k, v| [ v[:long], k ] } }, "Crypto" => { label: nil, options: [], message: "Crypto accounts track cryptocurrency holdings." }, "OtherAsset" => { label: nil, options: [], message: "No additional options needed for Other Assets." } } end def complete_account_setup account_types = params[:account_types] || {} account_subtypes = params[:account_subtypes] || {} valid_types = Provider::SophtronAdapter.supported_account_types created_accounts = [] skipped_count = 0 begin ActiveRecord::Base.transaction do account_types.each do |sophtron_account_id, selected_type| if selected_type == "skip" || selected_type.blank? skipped_count += 1 next end unless valid_types.include?(selected_type) Rails.logger.warn("Invalid account type '#{selected_type}' submitted for Sophtron account #{sophtron_account_id}") next end sophtron_account = @sophtron_item.sophtron_accounts.find_by(id: sophtron_account_id) unless sophtron_account Rails.logger.warn("Sophtron account #{sophtron_account_id} not found for item #{@sophtron_item.id}") next end if sophtron_account.account_provider.present? Rails.logger.info("Sophtron account #{sophtron_account_id} already linked, skipping") next end selected_subtype = account_subtypes[sophtron_account_id] selected_subtype = "credit_card" if selected_type == "CreditCard" && selected_subtype.blank? account = Account.create_and_sync( { family: Current.family, name: sophtron_account.name, balance: sophtron_account.balance || 0, currency: sophtron_account.currency || "USD", accountable_type: selected_type, accountable_attributes: selected_subtype.present? ? { subtype: selected_subtype } : {} }, skip_initial_sync: true ) AccountProvider.create!(account: account, provider: sophtron_account) created_accounts << account end end rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e Rails.logger.error("Sophtron account setup failed: #{e.class} - #{e.message}") Rails.logger.error(e.backtrace.first(10).join("\n")) flash[:alert] = t(".creation_failed") redirect_to accounts_path, status: :see_other return rescue StandardError => e Rails.logger.error("Sophtron account setup failed unexpectedly: #{e.class} - #{e.message}") Rails.logger.error(e.backtrace.first(10).join("\n")) flash[:alert] = t(".unexpected_error") redirect_to accounts_path, status: :see_other return end @sophtron_item.start_initial_load_later if created_accounts.any? flash[:notice] = if created_accounts.any? t(".success", count: created_accounts.count) elsif skipped_count > 0 t(".all_skipped") else t(".no_accounts") end if turbo_frame_request? @manual_accounts = Account.uncached { Current.family.accounts.visible_manual.order(:name).to_a } @sophtron_items = Current.family.sophtron_items.ordered manual_accounts_stream = if @manual_accounts.any? turbo_stream.update( "manual-accounts", partial: "accounts/index/manual_accounts", locals: { accounts: @manual_accounts } ) else turbo_stream.replace("manual-accounts", view_context.tag.div(id: "manual-accounts")) end render turbo_stream: [ manual_accounts_stream, turbo_stream.replace( ActionView::RecordIdentifier.dom_id(@sophtron_item), partial: "sophtron_items/sophtron_item", locals: { sophtron_item: @sophtron_item } ) ] + Array(flash_notification_stream_items) else redirect_to accounts_path, status: :see_other end end private def configured_sophtron_item Current.family.configured_sophtron_item end def normalized_security_answers raw_answers = Array(params[:security_answers]).flatten return if raw_answers.size > MAX_SECURITY_ANSWERS return if raw_answers.any? { |answer| answer.to_s.length > MAX_SECURITY_ANSWER_LENGTH } answers = raw_answers.filter_map do |answer| answer.to_s.strip.presence end return if answers.empty? answers end def sophtron_response_data!(response) Provider::Sophtron.response_data!(response) end def verify_and_provision_customer(item) provider = item.sophtron_provider raise Provider::Sophtron::Error.new("Sophtron provider is not configured", :configuration_error) unless provider sophtron_response_data!(provider.health_check_auth) item.ensure_customer!(provider: provider) true rescue Provider::Sophtron::Error => e item.update(status: :requires_update, last_connection_error: e.message) Rails.logger.error("Sophtron customer provisioning failed: #{e.message}") false end def render_sophtron_panel_success(action_name) if turbo_frame_request? flash.now[:notice] = t("sophtron_items.#{action_name}.success") @sophtron_items = Current.family.sophtron_items.ordered render turbo_stream: [ turbo_stream.replace( "sophtron-providers-panel", partial: "settings/providers/sophtron_panel", locals: { sophtron_items: @sophtron_items } ), *flash_notification_stream_items ] else redirect_to accounts_path, notice: t("sophtron_items.#{action_name}.success"), status: :see_other end end def render_sophtron_panel_error(view_name, message) @error_message = message if turbo_frame_request? render turbo_stream: turbo_stream.replace( "sophtron-providers-panel", partial: "settings/providers/sophtron_panel", locals: { error_message: @error_message } ), status: :unprocessable_entity else render view_name, status: :unprocessable_entity end end def render_or_redirect_setup_required if turbo_frame_request? render partial: "sophtron_items/setup_required", layout: false else redirect_to settings_providers_path, alert: t("sophtron_items.select_accounts.no_credentials_configured") end end def item_for_institution_connection(item) return item unless connect_new_institution_flow? && should_create_sophtron_item_for_new_institution?(item) Current.family.sophtron_items.create!( name: item.name.presence || t("sophtron_items.defaults.name"), user_id: item.user_id, access_key: item.access_key, base_url: item.base_url, customer_id: item.customer_id, customer_name: item.customer_name, raw_customer_payload: item.raw_customer_payload, sync_start_date: item.sync_start_date ) end def should_create_sophtron_item_for_new_institution?(item) item.user_institution_id.present? || item.current_job_id.present? || item.institution_id.present? || item.institution_name.present? || item.sophtron_accounts.exists? end def prepare_connection_form(item, account: nil) @sophtron_item = item @account = account @accountable_type = params[:accountable_type] || "Depository" @return_to = safe_return_to_path @connect_new_institution = connect_new_institution_flow? @institution_search = params[:institution_name].to_s.strip @institutions = [] if @institution_search.length >= 2 @institutions = sophtron_response_data!(item.sophtron_provider.search_institutions(@institution_search)) end end def render_account_selection(item, force_refresh: false) @available_accounts = item.reject_already_linked(item.fetch_remote_accounts(force: force_refresh)) @accountable_type = params[:accountable_type] || "Depository" @return_to = safe_return_to_path if params[:account_id].present? @account = Current.family.accounts.find(params[:account_id]) render :select_existing_account, layout: false else render :select_accounts, layout: false end end def render_account_selection_if_accounts_available(item) accounts = item.fetch_remote_accounts(force: true) return false if accounts.empty? item.update!( current_job_id: nil, last_connection_error: nil, pending_account_setup: true, status: :good ) @available_accounts = item.reject_already_linked(accounts) @accountable_type = params[:accountable_type] || "Depository" @return_to = safe_return_to_path if params[:account_id].present? @account = Current.family.accounts.find(params[:account_id]) render :select_existing_account, layout: false else render :select_accounts, layout: false end true rescue Provider::Sophtron::Error => e Rails.logger.info("Sophtron accounts are not available after completed job #{item.current_job_id}: #{e.message}") false end def render_pending_connection_status if @poll_attempt >= connection_status_max_polls render_connection_timeout return end prepare_connection_status_context @next_poll_attempt = @poll_attempt + 1 render :connection_status, layout: false end def prepare_connection_status_context @accountable_type = params[:accountable_type] || "Depository" @account_id = params[:account_id] @return_to = safe_return_to_path @poll_interval_ms = CONNECTION_STATUS_POLL_INTERVAL_MS @post_mfa_polling = post_mfa_polling? @max_poll_attempts = connection_status_max_polls end def requested_poll_attempt poll_attempt = params[:poll_attempt].to_i poll_attempt.positive? ? poll_attempt : 1 end def render_connection_timeout @poll_attempt = connection_status_max_polls if @poll_attempt.to_i > connection_status_max_polls @poll_attempt = 1 if @poll_attempt.to_i < 1 @sophtron_item.update!( last_connection_error: t(".timeout"), status: :requires_update ) prepare_connection_status_context @timed_out = true render :connection_status, layout: false end def connection_status_max_polls if post_mfa_polling? POST_MFA_CONNECTION_STATUS_MAX_POLLS elsif login_progress_polling? LOGIN_PROGRESS_CONNECTION_STATUS_MAX_POLLS else CONNECTION_STATUS_MAX_POLLS end end def post_mfa_polling? ActiveModel::Type::Boolean.new.cast(params[:post_mfa]) || post_mfa_job_payload?(@sophtron_item.raw_job_payload) end def post_mfa_job_payload?(job_payload) job = (job_payload || {}).with_indifferent_access job[:TokenInput].present? || %w[TokenInput TransactionTable].include?(job[:LastStep].to_s) end def login_progress_polling? login_progress_job_payload?(@sophtron_item.raw_job_payload) end def login_progress_job_payload?(job_payload) job = (job_payload || {}).with_indifferent_access last_status = job[:LastStatus] || job[:last_status] return false if Provider::Sophtron.failure_job_status?(last_status) job[:LastStep].present? || job[:last_step].present? || last_status.present? end def prefetch_request? [ request.headers["X-Sec-Purpose"], request.headers["Sec-Purpose"], request.headers["Purpose"] ].any? { |value| value.to_s.include?("prefetch") } end def render_institution_connection_error(message) render_api_error( message, select_accounts_sophtron_items_path(connection_context_params.except(:post_mfa, "post_mfa")), heading: t("sophtron_items.api_error.institution_unable_to_connect"), issue_keys: %w[bank_credentials verification_code institution_timeout unsupported_mfa], action_label: t("sophtron_items.api_error.try_again") ) end def sophtron_connection_failure_message(job) last_status = job.with_indifferent_access[:LastStatus].to_s return t("sophtron_items.connection_status.failed_timeout") if last_status.match?(/timeout/i) t("sophtron_items.connection_status.failed") end def render_api_error(message, return_path, heading: nil, issue_keys: nil, action_label: nil) render partial: "sophtron_items/api_error", locals: { error_message: message, return_path: return_path, heading: heading, issue_keys: issue_keys, action_label: action_label }, layout: false end def redirect_after_account_link(return_to, created_accounts, already_linked_accounts, invalid_accounts) if invalid_accounts.any? && created_accounts.empty? && already_linked_accounts.empty? redirect_to new_account_path, alert: t(".invalid_account_names", count: invalid_accounts.count) elsif invalid_accounts.any? && (created_accounts.any? || already_linked_accounts.any?) redirect_to return_to || accounts_path, alert: t(".partial_invalid", created_count: created_accounts.count, already_linked_count: already_linked_accounts.count, invalid_count: invalid_accounts.count) elsif created_accounts.any? && already_linked_accounts.any? redirect_to return_to || accounts_path, notice: t(".partial_success", created_count: created_accounts.count, already_linked_count: already_linked_accounts.count, already_linked_names: already_linked_accounts.join(", ")) elsif created_accounts.any? redirect_to return_to || accounts_path, notice: t(".success", count: created_accounts.count) elsif already_linked_accounts.any? redirect_to return_to || accounts_path, alert: t(".all_already_linked", count: already_linked_accounts.count, names: already_linked_accounts.join(", ")) else redirect_to new_account_path, alert: t(".link_failed") end end def fetch_sophtron_accounts_from_api return nil unless @sophtron_item.sophtron_accounts.empty? return t("sophtron_items.setup_accounts.no_access_key") unless @sophtron_item.credentials_configured? return t("sophtron_items.setup_accounts.no_institution_connected") unless @sophtron_item.connected_to_institution? @sophtron_item.fetch_remote_accounts(force: true) nil rescue Provider::Sophtron::Error => e Rails.logger.error("Sophtron API error: #{e.message}") t("sophtron_items.setup_accounts.api_error") rescue StandardError => e Rails.logger.error("Unexpected error fetching Sophtron accounts: #{e.class}: #{e.message}") Rails.logger.error(e.backtrace.first(10).join("\n")) t("sophtron_items.setup_accounts.api_error") end def set_sophtron_item @sophtron_item = Current.family.sophtron_items.find(params[:id]) end def sophtron_params params.require(:sophtron_item).permit(:name, :user_id, :access_key, :base_url, :sync_start_date) end def connection_context_params params.permit(:accountable_type, :account_id, :return_to, :post_mfa, :connect_new_institution).to_h.compact end def connect_new_institution_flow? ActiveModel::Type::Boolean.new.cast(params[:connect_new_institution]) end def safe_return_to_path return nil if params[:return_to].blank? return_to = params[:return_to].to_s begin uri = URI.parse(return_to) return nil if uri.scheme.present? || uri.host.present? return nil if return_to.start_with?("//") return nil unless return_to.start_with?("/") return_to rescue URI::InvalidURIError nil end end end