diff --git a/app/soapbox/actions/apps.js b/app/soapbox/actions/apps.js
index b0824d384..7eb00c98c 100644
--- a/app/soapbox/actions/apps.js
+++ b/app/soapbox/actions/apps.js
@@ -16,10 +16,10 @@ export const APP_VERIFY_CREDENTIALS_REQUEST = 'APP_VERIFY_CREDENTIALS_REQUEST';
export const APP_VERIFY_CREDENTIALS_SUCCESS = 'APP_VERIFY_CREDENTIALS_SUCCESS';
export const APP_VERIFY_CREDENTIALS_FAIL = 'APP_VERIFY_CREDENTIALS_FAIL';
-export function createApp(params) {
+export function createApp(params, baseURL) {
return (dispatch, getState) => {
dispatch({ type: APP_CREATE_REQUEST, params });
- return baseClient().post('/api/v1/apps', params).then(({ data: app }) => {
+ return baseClient(null, baseURL).post('/api/v1/apps', params).then(({ data: app }) => {
dispatch({ type: APP_CREATE_SUCCESS, params, app });
return app;
}).catch(error => {
diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js
index 2918d41a2..7b357edfa 100644
--- a/app/soapbox/actions/auth.js
+++ b/app/soapbox/actions/auth.js
@@ -50,6 +50,7 @@ function createAuthApp() {
client_name: sourceCode.displayName,
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
scopes: 'read write follow push admin',
+ website: sourceCode.homepage,
};
return dispatch(createApp(params)).then(app => {
@@ -88,10 +89,8 @@ function createUserToken(username, password) {
password: password,
};
- return dispatch(obtainOAuthToken(params)).then(token => {
- dispatch(authLoggedIn(token));
- return token;
- });
+ return dispatch(obtainOAuthToken(params))
+ .then(token => dispatch(authLoggedIn(token)));
};
}
@@ -110,9 +109,8 @@ export function refreshUserToken() {
grant_type: 'refresh_token',
};
- return dispatch(obtainOAuthToken(params)).then(token => {
- dispatch(authLoggedIn(token));
- });
+ return dispatch(obtainOAuthToken(params))
+ .then(token => dispatch(authLoggedIn(token)));
};
}
@@ -126,10 +124,7 @@ export function otpVerify(code, mfa_token) {
code: code,
challenge_type: 'totp',
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
- }).then(({ data: token }) => {
- dispatch(authLoggedIn(token));
- return token;
- });
+ }).then(({ data: token }) => dispatch(authLoggedIn(token)));
};
}
@@ -209,12 +204,9 @@ export function register(params) {
return (dispatch, getState) => {
params.fullname = params.username;
- return dispatch(createAppAndToken()).then(() => {
- return dispatch(createAccount(params));
- }).then(({ token }) => {
- dispatch(authLoggedIn(token));
- return token;
- });
+ return dispatch(createAppAndToken())
+ .then(() => dispatch(createAccount(params)))
+ .then(({ token }) => dispatch(authLoggedIn(token)));
};
}
@@ -225,8 +217,8 @@ export function fetchCaptcha() {
}
export function authLoggedIn(token) {
- return {
- type: AUTH_LOGGED_IN,
- token,
+ return (dispatch, getState) => {
+ dispatch({ type: AUTH_LOGGED_IN, token });
+ return token;
};
}
diff --git a/app/soapbox/actions/external_auth.js b/app/soapbox/actions/external_auth.js
new file mode 100644
index 000000000..951f953bb
--- /dev/null
+++ b/app/soapbox/actions/external_auth.js
@@ -0,0 +1,65 @@
+/**
+ * External Auth: workflow for logging in to remote servers.
+ * @module soapbox/actions/external_auth
+ * @see module:soapbox/actions/auth
+ * @see module:soapbox/actions/apps
+ * @see module:soapbox/actions/oauth
+ */
+
+import { createApp } from 'soapbox/actions/apps';
+import { obtainOAuthToken } from 'soapbox/actions/oauth';
+import { authLoggedIn, verifyCredentials } from 'soapbox/actions/auth';
+import { parseBaseURL } from 'soapbox/utils/auth';
+import sourceCode from 'soapbox/utils/code';
+
+const scopes = 'read write follow push';
+
+export function createAppAndRedirect(host) {
+ return (dispatch, getState) => {
+ const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`);
+
+ const params = {
+ client_name: sourceCode.displayName,
+ redirect_uris: `${window.location.origin}/auth/external`,
+ website: sourceCode.homepage,
+ 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',
+ scopes,
+ });
+
+ localStorage.setItem('soapbox:external:app', JSON.stringify(app));
+ localStorage.setItem('soapbox:external:baseurl', baseURL);
+
+ window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`;
+ });
+ };
+}
+
+export function loginWithCode(code) {
+ return (dispatch, getState) => {
+ const { client_id, client_secret, redirect_uri } = JSON.parse(localStorage.getItem('soapbox:external:app'));
+ const baseURL = localStorage.getItem('soapbox:external:baseurl');
+
+ const params = {
+ client_id,
+ client_secret,
+ redirect_uri,
+ grant_type: 'authorization_code',
+ scope: scopes,
+ code,
+ };
+
+ return dispatch(obtainOAuthToken(params, baseURL))
+ .then(token => dispatch(authLoggedIn(token)))
+ .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL)))
+ .then(() => window.location.href = '/');
+ };
+}
diff --git a/app/soapbox/actions/oauth.js b/app/soapbox/actions/oauth.js
index e3ee95f3d..fe25d34f0 100644
--- a/app/soapbox/actions/oauth.js
+++ b/app/soapbox/actions/oauth.js
@@ -16,10 +16,10 @@ export const OAUTH_TOKEN_REVOKE_REQUEST = 'OAUTH_TOKEN_REVOKE_REQUEST';
export const OAUTH_TOKEN_REVOKE_SUCCESS = 'OAUTH_TOKEN_REVOKE_SUCCESS';
export const OAUTH_TOKEN_REVOKE_FAIL = 'OAUTH_TOKEN_REVOKE_FAIL';
-export function obtainOAuthToken(params) {
+export function obtainOAuthToken(params, baseURL) {
return (dispatch, getState) => {
dispatch({ type: OAUTH_TOKEN_CREATE_REQUEST, params });
- return baseClient().post('/oauth/token', params).then(({ data: token }) => {
+ return baseClient(null, baseURL).post('/oauth/token', params).then(({ data: token }) => {
dispatch({ type: OAUTH_TOKEN_CREATE_SUCCESS, params, token });
return token;
}).catch(error => {
diff --git a/app/soapbox/components/helmet.js b/app/soapbox/components/helmet.js
index e6da9b15e..91cc86c9d 100644
--- a/app/soapbox/components/helmet.js
+++ b/app/soapbox/components/helmet.js
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Helmet } from'react-helmet';
import { getSettings } from 'soapbox/actions/settings';
+import sourceCode from 'soapbox/utils/code';
const getNotifTotals = state => {
const notifications = state.getIn(['notifications', 'unread'], 0);
@@ -16,7 +17,7 @@ const mapStateToProps = state => {
const settings = getSettings(state);
return {
- siteTitle: state.getIn(['instance', 'title']),
+ siteTitle: state.getIn(['instance', 'title'], sourceCode.displayName),
unreadCount: getNotifTotals(state),
demetricator: settings.get('demetricator'),
};
diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js
index d3c8dbb0f..27f7ef614 100644
--- a/app/soapbox/containers/soapbox.js
+++ b/app/soapbox/containers/soapbox.js
@@ -13,7 +13,6 @@ import { Switch, BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4';
import UI from '../features/ui';
// import Introduction from '../features/introduction';
-import { fetchCustomEmojis } from '../actions/custom_emojis';
import { preload } from '../actions/preload';
import { IntlProvider } from 'react-intl';
import ErrorBoundary from '../components/error_boundary';
@@ -34,7 +33,6 @@ store.dispatch(preload());
store.dispatch(fetchMe());
store.dispatch(fetchInstance());
store.dispatch(fetchSoapboxConfig());
-store.dispatch(fetchCustomEmojis());
const mapStateToProps = (state) => {
const me = state.get('me');
diff --git a/app/soapbox/features/external_login/components/external_login_form.js b/app/soapbox/features/external_login/components/external_login_form.js
new file mode 100644
index 000000000..0fa91d590
--- /dev/null
+++ b/app/soapbox/features/external_login/components/external_login_form.js
@@ -0,0 +1,79 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms';
+import { createAppAndRedirect, loginWithCode } from 'soapbox/actions/external_auth';
+import LoadingIndicator from 'soapbox/components/loading_indicator';
+
+const messages = defineMessages({
+ instanceLabel: { id: 'login.fields.instance_label', defaultMessage: 'Instance' },
+ instancePlaceholder: { id: 'login.fields.instance_placeholder', defaultMessage: 'example.com' },
+});
+
+export default @connect()
+@injectIntl
+class ExternalLoginForm extends ImmutablePureComponent {
+
+ state = {
+ host: '',
+ isLoading: false,
+ }
+
+ handleHostChange = ({ target }) => {
+ this.setState({ host: target.value });
+ }
+
+ handleSubmit = e => {
+ const { dispatch } = this.props;
+ const { host } = this.state;
+
+ this.setState({ isLoading: true });
+
+ dispatch(createAppAndRedirect(host))
+ .then(() => this.setState({ isLoading: false }))
+ .catch(() => this.setState({ isLoading: false }));
+ }
+
+ componentDidMount() {
+ const code = new URLSearchParams(window.location.search).get('code');
+
+ if (code) {
+ this.setState({ code });
+ this.props.dispatch(loginWithCode(code));
+ }
+ }
+
+ render() {
+ const { intl } = this.props;
+ const { isLoading, code } = this.state;
+
+ if (code) {
+ return