Redesign forms, verify link ownership with rel="me" (#8703)
* Verify link ownership with rel="me" * Add explanation about verification to UI * Perform link verifications * Add click-to-copy widget for verification HTML * Redesign edit profile page * Redesign forms * Improve responsive design of settings pages * Restore landing page sign-up form * Fix typo * Support <link> tags, add spec * Fix links not being verified on first discovery and passive updatesth-downstream
parent
060a4af2d1
commit
d3105031f8
File diff suppressed because one or more lines are too long
@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VerifyLinkService < BaseService
|
||||
def call(field)
|
||||
@link_back = ActivityPub::TagManager.instance.url_for(field.account)
|
||||
@url = field.value
|
||||
|
||||
perform_request!
|
||||
|
||||
return unless link_back_present?
|
||||
|
||||
field.mark_verified!
|
||||
field.account.save!
|
||||
rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e
|
||||
Rails.logger.debug "Error fetching link #{@url}: #{e}"
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def perform_request!
|
||||
@body = Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
|
||||
res.code != 200 ? nil : res.body_with_limit
|
||||
end
|
||||
end
|
||||
|
||||
def link_back_present?
|
||||
return false if @body.empty?
|
||||
|
||||
Nokogiri::HTML(@body).xpath('//a[@rel="me"]|//link[@rel="me"]').any? { |link| link['href'] == @link_back }
|
||||
end
|
||||
end
|
@ -1,24 +1,32 @@
|
||||
- content_for :page_title do
|
||||
= t('auth.security')
|
||||
|
||||
%h4= t('auth.change_password')
|
||||
= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'auth_edit' }) do |f|
|
||||
= render 'shared/error_messages', object: resource
|
||||
|
||||
- if !use_seamless_external_login? || resource.encrypted_password.present?
|
||||
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }
|
||||
= f.input :password, placeholder: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }
|
||||
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
|
||||
= f.input :current_password, placeholder: t('simple_form.labels.defaults.current_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }
|
||||
.fields-group
|
||||
= f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, hint: false
|
||||
|
||||
.fields-group
|
||||
= f.input :password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'off' }, hint: false
|
||||
|
||||
.fields-group
|
||||
= f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'off' }
|
||||
|
||||
.fields-group
|
||||
= f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
- else
|
||||
%p.hint= t('users.seamless_external_login')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
= render 'sessions'
|
||||
|
||||
- if open_deletion?
|
||||
|
||||
%hr.spacer/
|
||||
%h4= t('auth.delete_account')
|
||||
%p.muted-hint= t('auth.delete_account_html', path: settings_delete_path)
|
||||
|
@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VerifyAccountLinksWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull', retry: false, unique: :until_executed
|
||||
|
||||
def perform(account_id)
|
||||
account = Account.find(account_id)
|
||||
|
||||
account.fields.each do |field|
|
||||
next unless !field.verified? && field.verifiable?
|
||||
VerifyLinkService.new.call(field)
|
||||
end
|
||||
|
||||
account.save! if account.changed?
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
@ -0,0 +1,51 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe VerifyLinkService, type: :service do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:account) { Fabricate(:account, username: 'alice') }
|
||||
let(:field) { Account::Field.new(account, 'name' => 'Website', 'value' => 'http://example.com') }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'http://example.com').to_return(status: 200, body: html)
|
||||
subject.call(field)
|
||||
end
|
||||
|
||||
context 'when a link contains an <a> back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<body>
|
||||
<a href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me">Follow me on Mastodon</a>
|
||||
</body>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a link contains a <link> back' do
|
||||
let(:html) do
|
||||
<<-HTML
|
||||
<!doctype html>
|
||||
<head>
|
||||
<link type="text/html" href="#{ActivityPub::TagManager.instance.url_for(account)}" rel="me" />
|
||||
</head>
|
||||
HTML
|
||||
end
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a link does not contain a link back' do
|
||||
let(:html) { '' }
|
||||
|
||||
it 'marks the field as verified' do
|
||||
expect(field.verified?).to be false
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in new issue