From 126c37a744198762adb5e58653381eb1b9579fcd Mon Sep 17 00:00:00 2001 From: crespire Date: Fri, 10 Jul 2026 22:45:09 -0400 Subject: [PATCH 1/8] Add User model with encrypted secure password and session token Introduces the persistence layer for admin auth: bcrypt-backed password_digest, a rotatable session_token from has_secure_token, and Active Record Encryption on both. An admins:sync rake task upserts admins from Rails.application.credentials.admins; db/seeds.rb and the docker entrypoint both invoke it so admin credentials propagate on every deploy without touching event fixtures. Encryption keys and the admins list live in encrypted credentials; the previous auth: entry (used by HTTP basic) is removed in the same edit. --- Gemfile | 2 +- Gemfile.lock | 2 ++ app/models/user.rb | 11 +++++++++++ bin/docker-entrypoint | 5 ++++- config/credentials.yml.enc | 2 +- db/migrate/20260711022932_create_users.rb | 14 ++++++++++++++ db/schema.rb | 12 +++++++++++- db/seeds.rb | 3 +++ lib/tasks/admins.rake | 13 +++++++++++++ 9 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 app/models/user.rb create mode 100644 db/migrate/20260711022932_create_users.rb create mode 100644 lib/tasks/admins.rake diff --git a/Gemfile b/Gemfile index c26442f..374be9e 100644 --- a/Gemfile +++ b/Gemfile @@ -33,7 +33,7 @@ gem 'redis', '>= 4.0.1' # gem "kredis" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem "bcrypt", "~> 3.1.7" +gem 'bcrypt', '~> 3.1.7' # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[windows jruby] diff --git a/Gemfile.lock b/Gemfile.lock index ba7772c..9c5ea0f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,7 @@ GEM tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) base64 (0.3.0) + bcrypt (3.1.22) benchmark (0.5.0) bigdecimal (4.1.2) bindex (0.8.1) @@ -262,6 +263,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + bcrypt (~> 3.1.7) bootsnap cssbundling-rails csv diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..5264bfb --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,11 @@ +class User < ApplicationRecord + has_secure_password + has_secure_token :session_token + + encrypts :password_digest + encrypts :session_token, deterministic: true + + normalizes :username, with: ->(v) { v.to_s.strip.downcase } + + validates :username, presence: true, uniqueness: true +end diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index 67ef493..9b33b9c 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -1,8 +1,11 @@ #!/bin/bash -e -# If running the rails server then create or migrate existing database +# If running the rails server then create or migrate existing database, +# then upsert admins from encrypted credentials. admins:sync is idempotent +# (find_or_initialize_by on username) and does not touch event fixtures. if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then ./bin/rails db:prepare + ./bin/rails admins:sync fi exec "${@}" diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 9af46a3..d4d9eec 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -L18YmnmQ7HVCtRlwFNxwc6o+I59OjZMzUA2KVdQ+TwesMDTOH7amkAqJHrILU9XvCaP+v3WBFdwZb8+MoLE6CS7AUiKh5tP99uNiw8Ri/iWSNIvX6jZorKlOdszaKs/KGr+0Exe3NVvOUeD6t/8CRWKTdQvTwOedpzloIhhlIz0r0Fi/+tT2dzezH91aKKyCr3hKYbkD7Wvai8/36zGC3JOWepTjsRxsHA6PCjwV24g+7oBnsH7KiboW1whGuSz8SOVyPHJZFQvwFi571oAxQLY0qUW7C0qkUA/8TMoIcEtmcLEPbLJE9Nya/pe07xR8MQetKb2q29HD1Aa4/leygO2EyONUMpDPg0Cm/XJTOswvkhGsmp+yXVeyjr8ISmVyKR7X25+a8Yhj9JuTnP+SRb0FIWnjWPD5rRE/VFKA/96JEiWfHp4IaZ9iHwhJ9C5AP7G2n280mwcrWiKTaqc0U2g2dkb6XDxfy+ozAl1T0wSRlCnB9twGSyVAH9eSaIzzeZx6i46uPlP6q/Y4jWp8lfNaKxCoGYixeQzlK6DwKHnmbdmtx/2xgJEmWdDt--VPOiYqTn+QXyR0PA--1bBCdyTlIYOVD/PPlfLLNA== \ No newline at end of file +Ukz+P33RYHWu9L3Y3cTWNa1OT6pp0XBxHF8fTmkOkWsTxtQ1vhcI55K2BuSg7WedwQa/CLaGQXVwn1qs/mO0HSglXYGp3T/x+EfNXjGOp3TDlwgwS+Q3fCqkwXpD4/50FXlEVU5MgsY+yEfwWyy/XpY/byHQacyoj+UIfPMiPE8WEyLA8ImDxEQLH6UxBmY9GqXUTRJiyD6fxiIChuHnyNabwIDklRaYvObwiJd1aZUgj4hh5bNl3YNCtN4mUbV1Vd9HjDQsoY/5mugkK0F4SSnnqL3/40pZQblRUmz9kKF5AEJS/eH4fOPgZPC3es+4gT8PllpF1mNSsl/pM6wuHmgcIgZA3Vp5mpiAfvjec8igUsbnBkYIj5TpcSRZjxWvubGuMMaQ455dN25fV77AD4bdCGtE55AI39v4fPQekOJFJRvhxWrdsSmPk60d0mBmiPNGKwdHKG33diylbfqIZe0KNWwIpawSmoVRK4YkAIYws24ZaRHp3vw7zK48ryx2lQ7Q+2vez7HklSTGOXWdHhKyXKMr3y9+PNxiEEQxqCGwrtcCTMrXXX2zFvpUra3hB27eNVapx2bXEJZKNNS46Copi7gCyziR00KrFIUSO1M=--U9HaVnhO9PdPHU2z--vPBFOb2my1bEWWgRaoWSGw== \ No newline at end of file diff --git a/db/migrate/20260711022932_create_users.rb b/db/migrate/20260711022932_create_users.rb new file mode 100644 index 0000000..cddbe7b --- /dev/null +++ b/db/migrate/20260711022932_create_users.rb @@ -0,0 +1,14 @@ +class CreateUsers < ActiveRecord::Migration[8.0] + def change + create_table :users do |t| + t.string :username, null: false + t.string :password_digest, null: false + t.string :session_token + + t.timestamps + end + + add_index :users, :username, unique: true + add_index :users, :session_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 4a881eb..1304014 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_05_05_011213) do +ActiveRecord::Schema[8.0].define(version: 2026_07_11_022932) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -67,6 +67,16 @@ t.index ["slug"], name: "index_events_on_slug", unique: true end + create_table "users", force: :cascade do |t| + t.string "username", null: false + t.string "password_digest", null: false + t.string "session_token" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["session_token"], name: "index_users_on_session_token", unique: true + t.index ["username"], name: "index_users_on_username", unique: true + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" end diff --git a/db/seeds.rb b/db/seeds.rb index ef5d066..c5bbc13 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -120,3 +120,6 @@ e.description = event[:description] e.save! end + +Rails.application.load_tasks +Rake::Task['admins:sync'].invoke diff --git a/lib/tasks/admins.rake b/lib/tasks/admins.rake new file mode 100644 index 0000000..1af6c63 --- /dev/null +++ b/lib/tasks/admins.rake @@ -0,0 +1,13 @@ +namespace :admins do + desc 'Upsert admin users from Rails.application.credentials.admins' + task sync: :environment do + admins = Rails.application.credentials.admins || + (Rails.env.production? ? raise('admins missing from credentials') : [{ username: 'trbadmin', password: 'admin' }]) + + admins.each do |attrs| + user = User.find_or_initialize_by(username: attrs[:username]) + user.password = attrs[:password] + user.save! + end + end +end From 5271682ca1941013254b9519e90fbfcab1b69da5 Mon Sep 17 00:00:00 2001 From: crespire Date: Fri, 10 Jul 2026 22:45:18 -0400 Subject: [PATCH 2/8] Replace HTTP basic auth with session-based login for admin Admin::BaseController now runs before_action :authenticate_admin! instead of http_basic_authenticate_with. Sessions are keyed on the user's session_token (not user id) and looked up via .sole so any duplicate raises rather than silently signing someone in. Sign-in rotates the token; sign-out nils it, revoking the cookie server-side. Adds a sign-out button to the navigation partial when signed_in?, and a minimal Tailwind login view at /login. --- app/controllers/admin/base_controller.rb | 5 +-- app/controllers/application_controller.rb | 23 ++++++++++++ app/controllers/sessions_controller.rb | 26 ++++++++++++++ app/views/layouts/common/_navigation.html.erb | 6 ++++ app/views/sessions/new.html.erb | 35 +++++++++++++++++++ config/routes.rb | 4 +++ 6 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/views/sessions/new.html.erb diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 5d421f7..c17667f 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -1,8 +1,5 @@ module Admin class BaseController < ApplicationController - http_basic_authenticate_with( - name: Rails.env.production? ? Rails.application.credentials.auth.user : 'admin', - password: Rails.env.production? ? Rails.application.credentials.auth.password : 'admin' - ) + before_action :authenticate_admin! end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d1..3933e88 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,25 @@ class ApplicationController < ActionController::Base + helper_method :current_user, :signed_in? + + private + + def current_user + return @current_user if defined?(@current_user) + + token = session[:session_token] + @current_user = token && User.where(session_token: token).sole + rescue ActiveRecord::RecordNotFound + @current_user = nil + end + + def signed_in? + current_user.present? + end + + def authenticate_admin! + return if signed_in? + + session[:return_to] = request.fullpath if request.get? + redirect_to login_path, alert: 'Please sign in to continue.' + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..a7a85cc --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,26 @@ +class SessionsController < ApplicationController + rate_limit to: 10, within: 3.minutes, only: :create, + with: -> { redirect_to login_path, alert: 'Too many attempts. Try again in a few minutes.' } + + def new; end + + def create + user = User.authenticate_by(username: params[:username].to_s, password: params[:password].to_s) + if user + user.regenerate_session_token + return_to = session[:return_to] + reset_session + session[:session_token] = user.session_token + redirect_to(return_to || admin_events_path, notice: 'Signed in.') + else + flash.now[:alert] = 'Invalid username or password.' + render :new, status: :unprocessable_entity + end + end + + def destroy + current_user&.update!(session_token: nil) + reset_session + redirect_to login_path, notice: 'Signed out.' + end +end diff --git a/app/views/layouts/common/_navigation.html.erb b/app/views/layouts/common/_navigation.html.erb index 4d993ae..1d74806 100644 --- a/app/views/layouts/common/_navigation.html.erb +++ b/app/views/layouts/common/_navigation.html.erb @@ -21,6 +21,9 @@ <% end %> <%= link_to "Chat", chat_path, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors" %> <%= link_to "Past Events", past_events_path, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors" %> + <% if signed_in? %> + <%= button_to "Sign out", logout_path, method: :delete, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors bg-transparent border-0 p-0 cursor-pointer" %> + <% end %> @@ -47,6 +50,9 @@ <% end %> <%= link_to "Chat", chat_path, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors px-3 py-2 rounded-lg hover:bg-ruby-50" %> <%= link_to "Past Events", past_events_path, class: "text-gray-700 hover:text-ruby-500 font-medium transition-colors px-3 py-2 rounded-lg hover:bg-ruby-50" %> + <% if signed_in? %> + <%= button_to "Sign out", logout_path, method: :delete, class: "text-left text-gray-700 hover:text-ruby-500 font-medium transition-colors px-3 py-2 rounded-lg hover:bg-ruby-50 bg-transparent border-0 cursor-pointer w-full" %> + <% end %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000..c6a5107 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,35 @@ +<% content_for :page_title, "Sign in" %> + +
+
+

