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
|
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!
|
def authenticate_user!
|
||||||
redirect_to(new_auth_user_session_path) unless user_signed_in?
|
redirect_to(new_auth_user_session_path) unless user_signed_in?
|
||||||
end
|
end
|
||||||
|
|
@ -73,6 +77,11 @@ class ApplicationController < ActionController::Base
|
||||||
status: :service_unavailable
|
status: :service_unavailable
|
||||||
end
|
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)
|
def redirect_back!(default=:back)
|
||||||
redirect_to(params[:return_to] || default)
|
redirect_to(params[:return_to] || default)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,39 @@
|
||||||
class AuthUser < AuthRecord
|
class AuthUser < AuthRecord
|
||||||
self.table_name = 'users'
|
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,
|
devise :database_authenticatable, :encryptable, :registerable, :validatable,
|
||||||
:rememberable, :trackable, :recoverable, :omniauthable,
|
:rememberable, :trackable, :recoverable, :omniauthable,
|
||||||
omniauth_providers: [:neopass]
|
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},
|
validates :name, presence: true, uniqueness: {case_sensitive: false},
|
||||||
length: {maximum: 30}
|
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.
|
# 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|
|
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_unicode_520_ci", force: :cascade do |t|
|
||||||
t.integer "species_id", null: false
|
t.integer "species_id", null: false
|
||||||
t.integer "color_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
|
t.string "secret", limit: 64, null: false
|
||||||
end
|
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|
|
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 "progress", default: 0, null: false
|
||||||
t.integer "goal", null: false
|
t.integer "goal", null: false
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue