Files
sure/config/routes.rb
T
Igor OliveiraandJuan José Mata 05a787330d Fix Wise incoming transfers by implementing Strong Customer Authentication (#3391)
* Support Wise Strong Customer Authentication for balance statements

The balance-statement endpoint always 403s because it requires a signed
one-time-token challenge (SCA) that Sure never implemented, so every sync
silently fell back to /v1/transfers — an outgoing-only endpoint — meaning
incoming payments into a Wise balance never synced.

Adds a per-item RSA keypair (private key encrypted at rest) that signs the
SCA challenge and retries the statement request once, plus a settings UI
to generate the keypair and register its public key with Wise.

Fixes #3384

* Backfill incoming statements past legacy transfers; fix review nits

Backfill: once statements start succeeding for an account that already has
legacy /v1/transfers rows, the fetch window was clamped to end the day
before the oldest legacy transfer, so the window where incoming payments
were actually missing (the recent window transfers already "covered" with
outgoing-only data) was never re-fetched. Statement rows in that overlap
are now kept when they're incoming and dropped when outgoing, since the
legacy transfer rows already account for the outgoing side.

Also: replace the inline onclick handler on the SCA public key display with
the existing clipboard Stimulus controller (copy button, matching the API
key reveal pattern), and correct the regenerate-keypair confirmation text,
which implied local regeneration revokes the key with Wise -- it doesn't;
the old public key stays valid there until removed manually.

* Avoid double-booking internal cross-currency conversions on statement backfill

The backfilled statement fetch's outgoing/incoming filter only looked at
sign: a positive (credit) statement row was always kept in the legacy
overlap window. But a legacy transfer row can itself be incoming for this
account when it's the target side of a conversion between two of the
profile's own balances -- Wise already fully captures both legs of those
via /v1/transfers, unlike genuine external payments.

Now an incoming statement row in the overlap window is dropped only when
it matches a known incoming legacy transfer's date and amount, so internal
conversions aren't duplicated while external incoming payments (no legacy
counterpart) still backfill correctly.

* Never drop an incoming statement row on a date/amount heuristic

The previous fix dropped an incoming statement row in the legacy-overlap
window when it matched a known incoming legacy transfer's date and amount,
to avoid double-booking internal cross-currency conversions. But nothing
short of an endpoint-proven correlation id can tell that apart from a
genuine external payment that happens to share the same date and amount --
and silently losing a real transaction is worse than an occasional visible,
user-correctable duplicate. Incoming rows are kept unconditionally again.

Instead, bound the exposure at the source: the /v1/transfers fallback now
stops running for an account as soon as it has a successful statement row,
since statements alone cover both directions from then on. This leaves only
a narrow, one-time window (the initial backfill of historical internal
conversions) where a duplicate can occur, rather than an indefinite one.

* Gate the transfer fallback per-account, not per-item

legacy_transfer_import_needed? decides whether to fetch /v1/transfers at
all, but that decision is profile-wide -- true as soon as any one account
still needs the fallback. store_transfers_per_account then merged those
transfers into every currency-matching account by currency alone, with no
check for whether that specific account had already migrated to
statements. A still-legacy account in one currency was enough to make an
already-migrated account in the same currency re-absorb a movement its own
statements already had, double-booked under a different key.

account_transfers is now cleared for any account that already has
statement rows, regardless of why the profile-wide fetch ran.

* Handle SCA controller errors, corrupted keys, and adapter test coverage

- generate_sca_keypair now rescues like every other mutating action in
  this controller, logging and re-rendering the panel with an error
  instead of a raw 500 if the update ever raises.
- sca_configured? now depends on sca_public_key actually parsing, not just
  sca_private_key being present, so a corrupted/unparsable stored key
  (encryption misconfig, manual DB edit) falls back to the "generate a
  keypair" UI state instead of rendering a public key box around nothing.
- Added test/models/provider/wise_adapter_test.rb, which had no coverage
  at all, to cover build_provider's family/wise_item_id resolution and
  that sca_private_key actually reaches the constructed Provider::Wise.

* Add logging to Wise sync

---------

Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-09-05 08:37:32 +02:00

957 lines
28 KiB
Ruby

