Merge branch 'master' into purge-config-auth-token

This commit is contained in:
Kiwi
2018-02-27 18:05:45 +01:00
committed by GitHub
29 changed files with 236 additions and 239 deletions
+3 -2
View File
@@ -13,6 +13,7 @@ public
!plugins/talk-plugin-facebook-auth
!plugins/talk-plugin-featured-comments
!plugins/talk-plugin-flag-details
!plugins/talk-plugin-google-auth
!plugins/talk-plugin-ignore-user
!plugins/talk-plugin-like
!plugins/talk-plugin-love
@@ -21,6 +22,7 @@ public
!plugins/talk-plugin-moderation-actions
!plugins/talk-plugin-offtopic
!plugins/talk-plugin-permalink
!plugins/talk-plugin-profile-settings
!plugins/talk-plugin-remember-sort
!plugins/talk-plugin-respect
!plugins/talk-plugin-sort-most-liked
@@ -31,5 +33,4 @@ public
!plugins/talk-plugin-sort-oldest
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-toxic-comments
!plugins/talk-plugin-viewing-options
!plugins/talk-plugin-profile-settings
!plugins/talk-plugin-viewing-options
+30 -10
View File
@@ -1,14 +1,34 @@
import { HANDLE_SUCCESSFUL_LOGIN } from 'coral-framework/constants/auth';
import { getStaticConfiguration } from 'coral-framework/services/staticConfiguration';
import { createPostMessage } from 'coral-framework/services/postMessage';
document.addEventListener('DOMContentLoaded', () => {
// Get the auth element and parse it as JSON by decoding it.
const auth = document.getElementById('auth');
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = auth.innerText;
try {
const staticConfig = getStaticConfiguration();
const { STATIC_ORIGIN: origin } = staticConfig;
const postMessage = createPostMessage(origin);
// Set the item in localStorage.
localStorage.setItem('auth', doc.body.textContent);
// Get the auth element and parse it as JSON by decoding it.
const auth = document.getElementById('auth');
const doc = document.implementation.createHTMLDocument('');
doc.body.innerHTML = auth.innerText;
// Close the window.
setTimeout(() => {
window.close();
}, 50);
// Auth state is contained within the node.
const { err, data } = JSON.parse(doc.body.textContent);
if (err) {
// TODO: send back the error message.
console.error(err);
} else {
// The data will contain a user and a token.
const { user, token } = data;
// Send the state back.
postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token });
}
} finally {
// Always close the window.
setTimeout(() => {
window.close();
}, 50);
}
});
@@ -1,75 +0,0 @@
import * as actions from '../constants/asset';
import { notify } from 'coral-framework/actions/notification';
import t from 'coral-framework/services/i18n';
export const fetchAssetRequest = () => ({ type: actions.FETCH_ASSET_REQUEST });
export const fetchAssetSuccess = asset => ({
type: actions.FETCH_ASSET_SUCCESS,
asset,
});
export const fetchAssetFailure = error => ({
type: actions.FETCH_ASSET_FAILURE,
error,
});
const updateAssetSettingsRequest = () => ({
type: actions.UPDATE_ASSET_SETTINGS_REQUEST,
});
const updateAssetSettingsSuccess = settings => ({
type: actions.UPDATE_ASSET_SETTINGS_SUCCESS,
settings,
});
const updateAssetSettingsFailure = error => ({
type: actions.UPDATE_ASSET_SETTINGS_FAILURE,
error,
});
export const updateConfiguration = newConfig => (
dispatch,
getState,
{ rest }
) => {
const assetId = getState().asset.id;
dispatch(updateAssetSettingsRequest());
rest(`/assets/${assetId}/settings`, { method: 'PUT', body: newConfig })
.then(() => {
dispatch(notify('success', t('framework.success_update_settings')));
dispatch(updateAssetSettingsSuccess(newConfig));
})
.catch(error => {
console.error(error);
dispatch(updateAssetSettingsFailure(error));
});
};
export const updateOpenStream = closedBody => (
dispatch,
getState,
{ rest }
) => {
const assetId = getState().asset.id;
dispatch(fetchAssetRequest());
rest(`/assets/${assetId}/status`, { method: 'PUT', body: closedBody })
.then(() => {
dispatch(notify('success', t('framework.success_update_settings')));
dispatch(fetchAssetSuccess(closedBody));
})
.catch(error => {
console.error(error);
dispatch(fetchAssetFailure(error));
});
};
const openStream = () => ({ type: actions.OPEN_COMMENTS });
const closeStream = () => ({ type: actions.CLOSE_COMMENTS });
export const updateOpenStatus = status => dispatch => {
if (status === 'open') {
dispatch(openStream());
dispatch(updateOpenStream({ closedAt: null }));
} else {
dispatch(closeStream());
dispatch(updateOpenStream({ closedAt: new Date().getTime() }));
}
};
@@ -1,6 +0,0 @@
import { ADD_EXTERNAL_CONFIG } from '../constants/config';
export const addExternalConfig = config => ({
type: ADD_EXTERNAL_CONFIG,
config,
});
@@ -1,14 +1,10 @@
import * as actions from '../constants/login';
import { checkLogin } from 'coral-framework/actions/auth';
export const showSignInDialog = () => ({
type: actions.SHOW_SIGNIN_DIALOG,
});
export const hideSignInDialog = () => dispatch => {
dispatch(checkLogin());
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
};
export const hideSignInDialog = () => ({ type: actions.HIDE_SIGNIN_DIALOG });
export const focusSignInDialog = () => ({
type: actions.FOCUS_SIGNIN_DIALOG,
@@ -1,10 +0,0 @@
export const FETCH_ASSET_REQUEST = 'FETCH_ASSET_REQUEST';
export const FETCH_ASSET_FAILURE = 'FETCH_ASSET_FAILURE';
export const FETCH_ASSET_SUCCESS = 'FETCH_ASSET_SUCCESS';
export const UPDATE_ASSET_SETTINGS_REQUEST = 'UPDATE_ASSET_SETTINGS_REQUEST';
export const UPDATE_ASSET_SETTINGS_SUCCESS = 'UPDATE_ASSET_SETTINGS_SUCCESS';
export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
@@ -1 +0,0 @@
export const ADD_EXTERNAL_CONFIG = 'ADD_EXTERNAL_CONFIG';
@@ -2,7 +2,6 @@ import React from 'react';
import { compose, gql } from 'react-apollo';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import isEqual from 'lodash/isEqual';
import get from 'lodash/get';
import branch from 'recompose/branch';
import renderComponent from 'recompose/renderComponent';
@@ -14,12 +13,11 @@ import {
hideSignInDialog,
} from '../actions/login';
import { updateStatus } from 'coral-framework/actions/auth';
import { fetchAssetSuccess } from '../actions/asset';
import {
getDefinitionName,
getSlotFragmentSpreads,
} from 'coral-framework/utils';
import { withQuery } from 'coral-framework/hocs';
import { withQuery, withPopupAuthHandler } from 'coral-framework/hocs';
import Embed from '../components/Embed';
import Stream from '../tabs/stream/containers/Stream';
import AutomaticAssetClosure from './AutomaticAssetClosure';
@@ -107,12 +105,6 @@ class EmbedContainer extends React.Component {
this.props.data.refetch();
this.resubscribe(nextProps);
}
const { fetchAssetSuccess } = this.props;
if (!isEqual(nextProps.root.asset, this.props.root.asset)) {
// TODO: remove asset data from redux store.
fetchAssetSuccess(nextProps.root.asset);
}
}
componentDidUpdate(prevProps) {
@@ -298,7 +290,6 @@ EmbedContainer.propTypes = {
activeTab: PropTypes.string,
parentUrl: PropTypes.string,
data: PropTypes.object,
fetchAssetSuccess: PropTypes.func,
showSignInDialog: PropTypes.bool,
signInDialogFocus: PropTypes.bool,
};
@@ -322,7 +313,6 @@ const mapDispatchToProps = dispatch =>
bindActionCreators(
{
setActiveTab,
fetchAssetSuccess,
notify,
focusSignInDialog,
blurSignInDialog,
@@ -333,6 +323,7 @@ const mapDispatchToProps = dispatch =>
);
export default compose(
withPopupAuthHandler,
connect(mapStateToProps, mapDispatchToProps),
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
withEmbedQuery
@@ -1,28 +0,0 @@
import * as actions from '../constants/asset';
const initialState = {
closedAt: null,
settings: null,
title: null,
url: null,
features: {},
status: 'open',
moderation: null,
};
export default function asset(state = initialState, action) {
switch (action.type) {
case actions.FETCH_ASSET_SUCCESS:
return {
...state,
...action.asset,
};
case actions.UPDATE_ASSET_SETTINGS_SUCCESS:
return {
...state,
settings: action.settings,
};
default:
return state;
}
}
@@ -1,15 +0,0 @@
import { ADD_EXTERNAL_CONFIG } from '../constants/config';
const initialState = {};
export default function config(state = initialState, action) {
switch (action.type) {
case ADD_EXTERNAL_CONFIG:
return {
...state,
...action.config,
};
default:
return state;
}
}
@@ -1,5 +1,4 @@
import login from './login';
import asset from './asset';
import embed from './embed';
import configure from './configure';
import stream from './stream';
@@ -7,7 +6,6 @@ import profile from './profile';
export default {
login,
asset,
embed,
configure,
stream,
+14 -2
View File
@@ -63,13 +63,25 @@ export const setAuthToken = token => (dispatch, _, { localStorage }) => {
export const handleSuccessfulLogin = (user, token) => (
dispatch,
_,
{ client, localStorage }
{ client, localStorage, postMessage }
) => {
const { exp } = jwtDecode(token);
if (localStorage) {
localStorage.setItem('exp', jwtDecode(token).exp);
localStorage.setItem('exp', exp);
localStorage.setItem('token', token);
}
// Send the message via the messages service to the window.opener if it
// exists.
if (window.opener) {
postMessage.post(
actions.HANDLE_SUCCESSFUL_LOGIN,
{ user, token },
window.opener
);
}
client.resetWebsocket();
dispatch({
@@ -17,6 +17,7 @@ class TalkProvider extends React.Component {
store: this.props.store,
pymLocalStorage: this.props.pymLocalStorage,
pymSessionStorage: this.props.pymSessionStorage,
postMessage: this.props.postMessage,
};
}
@@ -39,6 +40,7 @@ TalkProvider.childContextTypes = {
pymSessionStorage: PropTypes.object,
history: PropTypes.object,
store: PropTypes.object,
postMessage: PropTypes.object,
};
TalkProvider.propTypes = TalkProvider.childContextTypes;
+1 -1
View File
@@ -698,7 +698,7 @@ export const withCloseAsset = withMutation(
const fragmentId = `Asset_${id}`;
const data = {
__typename: 'Asset',
closedAt: new Date(),
closedAt: new Date().toISOString(),
isClosed: true,
};
proxy.writeFragment({ fragment, id: fragmentId, data });
+1
View File
@@ -10,6 +10,7 @@ export { default as withSignIn } from './withSignIn';
export { default as withSignUp } from './withSignUp';
export { default as withForgotPassword } from './withForgotPassword';
export { default as withSetUsername } from './withSetUsername';
export { default as withPopupAuthHandler } from './withPopupAuthHandler';
export {
default as withResendEmailConfirmation,
} from './withResendEmailConfirmation';
@@ -0,0 +1,54 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import PropTypes from 'prop-types';
import { HANDLE_SUCCESSFUL_LOGIN } from 'coral-framework/constants/auth';
import {
handleSuccessfulLogin,
setAuthToken,
} from 'coral-framework/actions/auth';
/**
* WithPopupAuthHandler listens to successful logins over
* the `postMessage` service.
*/
export default hoistStatics(WrappedComponent => {
class WithPopupAuthHandler extends React.Component {
static contextTypes = {
store: PropTypes.object,
postMessage: PropTypes.object,
};
constructor(props, context) {
super(props, context);
context.postMessage.subscribe(this.handleAuth);
}
componentWillUnmount() {
this.context.postMessage.unsubscribe(this.handleAuth);
}
handleAuth = ({ name, data }) => {
if (name !== HANDLE_SUCCESSFUL_LOGIN) {
return;
}
const { store } = this.context;
// data will contain the user and token.
const { user, token } = data;
if (user && token) {
store.dispatch(handleSuccessfulLogin(user, token));
} else if (token) {
store.dispatch(setAuthToken(token));
} else {
console.error('Invalid auth data supplied', data);
}
};
render() {
return <WrappedComponent {...this.props} />;
}
}
return WithPopupAuthHandler;
});
+5 -1
View File
@@ -14,6 +14,7 @@ import { createPluginsService } from './plugins';
import { createNotificationService } from './notification';
import { createGraphQLRegistry } from './graphqlRegistry';
import { createGraphQLService } from './graphql';
import { createPostMessage } from './postMessage';
import globalFragments from 'coral-framework/graphql/fragments';
import {
createStorage,
@@ -118,7 +119,7 @@ export async function createContext({
});
const staticConfig = getStaticConfiguration();
let { LIVE_URI: liveUri } = staticConfig;
let { LIVE_URI: liveUri, STATIC_ORIGIN: origin } = staticConfig;
if (liveUri == null) {
// The protocol must match the origin protocol, secure/insecure.
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
@@ -128,6 +129,8 @@ export async function createContext({
liveUri = `${protocol}://${location.host}${BASE_PATH}api/v1/live`;
}
const postMessage = createPostMessage(origin);
const client = createClient({
uri: `${BASE_PATH}api/v1/graph/ql`,
liveUri,
@@ -158,6 +161,7 @@ export async function createContext({
pymLocalStorage,
pymSessionStorage,
inIframe,
postMessage,
};
// Load framework fragments.
@@ -0,0 +1,87 @@
import set from 'lodash/set';
import has from 'lodash/has';
import get from 'lodash/get';
import remove from 'lodash/remove';
/**
* createPostMessage returns a service that deals with cross
* window communication using the postMessage API.
* @param {string} origin
* @param {string} scope
* @return {Object} postMessage service
*/
export function createPostMessage(origin, scope = 'client') {
// Store a reference to each listener added.
const listeners = {};
// withOriginCheck wraps a given handler for a messages event with a check to
// see if the origin matches.
const withOriginCheck = handler => {
return event => {
if (get(event, 'origin') !== origin) {
return;
}
if (get(event, 'data.scope') !== scope) {
return;
}
// Pass the handler the event details.
handler(event);
};
};
// withParseData will parse the data.
const withParseData = handler => {
return event => {
const data = get(event, 'data.data');
const name = get(event, 'data.name');
if (!data || !name) {
return;
}
handler({ data, name, event });
};
};
return {
post(name, data, target = window.opener) {
if (!target) {
return;
}
// Serialize the message to be sent via postMessage.
const msg = { name, data, scope };
// Send the message.
target.postMessage(msg, origin);
},
subscribe: (handler, target = window) => {
// If this handler is already attached to the target, detach it.
if (has(listeners, [target, handler])) {
this.unsubscribeFromMessages(handler, target);
}
// Wrap the listener with a origin check.
const listener = withOriginCheck(withParseData(handler));
// Save a reference to the compiled listener.
set(listeners, [target, handler], listener);
// Attach the listener to the target.
target.addEventListener('message', listener);
},
unsubscribe: (handler, target = window) => {
if (!has(listeners, [target, handler])) {
return;
}
const listener = get(listeners, [target, handler]);
// Remove the listener from the target.
target.removeEventListener('message', listener);
remove(listeners, [target, handler]);
},
};
}
+14 -7
View File
@@ -1,9 +1,10 @@
import uuid from 'uuid/v4';
function getStorage(type) {
let storage = window[type],
x = '__storage_test__';
let storage;
try {
storage = window[type];
const x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
} catch (e) {
@@ -11,6 +12,8 @@ function getStorage(type) {
e instanceof DOMException &&
// everything except Firefox
(e.code === 22 ||
// SecurityError related to having 3rd party cookies disabled.
e.code === 18 ||
// Firefox
e.code === 1014 ||
@@ -19,14 +22,18 @@ function getStorage(type) {
// everything except Firefox
e.name === 'QuotaExceededError' ||
// Firefox
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
// acknowledge QuotaExceededError only if there's something already stored
storage.length !== 0;
e.name === 'NS_ERROR_DOM_QUOTA_REACHED');
if (!ignore) {
console.warning(e); // eslint-disable-line
return null;
console.warn(e);
}
// When third party cookies are disabled, session storage is readable/
// writable, but localStorage is not. Try to get the sessionStorage to use.
if (type !== 'sessionStorage') {
return getStorage('sessionStorage');
}
}
return storage;
}
+8 -1
View File
@@ -1,6 +1,12 @@
const SettingsService = require('../services/settings');
const { BASE_URL, BASE_PATH, MOUNT_PATH, STATIC_URL } = require('../url');
const {
BASE_URL,
BASE_PATH,
MOUNT_PATH,
STATIC_URL,
STATIC_ORIGIN,
} = require('../url');
const { RECAPTCHA_PUBLIC, WEBSOCKET_LIVE_URI } = require('../config');
@@ -15,6 +21,7 @@ const TEMPLATE_LOCALS = {
TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC,
LIVE_URI: WEBSOCKET_LIVE_URI,
STATIC_URL,
STATIC_ORIGIN,
},
};
@@ -4,10 +4,6 @@ import Main from '../components/Main';
import { connect } from 'plugin-api/beta/client/hocs';
import { bindActionCreators } from 'redux';
import { setView } from '../actions';
import {
setAuthToken,
handleSuccessfulLogin,
} from 'plugin-api/beta/client/actions/auth';
import * as views from '../enums/views';
class MainContainer extends React.Component {
@@ -24,7 +20,6 @@ class MainContainer extends React.Component {
componentDidMount() {
this.resizeHeight();
this.listenToStorageChanges();
}
componentDidUpdate(prevProps) {
@@ -33,40 +28,6 @@ class MainContainer extends React.Component {
}
}
componentWillUnmount() {
this.unlisten();
}
listenToStorageChanges() {
window.addEventListener('storage', this.handleAuth);
}
unlisten() {
window.removeEventListener('storage', this.handleAuth);
}
// External logins store auth data into `auth`, we use it to detect
// a successful sign in.
handleAuth = e => {
if (e.key === 'auth') {
const { err, data } = JSON.parse(e.newValue);
if (err) {
console.error(err);
} else if (data && data.token) {
if (data.user) {
this.props.handleSuccessfulLogin(data.user, data.token);
} else {
this.props.setAuthToken(data.token);
}
this.unlisten();
localStorage.removeItem('auth');
window.close();
} else {
console.error('auth was set, but did not contain a token');
}
}
};
render() {
return <Main onResetView={this.resetView} view={this.props.view} />;
}
@@ -75,8 +36,6 @@ class MainContainer extends React.Component {
MainContainer.propTypes = {
view: PropTypes.string.isRequired,
setView: PropTypes.func.isRequired,
handleSuccessfulLogin: PropTypes.func.isRequired,
setAuthToken: PropTypes.func.isRequired,
};
const mapStateToProps = ({ talkPluginAuth: state }) => ({
@@ -87,8 +46,6 @@ const mapDispatchToProps = dispatch =>
bindActionCreators(
{
setView,
handleSuccessfulLogin,
setAuthToken,
},
dispatch
);
@@ -1,7 +1,3 @@
export const loginWithFacebook = () => (dispatch, _, { rest }) => {
window.open(
`${rest.uri}/auth/facebook`,
'Continue with Facebook',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
window.location = `${rest.uri}/auth/facebook`;
};
@@ -1,7 +1,3 @@
export const loginWithGoogle = () => (dispatch, _, { rest }) => {
window.open(
`${rest.uri}/auth/google`,
'Continue with Google',
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
);
window.location = `${rest.uri}/auth/google`;
};
@@ -3,7 +3,5 @@ import GoogleButton from '../containers/GoogleButton';
import { t } from 'plugin-api/beta/client/services';
export default () => {
return (
<GoogleButton>{t('talk-plugin-google-auth.sign_in')}</GoogleButton>
);
return <GoogleButton>{t('talk-plugin-google-auth.sign_in')}</GoogleButton>;
};
@@ -3,7 +3,5 @@ import GoogleButton from '../containers/GoogleButton';
import { t } from 'plugin-api/beta/client/services';
export default () => {
return (
<GoogleButton>{t('talk-plugin-google-auth.sign_up')}</GoogleButton>
);
return <GoogleButton>{t('talk-plugin-google-auth.sign_up')}</GoogleButton>;
};
+3
View File
@@ -18,9 +18,12 @@ const MOUNT_PATH = ROOT_URL_MOUNT_PATH ? BASE_PATH : '/';
// The STATIC_URL is the url where static assets should be loaded from.
const STATIC_URL = trailingSlash(STATIC_URI);
const STATIC_ORIGIN = new URL(STATIC_URI).origin;
module.exports = {
BASE_URL,
BASE_PATH,
MOUNT_PATH,
STATIC_URL,
STATIC_ORIGIN,
};
+3
View File
@@ -1,5 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<%- include partials/data %>
</head>
<body>
<script type="application/json" id="auth"><%- encodeJSONForHTML(auth) %></script>
<script type="text/javascript" src="<%= STATIC_URL %>static/coral-auth-callback/bundle.js"></script>
+3
View File
@@ -0,0 +1,3 @@
<%_ if (data != null) { _%>
<script id="data" type="application/json"><%- JSON.stringify(data) %></script>
<%_ } _%>
+1 -3
View File
@@ -18,7 +18,5 @@
<%_ if (locals.customCssUrl) { _%>
<link href="<%= customCssUrl %>" rel="stylesheet" type="text/css">
<%_ } _%>
<%_ if (data != null) { _%>
<script id="data" type="application/json"><%- JSON.stringify(data) %></script>
<%_ } _%>
<%- include data %>
<base href="<%= BASE_URL %>"/>