Standalone: support running Soapbox FE on a subdomain

reply-to-ui
Alex Gleason 2021-08-22 14:34:58 -05:00
rodzic 1b30468366
commit 0b4b3479ea
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
15 zmienionych plików z 222 dodań i 32 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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 } = JSON.parse(localStorage.getItem('soapbox:external:app'));
const baseURL = localStorage.getItem('soapbox:external:baseurl');
const params = {
client_id,
client_secret,
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
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 = '/');
};
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -37,4 +37,5 @@ module.exports = {
url: pkg.repository.url,
repository: shortRepoName(pkg.repository.url),
version: version(pkg),
homepage: pkg.homepage,
};