to the APImaster
@@ -1,14 +1,60 @@ | |||||
Rails: | Rails: | ||||
Enabled: true | Enabled: true | ||||
Metrics/LineLength: | |||||
Enabled: false | |||||
Style/PerlBackrefs: | Style/PerlBackrefs: | ||||
AutoCorrect: false | AutoCorrect: false | ||||
Style/ClassAndModuleChildren: | Style/ClassAndModuleChildren: | ||||
Enabled: false | 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 | 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; | display: block; | ||||
font-family: 'Roboto Mono', monospace; | font-family: 'Roboto Mono', monospace; | ||||
font-weight: 400; | font-weight: 400; | ||||
@@ -110,42 +99,42 @@ | |||||
.actions { | .actions { | ||||
margin-top: 30px; | margin-top: 30px; | ||||
} | |||||
button { | |||||
display: block; | |||||
width: 100%; | |||||
border: 0; | |||||
border-radius: 4px; | |||||
background: #2b90d9; | |||||
color: #fff; | |||||
font-size: 18px; | |||||
padding: 10px; | |||||
text-transform: uppercase; | |||||
cursor: pointer; | |||||
font-weight: 500; | |||||
outline: 0; | |||||
margin-bottom: 10px; | |||||
button { | |||||
display: block; | |||||
width: 100%; | |||||
border: 0; | |||||
border-radius: 4px; | |||||
background: #2b90d9; | |||||
color: #fff; | |||||
font-size: 18px; | |||||
padding: 10px; | |||||
text-transform: uppercase; | |||||
cursor: pointer; | |||||
font-weight: 500; | |||||
outline: 0; | |||||
margin-bottom: 10px; | |||||
&:hover { | |||||
background-color: lighten(#2b90d9, 5%); | |||||
} | |||||
&:hover { | |||||
background-color: lighten(#2b90d9, 5%); | |||||
} | |||||
&:active, &:focus { | |||||
position: relative; | |||||
top: 1px; | |||||
background-color: darken(#2b90d9, 5%); | |||||
} | |||||
&:active, &:focus { | |||||
position: relative; | |||||
top: 1px; | |||||
background-color: darken(#2b90d9, 5%); | |||||
} | |||||
&.negative { | |||||
background: #df405a; | |||||
&.negative { | |||||
background: #df405a; | |||||
&:hover { | |||||
background-color: lighten(#df405a, 5%); | |||||
} | |||||
&:hover { | |||||
background-color: lighten(#df405a, 5%); | |||||
} | |||||
&:active, &:focus { | |||||
background-color: darken(#df405a, 5%); | |||||
} | |||||
&:active, &:focus { | |||||
background-color: darken(#df405a, 5%); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -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. | # 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 | class PublicChannel < ApplicationCable::Channel | ||||
def subscribed | def subscribed | ||||
stream_from 'timeline:public', -> (encoded_message) do | |||||
stream_from 'timeline:public', lambda do |encoded_message| | |||||
message = ActiveSupport::JSON.decode(encoded_message) | message = ActiveSupport::JSON.decode(encoded_message) | ||||
status = Status.find_by(id: message['id']) | status = Status.find_by(id: message['id']) | ||||
@@ -1,5 +1,7 @@ | |||||
class Api::V1::AccountsController < ApiController | 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] | before_action :set_account, except: [:verify_credentials, :suggestions] | ||||
respond_to :json | respond_to :json | ||||
@@ -1,5 +1,5 @@ | |||||
class Api::V1::FollowsController < ApiController | class Api::V1::FollowsController < ApiController | ||||
before_action :doorkeeper_authorize! | |||||
before_action -> { doorkeeper_authorize! :follow } | |||||
respond_to :json | respond_to :json | ||||
def create | def create | ||||
@@ -1,5 +1,5 @@ | |||||
class Api::V1::MediaController < ApiController | class Api::V1::MediaController < ApiController | ||||
before_action :doorkeeper_authorize! | |||||
before_action -> { doorkeeper_authorize! :write } | |||||
respond_to :json | respond_to :json | ||||
def create | def create | ||||
@@ -1,5 +1,7 @@ | |||||
class Api::V1::StatusesController < ApiController | 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 | respond_to :json | ||||
def show | def show | ||||
@@ -1,7 +1,10 @@ | |||||
class ApiController < ApplicationController | class ApiController < ApplicationController | ||||
protect_from_forgery with: :null_session | protect_from_forgery with: :null_session | ||||
skip_before_action :verify_authenticity_token | skip_before_action :verify_authenticity_token | ||||
before_action :set_rate_limit_headers | |||||
rescue_from ActiveRecord::RecordInvalid do |e| | rescue_from ActiveRecord::RecordInvalid do |e| | ||||
render json: { error: e.to_s }, status: 422 | render json: { error: e.to_s }, status: 422 | ||||
end | end | ||||
@@ -22,8 +25,27 @@ class ApiController < ApplicationController | |||||
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503 | render json: { error: 'Remote SSL certificate could not be verified' }, status: 503 | ||||
end | 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 | 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 | def current_resource_owner | ||||
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token | User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token | ||||
end | end | ||||
@@ -15,6 +15,6 @@ class HomeController < ApplicationController | |||||
end | end | ||||
def find_or_create_access_token | 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 | ||||
end | end |
@@ -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) | def get(limit, max_id = nil, since_id = nil) | ||||
max_id = '+inf' if max_id.blank? | max_id = '+inf' if max_id.blank? | ||||
since_id = '-inf' if since_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 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' | if unhydrated.empty? && max_id == '+inf' && since_id == '-inf' | ||||
@@ -34,7 +34,8 @@ class MediaAttachment < ApplicationRecord | |||||
image? ? 'image' : 'video' | image? ? 'image' : 'video' | ||||
end | end | ||||
private | |||||
private | |||||
def self.file_styles(f) | def self.file_styles(f) | ||||
if f.instance.image? | 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] |
@@ -0,0 +1,2 @@ | |||||
.flash-message#error_explanation | |||||
= @pre_auth.error_response.body[:error_description] |
@@ -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' |
@@ -0,0 +1 @@ | |||||
%code= params[:code] |
@@ -12,7 +12,7 @@ Rails.application.configure do | |||||
# Full error reports are disabled and caching is turned on. | # Full error reports are disabled and caching is turned on. | ||||
config.consider_all_requests_local = false | 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 | # Disable serving static files from the `/public` folder by default since | ||||
# Apache or NGINX already handles this. | # Apache or NGINX already handles this. | ||||
@@ -50,8 +50,8 @@ Doorkeeper.configure do | |||||
# Define access token scopes for your provider | # Define access token scopes for your provider | ||||
# For more information go to | # For more information go to | ||||
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes | # 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. | # Change the way client credentials are retrieved from the request object. | ||||
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then | # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then | ||||
@@ -1,5 +1,5 @@ | |||||
Rabl.configure do |config| | Rabl.configure do |config| | ||||
config.cache_all_output = true | |||||
config.cache_all_output = false | |||||
config.cache_sources = !!Rails.env.production? | config.cache_sources = !!Rails.env.production? | ||||
config.include_json_root = false | config.include_json_root = false | ||||
config.view_paths = [Rails.root.join('app/views')] | config.view_paths = [Rails.root.join('app/views')] | ||||
@@ -1,9 +1,19 @@ | |||||
class Rack::Attack | 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 | 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 | ||||
end | end |
@@ -15,6 +15,10 @@ en: | |||||
secured_uri: 'must be an HTTPS/SSL URI.' | secured_uri: 'must be an HTTPS/SSL URI.' | ||||
doorkeeper: | doorkeeper: | ||||
scopes: | |||||
read: read your account's data | |||||
write: post on your behalf | |||||
follow: follow, block, unblock and unfollow accounts | |||||
applications: | applications: | ||||
confirmations: | confirmations: | ||||
destroy: 'Are you sure?' | destroy: 'Are you sure?' | ||||
@@ -7,7 +7,9 @@ Rails.application.routes.draw do | |||||
mount Sidekiq::Web => '/sidekiq' | mount Sidekiq::Web => '/sidekiq' | ||||
end | 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/host-meta', to: 'xrd#host_meta', as: :host_meta | ||||
get '.well-known/webfinger', to: 'xrd#webfinger', as: :webfinger | 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! | web_app.save! |
@@ -2,9 +2,7 @@ namespace :mastodon do | |||||
namespace :media do | namespace :media do | ||||
desc 'Removes media attachments that have not been assigned to any status for longer than a day' | desc 'Removes media attachments that have not been assigned to any status for longer than a day' | ||||
task clear: :environment do | 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 | ||||
end | end | ||||