Change authorized applications page (#17656)
* Change authorized applications page * Hide revoke button for superapps and suspended accounts * Clean up db/schema.rbpull/17682/head
parent
233f7e6174
commit
50ea54b3ed
|
@ -5,6 +5,7 @@ class Api::BaseController < ApplicationController
|
||||||
DEFAULT_ACCOUNTS_LIMIT = 40
|
DEFAULT_ACCOUNTS_LIMIT = 40
|
||||||
|
|
||||||
include RateLimitHeaders
|
include RateLimitHeaders
|
||||||
|
include AccessTokenTrackingConcern
|
||||||
|
|
||||||
skip_before_action :store_current_location
|
skip_before_action :store_current_location
|
||||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||||
|
|
|
@ -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
|
module SessionTrackingConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
UPDATE_SIGN_IN_HOURS = 24
|
SESSION_UPDATE_FREQUENCY = 24.hours.freeze
|
||||||
|
|
||||||
included do
|
included do
|
||||||
before_action :set_session_activity
|
before_action :set_session_activity
|
||||||
|
@ -17,6 +17,6 @@ module SessionTrackingConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def session_needs_update?
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module UserTrackingConcern
|
module UserTrackingConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
UPDATE_SIGN_IN_FREQUENCY = 24.hours.freeze
|
SIGN_IN_UPDATE_FREQUENCY = 24.hours.freeze
|
||||||
|
|
||||||
included do
|
included do
|
||||||
before_action :update_user_sign_in
|
before_action :update_user_sign_in
|
||||||
|
@ -16,6 +16,6 @@ module UserTrackingConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_needs_sign_in_update?
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -224,4 +224,19 @@ module ApplicationHelper
|
||||||
content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
|
content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
|
||||||
# rubocop:enable Rails/OutputSafety
|
# rubocop:enable Rails/OutputSafety
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -907,6 +907,12 @@ a.name-tag,
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.account-role {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.announcements-list__item__title {
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
|
@ -925,6 +931,10 @@ a.name-tag,
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__permissions {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
.container-alt {
|
.container-alt {
|
||||||
width: 700px;
|
width: 700px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-top: 40px;
|
|
||||||
|
|
||||||
@media screen and (max-width: 740px) {
|
@media screen and (max-width: 740px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -67,22 +66,20 @@
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
padding-bottom: 0;
|
|
||||||
margin-bottom: -30px;
|
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid $ui-base-color;
|
||||||
|
|
||||||
@media screen and (max-width: 440px) {
|
@media screen and (max-width: 440px) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
margin-right: 8px;
|
margin-right: 10px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -96,7 +93,7 @@
|
||||||
.name {
|
.name {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
width: calc(100% - 88px);
|
width: calc(100% - 90px);
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -110,7 +107,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
margin-left: 8px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -800,9 +800,41 @@ code {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 740px) and (min-width: 441px) {
|
.oauth-prompt {
|
||||||
margin-top: 40px;
|
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;
|
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)
|
update(revoked_at: clock.now.utc)
|
||||||
end
|
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
|
def push_to_streaming_api
|
||||||
Redis.current.publish("timeline:access_token:#{id}", Oj.dump(event: :kill)) if revoked? || destroyed?
|
Redis.current.publish("timeline:access_token:#{id}", Oj.dump(event: :kill)) if revoked? || destroyed?
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,4 +8,8 @@ module ApplicationExtension
|
||||||
validates :website, url: true, length: { maximum: 2_000 }, if: :website?
|
validates :website, url: true, length: { maximum: 2_000 }, if: :website?
|
||||||
validates :redirect_uri, length: { maximum: 2_000 }
|
validates :redirect_uri, length: { maximum: 2_000 }
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -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
|
|
@ -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'
|
= fa_icon 'sign-out'
|
||||||
|
|
||||||
.container-alt= yield
|
.container-alt= yield
|
||||||
|
|
||||||
.modal-layout__mastodon
|
.modal-layout__mastodon
|
||||||
%div
|
%div
|
||||||
%img{alt:'', draggable:'false', src:"#{mascot_url}"}
|
%img{alt: '', draggable: 'false', src: mascot_url }
|
||||||
|
|
||||||
= render template: 'layouts/application'
|
= render template: 'layouts/application'
|
||||||
|
|
|
@ -1,26 +1,38 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('doorkeeper.authorizations.new.title')
|
= t('doorkeeper.authorizations.new.title')
|
||||||
|
|
||||||
.form-container
|
.form-container.simple_form
|
||||||
.oauth-prompt
|
.oauth-prompt
|
||||||
%h2= t('doorkeeper.authorizations.new.prompt', client_name: @pre_auth.client.name)
|
%h3= t('doorkeeper.authorizations.new.title')
|
||||||
|
|
||||||
%p
|
%p= t('doorkeeper.authorizations.new.prompt_html', client_name: content_tag(:strong, @pre_auth.client.name))
|
||||||
= t('doorkeeper.authorizations.new.able_to')
|
|
||||||
!= @pre_auth.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.map { |s| "<strong>#{s}</strong>" }.to_sentence
|
|
||||||
|
|
||||||
= form_tag oauth_authorization_path, method: :post, class: 'simple_form' do
|
%h3= t('doorkeeper.authorizations.new.review_permissions')
|
||||||
= 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
|
%ul.permissions-list
|
||||||
= hidden_field_tag :client_id, @pre_auth.client.uid
|
- grouped_scopes(@pre_auth.scopes).each do |scope|
|
||||||
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
|
%li.permissions-list__item
|
||||||
= hidden_field_tag :state, @pre_auth.state
|
.permissions-list__item__icon
|
||||||
= hidden_field_tag :response_type, @pre_auth.response_type
|
= fa_icon('check')
|
||||||
= hidden_field_tag :scope, @pre_auth.scope
|
.permissions-list__item__text
|
||||||
= button_tag t('doorkeeper.authorizations.buttons.deny'), type: :submit, class: 'negative'
|
.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
|
- content_for :page_title do
|
||||||
= t('doorkeeper.authorized_applications.index.title')
|
= t('doorkeeper.authorized_applications.index.title')
|
||||||
|
|
||||||
.table-wrapper
|
%p= t('doorkeeper.authorized_applications.index.description_html')
|
||||||
%table.table
|
|
||||||
%thead
|
%hr.spacer/
|
||||||
%tr
|
|
||||||
%th= t('doorkeeper.authorized_applications.index.application')
|
.announcements-list
|
||||||
%th= t('doorkeeper.authorized_applications.index.scopes')
|
- @applications.each do |application|
|
||||||
%th= t('doorkeeper.authorized_applications.index.created_at')
|
.announcements-list__item
|
||||||
%th
|
- if application.website.present?
|
||||||
%tbody
|
= link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer', class: 'announcements-list__item__title'
|
||||||
- @applications.each do |application|
|
- else
|
||||||
%tr
|
%strong.announcements-list__item__title
|
||||||
%td
|
= application.name
|
||||||
- if application.website.blank?
|
- if application.superapp?
|
||||||
= application.name
|
%span.account-role.moderator= t('doorkeeper.authorized_applications.index.superapp')
|
||||||
- else
|
|
||||||
= link_to application.name, application.website, target: '_blank', rel: 'noopener noreferrer'
|
.announcements-list__item__action-bar
|
||||||
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join(', ')
|
.announcements-list__item__meta
|
||||||
%td= l application.created_at
|
- if application.most_recently_used_access_token
|
||||||
%td
|
= t('doorkeeper.authorized_applications.index.last_used_at', date: l(application.most_recently_used_access_token.last_used_at.to_date))
|
||||||
- unless application.superapp? || current_account.suspended?
|
- else
|
||||||
= 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') }
|
= 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
|
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)
|
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
|
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
|
end
|
||||||
|
|
||||||
def clean_expired_ip_blocks!
|
def clean_expired_ip_blocks!
|
||||||
|
|
|
@ -60,8 +60,8 @@ en:
|
||||||
error:
|
error:
|
||||||
title: An error has occurred
|
title: An error has occurred
|
||||||
new:
|
new:
|
||||||
able_to: It will be able to
|
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>"
|
||||||
prompt: Application %{client_name} requests access to your account
|
review_permissions: Review permissions
|
||||||
title: Authorization required
|
title: Authorization required
|
||||||
show:
|
show:
|
||||||
title: Copy this authorization code and paste it to the application.
|
title: Copy this authorization code and paste it to the application.
|
||||||
|
@ -71,10 +71,12 @@ en:
|
||||||
confirmations:
|
confirmations:
|
||||||
revoke: Are you sure?
|
revoke: Are you sure?
|
||||||
index:
|
index:
|
||||||
application: Application
|
authorized_at: Authorized on %{date}
|
||||||
created_at: Authorized
|
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.
|
||||||
date_format: "%Y-%m-%d %H:%M:%S"
|
last_used_at: Last used on %{date}
|
||||||
scopes: Scopes
|
never_used: Never used
|
||||||
|
scopes: Permissions
|
||||||
|
superapp: Internal
|
||||||
title: Your authorized applications
|
title: Your authorized applications
|
||||||
errors:
|
errors:
|
||||||
messages:
|
messages:
|
||||||
|
@ -110,6 +112,33 @@ en:
|
||||||
authorized_applications:
|
authorized_applications:
|
||||||
destroy:
|
destroy:
|
||||||
notice: Application revoked.
|
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:
|
layouts:
|
||||||
admin:
|
admin:
|
||||||
nav:
|
nav:
|
||||||
|
@ -124,6 +153,7 @@ en:
|
||||||
admin:write: modify all data on the server
|
admin:write: modify all data on the server
|
||||||
admin:write:accounts: perform moderation actions on accounts
|
admin:write:accounts: perform moderation actions on accounts
|
||||||
admin:write:reports: perform moderation actions on reports
|
admin:write:reports: perform moderation actions on reports
|
||||||
|
crypto: use end-to-end encryption
|
||||||
follow: modify account relationships
|
follow: modify account relationships
|
||||||
push: receive your push notifications
|
push: receive your push notifications
|
||||||
read: read all your account's data
|
read: read all your account's data
|
||||||
|
@ -143,6 +173,7 @@ en:
|
||||||
write:accounts: modify your profile
|
write:accounts: modify your profile
|
||||||
write:blocks: block accounts and domains
|
write:blocks: block accounts and domains
|
||||||
write:bookmarks: bookmark posts
|
write:bookmarks: bookmark posts
|
||||||
|
write:conversations: mute and delete conversations
|
||||||
write:favourites: favourite posts
|
write:favourites: favourite posts
|
||||||
write:filters: create filters
|
write:filters: create filters
|
||||||
write:follows: follow people
|
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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -630,6 +630,8 @@ ActiveRecord::Schema.define(version: 2022_02_24_010024) do
|
||||||
t.string "scopes"
|
t.string "scopes"
|
||||||
t.bigint "application_id"
|
t.bigint "application_id"
|
||||||
t.bigint "resource_owner_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 ["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 ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id"
|
||||||
t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true
|
t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue