mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-22 14:05:42 +01:00
Remove Keybase integration (#17045)
This commit is contained in:
parent
12b3ff6c6d
commit
7de0ee7aba
43 changed files with 25 additions and 1215 deletions
|
@ -1,23 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::ProofsController < Api::BaseController
|
||||
include AccountOwnedConcern
|
||||
|
||||
skip_before_action :require_authenticated_user!
|
||||
|
||||
before_action :set_provider
|
||||
|
||||
def index
|
||||
render json: @account, serializer: @provider.serializer_class
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_provider
|
||||
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
def username_param
|
||||
params[:username]
|
||||
end
|
||||
end
|
|
@ -5,8 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
|||
before_action :set_account
|
||||
|
||||
def index
|
||||
@proofs = @account.suspended? ? [] : @account.identity_proofs.active
|
||||
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
||||
render json: []
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::IdentityProofsController < Settings::BaseController
|
||||
before_action :check_required_params, only: :new
|
||||
|
||||
def index
|
||||
@proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc)
|
||||
@proofs.each(&:refresh!)
|
||||
end
|
||||
|
||||
def new
|
||||
@proof = current_account.identity_proofs.new(
|
||||
token: params[:token],
|
||||
provider: params[:provider],
|
||||
provider_username: params[:provider_username]
|
||||
)
|
||||
|
||||
if current_account.username.casecmp(params[:username]).zero?
|
||||
render layout: 'auth'
|
||||
else
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params)
|
||||
@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
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@proof = current_account.identity_proofs.find(params[:id])
|
||||
@proof.destroy!
|
||||
redirect_to settings_identity_proofs_path, success: I18n.t('identity_proofs.removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_required_params
|
||||
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
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WellKnown
|
||||
class KeybaseProofConfigController < ActionController::Base
|
||||
def show
|
||||
render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer, root: 'keybase_config'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
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,
|
||||
skipNotFound: true,
|
||||
});
|
|
@ -123,7 +123,7 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { account, intl, domain, identity_proofs } = this.props;
|
||||
const { account, intl, domain } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
|
@ -297,20 +297,8 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
<div className='account__header__extra'>
|
||||
<div className='account__header__bio'>
|
||||
{(fields.size > 0 || identity_proofs.size > 0) && (
|
||||
{fields.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 noreferrer'><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 noreferrer'><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')} className='translate' />
|
||||
|
|
|
@ -11,7 +11,6 @@ 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,
|
||||
|
@ -92,7 +91,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { account, hideTabs, identity_proofs } = this.props;
|
||||
const { account, hideTabs } = this.props;
|
||||
|
||||
if (account === null) {
|
||||
return null;
|
||||
|
@ -104,7 +103,6 @@ export default class Header extends ImmutablePureComponent {
|
|||
|
||||
<InnerHeader
|
||||
account={account}
|
||||
identity_proofs={identity_proofs}
|
||||
onFollow={this.handleFollow}
|
||||
onBlock={this.handleBlock}
|
||||
onMention={this.handleMention}
|
||||
|
|
|
@ -21,7 +21,6 @@ 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' },
|
||||
|
@ -34,7 +33,6 @@ 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;
|
||||
|
|
|
@ -12,7 +12,6 @@ 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';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
import TimelineHint from 'mastodon/components/timeline_hint';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
|
@ -80,7 +79,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
const { accountId, withReplies, dispatch } = this.props;
|
||||
|
||||
dispatch(fetchAccount(accountId));
|
||||
dispatch(fetchAccountIdentityProofs(accountId));
|
||||
|
||||
if (!withReplies) {
|
||||
dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
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;
|
||||
}
|
||||
};
|
|
@ -32,7 +32,6 @@ import filters from './filters';
|
|||
import conversations from './conversations';
|
||||
import suggestions from './suggestions';
|
||||
import polls from './polls';
|
||||
import identity_proofs from './identity_proofs';
|
||||
import trends from './trends';
|
||||
import missed_updates from './missed_updates';
|
||||
import announcements from './announcements';
|
||||
|
@ -69,7 +68,6 @@ const reducers = {
|
|||
notifications,
|
||||
height_cache,
|
||||
custom_emojis,
|
||||
identity_proofs,
|
||||
lists,
|
||||
listEditor,
|
||||
listAdder,
|
||||
|
|
|
@ -999,68 +999,6 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
.connection-prompt {
|
||||
margin-bottom: 25px;
|
||||
|
||||
.fa-link {
|
||||
background-color: darken($ui-base-color, 4%);
|
||||
border-radius: 100%;
|
||||
font-size: 24px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
&__column {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
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 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__connection {
|
||||
background-color: lighten($ui-base-color, 8%);
|
||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 25px 10px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&::after {
|
||||
background-color: darken($ui-base-color, 4%);
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&__row {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.input.user_confirm_password,
|
||||
.input.user_website {
|
||||
&:not(.field_with_errors) {
|
||||
|
|
|
@ -18,7 +18,6 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
|||
atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' },
|
||||
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
|
||||
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
|
||||
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
|
||||
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
||||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ProofProvider
|
||||
SUPPORTED_PROVIDERS = %w(keybase).freeze
|
||||
|
||||
def self.find(identifier, proof = nil)
|
||||
case identifier
|
||||
when 'keybase'
|
||||
ProofProvider::Keybase.new(proof)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,69 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProofProvider::Keybase
|
||||
BASE_URL = ENV.fetch('KEYBASE_BASE_URL', 'https://keybase.io')
|
||||
DOMAIN = ENV.fetch('KEYBASE_DOMAIN', Rails.configuration.x.web_domain)
|
||||
|
||||
class Error < StandardError; end
|
||||
|
||||
class ExpectedProofLiveError < Error; end
|
||||
|
||||
class UnexpectedResponseError < Error; end
|
||||
|
||||
def initialize(proof = nil)
|
||||
@proof = proof
|
||||
end
|
||||
|
||||
def serializer_class
|
||||
ProofProvider::Keybase::Serializer
|
||||
end
|
||||
|
||||
def worker_class
|
||||
ProofProvider::Keybase::Worker
|
||||
end
|
||||
|
||||
def validate!
|
||||
unless @proof.token&.size == 66
|
||||
@proof.errors.add(:base, I18n.t('identity_proofs.errors.keybase.invalid_token'))
|
||||
return
|
||||
end
|
||||
|
||||
# Do not perform synchronous validation for remote accounts
|
||||
return if @proof.provider_username.blank? || !@proof.account.local?
|
||||
|
||||
if verifier.valid?
|
||||
@proof.verified = true
|
||||
@proof.live = false
|
||||
else
|
||||
@proof.errors.add(:base, I18n.t('identity_proofs.errors.keybase.verification_failed', kb_username: @proof.provider_username))
|
||||
end
|
||||
end
|
||||
|
||||
def refresh!
|
||||
worker_class.new.perform(@proof)
|
||||
rescue ProofProvider::Keybase::Error
|
||||
nil
|
||||
end
|
||||
|
||||
def on_success_path(user_agent = nil)
|
||||
verifier.on_success_path(user_agent)
|
||||
end
|
||||
|
||||
def badge
|
||||
@badge ||= ProofProvider::Keybase::Badge.new(@proof.account.username, @proof.provider_username, @proof.token, domain)
|
||||
end
|
||||
|
||||
def verifier
|
||||
@verifier ||= ProofProvider::Keybase::Verifier.new(@proof.account.username, @proof.provider_username, @proof.token, domain)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def domain
|
||||
if @proof.account.local?
|
||||
DOMAIN
|
||||
else
|
||||
@proof.account.domain
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,45 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProofProvider::Keybase::Badge
|
||||
include RoutingHelper
|
||||
|
||||
def initialize(local_username, provider_username, token, domain)
|
||||
@local_username = local_username
|
||||
@provider_username = provider_username
|
||||
@token = token
|
||||
@domain = domain
|
||||
end
|
||||
|
||||
def proof_url
|
||||
"#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}/sigchain\##{@token}"
|
||||
end
|
||||
|
||||
def profile_url
|
||||
"#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}"
|
||||
end
|
||||
|
||||
def icon_url
|
||||
"#{ProofProvider::Keybase::BASE_URL}/#{@provider_username}/proof_badge/#{@token}?username=#{@local_username}&domain=#{@domain}"
|
||||
end
|
||||
|
||||
def avatar_url
|
||||
Rails.cache.fetch("proof_providers/keybase/#{@provider_username}/avatar_url", expires_in: 5.minutes) { remote_avatar_url } || default_avatar_url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remote_avatar_url
|
||||
request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/user/pic_url.json", params: { username: @provider_username })
|
||||
|
||||
request.perform do |res|
|
||||
json = Oj.load(res.body_with_limit, mode: :strict)
|
||||
json['pic_url'] if json.is_a?(Hash)
|
||||
end
|
||||
rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError
|
||||
nil
|
||||
end
|
||||
|
||||
def default_avatar_url
|
||||
asset_pack_path('media/images/proof_providers/keybase.png')
|
||||
end
|
||||
end
|
|
@ -1,76 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProofProvider::Keybase::ConfigSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
include ActionView::Helpers::TextHelper
|
||||
|
||||
attributes :version, :domain, :display_name, :username,
|
||||
:brand_color, :logo, :description, :prefill_url,
|
||||
:profile_url, :check_url, :check_path, :avatar_path,
|
||||
:contact
|
||||
|
||||
def version
|
||||
1
|
||||
end
|
||||
|
||||
def domain
|
||||
ProofProvider::Keybase::DOMAIN
|
||||
end
|
||||
|
||||
def display_name
|
||||
Setting.site_title
|
||||
end
|
||||
|
||||
def logo
|
||||
{
|
||||
svg_black: full_asset_url(asset_pack_path('media/images/logo_transparent_black.svg')),
|
||||
svg_white: full_asset_url(asset_pack_path('media/images/logo_transparent_white.svg')),
|
||||
svg_full: full_asset_url(asset_pack_path('media/images/logo.svg')),
|
||||
svg_full_darkmode: full_asset_url(asset_pack_path('media/images/logo.svg')),
|
||||
}
|
||||
end
|
||||
|
||||
def brand_color
|
||||
'#282c37'
|
||||
end
|
||||
|
||||
def description
|
||||
strip_tags(Setting.site_short_description.presence || I18n.t('about.about_mastodon_html'))
|
||||
end
|
||||
|
||||
def username
|
||||
{ min: 1, max: 30, re: '[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?' }
|
||||
end
|
||||
|
||||
def prefill_url
|
||||
params = {
|
||||
provider: 'keybase',
|
||||
token: '%{sig_hash}',
|
||||
provider_username: '%{kb_username}',
|
||||
username: '%{username}',
|
||||
user_agent: '%{kb_ua}',
|
||||
}
|
||||
|
||||
CGI.unescape(new_settings_identity_proof_url(params))
|
||||
end
|
||||
|
||||
def profile_url
|
||||
CGI.unescape(short_account_url('%{username}'))
|
||||
end
|
||||
|
||||
def check_url
|
||||
CGI.unescape(api_proofs_url(username: '%{username}', provider: 'keybase'))
|
||||
end
|
||||
|
||||
def check_path
|
||||
['signatures']
|
||||
end
|
||||
|
||||
def avatar_path
|
||||
['avatar']
|
||||
end
|
||||
|
||||
def contact
|
||||
[Setting.site_contact_email.presence || 'unknown'].compact
|
||||
end
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProofProvider::Keybase::Serializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attribute :avatar
|
||||
|
||||
has_many :identity_proofs, key: :signatures
|
||||
|
||||
def avatar
|
||||
full_asset_url(object.avatar_original_url)
|
||||
end
|
||||
|
||||
class AccountIdentityProofSerializer < ActiveModel::Serializer
|
||||
attributes :sig_hash, :kb_username
|
||||
|
||||
def sig_hash
|
||||
object.token
|
||||
end
|
||||
|
||||
def kb_username
|
||||
object.provider_username
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,59 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProofProvider::Keybase::Verifier
|
||||
def initialize(local_username, provider_username, token, domain)
|
||||
@local_username = local_username
|
||||
@provider_username = provider_username
|
||||
@token = token
|
||||
@domain = domain
|
||||
end
|
||||
|
||||
def valid?
|
||||
request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/sig/proof_valid.json", params: query_params)
|
||||
|
||||
request.perform do |res|
|
||||
json = Oj.load(res.body_with_limit, mode: :strict)
|
||||
|
||||
if json.is_a?(Hash)
|
||||
json.fetch('proof_valid', false)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError
|
||||
false
|
||||
end
|
||||
|
||||
def on_success_path(user_agent = nil)
|
||||
url = Addressable::URI.parse("#{ProofProvider::Keybase::BASE_URL}/_/proof_creation_success")
|
||||
url.query_values = query_params.merge(kb_ua: user_agent || 'unknown')
|
||||
url.to_s
|
||||
end
|
||||
|
||||
def status
|
||||
request = Request.new(:get, "#{ProofProvider::Keybase::BASE_URL}/_/api/1.0/sig/proof_live.json", params: query_params)
|
||||
|
||||
request.perform do |res|
|
||||
raise ProofProvider::Keybase::UnexpectedResponseError unless res.code == 200
|
||||
|
||||
json = Oj.load(res.body_with_limit, mode: :strict)
|
||||
|
||||
raise ProofProvider::Keybase::UnexpectedResponseError unless json.is_a?(Hash) && json.key?('proof_valid') && json.key?('proof_live')
|
||||
|
||||
json
|
||||
end
|
||||
rescue Oj::ParseError, HTTP::Error, OpenSSL::SSL::SSLError
|
||||
raise ProofProvider::Keybase::UnexpectedResponseError
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def query_params
|
||||
{
|
||||
domain: @domain,
|
||||
kb_username: @provider_username,
|
||||
username: @local_username,
|
||||
sig_hash: @token,
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,32 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProofProvider::Keybase::Worker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull', retry: 20, unique: :until_executed
|
||||
|
||||
sidekiq_retry_in do |count, exception|
|
||||
# Retry aggressively when the proof is valid but not live in Keybase.
|
||||
# This is likely because Keybase just hasn't noticed the proof being
|
||||
# served from here yet.
|
||||
|
||||
if exception.class == ProofProvider::Keybase::ExpectedProofLiveError
|
||||
case count
|
||||
when 0..2 then 0.seconds
|
||||
when 2..6 then 1.second
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def perform(proof_id)
|
||||
proof = proof_id.is_a?(AccountIdentityProof) ? proof_id : AccountIdentityProof.find(proof_id)
|
||||
status = proof.provider_instance.verifier.status
|
||||
|
||||
# If Keybase thinks the proof is valid, and it exists here in Mastodon,
|
||||
# then it should be live. Keybase just has to notice that it's here
|
||||
# and then update its state. That might take a couple seconds.
|
||||
raise ProofProvider::Keybase::ExpectedProofLiveError if status['proof_valid'] && !status['proof_live']
|
||||
|
||||
proof.update!(verified: status['proof_valid'], live: status['proof_live'])
|
||||
end
|
||||
end
|
|
@ -1,46 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_identity_proofs
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# provider :string default(""), not null
|
||||
# provider_username :string default(""), not null
|
||||
# token :text default(""), not null
|
||||
# verified :boolean default(FALSE), not null
|
||||
# live :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class AccountIdentityProof < ApplicationRecord
|
||||
belongs_to :account
|
||||
|
||||
validates :provider, inclusion: { in: ProofProvider::SUPPORTED_PROVIDERS }
|
||||
validates :provider_username, format: { with: /\A[a-z0-9_]+\z/i }, length: { minimum: 2, maximum: 30 }
|
||||
validates :provider_username, uniqueness: { scope: [:account_id, :provider] }
|
||||
validates :token, format: { with: /\A[a-f0-9]+\z/ }, length: { maximum: 66 }
|
||||
|
||||
validate :validate_with_provider, if: :token_changed?
|
||||
|
||||
scope :active, -> { where(verified: true, live: true) }
|
||||
|
||||
after_commit :queue_worker, if: :saved_change_to_token?
|
||||
|
||||
delegate :refresh!, :on_success_path, :badge, to: :provider_instance
|
||||
|
||||
def provider_instance
|
||||
@provider_instance ||= ProofProvider.find(provider, self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def queue_worker
|
||||
provider_instance.worker_class.perform_async(id)
|
||||
end
|
||||
|
||||
def validate_with_provider
|
||||
provider_instance.validate!
|
||||
end
|
||||
end
|
|
@ -7,8 +7,7 @@ module AccountAssociations
|
|||
# Local users
|
||||
has_one :user, inverse_of: :account, dependent: :destroy
|
||||
|
||||
# Identity proofs
|
||||
has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account
|
||||
# E2EE
|
||||
has_many :devices, dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Timelines
|
||||
|
|
|
@ -13,7 +13,7 @@ module AccountMerging
|
|||
|
||||
owned_classes = [
|
||||
Status, StatusPin, MediaAttachment, Poll, Report, Tombstone, Favourite,
|
||||
Follow, FollowRequest, Block, Mute, AccountIdentityProof,
|
||||
Follow, FollowRequest, Block, Mute,
|
||||
AccountModerationNote, AccountPin, AccountStat, ListAccount,
|
||||
PollVote, Mention, AccountDeletionRequest, AccountNote, FollowRecommendationSuppression
|
||||
]
|
||||
|
|
|
@ -6,8 +6,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
|||
context :security
|
||||
|
||||
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
||||
:moved_to, :property_value, :identity_proof,
|
||||
:discoverable, :olm, :suspended
|
||||
:moved_to, :property_value, :discoverable, :olm, :suspended
|
||||
|
||||
attributes :id, :type, :following, :followers,
|
||||
:inbox, :outbox, :featured, :featured_tags,
|
||||
|
@ -143,7 +142,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
|||
end
|
||||
|
||||
def virtual_attachments
|
||||
object.suspended? ? [] : (object.fields + object.identity_proofs.active)
|
||||
object.suspended? ? [] : object.fields
|
||||
end
|
||||
|
||||
def moved_to
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
# 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
|
|
@ -27,7 +27,6 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
create_account if @account.nil?
|
||||
update_account
|
||||
process_tags
|
||||
process_attachments
|
||||
|
||||
process_duplicate_accounts! if @options[:verified_webfinger]
|
||||
else
|
||||
|
@ -301,23 +300,6 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
return if @json['attachment'].blank?
|
||||
|
||||
previous_proofs = @account.identity_proofs.to_a
|
||||
current_proofs = []
|
||||
|
||||
as_array(@json['attachment']).each do |attachment|
|
||||
next unless equals_or_includes?(attachment['type'], 'IdentityProof')
|
||||
current_proofs << process_identity_proof(attachment)
|
||||
end
|
||||
|
||||
previous_proofs.each do |previous_proof|
|
||||
next if current_proofs.any? { |current_proof| current_proof.id == previous_proof.id }
|
||||
previous_proof.delete
|
||||
end
|
||||
end
|
||||
|
||||
def process_emoji(tag)
|
||||
return if skip_download?
|
||||
return if tag['name'].blank? || tag['icon'].blank? || tag['icon']['url'].blank?
|
||||
|
@ -334,12 +316,4 @@ class ActivityPub::ProcessAccountService < BaseService
|
|||
emoji.image_remote_url = image_url
|
||||
emoji.save
|
||||
end
|
||||
|
||||
def process_identity_proof(attachment)
|
||||
provider = attachment['signatureAlgorithm']
|
||||
provider_username = attachment['name']
|
||||
token = attachment['signatureValue']
|
||||
|
||||
@account.identity_proofs.where(provider: provider, provider_username: provider_username).find_or_create_by(provider: provider, provider_username: provider_username, token: token)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,6 @@ class DeleteAccountService < BaseService
|
|||
domain_blocks
|
||||
featured_tags
|
||||
follow_requests
|
||||
identity_proofs
|
||||
list_accounts
|
||||
migrations
|
||||
mute_relationships
|
||||
|
@ -45,7 +44,6 @@ class DeleteAccountService < BaseService
|
|||
domain_blocks
|
||||
featured_tags
|
||||
follow_requests
|
||||
identity_proofs
|
||||
list_accounts
|
||||
migrations
|
||||
mute_relationships
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
- proofs = account.identity_proofs.active
|
||||
- fields = account.fields
|
||||
|
||||
.public-account-bio
|
||||
- unless fields.empty? && proofs.empty?
|
||||
- unless fields.empty?
|
||||
.account__header__fields
|
||||
- proofs.each do |proof|
|
||||
%dl
|
||||
%dt= proof.provider.capitalize
|
||||
%dd.verified
|
||||
= link_to fa_icon('check'), proof.badge.proof_url, class: 'verified__mark', title: t('accounts.link_verified_on', date: l(proof.updated_at))
|
||||
= link_to proof.provider_username, proof.badge.profile_url
|
||||
|
||||
- fields.each do |field|
|
||||
%dl
|
||||
%dt.emojify{ title: field.name }= Formatter.instance.format_field(account, field.name, custom_emojify: true)
|
||||
|
|
|
@ -8,20 +8,12 @@
|
|||
= render 'application/card', account: @account
|
||||
|
||||
- account = @account
|
||||
- proofs = account.identity_proofs.active
|
||||
- fields = account.fields
|
||||
- unless fields.empty? && proofs.empty? && account.note.blank?
|
||||
- unless fields.empty? && account.note.blank?
|
||||
.admin-account-bio
|
||||
- unless fields.empty? && proofs.empty?
|
||||
- unless fields.empty?
|
||||
%div
|
||||
.account__header__fields
|
||||
- proofs.each do |proof|
|
||||
%dl
|
||||
%dt= proof.provider.capitalize
|
||||
%dd.verified
|
||||
= link_to fa_icon('check'), proof.badge.proof_url, class: 'verified__mark', title: t('accounts.link_verified_on', date: l(proof.updated_at))
|
||||
= link_to proof.provider_username, proof.badge.profile_url
|
||||
|
||||
- fields.each do |field|
|
||||
%dl
|
||||
%dt.emojify{ title: field.name }= Formatter.instance.format_field(account, field.name, custom_emojify: true)
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
%tr
|
||||
%td
|
||||
= link_to proof.badge.profile_url, class: 'name-tag' do
|
||||
= image_tag proof.badge.avatar_url, width: 15, height: 15, alt: '', class: 'avatar'
|
||||
%span.username
|
||||
= proof.provider_username
|
||||
%span= "(#{proof.provider.capitalize})"
|
||||
|
||||
%td
|
||||
- if proof.live?
|
||||
%span.positive-hint
|
||||
= fa_icon 'check-circle fw'
|
||||
= t('identity_proofs.active')
|
||||
- else
|
||||
%span.negative-hint
|
||||
= fa_icon 'times-circle fw'
|
||||
= t('identity_proofs.inactive')
|
||||
|
||||
%td
|
||||
= table_link_to 'external-link', t('identity_proofs.view_proof'), proof.badge.proof_url if proof.badge.proof_url
|
||||
= table_link_to 'trash', t('identity_proofs.remove'), settings_identity_proof_path(proof), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
|
@ -1,17 +0,0 @@
|
|||
- content_for :page_title do
|
||||
= t('settings.identity_proofs')
|
||||
|
||||
%p= t('identity_proofs.explanation_html')
|
||||
|
||||
- unless @proofs.empty?
|
||||
%hr.spacer/
|
||||
|
||||
.table-wrapper
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
%th= t('identity_proofs.identity')
|
||||
%th= t('identity_proofs.status')
|
||||
%th
|
||||
%tbody
|
||||
= render partial: 'settings/identity_proofs/proof', collection: @proofs, as: :proof
|
|
@ -1,36 +0,0 @@
|
|||
- content_for :page_title do
|
||||
= t('identity_proofs.authorize_connection_prompt')
|
||||
|
||||
.form-container
|
||||
.oauth-prompt
|
||||
%h2= t('identity_proofs.authorize_connection_prompt')
|
||||
|
||||
= simple_form_for @proof, url: settings_identity_proofs_url, html: { method: :post } do |f|
|
||||
= f.input :provider, as: :hidden
|
||||
= f.input :provider_username, as: :hidden
|
||||
= f.input :token, as: :hidden
|
||||
|
||||
= hidden_field_tag :user_agent, params[:user_agent]
|
||||
|
||||
.connection-prompt
|
||||
.connection-prompt__row.connection-prompt__connection
|
||||
.connection-prompt__column
|
||||
= image_tag current_account.avatar.url(:original), size: 96, class: 'account__avatar'
|
||||
|
||||
%p= t('identity_proofs.i_am_html', username: content_tag(:strong,current_account.username), service: site_hostname)
|
||||
|
||||
.connection-prompt__column.connection-prompt__column-sep
|
||||
= fa_icon 'link'
|
||||
|
||||
.connection-prompt__column
|
||||
= image_tag @proof.badge.avatar_url, size: 96, class: 'account__avatar'
|
||||
|
||||
%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'
|
|
@ -985,26 +985,6 @@ en:
|
|||
other: Something isn't quite right yet! Please review %{count} errors below
|
||||
html_validator:
|
||||
invalid_markup: 'contains invalid HTML markup: %{error}'
|
||||
identity_proofs:
|
||||
active: Active
|
||||
authorize: Yes, authorize
|
||||
authorize_connection_prompt: Authorize this cryptographic connection?
|
||||
errors:
|
||||
failed: The cryptographic connection failed. Please try again from %{provider}.
|
||||
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 from other platforms, such as Keybase. This lets other people send you encrypted messages on those platforms and allows them to trust that the content you send them comes from you.
|
||||
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}'
|
||||
remove: Remove proof from account
|
||||
removed: Successfully removed proof from account
|
||||
status: Verification status
|
||||
view_proof: View proof
|
||||
imports:
|
||||
errors:
|
||||
over_rows_processing_limit: contains more than %{count} rows
|
||||
|
@ -1279,7 +1259,6 @@ en:
|
|||
edit_profile: Edit profile
|
||||
export: Data export
|
||||
featured_tags: Featured hashtags
|
||||
identity_proofs: Identity proofs
|
||||
import: Import
|
||||
import_and_export: Import and export
|
||||
migrate: Account migration
|
||||
|
|
|
@ -7,7 +7,6 @@ SimpleNavigation::Configuration.run do |navigation|
|
|||
n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_url, if: -> { current_user.functional? } do |s|
|
||||
s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_url
|
||||
s.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url
|
||||
s.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*}, if: proc { current_account.identity_proofs.exists? }
|
||||
end
|
||||
|
||||
n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_url, if: -> { current_user.functional? } do |s|
|
||||
|
|
|
@ -25,7 +25,6 @@ Rails.application.routes.draw do
|
|||
get '.well-known/nodeinfo', to: 'well_known/nodeinfo#index', as: :nodeinfo, defaults: { format: 'json' }
|
||||
get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger
|
||||
get '.well-known/change-password', to: redirect('/auth/edit')
|
||||
get '.well-known/keybase-proof-config', to: 'well_known/keybase_proof_config#show'
|
||||
|
||||
get '/nodeinfo/2.0', to: 'well_known/nodeinfo#show', as: :nodeinfo_schema
|
||||
|
||||
|
@ -146,8 +145,6 @@ Rails.application.routes.draw do
|
|||
resource :confirmation, only: [:new, :create]
|
||||
end
|
||||
|
||||
resources :identity_proofs, only: [:index, :new, :create, :destroy]
|
||||
|
||||
resources :applications, except: [:edit] do
|
||||
member do
|
||||
post :regenerate
|
||||
|
@ -332,9 +329,6 @@ Rails.application.routes.draw do
|
|||
# OEmbed
|
||||
get '/oembed', to: 'oembed#show', as: :oembed
|
||||
|
||||
# Identity proofs
|
||||
get :proofs, to: 'proofs#index'
|
||||
|
||||
# JSON / REST API
|
||||
namespace :v1 do
|
||||
resources :statuses, only: [:create, :show, :destroy] do
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropAccountIdentityProofs < ActiveRecord::Migration[5.2]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
drop_table :account_identity_proofs
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
15
db/schema.rb
15
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2021_11_23_212714) do
|
||||
ActiveRecord::Schema.define(version: 2021_11_26_000907) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -51,18 +51,6 @@ ActiveRecord::Schema.define(version: 2021_11_23_212714) do
|
|||
t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true
|
||||
end
|
||||
|
||||
create_table "account_identity_proofs", force: :cascade do |t|
|
||||
t.bigint "account_id"
|
||||
t.string "provider", default: "", null: false
|
||||
t.string "provider_username", default: "", null: false
|
||||
t.text "token", default: "", null: false
|
||||
t.boolean "verified", default: false, null: false
|
||||
t.boolean "live", default: false, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id", "provider", "provider_username"], name: "index_account_proofs_on_account_and_provider_and_username", unique: true
|
||||
end
|
||||
|
||||
create_table "account_migrations", force: :cascade do |t|
|
||||
t.bigint "account_id"
|
||||
t.string "acct", default: "", null: false
|
||||
|
@ -1010,7 +998,6 @@ ActiveRecord::Schema.define(version: 2021_11_23_212714) do
|
|||
add_foreign_key "account_conversations", "conversations", on_delete: :cascade
|
||||
add_foreign_key "account_deletion_requests", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
|
||||
add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify
|
||||
add_foreign_key "account_migrations", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_moderation_notes", "accounts"
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe Api::ProofsController do
|
||||
let(:alice) { Fabricate(:account, username: 'alice') }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_valid.json?domain=cb6e6126.ngrok.io&kb_username=crypto_alice&sig_hash=111111111111111111111111111111111111111111111111111111111111111111&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":false}')
|
||||
stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_live.json?domain=cb6e6126.ngrok.io&kb_username=crypto_alice&sig_hash=111111111111111111111111111111111111111111111111111111111111111111&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":true}')
|
||||
stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_valid.json?domain=cb6e6126.ngrok.io&kb_username=hidden_alice&sig_hash=222222222222222222222222222222222222222222222222222222222222222222&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":true}')
|
||||
stub_request(:get, 'https://keybase.io/_/api/1.0/sig/proof_live.json?domain=cb6e6126.ngrok.io&kb_username=hidden_alice&sig_hash=222222222222222222222222222222222222222222222222222222222222222222&username=alice').to_return(status: 200, body: '{"proof_valid":true,"proof_live":true}')
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
describe 'with a non-existent username' do
|
||||
it '404s' do
|
||||
get :index, params: { username: 'nonexistent', provider: 'keybase' }
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a user that has no proofs' do
|
||||
it 'is an empty list of signatures' do
|
||||
get :index, params: { username: alice.username, provider: 'keybase' }
|
||||
|
||||
expect(body_as_json[:signatures]).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a user that has a live, valid proof' do
|
||||
let(:token1) { '111111111111111111111111111111111111111111111111111111111111111111' }
|
||||
let(:kb_name1) { 'crypto_alice' }
|
||||
|
||||
before do
|
||||
Fabricate(:account_identity_proof, account: alice, verified: true, live: true, token: token1, provider_username: kb_name1)
|
||||
end
|
||||
|
||||
it 'is a list with that proof in it' do
|
||||
get :index, params: { username: alice.username, provider: 'keybase' }
|
||||
|
||||
expect(body_as_json[:signatures]).to eq [
|
||||
{ kb_username: kb_name1, sig_hash: token1 },
|
||||
]
|
||||
end
|
||||
|
||||
describe 'add one that is neither live nor valid' do
|
||||
let(:token2) { '222222222222222222222222222222222222222222222222222222222222222222' }
|
||||
let(:kb_name2) { 'hidden_alice' }
|
||||
|
||||
before do
|
||||
Fabricate(:account_identity_proof, account: alice, verified: false, live: false, token: token2, provider_username: kb_name2)
|
||||
end
|
||||
|
||||
it 'is a list with both proofs' do
|
||||
get :index, params: { username: alice.username, provider: 'keybase' }
|
||||
|
||||
expect(body_as_json[:signatures]).to eq [
|
||||
{ kb_username: kb_name1, sig_hash: token1 },
|
||||
{ kb_username: kb_name2, sig_hash: token2 },
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a user that has an avatar' do
|
||||
let(:alice) { Fabricate(:account, username: 'alice', avatar: attachment_fixture('avatar.gif')) }
|
||||
|
||||
context 'and a proof' do
|
||||
let(:token1) { '111111111111111111111111111111111111111111111111111111111111111111' }
|
||||
let(:kb_name1) { 'crypto_alice' }
|
||||
|
||||
before do
|
||||
Fabricate(:account_identity_proof, account: alice, verified: true, live: true, token: token1, provider_username: kb_name1)
|
||||
get :index, params: { username: alice.username, provider: 'keybase' }
|
||||
end
|
||||
|
||||
it 'has two keys: signatures and avatar' do
|
||||
expect(body_as_json.keys).to match_array [:signatures, :avatar]
|
||||
end
|
||||
|
||||
it 'has the correct signatures' do
|
||||
expect(body_as_json[:signatures]).to eq [
|
||||
{ kb_username: kb_name1, sig_hash: token1 },
|
||||
]
|
||||
end
|
||||
|
||||
it 'has the correct avatar url' do
|
||||
expect(body_as_json[:avatar]).to match "https://cb6e6126.ngrok.io#{alice.avatar.url}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,186 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe Settings::IdentityProofsController do
|
||||
include RoutingHelper
|
||||
render_views
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:valid_token) { '1'*66 }
|
||||
let(:kbname) { 'kbuser' }
|
||||
let(:provider) { 'keybase' }
|
||||
let(:findable_id) { Faker::Number.number(digits: 5) }
|
||||
let(:unfindable_id) { Faker::Number.number(digits: 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: new_proof_params.merge(status_posting_params) }
|
||||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:status) { { 'proof_valid' => true, 'proof_live' => true } }
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
|
||||
describe 'new proof creation' do
|
||||
context 'GET #new' do
|
||||
before do
|
||||
allow_any_instance_of(ProofProvider::Keybase::Badge).to receive(:avatar_url) { full_pack_url('media/images/void.png') }
|
||||
end
|
||||
|
||||
context 'with all of the correct params' do
|
||||
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
|
||||
|
||||
context 'with params to prove the same username cased differently' do
|
||||
let(:capitalized_username) { new_proof_params.merge(username: user.account.username.upcase) }
|
||||
|
||||
it 'renders the new template' do
|
||||
get :new, params: capitalized_username
|
||||
expect(response).to render_template(:new)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'POST #create' do
|
||||
context 'when saving works' do
|
||||
before do
|
||||
allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
|
||||
allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true }
|
||||
allow_any_instance_of(AccountIdentityProof).to receive(:on_success_path) { root_url }
|
||||
end
|
||||
|
||||
it 'serializes a ProofProvider::Keybase::Worker' do
|
||||
expect(ProofProvider::Keybase::Worker).to receive(:perform_async)
|
||||
post :create, params: postable_params
|
||||
end
|
||||
|
||||
it 'delegates redirection to the proof provider' do
|
||||
expect_any_instance_of(AccountIdentityProof).to receive(:on_success_path)
|
||||
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
|
||||
before do
|
||||
allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { false }
|
||||
end
|
||||
|
||||
it 'redirects to :index' do
|
||||
post :create, params: postable_params
|
||||
expect(response).to redirect_to settings_identity_proofs_path
|
||||
end
|
||||
|
||||
it 'flashes a helpful message' do
|
||||
post :create, params: postable_params
|
||||
expect(flash[:alert]).to eq I18n.t('identity_proofs.errors.failed', provider: 'Keybase')
|
||||
end
|
||||
end
|
||||
|
||||
context 'it can also do an update if the provider and username match an existing proof' do
|
||||
before do
|
||||
allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true }
|
||||
allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
|
||||
Fabricate(:account_identity_proof, account: user.account, provider: provider, provider_username: kbname)
|
||||
allow_any_instance_of(AccountIdentityProof).to receive(:on_success_path) { root_url }
|
||||
end
|
||||
|
||||
it 'calls update with the new token' do
|
||||
expect_any_instance_of(AccountIdentityProof).to receive(:save) do |proof|
|
||||
expect(proof.token).to eq valid_token
|
||||
end
|
||||
|
||||
post :create, params: postable_params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
context 'with no existing proofs' do
|
||||
it 'shows the helpful explanation' do
|
||||
get :index
|
||||
expect(response.body).to match I18n.t('identity_proofs.explanation_html')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with two proofs' do
|
||||
before do
|
||||
allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true }
|
||||
@proof1 = Fabricate(:account_identity_proof, account: user.account)
|
||||
@proof2 = Fabricate(:account_identity_proof, account: user.account)
|
||||
allow_any_instance_of(AccountIdentityProof).to receive(:badge) { double(avatar_url: '', profile_url: '', proof_url: '') }
|
||||
allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) {}
|
||||
end
|
||||
|
||||
it 'has the first proof username on the page' do
|
||||
get :index
|
||||
expect(response.body).to match /#{Regexp.quote(@proof1.provider_username)}/
|
||||
end
|
||||
|
||||
it 'has the second proof username on the page' do
|
||||
get :index
|
||||
expect(response.body).to match /#{Regexp.quote(@proof2.provider_username)}/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
before do
|
||||
allow_any_instance_of(ProofProvider::Keybase::Verifier).to receive(:valid?) { true }
|
||||
@proof1 = Fabricate(:account_identity_proof, account: user.account)
|
||||
allow_any_instance_of(AccountIdentityProof).to receive(:badge) { double(avatar_url: '', profile_url: '', proof_url: '') }
|
||||
allow_any_instance_of(AccountIdentityProof).to receive(:refresh!) {}
|
||||
delete :destroy, params: { id: @proof1.id }
|
||||
end
|
||||
|
||||
it 'redirects to :index' do
|
||||
expect(response).to redirect_to settings_identity_proofs_path
|
||||
end
|
||||
|
||||
it 'removes the proof' do
|
||||
expect(AccountIdentityProof.where(id: @proof1.id).count).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,15 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe WellKnown::KeybaseProofConfigController, type: :controller do
|
||||
render_views
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'renders json' do
|
||||
get :show
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.media_type).to eq 'application/json'
|
||||
expect { JSON.parse(response.body) }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
Fabricator(:account_identity_proof) do
|
||||
account
|
||||
provider 'keybase'
|
||||
provider_username { sequence(:provider_username) { |i| "#{Faker::Lorem.characters(number: 15)}" } }
|
||||
token { sequence(:token) { |i| "#{i}#{Faker::Crypto.sha1()*2}"[0..65] } }
|
||||
verified false
|
||||
live false
|
||||
end
|
|
@ -1,82 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ProofProvider::Keybase::Verifier do
|
||||
let(:my_domain) { Rails.configuration.x.local_domain }
|
||||
|
||||
let(:keybase_proof) do
|
||||
local_proof = AccountIdentityProof.new(
|
||||
provider: 'Keybase',
|
||||
provider_username: 'cryptoalice',
|
||||
token: '11111111111111111111111111'
|
||||
)
|
||||
|
||||
described_class.new('alice', 'cryptoalice', '11111111111111111111111111', my_domain)
|
||||
end
|
||||
|
||||
let(:query_params) do
|
||||
"domain=#{my_domain}&kb_username=cryptoalice&sig_hash=11111111111111111111111111&username=alice"
|
||||
end
|
||||
|
||||
describe '#valid?' do
|
||||
let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_valid.json' }
|
||||
|
||||
context 'when valid' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":true}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'calls out to keybase and returns true' do
|
||||
expect(keybase_proof.valid?).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":0,"name":"OK"},"proof_valid":false}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'calls out to keybase and returns false' do
|
||||
expect(keybase_proof.valid?).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unexpected api response' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":100,"desc":"wrong size hex_id","fields":{"sig_hash":"wrong size hex_id"},"name":"INPUT_ERROR"}}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'swallows the error and returns false' do
|
||||
expect(keybase_proof.valid?).to eq false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#status' do
|
||||
let(:base_url) { 'https://keybase.io/_/api/1.0/sig/proof_live.json' }
|
||||
|
||||
context 'with a normal response' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":0,"name":"OK"},"proof_live":false,"proof_valid":true}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'calls out to keybase and returns the status fields as proof_valid and proof_live' do
|
||||
expect(keybase_proof.status).to include({ 'proof_valid' => true, 'proof_live' => false })
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unexpected keybase response' do
|
||||
before do
|
||||
json_response_body = '{"status":{"code":100,"desc":"missing non-optional field sig_hash","fields":{"sig_hash":"missing non-optional field sig_hash"},"name":"INPUT_ERROR"}}'
|
||||
stub_request(:get, "#{base_url}?#{query_params}").to_return(status: 200, body: json_response_body)
|
||||
end
|
||||
|
||||
it 'raises a ProofProvider::Keybase::UnexpectedResponseError' do
|
||||
expect { keybase_proof.status }.to raise_error ProofProvider::Keybase::UnexpectedResponseError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,51 +30,6 @@ RSpec.describe ActivityPub::ProcessAccountService, type: :service do
|
|||
end
|
||||
end
|
||||
|
||||
context 'identity proofs' do
|
||||
let(:payload) do
|
||||
{
|
||||
id: 'https://foo.test',
|
||||
type: 'Actor',
|
||||
inbox: 'https://foo.test/inbox',
|
||||
attachment: [
|
||||
{ type: 'IdentityProof', name: 'Alice', signatureAlgorithm: 'keybase', signatureValue: 'a' * 66 },
|
||||
],
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'parses out of attachment' do
|
||||
allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
|
||||
|
||||
account = subject.call('alice', 'example.com', payload)
|
||||
|
||||
expect(account.identity_proofs.count).to eq 1
|
||||
|
||||
proof = account.identity_proofs.first
|
||||
|
||||
expect(proof.provider).to eq 'keybase'
|
||||
expect(proof.provider_username).to eq 'Alice'
|
||||
expect(proof.token).to eq 'a' * 66
|
||||
end
|
||||
|
||||
it 'removes no longer present proofs' do
|
||||
allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
|
||||
|
||||
account = Fabricate(:account, username: 'alice', domain: 'example.com')
|
||||
old_proof = Fabricate(:account_identity_proof, account: account, provider: 'keybase', provider_username: 'Bob', token: 'b' * 66)
|
||||
|
||||
subject.call('alice', 'example.com', payload)
|
||||
|
||||
expect(account.identity_proofs.count).to eq 1
|
||||
expect(account.identity_proofs.find_by(id: old_proof.id)).to be_nil
|
||||
end
|
||||
|
||||
it 'queues a validity check on the proof' do
|
||||
allow(ProofProvider::Keybase::Worker).to receive(:perform_async)
|
||||
account = subject.call('alice', 'example.com', payload)
|
||||
expect(ProofProvider::Keybase::Worker).to have_received(:perform_async)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is not suspended' do
|
||||
let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com') }
|
||||
|
||||
|
|
Loading…
Reference in a new issue