From 48875294c8852c6e75e2462cc7649dc399d24511 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 5 Sep 2017 13:57:35 -0600 Subject: [PATCH 1/3] initial support for changing the uri --- client/coral-framework/constants/config.js | 3 ++ client/coral-framework/services/bootstrap.js | 3 +- config.js | 3 ++ docs/_data/navigation.yml | 2 + docs/_docs/00-01-faq.md | 2 +- docs/_docs/02-01-configuration.md | 10 +++++ docs/_docs/02-05-persistence.md | 45 ++++++++++++++++++++ routes/admin/index.js | 6 ++- routes/embed/index.js | 27 ++++++------ 9 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 client/coral-framework/constants/config.js create mode 100644 docs/_docs/02-05-persistence.md diff --git a/client/coral-framework/constants/config.js b/client/coral-framework/constants/config.js new file mode 100644 index 000000000..81eab50e2 --- /dev/null +++ b/client/coral-framework/constants/config.js @@ -0,0 +1,3 @@ +const configElement = document.querySelector('#data'); + +export const CONFIG = JSON.parse(configElement ? configElement.textContent : '{}'); diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index ac94d8b29..eed8313d4 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -8,6 +8,7 @@ import thunk from 'redux-thunk'; import {loadTranslations} from './i18n'; import bowser from 'bowser'; import {BASE_PATH} from 'coral-framework/constants/url'; +import {CONFIG} from 'coral-framework/constants/config'; import {createPluginsService} from './plugins'; import {createNotificationService} from './notification'; import {createGraphQLRegistry} from './graphqlRegistry'; @@ -69,7 +70,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi }); const client = createClient({ uri: `${BASE_PATH}api/v1/graph/ql`, - liveUri: `${protocol}://${location.host}${BASE_PATH}api/v1/live`, + liveUri: CONFIG.LIVE_URI || `${protocol}://${location.host}${BASE_PATH}api/v1/live`, token, }); const plugins = createPluginsService(pluginsConfig); diff --git a/config.js b/config.js index f48159444..d7e247ec3 100644 --- a/config.js +++ b/config.js @@ -135,6 +135,9 @@ const CONFIG = { RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC, RECAPTCHA_SECRET: process.env.TALK_RECAPTCHA_SECRET, + // WEBSOCKET_LIVE_URI is the absolute url to the live endpoint. + WEBSOCKET_LIVE_URI: process.env.TALK_WEBSOCKET_LIVE_URI || null, + //------------------------------------------------------------------------------ // SMTP Server configuration //------------------------------------------------------------------------------ diff --git a/docs/_data/navigation.yml b/docs/_data/navigation.yml index 7226f6003..9fe39d103 100755 --- a/docs/_data/navigation.yml +++ b/docs/_data/navigation.yml @@ -38,6 +38,8 @@ docs: url: /docs/running/plugins/ - title: "Database Migrations" url: /docs/running/migrations/ + - title: "Persistence" + url: /docs/running/persistence/ - title: "Architecture" url: /docs/architecture/ children: diff --git a/docs/_docs/00-01-faq.md b/docs/_docs/00-01-faq.md index 01172f84a..e114fce72 100644 --- a/docs/_docs/00-01-faq.md +++ b/docs/_docs/00-01-faq.md @@ -91,7 +91,7 @@ source process), then: ``` cd docs -docker run --rm --volume=$PWD:/srv/jekyll -p 127.0.0.1:4000:4000 -it jekyll/jekyll:pages jekyll serve +docker run --rm --volume=$PWD:/srv/jekyll -p 127.0.0.1:4000:4000 -it jekyll/jekyll:pages bash -c "bundle install && jekyll serve" ``` You can edit the files in docs with any editor and view the live updates in a diff --git a/docs/_docs/02-01-configuration.md b/docs/_docs/02-01-configuration.md index df790b24b..6ffb9afc0 100644 --- a/docs/_docs/02-01-configuration.md +++ b/docs/_docs/02-01-configuration.md @@ -53,6 +53,7 @@ These are only used during the webpack build. - `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database. #### Advanced +{:.no_toc} - `TALK_REDIS_RECONNECTION_MAX_ATTEMPTS` (_optional_) - the amount of attempts that a redis connection will attempt to reconnect before aborting with an @@ -76,11 +77,19 @@ These are only used during the webpack build. - `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - When `TRUE`, disables the dynamic setup endpoint. (Default `FALSE`) #### Advanced +{:.no_toc} - `TALK_ROOT_URL_MOUNT_PATH` (_optional_) - when set to `TRUE`, the routes will be mounted onto the `` component of the `TALK_ROOT_URL`. You would use this when your upstream proxy cannot strip the prefix from the url. (Default `FALSE`) +- `TALK_WEBSOCKET_LIVE_URI` (_optional_) - used to override the location to + connect to the websocket endpoint to potentially another host. This should + be used when you need to route websocket requests out of your CDN in order to + serve traffic more efficiently for websockets. **Warning: if used without + managing the auth state manually, auth cannot be persisted, for further + information refer to the [Persistence Documentation]({{ "/docs/running/persistence/" | absolute_url }})** + (Default `${ssl ? 'ws' : 'wss'}://${location.host}${TALK_ROOT_URL_MOUNT_PATH}api/v1/live`) ### Word Filter @@ -105,6 +114,7 @@ variable. Refer to the [Secrets Documentation]({{ "/docs/running/secrets/" | abs on the contents of those variables.** #### Advanced +{:.no_toc} These are advanced settings for fine tuning the auth integration, and is not needed in most situations. diff --git a/docs/_docs/02-05-persistence.md b/docs/_docs/02-05-persistence.md new file mode 100644 index 000000000..fc67a9970 --- /dev/null +++ b/docs/_docs/02-05-persistence.md @@ -0,0 +1,45 @@ +--- +title: User Persistence +permalink: /docs/running/persistence/ +--- + +One of the biggest problems on the internet today is the proliferation of +sophisticated tracking systems and the outcome of invading user's privacy. This +has had quite a big impact on our design of Talk, as we've had to work around +the safeguards that are there to keep your data safe while still allowing our +application to run smoothly on the page it's embedded on. + +--- + +**Problem**: Safari has inconsistent behavior around localStorage when used +within an iFrame. + +**Solution**: We set a cookie instead when Safari is detected to store the auth +state. + +--- + +**Problem**: Safari's default privacy settings block cookies from domains that +do not match the current domain. + +**Solution**: When using Talk's built in auth, we will open a pop-out when +setting the cookie, so that the domain of the setting domain matches the issuer. + +--- + +**Problem**: When using a different domain for websockets, and using the built +in auth solution, cookies are not set on that domain for use with Safari. + +**No Solution Exists**: It is our expectation that for users that must deploy +Talk in environments that must run the websockets out of a separate domain will +use and integrate their own auth solution. During the login process in Talk, +users submit their user credentials to an auth endpoint, and receive a token +back, or for Safari, a cookie. Aggressive defaults in Safari make it not +possible to have one domain set cookies for another domain during this process. +This results in a situation where we have no way to persist the auth credentials +for this specific situation for the time being. + +--- + +It's important to understand that these problems are in fact there to improve +privacy for end users. \ No newline at end of file diff --git a/routes/admin/index.js b/routes/admin/index.js index 30bb69f5e..ab6189e85 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -1,7 +1,8 @@ const express = require('express'); const router = express.Router(); const { - RECAPTCHA_PUBLIC + RECAPTCHA_PUBLIC, + WEBSOCKET_LIVE_URI, } = require('../../config'); // Get /email-confirmation expects a signed JWT in the hash @@ -20,7 +21,8 @@ router.get('/password-reset', (req, res) => { router.get('*', (req, res) => { const data = { - TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC + TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC, + LIVE_URI: WEBSOCKET_LIVE_URI, }; res.render('admin', {data}); diff --git a/routes/embed/index.js b/routes/embed/index.js index 0aaa9ff9c..256d03b30 100644 --- a/routes/embed/index.js +++ b/routes/embed/index.js @@ -2,25 +2,24 @@ const express = require('express'); const router = express.Router(); const SettingsService = require('../../services/settings'); const { - RECAPTCHA_PUBLIC + RECAPTCHA_PUBLIC, + WEBSOCKET_LIVE_URI, } = require('../../config'); -router.use('/:embed', (req, res, next) => { +router.use('/:embed', async (req, res, next) => { switch (req.params.embed) { - case 'stream': - return SettingsService.retrieve() - .then(({customCssUrl}) => { - const data = { - TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC - }; + case 'stream': { + const {customCssUrl} = await SettingsService.retrieve(); + const data = { + TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC, + LIVE_URI: WEBSOCKET_LIVE_URI, + }; - return res.render('embed/stream', {customCssUrl, data}); - }); - default: - - // will return a 404. - return next(); + return res.render('embed/stream', {customCssUrl, data}); } + } + + return next(); }); module.exports = router; From 4a9b24f0964ce0882e3fa8ca796d96d228571228 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 5 Sep 2017 14:08:43 -0600 Subject: [PATCH 2/3] added more docs related to cookies --- docs/_docs/02-05-persistence.md | 9 ++++++-- docs/_docs/04-04-plugins-server.md | 34 +++++++++++++++--------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/docs/_docs/02-05-persistence.md b/docs/_docs/02-05-persistence.md index fc67a9970..a1d3a5e2f 100644 --- a/docs/_docs/02-05-persistence.md +++ b/docs/_docs/02-05-persistence.md @@ -41,5 +41,10 @@ for this specific situation for the time being. --- -It's important to understand that these problems are in fact there to improve -privacy for end users. \ No newline at end of file +If you are using a custom auth solution, (Which involves providing the user's +jwt token via the `auth_token` parameter on the call to +`Talk.render(... {auth_token})` and creating the +[tokenUserNotFound]({{ "/docs/plugins/server/" | absolute_url }}) hook to lookup +that user) then concerns related to cookies/localStorage are moot! As it isn't +necessary for Talk to maintain any state beyond what is passed to it via the +pym bridge. \ No newline at end of file diff --git a/docs/_docs/04-04-plugins-server.md b/docs/_docs/04-04-plugins-server.md index f807da141..e639cd308 100644 --- a/docs/_docs/04-04-plugins-server.md +++ b/docs/_docs/04-04-plugins-server.md @@ -85,7 +85,7 @@ configure the context plugin before it would be mounted at `context.plugins`. This plugin above would mount at: `context.plugins.Slack`, or, if you're using [object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`. -##### `Sort` +##### Field: `Sort` A special context hook, `Sort` will allow plugin authors to provide new methods to sort data. An example is as follows: @@ -269,22 +269,6 @@ The function is async, and should return the user object that was created in the database, or null if the user wasn't found. The `jwt` parameter of the object is the unpacked token, while `token` is the original jwt token string. -### Routes - -#### Field: `router` - -```js -(router) => { - router.get('/api/v1/people', (req, res) => { - res.json({people: [{name: 'Bob'}]}); - }); -} -``` - -The Router hook allows you to create a function that accepts the base express -router where you can mount any amount of middleware/routes to do any form of -action needed by external applications. - #### Field: `tags` The tags hook allows a plugin to define tags that are code controlled (added @@ -309,6 +293,22 @@ on how to create a hook for the `OFF_TOPIC` name: You can refer to `models/schema/tag.js` for the available schema to match when creating models to enable/disable specific features. +### Routes + +#### Field: `router` + +```js +(router) => { + router.get('/api/v1/people', (req, res) => { + res.json({people: [{name: 'Bob'}]}); + }); +} +``` + +The Router hook allows you to create a function that accepts the base express +router where you can mount any amount of middleware/routes to do any form of +action needed by external applications. + ### Authorization middleware The following example creates the requisite callback route and passport From 96e37ac7254b3d547f2dd9bbd2011d24ab794c51 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 7 Sep 2017 10:58:58 -0600 Subject: [PATCH 3/3] moved config loading to bootstrap --- client/coral-framework/constants/config.js | 3 -- client/coral-framework/services/bootstrap.js | 39 ++++++++++++++++++-- 2 files changed, 36 insertions(+), 6 deletions(-) delete mode 100644 client/coral-framework/constants/config.js diff --git a/client/coral-framework/constants/config.js b/client/coral-framework/constants/config.js deleted file mode 100644 index 81eab50e2..000000000 --- a/client/coral-framework/constants/config.js +++ /dev/null @@ -1,3 +0,0 @@ -const configElement = document.querySelector('#data'); - -export const CONFIG = JSON.parse(configElement ? configElement.textContent : '{}'); diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index eed8313d4..a3ae5b18e 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -8,7 +8,6 @@ import thunk from 'redux-thunk'; import {loadTranslations} from './i18n'; import bowser from 'bowser'; import {BASE_PATH} from 'coral-framework/constants/url'; -import {CONFIG} from 'coral-framework/constants/config'; import {createPluginsService} from './plugins'; import {createNotificationService} from './notification'; import {createGraphQLRegistry} from './graphqlRegistry'; @@ -16,6 +15,25 @@ import globalFragments from 'coral-framework/graphql/fragments'; import {createStorage} from 'coral-framework/services/storage'; import {createHistory} from 'coral-framework/services/history'; +/** + * getStaticConfiguration will return a singleton of the static configuration + * object provided via a JSON DOM element. + */ +const getStaticConfiguration = (() => { + let staticConfiguration = null; + return () => { + if (staticConfiguration != null) { + return staticConfiguration; + } + + const configElement = document.querySelector('#data'); + + staticConfiguration = JSON.parse(configElement ? configElement.textContent : '{}'); + + return staticConfiguration; + }; +})(); + /** * getAuthToken returns the active auth token or null * Note: this method does not have access to the cookie based token used by @@ -50,7 +68,6 @@ const getAuthToken = (store, storage) => { * @return {Object} context */ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification} = {}) { - const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); const storage = createStorage(); const history = createHistory(BASE_PATH); @@ -64,13 +81,28 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. return getAuthToken(store, storage); }; + const rest = createRestClient({ uri: `${BASE_PATH}api/v1`, token, }); + + // Try to get an overrided liveUri from the static config, if none is found, + // build it. + let {LIVE_URI: liveUri} = getStaticConfiguration(); + if (liveUri == null) { + + // The protocol must match the origin protocol, secure/insecure. + const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; + + // Compose the live url from this protocol, the current host + base path + // with the live path appended to it. + liveUri = `${protocol}://${location.host}${BASE_PATH}api/v1/live`; + } + const client = createClient({ uri: `${BASE_PATH}api/v1/graph/ql`, - liveUri: CONFIG.LIVE_URI || `${protocol}://${location.host}${BASE_PATH}api/v1/live`, + liveUri, token, }); const plugins = createPluginsService(pluginsConfig); @@ -80,6 +112,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi // Use default notification service (pym based) notification = createNotificationService(pym); } + const context = { client, pym,