Adding OAuth access scopes, fixing OAuth authorization UI, adding rate limiting
to the API
This commit is contained in:
		
							
								
								
									
										54
									
								
								.rubocop.yml
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								.rubocop.yml
									
									
									
									
									
								
							@@ -1,14 +1,60 @@
 | 
			
		||||
Rails:
 | 
			
		||||
  Enabled: true
 | 
			
		||||
 | 
			
		||||
Metrics/LineLength:
 | 
			
		||||
  Enabled: false
 | 
			
		||||
 | 
			
		||||
Style/PerlBackrefs:
 | 
			
		||||
  AutoCorrect: false
 | 
			
		||||
 | 
			
		||||
Style/ClassAndModuleChildren:
 | 
			
		||||
  Enabled: false
 | 
			
		||||
 | 
			
		||||
Documentation:
 | 
			
		||||
Metrics/BlockNesting:
 | 
			
		||||
  Max: 2
 | 
			
		||||
 | 
			
		||||
Metrics/LineLength:
 | 
			
		||||
  AllowURI: true
 | 
			
		||||
  Enabled: false
 | 
			
		||||
 | 
			
		||||
Metrics/MethodLength:
 | 
			
		||||
  CountComments: false
 | 
			
		||||
  Max: 10
 | 
			
		||||
 | 
			
		||||
Metrics/ModuleLength:
 | 
			
		||||
  Max: 100
 | 
			
		||||
 | 
			
		||||
Metrics/ParameterLists:
 | 
			
		||||
  Max: 4
 | 
			
		||||
  CountKeywordArgs: true
 | 
			
		||||
 | 
			
		||||
Style/AccessModifierIndentation:
 | 
			
		||||
  EnforcedStyle: indent
 | 
			
		||||
 | 
			
		||||
Style/CollectionMethods:
 | 
			
		||||
  Enabled: true
 | 
			
		||||
  PreferredMethods:
 | 
			
		||||
    find_all: 'select'
 | 
			
		||||
 | 
			
		||||
Style/Documentation:
 | 
			
		||||
  Enabled: false
 | 
			
		||||
 | 
			
		||||
Style/DoubleNegation:
 | 
			
		||||
  Enabled: false
 | 
			
		||||
 | 
			
		||||
Style/FrozenStringLiteralComment:
 | 
			
		||||
  Enabled: false
 | 
			
		||||
 | 
			
		||||
Style/SpaceInsideHashLiteralBraces:
 | 
			
		||||
  EnforcedStyle: space
 | 
			
		||||
 | 
			
		||||
Style/TrailingCommaInLiteral:
 | 
			
		||||
  EnforcedStyleForMultiline: 'comma'
 | 
			
		||||
 | 
			
		||||
Style/RegexpLiteral:
 | 
			
		||||
  Enabled: false
 | 
			
		||||
 | 
			
		||||
AllCops:
 | 
			
		||||
  TargetRubyVersion: 2.2
 | 
			
		||||
  Exclude:
 | 
			
		||||
  - 'spec/**/*'
 | 
			
		||||
  - 'db/**/*'
 | 
			
		||||
  - 'app/views/**/*'
 | 
			
		||||
  - 'config/**/*'
 | 
			
		||||
 
 | 
			
		||||