require "sidekiq/web"
require "sidekiq/cron/web"
Rails.application.routes.draw do
resources :questrade_items, only: [ :index, :new, :create, :show, :edit, :update, :destroy ] do
collection do
get :preload_accounts
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :indexa_capital_items, only: [ :index, :new, :create, :show, :edit, :update, :destroy ] do
collection do
get :preload_accounts
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :mercury_items, only: %i[index new create show edit update destroy] do
collection do
get :preload_accounts
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :wise_items, only: %i[index new create show edit update destroy] do
collection do
get :select_profiles
post :link_profiles
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
post :generate_sca_keypair
end
end
resources :brex_items, only: %i[index new create show edit update destroy] do
collection do
get :preload_accounts, to: "brex_items/account_flows#preload_accounts"
get :select_accounts, to: "brex_items/account_flows#select_accounts"
post :link_accounts, to: "brex_items/account_flows#link_accounts"
get :select_existing_account, to: "brex_items/account_flows#select_existing_account"
post :link_existing_account, to: "brex_items/account_flows#link_existing_account"
end
member do
post :sync
get :setup_accounts, to: "brex_items/account_setups#setup_accounts"
post :complete_account_setup, to: "brex_items/account_setups#complete_account_setup"
end
end
resources :coinbase_items, only: [ :index, :new, :create, :show, :edit, :update, :destroy ] do
collection do
get :preload_accounts
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :binance_items, only: [ :index, :new, :create, :show, :edit, :update, :destroy ] do
collection do
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :kraken_items, only: [ :create, :update, :destroy ] do
collection do
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
# Self-custody / on-chain wallets
resources :onchain_wallet_items, only: [ :update, :destroy ] do
collection do
get :new_wallet
post :preview_wallet
post :link_wallet
post :enable_crypto_prices
end
member do
post :sync
get :manage
get :review_tokens
post :update_tokens
delete :disconnect_wallet
delete :disconnect_asset
end
end
resources :snaptrade_items, only: [ :index, :show, :destroy ] do
collection do
get :preload_accounts
get :select_accounts
get :select_existing_account
post :link_existing_account
get :callback
get :oauth_authorize
get :oauth_callback
get :oauth_device_authorize
post :start_oauth_device_flow
end
member do
post :sync
get :connect
get :setup_accounts
post :complete_account_setup
get :connections
post :complete_oauth_device_flow
delete :delete_connection
end
end
resources :ibkr_items, only: [ :create, :update, :destroy ] do
collection do
get :select_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :trading212_items, only: [ :create, :update, :destroy ] do
collection do
get :select_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
# Trade Republic routes (login steps are TR-specific: web login needs a
# two-phase initiate/confirm handshake with the Trade Republic app)
resources :trade_republic_items, only: [ :show, :create, :update, :destroy ] do
collection do
get :select_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
post :repair
get :setup_accounts
post :complete_account_setup
post :initiate_login
post :complete_login
post :poll_login
post :initiate_qr_login
post :poll_qr_login
post :cancel_qr_login
end
end
# CoinStats routes
resources :coinstats_items, only: [ :index, :new, :create, :update, :destroy ] do
collection do
post :link_wallet
post :link_exchange
end
member do
post :sync
end
end
resources :enable_banking_items, only: [ :new, :create, :update, :destroy ] do
collection do
get :callback
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :select_bank
post :authorize
post :reauthorize
get :setup_accounts
post :complete_account_setup
post :new_connection
end
end
get ".well-known/oauth-protected-resource", to: "oauth_metadata#protected_resource"
get ".well-known/oauth-authorization-server", to: "oauth_metadata#authorization_server"
post "register", to: "oauth_registration#create"
use_doorkeeper do |mapping|
mapping.controllers authorizations: "oauth/authorizations"
end
# MFA routes
resource :mfa, controller: "mfa", only: [ :new, :create ] do
get :verify
post :verify, to: "mfa#verify_code"
post :webauthn_options
post :verify_webauthn
delete :disable
end
mount Lookbook::Engine, at: "/design-system" unless Rails.env.production?
if Rails.env.development?
mount Rswag::Api::Engine => "/api-docs"
mount Rswag::Ui::Engine => "/api-docs"
end
# Break-glass queue tooling. Development mounts it open for convenience;
# everywhere else (production, staging, test) the route only exists for a
# signed-in super admin — see app/constraints/super_admin_constraint.rb.
# An optional basic-auth second layer can be enabled via SIDEKIQ_WEB_USERNAME
# and SIDEKIQ_WEB_PASSWORD (config/initializers/sidekiq.rb).
if Rails.env.development?
mount Sidekiq::Web => "/sidekiq"
else
constraints SuperAdminConstraint.new do
mount Sidekiq::Web => "/sidekiq"
end
end
# AI chats
resources :chats do
resources :messages, only: :create do
member do
# Client-side watchdog reports a "Thinking…" bubble that never received
# a response (e.g. the background worker is down) so it can be failed.
post :report_timeout
end
end
member do
post :retry
end
end
resources :family_exports, only: %i[new create index destroy] do
member do
get :download
post :cancel
end
end
resources :syncs, only: [] do
member do
post :cancel
end
end
get "exports/archive/:token", to: "archived_exports#show", as: :archived_export
get "changelog", to: "pages#changelog"
get "feedback", to: "pages#feedback"
patch "dashboard/preferences", to: "pages#update_preferences"
resource :current_session, only: %i[update]
resource :registration, only: %i[new create]
resources :sessions, only: %i[index new create destroy]
# Passwordless sign-in with a discoverable passkey. Unauthenticated by design;
# rate limited alongside the MFA WebAuthn endpoints in Rack::Attack.
post "/sessions/passkey_options", to: "passkey_sessions#options", as: :passkey_session_options
post "/sessions/passkey", to: "passkey_sessions#create", as: :passkey_session
# Desktop app SSO: opens the flow in the system browser (so passkeys/WebAuthn
# work), then hands a single-use, PKCE-bound code back via the sure:// scheme
# which the desktop webview exchanges for a normal web session.
post "/sessions/desktop_exchange", to: "sessions#desktop_exchange", as: :desktop_sso_exchange
get "/auth/desktop/:provider", to: "sessions#desktop_sso_start"
get "/auth/mobile/:provider", to: "sessions#mobile_sso_start"
match "/auth/:provider/callback", to: "sessions#openid_connect", via: %i[get post]
match "/auth/failure", to: "sessions#failure", via: %i[get post]
get "/auth/logout/callback", to: "sessions#post_logout"
resource :oidc_account, only: [] do
get :link, on: :collection
post :create_link, on: :collection
get :new_user, on: :collection
post :create_user, on: :collection
end
resource :password_reset, only: %i[new create edit update]
resource :password, only: %i[edit update]
resource :email_confirmation, only: :new
resources :users, only: %i[update destroy] do
delete :reset, on: :member
delete :reset_with_sample_data, on: :member
patch :rule_prompt_settings, on: :member
get :resend_confirmation_email, on: :member
end
resource :onboarding, only: :show do
collection do
get :preferences
get :goals
get :trial
end
end
namespace :settings do
resource :profile, only: [ :show, :destroy ]
resource :preferences, only: %i[show update]
resource :budget_shares, only: :update
resource :appearance, only: %i[show update]
resource :debug, only: :show
resource :background_jobs, controller: "background_jobs", only: :show do
post :cancel
end
resource :hosting, only: %i[show update] do
delete :clear_cache, on: :collection
delete :disconnect_external_assistant, on: :collection
end
resource :payment, only: :show
resource :security, only: :show
resources :webauthn_credentials, only: %i[create destroy] do
post :options, on: :collection
end
resources :sso_identities, only: :destroy
resources :api_keys, only: [ :index, :show, :new, :create, :destroy ]
resource :mcp, controller: "mcp", only: :show do
delete "tokens/:token_id", to: "mcp#revoke", as: :revoke_token
end
resource :ai_prompts, only: :show
resource :llm_usage, only: :show
resource :guides, only: :show
get "bank_sync", to: redirect("/settings/providers", status: 301)
resource :providers, only: %i[show update] do
collection do
post :sync_all
post ":provider_key/sync", action: :sync, as: :sync_provider
get ":provider_key/connect_form", action: :connect_form, as: :connect_form
end
end
end
resource :subscription, only: %i[new show create] do
collection do
get :upgrade
get :success
end
end
resources :tags, except: :show do
resources :deletions, only: %i[new create], module: :tag
delete :destroy_all, on: :collection
end
namespace :category do
resource :dropdown, only: :show
end
resources :categories, except: :show do
resources :deletions, only: %i[new create], module: :category
get :merge, on: :collection
post :perform_merge, on: :collection
post :bootstrap, on: :collection
delete :destroy_all, on: :collection
end
resources :reports, only: %i[index] do
patch :update_preferences, on: :collection
get :export_transactions, on: :collection
get :google_sheets_instructions, on: :collection
get :print, on: :collection
get :picker, on: :collection
end
# Hub page fronting budgets + goals under a single "Plan" nav entry.
resource :plan, only: :show
resources :budgets, only: %i[index show edit update], param: :month_year do
post :copy_previous, on: :member
get :picker, on: :collection
resources :budget_categories, only: %i[index show update] do
post :move, on: :collection
end
end
resources :goals do
member do
patch :pause
patch :resume
patch :complete
patch :archive
patch :unarchive
patch :reopen
# Two actions on one path rather than one action branching on the verb:
# HEAD routes like GET but `request.get?` is false for it, so a branch
# would send a HEAD request down the write path. A goal can be partly
# spent more than once, so the write is a POST, not a PATCH on the goal.
get :consume
post :consume, action: :record_consumption, as: nil
end
resources :pledges, only: %i[new create destroy], controller: "goal_pledges" do
member do
patch :renew
end
end
end
resources :family_merchants, only: %i[index new create edit update destroy] do
collection do
get :merge
post :perform_merge
post :enhance
end
end
get :exchange_rate, to: "exchange_rates#show"
resources :transfers, only: %i[new create destroy show update] do
member do
post :mark_as_recurring
patch :tags, action: :update_tags
end
end
resources :imports, only: %i[index new show create update destroy] do
member do
post :publish
put :revert
put :apply_template
post :cancel
get :summary
end
resource :upload, only: %i[show update], module: :import
resource :configuration, only: %i[show update], module: :import
resource :clean, only: :show, module: :import
resource :confirm, only: :show, module: :import
resource :qif_category_selection, only: %i[show update], module: :import
resources :rows, only: %i[show update], module: :import
resources :mappings, only: :update, module: :import
end
resources :holdings, only: %i[index new show update destroy] do
member do
post :unlock_cost_basis
patch :remap_security
post :reset_security
post :sync_prices
end
end
resources :trades, only: %i[show new create update destroy] do
member do
post :unlock
end
end
resources :valuations, only: %i[show new create update destroy] do
post :confirm_create, on: :collection
post :confirm_update, on: :member
end
namespace :transactions do
resource :bulk_deletion, only: :create
resource :bulk_update, only: %i[new create]
resource :categorize, only: %i[show create] do
patch :assign_entry, on: :collection
get :preview_rule, on: :collection
end
end
resources :transactions, only: %i[index new create show update destroy] do
resource :split, only: %i[new create edit update destroy]
resource :transfer_match, only: %i[new create]
resource :pending_duplicate_merges, only: %i[new create]
resource :category, only: :update, controller: :transaction_categories
resources :attachments, only: %i[show create destroy], controller: :transaction_attachments
collection do
delete :clear_filter
end
member do
get :convert_to_trade
post :create_trade_from_transaction
post :mark_as_recurring
post :merge_duplicate
post :dismiss_duplicate
post :unlock
patch :tags, action: :update_tags
end
end
resources :bills, only: %i[index show] do
# POST for the same reason recurring_transactions#identify is: detection
# mutates (creates suggested series and occurrences), so it stays behind
# CSRF protection rather than a plain URL.
collection do
post :detect
post :ai_review, to: "bills/ai_reviews#create"
post :reset_feed_token
end
member do
get :smart_configuration, to: "bills/smart_configurations#show"
end
end
get "bills_feed/:token", to: "bills_feeds#show", as: :bills_feed, defaults: { format: :ics }
resources :recurring_occurrences, only: %i[show] do
member do
post :mark_paid
post :skip
post :reopen
patch :snooze
patch :override_amount
end
resources :allocations, controller: :recurring_allocations, only: %i[create]
end
resources :recurring_allocations, only: %i[destroy] do
member do
post :confirm
post :reject
end
end
resources :recurring_transactions, only: %i[index new create edit update destroy] do
collection do
# POST only: all three mutate. They accepted GET while DS::Link's method
# option was inert, which left destructive work sitting behind a plain
# URL and outside CSRF protection. Every call site passes method: :post.
post :identify
post :cleanup
post :smart_fill, to: "recurring_transactions/smart_fills#create"
patch :update_settings
end
member do
post :toggle_status
post :confirm
post :dismiss
end
end
resources :insights, only: %i[index] do
collection do
post :refresh
end
member do
patch :acknowledge
patch :unacknowledge
end
end
resources :accountable_sparklines, only: :show, param: :accountable_type
direct :entry do |entry, options|
if entry.new_record?
route_for entry.entryable_name.pluralize, options
else
route_for entry.entryable_name, entry, options
end
end
resources :rules, except: :show do
member do
get :confirm
post :apply
end
collection do
delete :destroy_all
get :confirm_all
post :apply_all
post :clear_ai_cache
end
end
resources :accounts, only: %i[index new show destroy], shallow: true do
member do
post :sync
get :sparkline
patch :toggle_active
patch :toggle_exclude_from_reports
patch :set_default
patch :remove_default
get :select_provider
get :confirm_unlink
delete :unlink
end
collection do
post :sync_all
end
resource :sharing, only: [ :show, :update ], controller: "account_sharings"
end
resources :account_statements, only: %i[index show create update destroy] do
member do
patch :link
patch :unlink
patch :reject
end
end
# Convenience routes for polymorphic paths
# Example: account_path(Account.new(accountable: Depository.new)) => /depositories/123
direct :edit_account do |model, options|
route_for "edit_#{model.accountable_name}", model, options
end
resources :depositories, only: %i[new create edit update]
resources :investments, only: %i[new create edit update]
resources :properties, only: %i[new create edit update] do
member do
get :balances
patch :update_balances
get :address
patch :update_address
end
end
resources :vehicles, only: %i[new create edit update]
resources :credit_cards, only: %i[new create edit update]
resources :loans, only: %i[new create edit update]
resources :cryptos, only: %i[new create edit update]
resources :other_assets, only: %i[new create edit update]
resources :other_liabilities, only: %i[new create edit update]
resources :securities, only: :index
resources :invite_codes, only: %i[index create destroy]
resources :invitations, only: [ :new, :create, :destroy ] do
get :accept, on: :member
end
# API routes
namespace :api do
namespace :v1 do
# Authentication endpoints
post "auth/signup", to: "auth#signup"
post "auth/login", to: "auth#login"
post "auth/refresh", to: "auth#refresh"
post "auth/sso_exchange", to: "auth#sso_exchange"
post "auth/sso_link", to: "auth#sso_link"
post "auth/sso_create_account", to: "auth#sso_create_account"
patch "auth/enable_ai", to: "auth#enable_ai"
# Production API endpoints
resources :accounts, only: [ :index, :show ]
resources :balances, only: [ :index, :show ]
resources :budgets, only: [ :index, :show ]
resources :budget_categories, only: [ :index, :show ]
resources :categories, only: [ :index, :show, :create ]
resources :merchants, only: [ :index, :show, :create ]
resources :rules, only: [ :index, :show ]
resources :rule_runs, only: [ :index, :show ]
resources :securities, only: [ :index, :show ]
resources :security_prices, only: [ :index, :show ]
resources :tags, only: [ :index, :show, :create, :update, :destroy ]
resources :transactions, only: [ :index, :show, :create, :update, :destroy ]
resources :trades, only: [ :index, :show, :create, :update, :destroy ]
resources :holdings, only: [ :index, :show ]
resources :transfers, only: [ :index, :show ]
resources :rejected_transfers, only: [ :index, :show ]
resources :valuations, only: [ :index, :create, :update, :show ]
resources :recurring_transactions, only: [ :index, :show, :create, :update, :destroy ]
resources :family_exports, only: [ :index, :show, :create ] do
get :download, on: :member
end
resources :imports, only: [ :index, :show, :create ] do
post :preflight, on: :collection
get :rows, on: :member
end
resources :import_sessions, only: [ :show, :create ] do
post :chunks, on: :member, action: :create_chunk
post :publish, on: :member
end
resource :usage, only: [ :show ], controller: :usage
resource :balance_sheet, only: [ :show ], controller: :balance_sheet
resources :insights, only: [ :index ]
resources :push_subscriptions, only: [ :create, :destroy ]
resource :family_settings, only: [ :show ], controller: :family_settings
post :sync, to: "sync#create", as: :sync_job
resources :syncs, only: [ :index, :show ] do
get :latest, on: :collection
end
resources :provider_connections, only: [ :index ]
resources :chats, only: [ :index, :show, :create, :update, :destroy ] do
resources :messages, only: [ :create ] do
post :retry, on: :collection
end
end
get "users/reset/status", to: "users#reset_status"
delete "users/reset", to: "users#reset"
delete "users/me", to: "users#destroy"
# Test routes for API controller testing (only available in test environment)
if Rails.env.test?
get "test", to: "test#index"
get "test_not_found", to: "test#not_found"
get "test_family_access", to: "test#family_access"
get "test_scope_required", to: "test#scope_required"
get "test_multiple_scopes_required", to: "test#multiple_scopes_required"
end
end
end
resources :currencies, only: %i[show]
resources :impersonation_sessions, only: [ :create ] do
post :join, on: :collection
delete :leave, on: :collection
member do
put :approve
put :reject
put :complete
end
end
resources :plaid_items, only: %i[new edit create destroy] do
collection do
get :select_existing_account
post :link_existing_account
end
member do
post :sync
end
end
resources :simplefin_items, only: %i[index new create show edit update destroy] do
collection do
get :select_existing_account
post :link_existing_account
end
member do
post :sync
post :balances
get :setup_accounts
post :complete_account_setup
post :dismiss_replacement_suggestion
end
end
resources :lunchflow_items, only: %i[index new create show edit update destroy] do
collection do
get :preload_accounts
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :redbark_items, only: %i[create update destroy] do
collection do
get :select_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :akahu_items, only: %i[index new create show edit update destroy] do
collection do
get :preload_accounts
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :up_items, only: %i[index new create show edit update destroy] do
collection do
get :preload_accounts
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :sync
get :setup_accounts
post :complete_account_setup
end
end
resources :sophtron_items, only: %i[index new create show edit update destroy] do
collection do
get :preload_accounts
get :select_accounts
post :link_accounts
get :select_existing_account
post :link_existing_account
end
member do
post :connect_institution
post :sync
post :toggle_manual_sync
post :balances
get :connection_status
post :submit_mfa
get :setup_accounts
post :complete_account_setup
end
end
namespace :webhooks do
post "plaid"
post "plaid_eu"
post "stripe"
end
get "redis-configuration-error", to: "pages#redis_configuration_error"
# MCP server endpoint for external AI assistants (JSON-RPC 2.0)
post "mcp", to: "mcp#handle"
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
# Render dynamic PWA files from app/views/pwa/*
get "service-worker" => "pwa#service_worker", as: :pwa_service_worker, defaults: { format: :js }
get "manifest" => "pwa#manifest", as: :pwa_manifest, defaults: { format: :json }
get "imports/:import_id/upload/sample_csv", to: "import/uploads#sample_csv", as: :import_upload_sample_csv
privacy_url = ENV["LEGAL_PRIVACY_URL"].presence
terms_url = ENV["LEGAL_TERMS_URL"].presence
get "privacy", to: privacy_url ? redirect(privacy_url) : "pages#privacy"
get "terms", to: terms_url ? redirect(terms_url) : "pages#terms"
get "intro", to: "pages#intro"
# Admin namespace for super admin functionality
namespace :admin do
resources :sso_providers do
member do
patch :toggle
post :test_connection
end
end
resources :users, only: [ :index, :update, :destroy ] do
get :deletion, on: :member
end
resources :sso_identity_blocks, only: [ :destroy ]
resources :invitations, only: [ :destroy ]
resources :families, only: [ :destroy ] do
member do
delete :invitations, to: "invitations#destroy_all"
end
end
# Singular `resource :system_health` would otherwise route to
# `Admin::SystemHealthsController` (Rails pluralizes the controller
# name even for singular resources, unlike its plural siblings above
# that happen to round-trip cleanly). The controller file is singular,
# so name it explicitly.
resource :system_health, only: :show, controller: "system_health" do
post :verify_worker_ai
end
end
# Defines the root path route ("/")
root "pages#dashboard"
end