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 updatesmaster
parent
f8b54d229f
commit
f4d549d300
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