From aa90aa3b767bbb68efa1c3ad358f6b62077a554b Mon Sep 17 00:00:00 2001 From: compwron Date: Sun, 9 Aug 2026 22:48:42 +0000 Subject: [PATCH 01/16] Pin active_model_serializers to the 0.9 series The constraint `~> 0.9` also permits 0.10, so any `bundle update` would silently pull in active_model_serializers 0.10. That release is a rewrite with a different JSON output format, and the Ember client consumes the 0.9 format via ActiveModelAdapter and EmbeddedRecordsMixin, so the jump would break the API contract without any change to app code. Moving to 0.10 means changing the serializers (24 call sites still use the 0.9-only `embed:`/`embed_in_root:`/`self.root` API) and the frontend together. Pin to `~> 0.9.8` so that stays a deliberate decision. Co-Authored-By: Claude Opus 5 (1M context) --- backend/Gemfile | 6 ++++-- backend/Gemfile.lock | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/Gemfile b/backend/Gemfile index 7d2a28f7..dee40c51 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -10,8 +10,10 @@ gem "rails", "~> 7.1.0" gem "rake" gem "sprockets-rails" -# JSON serializer -gem "active_model_serializers", "~> 0.9" +# JSON serializer. Pinned to the 0.9 series on purpose: `~> 0.9` would allow +# 0.10, which is a rewrite with a different JSON format that the Ember client +# (ActiveModelAdapter + EmbeddedRecordsMixin) cannot consume. +gem "active_model_serializers", "~> 0.9.8" # Use postgresql and mongo as the database for Active Record gem "mongoid", "8.1.3" # https://www.mongodb.com/docs/mongoid/current/reference/compatibility/#rails-compatibility diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index 3ffebe8a..bfb1e380 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -516,7 +516,7 @@ PLATFORMS ruby DEPENDENCIES - active_model_serializers (~> 0.9) + active_model_serializers (~> 0.9.8) annotate awesome_print better_errors From 61aa1540c96037ab0acf7719f7e7ec69f309c11b Mon Sep 17 00:00:00 2001 From: compwron Date: Sun, 9 Aug 2026 22:52:26 +0000 Subject: [PATCH 02/16] Replace Rails.application.secrets with ENV reads `Rails.application.secrets` is deprecated in Rails 7.1 and removed in 7.2, so this has to go before the framework can be upgraded. Every value in config/secrets.yml was already just an ENV read, and ENV is what the rest of the app uses for configuration (27 call sites, including ENV["BASE_URL"] in notifications_mailer two lines below one of the calls replaced here), so the secrets indirection bought nothing. secret_key_base is unaffected in production: Rails resolves ENV["SECRET_KEY_BASE"] ahead of secrets.yml, and the production block only interpolated that variable anyway. Development and test fall back to Rails' generated local secret. The two literals in the `test:` block move to config/environments/test.rb. They are test fixtures rather than developer configuration: the Tomorrow.io key has to match the URI recorded in the WeatherRetriever VCR cassettes, and the from-address is asserted directly in the mailer specs. Keeping them in the environment file means the suite still runs without any .env setup, in CI and locally. They are assigned when blank rather than when nil because env-example ships SMTP_EMAIL_FROM with an empty value. Co-Authored-By: Claude Opus 5 (1M context) --- backend/app/jobs/notes_export_job.rb | 2 +- backend/app/mailers/application_mailer.rb | 2 +- .../app/mailers/checkin_reminder_mailer.rb | 4 +-- backend/app/mailers/notifications_mailer.rb | 2 +- backend/app/mailers/top_posts_mailer.rb | 2 +- backend/config/environments/test.rb | 12 +++++++ backend/config/initializers/tomorrowio_rb.rb | 2 +- backend/config/secrets.yml | 32 ------------------- .../mailers/checkin_reminder_mailer_spec.rb | 2 +- 9 files changed, 20 insertions(+), 40 deletions(-) delete mode 100644 backend/config/secrets.yml diff --git a/backend/app/jobs/notes_export_job.rb b/backend/app/jobs/notes_export_job.rb index 9379bd8a..27c6a329 100644 --- a/backend/app/jobs/notes_export_job.rb +++ b/backend/app/jobs/notes_export_job.rb @@ -11,7 +11,7 @@ def perform(user_id) body = note_details.map { |detail| detail.join(",") } ActionMailer::Base.mail( - from: Rails.application.secrets.smtp_email_from, + from: ENV["SMTP_EMAIL_FROM"], to: user.email, subject: "Flaredown data export", body: body diff --git a/backend/app/mailers/application_mailer.rb b/backend/app/mailers/application_mailer.rb index 0f6dcd12..b49f75c7 100644 --- a/backend/app/mailers/application_mailer.rb +++ b/backend/app/mailers/application_mailer.rb @@ -1,5 +1,5 @@ class ApplicationMailer < ActionMailer::Base - default from: Rails.application.secrets.smtp_email_from + default from: ENV["SMTP_EMAIL_FROM"] REGEXP = /\A\s*([-\p{L}\d+._]{1,64})@((?:[-\p{L}\d]+\.)+\p{L}{2,})\s*\z/i diff --git a/backend/app/mailers/checkin_reminder_mailer.rb b/backend/app/mailers/checkin_reminder_mailer.rb index c58ab181..b48263ae 100644 --- a/backend/app/mailers/checkin_reminder_mailer.rb +++ b/backend/app/mailers/checkin_reminder_mailer.rb @@ -11,9 +11,9 @@ def remind(notification_hash) return unless notify_token return if user&.rejected_type.present? # Rejected via AWS SES - @click_here_link = Rails.application.secrets.base_url + @click_here_link = ENV["BASE_URL"] @unsubscribe_link = - Rails.application.secrets.base_url + "/unsubscribe/#{User.find_by(email: @email).notify_token}?stop_remind" + ENV["BASE_URL"] + "/unsubscribe/#{User.find_by(email: @email).notify_token}?stop_remind" attachments.inline["optional_email_img.png"] = File.read("public/images/optional_email_img.png") mail(to: @email, subject: I18n.t("checkin_reminder_mailer.subject")) diff --git a/backend/app/mailers/notifications_mailer.rb b/backend/app/mailers/notifications_mailer.rb index 9b9fc1d5..3c50a874 100644 --- a/backend/app/mailers/notifications_mailer.rb +++ b/backend/app/mailers/notifications_mailer.rb @@ -7,7 +7,7 @@ def notify(notification_hash) @email = notification_hash[:email] return unless valid_email?(@email) - @unsubscribe_link = Rails.application.secrets.base_url + "/unsubscribe/#{User.find_by(email: @email).notify_token}" + @unsubscribe_link = ENV["BASE_URL"] + "/unsubscribe/#{User.find_by(email: @email).notify_token}" @data = notification_hash[:data] mail(to: @email, subject: "New response to your Flaredown message") diff --git a/backend/app/mailers/top_posts_mailer.rb b/backend/app/mailers/top_posts_mailer.rb index ab283545..350879e4 100644 --- a/backend/app/mailers/top_posts_mailer.rb +++ b/backend/app/mailers/top_posts_mailer.rb @@ -6,7 +6,7 @@ def notify(mailer_hash) return unless valid_email?(@email) @unsubscribe_link = - Rails.application.secrets.base_url + "/unsubscribe/#{mailer_hash[:notify_token]}?notify_top_posts=false" + ENV["BASE_URL"] + "/unsubscribe/#{mailer_hash[:notify_token]}?notify_top_posts=false" @top_posts = Post.where(:_type => "Post", :_id.in => mailer_hash[:top_posts_ids]) mail(to: @email, subject: 'Weekly "top posts"') diff --git a/backend/config/environments/test.rb b/backend/config/environments/test.rb index 772fdeec..edc5bdfa 100644 --- a/backend/config/environments/test.rb +++ b/backend/config/environments/test.rb @@ -1,5 +1,17 @@ require "active_support/core_ext/integer/time" +# Values the `test:` block of config/secrets.yml used to supply. Set here rather +# than in a .env file so the suite runs the same way locally, in Docker and in +# CI. This file is loaded before config/initializers, which is where +# TOMORROW_IO_KEY is read. The API key is not a real one: it has to match the +# URI recorded in the WeatherRetriever VCR cassettes. +# +# Assigned when blank rather than when nil: env-example ships SMTP_EMAIL_FROM +# with an empty value, so a developer's .env sets it to "" rather than leaving +# it unset. +ENV["TOMORROW_IO_KEY"] = "MY_MEGA_TOMORROW_IO_KEY" if ENV["TOMORROW_IO_KEY"].blank? +ENV["SMTP_EMAIL_FROM"] = "from@some.email" if ENV["SMTP_EMAIL_FROM"].blank? + # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped diff --git a/backend/config/initializers/tomorrowio_rb.rb b/backend/config/initializers/tomorrowio_rb.rb index 3ebaf6c5..c1e2c4c9 100644 --- a/backend/config/initializers/tomorrowio_rb.rb +++ b/backend/config/initializers/tomorrowio_rb.rb @@ -1 +1 @@ -Tomorrowiorb.api_key = Rails.application.secrets.tomorrow_io_key +Tomorrowiorb.api_key = ENV["TOMORROW_IO_KEY"] diff --git a/backend/config/secrets.yml b/backend/config/secrets.yml deleted file mode 100644 index 97b476f0..00000000 --- a/backend/config/secrets.yml +++ /dev/null @@ -1,32 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key is used for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! - -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. - -# Make sure the secrets in this file are kept private -# if you're sharing your code publicly. - -common: &common - tomorrow_io_key: <%= ENV['TOMORROW_IO_KEY'] %> - smtp_email_from: <%= ENV['SMTP_EMAIL_FROM'] %> - base_url: <%= ENV['BASE_URL'] %> - -development: - <<: *common - secret_key_base: 27171ba40cad58521a51000b5e2f74f527a00765ce2041c7c9f903901d3d1dc57b19979e74ad9a46f9fb0044890bf7a6129f599718dfbceb66a569805da816fa - -test: - <<: *common - tomorrow_io_key: 'MY_MEGA_TOMORROW_IO_KEY' - secret_key_base: c72c35331a3c6ab7e521ee57c867bdb2a081150064e16f7e83f9455f135d1efe72db2d7a7cc43ff9d671de3ae2afd17fc9aee9d823a695940f7f3cbca8c67116 - smtp_email_from: 'from@some.email' - -# Do not keep production secrets in the repository, -# instead read values from the environment. -production: - <<: *common - secret_key_base: <%= ENV['SECRET_KEY_BASE'] %> diff --git a/backend/spec/mailers/checkin_reminder_mailer_spec.rb b/backend/spec/mailers/checkin_reminder_mailer_spec.rb index 5c644d65..28c2e180 100644 --- a/backend/spec/mailers/checkin_reminder_mailer_spec.rb +++ b/backend/spec/mailers/checkin_reminder_mailer_spec.rb @@ -12,7 +12,7 @@ end it "renders the body" do - body = I18n.t("checkin_reminder_mailer.body.text", base_url: Rails.application.secrets.base_url) + body = I18n.t("checkin_reminder_mailer.body.text", base_url: ENV["BASE_URL"]) expect(mail.body.encoded.strip.tr('\"', "")).to match(body) end From 87da3b0805fd653ee074e5e6e339e3cc0231ca7b Mon Sep 17 00:00:00 2001 From: compwron Date: Sun, 9 Aug 2026 22:53:41 +0000 Subject: [PATCH 03/16] Remove the redundant 7.1 framework defaults initializer config/initializers/new_framework_defaults_7_1.rb is the leftover scaffold from the upgrade to 7.1. Every line in it is uncommented, which is what the file is for during an upgrade, but config/application.rb has since been moved to `load_defaults 7.1` and now applies all 23 of those settings itself. Dumping each setting with and without the file gives identical output. It is not harmless to leave behind: it assigns active_record.allow_deprecated_singular_associations_name, which Rails 8.1 removes, so the app fails to boot on 8.1 with the file in place. Co-Authored-By: Claude Opus 5 (1M context) --- .../new_framework_defaults_7_1.rb | 288 ------------------ 1 file changed, 288 deletions(-) delete mode 100644 backend/config/initializers/new_framework_defaults_7_1.rb diff --git a/backend/config/initializers/new_framework_defaults_7_1.rb b/backend/config/initializers/new_framework_defaults_7_1.rb deleted file mode 100644 index 27d1fab2..00000000 --- a/backend/config/initializers/new_framework_defaults_7_1.rb +++ /dev/null @@ -1,288 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file eases your Rails 7.1 framework defaults upgrade. -# -# Uncomment each configuration one by one to switch to the new default. -# Once your application is ready to run with all new defaults, you can remove -# this file and set the `config.load_defaults` to `7.1`. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html -# https://guides.rubyonrails.org/configuring.html#default-values-for-target-version-7-1 - -### -# No longer add autoloaded paths into `$LOAD_PATH`. This means that you won't be able -# to manually require files that are managed by the autoloader, which you shouldn't do anyway. -# -# This will reduce the size of the load path, making `require` faster if you don't use bootsnap, or reduce the size -# of the bootsnap cache if you use it. -# -# To set this configuration, add the following line to `config/application.rb` (NOT this file): -# config.add_autoload_paths_to_load_path = false (DONE) - -### -# Remove the default X-Download-Options headers since it is used only by Internet Explorer. -# If you need to support Internet Explorer, add back `"X-Download-Options" => "noopen"`. -#++ -Rails.application.config.action_dispatch.default_headers = { - "X-Frame-Options" => "SAMEORIGIN", - "X-XSS-Protection" => "0", - "X-Content-Type-Options" => "nosniff", - "X-Permitted-Cross-Domain-Policies" => "none", - "Referrer-Policy" => "strict-origin-when-cross-origin" -} - -### -# Do not treat an `ActionController::Parameters` instance -# as equal to an equivalent `Hash` by default. -#++ -Rails.application.config.action_controller.allow_deprecated_parameters_hash_equality = false - -### -# Active Record Encryption now uses SHA-256 as its hash digest algorithm. -# -# There are 3 scenarios to consider. -# -# 1. If you have data encrypted with previous Rails versions, and you have -# +config.active_support.key_generator_hash_digest_class+ configured as SHA1 (the default -# before Rails 7.0), you need to configure SHA-1 for Active Record Encryption too: -#++ -# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA1 -# -# 2. If you have +config.active_support.key_generator_hash_digest_class+ configured as SHA256 (the new default -# in 7.0), then you need to configure SHA-256 for Active Record Encryption: -#++ -# Rails.application.config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256 -# -# 3. If you don't currently have data encrypted with Active Record encryption, you can disable this setting to -# configure the default behavior starting 7.1+: -#++ -# - NOTE: I don't see `key_generator_hash_digest_class`` or `encrypts` used anywhwere -# - If app is using Active Record Encryption, will need to do 1 or 2 above -Rails.application.config.active_record.encryption.support_sha1_for_non_deterministic_encryption = false - -### -# No longer run after_commit callbacks on the first of multiple Active Record -# instances to save changes to the same database row within a transaction. -# Instead, run these callbacks on the instance most likely to have internal -# state which matches what was committed to the database, typically the last -# instance to save. -#++ -Rails.application.config.active_record.run_commit_callbacks_on_first_saved_instances_in_transaction = false - -### -# Configures SQLite with a strict strings mode, which disables double-quoted string literals. -# -# SQLite has some quirks around double-quoted string literals. -# It first tries to consider double-quoted strings as identifier names, but if they don't exist -# it then considers them as string literals. Because of this, typos can silently go unnoticed. -# For example, it is possible to create an index for a non existing column. -# See https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted for more details. -#++ -Rails.application.config.active_record.sqlite3_adapter_strict_strings_by_default = true - -### -# Disable deprecated singular associations names. -#++ -Rails.application.config.active_record.allow_deprecated_singular_associations_name = false - -### -# Enable the Active Job `BigDecimal` argument serializer, which guarantees -# roundtripping. Without this serializer, some queue adapters may serialize -# `BigDecimal` arguments as simple (non-roundtrippable) strings. -# -# When deploying an application with multiple replicas, old (pre-Rails 7.1) -# replicas will not be able to deserialize `BigDecimal` arguments from this -# serializer. Therefore, this setting should only be enabled after all replicas -# have been successfully upgraded to Rails 7.1. -#++ -Rails.application.config.active_job.use_big_decimal_serializer = true - -### -# Specify if an `ArgumentError` should be raised if `Rails.cache` `fetch` or -# `write` are given an invalid `expires_at` or `expires_in` time. -# Options are `true`, and `false`. If `false`, the exception will be reported -# as `handled` and logged instead. -#++ -Rails.application.config.active_support.raise_on_invalid_cache_expiration_time = true - -### -# Specify whether Query Logs will format tags using the SQLCommenter format -# (https://open-telemetry.github.io/opentelemetry-sqlcommenter/), or using the legacy format. -# Options are `:legacy` and `:sqlcommenter`. -#++ -Rails.application.config.active_record.query_log_tags_format = :sqlcommenter - -### -# Specify the default serializer used by `MessageEncryptor` and `MessageVerifier` -# instances. -# -# The legacy default is `:marshal`, which is a potential vector for -# deserialization attacks in cases where a message signing secret has been -# leaked. -# -# In Rails 7.1, the new default is `:json_allow_marshal` which serializes and -# deserializes with `ActiveSupport::JSON`, but can fall back to deserializing -# with `Marshal` so that legacy messages can still be read. -# -# In Rails 7.2, the default will become `:json` which serializes and -# deserializes with `ActiveSupport::JSON` only. -# -# Alternatively, you can choose `:message_pack` or `:message_pack_allow_marshal`, -# which serialize with `ActiveSupport::MessagePack`. `ActiveSupport::MessagePack` -# can roundtrip some Ruby types that are not supported by JSON, and may provide -# improved performance, but it requires the `msgpack` gem. -# -# For more information, see -# https://guides.rubyonrails.org/v7.1/configuring.html#config-active-support-message-serializer -# -# NOTE: Does rolling deploy apply here? -# If you are performing a rolling deploy of a Rails 7.1 upgrade, wherein servers -# that have not yet been upgraded must be able to read messages from upgraded -# servers, first deploy without changing the serializer, then set the serializer -# in a subsequent deploy. -#++ -Rails.application.config.active_support.message_serializer = :json_allow_marshal - -### -# Enable a performance optimization that serializes message data and metadata -# together. This changes the message format, so messages serialized this way -# cannot be read by older versions of Rails. However, messages that use the old -# format can still be read, regardless of whether this optimization is enabled. -# -# NOTE: Does rolling deploy apply here? -# To perform a rolling deploy of a Rails 7.1 upgrade, wherein servers that have -# not yet been upgraded must be able to read messages from upgraded servers, -# leave this optimization off on the first deploy, then enable it on a -# subsequent deploy. -#++ -Rails.application.config.active_support.use_message_serializer_for_metadata = true - -### -# Set the maximum size for Rails log files. -# -# `config.load_defaults 7.1` does not set this value for environments other than -# development and test. -#++ -if Rails.env.local? - Rails.application.config.log_file_size = 100 * 1024 * 1024 -end - -### -# Enable raising on assignment to attr_readonly attributes. The previous -# behavior would allow assignment but silently not persist changes to the -# database. -#++ -Rails.application.config.active_record.raise_on_assign_to_attr_readonly = true - -### -# Enable validating only parent-related columns for presence when the parent is mandatory. -# The previous behavior was to validate the presence of the parent record, which performed an extra query -# to get the parent every time the child record was updated, even when parent has not changed. -#++ -Rails.application.config.active_record.belongs_to_required_validates_foreign_key = false - -### -# Enable precompilation of `config.filter_parameters`. Precompilation can -# improve filtering performance, depending on the quantity and types of filters. -#++ -Rails.application.config.precompile_filter_parameters = true - -### -# Enable before_committed! callbacks on all enrolled records in a transaction. -# The previous behavior was to only run the callbacks on the first copy of a record -# if there were multiple copies of the same record enrolled in the transaction. -#++ -Rails.application.config.active_record.before_committed_on_all_records = true - -### -# Disable automatic column serialization into YAML. -# To keep the historic behavior, you can set it to `YAML`, however it is -# recommended to explicitly define the serialization method for each column -# rather than to rely on a global default. -#++ -Rails.application.config.active_record.default_column_serializer = nil - -### -# Enable a performance optimization that serializes Active Record models -# in a faster and more compact way. -# -# NOTE: Does rolling deploy apply here? -# To perform a rolling deploy of a Rails 7.1 upgrade, wherein servers that have -# not yet been upgraded must be able to read caches from upgraded servers, -# leave this optimization off on the first deploy, then enable it on a -# subsequent deploy. -#++ -Rails.application.config.active_record.marshalling_format_version = 7.1 - -### -# Run `after_commit` and `after_*_commit` callbacks in the order they are defined in a model. -# This matches the behaviour of all other callbacks. -# In previous versions of Rails, they ran in the inverse order. -#++ -Rails.application.config.active_record.run_after_transaction_callbacks_in_order_defined = true - -### -# Whether a `transaction` block is committed or rolled back when exited via `return`, `break` or `throw`. -#++ -Rails.application.config.active_record.commit_transaction_on_non_local_return = true - -### -# Controls when to generate a value for has_secure_token declarations. -#++ -Rails.application.config.active_record.generate_secure_token_on = :initialize - -### -# ** Please read carefully, this must be configured in config/application.rb ** -# Change the format of the cache entry. -# -# Changing this default means that all new cache entries added to the cache -# will have a different format that is not supported by Rails 7.0 -# applications. -# -# Only change this value after your application is fully deployed to Rails 7.1 -# and you have no plans to rollback. -# When you're ready to change format, add this to `config/application.rb` (NOT -# this file): -# NOTE: Does rolling deploy apply here? -# - TODO: If so, revert change in app config & create issue for this to be done after deploying? -# config.active_support.cache_format_version = 7.1 (DOME) - -### -# Configure Action View to use HTML5 standards-compliant sanitizers when they are supported on your -# platform. -# -# `Rails::HTML::Sanitizer.best_supported_vendor` will cause Action View to use HTML5-compliant -# sanitizers if they are supported, else fall back to HTML4 sanitizers. -# -# In previous versions of Rails, Action View always used `Rails::HTML4::Sanitizer` as its vendor. -#++ -Rails.application.config.action_view.sanitizer_vendor = Rails::HTML::Sanitizer.best_supported_vendor - -### -# Configure Action Text to use an HTML5 standards-compliant sanitizer when it is supported on your -# platform. -# -# `Rails::HTML::Sanitizer.best_supported_vendor` will cause Action Text to use HTML5-compliant -# sanitizers if they are supported, else fall back to HTML4 sanitizers. -# -# In previous versions of Rails, Action Text always used `Rails::HTML4::Sanitizer` as its vendor. -#++ -# NOTE: App is not using Action Text, this is ignored when load_defaults: 7.1 is enabled. -# Rails.application.config.action_text.sanitizer_vendor = Rails::HTML::Sanitizer.best_supported_vendor - -### -# Configure the log level used by the DebugExceptions middleware when logging -# uncaught exceptions during requests. -#++ -Rails.application.config.action_dispatch.debug_exception_log_level = :error - -### -# Configure the test helpers in Action View, Action Dispatch, and rails-dom-testing to use HTML5 -# parsers. -# -# Nokogiri::HTML5 isn't supported on JRuby, so JRuby applications must set this to :html4. -# -# In previous versions of Rails, these test helpers always used an HTML4 parser. -#++ -Rails.application.config.dom_testing_default_html_version = :html5 From 40af40a640b7df5bd82fbb38353f35188d6a1ba1 Mon Sep 17 00:00:00 2001 From: compwron Date: Sun, 9 Aug 2026 22:54:34 +0000 Subject: [PATCH 04/16] Use the positional form of ActiveRecord::Enum Rails 7 deprecated `enum name: values` and Rails 8 removed it, so the keyword form raises ArgumentError on 8.x. The positional form has been supported since 7.0 and is equivalent here: both map to {"mb"=>0, "in"=>1} and {"f"=>0, "c"=>1} and generate the same predicates and scopes. Co-Authored-By: Claude Opus 5 (1M context) --- backend/app/models/profile.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/models/profile.rb b/backend/app/models/profile.rb index 6718a593..9aae8d3f 100644 --- a/backend/app/models/profile.rb +++ b/backend/app/models/profile.rb @@ -22,8 +22,8 @@ # class Profile < ActiveRecord::Base - enum pressure_units: %i[mb in] - enum temperature_units: %i[f c] + enum :pressure_units, %i[mb in] + enum :temperature_units, %i[f c] # # Associations From 0a385faa287f6216f095e2d1889f69fc854054ae Mon Sep 17 00:00:00 2001 From: compwron Date: Sun, 9 Aug 2026 22:56:27 +0000 Subject: [PATCH 05/16] Use the test queue adapter in the test environment The suite relied on ActiveJob::TestHelper installing the test adapter itself. That works on 7.1 but not on 8.x, where the adapter stays :sidekiq inside examples, so perform_enqueued_jobs silently stops running anything and DataExportJob's mailer expectation fails with no obvious cause. Configuring the adapter explicitly is the documented approach and does not depend on the helper's internals. It also stops the suite from pushing to Redis: TrackableUsage enqueues SwitchTrackableVisibility in an after_commit hook, so any spec touching that model was writing to a real queue. The suite no longer opens a Redis connection at all. Jobs invoked through Sidekiq's own perform_async do not go through Active Job and are unaffected. Co-Authored-By: Claude Opus 5 (1M context) --- backend/config/environments/test.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/config/environments/test.rb b/backend/config/environments/test.rb index edc5bdfa..55f02332 100644 --- a/backend/config/environments/test.rb +++ b/backend/config/environments/test.rb @@ -53,6 +53,12 @@ # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + # Likewise, don't push Active Job work to Redis from the suite. This overrides + # the :sidekiq adapter set in config/application.rb, and only affects the + # Active Job path (perform_later/deliver_later); workers invoked through + # Sidekiq's own perform_async are unaffected. + config.active_job.queue_adapter = :test + # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr From dcb335dc0a98f993b4fbfe3d233f9ee978ce61cc Mon Sep 17 00:00:00 2001 From: compwron Date: Mon, 10 Aug 2026 22:08:05 +0000 Subject: [PATCH 06/16] Cover the OmniAuth callback endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing in the suite touched OmniAuth, which is awkward timing: the omniauth 1 -> 2 bump that follows changes the request phase to POST-only with CSRF protection, and there was no test to say whether anything broke. The request phase turns out not to matter here. The Ember client never calls it: it signs the user in with Facebook's JavaScript SDK via Torii and then POSTs straight to /api/auth/facebook/callback (frontend/app/authenticators/facebook.js:19, with `namespace: "api"` from the ajax service), where the strategy picks the authorization code out of the `fbsr_` cookie the SDK left behind. That cookie path is what needs protecting, so that is what these cover. The request spec drives the real middleware stack — Session, Warden, the Facebook strategy — through OmniAuth's test mode, so it exercises the callback dispatch and the on_failure proc in config/initializers/devise.rb rather than just the controller. The controller spec covers the three branches of handle_omniauth, including the invited-but-not-yet-accepted user, which is easy to regress because it reads as a successful lookup. Co-Authored-By: Claude Opus 5 (1M context) --- .../v1/omniauth_callbacks_controller_spec.rb | 54 +++++++++++++++++++ backend/spec/requests/omniauth_spec.rb | 40 ++++++++++++++ backend/spec/support/response.rb | 1 + 3 files changed, 95 insertions(+) create mode 100644 backend/spec/controllers/api/v1/omniauth_callbacks_controller_spec.rb create mode 100644 backend/spec/requests/omniauth_spec.rb diff --git a/backend/spec/controllers/api/v1/omniauth_callbacks_controller_spec.rb b/backend/spec/controllers/api/v1/omniauth_callbacks_controller_spec.rb new file mode 100644 index 00000000..2fef864a --- /dev/null +++ b/backend/spec/controllers/api/v1/omniauth_callbacks_controller_spec.rb @@ -0,0 +1,54 @@ +require "rails_helper" + +RSpec.describe Api::V1::OmniauthCallbacksController do + let(:user) { create(:user) } + + def auth_hash_for(email) + OmniAuth::AuthHash.new( + provider: "facebook", + uid: "1234567890", + info: {email: email, name: "Test User"} + ) + end + + describe "facebook" do + before { request.env["omniauth.auth"] = auth_hash_for(email) } + + context "when the provider's email belongs to a registered user" do + let(:email) { user.email } + + it "responds with that user's session" do + post :facebook + + expect(response.status).to eq 200 + expect(response_body[:user_id]).to eq user.id + expect(response_body[:email]).to eq user.email + expect(response_body[:token]).to eq user.authentication_token + end + end + + context "when no user has the provider's email" do + let(:email) { "stranger@example.com" } + + it "responds with 401" do + post :facebook + + expect(response.status).to eq 401 + expect(response_body[:errors]).to eq "User not found" + end + end + + context "when the user has not accepted their invitation" do + let(:email) { "invited@example.com" } + + before { User.invite!(email: email) } + + it "responds with 401" do + post :facebook + + expect(response.status).to eq 401 + expect(response_body[:errors]).to eq "User not found" + end + end + end +end diff --git a/backend/spec/requests/omniauth_spec.rb b/backend/spec/requests/omniauth_spec.rb new file mode 100644 index 00000000..90928772 --- /dev/null +++ b/backend/spec/requests/omniauth_spec.rb @@ -0,0 +1,40 @@ +require "rails_helper" + +# The Ember client does not use OmniAuth's request phase: it signs the user in +# with Facebook's JavaScript SDK and then POSTs straight to the callback path, +# where the strategy reads the `fbsr_` cookie the SDK left behind. These +# examples cover that path through the real middleware stack. +RSpec.describe "OmniAuth", type: :request do + let(:user) { create(:user) } + + around do |example| + OmniAuth.config.test_mode = true + example.run + ensure + OmniAuth.config.mock_auth.delete(:facebook) + OmniAuth.config.test_mode = false + end + + it "signs a user in through the facebook callback" do + OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new( + provider: "facebook", + uid: "1234567890", + info: {email: user.email, name: "Test User"} + ) + + post "/api/auth/facebook/callback" + + expect(response.status).to eq 200 + expect(response_body[:user_id]).to eq user.id + expect(response_body[:token]).to eq user.authentication_token + end + + it "renders the failure message when the provider rejects the request" do + OmniAuth.config.mock_auth[:facebook] = :invalid_credentials + + post "/api/auth/facebook/callback" + + expect(response.status).to eq 401 + expect(response_body[:errors]).to eq "Invalid credentials" + end +end diff --git a/backend/spec/support/response.rb b/backend/spec/support/response.rb index 9b441f79..2ae1c904 100644 --- a/backend/spec/support/response.rb +++ b/backend/spec/support/response.rb @@ -14,4 +14,5 @@ def response_body RSpec.configure do |config| config.include ResponseHelpers, type: :controller + config.include ResponseHelpers, type: :request end From 827dd7a36017ceee53e3859b2207f851e3468534 Mon Sep 17 00:00:00 2001 From: compwron Date: Mon, 10 Aug 2026 22:11:48 +0000 Subject: [PATCH 07/16] Drop rails_12factor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gem has been unmaintained since 2016 and Heroku stopped recommending it long before that. It is two railties: one forces static file serving on, the other replaces the logger with its own stdout logger. Neither is wanted here. There is no backend/public directory, app/assets/{images, javascripts,stylesheets} are all empty, and ApplicationController inherits from ActionController::API, so nothing is served out of public/ — the only asset reference in any view is an inline mail attachment (app/views/checkin_reminder_mailer/remind.html.erb:8). Removing the railtie returns config.public_file_server.enabled to what production.rb already says it should be, which is off unless RAILS_SERVE_STATIC_FILES is set. The logger railtie was doing real harm: it runs in a before_initialize hook, which fires after config/environments/production.rb is evaluated, so it was overwriting the logger that file configures. Booting production with and without the gem: logger RailsStdoutLogging::StdoutLogger -> ActiveSupport::BroadcastLogger formatter Logger::SimpleFormatter -> Logger::Formatter static true -> false Both loggers write to stdout, so nothing is lost, but production regains the formatter it asks for. Heads-up for whoever watches the logs: lines pick up the standard severity and timestamp prefix, so anything parsing them by position should be checked. Co-Authored-By: Claude Opus 5 (1M context) --- backend/Gemfile | 4 ---- backend/Gemfile.lock | 6 ------ 2 files changed, 10 deletions(-) diff --git a/backend/Gemfile b/backend/Gemfile index dee40c51..330b0516 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -101,10 +101,6 @@ group :test do gem "webmock" end -group :production do - gem "rails_12factor" -end - # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby] diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index bfb1e380..b544027f 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -375,11 +375,6 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails_12factor (0.0.3) - rails_serve_static_assets - rails_stdout_logging - rails_serve_static_assets (0.0.5) - rails_stdout_logging (0.0.5) railties (7.1.5.2) actionpack (= 7.1.5.2) activesupport (= 7.1.5.2) @@ -558,7 +553,6 @@ DEPENDENCIES rack-cors (= 2.0.1) rack-timeout rails (~> 7.1.0) - rails_12factor rake rspec-rails ruby-progressbar From 50ee9b18b9b3e6d5fda030a8d344e5718f374ef7 Mon Sep 17 00:00:00 2001 From: compwron Date: Mon, 10 Aug 2026 22:16:22 +0000 Subject: [PATCH 08/16] Upgrade OmniAuth to 2.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OmniAuth 1.8.1 pins `rack < 3`, which blocks Rails 8, and carries CVE-2015-9284: the request phase answers GET, so a third-party page can silently start an auth flow. Version 2 fixes that by making the request phase POST-only with a CSRF check. omniauth-facebook has to move with it, 3.0.0 -> 11.0.0, since v3 depends on omniauth-oauth2 ~> 1.2, which wants omniauth 1. The request-phase change does not affect this app. The Ember client never calls it — it authenticates with Facebook's JavaScript SDK and POSTs to the callback path directly — and the native app has no Facebook login at all. The unused GET /api/auth/facebook route now falls through to passthru instead of starting a flow, which is the point of the CVE fix. The token exchange did need a change. omniauth-oauth2 1.9 pulls in oauth2 2.x, which flipped the default `auth_scheme` from `:request_body` to `:basic_auth`. That sends nothing but an Authorization header: POST https://graph.facebook.com/v24.0/oauth/access_token Authorization: Basic MTIzNDU2Nzg5MDpmYWNlYm9vay1hcHAtc2VjcmV0 code=fb-auth-code&grant_type=authorization_code&redirect_uri= Facebook's token endpoint takes client_id and client_secret as parameters and ignores that header, so every Facebook login would have failed. Neither omniauth-oauth2 nor omniauth-facebook overrides the default, so config/initializers/devise.rb now sets it back to what oauth2 1.4.7 sent. The new spec drives the real callback phase — a signed fbsr_ cookie in, Facebook's endpoints stubbed — rather than OmniAuth's test mode, which returns before any of this happens. It fails on the default auth_scheme, so it is the regression test for the above rather than a restatement of it. The Facebook credentials it needs are set in config/environments/test.rb alongside the other test-only values, since env-example ships them empty. Also bumped as dependencies of the above: oauth2 1.4.7 -> 2.0.25, faraday 1.8 -> 2.14, hashie 3.5 -> 5.1, jwt 2.3 -> 3.2. Co-Authored-By: Claude Opus 5 (1M context) --- backend/Gemfile | 4 +- backend/Gemfile.lock | 86 ++++++++++++++------------ backend/config/environments/test.rb | 5 ++ backend/config/initializers/devise.rb | 7 ++- backend/spec/requests/omniauth_spec.rb | 65 +++++++++++++++++-- 5 files changed, 122 insertions(+), 45 deletions(-) diff --git a/backend/Gemfile b/backend/Gemfile index 330b0516..f4d85c15 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -27,8 +27,8 @@ gem "cancancan", "~> 3.6.1" gem "cancancan-mongoid", "~> 2.0" gem "devise", "~> 4.8" gem "devise_invitable", "~> 2.0" -gem "omniauth", "~> 1.8" -gem "omniauth-facebook", "~> 3.0" +gem "omniauth", "~> 2.1" +gem "omniauth-facebook", "~> 11.0" # Colored output to console gem "colored" diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index b544027f..a301e378 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -87,7 +87,11 @@ GEM annotate (3.2.0) activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) + anonymous_loader (0.1.3) + version_gem (~> 1.1, >= 1.1.14) ast (2.4.2) + auth-sanitizer (0.2.3) + version_gem (~> 1.1, >= 1.1.14) awesome_print (1.9.2) base64 (0.3.0) bcrypt (3.1.20) @@ -185,25 +189,12 @@ GEM factory_bot_rails (6.4.3) factory_bot (~> 6.4) railties (>= 5.0.0) - faraday (1.8.0) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0.1) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.1) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - multipart-post (>= 1.2, < 3) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.4) + net-http (~> 0.5) ferrum (0.14) addressable (~> 2.5) concurrent-ruby (~> 1.1) @@ -221,7 +212,8 @@ GEM activerecord (>= 4.2, < 7.2) request_store (~> 1.0) hashdiff (1.2.1) - hashie (3.5.7) + hashie (5.1.0) + logger httpclient (2.8.3) i18n (1.14.7) concurrent-ruby (~> 1.0) @@ -232,7 +224,8 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.7.1) - jwt (2.3.0) + jwt (3.2.0) + base64 kaminari-actionview (1.2.1) actionview kaminari-core (= 1.2.1) @@ -277,13 +270,15 @@ GEM mongoid (>= 3.0, < 10.0) mongoid-compatibility (>= 0.5.1) multi_json (1.15.0) - multi_xml (0.6.0) - multipart-post (2.1.1) + multi_xml (0.9.1) + bigdecimal (>= 3.1, < 5) mutex_m (0.3.0) nearest_time_zone (0.0.4) andand kdtree require_all + net-http (0.9.1) + uri (>= 0.11.1) net-imap (0.5.12) date net-protocol @@ -297,20 +292,27 @@ GEM nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oauth2 (1.4.7) - faraday (>= 0.8, < 2.0) - jwt (>= 1.0, < 3.0) - multi_json (~> 1.3) + oauth2 (2.0.25) + anonymous_loader (~> 0.1, >= 0.1.3) + auth-sanitizer (~> 0.2, >= 0.2.3) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) multi_xml (~> 0.5) - rack (>= 1.2, < 3) - omniauth (1.8.1) - hashie (>= 3.4.6, < 3.6.0) - rack (>= 1.6.2, < 3) - omniauth-facebook (3.0.0) - omniauth-oauth2 (~> 1.2) - omniauth-oauth2 (1.5.0) - oauth2 (~> 1.1) - omniauth (~> 1.2) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0, >= 2.0.7) + version_gem (~> 1.1, >= 1.1.14) + omniauth (2.1.4) + hashie (>= 3.4.6) + logger + rack (>= 2.2.3) + rack-protection + omniauth-facebook (11.0.0) + bigdecimal + omniauth-oauth2 (>= 1.2, < 3) + omniauth-oauth2 (1.9.0) + oauth2 (>= 2.0.2, < 3) + omniauth (~> 2.0) orm_adapter (0.5.0) parallel (1.24.0) parser (3.3.0.5) @@ -346,6 +348,9 @@ GEM rack (2.2.21) rack-cors (2.0.1) rack (>= 2.0.0) + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) rack-session (1.0.2) rack (< 3) rack-test (2.2.0) @@ -456,6 +461,9 @@ GEM simplecov_json_formatter (0.1.4) sixarm_ruby_unaccent (1.2.0) smart_properties (1.17.0) + snaky_hash (2.0.7) + hashie (>= 0.1.0, < 6) + version_gem (~> 1.1, >= 1.1.14) sprockets (4.2.2) concurrent-ruby (~> 1.0) logger @@ -490,8 +498,10 @@ GEM concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) uniform_notifier (1.16.0) + uri (1.1.1) vcr (6.3.1) base64 + version_gem (1.1.15) warden (1.2.9) rack (>= 2.0.9) webmock (3.26.1) @@ -542,8 +552,8 @@ DEPENDENCIES mongoid (= 8.1.3) mongoid-rspec nearest_time_zone - omniauth (~> 1.8) - omniauth-facebook (~> 3.0) + omniauth (~> 2.1) + omniauth-facebook (~> 11.0) pg pry-byebug pry-doc diff --git a/backend/config/environments/test.rb b/backend/config/environments/test.rb index 55f02332..caedf19a 100644 --- a/backend/config/environments/test.rb +++ b/backend/config/environments/test.rb @@ -12,6 +12,11 @@ ENV["TOMORROW_IO_KEY"] = "MY_MEGA_TOMORROW_IO_KEY" if ENV["TOMORROW_IO_KEY"].blank? ENV["SMTP_EMAIL_FROM"] = "from@some.email" if ENV["SMTP_EMAIL_FROM"].blank? +# Also not real. The Facebook strategy captures these at boot, and spec/requests/ +# omniauth_spec.rb signs its fbsr_ cookie with the secret. +ENV["FACEBOOK_APP_ID"] = "1234567890" if ENV["FACEBOOK_APP_ID"].blank? +ENV["FACEBOOK_APP_SECRET"] = "facebook-app-secret" if ENV["FACEBOOK_APP_SECRET"].blank? + # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped diff --git a/backend/config/initializers/devise.rb b/backend/config/initializers/devise.rb index 28814a40..ebc66e80 100644 --- a/backend/config/initializers/devise.rb +++ b/backend/config/initializers/devise.rb @@ -237,7 +237,12 @@ # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' - config.omniauth :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"], {} + # auth_scheme is set explicitly because oauth2 2.0 changed its default from + # :request_body to :basic_auth. Facebook's token endpoint reads client_id and + # client_secret as request parameters and ignores the Authorization header, so + # taking the new default would send it a request with no credentials at all. + config.omniauth :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"], + client_options: {auth_scheme: :request_body} # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. diff --git a/backend/spec/requests/omniauth_spec.rb b/backend/spec/requests/omniauth_spec.rb index 90928772..bba83a40 100644 --- a/backend/spec/requests/omniauth_spec.rb +++ b/backend/spec/requests/omniauth_spec.rb @@ -6,21 +6,27 @@ # examples cover that path through the real middleware stack. RSpec.describe "OmniAuth", type: :request do let(:user) { create(:user) } + let(:app_id) { ENV["FACEBOOK_APP_ID"] } + let(:app_secret) { ENV["FACEBOOK_APP_SECRET"] } around do |example| - OmniAuth.config.test_mode = true example.run ensure OmniAuth.config.mock_auth.delete(:facebook) OmniAuth.config.test_mode = false end + def mock_facebook(response) + OmniAuth.config.test_mode = true + OmniAuth.config.mock_auth[:facebook] = response + end + it "signs a user in through the facebook callback" do - OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new( + mock_facebook(OmniAuth::AuthHash.new( provider: "facebook", uid: "1234567890", info: {email: user.email, name: "Test User"} - ) + )) post "/api/auth/facebook/callback" @@ -30,11 +36,62 @@ end it "renders the failure message when the provider rejects the request" do - OmniAuth.config.mock_auth[:facebook] = :invalid_credentials + mock_facebook(:invalid_credentials) post "/api/auth/facebook/callback" expect(response.status).to eq 401 expect(response_body[:errors]).to eq "Invalid credentials" end + + describe "the token exchange" do + # OmniAuth's test mode short-circuits before the strategy talks to Facebook, + # so this drives the real callback phase instead. It is the only coverage of + # how the client credentials reach the token endpoint, and oauth2 2.0 changed + # the default for that (see config/initializers/devise.rb). + let(:graph) { "https://graph.facebook.com/v24.0" } + + # The cookie the Facebook JavaScript SDK leaves behind: a payload carrying an + # authorization code, signed with the app secret. + let(:signed_request) do + payload = base64_url(JSON.dump("algorithm" => "HMAC-SHA256", "code" => "fb-auth-code", "user_id" => "1234567890")) + "#{base64_url(OpenSSL::HMAC.digest("SHA256", app_secret, payload))}.#{payload}" + end + + def base64_url(value) + Base64.urlsafe_encode64(value, padding: false) + end + + before do + stub_request(:get, "#{graph}/me") + .with(query: hash_including("fields" => "name,email")) + .to_return( + status: 200, + body: {id: "1234567890", name: "Test User", email: user.email}.to_json, + headers: {"Content-Type" => "application/json"} + ) + + cookies["fbsr_#{app_id}"] = signed_request + end + + it "sends the client credentials to Facebook in the request body" do + token_request = stub_request(:post, "#{graph}/oauth/access_token") + .with(body: hash_including( + "client_id" => app_id, + "client_secret" => app_secret, + "code" => "fb-auth-code" + )) + .to_return( + status: 200, + body: {access_token: "fb-access-token", token_type: "bearer"}.to_json, + headers: {"Content-Type" => "application/json"} + ) + + post "/api/auth/facebook/callback" + + expect(token_request).to have_been_requested + expect(response.status).to eq 200 + expect(response_body[:user_id]).to eq user.id + end + end end From fee2f38f9be387c41ddbf522f7c24155310cd563 Mon Sep 17 00:00:00 2001 From: compwron Date: Mon, 10 Aug 2026 22:24:17 +0000 Subject: [PATCH 09/16] Upgrade to Rails 8.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rails 7.1 went out of support on 2025-10-01. 7.2 reached EOL on 2026-08-09 and 8.0 goes EOL on 2026-11-07, so 8.1 (supported to 2027-10-10) is the only target worth the move, and the intermediate versions can be skipped. Three gems capped the framework below 7.2 and had to move with it. These are resolver failures, not guesses: mongoid 8.1.3 activemodel >= 5.1, < 7.2 -> 9.1.0 globalize 6.3.0 activerecord >= 4.2, < 7.2 -> 7.1.3 annotate 3.2.0 activerecord >= 3.2, < 8.0 -> replaced annotate has had no release since 2022 and its cap has not moved. Left alone, bundler resolves *backwards* to annotate 2.6.5 from 2015 rather than report a conflict, which is worse than a failure because it is silent. annotaterb is the maintained successor. Nothing in the repo automates it — there is no auto_annotate_models.rake — so this only affects running it by hand, and the existing schema headers are untouched. Say so if you would rather just drop it. Two more only fail at runtime, so the resolver had nothing to say about them: - bullet raises "Bullet does not support active_record 8.1.3.1 yet" from config/application.rb:20 the moment anything boots. It is unpinned, but a conservative resolve keeps whatever is in the lockfile, so it needed an explicit update: 7.2.0 -> 8.1.3. - config/puma.rb referenced DefaultRackup, which Puma 6 removed, so the web process would not have started under Puma 8. The line only ever restated the default of config.ru and is deleted rather than replaced. on_worker_boot is now before_worker_boot, its name since Puma 8. Sidekiq 8 refuses to start against Redis older than 7.0.0, so the pins in docker-compose.yml, .tool-versions and the CI workflow move from 6.2.3 to 7.2.15, matching what Heroku Key-Value Store offers. The suite never noticed because it runs on the :test queue adapter, but `make start` would have brought up a worker that died on boot. Postgres and MongoDB are also long out of support; they are not blocking, so they are left for their own change. Verified on 8.1.3.1 with Mongoid 9.1, Rack 3.2, Devise 5, Puma 8 and Sidekiq 8: rspec 321 examples, 0 failures, no deprecations standardrb / erb lint clean boot development, test and production puma -C config/puma.rb serves GET / with the expected JSON sidekiq -C sidekiq.yml boots and connects to Redis CORS preflight 200, Access-Control-Allow-Origin intact (rack-cors is pinned at 2.0.1, which predates Rack 3, so this was worth checking) Mongoid 9 turns on immutable_ids and map_big_decimal_to_decimal128. Neither applies: no model declares a BigDecimal field, and the only _id in application code is an aggregation pipeline key in app/models/reaction.rb:23, which reaction_spec covers. active_model_serializers stays on 0.9.8 and the JSON format with it. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/backend.yml | 2 +- .tool-versions | 2 +- backend/Gemfile | 14 +- backend/Gemfile.lock | 235 +++++++++++++++++----------------- backend/config/puma.rb | 8 +- docker-compose.yml | 2 +- 6 files changed, 131 insertions(+), 132 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 305e4bf2..197b915b 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -93,7 +93,7 @@ jobs: services: redis: - image: redis:6.2.3-alpine + image: redis:7.2.15-alpine ports: ["6379:6379"] options: --entrypoint redis-server diff --git a/.tool-versions b/.tool-versions index 08108905..b2e9baaa 100644 --- a/.tool-versions +++ b/.tool-versions @@ -2,4 +2,4 @@ nodejs 12.22.6 ruby 3.2.3 postgres 12.8 mongodb 4.4.9 -redis 6.2.3 +redis 7.2.15 diff --git a/backend/Gemfile b/backend/Gemfile index f4d85c15..cdfd2fcc 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -6,7 +6,7 @@ ruby "3.2.3" gem "dotenv-rails", groups: %i[development test] # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem "rails", "~> 7.1.0" +gem "rails", "~> 8.1.0" gem "rake" gem "sprockets-rails" @@ -16,16 +16,16 @@ gem "sprockets-rails" gem "active_model_serializers", "~> 0.9.8" # Use postgresql and mongo as the database for Active Record -gem "mongoid", "8.1.3" # https://www.mongodb.com/docs/mongoid/current/reference/compatibility/#rails-compatibility +gem "mongoid", "9.1.0" # https://www.mongodb.com/docs/mongoid/current/reference/compatibility/#rails-compatibility gem "pg" # Use Puma as the app server -gem "puma", "5.6.8" +gem "puma", "8.0.2" # Authentication libraries gem "cancancan", "~> 3.6.1" gem "cancancan-mongoid", "~> 2.0" -gem "devise", "~> 4.8" +gem "devise", "~> 5.0" gem "devise_invitable", "~> 2.0" gem "omniauth", "~> 2.1" gem "omniauth-facebook", "~> 11.0" @@ -34,7 +34,7 @@ gem "omniauth-facebook", "~> 11.0" gem "colored" # Background jobs -gem "sidekiq", "~> 7.3" +gem "sidekiq", "~> 8.1" # Structured seed data gem "seedbank" @@ -84,7 +84,9 @@ group :development, :test do end group :development do - gem "annotate" + # Successor to `annotate`, which was last released in 2022 and requires + # activerecord < 8.0. + gem "annotaterb" gem "awesome_print" gem "better_errors" gem "brakeman" diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index a301e378..d3953b68 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -1,51 +1,48 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.1.5.2) - actionpack (= 7.1.5.2) - activesupport (= 7.1.5.2) + action_text-trix (2.1.19) + railties + actioncable (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.5.2) - actionpack (= 7.1.5.2) - activejob (= 7.1.5.2) - activerecord (= 7.1.5.2) - activestorage (= 7.1.5.2) - activesupport (= 7.1.5.2) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.5.2) - actionpack (= 7.1.5.2) - actionview (= 7.1.5.2) - activejob (= 7.1.5.2) - activesupport (= 7.1.5.2) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) + mail (>= 2.8.0) + actionmailer (8.1.3.1) + actionpack (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activesupport (= 8.1.3.1) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.5.2) - actionview (= 7.1.5.2) - activesupport (= 7.1.5.2) + actionpack (8.1.3.1) + actionview (= 8.1.3.1) + activesupport (= 8.1.3.1) nokogiri (>= 1.8.5) - racc rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.5.2) - actionpack (= 7.1.5.2) - activerecord (= 7.1.5.2) - activestorage (= 7.1.5.2) - activesupport (= 7.1.5.2) + useragent (~> 0.16) + actiontext (8.1.3.1) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.5.2) - activesupport (= 7.1.5.2) + actionview (8.1.3.1) + activesupport (= 8.1.3.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -53,40 +50,40 @@ GEM active_model_serializers (0.9.8) activemodel (>= 3.2) concurrent-ruby (~> 1.0) - activejob (7.1.5.2) - activesupport (= 7.1.5.2) + activejob (8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.3.6) - activemodel (7.1.5.2) - activesupport (= 7.1.5.2) - activerecord (7.1.5.2) - activemodel (= 7.1.5.2) - activesupport (= 7.1.5.2) + activemodel (8.1.3.1) + activesupport (= 8.1.3.1) + activerecord (8.1.3.1) + activemodel (= 8.1.3.1) + activesupport (= 8.1.3.1) timeout (>= 0.4.0) - activestorage (7.1.5.2) - actionpack (= 7.1.5.2) - activejob (= 7.1.5.2) - activerecord (= 7.1.5.2) - activesupport (= 7.1.5.2) + activestorage (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activesupport (= 8.1.3.1) marcel (~> 1.0) - activesupport (7.1.5.2) + activesupport (8.1.3.1) base64 - benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) - mutex_m securerandom (>= 0.3) - tzinfo (~> 2.0) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) andand (1.3.3) - annotate (3.2.0) - activerecord (>= 3.2, < 8.0) - rake (>= 10.4, < 14.0) + annotaterb (4.24.0) + activerecord (>= 6.0.0) + activesupport (>= 6.0.0) anonymous_loader (0.1.3) version_gem (~> 1.1, >= 1.1.14) ast (2.4.2) @@ -94,8 +91,7 @@ GEM version_gem (~> 1.1, >= 1.1.14) awesome_print (1.9.2) base64 (0.3.0) - bcrypt (3.1.20) - benchmark (0.5.0) + bcrypt (3.1.22) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -107,14 +103,14 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (3.3.1) + bigdecimal (4.1.2) brakeman (6.1.2) racc - bson (4.15.0) + bson (5.2.0) bugsnag (6.27.1) concurrent-ruby (~> 1.0) builder (3.3.0) - bullet (7.2.0) + bullet (8.1.3) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.1.3) @@ -134,8 +130,8 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) colored (1.2) - concurrent-ruby (1.3.5) - connection_pool (2.5.5) + concurrent-ruby (1.3.8) + connection_pool (3.0.2) countries (4.0.1) i18n_data (~> 0.13.0) sixarm_ruby_unaccent (~> 1.1) @@ -159,10 +155,10 @@ GEM date (3.5.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (4.9.4) + devise (5.0.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0) + railties (>= 7.0) responders warden (~> 1.2.3) devise_invitable (2.0.11) @@ -207,15 +203,16 @@ GEM csv (>= 3.0.0) globalid (1.3.0) activesupport (>= 6.1) - globalize (6.3.0) - activemodel (>= 4.2, < 7.2) - activerecord (>= 4.2, < 7.2) + globalize (7.1.3) + activemodel (>= 7.0, < 8.2) + activerecord (>= 7.0, < 8.2) + activesupport (>= 7.0, < 8.2) request_store (~> 1.0) hashdiff (1.2.1) hashie (5.1.0) logger httpclient (2.8.3) - i18n (1.14.7) + i18n (1.15.2) concurrent-ruby (~> 1.0) i18n_data (0.13.0) io-console (0.8.1) @@ -223,7 +220,7 @@ GEM pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.7.1) + json (2.21.2) jwt (3.2.0) base64 kaminari-actionview (1.2.1) @@ -250,19 +247,22 @@ GEM net-imap net-pop net-smtp - marcel (1.0.4) + marcel (1.2.1) matrix (0.4.2) method_source (1.1.0) mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (5.26.2) - mongo (2.20.1) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + mongo (2.25.0) + base64 bson (>= 4.14.1, < 6.0.0) - mongoid (8.1.3) - activemodel (>= 5.1, < 7.2, != 7.0.0) + mongoid (9.1.0) + activemodel (>= 5.1, < 8.2, != 7.0.0) concurrent-ruby (>= 1.0.5, < 2.0) mongo (>= 2.18.0, < 3.0.0) - ruby2_keywords (~> 0.0.5) + ostruct mongoid-compatibility (0.6.0) activesupport mongoid (>= 2.0) @@ -272,7 +272,6 @@ GEM multi_json (1.15.0) multi_xml (0.9.1) bigdecimal (>= 3.1, < 5) - mutex_m (0.3.0) nearest_time_zone (0.0.4) andand kdtree @@ -288,7 +287,7 @@ GEM timeout net-smtp (0.5.1) net-protocol - nio4r (2.7.3) + nio4r (2.7.5) nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -314,6 +313,7 @@ GEM oauth2 (>= 2.0.2, < 3) omniauth (~> 2.0) orm_adapter (0.5.0) + ostruct (0.6.3) parallel (1.24.0) parser (3.3.0.5) ast (~> 2.4.1) @@ -322,6 +322,7 @@ GEM pp (0.6.3) prettyprint prettyprint (0.2.0) + prism (1.9.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -337,7 +338,7 @@ GEM date stringio public_suffix (6.0.2) - puma (5.6.8) + puma (8.0.2) nio4r (~> 2.0) pusher (2.0.3) httpclient (~> 2.8) @@ -345,34 +346,35 @@ GEM pusher-signature (~> 0.1.8) pusher-signature (0.1.8) racc (1.8.1) - rack (2.2.21) + rack (3.2.6) rack-cors (2.0.1) rack (>= 2.0.0) - rack-protection (3.2.0) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.2) base64 (>= 0.1.0) - rack (~> 2.2, >= 2.2.4) - rack-session (1.0.2) - rack (< 3) + rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) rack-timeout (0.7.0) - rackup (1.0.1) - rack (< 3) - webrick - rails (7.1.5.2) - actioncable (= 7.1.5.2) - actionmailbox (= 7.1.5.2) - actionmailer (= 7.1.5.2) - actionpack (= 7.1.5.2) - actiontext (= 7.1.5.2) - actionview (= 7.1.5.2) - activejob (= 7.1.5.2) - activemodel (= 7.1.5.2) - activerecord (= 7.1.5.2) - activestorage (= 7.1.5.2) - activesupport (= 7.1.5.2) + rackup (2.3.1) + rack (>= 3) + rails (8.1.3.1) + actioncable (= 8.1.3.1) + actionmailbox (= 8.1.3.1) + actionmailer (= 8.1.3.1) + actionpack (= 8.1.3.1) + actiontext (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activemodel (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) bundler (>= 1.15.0) - railties (= 7.1.5.2) + railties (= 8.1.3.1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -380,13 +382,14 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.1.5.2) - actionpack (= 7.1.5.2) - activesupport (= 7.1.5.2) - irb + railties (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.2.1) @@ -394,7 +397,7 @@ GEM erb psych (>= 4.0.0) tsort - redis-client (0.26.1) + redis-client (0.30.1) connection_pool regexp_parser (2.9.0) reline (0.6.3) @@ -441,18 +444,17 @@ GEM rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) securerandom (0.4.1) seedbank (0.5.0) rake (>= 10.0) shoulda-matchers (6.2.0) activesupport (>= 5.2.0) - sidekiq (7.3.9) - base64 - connection_pool (>= 2.3.0) - logger - rack (>= 2.2.4) - redis-client (>= 0.22.2) + sidekiq (8.1.6) + connection_pool (>= 3.0.0) + json (>= 2.16.0) + logger (>= 1.7.0) + rack (>= 3.2.0) + redis-client (>= 0.29.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -497,8 +499,9 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) - uniform_notifier (1.16.0) + uniform_notifier (1.18.0) uri (1.1.1) + useragent (0.16.11) vcr (6.3.1) base64 version_gem (1.1.15) @@ -522,7 +525,7 @@ PLATFORMS DEPENDENCIES active_model_serializers (~> 0.9.8) - annotate + annotaterb awesome_print better_errors brakeman @@ -537,7 +540,7 @@ DEPENDENCIES cuprite database_cleaner database_cleaner-mongoid - devise (~> 4.8) + devise (~> 5.0) devise_invitable (~> 2.0) dotenv-rails erb_lint @@ -549,7 +552,7 @@ DEPENDENCIES kaminari-actionview kaminari-mongoid letter_opener - mongoid (= 8.1.3) + mongoid (= 9.1.0) mongoid-rspec nearest_time_zone omniauth (~> 2.1) @@ -558,17 +561,17 @@ DEPENDENCIES pry-byebug pry-doc pry-rails - puma (= 5.6.8) + puma (= 8.0.2) pusher rack-cors (= 2.0.1) rack-timeout - rails (~> 7.1.0) + rails (~> 8.1.0) rake rspec-rails ruby-progressbar seedbank shoulda-matchers - sidekiq (~> 7.3) + sidekiq (~> 8.1) simplecov sprockets-rails standardrb diff --git a/backend/config/puma.rb b/backend/config/puma.rb index d5b361d8..09505c6a 100755 --- a/backend/config/puma.rb +++ b/backend/config/puma.rb @@ -1,11 +1,5 @@ #!/usr/bin/env puma -# Load "path" as a rackup file. -# -# The default is "config.ru". -# -rackup DefaultRackup - port Integer(ENV.fetch("PORT") { 3000 }) environment ENV.fetch("RACK_ENV") { "development" } @@ -33,7 +27,7 @@ ActiveRecord::Base.connection_pool.disconnect! end -on_worker_boot do +before_worker_boot do # Worker specific setup for Rails 4.1+ # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot ActiveSupport.on_load(:active_record) do diff --git a/docker-compose.yml b/docker-compose.yml index 12413d5e..2c3dfcfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -97,7 +97,7 @@ services: expose: - 27017 redis: - image: redis:6.2.3-alpine + image: redis:7.2.15-alpine volumes: - redis:/data expose: From 2c82d0d932aac8d061314802135730b1e695aeea Mon Sep 17 00:00:00 2001 From: compwron Date: Mon, 10 Aug 2026 22:28:17 +0000 Subject: [PATCH 10/16] Load the Rails 8.1 framework defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app was running 8.1 on 7.1's defaults, which is the supported way to land an upgrade but not somewhere to stay: it keeps every behaviour change opted out indefinitely and leaves the next upgrade to deal with all of them at once. Dumping all 121 framework settings before and after, nine change: action_controller.action_on_path_relative_redirect :log -> :raise action_controller.escape_json_responses -> false action_dispatch.strict_freshness false -> true action_view.remove_hidden_field_autocomplete -> true action_view.render_tracker -> :ruby active_record.postgresql_adapter_decode_dates -> true active_record.raise_on_missing_required_finder_order_columns -> true active_record.validate_migration_timestamps -> true active_support.escape_js_separators_in_json -> false Most are inert here. There is no redirect_to, fresh_when or stale? anywhere in app/, config/ or lib/, and no form helpers — the only views are mailers. escape_json_responses is the one that changes what goes over the wire: 7.1 {"body":"hi \u003cscript\u003ealert(1)\u003c/script\u003e \u0026 done"} 8.1 {"body":"hi & done"} This is safe for the clients we have. Both forms parse to the same string, so anything calling JSON.parse — the Ember client through ember-data, the native app through fetch — cannot tell the difference. The escaping only protects JSON that is interpolated straight into an HTML document, and this backend renders no HTML: ApplicationController is an ActionController::API. Rails has also deprecated setting it back, so it stops having any effect in 8.2. config.active_support.cache_format_version = 7.1 is deleted because load_defaults 8.1 already sets exactly that — dumping the settings with and without the line gives byte-identical output. There is no 8.x cache format, and assigning one raises Unrecognized ActiveSupport::Cache.format_version, so the line was only ever going to become a trap. rspec 321 examples 0 failures, standardrb and erb lint clean, and development, test and production all boot with no deprecation warnings. Co-Authored-By: Claude Opus 5 (1M context) --- backend/config/application.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/config/application.rb b/backend/config/application.rb index 2f848fe1..eb1d0b81 100644 --- a/backend/config/application.rb +++ b/backend/config/application.rb @@ -22,9 +22,8 @@ module Flaredown class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.1 + config.load_defaults 8.1 config.add_autoload_paths_to_load_path = false - config.active_support.cache_format_version = 7.1 # https://medium.com/@Nicholson85/handling-cors-issues-in-your-rails-api-120dfbcb8a24 # fix CORS issues in staging? From 38879a57367f27ff6a970c665f1312d94309da79 Mon Sep 17 00:00:00 2001 From: compwron Date: Mon, 10 Aug 2026 22:34:54 +0000 Subject: [PATCH 11/16] Upgrade to Ruby 3.4.10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruby 3.2 stopped receiving security fixes on 2026-03-31. 3.4.10 is the current release of the newest stable series and is well within what Rails 8.1 supports (>= 3.2). The pins live in five places — .ruby-version, .tool-versions, the Gemfile's ruby directive and the Dockerfile — plus backend/.ruby-version, which is a symlink to the root file and so follows on its own. The Dockerfile keeps its bundler 2.5.6 pin; that version drives Ruby 3.4 without complaint, so the lockfile's BUNDLED WITH does not move. Two former default gems now have to be declared: - csv, which app/jobs/data_export_job.rb:21 and lib/tasks/utils.rake use. It has been resolving only as another gem's dependency, which was already luck. - mutex_m, which httpclient requires at auth.rb:11, and pusher depends on httpclient. This one is only a problem in combination: activesupport 7.1 depended on mutex_m, so it was in the lockfile; Rails 8.1 dropped that dependency, and Ruby 3.4 dropped it from the default gems. Either change alone is harmless. Together they take the whole suite down at boot, with an error that names webmock rather than either culprit: NameError: undefined method 'do_get_block' for class 'WebMockHTTPClient' webmock installs an httpclient adapter when httpclient is present, and httpclient had failed halfway through loading. Even the newest pusher (2.1.1) still requires httpclient ~> 2.8, so declaring mutex_m is the way out. Also pry-doc 1.5.0 -> 1.7.0, which otherwise prints "ruby/3.4.10 isn't supported by this pry-doc version" on every run. Verified on a real 3.4.10 build rather than by inspection: rspec 321 examples, 0 failures, no warnings standardrb / erb lint clean boot development, test and production rake -T 90 tasks, so utils.rake's require "csv" resolves Co-Authored-By: Claude Opus 5 (1M context) --- .ruby-version | 2 +- .tool-versions | 2 +- backend/Dockerfile | 2 +- backend/Gemfile | 9 ++++++++- backend/Gemfile.lock | 11 +++++++---- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.ruby-version b/.ruby-version index b347b11e..84d6c676 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.3 +3.4.10 diff --git a/.tool-versions b/.tool-versions index b2e9baaa..94eb9d01 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,5 +1,5 @@ nodejs 12.22.6 -ruby 3.2.3 +ruby 3.4.10 postgres 12.8 mongodb 4.4.9 redis 7.2.15 diff --git a/backend/Dockerfile b/backend/Dockerfile index 276ca765..746a44d1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.2.3 +FROM ruby:3.4.10 # set working directory WORKDIR /app diff --git a/backend/Gemfile b/backend/Gemfile index cdfd2fcc..2ce0037f 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -ruby "3.2.3" +ruby "3.4.10" # Configuration management. keep on top of Gemfile gem "dotenv-rails", groups: %i[development test] @@ -36,6 +36,13 @@ gem "colored" # Background jobs gem "sidekiq", "~> 8.1" +# Both stopped being Ruby default gems in 3.4, so they have to be asked for. +# csv is used by DataExportJob and the trackings export task; mutex_m is required +# by httpclient, which pusher depends on. Until now each resolved by accident — +# csv as another gem's dependency, mutex_m as one of Rails 7.1's. +gem "csv" +gem "mutex_m" + # Structured seed data gem "seedbank" diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index d3953b68..ce28a6bb 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -272,6 +272,7 @@ GEM multi_json (1.15.0) multi_xml (0.9.1) bigdecimal (>= 3.1, < 5) + mutex_m (0.3.0) nearest_time_zone (0.0.4) andand kdtree @@ -329,9 +330,9 @@ GEM pry-byebug (3.10.1) byebug (~> 11.0) pry (>= 0.13, < 0.15) - pry-doc (1.5.0) + pry-doc (1.7.0) pry (~> 0.11) - yard (~> 0.9.11) + yard (~> 0.9.21) pry-rails (0.3.11) pry (>= 0.13.0) psych (5.2.6) @@ -517,7 +518,7 @@ GEM websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - yard (0.9.36) + yard (0.9.45) zeitwerk (2.7.3) PLATFORMS @@ -537,6 +538,7 @@ DEPENDENCIES capybara colored countries + csv cuprite database_cleaner database_cleaner-mongoid @@ -554,6 +556,7 @@ DEPENDENCIES letter_opener mongoid (= 9.1.0) mongoid-rspec + mutex_m nearest_time_zone omniauth (~> 2.1) omniauth-facebook (~> 11.0) @@ -582,7 +585,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.2.3p157 + ruby 3.4.10p104 BUNDLED WITH 2.5.6 From 2ae5fafdb880629d815748b297add5f8120b8c13 Mon Sep 17 00:00:00 2001 From: compwron Date: Mon, 10 Aug 2026 22:37:51 +0000 Subject: [PATCH 12/16] Move the dev and CI datastores off end-of-life versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostgreSQL 12 went out of support on 2024-11-21 and MongoDB 4.4 on 2024-02-29. Nothing in the upgrade required this — Mongoid 9's driver still speaks wire protocol 9 — but the pins were badly out of date and only ever describe the dev stack and CI. Production is unaffected: Heroku manages the Postgres version and MongoDB is hosted at mongodb.com, neither of which reads these files. Some evidence the app is not attached to the old versions: this container runs PostgreSQL 15.18 and MongoDB 7.0.14, several majors past both pins, and the suite is green on them. db/structure.sql was in fact dumped from PostgreSQL 13, not 12.8, and loads cleanly on 15.18 — 24 tables, no errors — so the file is not regenerated here and there is no pg_dump churn in this diff. The Mongo driver knows about 8.0 explicitly; it maps it to wire version 25 in mongo-2.25.0/lib/mongo/server/description.rb:899. **This will not start against an existing dev volume.** Neither database will open a data directory written by an older major version, and 4.4 -> 8.0 is several majors in one step, which MongoDB does not support in place. The dev data is disposable, so the fix is to throw it away and re-seed: make stop && docker compose down -v && make start && make seed CI creates its databases from scratch every run and is not affected. The CI action pin for MongoDB stays at 1.10.0. It passes mongodb-version straight through as a Docker image tag, so it can pull 8.0.28 as it stands, and leaving it alone keeps one less untested thing in this change. Honest limits: 17.10 and 8.0.28 are not verified here, because this container cannot run another database. The verification above is on 15.18 and 7.0.14. CI runs the suite against these images on every pull request, so this is exactly where being wrong is cheap and immediately visible. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/backend.yml | 4 ++-- .tool-versions | 4 ++-- docker-compose.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 197b915b..59eee7a3 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -98,7 +98,7 @@ jobs: options: --entrypoint redis-server db: - image: postgres:12.8-alpine + image: postgres:17.10-alpine env: POSTGRES_PASSWORD: password ports: @@ -125,7 +125,7 @@ jobs: - name: Start MongoDB uses: supercharge/mongodb-github-action@1.10.0 with: - mongodb-version: 4.4.9 + mongodb-version: 8.0.28 - name: Load database schema run: | diff --git a/.tool-versions b/.tool-versions index 94eb9d01..12ddf976 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,5 +1,5 @@ nodejs 12.22.6 ruby 3.4.10 -postgres 12.8 -mongodb 4.4.9 +postgres 17.10 +mongodb 8.0.28 redis 7.2.15 diff --git a/docker-compose.yml b/docker-compose.yml index 2c3dfcfe..d87c3d80 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -81,7 +81,7 @@ services: profiles: - tools postgres: - image: postgres:12.8-alpine + image: postgres:17.10-alpine restart: always environment: POSTGRES_PASSWORD: password @@ -91,7 +91,7 @@ services: expose: - 5432 mongodb: - image: mongo:4.4.9 + image: mongo:8.0.28 volumes: - mongodb:/data/db expose: From f34a09c556718ff8dc19132267abbd71f81fd309 Mon Sep 17 00:00:00 2001 From: compwron Date: Mon, 10 Aug 2026 22:38:15 +0000 Subject: [PATCH 13/16] Update the docs for the new versions README's Environment list and the two version references in CLAUDE.md still described the stack as it was before this branch. No prose changes, only the numbers. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 4 ++-- README.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index de8b57c0..683e3a2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Flaredown is a chronic-illness symptom tracker. It is a monorepo with three deployable apps: -- `backend/` — Rails 7.1 API (Ruby 3.2.3), the only backend for all clients. +- `backend/` — Rails 8.1 API (Ruby 3.4.10), the only backend for all clients. - `frontend/` — Ember.js 2.18 web app (the production web client at app.flaredown.com), proxies API calls to the backend. - `native/` — Expo / React Native + TypeScript app (newer, in-progress replacement for the Ember client). @@ -44,7 +44,7 @@ CI (`.github/workflows/{backend,frontend,native}.yml`) uses path filters — bac The backend uses **both PostgreSQL and MongoDB simultaneously**, split by data type: - **PostgreSQL (ActiveRecord)** — relational/reference data: `User` (Devise auth), `Condition`, `Symptom`, `Treatment`, `Food`, `Tag`, `Profile`, `Weather`, and the `user_*` join tables. These models subclass `ActiveRecord::Base` and carry a `# == Schema Information` header. Schema lives in `db/schema.rb` + `db/structure.sql`; migrations in `db/migrate/`. -- **MongoDB (Mongoid 8)** — high-volume, user-generated, schemaless data: `Checkin` (the core daily symptom/treatment/tag log), `Comment`, `Reaction`, `Pattern`, `Notification`, `HarveyBradshawIndex`, `Feedback`, `PromotionRate`, `OracleRequest`. These `include Mongoid::Document`. Config in `config/mongoid.yml`. +- **MongoDB (Mongoid 9)** — high-volume, user-generated, schemaless data: `Checkin` (the core daily symptom/treatment/tag log), `Comment`, `Reaction`, `Pattern`, `Notification`, `HarveyBradshawIndex`, `Feedback`, `PromotionRate`, `OracleRequest`. These `include Mongoid::Document`. Config in `config/mongoid.yml`. The two stores are linked by an **encrypted foreign key**: Mongo documents store `encrypted_user_id` (symmetric-encryption gem, see `config/symmetric-encryption.yml`) rather than a plain `user_id`, and dereference it back to the Postgres `User`. When querying check-in data by user, filter on `encrypted_user_id`, not `user_id`. `Checkin` embeds condition/symptom/treatment sub-documents inline. diff --git a/README.md b/README.md index ca07ac4b..3e2d02e8 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,10 @@ Help would be appreciated! Please join us in [slack #flaredown](https://join.sla ## Environment -* PostgreSQL 12.8 -* MongoDB 4.4.9 -* Redis 6.2.3 -* Ruby 3.2.3 +* PostgreSQL 17.10 +* MongoDB 8.0.28 +* Redis 7.2.15 +* Ruby 3.4.10 * Node 12.22.6 ## Installation From 33e26141062d80cf2163a1c9e36672d809b9aa9a Mon Sep 17 00:00:00 2001 From: compwron Date: Mon, 10 Aug 2026 23:09:41 +0000 Subject: [PATCH 14/16] Fix README instructions this upgrade invalidated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things in the README stopped being true. `bundle config set --local without 'production'` excluded a group that no longer exists — rails_12factor was the only gem in it, so dropping the gem took the group with it. The line is inert rather than harmful, but it writes a setting into backend/.bundle/config that means nothing. The datastore bump needs a note for anyone with an existing checkout. The README mentioned `docker compose down -v` only as an optional full reset; after this branch it is required once, because neither PostgreSQL nor MongoDB will open a data directory written by an older major version. Without it `docker compose --profile dev up` fails, and the error does not obviously point at the volume. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3e2d02e8..890dcf17 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,13 @@ Visit your app at [http://localhost:4300](http://localhost:4300). Frontend dependency changes are handled automatically by Docker. For a full reset of all local Docker data, including databases and dependency volumes, run `docker compose down -v`, then run the database setup command again afterward. +If you already had the stack running before the PostgreSQL 17 and MongoDB 8 upgrade, you have to do that reset once. Neither database will start against a data directory written by an older major version, so `docker compose --profile dev up` fails until the old volumes are gone. The local data is disposable: + +```bash +docker compose down -v +docker compose --profile tools run --rm app-setup +``` + ### Running natively #### Mac Prerequisites @@ -79,7 +86,6 @@ On macOS, you can install `libpq` by running `brew install libpq && brew link -- ```bash cd backend echo "gem: --no-ri --no-rdoc" > ~/.gemrc -bundle config set --local without 'production' bundle config set --local jobs 5 bundle config set --local retry 10 bundle install From 2dca56f34ab1a7903aa6f46d6cd9b927dbf98aba Mon Sep 17 00:00:00 2001 From: compwron Date: Wed, 12 Aug 2026 22:18:19 +0000 Subject: [PATCH 15/16] Give the test environment a DISCOURSE_URL `Flaredown.config.discourse_url` is a bare `ENV.fetch("DISCOURSE_URL")`, so rendering SessionSerializer raises KeyError wherever that variable is unset. Nothing exercised it until this branch covered the OmniAuth callback, which renders a session on success: KeyError: key not found: "DISCOURSE_URL" ./config/initializers/00_app.rb:26:in 'Flaredown::Settings#discourse_url' ./app/serializers/api/v1/session_serializer.rb:31 A developer's .env hides it -- env-example ships DISCOURSE_URL empty, and an empty value is enough for `fetch` -- so the three new examples pass locally and fail in CI, which has no .env. That is the whole of the rspec failure on this branch: reproduced by moving .env aside and running the suite with the workflow's environment, 321 examples, 3 failures, all three this KeyError. (Downloading the job's log needs admin rights on the repository, so the reproduction is the evidence.) The default belongs with the other test-only values here rather than in the two spec files, so that any later spec rendering a session is covered as well. Real environments keep the fetch and its fail-fast behaviour. Assigned when blank rather than when nil, for the env-example reason above. Co-Authored-By: Claude Opus 5 (1M context) --- backend/config/environments/test.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/config/environments/test.rb b/backend/config/environments/test.rb index caedf19a..69304392 100644 --- a/backend/config/environments/test.rb +++ b/backend/config/environments/test.rb @@ -17,6 +17,11 @@ ENV["FACEBOOK_APP_ID"] = "1234567890" if ENV["FACEBOOK_APP_ID"].blank? ENV["FACEBOOK_APP_SECRET"] = "facebook-app-secret" if ENV["FACEBOOK_APP_SECRET"].blank? +# Flaredown.config.discourse_url is a bare ENV.fetch, so rendering +# SessionSerializer raises KeyError wherever DISCOURSE_URL is unset. A .env hides +# that locally; CI has no .env. +ENV["DISCOURSE_URL"] = "https://community.flaredown.test" if ENV["DISCOURSE_URL"].blank? + # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped From df3bc7dc18f269b015dc3dbc050fc358686bcf72 Mon Sep 17 00:00:00 2001 From: compwron Date: Wed, 12 Aug 2026 22:18:19 +0000 Subject: [PATCH 16/16] Draw the routes before driving the OmniAuth callback spec/requests/omniauth_spec.rb passes in a full run and fails on its own: NoMethodError: undefined method 'each_key' for nil ./app/controllers/api/v1/omniauth_callbacks_controller.rb:27 Devise clears `OmniAuth.config.path_prefix` when it loads (devise/omniauth.rb:13) and sets it to `omniauth_path_prefix` only when `devise_for` draws its routes (devise/rails/routes.rb:443). The strategy is middleware and runs ahead of the router, so until the routes have been drawn it takes callback_path to be "/facebook/callback", does not recognise POST /api/auth/facebook/callback as a callback at all, and passes the request to the app with `omniauth.auth` unset -- the nil above. Routes are drawn lazily unless eager loading is on, so in a full run an earlier spec has already drawn them and the file passes. CI eager loads, `config.eager_load = ENV["CI"].present?`, so this was never the red build; it is a trap for anyone running the file by itself. Co-Authored-By: Claude Opus 5 (1M context) --- backend/spec/requests/omniauth_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/spec/requests/omniauth_spec.rb b/backend/spec/requests/omniauth_spec.rb index bba83a40..da0fda30 100644 --- a/backend/spec/requests/omniauth_spec.rb +++ b/backend/spec/requests/omniauth_spec.rb @@ -9,6 +9,15 @@ let(:app_id) { ENV["FACEBOOK_APP_ID"] } let(:app_secret) { ENV["FACEBOOK_APP_SECRET"] } + # Devise clears OmniAuth's path_prefix when it loads and sets it to + # omniauth_path_prefix only when devise_for draws its routes. The strategy runs + # ahead of the router, so until that has happened it takes callback_path to be + # "/facebook/callback", does not recognise these requests as callbacks, and + # passes them to the app with no omniauth.auth. Routes are drawn lazily unless + # eager loading is on, which is why the examples below otherwise pass in a full + # run -- an earlier spec has already drawn them -- and fail on their own. + before { Rails.application.reload_routes_unless_loaded } + around do |example| example.run ensure