@@ -127,6 +127,7 @@ module Admin | |||
:by_domain, | |||
:active, | |||
:pending, | |||
:disabled, | |||
:silenced, | |||
:suspended, | |||
:username, | |||
@@ -0,0 +1,32 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::Admin::AccountActionsController < Api::BaseController | |||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' } | |||
before_action :require_staff! | |||
before_action :set_account | |||
def create | |||
account_action = Admin::AccountAction.new(resource_params) | |||
account_action.target_account = @account | |||
account_action.current_account = current_account | |||
account_action.save! | |||
render_empty | |||
end | |||
private | |||
def set_account | |||
@account = Account.find(params[:account_id]) | |||
end | |||
def resource_params | |||
params.permit( | |||
:type, | |||
:report_id, | |||
:warning_preset_id, | |||
:text, | |||
:send_email_notification | |||
) | |||
end | |||
end |
@@ -0,0 +1,128 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::Admin::AccountsController < Api::BaseController | |||
include Authorization | |||
include AccountableConcern | |||
LIMIT = 100 | |||
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show] | |||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show] | |||
before_action :require_staff! | |||
before_action :set_accounts, only: :index | |||
before_action :set_account, except: :index | |||
before_action :require_local_account!, only: [:enable, :approve, :reject] | |||
after_action :insert_pagination_headers, only: :index | |||
FILTER_PARAMS = %i( | |||
local | |||
remote | |||
by_domain | |||
active | |||
pending | |||
disabled | |||
silenced | |||
suspended | |||
username | |||
display_name | |||
ip | |||
staff | |||
).freeze | |||
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze | |||
def index | |||
authorize :account, :index? | |||
render json: @accounts, each_serializer: REST::Admin::AccountSerializer | |||
end | |||
def show | |||
authorize @account, :show? | |||
render json: @account, serializer: REST::Admin::AccountSerializer | |||
end | |||
def enable | |||
authorize @account.user, :enable? | |||
@account.user.enable! | |||
log_action :enable, @account.user | |||
render json: @account, serializer: REST::Admin::AccountSerializer | |||
end | |||
def approve | |||
authorize @account.user, :approve? | |||
@account.user.approve! | |||
render json: @account, serializer: REST::Admin::AccountSerializer | |||
end | |||
def reject | |||
authorize @account.user, :reject? | |||
SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) | |||
render json: @account, serializer: REST::Admin::AccountSerializer | |||
end | |||
def unsilence | |||
authorize @account, :unsilence? | |||
@account.unsilence! | |||
log_action :unsilence, @account | |||
render json: @account, serializer: REST::Admin::AccountSerializer | |||
end | |||
def unsuspend | |||
authorize @account, :unsuspend? | |||
@account.unsuspend! | |||
log_action :unsuspend, @account | |||
render json: @account, serializer: REST::Admin::AccountSerializer | |||
end | |||
private | |||
def set_accounts | |||
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) | |||
end | |||
def set_account | |||
@account = Account.find(params[:id]) | |||
end | |||
def filtered_accounts | |||
AccountFilter.new(filter_params).results | |||
end | |||
def filter_params | |||
params.permit(*FILTER_PARAMS) | |||
end | |||
def insert_pagination_headers | |||
set_pagination_headers(next_path, prev_path) | |||
end | |||
def next_path | |||
api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? | |||
end | |||
def prev_path | |||
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? | |||
end | |||
def pagination_max_id | |||
@accounts.last.id | |||
end | |||
def pagination_since_id | |||
@accounts.first.id | |||
end | |||
def records_continue? | |||
@accounts.size == limit_param(LIMIT) | |||
end | |||
def pagination_params(core_params) | |||
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) | |||
end | |||
def require_local_account! | |||
forbidden unless @account.local? && @account.user.present? | |||
end | |||
end |
@@ -0,0 +1,108 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::Admin::ReportsController < Api::BaseController | |||
include Authorization | |||
include AccountableConcern | |||
LIMIT = 100 | |||
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show] | |||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show] | |||
before_action :require_staff! | |||
before_action :set_reports, only: :index | |||
before_action :set_report, except: :index | |||
after_action :insert_pagination_headers, only: :index | |||
FILTER_PARAMS = %i( | |||
resolved | |||
account_id | |||
target_account_id | |||
).freeze | |||
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze | |||
def index | |||
authorize :report, :index? | |||
render json: @reports, each_serializer: REST::Admin::ReportSerializer | |||
end | |||
def show | |||
authorize @report, :show? | |||
render json: @report, serializer: REST::Admin::ReportSerializer | |||
end | |||
def assign_to_self | |||
authorize @report, :update? | |||
@report.update!(assigned_account_id: current_account.id) | |||
log_action :assigned_to_self, @report | |||
render json: @report, serializer: REST::Admin::ReportSerializer | |||
end | |||
def unassign | |||
authorize @report, :update? | |||
@report.update!(assigned_account_id: nil) | |||
log_action :unassigned, @report | |||
render json: @report, serializer: REST::Admin::ReportSerializer | |||
end | |||
def reopen | |||
authorize @report, :update? | |||
@report.unresolve! | |||
log_action :reopen, @report | |||
render json: @report, serializer: REST::Admin::ReportSerializer | |||
end | |||
def resolve | |||
authorize @report, :update? | |||
@report.resolve!(current_account) | |||
log_action :resolve, @report | |||
render json: @report, serializer: REST::Admin::ReportSerializer | |||
end | |||
private | |||
def set_reports | |||
@reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) | |||
end | |||
def set_report | |||
@report = Report.find(params[:id]) | |||
end | |||
def filtered_reports | |||
ReportFilter.new(filter_params).results | |||
end | |||
def filter_params | |||
params.permit(*FILTER_PARAMS) | |||
end | |||
def insert_pagination_headers | |||
set_pagination_headers(next_path, prev_path) | |||
end | |||
def next_path | |||
api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue? | |||
end | |||
def prev_path | |||
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty? | |||
end | |||
def pagination_max_id | |||
@reports.last.id | |||
end | |||
def pagination_since_id | |||
@reports.first.id | |||
end | |||
def records_continue? | |||
@reports.size == limit_param(LIMIT) | |||
end | |||
def pagination_params(core_params) | |||
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) | |||
end | |||
end |
@@ -106,6 +106,8 @@ class Account < ApplicationRecord | |||
:confirmed?, | |||
:approved?, | |||
:pending?, | |||
:disabled?, | |||
:role, | |||
:admin?, | |||
:moderator?, | |||
:staff?, | |||
@@ -37,6 +37,8 @@ class AccountFilter | |||
Account.without_suspended | |||
when 'pending' | |||
accounts_with_users.merge User.pending | |||
when 'disabled' | |||
accounts_with_users.merge User.disabled | |||
when 'silenced' | |||
Account.silenced | |||
when 'suspended' | |||
@@ -13,6 +13,20 @@ module UserRoles | |||
admin? || moderator? | |||
end | |||
def role=(value) | |||
case value | |||
when 'admin' | |||
self.admin = true | |||
self.moderator = false | |||
when 'moderator' | |||
self.admin = false | |||
self.moderator = true | |||
else | |||
self.admin = false | |||
self.moderator = false | |||
end | |||
end | |||
def role | |||
if admin? | |||
'admin' | |||
@@ -17,6 +17,8 @@ | |||
# | |||
class Report < ApplicationRecord | |||
include Paginable | |||
belongs_to :account | |||
belongs_to :target_account, class_name: 'Account' | |||
belongs_to :action_taken_by_account, class_name: 'Account', optional: true | |||
@@ -26,6 +28,7 @@ class Report < ApplicationRecord | |||
scope :unresolved, -> { where(action_taken: false) } | |||
scope :resolved, -> { where(action_taken: true) } | |||
scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].each_with_object({}) { |k, h| h[k] = { user: [:invite_request, :invite] } }) } | |||
validates :comment, length: { maximum: 1000 } | |||
@@ -9,9 +9,11 @@ class ReportFilter | |||
def results | |||
scope = Report.unresolved | |||
params.each do |key, value| | |||
scope = scope.merge scope_for(key, value) | |||
end | |||
scope | |||
end | |||
@@ -87,6 +87,7 @@ class User < ApplicationRecord | |||
scope :approved, -> { where(approved: true) } | |||
scope :confirmed, -> { where.not(confirmed_at: nil) } | |||
scope :enabled, -> { where(disabled: false) } | |||
scope :disabled, -> { where(disabled: true) } | |||
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } | |||
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) } | |||
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } | |||
@@ -0,0 +1,77 @@ | |||
# frozen_string_literal: true | |||
class REST::Admin::AccountSerializer < ActiveModel::Serializer | |||
attributes :id, :username, :domain, :created_at, | |||
:email, :ip, :role, :confirmed, :suspended, | |||
:silenced, :disabled, :approved, :locale, | |||
:invite_request | |||
attribute :created_by_application_id, if: :created_by_application? | |||
attribute :invited_by_account_id, if: :invited? | |||
has_one :account, serializer: REST::AccountSerializer | |||
def id | |||
object.id.to_s | |||
end | |||
def email | |||
object.user_email | |||
end | |||
def ip | |||
object.user_current_sign_in_ip.to_s.presence | |||
end | |||
def role | |||
object.user_role | |||
end | |||
def suspended | |||
object.suspended? | |||
end | |||
def silenced | |||
object.silenced? | |||
end | |||
def confirmed | |||
object.user_confirmed? | |||
end | |||
def disabled | |||
object.user_disabled? | |||
end | |||
def approved | |||
object.user_approved? | |||
end | |||
def account | |||
object | |||
end | |||
def locale | |||
object.user_locale | |||
end | |||
def created_by_application_id | |||
object.user&.created_by_application_id&.to_s&.presence | |||
end | |||
def invite_request | |||
object.user&.invite_request&.text | |||
end | |||
def invited_by_account_id | |||
object.user&.invite&.user&.account_id&.to_s&.presence | |||
end | |||
def invited? | |||
object.user&.invited? | |||
end | |||
def created_by_application? | |||
object.user&.created_by_application_id&.present? | |||
end | |||
end |
@@ -0,0 +1,16 @@ | |||
# frozen_string_literal: true | |||
class REST::Admin::ReportSerializer < ActiveModel::Serializer | |||
attributes :id, :action_taken, :comment, :created_at, :updated_at | |||
has_one :account, serializer: REST::Admin::AccountSerializer | |||
has_one :target_account, serializer: REST::Admin::AccountSerializer | |||
has_one :assigned_account, serializer: REST::Admin::AccountSerializer | |||
has_one :action_taken_by_account, serializer: REST::Admin::AccountSerializer | |||
has_many :statuses, serializer: REST::StatusSerializer | |||
def id | |||
object.id.to_s | |||
end | |||
end |
@@ -80,7 +80,13 @@ Doorkeeper.configure do | |||
:'read:search', | |||
:'read:statuses', | |||
:follow, | |||
:push | |||
:push, | |||
:'admin:read', | |||
:'admin:read:accounts', | |||
:'admin:read:reports', | |||
:'admin:write', | |||
:'admin:write:accounts', | |||
:'admin:write:reports' | |||
# Change the way client credentials are retrieved from the request object. | |||
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then | |||
@@ -114,6 +114,12 @@ en: | |||
application: | |||
title: OAuth authorization required | |||
scopes: | |||
admin:read: read all data on the server | |||
admin:read:accounts: read sensitive information of all accounts | |||
admin:read:reports: read sensitive information of all reports and reported accounts | |||
admin:write: modify all data on the server | |||
admin:write:accounts: perform moderation actions on accounts | |||
admin:write:reports: perform moderation actions on reports | |||
follow: modify account relationships | |||
push: receive your push notifications | |||
read: read all your account's data | |||
@@ -398,6 +398,29 @@ Rails.application.routes.draw do | |||
namespace :push do | |||
resource :subscription, only: [:create, :show, :update, :destroy] | |||
end | |||
namespace :admin do | |||
resources :accounts, only: [:index, :show] do | |||
member do | |||
post :enable | |||
post :unsilence | |||
post :unsuspend | |||
post :approve | |||
post :reject | |||
end | |||
resource :action, only: [:create], controller: 'account_actions' | |||
end | |||
resources :reports, only: [:index, :show] do | |||
member do | |||
post :assign_to_self | |||
post :unassign | |||
post :reopen | |||
post :resolve | |||
end | |||
end | |||
end | |||
end | |||
namespace :v2 do | |||
@@ -0,0 +1,57 @@ | |||
require 'rails_helper' | |||
RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do | |||
render_views | |||
let(:role) { 'moderator' } | |||
let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } | |||
let(:scopes) { 'admin:read admin:write' } | |||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } | |||
let(:account) { Fabricate(:user).account } | |||
before do | |||
allow(controller).to receive(:doorkeeper_token) { token } | |||
end | |||
shared_examples 'forbidden for wrong scope' do |wrong_scope| | |||
let(:scopes) { wrong_scope } | |||
it 'returns http forbidden' do | |||
expect(response).to have_http_status(403) | |||
end | |||
end | |||
shared_examples 'forbidden for wrong role' do |wrong_role| | |||
let(:role) { wrong_role } | |||
it 'returns http forbidden' do | |||
expect(response).to have_http_status(403) | |||
end | |||
end | |||
describe 'POST #create' do | |||
before do | |||
post :create, params: { account_id: account.id, type: 'disable' } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
it 'performs action against account' do | |||
expect(account.reload.user_disabled?).to be true | |||
end | |||
it 'logs action' do | |||
log_item = Admin::ActionLog.last | |||
expect(log_item).to_not be_nil | |||
expect(log_item.action).to eq :disable | |||
expect(log_item.account_id).to eq user.account_id | |||
expect(log_item.target_id).to eq account.user.id | |||
end | |||
end | |||
end |
@@ -0,0 +1,147 @@ | |||
require 'rails_helper' | |||
RSpec.describe Api::V1::Admin::AccountsController, type: :controller do | |||
render_views | |||
let(:role) { 'moderator' } | |||
let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } | |||
let(:scopes) { 'admin:read admin:write' } | |||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } | |||
let(:account) { Fabricate(:user).account } | |||
before do | |||
allow(controller).to receive(:doorkeeper_token) { token } | |||
end | |||
shared_examples 'forbidden for wrong scope' do |wrong_scope| | |||
let(:scopes) { wrong_scope } | |||
it 'returns http forbidden' do | |||
expect(response).to have_http_status(403) | |||
end | |||
end | |||
shared_examples 'forbidden for wrong role' do |wrong_role| | |||
let(:role) { wrong_role } | |||
it 'returns http forbidden' do | |||
expect(response).to have_http_status(403) | |||
end | |||
end | |||
describe 'GET #index' do | |||
before do | |||
get :index | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
end | |||
describe 'GET #show' do | |||
before do | |||
get :show, params: { id: account.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
end | |||
describe 'POST #approve' do | |||
before do | |||
account.user.update(approved: false) | |||
post :approve, params: { id: account.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
it 'approves user' do | |||
expect(account.reload.user_approved?).to be true | |||
end | |||
end | |||
describe 'POST #reject' do | |||
before do | |||
account.user.update(approved: false) | |||
post :reject, params: { id: account.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
it 'removes user' do | |||
expect(User.where(id: account.user.id).count).to eq 0 | |||
end | |||
end | |||
describe 'POST #enable' do | |||
before do | |||
account.user.update(disabled: true) | |||
post :enable, params: { id: account.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
it 'enables user' do | |||
expect(account.reload.user_disabled?).to be false | |||
end | |||
end | |||
describe 'POST #unsuspend' do | |||
before do | |||
account.touch(:suspended_at) | |||
post :unsuspend, params: { id: account.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
it 'unsuspends account' do | |||
expect(account.reload.suspended?).to be false | |||
end | |||
end | |||
describe 'POST #unsilence' do | |||
before do | |||
account.touch(:silenced_at) | |||
post :unsilence, params: { id: account.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
it 'unsilences account' do | |||
expect(account.reload.silenced?).to be false | |||
end | |||
end | |||
end |
@@ -0,0 +1,109 @@ | |||
require 'rails_helper' | |||
RSpec.describe Api::V1::Admin::ReportsController, type: :controller do | |||
render_views | |||
let(:role) { 'moderator' } | |||
let(:user) { Fabricate(:user, role: role, account: Fabricate(:account, username: 'alice')) } | |||
let(:scopes) { 'admin:read admin:write' } | |||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } | |||
let(:report) { Fabricate(:report) } | |||
before do | |||
allow(controller).to receive(:doorkeeper_token) { token } | |||
end | |||
shared_examples 'forbidden for wrong scope' do |wrong_scope| | |||
let(:scopes) { wrong_scope } | |||
it 'returns http forbidden' do | |||
expect(response).to have_http_status(403) | |||
end | |||
end | |||
shared_examples 'forbidden for wrong role' do |wrong_role| | |||
let(:role) { wrong_role } | |||
it 'returns http forbidden' do | |||
expect(response).to have_http_status(403) | |||
end | |||
end | |||
describe 'GET #index' do | |||
before do | |||
get :index | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
end | |||
describe 'GET #show' do | |||
before do | |||
get :show, params: { id: report.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
end | |||
describe 'POST #resolve' do | |||
before do | |||
post :resolve, params: { id: report.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
end | |||
describe 'POST #reopen' do | |||
before do | |||
post :reopen, params: { id: report.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
end | |||
describe 'POST #assign_to_self' do | |||
before do | |||
post :assign_to_self, params: { id: report.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
end | |||
describe 'POST #unassign' do | |||
before do | |||
post :unassign, params: { id: report.id } | |||
end | |||
it_behaves_like 'forbidden for wrong scope', 'write:statuses' | |||
it_behaves_like 'forbidden for wrong role', 'user' | |||
it 'returns http success' do | |||
expect(response).to have_http_status(200) | |||
end | |||
end | |||
end |