@@ -85,18 +85,7 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .prompt {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    color: #9baec8;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
 | 
			
		||||
    .prompt-highlight {
 | 
			
		||||
      font-weight: 500;
 | 
			
		||||
      color: #fff;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  code.copypasteable {
 | 
			
		||||
  code {
 | 
			
		||||
    display: block;
 | 
			
		||||
    font-family: 'Roboto Mono', monospace;
 | 
			
		||||
    font-weight: 400;
 | 
			
		||||
@@ -110,6 +99,7 @@
 | 
			
		||||
 | 
			
		||||
  .actions {
 | 
			
		||||
    margin-top: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  button {
 | 
			
		||||
    display: block;
 | 
			
		||||
@@ -148,7 +138,6 @@
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flash-message {
 | 
			
		||||
@@ -180,3 +169,18 @@
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.oauth-prompt {
 | 
			
		||||
  margin-bottom: 30px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  color: #9baec8;
 | 
			
		||||
 | 
			
		||||
  h2 {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    margin-bottom: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  strong {
 | 
			
		||||
    color: #d9e1e8;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
 | 
			
		||||
class PublicChannel < ApplicationCable::Channel
 | 
			
		||||
  def subscribed
 | 
			
		||||
    stream_from 'timeline:public', -> (encoded_message) do
 | 
			
		||||
    stream_from 'timeline:public', lambda do |encoded_message|
 | 
			
		||||
      message = ActiveSupport::JSON.decode(encoded_message)
 | 
			
		||||
 | 
			
		||||
      status = Status.find_by(id: message['id'])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
class Api::V1::AccountsController < ApiController
 | 
			
		||||
  before_action :doorkeeper_authorize!
 | 
			
		||||
  before_action -> { doorkeeper_authorize! :read }, except: [:follow, :unfollow, :block, :unblock]
 | 
			
		||||
  before_action -> { doorkeeper_authorize! :follow }, only: [:follow, :unfollow, :block, :unblock]
 | 
			
		||||
 | 
			
		||||
  before_action :set_account, except: [:verify_credentials, :suggestions]
 | 
			
		||||
  respond_to    :json
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
class Api::V1::FollowsController < ApiController
 | 
			
		||||
  before_action :doorkeeper_authorize!
 | 
			
		||||
  before_action -> { doorkeeper_authorize! :follow }
 | 
			
		||||
  respond_to    :json
 | 
			
		||||
 | 
			
		||||
  def create
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
class Api::V1::MediaController < ApiController
 | 
			
		||||
  before_action :doorkeeper_authorize!
 | 
			
		||||
  before_action -> { doorkeeper_authorize! :write }
 | 
			
		||||
  respond_to    :json
 | 
			
		||||
 | 
			
		||||
  def create
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
class Api::V1::StatusesController < ApiController
 | 
			
		||||
  before_action :doorkeeper_authorize!
 | 
			
		||||
  before_action -> { doorkeeper_authorize! :read }, except: [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite]
 | 
			
		||||
  before_action -> { doorkeeper_authorize! :write }, only:  [:create, :destroy, :reblog, :unreblog, :favourite, :unfavourite]
 | 
			
		||||
 | 
			
		||||
  respond_to    :json
 | 
			
		||||
 | 
			
		||||
  def show
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
class ApiController < ApplicationController
 | 
			
		||||
  protect_from_forgery with: :null_session
 | 
			
		||||
 | 
			
		||||
  skip_before_action :verify_authenticity_token
 | 
			
		||||
 | 
			
		||||
  before_action :set_rate_limit_headers
 | 
			
		||||
 | 
			
		||||
  rescue_from ActiveRecord::RecordInvalid do |e|
 | 
			
		||||
    render json: { error: e.to_s }, status: 422
 | 
			
		||||
  end
 | 
			
		||||
@@ -22,8 +25,27 @@ class ApiController < ApplicationController
 | 
			
		||||
    render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def doorkeeper_unauthorized_render_options(*)
 | 
			
		||||
    { json: { error: 'Not authorized' } }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def doorkeeper_forbidden_render_options(*)
 | 
			
		||||
    { json: { error: 'This action is outside the authorized scopes' } }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  protected
 | 
			
		||||
 | 
			
		||||
  def set_rate_limit_headers
 | 
			
		||||
    return if request.env['rack.attack.throttle_data'].nil?
 | 
			
		||||
 | 
			
		||||
    now        = Time.now.utc
 | 
			
		||||
    match_data = request.env['rack.attack.throttle_data']['api']
 | 
			
		||||
 | 
			
		||||
    response.headers['X-RateLimit-Limit']     = match_data[:limit].to_s
 | 
			
		||||
    response.headers['X-RateLimit-Remaining'] = (match_data[:limit] - match_data[:count]).to_s
 | 
			
		||||
    response.headers['X-RateLimit-Reset']     = (now + (match_data[:period] - now.to_i % match_data[:period])).to_s
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def current_resource_owner
 | 
			
		||||
    User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,6 @@ class HomeController < ApplicationController
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def find_or_create_access_token
 | 
			
		||||
    Doorkeeper::AccessToken.find_or_create_for(Doorkeeper::Application.where(superapp: true).first, current_user.id, nil, Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.refresh_token_enabled?)
 | 
			
		||||
    Doorkeeper::AccessToken.find_or_create_for(Doorkeeper::Application.where(superapp: true).first, current_user.id, 'read write follow', Doorkeeper.configuration.access_token_expires_in, Doorkeeper.configuration.refresh_token_enabled?)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								app/controllers/oauth/authorizations_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/controllers/oauth/authorizations_controller.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
 | 
			
		||||
  before_action :store_current_location
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def store_current_location
 | 
			
		||||
    store_location_for(:user, request.url)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -7,7 +7,7 @@ class Feed
 | 
			
		||||
  def get(limit, max_id = nil, since_id = nil)
 | 
			
		||||
    max_id     = '+inf' if max_id.blank?
 | 
			
		||||
    since_id   = '-inf' if since_id.blank?
 | 
			
		||||
    unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).collect(&:last).map(&:to_i)
 | 
			
		||||
    unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i)
 | 
			
		||||
 | 
			
		||||
    # If we're after most recent items and none are there, we need to precompute the feed
 | 
			
		||||
    if unhydrated.empty? && max_id == '+inf' && since_id == '-inf'
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,8 @@ class MediaAttachment < ApplicationRecord
 | 
			
		||||
    image? ? 'image' : 'video'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
private
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def self.file_styles(f)
 | 
			
		||||
    if f.instance.image?
 | 
			
		||||
      {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
.prompt= t('doorkeeper.authorizations.error.title')
 | 
			
		||||
 | 
			
		||||
#error_explanation
 | 
			
		||||
  = @pre_auth.error_response.body[:error_description]
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
.prompt= raw t('.prompt', client_name: "<strong class=\"prompt-highlight\">#{ @pre_auth.client.name }</strong>")
 | 
			
		||||
 | 
			
		||||
/- if @pre_auth.scopes.count > 0
 | 
			
		||||
/  .scope-permission-prompt
 | 
			
		||||
/    %p= t('.able_to')
 | 
			
		||||
 | 
			
		||||
/    %ul.scope-permissions
 | 
			
		||||
/      - @pre_auth.scopes.each do |scope|
 | 
			
		||||
/        %li= t scope, scope: [:doorkeeper, :scopes]
 | 
			
		||||
 | 
			
		||||
.actions
 | 
			
		||||
  = form_tag oauth_authorization_path, method: :post do
 | 
			
		||||
    = hidden_field_tag :client_id, @pre_auth.client.uid
 | 
			
		||||
    = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
 | 
			
		||||
    = hidden_field_tag :state, @pre_auth.state
 | 
			
		||||
    = hidden_field_tag :response_type, @pre_auth.response_type
 | 
			
		||||
    = hidden_field_tag :scope, @pre_auth.scope
 | 
			
		||||
    = button_tag t('doorkeeper.authorizations.buttons.authorize'), type: :submit
 | 
			
		||||
 | 
			
		||||
  = form_tag oauth_authorization_path, method: :delete do
 | 
			
		||||
    = hidden_field_tag :client_id, @pre_auth.client.uid
 | 
			
		||||
    = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
 | 
			
		||||
    = hidden_field_tag :state, @pre_auth.state
 | 
			
		||||
    = hidden_field_tag :response_type, @pre_auth.response_type
 | 
			
		||||
    = hidden_field_tag :scope, @pre_auth.scope
 | 
			
		||||
    = button_tag t('doorkeeper.authorizations.buttons.deny'), type: :submit, class: 'negative'
 | 
			
		||||
@@ -1,2 +0,0 @@
 | 
			
		||||
.prompt= t('.title')
 | 
			
		||||
%code.copypasteable= params[:code]
 | 
			
		||||
							
								
								
									
										2
									
								
								app/views/oauth/authorizations/error.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/views/oauth/authorizations/error.html.haml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
.flash-message#error_explanation
 | 
			
		||||
  = @pre_auth.error_response.body[:error_description]
 | 
			
		||||
							
								
								
									
										25
									
								
								app/views/oauth/authorizations/new.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/views/oauth/authorizations/new.html.haml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
.oauth-prompt
 | 
			
		||||
  %h2
 | 
			
		||||
    Application
 | 
			
		||||
    %strong=@pre_auth.client.name
 | 
			
		||||
    requests access to your account
 | 
			
		||||
 | 
			
		||||
  %p
 | 
			
		||||
    It will be able to
 | 
			
		||||
    = @pre_auth.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.map { |s| "<strong>#{s}</strong>"}.to_sentence.html_safe
 | 
			
		||||
 | 
			
		||||
= form_tag oauth_authorization_path, method: :post, class: 'simple_form' do
 | 
			
		||||
  = hidden_field_tag :client_id, @pre_auth.client.uid
 | 
			
		||||
  = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
 | 
			
		||||
  = hidden_field_tag :state, @pre_auth.state
 | 
			
		||||
  = hidden_field_tag :response_type, @pre_auth.response_type
 | 
			
		||||
  = hidden_field_tag :scope, @pre_auth.scope
 | 
			
		||||
  = button_tag t('doorkeeper.authorizations.buttons.authorize'), type: :submit
 | 
			
		||||
 | 
			
		||||
= form_tag oauth_authorization_path, method: :delete, class: 'simple_form' do
 | 
			
		||||
  = hidden_field_tag :client_id, @pre_auth.client.uid
 | 
			
		||||
  = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
 | 
			
		||||
  = hidden_field_tag :state, @pre_auth.state
 | 
			
		||||
  = hidden_field_tag :response_type, @pre_auth.response_type
 | 
			
		||||
  = hidden_field_tag :scope, @pre_auth.scope
 | 
			
		||||
  = button_tag t('doorkeeper.authorizations.buttons.deny'), type: :submit, class: 'negative'
 | 
			
		||||
							
								
								
									
										1
									
								
								app/views/oauth/authorizations/show.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/views/oauth/authorizations/show.html.haml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
%code= params[:code]
 | 
			
		||||
@@ -12,7 +12,7 @@ Rails.application.configure do
 | 
			
		||||
 | 
			
		||||
  # Full error reports are disabled and caching is turned on.
 | 
			
		||||
  config.consider_all_requests_local       = false
 | 
			
		||||
  config.action_controller.perform_caching = false
 | 
			
		||||
  config.action_controller.perform_caching = true
 | 
			
		||||
 | 
			
		||||
  # Disable serving static files from the `/public` folder by default since
 | 
			
		||||
  # Apache or NGINX already handles this.
 | 
			
		||||
 
 | 
			
		||||
@@ -50,8 +50,8 @@ Doorkeeper.configure do
 | 
			
		||||
  # Define access token scopes for your provider
 | 
			
		||||
  # For more information go to
 | 
			
		||||
  # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
 | 
			
		||||
  # default_scopes  :public
 | 
			
		||||
  # optional_scopes :write, :follow
 | 
			
		||||
  default_scopes  :read
 | 
			
		||||
  optional_scopes :write, :follow
 | 
			
		||||
 | 
			
		||||
  # Change the way client credentials are retrieved from the request object.
 | 
			
		||||
  # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
Rabl.configure do |config|
 | 
			
		||||
  config.cache_all_output  = true
 | 
			
		||||
  config.cache_all_output  = false
 | 
			
		||||
  config.cache_sources     = !!Rails.env.production?
 | 
			
		||||
  config.include_json_root = false
 | 
			
		||||
  config.view_paths        = [Rails.root.join('app/views')]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,19 @@
 | 
			
		||||
class Rack::Attack
 | 
			
		||||
  throttle('get-req/ip', limit: 300, period: 5.minutes) do |req|
 | 
			
		||||
    req.ip if req.get?
 | 
			
		||||
  # Rate limits for the API
 | 
			
		||||
  throttle('api', limit: 150, period: 5.minutes) do |req|
 | 
			
		||||
    req.ip if req.path.match(/\A\/api\//)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  throttle('post-req/ip', limit: 100, period: 5.minutes) do |req|
 | 
			
		||||
    req.ip if req.post?
 | 
			
		||||
  self.throttled_response = lambda do |env|
 | 
			
		||||
    now        = Time.now.utc
 | 
			
		||||
    match_data = env['rack.attack.match_data']
 | 
			
		||||
 | 
			
		||||
    headers = {
 | 
			
		||||
      'X-RateLimit-Limit'     => match_data[:limit].to_s,
 | 
			
		||||
      'X-RateLimit-Remaining' => '0',
 | 
			
		||||
      'X-RateLimit-Reset'     => (now + (match_data[:period] - now.to_i % match_data[:period])).to_s
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [429, headers, [{ error: 'Throttled' }.to_json]]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,10 @@ en:
 | 
			
		||||
              secured_uri: 'must be an HTTPS/SSL URI.'
 | 
			
		||||
 | 
			
		||||
  doorkeeper:
 | 
			
		||||
    scopes:
 | 
			
		||||
      read: read your account's data
 | 
			
		||||
      write: post on your behalf
 | 
			
		||||
      follow: follow, block, unblock and unfollow accounts
 | 
			
		||||
    applications:
 | 
			
		||||
      confirmations:
 | 
			
		||||
        destroy: 'Are you sure?'
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,9 @@ Rails.application.routes.draw do
 | 
			
		||||
    mount Sidekiq::Web => '/sidekiq'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  use_doorkeeper
 | 
			
		||||
  use_doorkeeper do
 | 
			
		||||
    controllers authorizations: 'oauth/authorizations'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  get '.well-known/host-meta', to: 'xrd#host_meta', as: :host_meta
 | 
			
		||||
  get '.well-known/webfinger', to: 'xrd#webfinger', as: :webfinger
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +1,2 @@
 | 
			
		||||
web_app = Doorkeeper::Application.new(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri)
 | 
			
		||||
web_app = Doorkeeper::Application.new(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow')
 | 
			
		||||
web_app.save!
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,7 @@ namespace :mastodon do
 | 
			
		||||
  namespace :media do
 | 
			
		||||
    desc 'Removes media attachments that have not been assigned to any status for longer than a day'
 | 
			
		||||
    task clear: :environment do
 | 
			
		||||
      MediaAttachment.where(status_id: nil).where('created_at < ?', 1.day.ago).find_each do |m|
 | 
			
		||||
        m.destroy
 | 
			
		||||
      end
 | 
			
		||||
      MediaAttachment.where(status_id: nil).where('created_at < ?', 1.day.ago).find_each(&:destroy)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user