Sign in

+ + <%= form_with url: login_path, scope: :session, local: true do |form| %> +
+
+ <%= form.label :username, class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= form.text_field :username, + name: :username, + value: params[:username], + autocomplete: "username", + autofocus: true, + required: true, + class: "block w-full px-4 py-3 rounded-lg border border-gray-300 shadow-sm focus:border-ruby-500 focus:ring-1 focus:ring-ruby-500 transition-colors" %> +
+ +
+ <%= form.label :password, class: "block text-sm font-medium text-gray-700 mb-1" %> + <%= form.password_field :password, + name: :password, + autocomplete: "current-password", + required: true, + class: "block w-full px-4 py-3 rounded-lg border border-gray-300 shadow-sm focus:border-ruby-500 focus:ring-1 focus:ring-ruby-500 transition-colors" %> +
+ +
+ <%= form.submit "Sign in", class: "btn-primary w-full" %> +
+
+ <% end %> +
+
diff --git a/config/routes.rb b/config/routes.rb index 3c637d0..f99a8ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,8 @@ Rails.application.routes.draw do + get 'login', to: 'sessions#new', as: :login + post 'login', to: 'sessions#create' + delete 'logout', to: 'sessions#destroy', as: :logout + namespace :admin do root to: redirect('/admin/events') resources :events From ec74aaa8cd8bdbec3233c82d3d2b76efba5bf3f4 Mon Sep 17 00:00:00 2001 From: crespire Date: Fri, 10 Jul 2026 22:45:35 -0400 Subject: [PATCH 3/8] Migrate admin controller tests from HTTP basic to session login Adds a sign_in_as helper to test_helper for integration tests, rewrites Admin::EventsControllerTest to sign in through /login instead of sending Authorization headers, and adds SessionsControllerTest covering GET/POST/DELETE plus the unauthenticated redirect and username normalization paths. --- .../admin/events_controller_test.rb | 57 +++++-------------- test/controllers/sessions_controller_test.rb | 53 +++++++++++++++++ test/test_helper.rb | 12 ++++ 3 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 test/controllers/sessions_controller_test.rb diff --git a/test/controllers/admin/events_controller_test.rb b/test/controllers/admin/events_controller_test.rb index 3512f82..2e18c8a 100644 --- a/test/controllers/admin/events_controller_test.rb +++ b/test/controllers/admin/events_controller_test.rb @@ -3,6 +3,10 @@ class Admin::EventsControllerTest < ActionDispatch::IntegrationTest def setup Event.destroy_all + User.destroy_all + @admin = User.create!(username: 'trbadmin', password: 'admin') + sign_in_as(@admin) + begining = Date.parse('2024-01-01').beginning_of_year dates = 12.times.map { |i| begining + i.months } @events = dates.map do |date| @@ -20,27 +24,14 @@ def setup end test 'should load the index page' do - get admin_events_path, - headers: { - Authorization: - ActionController::HttpAuthentication::Basic.encode_credentials( - 'admin', 'admin' - ) - } - + get admin_events_path assert_response :success end test 'index should show draft events' do Event.update_all(status: :draft) - get admin_events_path, - headers: { - Authorization: - ActionController::HttpAuthentication::Basic.encode_credentials( - 'admin', 'admin' - ) - } + get admin_events_path assert_response :success assert_equal 'text/html; charset=utf-8', response.content_type @@ -48,26 +39,14 @@ def setup end test 'should show a single event' do - get admin_event_path(Event.first.slug), - headers: { - Authorization: - ActionController::HttpAuthentication::Basic.encode_credentials( - 'admin', 'admin' - ) - } + get admin_event_path(Event.first.slug) assert_response :ok assert response.body.include?(Event.first.name) end test 'should redirect to events page if event is not found' do - get admin_event_path('nonexistent-event'), - headers: { - Authorization: - ActionController::HttpAuthentication::Basic.encode_credentials( - 'admin', 'admin' - ) - } + get admin_event_path('nonexistent-event') assert_response :redirect assert_redirected_to admin_events_path @@ -75,13 +54,7 @@ def setup test 'edit form should show all fields for the event' do # Also tests the form has all our model's attributes - get admin_event_path(Event.first.slug), - headers: { - Authorization: - ActionController::HttpAuthentication::Basic.encode_credentials( - 'admin', 'admin' - ) - } + get admin_event_path(Event.first.slug) # Get all attributes and rich text associations editable = Event.rich_text_association_names.map { |assoc| @@ -116,12 +89,6 @@ def setup 'start_at' => '2024-11-25T19:30', 'status' => 'published' } - }, - headers: { - Authorization: - ActionController::HttpAuthentication::Basic.encode_credentials( - 'admin', 'admin' - ) } got = Event.last @@ -130,4 +97,10 @@ def setup } assert_equal(want[:start_at].utc, got.start_at) end + + test 'unauthenticated request redirects to login' do + reset! + get admin_events_path + assert_redirected_to login_path + end end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb new file mode 100644 index 0000000..970fe6f --- /dev/null +++ b/test/controllers/sessions_controller_test.rb @@ -0,0 +1,53 @@ +require 'test_helper' + +class SessionsControllerTest < ActionDispatch::IntegrationTest + def setup + User.destroy_all + @admin = User.create!(username: 'trbadmin', password: 'admin') + end + + test 'GET /login renders the form' do + get login_path + assert_response :success + assert_select 'input[name=username]' + assert_select 'input[name=password][type=password]' + end + + test 'POST /login with valid credentials signs in and rotates session token' do + original_token = @admin.session_token + + post login_path, params: { username: 'trbadmin', password: 'admin' } + + assert_redirected_to admin_events_path + @admin.reload + assert_not_equal original_token, @admin.session_token + end + + test 'POST /login normalizes username case and whitespace' do + post login_path, params: { username: ' TRBADMIN ', password: 'admin' } + assert_redirected_to admin_events_path + end + + test 'POST /login with wrong password re-renders' do + post login_path, params: { username: 'trbadmin', password: 'nope' } + assert_response :unprocessable_entity + end + + test 'POST /login with unknown user re-renders' do + post login_path, params: { username: 'nobody', password: 'admin' } + assert_response :unprocessable_entity + end + + test 'DELETE /logout clears session token and redirects' do + post login_path, params: { username: 'trbadmin', password: 'admin' } + delete logout_path + + assert_redirected_to login_path + assert_nil @admin.reload.session_token + end + + test 'unauthenticated request to admin redirects to login' do + get admin_events_path + assert_redirected_to login_path + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 62f75b9..7de7026 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -13,3 +13,15 @@ class TestCase # Add more helper methods to be used by all tests here... end end + +module SignInHelper + def sign_in_as(user, password: 'admin') + post login_path, params: { username: user.username, password: password } + end +end + +module ActionDispatch + class IntegrationTest + include SignInHelper + end +end From 92545b3bf38c7097e6e2db46afd9e3b273864267 Mon Sep 17 00:00:00 2001 From: crespire Date: Fri, 10 Jul 2026 22:45:41 -0400 Subject: [PATCH 4/8] Document admin auth setup in README Notes that admin users and Active Record Encryption keys live in encrypted credentials, and points at credentials:edit + db:seed as the workflow for adding or rotating admins. --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 0f61841..f4d0062 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,17 @@ The database and storage configurations are provided in a `.sample` file, make s This should get the application setup and ready to run. +### Admin authentication +Admin access (creating/editing events) is gated behind a login screen backed by a `User` model with `has_secure_password`. Admin users are seeded from the encrypted credentials under the `admins:` key — add or update entries there and re-run `bin/rails db:seed` to upsert. The credentials file also holds the `active_record_encryption` keys used to encrypt `password_digest` and `session_token` at rest. To rotate or add admins: + +``` +bin/rails credentials:edit +# then: +bin/rails db:seed +``` + +The default seeded admin is `trbadmin`. + To run the application locally, in two separate consoles: 1. Run `bin/dev` - watches and rebuilds JS and CSS (runs `Procfile.dev` via foreman) 1. Run `bin/rails s` - starts the Rails server From 63b03f05b3d15e50dbb90b73dbc13c1eaba5b741 Mon Sep 17 00:00:00 2001 From: crespire Date: Sat, 11 Jul 2026 11:44:24 -0400 Subject: [PATCH 5/8] Configure static Active Record encryption keys for the test environment CI has no master.key, so Rails can't decrypt credentials at boot and the User model's encrypts declarations fail with MissingKeyError. Test data is throwaway; fixed keys in test.rb keep the suite bootable without exposing the production master key to CI. --- config/environments/test.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/environments/test.rb b/config/environments/test.rb index 1ab0001..7054193 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -52,4 +52,11 @@ # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true + + # Static Active Record encryption keys for the test suite. Production keys + # live in encrypted credentials; test data is throwaway and CI doesn't have + # (nor should it need) the master key. + config.active_record.encryption.primary_key = 'test_primary_key_not_for_production_use_1234' + config.active_record.encryption.deterministic_key = 'test_deterministic_key_not_for_production_1234' + config.active_record.encryption.key_derivation_salt = 'test_key_derivation_salt_not_for_production_1234' end From 297673499b5426cf18fbfbabacce746e9194c204 Mon Sep 17 00:00:00 2001 From: crespire Date: Sat, 11 Jul 2026 11:55:31 -0400 Subject: [PATCH 6/8] Bundle update --- Gemfile.lock | 58 ++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9c5ea0f..8278d10 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,12 +77,12 @@ GEM benchmark (0.5.0) bigdecimal (4.1.2) bindex (0.8.1) - bootsnap (1.24.2) + bootsnap (1.24.6) msgpack (~> 1.2) builder (3.3.0) - concurrent-ruby (1.3.6) + concurrent-ruby (1.3.7) connection_pool (3.0.2) - crass (1.0.6) + crass (1.0.7) cssbundling-rails (1.4.3) railties (>= 6.0.0) csv (3.3.5) @@ -96,14 +96,14 @@ GEM ffi (1.17.4-arm64-darwin) ffi (1.17.4-x86_64-darwin) ffi (1.17.4-x86_64-linux-gnu) - globalid (1.3.0) + globalid (1.4.0) activesupport (>= 6.1) honeybadger (5.30.0) logger ostruct - i18n (1.14.8) + i18n (1.15.2) concurrent-ruby (~> 1.0) - icalendar (2.12.2) + icalendar (2.12.3) base64 ice_cube (~> 0.16) logger @@ -118,7 +118,7 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - jbuilder (2.14.1) + jbuilder (2.15.1) actionview (>= 7.0.0) activesupport (>= 7.0.0) jsbundling-rails (1.3.1) @@ -127,21 +127,21 @@ GEM loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.9.0) + mail (2.9.1) logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - marcel (1.1.0) + marcel (1.2.1) mini_magick (5.3.1) logger mini_mime (1.1.5) minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) - msgpack (1.8.0) - net-imap (0.6.4) + msgpack (1.8.3) + net-imap (0.6.4.1) date net-protocol net-pop (0.1.2) @@ -151,24 +151,21 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.19.3-arm64-darwin) + nokogiri (1.19.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.3-x86_64-darwin) + nokogiri (1.19.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.19.3-x86_64-linux-gnu) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) ostruct (0.6.3) pg (1.6.3-arm64-darwin) pg (1.6.3-x86_64-darwin) pg (1.6.3-x86_64-linux) - pp (0.6.3) + pp (0.6.4) prettyprint prettyprint (0.2.0) prism (1.9.0) - psych (5.3.1) - date - stringio - puma (8.0.1) + puma (8.0.2) nio4r (~> 2.0) racc (1.8.1) rack (3.2.6) @@ -210,13 +207,18 @@ GEM tsort (>= 0.2) zeitwerk (~> 2.6) rake (13.4.2) - rdoc (7.2.0) + rbs (4.0.3) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) erb - psych (>= 4.0.0) + prism (>= 1.6.0) + rbs (>= 4.0.0) tsort redis (5.4.1) redis-client (>= 0.22.0) - redis-client (0.28.0) + redis-client (0.30.0) connection_pool reline (0.6.3) io-console (~> 0.5) @@ -234,7 +236,6 @@ GEM sprockets (>= 3.0.0) stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.2.0) thor (1.5.0) timeout (0.6.1) tsort (0.2.0) @@ -245,16 +246,15 @@ GEM concurrent-ruby (~> 1.0) uri (1.1.1) useragent (0.16.11) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) + web-console (4.3.0) + actionview (>= 8.0.0) bindex (>= 0.4.0) - railties (>= 6.0.0) - websocket-driver (0.8.0) + railties (>= 8.0.0) + websocket-driver (0.8.2) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.7.5) + zeitwerk (2.8.2) PLATFORMS arm64-darwin-23 From 98a8670a553e51cef0f201fcbf7f7075ad955737 Mon Sep 17 00:00:00 2001 From: crespire Date: Sat, 11 Jul 2026 11:57:46 -0400 Subject: [PATCH 7/8] Inline admin upsert in seeds.rb to avoid task reload loop Calling Rails.application.load_tasks from within db:seed re-appends another load_seed action to db:seed's action list; rake then executes the appended action, which reruns seeds.rb, which calls load_tasks again, and the action list doubles on every pass. The rake task remains for manual invocation and the docker entrypoint. --- db/seeds.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index c5bbc13..1c91416 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -121,5 +121,10 @@ e.save! end -Rails.application.load_tasks -Rake::Task['admins:sync'].invoke +admins = Rails.application.credentials.admins || + (Rails.env.production? ? raise('admins missing from credentials') : [{ username: 'trbadmin', password: 'admin' }]) +admins.each do |attrs| + user = User.find_or_initialize_by(username: attrs[:username]) + user.password = attrs[:password] + user.save! +end From e0c4f7c893d9b7ed0826f49a3ab06f202c1903e1 Mon Sep 17 00:00:00 2001 From: crespire Date: Sat, 11 Jul 2026 11:58:00 -0400 Subject: [PATCH 8/8] Fix Gemlock after merge --- Gemfile.lock | 3 --- 1 file changed, 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9afe2ac..8278d10 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -165,9 +165,6 @@ GEM prettyprint prettyprint (0.2.0) prism (1.9.0) - psych (5.3.1) - date - stringio puma (8.0.2) nio4r (~> 2.0) racc (1.8.1)