mirror of
https://github.com/astuto/astuto.git
synced 2025-12-16 11:47:56 +01:00
Improve rails controllers (#118)
This commit is contained in:
committed by
GitHub
parent
8e75a85873
commit
94f77517a8
7
Gemfile
7
Gemfile
@@ -4,7 +4,6 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
|||||||
ruby '2.6.6'
|
ruby '2.6.6'
|
||||||
|
|
||||||
gem 'rails', '6.0.4.7'
|
gem 'rails', '6.0.4.7'
|
||||||
gem 'i18n-js'
|
|
||||||
|
|
||||||
gem 'pg', '>= 0.18', '< 2.0'
|
gem 'pg', '>= 0.18', '< 2.0'
|
||||||
|
|
||||||
@@ -23,6 +22,12 @@ gem 'bootsnap', '>= 1.4.2', require: false
|
|||||||
# Authentication
|
# Authentication
|
||||||
gem 'devise', '4.7.3'
|
gem 'devise', '4.7.3'
|
||||||
|
|
||||||
|
# Authorization
|
||||||
|
gem 'pundit', '2.2.0'
|
||||||
|
|
||||||
|
# I18n (forward locales to JS)
|
||||||
|
gem 'i18n-js'
|
||||||
|
|
||||||
# Administration panel
|
# Administration panel
|
||||||
gem "administrate", '0.16.0'
|
gem "administrate", '0.16.0'
|
||||||
|
|
||||||
|
|||||||
@@ -146,18 +146,22 @@ GEM
|
|||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
|
mini_portile2 (2.8.0)
|
||||||
minitest (5.15.0)
|
minitest (5.15.0)
|
||||||
momentjs-rails (2.29.1.1)
|
momentjs-rails (2.29.1.1)
|
||||||
railties (>= 3.1)
|
railties (>= 3.1)
|
||||||
msgpack (1.5.1)
|
msgpack (1.5.1)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.3-x86_64-linux)
|
nokogiri (1.13.3)
|
||||||
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
pg (1.3.5)
|
pg (1.3.5)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (4.3.12)
|
puma (4.3.12)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
|
pundit (2.2.0)
|
||||||
|
activesupport (>= 3.0.0)
|
||||||
racc (1.6.0)
|
racc (1.6.0)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-proxy (0.7.2)
|
rack-proxy (0.7.2)
|
||||||
@@ -235,7 +239,7 @@ GEM
|
|||||||
sprockets (>= 2.8, < 4.0)
|
sprockets (>= 2.8, < 4.0)
|
||||||
sprockets-rails (>= 2.0, < 4.0)
|
sprockets-rails (>= 2.0, < 4.0)
|
||||||
tilt (>= 1.1, < 3)
|
tilt (>= 1.1, < 3)
|
||||||
sassc (2.1.0-x86_64-linux)
|
sassc (2.1.0)
|
||||||
ffi (~> 1.9)
|
ffi (~> 1.9)
|
||||||
sassc-rails (2.1.2)
|
sassc-rails (2.1.2)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
@@ -305,6 +309,7 @@ DEPENDENCIES
|
|||||||
listen (>= 3.0.5, < 3.2)
|
listen (>= 3.0.5, < 3.2)
|
||||||
pg (>= 0.18, < 2.0)
|
pg (>= 0.18, < 2.0)
|
||||||
puma (~> 4.3)
|
puma (~> 4.3)
|
||||||
|
pundit (= 2.2.0)
|
||||||
rails (= 6.0.4.7)
|
rails (= 6.0.4.7)
|
||||||
react-rails (~> 2.6.0)
|
react-rails (~> 2.6.0)
|
||||||
rspec-rails (~> 3.8.2)
|
rspec-rails (~> 3.8.2)
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
|
include Pundit::Authorization
|
||||||
|
|
||||||
|
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
||||||
|
|
||||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||||
before_action :load_boards
|
before_action :load_boards
|
||||||
|
|
||||||
@@ -12,4 +16,12 @@ class ApplicationController < ActionController::Base
|
|||||||
def load_boards
|
def load_boards
|
||||||
@boards = Board.select(:id, :name).order(order: :asc)
|
@boards = Board.select(:id, :name).order(order: :asc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def user_not_authorized
|
||||||
|
render json: {
|
||||||
|
error: t('backend.errors.unauthorized')
|
||||||
|
}, status: :unauthorized
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class BoardsController < ApplicationController
|
class BoardsController < ApplicationController
|
||||||
include ApplicationHelper
|
include ApplicationHelper
|
||||||
|
|
||||||
before_action :authenticate_admin, only: [:create, :update, :update_order, :destroy]
|
before_action :authenticate_user!, only: [:create, :update, :update_order, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
boards = Board.order(order: :asc)
|
boards = Board.order(order: :asc)
|
||||||
@@ -15,6 +15,7 @@ class BoardsController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
board = Board.new(board_params)
|
board = Board.new(board_params)
|
||||||
|
authorize board
|
||||||
|
|
||||||
if board.save
|
if board.save
|
||||||
render json: board, status: :created
|
render json: board, status: :created
|
||||||
@@ -27,13 +28,12 @@ class BoardsController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
board = Board.find(params[:id])
|
board = Board.find(params[:id])
|
||||||
|
authorize board
|
||||||
board.assign_attributes(board_params)
|
board.assign_attributes(board_params)
|
||||||
|
|
||||||
if board.save
|
if board.save
|
||||||
render json: board, status: :ok
|
render json: board, status: :ok
|
||||||
else
|
else
|
||||||
print board.errors.full_messages
|
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
error: board.errors.full_messages
|
error: board.errors.full_messages
|
||||||
}, status: :unprocessable_entity
|
}, status: :unprocessable_entity
|
||||||
@@ -42,6 +42,7 @@ class BoardsController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
board = Board.find(params[:id])
|
board = Board.find(params[:id])
|
||||||
|
authorize board
|
||||||
|
|
||||||
if board.destroy
|
if board.destroy
|
||||||
render json: {
|
render json: {
|
||||||
@@ -55,6 +56,8 @@ class BoardsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_order
|
def update_order
|
||||||
|
authorize Board
|
||||||
|
|
||||||
workflow_output = ReorderWorkflow.new(
|
workflow_output = ReorderWorkflow.new(
|
||||||
entity_classname: Board,
|
entity_classname: Board,
|
||||||
column_name: 'order',
|
column_name: 'order',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class CommentsController < ApplicationController
|
|||||||
comment = Comment.new(comment_params)
|
comment = Comment.new(comment_params)
|
||||||
|
|
||||||
if comment.save
|
if comment.save
|
||||||
send_notifications(comment)
|
SendNotificationForCommentWorkflow.new(comment: comment).run
|
||||||
|
|
||||||
render json: comment.attributes.merge(
|
render json: comment.attributes.merge(
|
||||||
{ user_full_name: current_user.full_name, user_email: current_user.email }
|
{ user_full_name: current_user.full_name, user_email: current_user.email }
|
||||||
@@ -37,14 +37,9 @@ class CommentsController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
comment = Comment.find(params[:id])
|
comment = Comment.find(params[:id])
|
||||||
|
authorize comment
|
||||||
comment.assign_attributes(comment_params)
|
comment.assign_attributes(comment_params)
|
||||||
|
|
||||||
if !current_user.power_user? && current_user.id != post.user_id
|
|
||||||
render json: t('backend.errors.unauthorized'), status: :unauthorized
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if comment.save
|
if comment.save
|
||||||
render json: comment.attributes.merge(
|
render json: comment.attributes.merge(
|
||||||
{ user_full_name: current_user.full_name, user_email: current_user.email }
|
{ user_full_name: current_user.full_name, user_email: current_user.email }
|
||||||
@@ -57,6 +52,7 @@ class CommentsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def comment_params
|
def comment_params
|
||||||
params
|
params
|
||||||
.require(:comment)
|
.require(:comment)
|
||||||
@@ -66,32 +62,4 @@ class CommentsController < ApplicationController
|
|||||||
post_id: params[:post_id]
|
post_id: params[:post_id]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_notifications(comment)
|
|
||||||
if comment.is_post_update # Post update
|
|
||||||
UserMailer.notify_followers_of_post_update(comment: comment).deliver_later
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if comment.parent_id == nil # Reply to a post
|
|
||||||
user = comment.post.user
|
|
||||||
|
|
||||||
if comment.user.id != user.id and
|
|
||||||
user.notifications_enabled? and
|
|
||||||
comment.post.follows.exists?(user_id: user.id)
|
|
||||||
|
|
||||||
UserMailer.notify_post_owner(comment: comment).deliver_later
|
|
||||||
end
|
|
||||||
else # Reply to a comment
|
|
||||||
parent_comment = comment.parent
|
|
||||||
user = parent_comment.user
|
|
||||||
|
|
||||||
if user.notifications_enabled? and
|
|
||||||
parent_comment.user.id != comment.user.id
|
|
||||||
|
|
||||||
UserMailer.notify_comment_owner(comment: comment).deliver_later
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class PostStatusesController < ApplicationController
|
class PostStatusesController < ApplicationController
|
||||||
include ApplicationHelper
|
include ApplicationHelper
|
||||||
|
|
||||||
before_action :authenticate_admin, only: [:create, :update, :update_order, :destroy]
|
before_action :authenticate_user!, only: [:create, :update, :update_order, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
post_statuses = PostStatus.order(order: :asc)
|
post_statuses = PostStatus.order(order: :asc)
|
||||||
@@ -11,6 +11,7 @@ class PostStatusesController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
post_status = PostStatus.new(post_status_params)
|
post_status = PostStatus.new(post_status_params)
|
||||||
|
authorize post_status
|
||||||
|
|
||||||
if post_status.save
|
if post_status.save
|
||||||
render json: post_status, status: :created
|
render json: post_status, status: :created
|
||||||
@@ -23,6 +24,7 @@ class PostStatusesController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
post_status = PostStatus.find(params[:id])
|
post_status = PostStatus.find(params[:id])
|
||||||
|
authorize post_status
|
||||||
post_status.assign_attributes(post_status_params)
|
post_status.assign_attributes(post_status_params)
|
||||||
|
|
||||||
if post_status.save
|
if post_status.save
|
||||||
@@ -36,6 +38,7 @@ class PostStatusesController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
post_status = PostStatus.find(params[:id])
|
post_status = PostStatus.find(params[:id])
|
||||||
|
authorize post_status
|
||||||
|
|
||||||
if post_status.destroy
|
if post_status.destroy
|
||||||
render json: {
|
render json: {
|
||||||
@@ -49,6 +52,8 @@ class PostStatusesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_order
|
def update_order
|
||||||
|
authorize PostStatus
|
||||||
|
|
||||||
workflow_output = ReorderWorkflow.new(
|
workflow_output = ReorderWorkflow.new(
|
||||||
entity_classname: PostStatus,
|
entity_classname: PostStatus,
|
||||||
column_name: 'order',
|
column_name: 'order',
|
||||||
|
|||||||
@@ -52,11 +52,7 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
post = Post.find(params[:id])
|
post = Post.find(params[:id])
|
||||||
|
authorize post
|
||||||
if !current_user.power_user? && current_user.id != post.user_id
|
|
||||||
render json: t('backend.errors.unauthorized'), status: :unauthorized
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
post.board_id = params[:post][:board_id] if params[:post].has_key?(:board_id)
|
post.board_id = params[:post][:board_id] if params[:post].has_key?(:board_id)
|
||||||
|
|
||||||
@@ -91,7 +87,7 @@ class PostsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
defaults = { board_id: Board.first.id }
|
defaults = Board.first ? { board_id: Board.first.id } : {}
|
||||||
|
|
||||||
params
|
params
|
||||||
.permit(:board_id, :post_status_id, :page, :search)
|
.permit(:board_id, :post_status_id, :page, :search)
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
module BoardsHelper
|
|
||||||
end
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
module CommentsHelper
|
|
||||||
end
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
module LikesHelper
|
|
||||||
end
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
module PostsHelper
|
|
||||||
end
|
|
||||||
53
app/policies/application_policy.rb
Normal file
53
app/policies/application_policy.rb
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ApplicationPolicy
|
||||||
|
attr_reader :user, :record
|
||||||
|
|
||||||
|
def initialize(user, record)
|
||||||
|
@user = user
|
||||||
|
@record = record
|
||||||
|
end
|
||||||
|
|
||||||
|
def index?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def show?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def new?
|
||||||
|
create?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit?
|
||||||
|
update?
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
class Scope
|
||||||
|
def initialize(user, scope)
|
||||||
|
@user = user
|
||||||
|
@scope = scope
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve
|
||||||
|
raise NotImplementedError, "You must define #resolve in #{self.class}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :user, :scope
|
||||||
|
end
|
||||||
|
end
|
||||||
17
app/policies/board_policy.rb
Normal file
17
app/policies/board_policy.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class BoardPolicy < ApplicationPolicy
|
||||||
|
def create?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_order?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
end
|
||||||
5
app/policies/comment_policy.rb
Normal file
5
app/policies/comment_policy.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class CommentPolicy < ApplicationPolicy
|
||||||
|
def update?
|
||||||
|
user == record.user or user.power_user?
|
||||||
|
end
|
||||||
|
end
|
||||||
5
app/policies/post_policy.rb
Normal file
5
app/policies/post_policy.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class PostPolicy < ApplicationPolicy
|
||||||
|
def update?
|
||||||
|
user == record.user or user.power_user?
|
||||||
|
end
|
||||||
|
end
|
||||||
17
app/policies/post_status_policy.rb
Normal file
17
app/policies/post_status_policy.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class PostStatusPolicy < ApplicationPolicy
|
||||||
|
def create?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_order?
|
||||||
|
user.admin?
|
||||||
|
end
|
||||||
|
end
|
||||||
34
app/workflows/SendNotificationForCommentWorkflow.rb
Normal file
34
app/workflows/SendNotificationForCommentWorkflow.rb
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
class SendNotificationForCommentWorkflow
|
||||||
|
attr_accessor :comment
|
||||||
|
|
||||||
|
def initialize(comment: "")
|
||||||
|
@comment = comment
|
||||||
|
end
|
||||||
|
|
||||||
|
def run
|
||||||
|
if comment.is_post_update # Post update
|
||||||
|
UserMailer.notify_followers_of_post_update(comment: comment).deliver_later
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if comment.parent_id == nil # Reply to a post
|
||||||
|
user = comment.post.user
|
||||||
|
|
||||||
|
if comment.user.id != user.id and
|
||||||
|
user.notifications_enabled? and
|
||||||
|
comment.post.follows.exists?(user_id: user.id)
|
||||||
|
|
||||||
|
UserMailer.notify_post_owner(comment: comment).deliver_later
|
||||||
|
end
|
||||||
|
else # Reply to a comment
|
||||||
|
parent_comment = comment.parent
|
||||||
|
user = parent_comment.user
|
||||||
|
|
||||||
|
if user.notifications_enabled? and
|
||||||
|
parent_comment.user.id != comment.user.id
|
||||||
|
|
||||||
|
UserMailer.notify_comment_owner(comment: comment).deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -251,7 +251,7 @@ Devise.setup do |config|
|
|||||||
# should add them to the navigational formats lists.
|
# should add them to the navigational formats lists.
|
||||||
#
|
#
|
||||||
# The "*/*" below is required to match Internet Explorer requests.
|
# The "*/*" below is required to match Internet Explorer requests.
|
||||||
# config.navigational_formats = ['*/*', :html]
|
config.navigational_formats = ['*/*', :html]
|
||||||
|
|
||||||
# The default HTTP method used to sign out a resource. Default is :delete.
|
# The default HTTP method used to sign out a resource. Default is :delete.
|
||||||
config.sign_out_via = :delete
|
config.sign_out_via = :delete
|
||||||
|
|||||||
61
spec/requests/posts_controller_auth_spec.rb
Normal file
61
spec/requests/posts_controller_auth_spec.rb
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'requests to posts controller', type: :request do
|
||||||
|
let(:user) { FactoryBot.create(:user) }
|
||||||
|
let(:moderator) { FactoryBot.create(:moderator) }
|
||||||
|
let(:admin) { FactoryBot.create(:admin) }
|
||||||
|
|
||||||
|
let(:p) { FactoryBot.create(:post) }
|
||||||
|
let(:board) { FactoryBot.build_stubbed(:board) }
|
||||||
|
|
||||||
|
let(:headers) { headers = { "ACCEPT" => "application/json" } }
|
||||||
|
|
||||||
|
context 'when user is not logged in' do
|
||||||
|
it 'fulfills index action' do
|
||||||
|
get posts_path
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
it 'fulfills show action' do
|
||||||
|
get posts_path(p)
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
it 'blocks create action' do
|
||||||
|
post posts_path, params: { post: { title: p.title } }, headers: headers
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
it 'blocks update action' do
|
||||||
|
patch post_path(p), params: { post: { title: p.title } }, headers: headers
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'user role' do
|
||||||
|
before(:each) do
|
||||||
|
user.confirm
|
||||||
|
sign_in user
|
||||||
|
end
|
||||||
|
|
||||||
|
# it 'fulfills create action' do
|
||||||
|
# post posts_path, params: { post: { title: p.title, board_id: board.id, user_id: user.id } }, headers: headers
|
||||||
|
# expect(response).to have_http_status(:success)
|
||||||
|
# end
|
||||||
|
it 'blocks update action if from different user' do
|
||||||
|
expect(user.id).not_to eq(p.user_id)
|
||||||
|
patch post_path(p), params: { post: { title: p.title } }, headers: headers
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'moderator role' do
|
||||||
|
before(:each) do
|
||||||
|
moderator.confirm
|
||||||
|
sign_in moderator
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fulfills update action' do
|
||||||
|
expect(user.id).not_to eq(p.user_id)
|
||||||
|
patch post_path(p), params: { post: { title: p.title } }, headers: headers
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user