Merge branch 'ethereum' into 'develop'

Mitra: support Ethereum login

See merge request soapbox-pub/soapbox-fe!1034
improve-ci
Alex Gleason 2022-02-11 01:52:31 +00:00
commit a9346a6db6
7 zmienionych plików z 122 dodań i 42 usunięć

Wyświetl plik

@ -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,11 +34,11 @@ const fetchExternalInstance = baseURL => {
}); });
}; };
export function createAppAndRedirect(host) { function createExternalApp(instance, baseURL) {
return (dispatch, getState) => { return (dispatch, getState) => {
const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`); // Mitra: skip creating the auth app
if (getQuirks(instance).noApps) return new Promise(f => f({}));
return fetchExternalInstance(baseURL).then(instance => {
const { scopes } = getFeatures(instance); const { scopes } = getFeatures(instance);
const params = { const params = {
@ -46,7 +48,15 @@ export function createAppAndRedirect(host) {
scopes, scopes,
}; };
return dispatch(createApp(params, baseURL)).then(app => { 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 { client_id, redirect_uri } = app;
const query = new URLSearchParams({ const query = new URLSearchParams({
@ -62,6 +72,48 @@ export function createAppAndRedirect(host) {
window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`; 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) => {
const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`);
return fetchExternalInstance(baseURL).then(instance => {
const features = getFeatures(instance);
const quirks = getQuirks(instance);
if (features.ethereumLogin && quirks.noOAuthForm) {
return dispatch(externalEthereumLogin(instance, baseURL));
} else {
return dispatch(externalAuthorize(instance, baseURL));
}
}); });
}; };
} }

Wyświetl plik

@ -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>

Wyświetl plik

@ -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 }));
} }

Wyświetl plik

@ -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

Wyświetl plik

@ -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 };
});
});
};

Wyświetl plik

@ -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,
}; };
}); });

Wyświetl plik

@ -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';