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 updateslocal
parent
f8b54d229f
commit
f4d549d300
46 changed files with 756 additions and 305 deletions
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