diff --git a/Gemfile b/Gemfile index 075e52a1e..64dd5099e 100644 --- a/Gemfile +++ b/Gemfile @@ -60,6 +60,7 @@ gem "countries" # OAuth & API Security gem "doorkeeper" gem "rack-attack", "~> 6.6" +gem "rack-cors" gem "pundit" gem "faraday" gem "faraday-retry" diff --git a/Gemfile.lock b/Gemfile.lock index 540bb38c4..dfc8ee3c9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -477,6 +477,9 @@ GEM rack (3.1.18) rack-attack (6.7.0) rack (>= 1.0, < 4) + rack-cors (3.0.0) + logger + rack (>= 3.0.14) rack-mini-profiler (4.0.0) rack (>= 1.2.0) rack-oauth2 (2.2.1) @@ -822,6 +825,7 @@ DEPENDENCIES puma (>= 5.0) pundit rack-attack (~> 6.6) + rack-cors rack-mini-profiler rails (~> 7.2.2) rails-settings-cached diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 000000000..8ffb294d4 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# CORS configuration for API access from mobile clients (Flutter) and other external apps. +# +# This enables Cross-Origin Resource Sharing for the /api, /oauth, and /sessions endpoints, +# allowing the Flutter mobile client and other authorized clients to communicate +# with the Rails backend. + +Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + # Allow requests from any origin for API endpoints + # Mobile apps and development environments need flexible CORS + origins "*" + + # API endpoints for mobile client and third-party integrations + resource "/api/*", + headers: :any, + methods: %i[get post put patch delete options head], + expose: %w[X-Request-Id X-Runtime], + max_age: 86400 + + # OAuth endpoints for authentication flows + resource "/oauth/*", + headers: :any, + methods: %i[get post put patch delete options head], + expose: %w[X-Request-Id X-Runtime], + max_age: 86400 + + # Session endpoints for webview-based authentication + resource "/sessions/*", + headers: :any, + methods: %i[get post delete options head], + expose: %w[X-Request-Id X-Runtime], + max_age: 86400 + end +end diff --git a/test/integration/cors_test.rb b/test/integration/cors_test.rb new file mode 100644 index 000000000..e16d98d3c --- /dev/null +++ b/test/integration/cors_test.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "test_helper" + +class CorsTest < ActionDispatch::IntegrationTest + test "rack cors is configured in middleware stack" do + middleware_classes = Rails.application.middleware.map(&:klass) + assert_includes middleware_classes, Rack::Cors, "Rack::Cors should be in middleware stack" + end + + test "cors headers are returned for api endpoints" do + get "/api/v1/usage", headers: { "Origin" => "http://localhost:3000" } + + assert_equal "*", response.headers["Access-Control-Allow-Origin"] + assert response.headers["Access-Control-Expose-Headers"].present? + end + + test "cors preflight request is handled for api endpoints" do + # Simulate a preflight OPTIONS request + options "/api/v1/transactions", + headers: { + "Origin" => "http://localhost:3000", + "Access-Control-Request-Method" => "POST", + "Access-Control-Request-Headers" => "Content-Type, Authorization" + } + + assert_response :ok + assert_equal "*", response.headers["Access-Control-Allow-Origin"] + assert response.headers["Access-Control-Allow-Methods"].present? + assert_includes response.headers["Access-Control-Allow-Methods"], "POST" + end + + test "cors headers are returned for oauth endpoints" do + post "/oauth/token", + params: { grant_type: "authorization_code", code: "test" }, + headers: { "Origin" => "http://localhost:3000" } + + assert_equal "*", response.headers["Access-Control-Allow-Origin"] + end + + test "cors preflight request is handled for oauth endpoints" do + options "/oauth/token", + headers: { + "Origin" => "http://localhost:3000", + "Access-Control-Request-Method" => "POST", + "Access-Control-Request-Headers" => "Content-Type" + } + + assert_response :ok + assert_equal "*", response.headers["Access-Control-Allow-Origin"] + end + + test "cors headers are returned for session endpoints" do + post "/sessions", + params: { email: "test@example.com", password: "password" }, + headers: { "Origin" => "http://localhost:3000" } + + assert_equal "*", response.headers["Access-Control-Allow-Origin"] + end + + test "cors preflight request is handled for session endpoints" do + options "/sessions/new", + headers: { + "Origin" => "http://localhost:3000", + "Access-Control-Request-Method" => "GET", + "Access-Control-Request-Headers" => "Content-Type" + } + + assert_response :ok + assert_equal "*", response.headers["Access-Control-Allow-Origin"] + end +end