diff --git a/app/assets/images/cancel.svg b/app/assets/images/cancel.svg new file mode 100644 index 0000000..3327c05 --- /dev/null +++ b/app/assets/images/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/avatars.css b/app/assets/stylesheets/avatars.css index 4407272..9c3b520 100644 --- a/app/assets/stylesheets/avatars.css +++ b/app/assets/stylesheets/avatars.css @@ -7,6 +7,7 @@ inline-size: var(--avatar-size, 5ch); margin: 0; place-items: center; + position: relative; img { aspect-ratio: 1; @@ -16,6 +17,25 @@ inline-size: var(--avatar-size, 5ch); max-inline-size: 100%; object-fit: cover; + + .banned & { + opacity: 0.5; + } + } + + .banned &:after { + background: url(cancel.svg) no-repeat center center; + block-size: auto; + content: ""; + filter: invert(0%); + inline-size: var(--avatar-size, 5ch); + inset: 0; + max-inline-size: 100%; + position: absolute; + + @media (prefers-color-scheme: dark) { + filter: invert(100%); + } } } diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 0691d88..be05d66 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -3,7 +3,7 @@ class AccountsController < ApplicationController before_action :set_account def edit - set_page_and_extract_portion_from User.active.ordered, per_page: 500 + set_page_and_extract_portion_from account_users.ordered, per_page: 500 end def update @@ -19,4 +19,12 @@ class AccountsController < ApplicationController def account_params params.require(:account).permit(:name, :logo) end + + def account_users + if Current.user.can_administer? + User.where(status: [ :active, :banned ]) + else + User.active + end + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6463f2d..5e60f1e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,4 @@ class ApplicationController < ActionController::Base - include AllowBrowser, Authentication, Authorization, SetCurrentRequest, SetPlatform, TrackedRoomVisit, VersionHeaders + include AllowBrowser, Authentication, Authorization, BlockBannedRequests, SetCurrentRequest, SetPlatform, TrackedRoomVisit, VersionHeaders include Turbo::Streams::Broadcasts, Turbo::Streams::StreamName end diff --git a/app/controllers/concerns/block_banned_requests.rb b/app/controllers/concerns/block_banned_requests.rb new file mode 100644 index 0000000..6485c5a --- /dev/null +++ b/app/controllers/concerns/block_banned_requests.rb @@ -0,0 +1,16 @@ +module BlockBannedRequests + extend ActiveSupport::Concern + + included do + before_action :reject_banned_ip, unless: :safe_request? + end + + private + def reject_banned_ip + head :too_many_requests if Ban.banned?(request.remote_ip) + end + + def safe_request? + request.get? || request.head? + end +end diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index d93a9c6..959b652 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -42,7 +42,7 @@ class MessagesController < ApplicationController def destroy @message.destroy - @message.broadcast_remove_to @room, :messages + @message.broadcast_remove end private diff --git a/app/controllers/users/bans_controller.rb b/app/controllers/users/bans_controller.rb new file mode 100644 index 0000000..5360f15 --- /dev/null +++ b/app/controllers/users/bans_controller.rb @@ -0,0 +1,19 @@ +class Users::BansController < ApplicationController + before_action :ensure_can_administer + before_action :set_user + + def create + @user.ban + redirect_to @user + end + + def destroy + @user.unban + redirect_to @user + end + + private + def set_user + @user = User.find(params[:user_id]) + end +end diff --git a/app/jobs/remove_banned_content_job.rb b/app/jobs/remove_banned_content_job.rb new file mode 100644 index 0000000..9fbc093 --- /dev/null +++ b/app/jobs/remove_banned_content_job.rb @@ -0,0 +1,5 @@ +class RemoveBannedContentJob < ApplicationJob + def perform(user) + user.remove_banned_content + end +end diff --git a/app/models/ban.rb b/app/models/ban.rb new file mode 100644 index 0000000..be5a936 --- /dev/null +++ b/app/models/ban.rb @@ -0,0 +1,20 @@ +class Ban < ApplicationRecord + belongs_to :user + + validate :ip_address_is_public + + def self.banned?(ip_address) + exists?(ip_address: ip_address) + end + + private + def ip_address_is_public + ip = IPAddr.new(ip_address) + + if ip.loopback? || ip.private? || ip.link_local? + errors.add(:ip_address, "cannot be a private or internal IP address") + end + rescue IPAddr::InvalidAddressError + errors.add(:ip_address, "is not a valid IP address") + end +end diff --git a/app/models/message/broadcasts.rb b/app/models/message/broadcasts.rb index 78ed381..1f909dc 100644 --- a/app/models/message/broadcasts.rb +++ b/app/models/message/broadcasts.rb @@ -3,4 +3,8 @@ module Message::Broadcasts broadcast_append_to room, :messages, target: [ room, :messages ] ActionCable.server.broadcast("unread_rooms", { roomId: room.id }) end + + def broadcast_remove + broadcast_remove_to room, :messages + end end diff --git a/app/models/user.rb b/app/models/user.rb index de5ba4c..20d05dd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,5 @@ class User < ApplicationRecord - include Avatar, Bot, Mentionable, Role, Transferable + include Avatar, Bannable, Bot, Mentionable, Role, Transferable has_many :memberships, dependent: :delete_all has_many :rooms, through: :memberships @@ -13,8 +13,9 @@ class User < ApplicationRecord has_many :searches, dependent: :delete_all has_many :sessions, dependent: :destroy + has_many :bans, dependent: :destroy - scope :active, -> { where(active: true) } + enum :status, %i[ active deactivated banned ], default: :active has_secure_password validations: false @@ -40,14 +41,10 @@ class User < ApplicationRecord searches.delete_all sessions.delete_all - update! active: false, email_address: deactived_email_address + update! status: :deactivated, email_address: deactived_email_address end end - def deactivated? - !active? - end - def reset_remote_connections close_remote_connections reconnect: true end diff --git a/app/models/user/bannable.rb b/app/models/user/bannable.rb new file mode 100644 index 0000000..9d714ab --- /dev/null +++ b/app/models/user/bannable.rb @@ -0,0 +1,42 @@ +module User::Bannable + extend ActiveSupport::Concern + + def ban + transaction do + create_bans_from_sessions + apply_ban + banned! + end + end + + def unban + transaction do + bans.delete_all + active! + end + end + + def remove_banned_content_later + RemoveBannedContentJob.perform_later(self) + end + + def remove_banned_content + messages.each do |message| + message.destroy + message.broadcast_remove + end + end + + private + def create_bans_from_sessions + sessions.pluck(:ip_address).compact_blank.uniq.each do |ip| + bans.create!(ip_address: ip) + end + end + + def apply_ban + close_remote_connections + sessions.delete_all + remove_banned_content_later + end +end diff --git a/app/views/accounts/users/_user.html.erb b/app/views/accounts/users/_user.html.erb index 21ebb0a..470e7f8 100644 --- a/app/views/accounts/users/_user.html.erb +++ b/app/views/accounts/users/_user.html.erb @@ -1,4 +1,4 @@ -
  • +
  • ">
    <%= avatar_tag user, loading: :lazy %>
    @@ -9,7 +9,7 @@ - <% if Current.user.can_administer? && user != Current.user %> + <% if Current.user.can_administer? && user != Current.user && user.active? %> <% unless user.bot? %> <%= form_with model: user, url: account_user_path(user), data: { controller: "form" }, method: :patch do | form | %>