Merge pull request #194 from Retrospring/refactor/user-bans
Implement ban history
This commit is contained in:
commit
96e48a833b
3
Gemfile
3
Gemfile
|
@ -36,6 +36,9 @@ gem "hcaptcha", "~> 6.0", git: "https://github.com/Retrospring/hcaptcha.git", re
|
||||||
|
|
||||||
gem "rolify", "~> 5.2"
|
gem "rolify", "~> 5.2"
|
||||||
|
|
||||||
|
gem "dry-initializer", "~> 3.0"
|
||||||
|
gem "dry-types", "~> 1.4"
|
||||||
|
|
||||||
gem 'ruby-progressbar'
|
gem 'ruby-progressbar'
|
||||||
|
|
||||||
gem 'rails_admin'
|
gem 'rails_admin'
|
||||||
|
|
21
Gemfile.lock
21
Gemfile.lock
|
@ -142,6 +142,25 @@ GEM
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
|
dry-configurable (0.12.1)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 0.5, >= 0.5.0)
|
||||||
|
dry-container (0.8.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-configurable (~> 0.1, >= 0.1.3)
|
||||||
|
dry-core (0.7.1)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-inflector (0.2.1)
|
||||||
|
dry-initializer (3.0.4)
|
||||||
|
dry-logic (1.2.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 0.5, >= 0.5)
|
||||||
|
dry-types (1.5.1)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-container (~> 0.3)
|
||||||
|
dry-core (~> 0.5, >= 0.5)
|
||||||
|
dry-inflector (~> 0.1, >= 0.1.2)
|
||||||
|
dry-logic (~> 1.0, >= 1.0.2)
|
||||||
equalizer (0.0.11)
|
equalizer (0.0.11)
|
||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
excon (0.89.0)
|
excon (0.89.0)
|
||||||
|
@ -589,6 +608,8 @@ DEPENDENCIES
|
||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
devise-async
|
devise-async
|
||||||
devise-i18n
|
devise-i18n
|
||||||
|
dry-initializer (~> 3.0)
|
||||||
|
dry-types (~> 1.4)
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
fake_email_validator
|
fake_email_validator
|
||||||
faker
|
faker
|
||||||
|
|
58
Rakefile
58
Rakefile
|
@ -154,9 +154,11 @@ namespace :justask do
|
||||||
fail "screen name required" if args[:screen_name].nil?
|
fail "screen name required" if args[:screen_name].nil?
|
||||||
user = User.find_by_screen_name(args[:screen_name])
|
user = User.find_by_screen_name(args[:screen_name])
|
||||||
fail "user #{args[:screen_name]} not found" if user.nil?
|
fail "user #{args[:screen_name]} not found" if user.nil?
|
||||||
user.permanently_banned = true
|
UseCase::User::Ban.call(
|
||||||
user.ban_reason = args[:reason]
|
target_user_id: user.id,
|
||||||
user.save!
|
expiry: nil,
|
||||||
|
reason: args[:reason],
|
||||||
|
)
|
||||||
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -164,10 +166,11 @@ namespace :justask do
|
||||||
task :ban, [:screen_name, :reason] => :environment do |t, args|
|
task :ban, [:screen_name, :reason] => :environment do |t, args|
|
||||||
fail "screen name required" if args[:screen_name].nil?
|
fail "screen name required" if args[:screen_name].nil?
|
||||||
user = User.find_by_screen_name(args[:screen_name])
|
user = User.find_by_screen_name(args[:screen_name])
|
||||||
user.permanently_banned = false
|
UseCase::User::Ban.call(
|
||||||
user.banned_until = DateTime.current + 1
|
target_user_id: user.id,
|
||||||
user.ban_reason = args[:reason]
|
expiry: DateTime.current + 1,
|
||||||
user.save!
|
reason: args[:reason],
|
||||||
|
)
|
||||||
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -175,10 +178,11 @@ namespace :justask do
|
||||||
task :week_ban, [:screen_name, :reason] => :environment do |t, args|
|
task :week_ban, [:screen_name, :reason] => :environment do |t, args|
|
||||||
fail "screen name required" if args[:screen_name].nil?
|
fail "screen name required" if args[:screen_name].nil?
|
||||||
user = User.find_by_screen_name(args[:screen_name])
|
user = User.find_by_screen_name(args[:screen_name])
|
||||||
user.permanently_banned = false
|
UseCase::User::Ban.call(
|
||||||
user.banned_until = DateTime.current + 7
|
target_user_id: user.id,
|
||||||
user.ban_reason = args[:reason]
|
expiry: DateTime.current + 7,
|
||||||
user.save!
|
reason: args[:reason],
|
||||||
|
)
|
||||||
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -186,10 +190,11 @@ namespace :justask do
|
||||||
task :month_ban, [:screen_name, :reason] => :environment do |t, args|
|
task :month_ban, [:screen_name, :reason] => :environment do |t, args|
|
||||||
fail "screen name required" if args[:screen_name].nil?
|
fail "screen name required" if args[:screen_name].nil?
|
||||||
user = User.find_by_screen_name(args[:screen_name])
|
user = User.find_by_screen_name(args[:screen_name])
|
||||||
user.permanently_banned = false
|
UseCase::User::Ban.call(
|
||||||
user.banned_until = DateTime.current + 30
|
target_user_id: user.id,
|
||||||
user.ban_reason = args[:reason]
|
expiry: DateTime.current + 30,
|
||||||
user.save!
|
reason: args[:reason],
|
||||||
|
)
|
||||||
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -197,10 +202,11 @@ namespace :justask do
|
||||||
task :year_ban, [:screen_name, :reason] => :environment do |t, args|
|
task :year_ban, [:screen_name, :reason] => :environment do |t, args|
|
||||||
fail "screen name required" if args[:screen_name].nil?
|
fail "screen name required" if args[:screen_name].nil?
|
||||||
user = User.find_by_screen_name(args[:screen_name])
|
user = User.find_by_screen_name(args[:screen_name])
|
||||||
user.permanently_banned = false
|
UseCase::User::Ban.call(
|
||||||
user.banned_until = DateTime.current + 365
|
target_user_id: user.id,
|
||||||
user.ban_reason = args[:reason]
|
expiry: DateTime.current + 365,
|
||||||
user.save!
|
reason: args[:reason],
|
||||||
|
)
|
||||||
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -208,10 +214,11 @@ namespace :justask do
|
||||||
task :aeon_ban, [:screen_name, :reason] => :environment do |t, args|
|
task :aeon_ban, [:screen_name, :reason] => :environment do |t, args|
|
||||||
fail "screen name required" if args[:screen_name].nil?
|
fail "screen name required" if args[:screen_name].nil?
|
||||||
user = User.find_by_screen_name(args[:screen_name])
|
user = User.find_by_screen_name(args[:screen_name])
|
||||||
user.permanently_banned = false
|
UseCase::User::Ban.call(
|
||||||
user.banned_until = DateTime.current + 365_000_000_000
|
target_user_id: user.id,
|
||||||
user.ban_reason = args[:reason]
|
expiry: DateTime.current + 365_000_000_000,
|
||||||
user.save!
|
reason: args[:reason],
|
||||||
|
)
|
||||||
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
puts "#{user.screen_name} got hit by\033[5m YE OLDE BANHAMMER\033[0m!!1!"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -220,10 +227,7 @@ namespace :justask do
|
||||||
fail "screen name required" if args[:screen_name].nil?
|
fail "screen name required" if args[:screen_name].nil?
|
||||||
user = User.find_by_screen_name(args[:screen_name])
|
user = User.find_by_screen_name(args[:screen_name])
|
||||||
fail "user #{args[:screen_name]} not found" if user.nil?
|
fail "user #{args[:screen_name]} not found" if user.nil?
|
||||||
user.permanently_banned = false
|
UseCase::User::Unban.call(user.id)
|
||||||
user.banned_until = nil
|
|
||||||
user.ban_reason = nil
|
|
||||||
user.save!
|
|
||||||
puts "#{user.screen_name} is no longer banned."
|
puts "#{user.screen_name} is no longer banned."
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
require 'use_case/user/ban'
|
||||||
|
require 'use_case/user/unban'
|
||||||
|
require 'errors'
|
||||||
|
|
||||||
class Ajax::ModerationController < AjaxController
|
class Ajax::ModerationController < AjaxController
|
||||||
def vote
|
def vote
|
||||||
params.require :id
|
params.require :id
|
||||||
|
@ -108,36 +112,49 @@ class Ajax::ModerationController < AjaxController
|
||||||
|
|
||||||
params.require :user
|
params.require :user
|
||||||
params.require :ban
|
params.require :ban
|
||||||
params.require :permaban
|
|
||||||
|
|
||||||
reason = params[:reason]
|
duration = params[:duration].to_i
|
||||||
target = User.find_by_screen_name!(params[:user])
|
duration_unit = params[:duration_unit].to_s
|
||||||
unban = params[:ban] == "0"
|
reason = params[:reason].to_s
|
||||||
perma = params[:permaban] == "1"
|
target_user = User.find_by_screen_name!(params[:user])
|
||||||
|
unban = params[:ban] == '0'
|
||||||
|
perma = params[:duration].blank?
|
||||||
|
|
||||||
buntil = DateTime.strptime params[:until], "%m/%d/%Y %I:%M %p" unless unban || perma
|
if !unban && target_user.has_role?(:administrator)
|
||||||
|
|
||||||
if !unban && target.has_role?(:administrator)
|
|
||||||
@response[:status] = :nopriv
|
@response[:status] = :nopriv
|
||||||
@response[:message] = I18n.t('messages.moderation.ban.nopriv')
|
@response[:message] = I18n.t('messages.moderation.ban.nopriv')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if unban
|
if unban
|
||||||
target.unban
|
UseCase::User::Unban.call(target_user.id)
|
||||||
@response[:message] = I18n.t('messages.moderation.ban.unban')
|
@response[:message] = I18n.t('messages.moderation.ban.unban')
|
||||||
@response[:success] = true
|
@response[:success] = true
|
||||||
|
@response[:status] = :okay
|
||||||
|
return
|
||||||
elsif perma
|
elsif perma
|
||||||
target.ban nil, reason
|
|
||||||
@response[:message] = I18n.t('messages.moderation.ban.perma')
|
@response[:message] = I18n.t('messages.moderation.ban.perma')
|
||||||
|
expiry = nil
|
||||||
else
|
else
|
||||||
target.ban buntil, reason
|
params.require :duration
|
||||||
@response[:message] = I18n.t('messages.moderation.ban.temp', date: buntil.to_s)
|
params.require :duration_unit
|
||||||
|
|
||||||
|
raise Errors::InvalidBanDuration unless %w[hours days weeks months].include? duration_unit
|
||||||
|
|
||||||
|
expiry = DateTime.now + duration.public_send(duration_unit)
|
||||||
|
@response[:message] = I18n.t('messages.moderation.ban.temp', date: expiry.to_s)
|
||||||
end
|
end
|
||||||
target.save!
|
|
||||||
|
UseCase::User::Ban.call(
|
||||||
|
target_user_id: target_user.id,
|
||||||
|
expiry: expiry,
|
||||||
|
reason: reason,
|
||||||
|
source_user_id: current_user.id)
|
||||||
|
|
||||||
|
target_user.save!
|
||||||
|
|
||||||
@response[:status] = :okay
|
@response[:status] = :okay
|
||||||
@response[:success] = target.banned? == !unban
|
@response[:success] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
def privilege
|
def privilege
|
||||||
|
@ -162,7 +179,7 @@ class Ajax::ModerationController < AjaxController
|
||||||
|
|
||||||
@response[:checked] = status
|
@response[:checked] = status
|
||||||
type = params[:type].downcase
|
type = params[:type].downcase
|
||||||
target_role = {"admin" => "administrator"}.fetch(type, type).to_sym
|
target_role = {'admin' => 'administrator'}.fetch(type, type).to_sym
|
||||||
|
|
||||||
if status
|
if status
|
||||||
target_user.add_role target_role
|
target_user.add_role target_role
|
||||||
|
|
|
@ -40,10 +40,11 @@ class ApplicationController < ActionController::Base
|
||||||
name = current_user.screen_name
|
name = current_user.screen_name
|
||||||
# obligatory '2001: A Space Odyssey' reference
|
# obligatory '2001: A Space Odyssey' reference
|
||||||
flash[:notice] = t('flash.ban.error', name: name)
|
flash[:notice] = t('flash.ban.error', name: name)
|
||||||
unless current_user.ban_reason.nil?
|
current_ban = current_user.bans.current.first
|
||||||
flash[:notice] += "\n#{t('flash.ban.reason', reason: current_user.ban_reason)}"
|
unless current_ban&.reason.nil?
|
||||||
|
flash[:notice] += "\n#{t('flash.ban.reason', reason: current_user.bans.current.first.reason)}"
|
||||||
end
|
end
|
||||||
if not current_user.permanently_banned?
|
unless current_ban&.permanently_banned?
|
||||||
# TODO format banned_until
|
# TODO format banned_until
|
||||||
flash[:notice] += "\n#{t('flash.ban.until', time: current_user.banned_until)}"
|
flash[:notice] += "\n#{t('flash.ban.until', time: current_user.banned_until)}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,7 @@ load = ->
|
||||||
banCheckbox = modalForm.querySelector('[name="ban"][type="checkbox"]')
|
banCheckbox = modalForm.querySelector('[name="ban"][type="checkbox"]')
|
||||||
permabanCheckbox = modalForm.querySelector('[name="permaban"][type="checkbox"]')
|
permabanCheckbox = modalForm.querySelector('[name="permaban"][type="checkbox"]')
|
||||||
|
|
||||||
|
if banCheckbox
|
||||||
banCheckbox.addEventListener "change", (event) ->
|
banCheckbox.addEventListener "change", (event) ->
|
||||||
$t = $ this
|
$t = $ this
|
||||||
if $t.is(":checked")
|
if $t.is(":checked")
|
||||||
|
@ -22,20 +23,18 @@ load = ->
|
||||||
modalForm.addEventListener "submit", (event) ->
|
modalForm.addEventListener "submit", (event) ->
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
checktostr = (el) ->
|
|
||||||
if el.checked
|
|
||||||
"1"
|
|
||||||
else
|
|
||||||
"0"
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
ban: checktostr banCheckbox
|
ban: "0"
|
||||||
permaban: checktostr permabanCheckbox
|
|
||||||
until: modalForm.elements["until"].value.trim()
|
|
||||||
reason: modalForm.elements["reason"].value.trim()
|
|
||||||
user: modalForm.elements["user"].value
|
user: modalForm.elements["user"].value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if banCheckbox && banCheckbox.checked
|
||||||
|
data.ban = "1"
|
||||||
|
data.reason = modalForm.elements["reason"].value.trim()
|
||||||
|
unless permabanCheckbox.checked
|
||||||
|
data.duration = modalForm.elements["duration"].value.trim()
|
||||||
|
data.duration_unit = modalForm.elements["duration_unit"].value.trim()
|
||||||
|
|
||||||
$.ajax
|
$.ajax
|
||||||
url: '/ajax/mod/ban'
|
url: '/ajax/mod/ban'
|
||||||
type: 'POST'
|
type: 'POST'
|
||||||
|
@ -43,6 +42,7 @@ load = ->
|
||||||
success: (data, status, jqxhr) ->
|
success: (data, status, jqxhr) ->
|
||||||
showNotification data.message, data.success
|
showNotification data.message, data.success
|
||||||
error: (jqxhr, status, error) ->
|
error: (jqxhr, status, error) ->
|
||||||
|
console.error 'request failed', data
|
||||||
console.log jqxhr, status, error
|
console.log jqxhr, status, error
|
||||||
showNotification translate('frontend.error.message'), false
|
showNotification translate('frontend.error.message'), false
|
||||||
complete: (jqxhr, status) ->
|
complete: (jqxhr, status) ->
|
||||||
|
|
|
@ -49,6 +49,11 @@ class User < ApplicationRecord
|
||||||
has_one :profile, dependent: :destroy
|
has_one :profile, dependent: :destroy
|
||||||
has_one :theme, dependent: :destroy
|
has_one :theme, dependent: :destroy
|
||||||
|
|
||||||
|
has_many :bans, class_name: 'UserBan', dependent: :destroy
|
||||||
|
has_many :banned_users, class_name: 'UserBan',
|
||||||
|
foreign_key: 'banned_by_id',
|
||||||
|
dependent: :nullify
|
||||||
|
|
||||||
SCREEN_NAME_REGEX = /\A[a-zA-Z0-9_]{1,16}\z/
|
SCREEN_NAME_REGEX = /\A[a-zA-Z0-9_]{1,16}\z/
|
||||||
WEBSITE_REGEX = /https?:\/\/([A-Za-z.\-]+)\/?(?:.*)/i
|
WEBSITE_REGEX = /https?:\/\/([A-Za-z.\-]+)\/?(?:.*)/i
|
||||||
|
|
||||||
|
@ -218,21 +223,31 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# forwards fill
|
|
||||||
def banned?
|
def banned?
|
||||||
self.permanently_banned? or ((not self.banned_until.nil?) and self.banned_until >= DateTime.current)
|
self.bans.current.count > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def unban
|
def unban
|
||||||
self.update(permanently_banned: false, ban_reason: nil, banned_until: nil)
|
UseCase::User::Unban.call(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ban(buntil=nil, reason=nil)
|
# Bans a user.
|
||||||
if buntil == nil
|
# @param duration [Integer?] Ban duration
|
||||||
self.update(permanently_banned: true, ban_reason: reason)
|
# @param duration_unit [String, nil] Unit for the <code>duration</code> parameter. Accepted units: hours, days, weeks, months
|
||||||
|
# @param reason [String] Reason for the ban. This is displayed to the user.
|
||||||
|
# @param banned_by [User] User who instated the ban
|
||||||
|
def ban(duration, duration_unit = 'hours', reason = nil, banned_by = nil)
|
||||||
|
if duration
|
||||||
|
expiry = duration.public_send(duration_unit)
|
||||||
else
|
else
|
||||||
self.update(permanently_banned: false, banned_until: buntil, ban_reason: reason)
|
expiry = nil
|
||||||
end
|
end
|
||||||
|
UseCase::User::Ban.call(
|
||||||
|
target_user_id: id,
|
||||||
|
expiry: expiry,
|
||||||
|
reason: reason,
|
||||||
|
source_user_id: banned_by&.id
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_export?
|
def can_export?
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class UserBan < ApplicationRecord
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :banned_by, class_name: 'User'
|
||||||
|
|
||||||
|
scope :current, -> { where('expires_at IS NULL or expires_at > NOW()') }
|
||||||
|
end
|
|
@ -1,6 +1,7 @@
|
||||||
|
- current_ban = user.bans.current.first
|
||||||
.modal.fade#modal-ban{ aria: { hidden: true, labelledby: 'modal-ban-label' }, role: :dialog, tabindex: -1 }
|
.modal.fade#modal-ban{ aria: { hidden: true, labelledby: 'modal-ban-label' }, role: :dialog, tabindex: -1 }
|
||||||
.modal-dialog
|
.modal-dialog
|
||||||
.modal-content
|
.modal-content#ban-control-super
|
||||||
.modal-header
|
.modal-header
|
||||||
%h5.modal-title#modal-ban-label
|
%h5.modal-title#modal-ban-label
|
||||||
= t 'views.modal.bancontrol.title'
|
= t 'views.modal.bancontrol.title'
|
||||||
|
@ -9,13 +10,38 @@
|
||||||
%span.sr-only Close
|
%span.sr-only Close
|
||||||
= bootstrap_form_tag(url: '/mod/ban', html: { method: :post, novalidate: :novalidate }) do |f|
|
= bootstrap_form_tag(url: '/mod/ban', html: { method: :post, novalidate: :novalidate }) do |f|
|
||||||
= f.hidden_field :user, value: user.screen_name
|
= f.hidden_field :user, value: user.screen_name
|
||||||
.modal-body#ban-control-super
|
- if current_ban.nil?
|
||||||
|
.modal-body
|
||||||
= f.check_box :ban, label: t('views.modal.bancontrol.ban'), checked: user.banned?
|
= f.check_box :ban, label: t('views.modal.bancontrol.ban'), checked: user.banned?
|
||||||
#ban-controls{ style: user.banned? ? '' : 'display: none' }
|
#ban-controls{ style: user.banned? ? '' : 'display: none' }
|
||||||
= f.check_box :permaban, label: t('views.modal.bancontrol.permanent'), checked: user.permanently_banned?
|
= f.check_box :permaban, label: t('views.modal.bancontrol.permanent'), checked: user.permanently_banned?
|
||||||
#ban-controls-time{ style: user.permanently_banned? ? 'display: none' : '' }
|
#ban-controls-time{ style: user.permanently_banned? ? 'display: none' : '' }
|
||||||
= f.text_field :until, label: '', required: true, value: (user.banned_until || DateTime.current).strftime('%m/%d/%Y %I:%M %p')
|
= f.text_field :duration, label: '', required: true
|
||||||
= f.text_field :reason, placeholder: t('views.modal.bancontrol.reason'), value: user.ban_reason
|
.form-check.form-check-inline
|
||||||
|
= f.radio_button :duration_unit, 'hours', label: 'Hours', checked: true
|
||||||
|
= f.radio_button :duration_unit, 'days', label: 'Days'
|
||||||
|
= f.radio_button :duration_unit, 'weeks', label: 'Weeks'
|
||||||
|
= f.radio_button :duration_unit, 'months', label: 'Months'
|
||||||
|
= f.text_field :reason, placeholder: t('views.modal.bancontrol.reason'), value: user.bans.current.first&.reason
|
||||||
.modal-footer
|
.modal-footer
|
||||||
%button.btn.btn-default{ name: 'stop-time', type: :button, data: { dismiss: :modal } }= t 'views.actions.close'
|
%button.btn.btn-default{ name: 'stop-time', type: :button, data: { dismiss: :modal } }= t 'views.actions.close'
|
||||||
= f.submit t('views.modal.bancontrol.hammertime'), class: 'btn btn-primary', name: 'hammer-time'
|
= f.submit t('views.modal.bancontrol.hammertime'), class: 'btn btn-primary', name: 'hammer-time'
|
||||||
|
- else
|
||||||
|
= f.hidden_field :ban, value: '0'
|
||||||
|
.modal-body
|
||||||
|
- if current_ban.expires_at.nil?
|
||||||
|
This user is currently permanently banned for
|
||||||
|
%strong= current_ban.reason
|
||||||
|
- else
|
||||||
|
This user is currently banned until
|
||||||
|
%strong= current_ban.expires_at
|
||||||
|
for
|
||||||
|
%strong= current_ban.reason
|
||||||
|
- if current_ban.banned_by.present?
|
||||||
|
%br
|
||||||
|
This ban was instated by
|
||||||
|
%strong= current_ban.banned_by.profile.safe_name
|
||||||
|
on
|
||||||
|
%strong= current_ban.created_at
|
||||||
|
.modal-footer
|
||||||
|
= f.submit 'Unban', class: 'btn btn-primary', name: 'hammer-time'
|
||||||
|
|
|
@ -44,5 +44,6 @@ RailsAdmin.config do |config|
|
||||||
Smile
|
Smile
|
||||||
Theme
|
Theme
|
||||||
User
|
User
|
||||||
|
UserBan
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
class CreateUserBans < ActiveRecord::Migration[5.2]
|
||||||
|
def up
|
||||||
|
create_table :user_bans do |t|
|
||||||
|
t.bigint :user_id
|
||||||
|
t.string :reason
|
||||||
|
t.datetime :expires_at
|
||||||
|
t.bigint :banned_by_id, nullable: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
# foxy's functional fqueries
|
||||||
|
execute "INSERT INTO user_bans
|
||||||
|
(user_id, reason, expires_at, created_at, updated_at)
|
||||||
|
SELECT users.id, users.ban_reason, users.banned_until, users.updated_at, NOW() FROM users
|
||||||
|
WHERE banned_until IS NOT NULL AND NOT permanently_banned;"
|
||||||
|
|
||||||
|
|
||||||
|
execute "INSERT INTO user_bans
|
||||||
|
(user_id, reason, expires_at, created_at, updated_at)
|
||||||
|
SELECT users.id, users.ban_reason, NULL, users.updated_at, NOW() FROM users
|
||||||
|
WHERE permanently_banned;"
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :user_bans
|
||||||
|
end
|
||||||
|
end
|
|
@ -248,6 +248,15 @@ ActiveRecord::Schema.define(version: 2022_01_05_171216) do
|
||||||
t.index ["user_id", "code"], name: "index_totp_recovery_codes_on_user_id_and_code"
|
t.index ["user_id", "code"], name: "index_totp_recovery_codes_on_user_id_and_code"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "user_bans", force: :cascade do |t|
|
||||||
|
t.bigint "user_id"
|
||||||
|
t.string "reason"
|
||||||
|
t.datetime "expires_at"
|
||||||
|
t.bigint "banned_by_id"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
end
|
||||||
|
|
||||||
create_table "users", id: :bigint, default: -> { "gen_timestamp_id('users'::text)" }, force: :cascade do |t|
|
create_table "users", id: :bigint, default: -> { "gen_timestamp_id('users'::text)" }, force: :cascade do |t|
|
||||||
t.string "email", default: "", null: false
|
t.string "email", default: "", null: false
|
||||||
t.string "encrypted_password", default: "", null: false
|
t.string "encrypted_password", default: "", null: false
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
module Errors
|
||||||
|
class Base < StandardError
|
||||||
|
def status
|
||||||
|
500
|
||||||
|
end
|
||||||
|
|
||||||
|
def code
|
||||||
|
@code ||= self.class.name.sub('Errors::', '').underscore
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class BadRequest < Base
|
||||||
|
def status
|
||||||
|
400
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class InvalidBanDuration < BadRequest
|
||||||
|
end
|
||||||
|
|
||||||
|
class Forbidden < Base
|
||||||
|
def status
|
||||||
|
403
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'dry-types'
|
||||||
|
|
||||||
|
module Types
|
||||||
|
include Dry.Types()
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'dry-initializer'
|
||||||
|
require 'types'
|
||||||
|
require 'errors'
|
||||||
|
|
||||||
|
module UseCase
|
||||||
|
class Base
|
||||||
|
extend Dry::Initializer
|
||||||
|
|
||||||
|
def self.call(*args, **kwargs)
|
||||||
|
new(*args, **kwargs).call
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,56 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'use_case/base'
|
||||||
|
|
||||||
|
module UseCase
|
||||||
|
module User
|
||||||
|
class Ban < UseCase::Base
|
||||||
|
REASON_SPAM = 'Spam'
|
||||||
|
REASON_HARASSMENT = 'Harassment'
|
||||||
|
REASON_BAN_EVASION = 'Ban evasion'
|
||||||
|
|
||||||
|
option :target_user_id, type: Types::Coercible::Integer
|
||||||
|
option :expiry, types: Types::Nominal::DateTime.optional
|
||||||
|
option :source_user_id, type: Types::Coercible::Integer.optional
|
||||||
|
option :reason, type: Types::Coercible::String.optional
|
||||||
|
|
||||||
|
def call
|
||||||
|
ban = ::UserBan.create!(
|
||||||
|
user: target_user,
|
||||||
|
expires_at: expiry,
|
||||||
|
banned_by: source_user,
|
||||||
|
reason: reason
|
||||||
|
)
|
||||||
|
|
||||||
|
if reason == REASON_SPAM
|
||||||
|
target_user.update!(
|
||||||
|
profile_picture: nil,
|
||||||
|
profile_header: nil
|
||||||
|
)
|
||||||
|
target_user.profile.update!(
|
||||||
|
display_name: nil,
|
||||||
|
description: '',
|
||||||
|
location: '',
|
||||||
|
website: '',
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
ban: ban
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def target_user
|
||||||
|
@target_user ||= ::User.find(target_user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def source_user
|
||||||
|
if source_user_id
|
||||||
|
@source_user ||= ::User.find(source_user_id)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'use_case/base'
|
||||||
|
|
||||||
|
module UseCase
|
||||||
|
module User
|
||||||
|
class Unban < UseCase::Base
|
||||||
|
param :target_user_id, type: Types::Coercible::Integer
|
||||||
|
|
||||||
|
def call
|
||||||
|
UserBan.current.where(user_id: target_user_id).update_all(
|
||||||
|
# -1s to account for flakyness with timings in tests
|
||||||
|
expires_at: DateTime.now - 1.second
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -373,9 +373,9 @@ describe Ajax::ModerationController, :ajax_controller, type: :controller do
|
||||||
{
|
{
|
||||||
user: user_param,
|
user: user_param,
|
||||||
ban: ban,
|
ban: ban,
|
||||||
permaban: permaban,
|
|
||||||
reason: "just a prank, bro",
|
reason: "just a prank, bro",
|
||||||
until: wrongly_formatted_date_ugh
|
duration: duration,
|
||||||
|
duration_unit: duration_unit,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -414,17 +414,17 @@ describe Ajax::ModerationController, :ajax_controller, type: :controller do
|
||||||
|
|
||||||
context "when ban = 0" do
|
context "when ban = 0" do
|
||||||
let(:ban) { "0" }
|
let(:ban) { "0" }
|
||||||
let(:wrongly_formatted_date_ugh) { nil }
|
|
||||||
|
|
||||||
"01".each_char do |pb|
|
"01".each_char do |pb|
|
||||||
context "when permaban = #{pb}" do
|
context "when permaban = #{pb}" do
|
||||||
let(:permaban) { pb }
|
let(:duration) { pb == '0' ? 3 : nil }
|
||||||
|
let(:duration_unit) { pb == '0' ? 'hours' : nil }
|
||||||
|
|
||||||
context "when user is already banned" do
|
context "when user is already banned" do
|
||||||
before { target_user.ban }
|
before { target_user.ban(nil) }
|
||||||
|
|
||||||
it "unbans the user" do
|
it "unbans the user" do
|
||||||
expect { subject }.to(change { target_user.reload.banned? }.from(true).to(false))
|
expect { subject }.to change { target_user.reload.banned? }.from(true).to(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples "returns the expected response"
|
include_examples "returns the expected response"
|
||||||
|
@ -443,16 +443,17 @@ describe Ajax::ModerationController, :ajax_controller, type: :controller do
|
||||||
|
|
||||||
context "when ban = 1" do
|
context "when ban = 1" do
|
||||||
let(:ban) { "1" }
|
let(:ban) { "1" }
|
||||||
let(:wrongly_formatted_date_ugh) { "4/20/2420 12:00 AM" }
|
|
||||||
|
|
||||||
context "when permaban = 0" do
|
context "when permaban = 0" do
|
||||||
let(:permaban) { "0" }
|
let(:duration) { 3 }
|
||||||
|
let(:duration_unit) { 'hours' }
|
||||||
|
|
||||||
it "bans the user until 2420-04-20" do
|
it "bans the user for 3 hours" do
|
||||||
expect { subject }.to(change { target_user.reload.banned? }.from(false).to(true))
|
Timecop.freeze do
|
||||||
expect(target_user).not_to be_permanently_banned
|
expect { subject }.to change { target_user.reload.banned? }.from(false).to(true)
|
||||||
expect(target_user.ban_reason).to eq("just a prank, bro")
|
expect(target_user.bans.current.first.reason).to eq("just a prank, bro")
|
||||||
expect(target_user.banned_until).to eq(DateTime.strptime(wrongly_formatted_date_ugh, "%m/%d/%Y %I:%M %p"))
|
expect(target_user.bans.current.first.expires_at.to_i).to eq((Time.now.utc + 3.hours).to_i)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples "returns the expected response"
|
include_examples "returns the expected response"
|
||||||
|
@ -461,13 +462,13 @@ describe Ajax::ModerationController, :ajax_controller, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when permaban = 1" do
|
context "when permaban = 1" do
|
||||||
let(:permaban) { "1" }
|
let(:duration) { nil }
|
||||||
|
let(:duration_unit) { nil }
|
||||||
|
|
||||||
it "bans the user for all eternity" do
|
it "bans the user for all eternity" do
|
||||||
expect { subject }.to(change { target_user.reload.banned? }.from(false).to(true))
|
expect { subject }.to change { target_user.reload.banned? }.from(false).to(true)
|
||||||
expect(target_user).to be_permanently_banned
|
expect(target_user.bans.current.first.reason).to eq("just a prank, bro")
|
||||||
expect(target_user.ban_reason).to eq("just a prank, bro")
|
expect(target_user.bans.current.first.expires_at).to be_nil
|
||||||
expect(target_user.banned_until).to be_nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples "returns the expected response"
|
include_examples "returns the expected response"
|
||||||
|
@ -477,11 +478,38 @@ describe Ajax::ModerationController, :ajax_controller, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when reason = Spam" do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
user: target_user.screen_name,
|
||||||
|
ban: "1",
|
||||||
|
reason: "Spam",
|
||||||
|
duration: nil,
|
||||||
|
duration_unit: nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it "empties the user's profile" do
|
||||||
|
user.profile.display_name = "Veggietales Facts"
|
||||||
|
user.profile.description = "Are you a fan of Veggietales? Want to expand your veggie knowledge? Here at Veggietales Facts, we tweet trivia for fans like you."
|
||||||
|
user.profile.location = "Hell"
|
||||||
|
user.profile.website = "https://twitter.com/veggiefact"
|
||||||
|
|
||||||
|
expect { subject }.to change { target_user.reload.banned? }.from(false).to(true)
|
||||||
|
expect(target_user.bans.current.first.reason).to eq("Spam")
|
||||||
|
|
||||||
|
expect(target_user.profile.display_name).to be_nil
|
||||||
|
expect(target_user.profile.description).to be_empty
|
||||||
|
expect(target_user.profile.location).to be_empty
|
||||||
|
expect(target_user.profile.website).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when user does not exist" do
|
context "when user does not exist" do
|
||||||
let(:user_param) { "fritz-fantom" }
|
let(:user_param) { "fritz-fantom" }
|
||||||
let(:ban) { "1" }
|
let(:ban) { "1" }
|
||||||
let(:permaban) { "1" }
|
let(:duration) { nil }
|
||||||
let(:wrongly_formatted_date_ugh) { "4/20/2420 12:00 AM" }
|
let(:duration_unit) { nil }
|
||||||
let(:expected_response) do
|
let(:expected_response) do
|
||||||
{
|
{
|
||||||
"success" => false,
|
"success" => false,
|
||||||
|
|
Loading…
Reference in New Issue