mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-28 00:16:48 +09:00
The sessions end wherever the password changes, but only the profile page closed the live connections, so a change from a console left a signed-out device receiving messages until it dropped. Close them after commit from User, logging rather than failing if the realtime service can't be reached, since the password is saved by then.
91 lines
3.2 KiB
Ruby
91 lines
3.2 KiB
Ruby
class User < ApplicationRecord
|
||
include Avatar, Bannable, Bot, Mentionable, Role, Transferable
|
||
|
||
has_many :memberships, dependent: :delete_all
|
||
has_many :rooms, through: :memberships
|
||
|
||
has_many :reachable_messages, through: :rooms, source: :messages
|
||
has_many :messages, dependent: :destroy, foreign_key: :creator_id
|
||
|
||
has_many :push_subscriptions, class_name: "Push::Subscription", dependent: :delete_all
|
||
|
||
has_many :boosts, dependent: :destroy, foreign_key: :booster_id
|
||
has_many :searches, dependent: :delete_all
|
||
|
||
has_many :sessions, dependent: :destroy
|
||
has_many :bans, dependent: :destroy
|
||
|
||
enum :status, %i[ active deactivated banned ], default: :active
|
||
|
||
has_secure_password validations: false
|
||
|
||
after_create_commit :grant_membership_to_open_rooms
|
||
after_update :sign_out_other_sessions, if: :saved_change_to_password_digest?
|
||
after_update_commit :disconnect_signed_out_sessions, if: :saved_change_to_password_digest?
|
||
|
||
scope :ordered, -> { order("LOWER(name)") }
|
||
scope :filtered_by, ->(query) { where("name like ?", "%#{query}%") }
|
||
|
||
def initials
|
||
name.scan(/\b\w/).join
|
||
end
|
||
|
||
def title
|
||
[ name, bio ].compact_blank.join(" – ")
|
||
end
|
||
|
||
def deactivate
|
||
transaction do
|
||
close_remote_connections
|
||
|
||
memberships.without_direct_rooms.delete_all
|
||
push_subscriptions.delete_all
|
||
searches.delete_all
|
||
sessions.delete_all
|
||
|
||
update! status: :deactivated, email_address: deactived_email_address
|
||
end
|
||
end
|
||
|
||
def reset_remote_connections
|
||
close_remote_connections reconnect: true
|
||
end
|
||
|
||
private
|
||
# A new password is how someone shuts out whoever else got into their account, so it ends the sessions opened
|
||
# with the old one. The session it was changed from, if any, stays.
|
||
def sign_out_other_sessions
|
||
sessions.excluding(Current.session).delete_all
|
||
end
|
||
|
||
# After commit, so a connection that drops can't come back on a session that's still there. The password is
|
||
# saved by then, so a realtime service that can't be reached mustn't fail the change.
|
||
def disconnect_signed_out_sessions
|
||
reset_remote_connections
|
||
rescue => error
|
||
Rails.logger.warn "Could not disconnect remote connections after a password change: #{error.class}"
|
||
end
|
||
|
||
# One statement, which SQLite runs under the write lock, so no room is closed between reading the open ones and
|
||
# inserting. A room that Room#destroy_later closes is either closed before, and skipped, or after, and takes this
|
||
# membership away with the others. A room that granted itself to every user when it was created is skipped too,
|
||
# as insert_all did.
|
||
def grant_membership_to_open_rooms
|
||
now = Time.current
|
||
|
||
Membership.connection.execute Membership.sanitize_sql([ <<~SQL, id, now, now ])
|
||
insert into memberships (room_id, user_id, created_at, updated_at)
|
||
select id, ?, ?, ? from rooms where type = 'Rooms::Open'
|
||
on conflict do nothing
|
||
SQL
|
||
end
|
||
|
||
def deactived_email_address
|
||
email_address&.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
|
||
end
|
||
|
||
def close_remote_connections(reconnect: false)
|
||
ActionCable.server.remote_connections.where(current_user: self).disconnect reconnect: reconnect
|
||
end
|
||
end
|