mirror of
https://github.com/we-promise/sure.git
synced 2026-09-09 08:34:26 +00:00
* 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>
957 lines
28 KiB
Ruby
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
|