diff options
Diffstat (limited to 'app')
17 files changed, 158 insertions, 20 deletions
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 { <div className='account__header__extra'> <div className='account__header__bio'> - {fields.size > 0 && ( + { (fields.size > 0 || identity_proofs.size > 0) && ( <div className='account__header__fields'> + {identity_proofs.map((proof, i) => ( + <dl key={i}> + <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} /> + + <dd className='verified'> + <a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> + <Icon id='check' className='verified__mark' /> + </span></a> + <a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> + </dd> + </dl> + ))} {fields.map((pair, i) => ( <dl key={i}> <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> 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 <MissingIndicator />; @@ -96,6 +97,7 @@ export default class Header extends ImmutablePureComponent { <InnerHeader account={account} + identity_proofs={identity_proofs} onFollow={this.handleFollow} onBlock={this.handleBlock} onMention={this.handleMention} diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index a06a0b095..4d4ae6e82 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -21,6 +21,7 @@ import { openModal } from '../../../actions/modal'; import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { unfollowModal } from '../../../initial_state'; +import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, @@ -35,6 +36,7 @@ const makeMapStateToProps = () => { 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' |