feat: add write lock for AuthUser migration preparation
This commit implements a temporary write lock on the AuthUser model to prepare for consolidating the openneo_id database into the main database. Changes: - AuthUser: Added before_save/before_destroy callbacks that raise TemporarilyReadOnly exception to prevent any writes - AuthUser: Override update_tracked_fields! to silently skip Devise login tracking (prevents crashes during login while maintaining read access) - ApplicationController: Added rescue_from handler for TemporarilyReadOnly that redirects to root with a friendly maintenance message - Migration: Created CopyAuthUsersTableToMainDatabase to copy openneo_id.users → openneo_impress.auth_users (preserves all IDs) This allows us to run the data copy migration in production while: - Keeping existing users able to log in and use the site - Blocking new registrations, settings changes, and NeoPass connections - Losing some login tracking data (acceptable tradeoff) Next step: Deploy this, then run the migration in production while the table is stable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
77872311e6
commit
604a8667cf
4 changed files with 111 additions and 1 deletions
|
|
@ -27,6 +27,10 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
rescue_from ActiveRecord::ConnectionTimeoutError, with: :on_db_timeout
|
||||
|
||||
# TEMPORARY: Rescue handler for database migration write lock
|
||||
# This catches attempts to modify AuthUser records during the migration.
|
||||
rescue_from AuthUser::TemporarilyReadOnly, with: :on_auth_user_read_only
|
||||
|
||||
def authenticate_user!
|
||||
redirect_to(new_auth_user_session_path) unless user_signed_in?
|
||||
end
|
||||
|
|
@ -73,6 +77,11 @@ class ApplicationController < ActionController::Base
|
|||
status: :service_unavailable
|
||||
end
|
||||
|
||||
def on_auth_user_read_only
|
||||
flash[:alert] = "Account changes are temporarily unavailable during maintenance. Please try again shortly."
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
def redirect_back!(default=:back)
|
||||
redirect_to(params[:return_to] || default)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,10 +1,39 @@
|
|||
class AuthUser < AuthRecord
|
||||
self.table_name = 'users'
|
||||
|
||||
# TEMPORARY: Write lock for database migration
|
||||
# During the migration to consolidate databases, we need to prevent writes to
|
||||
# the AuthUser table while keeping reads (like login) working. This will be
|
||||
# removed in the next phase of the migration.
|
||||
class TemporarilyReadOnly < StandardError; end
|
||||
|
||||
# Block all save attempts via before_save callback (more reliable than overriding methods)
|
||||
before_save :prevent_writes_during_migration
|
||||
before_destroy :prevent_writes_during_migration
|
||||
|
||||
def prevent_writes_during_migration
|
||||
raise TemporarilyReadOnly, "Account changes temporarily unavailable during maintenance"
|
||||
end
|
||||
|
||||
devise :database_authenticatable, :encryptable, :registerable, :validatable,
|
||||
:rememberable, :trackable, :recoverable, :omniauthable,
|
||||
omniauth_providers: [:neopass]
|
||||
|
||||
# Disable Devise's trackable feature during migration to prevent login tracking updates
|
||||
# This means we'll lose some login tracking data during the migration window, but that's
|
||||
# acceptable compared to breaking logins entirely.
|
||||
def update_tracked_fields!(request)
|
||||
# Normally this would update sign_in_count, current_sign_in_at, etc.
|
||||
# During migration, we just skip it silently.
|
||||
end
|
||||
|
||||
# Disable Devise's rememberable feature during migration to prevent logout from crashing
|
||||
# This means remember_created_at won't be cleared on logout, but that's acceptable.
|
||||
def forget_me!
|
||||
# Normally this would update remember_created_at to nil.
|
||||
# During migration, we just skip it silently.
|
||||
end
|
||||
|
||||
validates :name, presence: true, uniqueness: {case_sensitive: false},
|
||||
length: {maximum: 30}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
class CopyAuthUsersTableToMainDatabase < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
# Create auth_users table in openneo_impress with same structure as openneo_id.users
|
||||
# This preserves all IDs, data, and constraints from the source table.
|
||||
create_table "auth_users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t|
|
||||
t.string "name", limit: 30, null: false
|
||||
t.string "encrypted_password", limit: 64
|
||||
t.string "email", limit: 50
|
||||
t.string "password_salt", limit: 32
|
||||
t.string "reset_password_token"
|
||||
t.integer "sign_in_count", default: 0
|
||||
t.datetime "current_sign_in_at", precision: nil
|
||||
t.datetime "last_sign_in_at", precision: nil
|
||||
t.string "current_sign_in_ip"
|
||||
t.string "last_sign_in_ip"
|
||||
t.integer "failed_attempts", default: 0
|
||||
t.string "unlock_token"
|
||||
t.datetime "locked_at", precision: nil
|
||||
t.datetime "created_at", precision: nil
|
||||
t.datetime "updated_at", precision: nil
|
||||
t.datetime "reset_password_sent_at", precision: nil
|
||||
t.datetime "remember_created_at"
|
||||
t.string "provider"
|
||||
t.string "uid"
|
||||
t.string "neopass_email"
|
||||
end
|
||||
|
||||
# Add indexes (matching openneo_id.users schema)
|
||||
add_index "auth_users", ["email"], name: "index_auth_users_on_email", unique: true
|
||||
add_index "auth_users", ["provider", "uid"], name: "index_auth_users_on_provider_and_uid", unique: true
|
||||
add_index "auth_users", ["reset_password_token"], name: "index_auth_users_on_reset_password_token", unique: true
|
||||
add_index "auth_users", ["unlock_token"], name: "index_auth_users_on_unlock_token", unique: true
|
||||
|
||||
# Copy all data from openneo_id.users to openneo_impress.auth_users
|
||||
# This preserves all IDs so that User.remote_id continues to reference the correct AuthUser
|
||||
execute <<-SQL
|
||||
INSERT INTO openneo_impress.auth_users
|
||||
SELECT * FROM openneo_id.users
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table "auth_users"
|
||||
end
|
||||
end
|
||||
29
db/schema.rb
29
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: 2025_02_16_041650) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_11_02_064247) do
|
||||
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||
t.integer "species_id", null: false
|
||||
t.integer "color_id", null: false
|
||||
|
|
@ -32,6 +32,33 @@ ActiveRecord::Schema[8.0].define(version: 2025_02_16_041650) do
|
|||
t.string "secret", limit: 64, null: false
|
||||
end
|
||||
|
||||
create_table "auth_users", id: { type: :integer, unsigned: true }, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t|
|
||||
t.string "name", limit: 30, null: false
|
||||
t.string "encrypted_password", limit: 64
|
||||
t.string "email", limit: 50
|
||||
t.string "password_salt", limit: 32
|
||||
t.string "reset_password_token"
|
||||
t.integer "sign_in_count", default: 0
|
||||
t.datetime "current_sign_in_at", precision: nil
|
||||
t.datetime "last_sign_in_at", precision: nil
|
||||
t.string "current_sign_in_ip"
|
||||
t.string "last_sign_in_ip"
|
||||
t.integer "failed_attempts", default: 0
|
||||
t.string "unlock_token"
|
||||
t.datetime "locked_at", precision: nil
|
||||
t.datetime "created_at", precision: nil
|
||||
t.datetime "updated_at", precision: nil
|
||||
t.datetime "reset_password_sent_at", precision: nil
|
||||
t.datetime "remember_created_at"
|
||||
t.string "provider"
|
||||
t.string "uid"
|
||||
t.string "neopass_email"
|
||||
t.index ["email"], name: "index_auth_users_on_email", unique: true
|
||||
t.index ["provider", "uid"], name: "index_auth_users_on_provider_and_uid", unique: true
|
||||
t.index ["reset_password_token"], name: "index_auth_users_on_reset_password_token", unique: true
|
||||
t.index ["unlock_token"], name: "index_auth_users_on_unlock_token", unique: true
|
||||
end
|
||||
|
||||
create_table "campaigns", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||
t.integer "progress", default: 0, null: false
|
||||
t.integer "goal", null: false
|
||||
|
|
|
|||
Loading…
Reference in a new issue