From f125fac0778a4f49f98919f7abe25e80340c6b7e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 22 Feb 2018 14:22:58 -0700 Subject: [PATCH] replaced localStorage state events with postMessage --- client/coral-auth-callback/src/index.js | 35 +++++++--- .../coral-embed-stream/src/actions/login.js | 6 +- .../src/containers/Embed.js | 32 +++++++++ client/coral-framework/actions/auth.js | 15 +++- .../services/messages/constants.js | 11 +++ .../services/messages/index.js | 2 + .../services/messages/sendMessage.js | 13 ++++ .../services/messages/subscriptions.js | 70 +++++++++++++++++++ middleware/staticTemplate.js | 9 ++- .../client/actions.js | 6 +- .../talk-plugin-google-auth/client/actions.js | 6 +- url.js | 3 + views/auth-callback.ejs | 3 + views/partials/data.ejs | 3 + views/partials/head.ejs | 4 +- 15 files changed, 188 insertions(+), 30 deletions(-) create mode 100644 client/coral-framework/services/messages/constants.js create mode 100644 client/coral-framework/services/messages/index.js create mode 100644 client/coral-framework/services/messages/sendMessage.js create mode 100644 client/coral-framework/services/messages/subscriptions.js create mode 100644 views/partials/data.ejs diff --git a/client/coral-auth-callback/src/index.js b/client/coral-auth-callback/src/index.js index b7f3ed226..204a86470 100644 --- a/client/coral-auth-callback/src/index.js +++ b/client/coral-auth-callback/src/index.js @@ -1,14 +1,29 @@ +import { HANDLE_SUCCESSFUL_LOGIN } from 'coral-framework/constants/auth'; +import sendMessage from 'coral-framework/services/messages/sendMessage'; + 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 { + // 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; - // Set the item in localStorage. - localStorage.setItem('auth', doc.body.textContent); + // 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; - // Close the window. - setTimeout(() => { - window.close(); - }, 50); + // Send the state back. + sendMessage(HANDLE_SUCCESSFUL_LOGIN, { user, token }); + } + } finally { + // Always close the window. + setTimeout(() => { + window.close(); + }, 50); + } }); diff --git a/client/coral-embed-stream/src/actions/login.js b/client/coral-embed-stream/src/actions/login.js index c602bf7b6..fe1f76149 100644 --- a/client/coral-embed-stream/src/actions/login.js +++ b/client/coral-embed-stream/src/actions/login.js @@ -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, diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index f8486fd14..acdbb184b 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -29,6 +29,17 @@ import t from 'coral-framework/services/i18n'; import PropTypes from 'prop-types'; import { setActiveTab } from '../actions/embed'; +// TODO: refactor out to a HOC +import { HANDLE_SUCCESSFUL_LOGIN } from 'coral-framework/constants/auth'; +import { + handleSuccessfulLogin, + checkLogin, +} from 'coral-framework/actions/auth'; +import { + subscribeToMessages, + unsubscribeFromMessages, +} from 'coral-framework/services/messages'; + class EmbedContainer extends React.Component { static contextTypes = { pym: PropTypes.object, @@ -37,6 +48,8 @@ class EmbedContainer extends React.Component { subscriptions = []; subscribeToUpdates(props = this.props) { + subscribeToMessages(this.handleAuth); + if (props.currentUser) { const newSubscriptions = [ { @@ -86,8 +99,23 @@ class EmbedContainer extends React.Component { unsubscribe() { this.subscriptions.forEach(unsubscribe => unsubscribe()); this.subscriptions = []; + unsubscribeFromMessages(this.handleAuth); } + // TODO: refactor out to a HOC + handleAuth = ({ name, data }) => { + if (name !== HANDLE_SUCCESSFUL_LOGIN) { + return; + } + + // data will contain the user and token. + const { user, token } = data; + const { handleSuccessfulLogin, checkLogin } = this.props; + + handleSuccessfulLogin(user, token); + checkLogin(); + }; + resubscribe(props) { this.unsubscribe(); this.subscribeToUpdates(props); @@ -301,6 +329,8 @@ EmbedContainer.propTypes = { fetchAssetSuccess: PropTypes.func, showSignInDialog: PropTypes.bool, signInDialogFocus: PropTypes.bool, + handleSuccessfulLogin: PropTypes.func.isRequired, + checkLogin: PropTypes.func.isRequired, }; const mapStateToProps = state => ({ @@ -328,6 +358,8 @@ const mapDispatchToProps = dispatch => blurSignInDialog, hideSignInDialog, updateStatus, + handleSuccessfulLogin, + checkLogin, }, dispatch ); diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 0c4780548..6fe8ad508 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -1,5 +1,6 @@ import * as actions from '../constants/auth'; import jwtDecode from 'jwt-decode'; +import { sendMessage } from '../services/messages'; function cleanAuthData(localStorage) { localStorage.removeItem('token'); @@ -65,11 +66,23 @@ export const handleSuccessfulLogin = (user, token) => ( _, { client, localStorage } ) => { + 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) { + sendMessage( + actions.HANDLE_SUCCESSFUL_LOGIN, + { user, token }, + window.opener + ); + } + client.resetWebsocket(); dispatch({ diff --git a/client/coral-framework/services/messages/constants.js b/client/coral-framework/services/messages/constants.js new file mode 100644 index 000000000..7204194f7 --- /dev/null +++ b/client/coral-framework/services/messages/constants.js @@ -0,0 +1,11 @@ +import { getStaticConfiguration } from 'coral-framework/services/staticConfiguration'; + +// Load the static url from the static configuration, we'll use it to formulate +// the authorized domain to allow the message to be sent to. +const { STATIC_ORIGIN } = getStaticConfiguration(); + +// Export the origin that will be sent. +export { STATIC_ORIGIN as ORIGIN }; + +// Scope all the requests made via the messages service. +export const SCOPE = 'client'; diff --git a/client/coral-framework/services/messages/index.js b/client/coral-framework/services/messages/index.js new file mode 100644 index 000000000..311034624 --- /dev/null +++ b/client/coral-framework/services/messages/index.js @@ -0,0 +1,2 @@ +export { default as sendMessage } from './sendMessage'; +export { subscribeToMessages, unsubscribeFromMessages } from './subscriptions'; diff --git a/client/coral-framework/services/messages/sendMessage.js b/client/coral-framework/services/messages/sendMessage.js new file mode 100644 index 000000000..a0e7dd803 --- /dev/null +++ b/client/coral-framework/services/messages/sendMessage.js @@ -0,0 +1,13 @@ +import { SCOPE, ORIGIN } from './constants'; + +export default (name, data, target = window.opener) => { + if (!target) { + return; + } + + // Serialize the message to be sent via postMessage. + const msg = { name, data, scope: SCOPE }; + + // Send the message. + target.postMessage(msg, ORIGIN); +}; diff --git a/client/coral-framework/services/messages/subscriptions.js b/client/coral-framework/services/messages/subscriptions.js new file mode 100644 index 000000000..984a8377d --- /dev/null +++ b/client/coral-framework/services/messages/subscriptions.js @@ -0,0 +1,70 @@ +import { SCOPE, ORIGIN } from './constants'; + +import set from 'lodash/set'; +import has from 'lodash/has'; +import get from 'lodash/get'; +import remove from 'lodash/remove'; + +// withOriginCheck wraps a given handler for a messages event with a check to +// see if the origin matches. +const withOriginCheck = handler => { + return event => { + const origin = get(event, 'origin'); + if (origin !== ORIGIN) { + return; + } + + const scope = get(event, 'data.scope'); + if (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 }); + }; +}; + +// Store a reference to each listener added. +const listeners = {}; + +export const subscribeToMessages = (handler, target = window) => { + // If this handler is already attached to the target, detach it. + if (has(listeners, [target, handler])) { + 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); +}; + +export const unsubscribeFromMessages = (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]); +}; diff --git a/middleware/staticTemplate.js b/middleware/staticTemplate.js index 6848e59e1..5a7c34b60 100644 --- a/middleware/staticTemplate.js +++ b/middleware/staticTemplate.js @@ -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, }, }; diff --git a/plugins/talk-plugin-facebook-auth/client/actions.js b/plugins/talk-plugin-facebook-auth/client/actions.js index 5a9ebc66f..79f2d2a6a 100644 --- a/plugins/talk-plugin-facebook-auth/client/actions.js +++ b/plugins/talk-plugin-facebook-auth/client/actions.js @@ -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`; }; diff --git a/plugins/talk-plugin-google-auth/client/actions.js b/plugins/talk-plugin-google-auth/client/actions.js index 6519d9f2b..8b49bf39e 100644 --- a/plugins/talk-plugin-google-auth/client/actions.js +++ b/plugins/talk-plugin-google-auth/client/actions.js @@ -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`; }; diff --git a/url.js b/url.js index e855859de..294095d48 100644 --- a/url.js +++ b/url.js @@ -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, }; diff --git a/views/auth-callback.ejs b/views/auth-callback.ejs index 25603bda4..424889065 100644 --- a/views/auth-callback.ejs +++ b/views/auth-callback.ejs @@ -1,5 +1,8 @@ + + <%- include partials/data %> + diff --git a/views/partials/data.ejs b/views/partials/data.ejs new file mode 100644 index 000000000..f0bbf5925 --- /dev/null +++ b/views/partials/data.ejs @@ -0,0 +1,3 @@ +<%_ if (data != null) { _%> + +<%_ } _%> \ No newline at end of file diff --git a/views/partials/head.ejs b/views/partials/head.ejs index bc8b949d3..555f5689e 100644 --- a/views/partials/head.ejs +++ b/views/partials/head.ejs @@ -18,7 +18,5 @@ <%_ if (locals.customCssUrl) { _%> <%_ } _%> -<%_ if (data != null) { _%> - -<%_ } _%> +<%- include data %> \ No newline at end of file