diff --git a/package-lock.json b/package-lock.json
index c579a6649..1da365076 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1772,6 +1772,15 @@
"integrity": "sha512-p+gNRe4RPjpl1lTBUomFJ42P8ymArH/P93DFJ0iY873BJ4ZmogcKc6TbHgZQmtQMsy3jxcAo0HcTjidXwo8uKg==",
"dev": true
},
+ "@types/cors": {
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.4.tgz",
+ "integrity": "sha512-ipZjBVsm2tF/n8qFGOuGBkUij9X9ZswVi9G3bx/6dz7POpVa6gVHcj1wsX/LVEn9MMF41fxK/PnZPPoTD1UFPw==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*"
+ }
+ },
"@types/cross-spawn": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.0.tgz",
@@ -6861,6 +6870,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
+ "cors": {
+ "version": "2.8.4",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz",
+ "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=",
+ "requires": {
+ "object-assign": "^4",
+ "vary": "^1"
+ }
+ },
"cosmiconfig": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-3.1.0.tgz",
diff --git a/package.json b/package.json
index b8ee8623b..e02695d7f 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"cheerio": "^1.0.0-rc.2",
"consolidate": "0.14.0",
"convict": "^4.3.1",
+ "cors": "^2.8.4",
"dataloader": "^1.4.0",
"dotenv": "^6.0.0",
"dotenv-expand": "^4.2.0",
@@ -118,6 +119,7 @@
"@types/compression-webpack-plugin": "^0.4.2",
"@types/consolidate": "0.0.34",
"@types/convict": "^4.2.0",
+ "@types/cors": "^2.8.4",
"@types/cross-spawn": "^6.0.0",
"@types/dompurify": "0.0.31",
"@types/dotenv": "^4.0.3",
diff --git a/src/core/build/createWebpackConfig.ts b/src/core/build/createWebpackConfig.ts
index 8e0b32ddd..3aad5a28e 100644
--- a/src/core/build/createWebpackConfig.ts
+++ b/src/core/build/createWebpackConfig.ts
@@ -9,6 +9,7 @@ import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin";
import UglifyJsPlugin from "uglifyjs-webpack-plugin";
import webpack, { Configuration } from "webpack";
import ManifestPlugin from "webpack-manifest-plugin";
+import CDNWebpackPlugin from "./plugins/CDNWebpackPlugin";
import paths from "./paths";
@@ -438,12 +439,14 @@ export default function createWebpackConfig({
stream: [
// We ship polyfills by default
paths.appPolyfill,
+ paths.appPublicPath,
...devServerEntries,
paths.appStreamIndex,
],
auth: [
// We ship polyfills by default
paths.appPolyfill,
+ paths.appPublicPath,
...devServerEntries,
paths.appAuthIndex,
// Remove deactivated entries.
@@ -451,12 +454,14 @@ export default function createWebpackConfig({
install: [
// We ship polyfills by default
paths.appPolyfill,
+ paths.appPublicPath,
...devServerEntries,
paths.appInstallIndex,
],
admin: [
// We ship polyfills by default
paths.appPolyfill,
+ paths.appPublicPath,
...devServerEntries,
paths.appAdminIndex,
],
@@ -495,6 +500,9 @@ export default function createWebpackConfig({
inject: "body",
...htmlWebpackConfig,
}),
+ // Inject the pieces we need here to resolve all the now relative url's
+ // against the CDN if it's provided.
+ new CDNWebpackPlugin({ production: isProduction }),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
//
diff --git a/src/core/build/paths.ts b/src/core/build/paths.ts
index 937f3da1c..d89eb3362 100644
--- a/src/core/build/paths.ts
+++ b/src/core/build/paths.ts
@@ -17,6 +17,7 @@ export default {
appSrc: resolveSrc("."),
appTsconfig: resolveSrc("core/client/tsconfig.json"),
appPolyfill: resolveSrc("core/build/polyfills.js"),
+ appPublicPath: resolveSrc("core/build/publicPath.js"),
appLocales: resolveSrc("locales"),
appThemeVariables: resolveSrc("core/client/ui/theme/variables.ts"),
appThemeVariablesCSS: resolveSrc("core/client/ui/theme/variables.css"),
diff --git a/src/core/build/plugins/CDNWebpackPlugin.ts b/src/core/build/plugins/CDNWebpackPlugin.ts
new file mode 100644
index 000000000..21f17405d
--- /dev/null
+++ b/src/core/build/plugins/CDNWebpackPlugin.ts
@@ -0,0 +1,66 @@
+import { Hooks } from "html-webpack-plugin";
+import { Compiler, Plugin } from "webpack";
+
+export interface CDNWebpackPluginOptions {
+ production: boolean;
+}
+
+export default class CDNWebpackPlugin implements Plugin {
+ private production: boolean;
+
+ constructor({ production }: CDNWebpackPluginOptions) {
+ this.production = production;
+ }
+
+ private prefixAttribute(attr: string | boolean) {
+ if (!attr || typeof attr !== "string" || !attr.startsWith("/")) {
+ return attr;
+ }
+
+ return "{{ staticURI }}" + attr;
+ }
+
+ private prefixTag = (tag: {
+ tagName: string;
+ attributes: Record;
+ }) => {
+ switch (tag.tagName) {
+ case "link":
+ tag.attributes.href = this.prefixAttribute(tag.attributes.href);
+ break;
+ case "script":
+ tag.attributes.src = this.prefixAttribute(tag.attributes.src);
+ break;
+ }
+ };
+
+ public apply = (compiler: Compiler) => {
+ if (!this.production) {
+ return;
+ }
+
+ compiler.hooks.compilation.tap("CDNWebpackPlugin", compilation => {
+ (compilation.hooks as Hooks).htmlWebpackPluginAlterAssetTags.tapAsync(
+ "CDNWebpackPlugin",
+ (htmlPluginData, cb) => {
+ // Prefix all the asset's url's with the template.
+ htmlPluginData.head.forEach(this.prefixTag);
+ htmlPluginData.body.forEach(this.prefixTag);
+
+ // Insert the public path reference.
+ htmlPluginData.body.unshift({
+ tagName: "script",
+ attributes: {
+ type: "application/json",
+ id: "config",
+ },
+ innerHTML: "{{ staticURI | dump | safe }}",
+ voidTag: false,
+ });
+
+ return cb(null, htmlPluginData);
+ }
+ );
+ });
+ };
+}
diff --git a/src/core/build/publicPath.js b/src/core/build/publicPath.js
new file mode 100644
index 000000000..e3e6fd334
--- /dev/null
+++ b/src/core/build/publicPath.js
@@ -0,0 +1,3 @@
+__webpack_public_path__ = JSON.parse(
+ document.getElementById("config").innerText
+);
diff --git a/src/core/common/config.ts b/src/core/common/config.ts
index e02e44558..1ae579a03 100644
--- a/src/core/common/config.ts
+++ b/src/core/common/config.ts
@@ -27,6 +27,20 @@ convict.addFormat({
},
});
+// Add a custom format for the optional-url.
+convict.addFormat({
+ name: "optional-url",
+ validate: (url: string) => {
+ if (url) {
+ Joi.assert(url, Joi.string().uri());
+ }
+ },
+ // Ensure that there is no ending slash.
+ coerce: (url: string) => {
+ return url.replace(/\/$/, "");
+ },
+});
+
const config = convict({
env: {
doc: "The application environment.",
@@ -54,6 +68,7 @@ const config = convict({
default: "mongodb://127.0.0.1:27017/talk",
env: "MONGODB_URI",
arg: "mongodb",
+ sensitive: true,
},
redis: {
doc: "The Redis database to connect to.",
@@ -61,6 +76,7 @@ const config = convict({
default: "redis://127.0.0.1:6379",
env: "REDIS_URI",
arg: "redis",
+ sensitive: true,
},
signing_secret: {
doc: "",
@@ -68,6 +84,7 @@ const config = convict({
default: "keyboard cat", // TODO: (wyattjoh) evaluate best solution
env: "SIGNING_SECRET",
arg: "signingSecret",
+ sensitive: true,
},
signing_algorithm: {
doc: "",
@@ -93,6 +110,13 @@ const config = convict({
env: "LOGGING_LEVEL",
arg: "logging",
},
+ static_uri: {
+ doc: "The URL that static assets will be hosted from",
+ format: "optional-url",
+ default: "",
+ env: "STATIC_URI",
+ arg: "staticUri",
+ },
disable_tenant_caching: {
doc:
"Disables the tenant caching, all tenants will be loaded from MongoDB each time it's needed",
diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts
index baaae0a9c..c4ae82ab3 100644
--- a/src/core/server/app/index.ts
+++ b/src/core/server/app/index.ts
@@ -1,4 +1,5 @@
import cons from "consolidate";
+import cors from "cors";
import { Express } from "express";
import http from "http";
import { Redis } from "ioredis";
@@ -56,6 +57,9 @@ export async function createApp(options: AppOptions): Promise {
})
);
+ // Enable CORS headers for media assets, font's require them.
+ parent.use("/assets/media", cors());
+
// Static Files
parent.use("/assets", cacheHeadersMiddleware("1w"), serveStatic);
diff --git a/src/core/server/app/middleware/cacheHeaders.ts b/src/core/server/app/middleware/cacheHeaders.ts
index f11bfd901..bc6731169 100644
--- a/src/core/server/app/middleware/cacheHeaders.ts
+++ b/src/core/server/app/middleware/cacheHeaders.ts
@@ -1,7 +1,7 @@
import { RequestHandler } from "express";
import ms from "ms";
-export const nocacheMiddleware: RequestHandler = (req, res, next) => {
+export const noCacheMiddleware: RequestHandler = (req, res, next) => {
// Set cache control headers to prevent browsers/cdn's from caching these
// requests.
res.set({ "Cache-Control": "no-cache, no-store, must-revalidate" });
@@ -9,10 +9,12 @@ export const nocacheMiddleware: RequestHandler = (req, res, next) => {
next();
};
-export const cacheHeadersMiddleware = (duration?: string): RequestHandler => {
+export const cacheHeadersMiddleware = (
+ duration?: string | false
+): RequestHandler => {
const maxAge = duration ? Math.floor(ms(duration) / 1000) : false;
if (!maxAge) {
- return nocacheMiddleware;
+ return noCacheMiddleware;
}
return (req, res, next) => {
diff --git a/src/core/server/app/router/client.ts b/src/core/server/app/router/client.ts
index d75e3ad75..9b7cab8c2 100644
--- a/src/core/server/app/router/client.ts
+++ b/src/core/server/app/router/client.ts
@@ -11,10 +11,17 @@ export interface ClientTargetHandlerOptions {
/**
* cacheDuration is the cache duration that a given request should be cached for.
*/
- cacheDuration?: string;
+ cacheDuration?: string | false;
+
+ /**
+ * staticURI is prepended to the static url's that are included on the static
+ * pages.
+ */
+ staticURI: string;
}
export function createClientTargetRouter({
+ staticURI,
view,
cacheDuration = "1h",
}: ClientTargetHandlerOptions) {
@@ -22,7 +29,7 @@ export function createClientTargetRouter({
const router = express.Router();
router.get("/", cacheHeadersMiddleware(cacheDuration), (req, res) =>
- res.render(view)
+ res.render(view, { staticURI })
);
return router;
diff --git a/src/core/server/app/router/index.ts b/src/core/server/app/router/index.ts
index 57bda4b0e..27444b3c5 100644
--- a/src/core/server/app/router/index.ts
+++ b/src/core/server/app/router/index.ts
@@ -1,7 +1,7 @@
import express, { Router } from "express";
import { AppOptions } from "talk-server/app";
-import { nocacheMiddleware } from "talk-server/app/middleware/cacheHeaders";
+import { noCacheMiddleware } from "talk-server/app/middleware/cacheHeaders";
import { installedMiddleware } from "talk-server/app/middleware/installed";
import playground from "talk-server/app/middleware/playground";
import { RouterOptions } from "talk-server/app/router/types";
@@ -14,42 +14,56 @@ export async function createRouter(app: AppOptions, options: RouterOptions) {
// Create a router.
const router = express.Router();
- router.use("/api", nocacheMiddleware, await createAPIRouter(app, options));
+ router.use("/api", noCacheMiddleware, await createAPIRouter(app, options));
// Attach the GraphiQL if enabled.
if (app.config.get("enable_graphiql")) {
attachGraphiQL(router, app);
}
+ const staticURI = app.config.get("static_uri");
+
// Add the embed targets.
- router.use("/embed/stream", createClientTargetRouter({ view: "stream" }));
- router.use("/embed/auth", createClientTargetRouter({ view: "auth" }));
+ router.use(
+ "/embed/stream",
+ createClientTargetRouter({ staticURI, view: "stream" })
+ );
+ router.use(
+ "/embed/auth",
+ createClientTargetRouter({ staticURI, view: "auth", cacheDuration: false })
+ );
// Add the standalone targets.
router.use(
"/admin",
+ // If we aren't already installed, redirect the user to the install page.
installedMiddleware({
tenantCache: app.tenantCache,
}),
- createClientTargetRouter({ view: "admin" })
+ createClientTargetRouter({ staticURI, view: "admin", cacheDuration: false })
);
router.use(
"/install",
+ // If we're already installed, redirect the user to the admin page.
installedMiddleware({
tenantCache: app.tenantCache,
redirectIfInstalled: true,
redirectURL: "/admin",
}),
- createClientTargetRouter({ view: "install", cacheDuration: "" })
+ createClientTargetRouter({
+ staticURI,
+ view: "install",
+ cacheDuration: false,
+ })
);
// Handle the root path.
router.get(
"/",
+ // Redirect the user to the install page if they are not, otherwise redirect
+ // them to the admin.
installedMiddleware({ tenantCache: app.tenantCache }),
- (req, res, next) => {
- res.redirect("/admin");
- }
+ (req, res, next) => res.redirect("/admin")
);
return router;
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 5fbda5731..7529c2be7 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -38,9 +38,12 @@ class Server {
constructor(options: ServerOptions) {
this.parentApp = express();
+
+ // Load the configuration.
this.config = config
.load(options.config || {})
.validate({ allowed: "strict" });
+ logger.debug({ config: this.config.toString() }, "loaded configuration");
// Load the graph schemas.
this.schemas = {