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