Merge branch 'standalone' into 'develop'
Standalone: support running Soapbox FE on a subdomain Closes #76 See merge request soapbox-pub/soapbox-fe!685
This commit is contained in:
commit
9e966532fe
15 changed files with 222 additions and 32 deletions
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
65
app/soapbox/actions/external_auth.js
Normal file
65
app/soapbox/actions/external_auth.js
Normal file
|
@ -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 = '/');
|
||||
};
|
||||
}
|
|
@ -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 => {
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleForm onSubmit={this.handleSubmit}>
|
||||
<fieldset disabled={isLoading}>
|
||||
<FieldsGroup>
|
||||
<TextInput
|
||||
label={intl.formatMessage(messages.instanceLabel)}
|
||||
placeholder={intl.formatMessage(messages.instancePlaceholder)}
|
||||
name='host'
|
||||
value={this.state.host}
|
||||
onChange={this.handleHostChange}
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
</FieldsGroup>
|
||||
</fieldset>
|
||||
<div className='actions'>
|
||||
<button name='button' type='submit' className='btn button button-primary'>
|
||||
<FormattedMessage id='login.log_in' defaultMessage='Log in' />
|
||||
</button>
|
||||
</div>
|
||||
</SimpleForm>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
11
app/soapbox/features/external_login/index.js
Normal file
11
app/soapbox/features/external_login/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ExternalLoginForm from './components/external_login_form';
|
||||
|
||||
export default class ExternalLoginPage extends ImmutablePureComponent {
|
||||
|
||||
render() {
|
||||
return <ExternalLoginForm />;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { Switch, Route, Redirect } from 'react-router-dom';
|
||||
import NotificationsContainer from 'soapbox/features/ui/containers/notifications_container';
|
||||
import ModalContainer from 'soapbox/features/ui/containers/modal_container';
|
||||
import Header from './components/header';
|
||||
|
@ -9,10 +9,23 @@ import Footer from './components/footer';
|
|||
import LandingPage from '../landing_page';
|
||||
import AboutPage from '../about';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { isPrerendered } from 'soapbox/precheck';
|
||||
|
||||
const validInstance = state => {
|
||||
const v = state.getIn(['instance', 'version']);
|
||||
return v && typeof v === 'string' && v !== '0.0.0';
|
||||
};
|
||||
|
||||
const isStandalone = state => {
|
||||
const hasInstance = validInstance(state);
|
||||
const instanceFetchFailed = state.getIn(['meta', 'instance_fetch_failed']);
|
||||
|
||||
return !isPrerendered && !hasInstance && instanceFetchFailed;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
instance: state.get('instance'),
|
||||
soapbox: getSoapboxConfig(state),
|
||||
standalone: isStandalone(state),
|
||||
});
|
||||
|
||||
const wave = (
|
||||
|
@ -24,8 +37,11 @@ const wave = (
|
|||
class PublicLayout extends ImmutablePureComponent {
|
||||
|
||||
render() {
|
||||
const { instance } = this.props;
|
||||
if (instance.isEmpty()) return null;
|
||||
const { standalone } = this.props;
|
||||
|
||||
if (standalone) {
|
||||
return <Redirect to='/auth/external' />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='public-layout'>
|
||||
|
|
|
@ -43,6 +43,7 @@ import { isStaff, isAdmin } from 'soapbox/utils/accounts';
|
|||
import ProfileHoverCard from 'soapbox/components/profile_hover_card';
|
||||
import { getAccessToken } from 'soapbox/utils/auth';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis';
|
||||
|
||||
import {
|
||||
Status,
|
||||
|
@ -79,6 +80,7 @@ import {
|
|||
// GroupCreate,
|
||||
// GroupEdit,
|
||||
LoginPage,
|
||||
ExternalLogin,
|
||||
Preferences,
|
||||
EditProfile,
|
||||
SoapboxConfig,
|
||||
|
@ -195,6 +197,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<Switch>
|
||||
<WrappedRoute path='/auth/sign_in' component={LoginPage} publicRoute exact />
|
||||
<WrappedRoute path='/auth/reset_password' component={PasswordReset} publicRoute exact />
|
||||
<WrappedRoute path='/auth/external' component={ExternalLogin} publicRoute exact />
|
||||
<WrappedRoute path='/auth/edit' page={DefaultPage} component={SecurityForm} exact />
|
||||
<WrappedRoute path='/auth/mfa' page={DefaultPage} component={MfaForm} exact />
|
||||
|
||||
|
@ -458,6 +461,8 @@ class UI extends React.PureComponent {
|
|||
|
||||
setTimeout(() => this.props.dispatch(fetchScheduledStatuses()), 900);
|
||||
}
|
||||
|
||||
this.props.dispatch(fetchCustomEmojis());
|
||||
this.connectStreaming();
|
||||
}
|
||||
|
||||
|
@ -588,7 +593,10 @@ class UI extends React.PureComponent {
|
|||
const { draggingOver, mobile } = this.state;
|
||||
const { intl, children, location, dropdownMenuIsOpen, me } = this.props;
|
||||
|
||||
if (me === null || !streamingUrl) return null;
|
||||
// Wait for login to succeed or fail
|
||||
if (me === null) return null;
|
||||
// If login didn't fail, wait for streaming to become available
|
||||
if (me !== false && !streamingUrl) return null;
|
||||
|
||||
const handlers = me ? {
|
||||
help: this.handleHotkeyToggleHelp,
|
||||
|
|
|
@ -170,6 +170,10 @@ export function LoginPage() {
|
|||
return import(/* webpackChunkName: "features/auth_login" */'../../auth_login/components/login_page');
|
||||
}
|
||||
|
||||
export function ExternalLogin() {
|
||||
return import(/* webpackChunkName: "features/external_login" */'../../external_login');
|
||||
}
|
||||
|
||||
export function Preferences() {
|
||||
return import(/* webpackChunkName: "features/preferences" */'../../preferences');
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import './wdyr';
|
||||
import './precheck';
|
||||
// FIXME: Push notifications are temporarily removed
|
||||
// import * as registerPushNotifications from './actions/push_notifications';
|
||||
// import { default as Soapbox, store } from './containers/soapbox';
|
||||
|
|
11
app/soapbox/precheck.js
Normal file
11
app/soapbox/precheck.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Precheck: information about the site before anything renders.
|
||||
* @module soapbox/precheck
|
||||
*/
|
||||
|
||||
const hasTitle = Boolean(document.querySelector('title'));
|
||||
|
||||
const hasPrerenderPleroma = Boolean(document.getElementById('initial-results'));
|
||||
const hasPrerenderMastodon = Boolean(document.getElementById('initial-state'));
|
||||
|
||||
export const isPrerendered = hasTitle || hasPrerenderPleroma || hasPrerenderMastodon;
|
|
@ -1,6 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
|
||||
import { INSTANCE_FETCH_FAIL } from 'soapbox/actions/instance';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
@ -19,6 +20,8 @@ export default function meta(state = initialState, action) {
|
|||
case ME_FETCH_SUCCESS:
|
||||
case ME_PATCH_SUCCESS:
|
||||
return importAccount(state, fromJS(action.me));
|
||||
case INSTANCE_FETCH_FAIL:
|
||||
return state.set('instance_fetch_failed', true);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -37,4 +37,5 @@ module.exports = {
|
|||
url: pkg.repository.url,
|
||||
repository: shortRepoName(pkg.repository.url),
|
||||
version: version(pkg),
|
||||
homepage: pkg.homepage,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue