kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'ethereum' into 'develop'
Mitra: support Ethereum login See merge request soapbox-pub/soapbox-fe!1034improve-ci
commit
a9346a6db6
|
@ -13,7 +13,9 @@ import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/
|
||||||
import { obtainOAuthToken } from 'soapbox/actions/oauth';
|
import { obtainOAuthToken } from 'soapbox/actions/oauth';
|
||||||
import { parseBaseURL } from 'soapbox/utils/auth';
|
import { parseBaseURL } from 'soapbox/utils/auth';
|
||||||
import sourceCode from 'soapbox/utils/code';
|
import sourceCode from 'soapbox/utils/code';
|
||||||
|
import { getWalletAndSign } from 'soapbox/utils/ethereum';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
import { getQuirks } from 'soapbox/utils/quirks';
|
||||||
|
|
||||||
import { baseClient } from '../api';
|
import { baseClient } from '../api';
|
||||||
|
|
||||||
|
@ -32,36 +34,86 @@ const fetchExternalInstance = baseURL => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createAppAndRedirect(host) {
|
function createExternalApp(instance, baseURL) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
// Mitra: skip creating the auth app
|
||||||
|
if (getQuirks(instance).noApps) return new Promise(f => f({}));
|
||||||
|
|
||||||
|
const { scopes } = getFeatures(instance);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
client_name: sourceCode.displayName,
|
||||||
|
redirect_uris: `${window.location.origin}/auth/external`,
|
||||||
|
website: sourceCode.homepage,
|
||||||
|
scopes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return dispatch(createApp(params, baseURL));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function externalAuthorize(instance, baseURL) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const { scopes } = getFeatures(instance);
|
||||||
|
|
||||||
|
return dispatch(createExternalApp(instance, baseURL)).then(app => {
|
||||||
|
const { client_id, redirect_uri } = app;
|
||||||
|
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
client_id,
|
||||||
|
redirect_uri,
|
||||||
|
response_type: 'code',
|
||||||
|
scope: scopes,
|
||||||
|
});
|
||||||
|
|
||||||
|
localStorage.setItem('soapbox:external:app', JSON.stringify(app));
|
||||||
|
localStorage.setItem('soapbox:external:baseurl', baseURL);
|
||||||
|
localStorage.setItem('soapbox:external:scopes', scopes);
|
||||||
|
|
||||||
|
window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function externalEthereumLogin(instance, baseURL) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const loginMessage = instance.get('login_message');
|
||||||
|
|
||||||
|
return getWalletAndSign(loginMessage).then(({ wallet, signature }) => {
|
||||||
|
return dispatch(createExternalApp(instance, baseURL)).then(app => {
|
||||||
|
const params = {
|
||||||
|
grant_type: 'ethereum',
|
||||||
|
wallet_address: wallet.toLowerCase(),
|
||||||
|
client_id: app.client_id,
|
||||||
|
client_secret: app.client_secret,
|
||||||
|
password: signature,
|
||||||
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
|
scope: getFeatures(instance).scopes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return dispatch(obtainOAuthToken(params, baseURL))
|
||||||
|
.then(token => dispatch(authLoggedIn(token)))
|
||||||
|
.then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL)))
|
||||||
|
.then(account => dispatch(switchAccount(account.id)))
|
||||||
|
.then(() => window.location.href = '/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function externalLogin(host) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`);
|
const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`);
|
||||||
|
|
||||||
return fetchExternalInstance(baseURL).then(instance => {
|
return fetchExternalInstance(baseURL).then(instance => {
|
||||||
const { scopes } = getFeatures(instance);
|
const features = getFeatures(instance);
|
||||||
|
const quirks = getQuirks(instance);
|
||||||
|
|
||||||
const params = {
|
if (features.ethereumLogin && quirks.noOAuthForm) {
|
||||||
client_name: sourceCode.displayName,
|
return dispatch(externalEthereumLogin(instance, baseURL));
|
||||||
redirect_uris: `${window.location.origin}/auth/external`,
|
} else {
|
||||||
website: sourceCode.homepage,
|
return dispatch(externalAuthorize(instance, baseURL));
|
||||||
scopes,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return dispatch(createApp(params, baseURL)).then(app => {
|
|
||||||
const { client_id, redirect_uri } = app;
|
|
||||||
|
|
||||||
const query = new URLSearchParams({
|
|
||||||
client_id,
|
|
||||||
redirect_uri,
|
|
||||||
response_type: 'code',
|
|
||||||
scope: scopes,
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem('soapbox:external:app', JSON.stringify(app));
|
|
||||||
localStorage.setItem('soapbox:external:baseurl', baseURL);
|
|
||||||
localStorage.setItem('soapbox:external:scopes', scopes);
|
|
||||||
|
|
||||||
window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,10 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const instance = state.get('instance');
|
const instance = state.get('instance');
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseURL: getBaseURL(state),
|
baseURL: getBaseURL(state),
|
||||||
hasResetPasswordAPI: features.resetPasswordAPI,
|
features: getFeatures(instance),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,7 +27,7 @@ export default @connect(mapStateToProps)
|
||||||
class LoginForm extends ImmutablePureComponent {
|
class LoginForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl, isLoading, handleSubmit, baseURL, hasResetPasswordAPI } = this.props;
|
const { intl, isLoading, handleSubmit, baseURL, features } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className='simple_form new_user' method='post' onSubmit={handleSubmit}>
|
<form className='simple_form new_user' method='post' onSubmit={handleSubmit}>
|
||||||
|
@ -57,12 +56,8 @@ class LoginForm extends ImmutablePureComponent {
|
||||||
autoCapitalize='off'
|
autoCapitalize='off'
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
{/* <div className='input password user_password'>
|
|
||||||
<input
|
|
||||||
/>
|
|
||||||
</div> */}
|
|
||||||
<p className='hint subtle-hint'>
|
<p className='hint subtle-hint'>
|
||||||
{hasResetPasswordAPI ? (
|
{features.resetPasswordAPI ? (
|
||||||
<Link to='/auth/reset_password'>
|
<Link to='/auth/reset_password'>
|
||||||
<FormattedMessage id='login.reset_password_hint' defaultMessage='Trouble logging in?' />
|
<FormattedMessage id='login.reset_password_hint' defaultMessage='Trouble logging in?' />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { createAppAndRedirect, loginWithCode } from 'soapbox/actions/external_auth';
|
import { externalLogin, loginWithCode } from 'soapbox/actions/external_auth';
|
||||||
import LoadingIndicator from 'soapbox/components/loading_indicator';
|
import LoadingIndicator from 'soapbox/components/loading_indicator';
|
||||||
import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms';
|
import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms';
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class ExternalLoginForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
|
|
||||||
dispatch(createAppAndRedirect(host))
|
dispatch(externalLogin(host))
|
||||||
.then(() => this.setState({ isLoading: false }))
|
.then(() => this.setState({ isLoading: false }))
|
||||||
.catch(() => this.setState({ isLoading: false }));
|
.catch(() => this.setState({ isLoading: false }));
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ const initialState = ImmutableMap({
|
||||||
|
|
||||||
// Build Mastodon configuration from Pleroma instance
|
// Build Mastodon configuration from Pleroma instance
|
||||||
const pleromaToMastodonConfig = instance => {
|
const pleromaToMastodonConfig = instance => {
|
||||||
return {
|
return ImmutableMap({
|
||||||
statuses: ImmutableMap({
|
statuses: ImmutableMap({
|
||||||
max_characters: instance.get('max_toot_chars'),
|
max_characters: instance.get('max_toot_chars'),
|
||||||
}),
|
}),
|
||||||
|
@ -60,7 +60,7 @@ const pleromaToMastodonConfig = instance => {
|
||||||
min_expiration: instance.getIn(['poll_limits', 'min_expiration']),
|
min_expiration: instance.getIn(['poll_limits', 'min_expiration']),
|
||||||
max_expiration: instance.getIn(['poll_limits', 'max_expiration']),
|
max_expiration: instance.getIn(['poll_limits', 'max_expiration']),
|
||||||
}),
|
}),
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use new value only if old value is undefined
|
// Use new value only if old value is undefined
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
export const ethereum = () => window.ethereum;
|
||||||
|
|
||||||
|
export const hasEthereum = () => Boolean(ethereum());
|
||||||
|
|
||||||
|
// Requests an Ethereum wallet from the browser
|
||||||
|
// Returns a Promise containing the Ethereum wallet address (string).
|
||||||
|
export const getWallet = () => {
|
||||||
|
return ethereum().request({ method: 'eth_requestAccounts' })
|
||||||
|
.then(wallets => wallets[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Asks the browser to sign a message with Ethereum.
|
||||||
|
// Returns a Promise containing the signature (string).
|
||||||
|
export const signMessage = (wallet, message) => {
|
||||||
|
return ethereum().request({ method: 'personal_sign', params: [message, wallet] });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combines the above functions.
|
||||||
|
// Returns an object with the `wallet` and `signature`
|
||||||
|
export const getWalletAndSign = message => {
|
||||||
|
return getWallet().then(wallet => {
|
||||||
|
return signMessage(wallet, message).then(signature => {
|
||||||
|
return { wallet, signature };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -9,6 +9,7 @@ const any = arr => arr.some(Boolean);
|
||||||
// For uglification
|
// For uglification
|
||||||
export const MASTODON = 'Mastodon';
|
export const MASTODON = 'Mastodon';
|
||||||
export const PLEROMA = 'Pleroma';
|
export const PLEROMA = 'Pleroma';
|
||||||
|
export const MITRA = 'Mitra';
|
||||||
|
|
||||||
export const getFeatures = createSelector([
|
export const getFeatures = createSelector([
|
||||||
instance => parseVersion(instance.get('version')),
|
instance => parseVersion(instance.get('version')),
|
||||||
|
@ -83,6 +84,7 @@ export const getFeatures = createSelector([
|
||||||
accountEndorsements: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
accountEndorsements: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||||
quotePosts: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
quotePosts: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||||
birthdays: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
birthdays: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||||
|
ethereumLogin: v.software === MITRA,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import { parseVersion } from './features';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { parseVersion, PLEROMA, MITRA } from './features';
|
||||||
|
|
||||||
// For solving bugs between API implementations
|
// For solving bugs between API implementations
|
||||||
export const getQuirks = instance => {
|
export const getQuirks = createSelector([
|
||||||
const v = parseVersion(instance.get('version'));
|
instance => parseVersion(instance.get('version')),
|
||||||
|
], (v) => {
|
||||||
return {
|
return {
|
||||||
invertedPagination: v.software === 'Pleroma',
|
invertedPagination: v.software === PLEROMA,
|
||||||
|
noApps: v.software === MITRA,
|
||||||
|
noOAuthForm: v.software === MITRA,
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
|
|
||||||
export const getNextLinkName = getState =>
|
export const getNextLinkName = getState =>
|
||||||
getQuirks(getState().get('instance')).invertedPagination ? 'prev' : 'next';
|
getQuirks(getState().get('instance')).invertedPagination ? 'prev' : 'next';
|
||||||
|
|
Ładowanie…
Reference in New Issue