When banning a user, a reason can be provided. The user will see this reason when trying to log in. Also log bans and unbans in the staff action logs.
This commit is contained in:
parent
52b0c1c45f
commit
92a0729937
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
The modal for banning a user.
|
||||
|
||||
@class AdminBanUserController
|
||||
@extends Discourse.Controller
|
||||
@namespace Discourse
|
||||
@uses Discourse.ModalFunctionality
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AdminBanUserController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
|
||||
|
||||
actions: {
|
||||
ban: function() {
|
||||
var duration = parseInt(this.get('duration'), 10);
|
||||
if (duration > 0) {
|
||||
this.get('model').ban(duration, this.get('reason')).then(function() {
|
||||
window.location.reload();
|
||||
}, function(e) {
|
||||
var error = I18n.t('admin.user.ban_failed', { error: "http: " + e.status + " - " + e.body });
|
||||
bootbox.alert(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -142,21 +142,11 @@ Discourse.AdminUser = Discourse.User.extend({
|
|||
return banned_at.format('L') + " - " + banned_till.format('L');
|
||||
}.property('banned_till', 'banned_at'),
|
||||
|
||||
ban: function() {
|
||||
var duration = parseInt(window.prompt(I18n.t('admin.user.ban_duration')), 10);
|
||||
if (duration > 0) {
|
||||
Discourse.ajax("/admin/users/" + this.id + "/ban", {
|
||||
ban: function(duration, reason) {
|
||||
return Discourse.ajax("/admin/users/" + this.id + "/ban", {
|
||||
type: 'PUT',
|
||||
data: {duration: duration}
|
||||
}).then(function () {
|
||||
// succeeded
|
||||
window.location.reload();
|
||||
}, function(e) {
|
||||
// failure
|
||||
var error = I18n.t('admin.user.ban_failed', { error: "http: " + e.status + " - " + e.body });
|
||||
bootbox.alert(error);
|
||||
data: {duration: duration, reason: reason}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
unban: function() {
|
||||
|
|
|
@ -21,6 +21,9 @@ Discourse.StaffActionLog = Discourse.Model.extend({
|
|||
formatted += this.format('admin.logs.staff_actions.new_value', 'new_value');
|
||||
formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value');
|
||||
}
|
||||
if (!this.get('useModalForDetails')) {
|
||||
if (this.get('details')) formatted += this.get('details') + '<br/>';
|
||||
}
|
||||
return formatted;
|
||||
}.property('ip_address', 'email'),
|
||||
|
||||
|
@ -33,7 +36,7 @@ Discourse.StaffActionLog = Discourse.Model.extend({
|
|||
},
|
||||
|
||||
useModalForDetails: function() {
|
||||
return (this.get('details') && this.get('details').length > 0);
|
||||
return (this.get('details') && this.get('details').length > 100);
|
||||
}.property('action_name'),
|
||||
|
||||
useCustomModalForDetails: function() {
|
||||
|
|
|
@ -28,6 +28,13 @@ Discourse.AdminUserRoute = Discourse.Route.extend({
|
|||
controller.set('model', adminUser);
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
showBanModal: function(user) {
|
||||
Discourse.Route.showModal(this, 'admin_ban_user', user);
|
||||
this.controllerFor('modal').set('modalClass', 'ban-user-modal');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<div class="modal-body">
|
||||
<form>
|
||||
{{i18n admin.user.ban_duration}}
|
||||
{{textField value=duration maxlength="5" autofocus="autofocus" class="span2"}}
|
||||
{{i18n admin.user.ban_duration_units}}
|
||||
<br/>
|
||||
{{i18n admin.user.ban_reason_label}}
|
||||
<br/>
|
||||
{{textField value=reason class="span8"}}
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-danger' {{action ban}} data-dismiss="modal"><i class='icon icon-ban-circle'></i>{{i18n admin.user.ban}}</button>
|
||||
<a data-dismiss="modal">{{i18n cancel}}</a>
|
||||
</div>
|
|
@ -211,7 +211,7 @@
|
|||
{{i18n admin.user.banned_explanation}}
|
||||
{{else}}
|
||||
{{#if canBan}}
|
||||
<button class='btn btn-danger' {{action ban target="content"}}>
|
||||
<button class='btn btn-danger' {{action showBanModal this}}>
|
||||
<i class='icon icon-ban-circle'></i>
|
||||
{{i18n admin.user.ban}}
|
||||
</button>
|
||||
|
@ -220,6 +220,22 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if isBanned}}
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n admin.user.banned_by}}</div>
|
||||
<div class='long-value'>
|
||||
{{#link-to 'adminUser' banned_by}}{{avatar banned_by imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' banned_by}}{{banned_by.username}}{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n admin.user.ban_reason}}</div>
|
||||
<div class='long-value'>{{ban_reason}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n admin.user.blocked}}</div>
|
||||
<div class='value'>{{blocked}}</div>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
A modal view for banning a user.
|
||||
|
||||
@class AdminBanUserView
|
||||
@extends Discourse.ModalBodyView
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.AdminBanUserView = Discourse.ModalBodyView.extend({
|
||||
templateName: 'admin/templates/modal/admin_ban_user',
|
||||
title: I18n.t('admin.user.ban_modal_title')
|
||||
});
|
|
@ -5,6 +5,6 @@
|
|||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' {{action saveAutoClose}} data-dismiss="modal">{{i18n topic.auto_close_save}}</button>
|
||||
<a data-dismiss="modal">{{i18n topic.auto_close_cancel}}</a>
|
||||
<a data-dismiss="modal">{{i18n cancel}}</a>
|
||||
<button class='btn pull-right' {{action removeAutoClose}} data-dismiss="modal">{{i18n topic.auto_close_remove}}</button>
|
||||
</div>
|
||||
|
|
|
@ -42,7 +42,7 @@ class Admin::UsersController < Admin::AdminController
|
|||
@user.banned_till = params[:duration].to_i.days.from_now
|
||||
@user.banned_at = DateTime.now
|
||||
@user.save!
|
||||
# TODO logging
|
||||
StaffActionLogger.new(current_user).log_user_ban(@user, params[:reason])
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
|
@ -51,7 +51,7 @@ class Admin::UsersController < Admin::AdminController
|
|||
@user.banned_till = nil
|
||||
@user.banned_at = nil
|
||||
@user.save!
|
||||
# TODO logging
|
||||
StaffActionLogger.new(current_user).log_user_unban(@user)
|
||||
render nothing: true
|
||||
end
|
||||
|
||||
|
|
|
@ -28,7 +28,11 @@ class SessionController < ApplicationController
|
|||
if @user.confirm_password?(password)
|
||||
|
||||
if @user.is_banned?
|
||||
if reason = @user.ban_reason
|
||||
render json: { error: I18n.t("login.banned_with_reason", {date: I18n.l(@user.banned_till, format: :date_only), reason: reason}) }
|
||||
else
|
||||
render json: { error: I18n.t("login.banned", {date: I18n.l(@user.banned_till, format: :date_only)}) }
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
|
|
|
@ -360,6 +360,14 @@ class User < ActiveRecord::Base
|
|||
banned_till && banned_till > DateTime.now
|
||||
end
|
||||
|
||||
def ban_record
|
||||
UserHistory.for(self, :ban_user).order('id DESC').first
|
||||
end
|
||||
|
||||
def ban_reason
|
||||
ban_record.try(:details) if is_banned?
|
||||
end
|
||||
|
||||
# Use this helper to determine if the user has a particular trust level.
|
||||
# Takes into account admin, etc.
|
||||
def has_trust_level?(level)
|
||||
|
|
|
@ -18,7 +18,9 @@ class UserHistory < ActiveRecord::Base
|
|||
:checked_for_custom_avatar,
|
||||
:notified_about_avatar,
|
||||
:notified_about_sequential_replies,
|
||||
:notitied_about_dominating_topic)
|
||||
:notitied_about_dominating_topic,
|
||||
:ban_user,
|
||||
:unban_user)
|
||||
end
|
||||
|
||||
# Staff actions is a subset of all actions, used to audit actions taken by staff users.
|
||||
|
@ -27,7 +29,9 @@ class UserHistory < ActiveRecord::Base
|
|||
:change_trust_level,
|
||||
:change_site_setting,
|
||||
:change_site_customization,
|
||||
:delete_site_customization]
|
||||
:delete_site_customization,
|
||||
:ban_user,
|
||||
:unban_user]
|
||||
end
|
||||
|
||||
def self.staff_action_ids
|
||||
|
@ -48,6 +52,10 @@ class UserHistory < ActiveRecord::Base
|
|||
query
|
||||
end
|
||||
|
||||
def self.for(user, action_type)
|
||||
self.where(target_user_id: user.id, action: UserHistory.actions[action_type])
|
||||
end
|
||||
|
||||
def self.exists_for_user?(user, action_type, opts=nil)
|
||||
opts = opts || {}
|
||||
result = self.where(target_user_id: user.id, action: UserHistory.actions[action_type])
|
||||
|
|
|
@ -13,10 +13,12 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
|||
:flags_received_count,
|
||||
:private_topics_count,
|
||||
:can_delete_all_posts,
|
||||
:can_be_deleted
|
||||
:can_be_deleted,
|
||||
:ban_reason
|
||||
|
||||
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
|
||||
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
|
||||
has_one :banned_by, serializer: BasicUserSerializer, embed: :objects
|
||||
|
||||
def can_revoke_admin
|
||||
scope.can_revoke_admin?(object)
|
||||
|
@ -54,4 +56,8 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
|||
api_key.present?
|
||||
end
|
||||
|
||||
def banned_by
|
||||
object.ban_record.try(:acting_user)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -57,6 +57,23 @@ class StaffActionLogger
|
|||
}))
|
||||
end
|
||||
|
||||
def log_user_ban(user, reason, opts={})
|
||||
raise Discourse::InvalidParameters.new('user is nil') unless user
|
||||
UserHistory.create( params(opts).merge({
|
||||
action: UserHistory.actions[:ban_user],
|
||||
target_user_id: user.id,
|
||||
details: reason
|
||||
}))
|
||||
end
|
||||
|
||||
def log_user_unban(user, opts={})
|
||||
raise Discourse::InvalidParameters.new('user is nil') unless user
|
||||
UserHistory.create( params(opts).merge({
|
||||
action: UserHistory.actions[:unban_user],
|
||||
target_user_id: user.id
|
||||
}))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def params(opts)
|
||||
|
|
|
@ -657,7 +657,6 @@ en:
|
|||
auto_close_notice: "This topic will automatically close %{timeLeft}."
|
||||
auto_close_title: 'Auto-Close Settings'
|
||||
auto_close_save: "Save"
|
||||
auto_close_cancel: "Cancel"
|
||||
auto_close_remove: "Don't Auto-Close This Topic"
|
||||
|
||||
progress:
|
||||
|
@ -1260,6 +1259,8 @@ en:
|
|||
change_site_setting: "change site setting"
|
||||
change_site_customization: "change site customization"
|
||||
delete_site_customization: "delete site customization"
|
||||
ban_user: "ban user"
|
||||
unban_user: "unban user"
|
||||
screened_emails:
|
||||
title: "Screened Emails"
|
||||
description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed."
|
||||
|
@ -1331,7 +1332,11 @@ en:
|
|||
user:
|
||||
ban_failed: "Something went wrong banning this user {{error}}"
|
||||
unban_failed: "Something went wrong unbanning this user {{error}}"
|
||||
ban_duration: "How long would you like to ban the user for? (days)"
|
||||
ban_duration: "How long would you like to ban the user for?"
|
||||
ban_duration_units: "(days)"
|
||||
ban_reason_label: "Why are you banning? When the user tries to log in, they will see this text. Keep it short."
|
||||
ban_reason: "Reason for Ban"
|
||||
banned_by: "Banned by"
|
||||
delete_all_posts: "Delete all posts"
|
||||
delete_all_posts_confirm: "You are about to delete %{posts} posts and %{topics} topics. Are you sure?"
|
||||
ban: "Ban"
|
||||
|
@ -1391,6 +1396,7 @@ en:
|
|||
banned_explanation: "A banned user can't log in."
|
||||
block_explanation: "A blocked user can't post or start topics."
|
||||
trust_level_change_failed: "There was a problem changing the user's trust level."
|
||||
ban_modal_title: "Ban User"
|
||||
|
||||
site_content:
|
||||
none: "Choose a type of content to begin editing."
|
||||
|
|
|
@ -777,6 +777,7 @@ en:
|
|||
activate_email: "You're almost done! We sent an activation email to <b>%{email}</b>. Please follow the instructions in the email to activate your account."
|
||||
not_activated: "You can't log in yet. We sent an activation email to you. Please follow the instructions in the email to activate your account."
|
||||
banned: "You can't log in until %{date}."
|
||||
banned_with_reason: "You can't log in until %{date}. The reason you were banned: %{reason}"
|
||||
errors: "%{errors}"
|
||||
not_available: "Not available. Try %{suggestion}?"
|
||||
something_already_taken: "Something went wrong, perhaps the username or email is already registered. Try the forgot password link."
|
||||
|
|
|
@ -115,4 +115,39 @@ describe StaffActionLogger do
|
|||
json['header'].should == site_customization.header
|
||||
end
|
||||
end
|
||||
|
||||
describe "log_user_ban" do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
it "raises an error when arguments are missing" do
|
||||
expect { logger.log_user_ban(nil, nil) }.to raise_error(Discourse::InvalidParameters)
|
||||
expect { logger.log_user_ban(nil, "He was bad.") }.to raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
it "reason arg is optional" do
|
||||
expect { logger.log_user_ban(user, nil) }.to_not raise_error
|
||||
end
|
||||
|
||||
it "creates a new UserHistory record" do
|
||||
reason = "He was a big meanie."
|
||||
log_record = logger.log_user_ban(user, reason)
|
||||
log_record.should be_valid
|
||||
log_record.details.should == reason
|
||||
log_record.target_user.should == user
|
||||
end
|
||||
end
|
||||
|
||||
describe "log_user_unban" do
|
||||
let(:user) { Fabricate(:user, banned_at: 1.day.ago, banned_till: 7.days.from_now) }
|
||||
|
||||
it "raises an error when argument is missing" do
|
||||
expect { logger.log_user_unban(nil) }.to raise_error(Discourse::InvalidParameters)
|
||||
end
|
||||
|
||||
it "creates a new UserHistory record" do
|
||||
log_record = logger.log_user_unban(user)
|
||||
log_record.should be_valid
|
||||
log_record.target_user.should == user
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue