diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1a3d973e..6a31d319 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/models/auth_user.rb b/app/models/auth_user.rb index 5894e749..a5f75433 100644 --- a/app/models/auth_user.rb +++ b/app/models/auth_user.rb @@ -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} diff --git a/db/migrate/20251102064247_copy_auth_users_table_to_main_database.rb b/db/migrate/20251102064247_copy_auth_users_table_to_main_database.rb new file mode 100644 index 00000000..260beeac --- /dev/null +++ b/db/migrate/20251102064247_copy_auth_users_table_to_main_database.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 809c71ff..54583185 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: 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