From 236191794412724f865b99bf5c0bb7c33a51ae67 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 21 Mar 2019 23:33:18 +0100 Subject: Mark the 410 gone response for suspended accounts as cachable (#10339) This will help a great deal with #9377 when a caching reverse proxy is configured. --- app/controllers/concerns/account_controller_concern.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'app/controllers') diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 8817fd7de..4f28941ae 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -69,6 +69,10 @@ module AccountControllerConcern end def check_account_suspension - gone if @account.suspended? + if @account.suspended? + skip_session! + expires_in(3.minutes, public: true) + gone + end end end -- cgit From 555c4e11baf58401c1bdd915e4ecef679e6ae514 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 23 Mar 2019 14:07:04 +0100 Subject: Add validations to admin settings (#10348) * Add validations to admin settings - Validate correct HTML markup - Validate presence of contact username & e-mail - Validate that all usernames are valid - Validate that enums have expected values * Fix code style issue * Fix tests --- app/controllers/admin/settings_controller.rb | 73 ++---------- app/models/form/admin_settings.rb | 131 ++++++++++++++------- app/validators/existing_username_validator.rb | 20 ++++ app/validators/html_validator.rb | 14 +++ app/views/admin/settings/edit.html.haml | 1 + config/locales/en.yml | 5 + config/navigation.rb | 2 +- spec/controllers/admin/settings_controller_spec.rb | 4 + 8 files changed, 140 insertions(+), 110 deletions(-) create mode 100644 app/validators/existing_username_validator.rb create mode 100644 app/validators/html_validator.rb (limited to 'app/controllers') diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index a763597f2..dc1c79b7f 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -2,84 +2,29 @@ module Admin class SettingsController < BaseController - ADMIN_SETTINGS = %w( - site_contact_username - site_contact_email - site_title - site_short_description - site_description - site_extended_description - site_terms - registrations_mode - closed_registrations_message - open_deletion - timeline_preview - show_staff_badge - bootstrap_timeline_accounts - theme - thumbnail - hero - mascot - min_invite_role - activity_api_enabled - peers_api_enabled - show_known_fediverse_at_about_page - preview_sensitive_media - custom_css - profile_directory - ).freeze - - BOOLEAN_SETTINGS = %w( - open_deletion - timeline_preview - show_staff_badge - activity_api_enabled - peers_api_enabled - show_known_fediverse_at_about_page - preview_sensitive_media - profile_directory - ).freeze - - UPLOAD_SETTINGS = %w( - thumbnail - hero - mascot - ).freeze - def edit authorize :settings, :show? + @admin_settings = Form::AdminSettings.new end def update authorize :settings, :update? - settings_params.each do |key, value| - if UPLOAD_SETTINGS.include?(key) - upload = SiteUpload.where(var: key).first_or_initialize(var: key) - upload.update(file: value) - else - setting = Setting.where(var: key).first_or_initialize(var: key) - setting.update(value: value_for_update(key, value)) - end - end + @admin_settings = Form::AdminSettings.new(settings_params) - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to edit_admin_settings_path + if @admin_settings.save + flash[:notice] = I18n.t('generic.changes_saved_msg') + redirect_to edit_admin_settings_path + else + render :edit + end end private def settings_params - params.require(:form_admin_settings).permit(ADMIN_SETTINGS) - end - - def value_for_update(key, value) - if BOOLEAN_SETTINGS.include?(key) - value == '1' - else - value - end + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end end end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index a21394a52..2d3aa726d 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -3,49 +3,90 @@ class Form::AdminSettings include ActiveModel::Model - delegate( - :site_contact_username, - :site_contact_username=, - :site_contact_email, - :site_contact_email=, - :site_title, - :site_title=, - :site_short_description, - :site_short_description=, - :site_description, - :site_description=, - :site_extended_description, - :site_extended_description=, - :site_terms, - :site_terms=, - :registrations_mode, - :registrations_mode=, - :closed_registrations_message, - :closed_registrations_message=, - :open_deletion, - :open_deletion=, - :timeline_preview, - :timeline_preview=, - :show_staff_badge, - :show_staff_badge=, - :bootstrap_timeline_accounts, - :bootstrap_timeline_accounts=, - :theme, - :theme=, - :min_invite_role, - :min_invite_role=, - :activity_api_enabled, - :activity_api_enabled=, - :peers_api_enabled, - :peers_api_enabled=, - :show_known_fediverse_at_about_page, - :show_known_fediverse_at_about_page=, - :preview_sensitive_media, - :preview_sensitive_media=, - :custom_css, - :custom_css=, - :profile_directory, - :profile_directory=, - to: Setting - ) + KEYS = %i( + site_contact_username + site_contact_email + site_title + site_short_description + site_description + site_extended_description + site_terms + registrations_mode + closed_registrations_message + open_deletion + timeline_preview + show_staff_badge + bootstrap_timeline_accounts + theme + min_invite_role + activity_api_enabled + peers_api_enabled + show_known_fediverse_at_about_page + preview_sensitive_media + custom_css + profile_directory + ).freeze + + BOOLEAN_KEYS = %i( + open_deletion + timeline_preview + show_staff_badge + activity_api_enabled + peers_api_enabled + show_known_fediverse_at_about_page + preview_sensitive_media + profile_directory + ).freeze + + UPLOAD_KEYS = %i( + thumbnail + hero + mascot + ).freeze + + attr_accessor(*KEYS) + + validates :site_short_description, :site_description, :site_extended_description, :site_terms, :closed_registrations_message, html: true + validates :registrations_mode, inclusion: { in: %w(open approved none) } + validates :min_invite_role, inclusion: { in: %w(disabled user moderator admin) } + validates :site_contact_email, :site_contact_username, presence: true + validates :site_contact_username, existing_username: true + validates :bootstrap_timeline_accounts, existing_username: { multiple: true } + + def initialize(_attributes = {}) + super + initialize_attributes + end + + def save + return false unless valid? + + KEYS.each do |key| + value = instance_variable_get("@#{key}") + + if UPLOAD_KEYS.include?(key) + upload = SiteUpload.where(var: key).first_or_initialize(var: key) + upload.update(file: value) + else + setting = Setting.where(var: key).first_or_initialize(var: key) + setting.update(value: typecast_value(key, value)) + end + end + end + + private + + def initialize_attributes + KEYS.each do |key| + instance_variable_set("@#{key}", Setting.public_send(key)) if instance_variable_get("@#{key}").nil? + end + end + + def typecast_value(key, value) + if BOOLEAN_KEYS.include?(key) + value == '1' + else + value + end + end end diff --git a/app/validators/existing_username_validator.rb b/app/validators/existing_username_validator.rb new file mode 100644 index 000000000..4388a0c98 --- /dev/null +++ b/app/validators/existing_username_validator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ExistingUsernameValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if value.blank? + + if options[:multiple] + missing_usernames = value.split(',').map { |username| username unless Account.find_local(username) }.compact + record.errors.add(attribute, I18n.t('existing_username_validator.not_found_multiple', usernames: missing_usernames.join(', '))) if missing_usernames.any? + else + record.errors.add(attribute, I18n.t('existing_username_validator.not_found')) unless Account.find_local(value) + end + end + + private + + def valid_html?(str) + Nokogiri::HTML.fragment(str).to_s == str + end +end diff --git a/app/validators/html_validator.rb b/app/validators/html_validator.rb new file mode 100644 index 000000000..882c35d41 --- /dev/null +++ b/app/validators/html_validator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class HtmlValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if value.blank? + record.errors.add(attribute, I18n.t('html_validator.invalid_markup')) unless valid_html?(value) + end + + private + + def valid_html?(str) + Nokogiri::HTML.fragment(str).to_s == str + end +end diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index d9b4bf01b..1c2c00f10 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -2,6 +2,7 @@ = t('admin.settings.title') = simple_form_for @admin_settings, url: admin_settings_path, html: { method: :patch } do |f| + = render 'shared/error_messages', object: @admin_settings .fields-group = f.input :site_title, wrapper: :with_label, label: t('admin.settings.site_title') diff --git a/config/locales/en.yml b/config/locales/en.yml index ba42e7ce1..d5ed20623 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -586,6 +586,9 @@ en: content: We're sorry, but something went wrong on our end. title: This page is not correct noscript_html: To use the Mastodon web application, please enable JavaScript. Alternatively, try one of the native apps for Mastodon for your platform. + existing_username_validator: + not_found: could not find a local user with that username + not_found_multiple: could not find %{usernames} exports: archive_takeout: date: Date @@ -633,6 +636,8 @@ en: validation_errors: one: Something isn't quite right yet! Please review the error below other: Something isn't quite right yet! Please review %{count} errors below + html_validator: + invalid_markup: contains invalid HTML markup identity_proofs: active: Active authorize: Yes, authorize diff --git a/config/navigation.rb b/config/navigation.rb index 07aec4b9d..f136141b3 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -37,7 +37,7 @@ SimpleNavigation::Configuration.run do |navigation| primary.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_dashboard_url, if: proc { current_user.staff? } do |admin| admin.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_url - admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? } + admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings} admin.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis} admin.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/relays} admin.item :subscriptions, safe_join([fa_icon('paper-plane-o fw'), t('admin.subscriptions.title')]), admin_subscriptions_url, if: -> { current_user.admin? } diff --git a/spec/controllers/admin/settings_controller_spec.rb b/spec/controllers/admin/settings_controller_spec.rb index 34f6bbdae..6cf0ee20a 100644 --- a/spec/controllers/admin/settings_controller_spec.rb +++ b/spec/controllers/admin/settings_controller_spec.rb @@ -19,6 +19,10 @@ RSpec.describe Admin::SettingsController, type: :controller do end describe 'PUT #update' do + before do + allow_any_instance_of(Form::AdminSettings).to receive(:valid?).and_return(true) + end + describe 'for a record that doesnt exist' do around do |example| before = Setting.site_extended_description -- cgit From e11796432514afb49f3d891f805973a37f00fcf1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 26 Mar 2019 01:24:19 +0100 Subject: Change icons of features on admin dashboard to remove bias (#10366) Red crosses implied that it was bad/unexpected that certain features were not enabled. In reality, they are options, so showing a green or grey power-off icon is more appropriate. Add status of timeline preview as well Fix sample accounts changing too frequently due to wrong query Sample accounts are intended to be sorted by popularity --- app/controllers/admin/dashboard_controller.rb | 1 + app/controllers/directories_controller.rb | 2 +- app/helpers/admin/dashboard_helper.rb | 10 +++ app/javascript/styles/mastodon/admin.scss | 5 ++ app/models/account.rb | 2 +- app/views/admin/dashboard/index.html.haml | 94 +++++++-------------------- config/locales/en.yml | 1 + 7 files changed, 43 insertions(+), 72 deletions(-) create mode 100644 app/helpers/admin/dashboard_helper.rb (limited to 'app/controllers') diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 22bbcec19..f23ed1508 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -29,6 +29,7 @@ module Admin @hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' @trending_hashtags = TrendingTags.get(7) @profile_directory = Setting.profile_directory + @timeline_preview = Setting.timeline_preview end private diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index ff7ff4a42..594907674 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -32,7 +32,7 @@ class DirectoriesController < ApplicationController end def set_accounts - @accounts = Account.discoverable.page(params[:page]).per(40).tap do |query| + @accounts = Account.discoverable.by_recent_status.page(params[:page]).per(40).tap do |query| query.merge!(Account.tagged_with(@tag.id)) if @tag end end diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb new file mode 100644 index 000000000..4ee2cdef4 --- /dev/null +++ b/app/helpers/admin/dashboard_helper.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Admin::DashboardHelper + def feature_hint(feature, enabled) + indicator = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ') + class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint' + + safe_join([feature, content_tag(:span, indicator, class: class_names)]) + end +end diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index f6bfe44cf..fd5c08f04 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -220,6 +220,11 @@ $content-width: 840px; color: $error-value-color; font-weight: 500; } + + .neutral-hint { + color: $dark-text-color; + font-weight: 500; + } } @media screen and (max-width: $no-columns-breakpoint) { diff --git a/app/models/account.rb b/app/models/account.rb index c2a0709f9..51e01246e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -94,7 +94,7 @@ class Account < ApplicationRecord scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) } - scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)).by_recent_status } + scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) } scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) } scope :popular, -> { order('account_stats.followers_count desc') } diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index fa3d70e9e..d448e3862 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -40,35 +40,17 @@ %h4= t 'admin.dashboard.features' %ul %li - = link_to t('admin.dashboard.feature_registrations'), edit_admin_settings_path - - if @registrations_enabled - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - = link_to t('admin.dashboard.feature_invites'), edit_admin_settings_path - - if @invites_enabled - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - = link_to t('admin.dashboard.feature_deletions'), edit_admin_settings_path - - if @deletions_enabled - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - = link_to t('admin.dashboard.feature_profile_directory'), edit_admin_settings_path - - if @profile_directory - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - = link_to t('admin.dashboard.feature_relay'), admin_relays_path - - if @relay_enabled - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' + = feature_hint(link_to(t('admin.dashboard.feature_registrations'), edit_admin_settings_path), @registrations_enabled) + %li + = feature_hint(link_to(t('admin.dashboard.feature_invites'), edit_admin_settings_path), @invites_enabled) + %li + = feature_hint(link_to(t('admin.dashboard.feature_deletions'), edit_admin_settings_path), @deletions_enabled) + %li + = feature_hint(link_to(t('admin.dashboard.feature_profile_directory'), edit_admin_settings_path), @profile_directory) + %li + = feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview) + %li + = feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled) .dashboard__widgets__versions %div @@ -103,47 +85,19 @@ %h4= t 'admin.dashboard.config' %ul %li - = t('admin.dashboard.search') - - if @search_enabled - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - = t('admin.dashboard.single_user_mode') - - if @single_user_mode - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - LDAP - - if @ldap_enabled - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - CAS - - if @cas_enabled - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - SAML - - if @saml_enabled - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - PAM - - if @pam_enabled - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' - %li - = t 'admin.dashboard.hidden_service' - - if @hidden_service - %span.pull-right.positive-hint= fa_icon 'check fw' - - else - %span.pull-right.negative-hint= fa_icon 'times fw' + = feature_hint(t('admin.dashboard.search'), @search_enabled) + %li + = feature_hint(t('admin.dashboard.single_user_mode'), @single_user_mode) + %li + = feature_hint('LDAP', @ldap_enabled) + %li + = feature_hint('CAS', @cas_enabled) + %li + = feature_hint('SAML', @saml_enabled) + %li + = feature_hint('PAM', @pam_enabled) + %li + = feature_hint(t('admin.dashboard.hidden_service'), @hidden_service) .dashboard__widgets__trends %div diff --git a/config/locales/en.yml b/config/locales/en.yml index b0bf2539c..ad1332fd2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -245,6 +245,7 @@ en: feature_profile_directory: Profile directory feature_registrations: Registrations feature_relay: Federation relay + feature_timeline_preview: Timeline preview features: Features hidden_service: Federation with hidden services open_reports: open reports -- cgit From 08ec7435ce10fc74c257e089d6a2e909287e3daa Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 28 Mar 2019 02:16:01 +0100 Subject: Add order options to relationship manager UI (#10404) --- app/controllers/relationships_controller.rb | 9 +++++---- app/helpers/admin/filter_helper.rb | 2 +- app/views/relationships/show.html.haml | 7 +++++++ config/locales/en.yml | 2 ++ 4 files changed, 15 insertions(+), 5 deletions(-) (limited to 'app/controllers') diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 84cb178a6..e6705c327 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -31,13 +31,14 @@ class RelationshipsController < ApplicationController def relationships_scope scope = begin if following_relationship? - current_account.following.joins(:account_stat) + current_account.following.eager_load(:account_stat).reorder(nil) else - current_account.followers.joins(:account_stat) + current_account.followers.eager_load(:account_stat).reorder(nil) end end - scope.merge!(Follow.recent) + scope.merge!(Follow.recent) if params[:order].blank? || params[:order] == 'recent' + scope.merge!(Account.by_recent_status) if params[:order] == 'active' scope.merge!(mutual_relationship_scope) if mutual_relationship? scope.merge!(moved_account_scope) if params[:status] == 'moved' scope.merge!(primary_account_scope) if params[:status] == 'primary' @@ -84,7 +85,7 @@ class RelationshipsController < ApplicationController end def current_params - params.slice(:page, :status, :relationship, :by_domain, :activity).permit(:page, :status, :relationship, :by_domain, :activity) + params.slice(:page, :status, :relationship, :by_domain, :activity, :order).permit(:page, :status, :relationship, :by_domain, :activity, :order) end def action_from_button diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 4fd36ef42..0bda25974 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -7,7 +7,7 @@ module Admin::FilterHelper CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze TAGS_FILTERS = %i(hidden).freeze INSTANCES_FILTERS = %i(limited by_domain).freeze - FOLLOWERS_FILTERS = %i(relationship status by_domain activity).freeze + FOLLOWERS_FILTERS = %i(relationship status by_domain activity order).freeze FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml index 63745119a..e6fff0ad6 100644 --- a/app/views/relationships/show.html.haml +++ b/app/views/relationships/show.html.haml @@ -25,11 +25,18 @@ %li= filter_link_to t('generic.all'), activity: nil %li= filter_link_to t('relationships.dormant'), activity: 'dormant' + .filter-subset + %strong= t 'generic.order_by' + %ul + %li= filter_link_to t('relationships.most_recent'), order: nil + %li= filter_link_to t('relationships.last_active'), order: 'active' + = form_for(@form, url: relationships_path, method: :patch) do |f| = hidden_field_tag :page, params[:page] || 1 = hidden_field_tag :relationship, params[:relationship] = hidden_field_tag :status, params[:status] = hidden_field_tag :activity, params[:activity] + = hidden_field_tag :order, params[:order] .batch-table .batch-table__toolbar diff --git a/config/locales/en.yml b/config/locales/en.yml index 42ba4e35e..d91e89d95 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -774,6 +774,8 @@ en: relationships: activity: Account activity dormant: Dormant + last_active: Last active + most_recent: Most recent moved: Moved mutual: Mutual primary: Primary -- cgit From 69141dca26f8a28d3aff63387b1c8d2bba7fdfa3 Mon Sep 17 00:00:00 2001 From: Alex Gessner Date: Thu, 28 Mar 2019 13:01:09 -0400 Subject: squashed identity proof updates (#10375) --- .../api/v1/accounts/identity_proofs_controller.rb | 19 ++++++++ .../settings/identity_proofs_controller.rb | 22 ++++++++- app/javascript/mastodon/actions/identity_proofs.js | 30 ++++++++++++ .../mastodon/features/account/components/header.js | 17 ++++++- .../features/account_timeline/components/header.js | 4 +- .../containers/header_container.js | 2 + .../mastodon/features/account_timeline/index.js | 3 ++ .../mastodon/reducers/identity_proofs.js | 25 ++++++++++ app/javascript/mastodon/reducers/index.js | 2 + app/javascript/styles/mastodon/containers.scss | 8 ++- app/javascript/styles/mastodon/forms.scss | 9 +++- app/lib/proof_provider/keybase.rb | 3 +- .../proof_provider/keybase/config_serializer.rb | 4 +- app/lib/proof_provider/keybase/verifier.rb | 6 +-- app/models/account_identity_proof.rb | 2 +- app/serializers/rest/identity_proof_serializer.rb | 17 +++++++ app/views/settings/identity_proofs/new.html.haml | 5 ++ config/locales/en.yml | 3 ++ config/routes.rb | 1 + .../settings/identity_proofs_controller_spec.rb | 57 ++++++++++++++++++++-- 20 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 app/controllers/api/v1/accounts/identity_proofs_controller.rb create mode 100644 app/javascript/mastodon/actions/identity_proofs.js create mode 100644 app/javascript/mastodon/reducers/identity_proofs.js create mode 100644 app/serializers/rest/identity_proof_serializer.rb (limited to 'app/controllers') diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb new file mode 100644 index 000000000..bea51ae11 --- /dev/null +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::IdentityProofsController < Api::BaseController + before_action :require_user! + before_action :set_account + + respond_to :json + + def index + @proofs = @account.identity_proofs.active + render json: @proofs, each_serializer: REST::IdentityProofSerializer + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end +end diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index 4a3b89a5e..8f857fdcc 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -18,7 +18,12 @@ class Settings::IdentityProofsController < Settings::BaseController provider_username: params[:provider_username] ) - render layout: 'auth' + if current_account.username == params[:username] + render layout: 'auth' + else + flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username) + redirect_to settings_identity_proofs_path + end end def create @@ -26,6 +31,7 @@ class Settings::IdentityProofsController < Settings::BaseController @proof.token = resource_params[:token] if @proof.save + PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof? redirect_to @proof.on_success_path(params[:user_agent]) else flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize) @@ -36,10 +42,22 @@ class Settings::IdentityProofsController < Settings::BaseController private def check_required_params - redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :token].all? { |k| params[k].present? } + redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? } end def resource_params params.require(:account_identity_proof).permit(:provider, :provider_username, :token) end + + def publish_proof? + ActiveModel::Type::Boolean.new.cast(post_params[:post_status]) + end + + def post_params + params.require(:account_identity_proof).permit(:post_status, :status_text) + end + + def set_body_classes + @body_classes = '' + end end diff --git a/app/javascript/mastodon/actions/identity_proofs.js b/app/javascript/mastodon/actions/identity_proofs.js new file mode 100644 index 000000000..449debf61 --- /dev/null +++ b/app/javascript/mastodon/actions/identity_proofs.js @@ -0,0 +1,30 @@ +import api from '../api'; + +export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL'; + +export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => { + dispatch(fetchAccountIdentityProofsRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`) + .then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err))); +}; + +export const fetchAccountIdentityProofsRequest = id => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, + id, +}); + +export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, + accountId, + identity_proofs, +}); + +export const fetchAccountIdentityProofsFail = (accountId, err) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, + accountId, + err, +}); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index d957de73d..76f50a5a4 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -62,6 +62,7 @@ class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, + identity_props: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -81,7 +82,7 @@ class Header extends ImmutablePureComponent { } render () { - const { account, intl, domain } = this.props; + const { account, intl, domain, identity_proofs } = this.props; if (!account) { return null; @@ -234,8 +235,20 @@ class Header extends ImmutablePureComponent {
- {fields.size > 0 && ( + { (fields.size > 0 || identity_proofs.size > 0) && (
+ {identity_proofs.map((proof, i) => ( +
+
+ +
+ + + + +
+
+ ))} {fields.map((pair, i) => (
diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 16ada18c0..27dfcc516 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -12,6 +12,7 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, + identity_proofs: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -84,7 +85,7 @@ export default class Header extends ImmutablePureComponent { } render () { - const { account, hideTabs } = this.props; + const { account, hideTabs, identity_proofs } = this.props; if (account === null) { return ; @@ -96,6 +97,7 @@ export default class Header extends ImmutablePureComponent { { const mapStateToProps = (state, { accountId }) => ({ account: getAccount(state, accountId), domain: state.getIn(['meta', 'domain']), + identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()), }); return mapStateToProps; diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index afc484c60..883f40d77 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -12,6 +12,7 @@ import ColumnBackButton from '../../components/column_back_button'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; +import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { const path = withReplies ? `${accountId}:with_replies` : accountId; @@ -42,6 +43,7 @@ class AccountTimeline extends ImmutablePureComponent { const { params: { accountId }, withReplies } = this.props; this.props.dispatch(fetchAccount(accountId)); + this.props.dispatch(fetchAccountIdentityProofs(accountId)); if (!withReplies) { this.props.dispatch(expandAccountFeaturedTimeline(accountId)); } @@ -51,6 +53,7 @@ class AccountTimeline extends ImmutablePureComponent { componentWillReceiveProps (nextProps) { if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { this.props.dispatch(fetchAccount(nextProps.params.accountId)); + this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); if (!nextProps.withReplies) { this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); } diff --git a/app/javascript/mastodon/reducers/identity_proofs.js b/app/javascript/mastodon/reducers/identity_proofs.js new file mode 100644 index 000000000..58af0a5fa --- /dev/null +++ b/app/javascript/mastodon/reducers/identity_proofs.js @@ -0,0 +1,25 @@ +import { Map as ImmutableMap, fromJS } from 'immutable'; +import { + IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, + IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, + IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, +} from '../actions/identity_proofs'; + +const initialState = ImmutableMap(); + +export default function identityProofsReducer(state = initialState, action) { + switch(action.type) { + case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST: + return state.set('isLoading', true); + case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL: + return state.set('isLoading', false); + case IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS: + return state.update(identity_proofs => identity_proofs.withMutations(map => { + map.set('isLoading', false); + map.set('loaded', true); + map.set(action.accountId, fromJS(action.identity_proofs)); + })); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index a7e9c4d0f..981ad8e64 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -30,6 +30,7 @@ import filters from './filters'; import conversations from './conversations'; import suggestions from './suggestions'; import polls from './polls'; +import identity_proofs from './identity_proofs'; const reducers = { dropdown_menu, @@ -56,6 +57,7 @@ const reducers = { notifications, height_cache, custom_emojis, + identity_proofs, lists, listEditor, listAdder, diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 2b1d988f2..368c2304b 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -10,12 +10,10 @@ } .logo-container { - margin: 100px auto; - margin-bottom: 50px; + margin: 100px auto 50px; - @media screen and (max-width: 400px) { - margin: 30px auto; - margin-bottom: 20px; + @media screen and (max-width: 500px) { + margin: 40px auto 0; } h1 { diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 3ea104786..91888d305 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -854,13 +854,19 @@ code { flex: 1; flex-direction: column; flex-shrink: 1; + max-width: 50%; &-sep { + align-self: center; flex-grow: 0; overflow: visible; position: relative; z-index: 1; } + + p { + word-break: break-word; + } } .account__avatar { @@ -882,12 +888,13 @@ code { height: 100%; left: 50%; position: absolute; + top: 0; width: 1px; } } &__row { - align-items: center; + align-items: flex-start; display: flex; flex-direction: row; } diff --git a/app/lib/proof_provider/keybase.rb b/app/lib/proof_provider/keybase.rb index 96322a265..672e1cb4b 100644 --- a/app/lib/proof_provider/keybase.rb +++ b/app/lib/proof_provider/keybase.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class ProofProvider::Keybase - BASE_URL = 'https://keybase.io' + BASE_URL = ENV.fetch('KEYBASE_BASE_URL', 'https://keybase.io') + DOMAIN = ENV.fetch('KEYBASE_DOMAIN', Rails.configuration.x.local_domain) class Error < StandardError; end diff --git a/app/lib/proof_provider/keybase/config_serializer.rb b/app/lib/proof_provider/keybase/config_serializer.rb index 557bafe84..5241d201f 100644 --- a/app/lib/proof_provider/keybase/config_serializer.rb +++ b/app/lib/proof_provider/keybase/config_serializer.rb @@ -14,7 +14,7 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer end def domain - Rails.configuration.x.local_domain + ProofProvider::Keybase::DOMAIN end def display_name @@ -66,6 +66,6 @@ class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer end def contact - [Setting.site_contact_email.presence].compact + [Setting.site_contact_email.presence || 'unknown'].compact end end diff --git a/app/lib/proof_provider/keybase/verifier.rb b/app/lib/proof_provider/keybase/verifier.rb index 86f249dd7..ab1422323 100644 --- a/app/lib/proof_provider/keybase/verifier.rb +++ b/app/lib/proof_provider/keybase/verifier.rb @@ -49,14 +49,10 @@ class ProofProvider::Keybase::Verifier def query_params { - domain: domain, + domain: ProofProvider::Keybase::DOMAIN, kb_username: @provider_username, username: @local_username, sig_hash: @token, } end - - def domain - Rails.configuration.x.local_domain - end end diff --git a/app/models/account_identity_proof.rb b/app/models/account_identity_proof.rb index e7a3f97e5..1ac234735 100644 --- a/app/models/account_identity_proof.rb +++ b/app/models/account_identity_proof.rb @@ -26,7 +26,7 @@ class AccountIdentityProof < ApplicationRecord scope :active, -> { where(verified: true, live: true) } - after_create_commit :queue_worker + after_commit :queue_worker, if: :saved_change_to_token? delegate :refresh!, :on_success_path, :badge, to: :provider_instance diff --git a/app/serializers/rest/identity_proof_serializer.rb b/app/serializers/rest/identity_proof_serializer.rb new file mode 100644 index 000000000..0e7415935 --- /dev/null +++ b/app/serializers/rest/identity_proof_serializer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class REST::IdentityProofSerializer < ActiveModel::Serializer + attributes :provider, :provider_username, :updated_at, :proof_url, :profile_url + + def proof_url + object.badge.proof_url + end + + def profile_url + object.badge.profile_url + end + + def provider + object.provider.capitalize + end +end diff --git a/app/views/settings/identity_proofs/new.html.haml b/app/views/settings/identity_proofs/new.html.haml index 8ce6e61c9..5e4e9895d 100644 --- a/app/views/settings/identity_proofs/new.html.haml +++ b/app/views/settings/identity_proofs/new.html.haml @@ -27,5 +27,10 @@ %p= t('identity_proofs.i_am_html', username: content_tag(:strong, @proof.provider_username), service: @proof.provider.capitalize) + .connection-prompt__post + = f.input :post_status, label: t('identity_proofs.publicize_checkbox'), as: :boolean, wrapper: :with_label, :input_html => { checked: true } + + = f.input :status_text, as: :text, input_html: { value: t('identity_proofs.publicize_toot', username: @proof.provider_username, service: @proof.provider.capitalize, url: @proof.badge.proof_url), rows: 4 } + = f.button :button, t('identity_proofs.authorize'), type: :submit = link_to t('simple_form.no'), settings_identity_proofs_url, class: 'button negative' diff --git a/config/locales/en.yml b/config/locales/en.yml index d91e89d95..64467be39 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -648,10 +648,13 @@ en: keybase: invalid_token: Keybase tokens are hashes of signatures and must be 66 hex characters verification_failed: Keybase does not recognize this token as a signature of Keybase user %{kb_username}. Please retry from Keybase. + wrong_user: Cannot create a proof for %{proving} while logged in as %{current}. Log in as %{proving} and try again. explanation_html: Here you can cryptographically connect your other identities, such as a Keybase profile. This lets other people send you encrypted messages and trust content you send them. i_am_html: I am %{username} on %{service}. identity: Identity inactive: Inactive + publicize_checkbox: 'And toot this:' + publicize_toot: 'It is proven! I am %{username} on %{service}: %{url}' status: Verification status view_proof: View proof imports: diff --git a/config/routes.rb b/config/routes.rb index 194b4c09b..a98dbb700 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -354,6 +354,7 @@ Rails.application.routes.draw do resources :followers, only: :index, controller: 'accounts/follower_accounts' resources :following, only: :index, controller: 'accounts/following_accounts' resources :lists, only: :index, controller: 'accounts/lists' + resources :identity_proofs, only: :index, controller: 'accounts/identity_proofs' member do post :follow diff --git a/spec/controllers/settings/identity_proofs_controller_spec.rb b/spec/controllers/settings/identity_proofs_controller_spec.rb index 46af3ccf4..5c05eb83c 100644 --- a/spec/controllers/settings/identity_proofs_controller_spec.rb +++ b/spec/controllers/settings/identity_proofs_controller_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' describe Settings::IdentityProofsController do + include RoutingHelper render_views let(:user) { Fabricate(:user) } @@ -9,8 +10,15 @@ describe Settings::IdentityProofsController do let(:provider) { 'keybase' } let(:findable_id) { Faker::Number.number(5) } let(:unfindable_id) { Faker::Number.number(5) } + let(:new_proof_params) do + { provider: provider, provider_username: kbname, token: valid_token, username: user.account.username } + end + let(:status_text) { "i just proved that i am also #{kbname} on #{provider}." } + let(:status_posting_params) do + { post_status: '0', status_text: status_text } + end let(:postable_params) do - { account_identity_proof: { provider: provider, provider_username: kbname, token: valid_token } } + { account_identity_proof: new_proof_params.merge(status_posting_params) } end before do @@ -19,10 +27,32 @@ describe Settings::IdentityProofsController do end describe 'new proof creation' do - context 'GET #new with no existing proofs' do - it 'redirects to :index' do - get :new - expect(response).to redirect_to settings_identity_proofs_path + context 'GET #new' do + context 'with all of the correct params' do + before do + allow_any_instance_of(ProofProvider::Keybase::Badge).to receive(:avatar_url) { full_pack_url('media/images/void.png') } + end + + it 'renders the template' do + get :new, params: new_proof_params + expect(response).to render_template(:new) + end + end + + context 'without any params' do + it 'redirects to :index' do + get :new, params: {} + expect(response).to redirect_to settings_identity_proofs_path + end + end + + context 'with params to prove a different, not logged-in user' do + let(:wrong_user_params) { new_proof_params.merge(username: 'someone_else') } + + it 'shows a helpful alert' do + get :new, params: wrong_user_params + expect(flash[:alert]).to eq I18n.t('identity_proofs.errors.wrong_user', proving: 'someone_else', current: user.account.username) + end end end @@ -44,6 +74,23 @@ describe Settings::IdentityProofsController do post :create, params: postable_params expect(response).to redirect_to root_url end + + it 'does not post a status' do + expect(PostStatusService).not_to receive(:new) + post :create, params: postable_params + end + + context 'and the user has requested to post a status' do + let(:postable_params_with_status) do + postable_params.tap { |p| p[:account_identity_proof][:post_status] = '1' } + end + + it 'posts a status' do + expect_any_instance_of(PostStatusService).to receive(:call).with(user.account, text: status_text) + + post :create, params: postable_params_with_status + end + end end context 'when saving fails' do -- cgit