Change authorized applications page (#17656)
* Change authorized applications page * Hide revoke button for superapps and suspended accounts * Clean up db/schema.rb
This commit is contained in:
		
							parent
							
								
									233f7e6174
								
							
						
					
					
						commit
						50ea54b3ed
					
				
					 20 changed files with 393 additions and 62 deletions
				
			
		| 
						 | 
				
			
			@ -5,6 +5,7 @@ class Api::BaseController < ApplicationController
 | 
			
		|||
  DEFAULT_ACCOUNTS_LIMIT = 40
 | 
			
		||||
 | 
			
		||||
  include RateLimitHeaders
 | 
			
		||||
  include AccessTokenTrackingConcern
 | 
			
		||||
 | 
			
		||||
  skip_before_action :store_current_location
 | 
			
		||||
  skip_before_action :require_functional!, unless: :whitelist_mode?
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										21
									
								
								app/controllers/concerns/access_token_tracking_concern.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/controllers/concerns/access_token_tracking_concern.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module AccessTokenTrackingConcern
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  ACCESS_TOKEN_UPDATE_FREQUENCY = 24.hours.freeze
 | 
			
		||||
 | 
			
		||||
  included do
 | 
			
		||||
    before_action :update_access_token_last_used
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def update_access_token_last_used
 | 
			
		||||
    doorkeeper_token.update_last_used(request) if access_token_needs_update?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def access_token_needs_update?
 | 
			
		||||
    doorkeeper_token.present? && (doorkeeper_token.last_used_at.nil? || doorkeeper_token.last_used_at < ACCESS_TOKEN_UPDATE_FREQUENCY.ago)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
module SessionTrackingConcern
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  UPDATE_SIGN_IN_HOURS = 24
 | 
			
		||||
  SESSION_UPDATE_FREQUENCY = 24.hours.freeze
 | 
			
		||||
 | 
			
		||||
  included do
 | 
			
		||||
    before_action :set_session_activity
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +17,6 @@ module SessionTrackingConcern
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def session_needs_update?
 | 
			
		||||
    !current_session.nil? && current_session.updated_at < UPDATE_SIGN_IN_HOURS.hours.ago
 | 
			
		||||
    !current_session.nil? && current_session.updated_at < SESSION_UPDATE_FREQUENCY.ago
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
module UserTrackingConcern
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  UPDATE_SIGN_IN_FREQUENCY = 24.hours.freeze
 | 
			
		||||
  SIGN_IN_UPDATE_FREQUENCY = 24.hours.freeze
 | 
			
		||||
 | 
			
		||||
  included do
 | 
			
		||||
    before_action :update_user_sign_in
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +16,6 @@ module UserTrackingConcern
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def user_needs_sign_in_update?
 | 
			
		||||
    user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_FREQUENCY.ago)
 | 
			
		||||
    user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < SIGN_IN_UPDATE_FREQUENCY.ago)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -224,4 +224,19 @@ module ApplicationHelper
 | 
			
		|||
    content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
 | 
			
		||||
    # rubocop:enable Rails/OutputSafety
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def grouped_scopes(scopes)
 | 
			
		||||
    scope_parser      = ScopeParser.new
 | 
			
		||||
    scope_transformer = ScopeTransformer.new
 | 
			
		||||
 | 
			
		||||
    scopes.each_with_object({}) do |str, h|
 | 
			
		||||
      scope = scope_transformer.apply(scope_parser.parse(str))
 | 
			
		||||
 | 
			
		||||
      if h[scope.key]
 | 
			
		||||
        h[scope.key].merge!(scope)
 | 
			
		||||
      else
 | 
			
		||||
        h[scope.key] = scope
 | 
			
		||||
      end
 | 
			
		||||
    end.values
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -907,6 +907,12 @@ a.name-tag,
 | 
			
		|||
      text-decoration: none;
 | 
			
		||||
      margin-bottom: 10px;
 | 
			
		||||
 | 
			
		||||
      .account-role {
 | 
			
		||||
        vertical-align: middle;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    a.announcements-list__item__title {
 | 
			
		||||
      &:hover,
 | 
			
		||||
      &:focus,
 | 
			
		||||
      &:active {
 | 
			
		||||
| 
						 | 
				
			
			@ -925,6 +931,10 @@ a.name-tag,
 | 
			
		|||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__permissions {
 | 
			
		||||
      margin-top: 10px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      border-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,6 @@
 | 
			
		|||
.container-alt {
 | 
			
		||||
  width: 700px;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
  margin-top: 40px;
 | 
			
		||||
 | 
			
		||||
  @media screen and (max-width: 740px) {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -67,22 +66,20 @@
 | 
			
		|||
  line-height: 18px;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  padding: 20px 0;
 | 
			
		||||
  padding-bottom: 0;
 | 
			
		||||
  margin-bottom: -30px;
 | 
			
		||||
  margin-top: 40px;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  border-bottom: 1px solid $ui-base-color;
 | 
			
		||||
 | 
			
		||||
  @media screen and (max-width: 440px) {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    padding-bottom: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .avatar {
 | 
			
		||||
    width: 40px;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
    margin-right: 8px;
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
 | 
			
		||||
    img {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +93,7 @@
 | 
			
		|||
  .name {
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    color: $secondary-text-color;
 | 
			
		||||
    width: calc(100% - 88px);
 | 
			
		||||
    width: calc(100% - 90px);
 | 
			
		||||
 | 
			
		||||
    .username {
 | 
			
		||||
      display: block;
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +107,7 @@
 | 
			
		|||
    display: block;
 | 
			
		||||
    font-size: 32px;
 | 
			
		||||
    line-height: 40px;
 | 
			
		||||
    margin-left: 8px;
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -800,9 +800,41 @@ code {
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  @media screen and (max-width: 740px) and (min-width: 441px) {
 | 
			
		||||
    margin-top: 40px;
 | 
			
		||||
.oauth-prompt {
 | 
			
		||||
  h3 {
 | 
			
		||||
    color: $ui-secondary-color;
 | 
			
		||||
    font-size: 17px;
 | 
			
		||||
    line-height: 22px;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
    margin-bottom: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  p {
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    line-height: 18px;
 | 
			
		||||
    margin-bottom: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .permissions-list {
 | 
			
		||||
    border: 1px solid $ui-base-color;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    background: darken($ui-base-color, 4%);
 | 
			
		||||
    margin-bottom: 30px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .actions {
 | 
			
		||||
    margin: 0 -10px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
 | 
			
		||||
    form {
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      padding: 0 10px;
 | 
			
		||||
      flex: 1 1 auto;
 | 
			
		||||
      min-height: 1px;
 | 
			
		||||
      width: 50%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1005,3 +1037,38 @@ code {
 | 
			
		|||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.permissions-list {
 | 
			
		||||
  &__item {
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
    color: $ui-secondary-color;
 | 
			
		||||
    border-bottom: 1px solid lighten($ui-base-color, 4%);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
 | 
			
		||||
    &__text {
 | 
			
		||||
      flex: 1 1 auto;
 | 
			
		||||
 | 
			
		||||
      &__title {
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &__type {
 | 
			
		||||
        color: $darker-text-color;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__icon {
 | 
			
		||||
      flex: 0 0 auto;
 | 
			
		||||
      font-size: 18px;
 | 
			
		||||
      width: 30px;
 | 
			
		||||
      color: $valid-value-color;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:last-child {
 | 
			
		||||
      border-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,10 @@ module AccessTokenExtension
 | 
			
		|||
    update(revoked_at: clock.now.utc)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update_last_used(request, clock = Time)
 | 
			
		||||
    update(last_used_at: clock.now.utc, last_used_ip: request.remote_ip)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def push_to_streaming_api
 | 
			
		||||
    Redis.current.publish("timeline:access_token:#{id}", Oj.dump(event: :kill)) if revoked? || destroyed?
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,4 +8,8 @@ module ApplicationExtension
 | 
			
		|||
    validates :website, url: true, length: { maximum: 2_000 }, if: :website?
 | 
			
		||||
    validates :redirect_uri, length: { maximum: 2_000 }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def most_recently_used_access_token
 | 
			
		||||
    @most_recently_used_access_token ||= access_tokens.where.not(last_used_at: nil).order(last_used_at: :desc).first
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										10
									
								
								app/lib/scope_parser.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/lib/scope_parser.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class ScopeParser < Parslet::Parser
 | 
			
		||||
  rule(:term)      { match('[a-z]').repeat(1).as(:term) }
 | 
			
		||||
  rule(:colon)     { str(':') }
 | 
			
		||||
  rule(:access)    { (str('write') | str('read')).as(:access) }
 | 
			
		||||
  rule(:namespace) { str('admin').as(:namespace) }
 | 
			
		||||
  rule(:scope)     { ((namespace >> colon).maybe >> ((access >> colon >> term) | access | term)).as(:scope) }
 | 
			
		||||
  root(:scope)
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										40
									
								
								app/lib/scope_transformer.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/lib/scope_transformer.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class ScopeTransformer < Parslet::Transform
 | 
			
		||||
  class Scope
 | 
			
		||||
    DEFAULT_TERM   = 'all'
 | 
			
		||||
    DEFAULT_ACCESS = %w(read write).freeze
 | 
			
		||||
 | 
			
		||||
    attr_reader :namespace, :term
 | 
			
		||||
 | 
			
		||||
    def initialize(scope)
 | 
			
		||||
      @namespace = scope[:namespace]&.to_s
 | 
			
		||||
      @access    = scope[:access] ? [scope[:access].to_s] : DEFAULT_ACCESS.dup
 | 
			
		||||
      @term      = scope[:term]&.to_s || DEFAULT_TERM
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def key
 | 
			
		||||
      @key ||= [@namespace, @term].compact.join('/')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def access
 | 
			
		||||
      @access.join('/')
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def merge(other_scope)
 | 
			
		||||
      clone.merge!(other_scope)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def merge!(other_scope)
 | 
			
		||||
      raise ArgumentError unless other_scope.namespace == namespace && other_scope.term == term
 | 
			
		||||
 | 
			
		||||
      @access.concat(other_scope.instance_variable_get('@access'))
 | 
			
		||||
      @access.uniq!
 | 
			
		||||
      @access.sort!
 | 
			
		||||
 | 
			
		||||
      self
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  rule(scope: subtree(:scope)) { Scope.new(scope) }
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -12,8 +12,9 @@
 | 
			
		|||
        = fa_icon 'sign-out'
 | 
			
		||||
 | 
			
		||||
  .container-alt= yield
 | 
			
		||||
 | 
			
		||||
  .modal-layout__mastodon
 | 
			
		||||
    %div
 | 
			
		||||
      %img{alt:'', draggable:'false', src:"#{mascot_url}"}
 | 
			
		||||
      %img{alt: '', draggable: 'false', src: mascot_url }
 | 
			
		||||
 | 
			
		||||
= render template: 'layouts/application'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +1,38 @@
 | 
			
		|||
- content_for :page_title do
 | 
			
		||||
  = t('doorkeeper.authorizations.new.title')
 | 
			
		||||
 | 
			
		||||
.form-container
 | 
			
		||||
.form-container.simple_form
 | 
			
		||||
  .oauth-prompt
 | 
			
		||||
    %h2= t('doorkeeper.authorizations.new.prompt', client_name: @pre_auth.client.name)
 | 
			
		||||
    %h3= t('doorkeeper.authorizations.new.title')
 | 
			
		||||
 | 
			
		||||
    %p
 | 
			
		||||
      = t('doorkeeper.authorizations.new.able_to')
 | 
			
		||||
      != @pre_auth.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.map { |s| "<strong>#{s}</strong>" }.to_sentence
 | 
			
		||||
    %p= t('doorkeeper.authorizations.new.prompt_html', client_name: content_tag(:strong, @pre_auth.client.name))
 | 
			
		||||
 | 
			
		||||
  = 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
 | 
			
		||||
    %h3= t('doorkeeper.authorizations.new.review_permissions')
 | 
			
		||||
 | 
			
		||||
  = 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'
 | 
			
		||||
    %ul.permissions-list
 | 
			
		||||
      - grouped_scopes(@pre_auth.scopes).each do |scope|
 | 
			
		||||
        %li.permissions-list__item
 | 
			
		||||
          .permissions-list__item__icon
 | 
			
		||||
            = fa_icon('check')
 | 
			
		||||
          .permissions-list__item__text
 | 
			
		||||
            .permissions-list__item__text__title
 | 
			
		||||
              = t(scope.key, scope: [:doorkeeper, :grouped_scopes, :title])
 | 
			
		||||
            .permissions-list__item__text__type
 | 
			
		||||
              = t(scope.access, scope: [:doorkeeper, :grouped_scopes, :access])
 | 
			
		||||
 | 
			
		||||
    .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,24 +1,44 @@
 | 
			
		|||
- content_for :page_title do
 | 
			
		||||
  = t('doorkeeper.authorized_applications.index.title')
 | 
			
		||||
 | 
			
		||||
.table-wrapper
 | 
			
		||||
  %table.table
 | 
			
		||||
    %thead
 | 
			
		||||
      %tr
 | 
			
		||||
        %th= t('doorkeeper.authorized_applications.index.application')
 | 
			
		||||
        %th= t('doorkeeper.authorized_applications.index.scopes')
 | 
			
		||||
        %th= t('doorkeeper.authorized_applications.index.created_at')
 | 
			
		||||
        %th
 | 
			
		||||
    %tbody
 | 
			
		||||
      - @applications.each do |application|
 | 
			
		||||
        %tr
 | 
			
		||||
          %td
 | 
			
		||||
            - if application.website.blank?
 | 
			
		||||
              = application.name
 | 
			
		||||
            - else
 | 
			
		||||
              = link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer'
 | 
			
		||||
          %th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ')
 | 
			
		||||
          %td= l application.created_at
 | 
			
		||||
          %td
 | 
			
		||||
            - unless application.superapp? || current_account.suspended?
 | 
			
		||||
              = table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') }
 | 
			
		||||
%p= t('doorkeeper.authorized_applications.index.description_html')
 | 
			
		||||
 | 
			
		||||
%hr.spacer/
 | 
			
		||||
 | 
			
		||||
.announcements-list
 | 
			
		||||
  - @applications.each do |application|
 | 
			
		||||
    .announcements-list__item
 | 
			
		||||
      - if application.website.present?
 | 
			
		||||
        = link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer', class: 'announcements-list__item__title'
 | 
			
		||||
      - else
 | 
			
		||||
        %strong.announcements-list__item__title
 | 
			
		||||
          = application.name
 | 
			
		||||
          - if application.superapp?
 | 
			
		||||
            %span.account-role.moderator= t('doorkeeper.authorized_applications.index.superapp')
 | 
			
		||||
 | 
			
		||||
      .announcements-list__item__action-bar
 | 
			
		||||
        .announcements-list__item__meta
 | 
			
		||||
          - if application.most_recently_used_access_token
 | 
			
		||||
            = t('doorkeeper.authorized_applications.index.last_used_at', date: l(application.most_recently_used_access_token.last_used_at.to_date))
 | 
			
		||||
          - else
 | 
			
		||||
            = t('doorkeeper.authorized_applications.index.never_used')
 | 
			
		||||
 | 
			
		||||
          •
 | 
			
		||||
 | 
			
		||||
          = t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date))
 | 
			
		||||
 | 
			
		||||
        - unless application.superapp? || current_account.suspended?
 | 
			
		||||
          %div
 | 
			
		||||
            = table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') }
 | 
			
		||||
 | 
			
		||||
      .announcements-list__item__permissions
 | 
			
		||||
        %ul.permissions-list
 | 
			
		||||
          - grouped_scopes(application.scopes).each do |scope|
 | 
			
		||||
            %li.permissions-list__item
 | 
			
		||||
              .permissions-list__item__icon
 | 
			
		||||
                = fa_icon('check')
 | 
			
		||||
              .permissions-list__item__text
 | 
			
		||||
                .permissions-list__item__text__title
 | 
			
		||||
                  = t(scope.key, scope: [:doorkeeper, :grouped_scopes, :title])
 | 
			
		||||
                .permissions-list__item__text__type
 | 
			
		||||
                  = t(scope.access, scope: [:doorkeeper, :grouped_scopes, :access])
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,7 @@ class Scheduler::IpCleanupScheduler
 | 
			
		|||
    SessionActivation.where('updated_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all
 | 
			
		||||
    User.where('current_sign_in_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(sign_up_ip: nil)
 | 
			
		||||
    LoginActivity.where('created_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all
 | 
			
		||||
    Doorkeeper::AccessToken.where('last_used_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(last_used_ip: nil)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def clean_expired_ip_blocks!
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,8 +60,8 @@ en:
 | 
			
		|||
      error:
 | 
			
		||||
        title: An error has occurred
 | 
			
		||||
      new:
 | 
			
		||||
        able_to: It will be able to
 | 
			
		||||
        prompt: Application %{client_name} requests access to your account
 | 
			
		||||
        prompt_html: "%{client_name} would like permission to access your account. It is a third-party application. <strong>If you do not trust it, then you should not authorize it.</strong>"
 | 
			
		||||
        review_permissions: Review permissions
 | 
			
		||||
        title: Authorization required
 | 
			
		||||
      show:
 | 
			
		||||
        title: Copy this authorization code and paste it to the application.
 | 
			
		||||
| 
						 | 
				
			
			@ -71,10 +71,12 @@ en:
 | 
			
		|||
      confirmations:
 | 
			
		||||
        revoke: Are you sure?
 | 
			
		||||
      index:
 | 
			
		||||
        application: Application
 | 
			
		||||
        created_at: Authorized
 | 
			
		||||
        date_format: "%Y-%m-%d %H:%M:%S"
 | 
			
		||||
        scopes: Scopes
 | 
			
		||||
        authorized_at: Authorized on %{date}
 | 
			
		||||
        description_html: These are applications that can access your account using the API. If there are applications you do not recognize here, or an application is misbehaving, you can revoke its access.
 | 
			
		||||
        last_used_at: Last used on %{date}
 | 
			
		||||
        never_used: Never used
 | 
			
		||||
        scopes: Permissions
 | 
			
		||||
        superapp: Internal
 | 
			
		||||
        title: Your authorized applications
 | 
			
		||||
    errors:
 | 
			
		||||
      messages:
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +112,33 @@ en:
 | 
			
		|||
      authorized_applications:
 | 
			
		||||
        destroy:
 | 
			
		||||
          notice: Application revoked.
 | 
			
		||||
    grouped_scopes:
 | 
			
		||||
      access:
 | 
			
		||||
        read: Read-only access
 | 
			
		||||
        read/write: Read and write access
 | 
			
		||||
        write: Write-only access
 | 
			
		||||
      title:
 | 
			
		||||
        accounts: Accounts
 | 
			
		||||
        admin/accounts: Administration of accounts
 | 
			
		||||
        admin/all: All administrative functions
 | 
			
		||||
        admin/reports: Administration of reports
 | 
			
		||||
        all: Everything
 | 
			
		||||
        blocks: Blocks
 | 
			
		||||
        bookmarks: Bookmarks
 | 
			
		||||
        conversations: Conversations
 | 
			
		||||
        crypto: End-to-end encryption
 | 
			
		||||
        favourites: Favourites
 | 
			
		||||
        filters: Filters
 | 
			
		||||
        follow: Relationships
 | 
			
		||||
        follows: Follows
 | 
			
		||||
        lists: Lists
 | 
			
		||||
        media: Media attachments
 | 
			
		||||
        mutes: Mutes
 | 
			
		||||
        notifications: Notifications
 | 
			
		||||
        push: Push notifications
 | 
			
		||||
        reports: Reports
 | 
			
		||||
        search: Search
 | 
			
		||||
        statuses: Posts
 | 
			
		||||
    layouts:
 | 
			
		||||
      admin:
 | 
			
		||||
        nav:
 | 
			
		||||
| 
						 | 
				
			
			@ -124,6 +153,7 @@ en:
 | 
			
		|||
      admin:write: modify all data on the server
 | 
			
		||||
      admin:write:accounts: perform moderation actions on accounts
 | 
			
		||||
      admin:write:reports: perform moderation actions on reports
 | 
			
		||||
      crypto: use end-to-end encryption
 | 
			
		||||
      follow: modify account relationships
 | 
			
		||||
      push: receive your push notifications
 | 
			
		||||
      read: read all your account's data
 | 
			
		||||
| 
						 | 
				
			
			@ -143,6 +173,7 @@ en:
 | 
			
		|||
      write:accounts: modify your profile
 | 
			
		||||
      write:blocks: block accounts and domains
 | 
			
		||||
      write:bookmarks: bookmark posts
 | 
			
		||||
      write:conversations: mute and delete conversations
 | 
			
		||||
      write:favourites: favourite posts
 | 
			
		||||
      write:filters: create filters
 | 
			
		||||
      write:follows: follow people
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
class AddLastUsedAtToOauthAccessTokens < ActiveRecord::Migration[6.1]
 | 
			
		||||
  def change
 | 
			
		||||
    add_column :oauth_access_tokens, :last_used_at, :datetime
 | 
			
		||||
    add_column :oauth_access_tokens, :last_used_ip, :inet
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
#
 | 
			
		||||
# It's strongly recommended that you check this file into your version control system.
 | 
			
		||||
 | 
			
		||||
ActiveRecord::Schema.define(version: 2022_02_24_010024) do
 | 
			
		||||
ActiveRecord::Schema.define(version: 2022_02_27_041951) do
 | 
			
		||||
 | 
			
		||||
  # These are extensions that must be enabled in order to support this database
 | 
			
		||||
  enable_extension "plpgsql"
 | 
			
		||||
| 
						 | 
				
			
			@ -630,6 +630,8 @@ ActiveRecord::Schema.define(version: 2022_02_24_010024) do
 | 
			
		|||
    t.string "scopes"
 | 
			
		||||
    t.bigint "application_id"
 | 
			
		||||
    t.bigint "resource_owner_id"
 | 
			
		||||
    t.datetime "last_used_at"
 | 
			
		||||
    t.inet "last_used_ip"
 | 
			
		||||
    t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true
 | 
			
		||||
    t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id"
 | 
			
		||||
    t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										89
									
								
								spec/lib/scope_transformer_spec.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								spec/lib/scope_transformer_spec.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,89 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe ScopeTransformer do
 | 
			
		||||
  describe '#apply' do
 | 
			
		||||
    subject { described_class.new.apply(ScopeParser.new.parse(input)) }
 | 
			
		||||
 | 
			
		||||
    shared_examples 'a scope' do |namespace, term, access|
 | 
			
		||||
      it 'parses the term' do
 | 
			
		||||
        expect(subject.term).to eq term
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'parses the namespace' do
 | 
			
		||||
        expect(subject.namespace).to eq namespace
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'parses the access' do
 | 
			
		||||
        expect(subject.access).to eq access
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "read"' do
 | 
			
		||||
      let(:input) { 'read' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', nil, 'all', 'read'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "write"' do
 | 
			
		||||
      let(:input) { 'write' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', nil, 'all', 'write'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "follow"' do
 | 
			
		||||
      let(:input) { 'follow' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', nil, 'follow', 'read/write'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "crypto"' do
 | 
			
		||||
      let(:input) { 'crypto' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', nil, 'crypto', 'read/write'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "push"' do
 | 
			
		||||
      let(:input) { 'push' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', nil, 'push', 'read/write'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "admin:read"' do
 | 
			
		||||
      let(:input) { 'admin:read' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', 'admin', 'all', 'read'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "admin:write"' do
 | 
			
		||||
      let(:input) { 'admin:write' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', 'admin', 'all', 'write'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "admin:read:accounts"' do
 | 
			
		||||
      let(:input) { 'admin:read:accounts' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', 'admin', 'accounts', 'read'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "admin:write:accounts"' do
 | 
			
		||||
      let(:input) { 'admin:write:accounts' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', 'admin', 'accounts', 'write'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "read:accounts"' do
 | 
			
		||||
      let(:input) { 'read:accounts' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', nil, 'accounts', 'read'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'for scope "write:accounts"' do
 | 
			
		||||
      let(:input) { 'write:accounts' }
 | 
			
		||||
 | 
			
		||||
      it_behaves_like 'a scope', nil, 'accounts', 'write'
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
		Reference in a new issue