diff --git a/app.js b/app.js index c5507da2e..b3400b939 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,6 @@ const express = require('express'); +const nunjucks = require('nunjucks'); +const cons = require('consolidate'); const trace = require('./middleware/trace'); const logging = require('./middleware/logging'); const path = require('path'); @@ -72,7 +74,26 @@ app.use( // VIEW CONFIGURATION //============================================================================== -app.set('views', path.join(__dirname, 'views')); +// configure the default views directory. +const views = path.join(__dirname, 'views'); +app.set('views', views); + +// reconfigure nunjucks. +cons.requires.nunjucks = nunjucks.configure(views, { + autoescape: true, + trimBlocks: true, + lstripBlocks: true, + watch: process.env.NODE_ENV === 'development', +}); + +// assign the nunjucks engine to .njk files. +app.engine('njk', cons.nunjucks); + +// assign the ejs engine to .ejs and .html files. +app.engine('ejs', cons.ejs); +app.engine('html', cons.ejs); + +// set .ejs as the default extension. app.set('view engine', 'ejs'); //============================================================================== diff --git a/config.js b/config.js index 6a1f999a7..77ed4ab2b 100644 --- a/config.js +++ b/config.js @@ -48,6 +48,10 @@ const CONFIG = { // request all of the records. Otherwise, minimum limits of 0 are enforced. ALLOW_NO_LIMIT_QUERIES: process.env.TALK_ALLOW_NO_LIMIT_QUERIES === 'TRUE', + // ENABLE_STRICT_CSP enables strict CSP enforcement, and will enforce as well + // as report CSP violations. + ENABLE_STRICT_CSP: process.env.TALK_ENABLE_STRICT_CSP === 'TRUE', + // LOGGING_LEVEL specifies the logging level used by the bunyan logger. LOGGING_LEVEL: ['fatal', 'error', 'warn', 'info', 'debug', 'trace'].includes( process.env.TALK_LOGGING_LEVEL diff --git a/docs/source/02-02-advanced-configuration.md b/docs/source/02-02-advanced-configuration.md index 5b638401d..c4f86dbb8 100644 --- a/docs/source/02-02-advanced-configuration.md +++ b/docs/source/02-02-advanced-configuration.md @@ -497,6 +497,14 @@ tracing of GraphQL requests. **Note: Apollo Engine is a premium service, charges may apply.** +## TALK_ENABLE_STRICT_CSP + +Setting this to `TRUE` will enforce the [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) +(or CSP). By default, this configuration is set to +[report only](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP#Testing_your_policy) +where the policy is not enforced, but any violations are reported to a provided +URI. (Default `FALSE`) + ## ALLOW_NO_LIMIT_QUERIES Setting this to `TRUE` will allow queries to execute without a limit (returns diff --git a/middleware/contentSecurityPolicy.js b/middleware/contentSecurityPolicy.js new file mode 100644 index 000000000..c47faa74e --- /dev/null +++ b/middleware/contentSecurityPolicy.js @@ -0,0 +1,55 @@ +const helmet = require('helmet'); +const { WEBSOCKET_LIVE_URI, ENABLE_STRICT_CSP } = require('../config'); +const { BASE_PATH, BASE_URL, STATIC_URL } = require('../url'); +const { URL } = require('url'); + +// websocketUri represents the host where we can connect for websocket requests. +const websocketUri = new URL(WEBSOCKET_LIVE_URI || BASE_URL); +websocketUri.protocol = websocketUri.protocol.startsWith('https') + ? 'wss' + : 'ws'; +const { origin: websocketSrc } = websocketUri; + +// staticSrc represents any static asset hosted on the static host. +const { host: staticSrc } = new URL(STATIC_URL); + +// nonceSrc represents the nonce source that is used to indicate a safe resource +// to load. +const nonceSrc = (req, res) => `'nonce-${res.locals.nonce}'`; + +module.exports = helmet.contentSecurityPolicy({ + directives: { + reportUri: `${BASE_PATH}api/v1/csp`, // report all policy violations to our reporting uri + defaultSrc: ["'none'"], // by default, do not allow anything at all + scriptSrc: [ + "'self'", + 'https://ajax.googleapis.com', // for jquery + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + styleSrc: [ + "'self'", + 'https://maxcdn.bootstrapcdn.com', // for bootstrap css + 'https://fonts.googleapis.com', // for google fonts + 'https://code.getmdl.io', // for mdl css + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + connectSrc: ["'self'", websocketSrc], + fontSrc: [ + "'self'", + 'https://maxcdn.bootstrapcdn.com', // for font-awesome + 'https://fonts.gstatic.com', // for google fonts + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + imgSrc: [ + "'self'", + staticSrc, // for any static files loaded from a cdn + nonceSrc, + ], + }, + browserSniff: false, + // Allow the configuration to disable strict enforcement of CSP. + reportOnly: !ENABLE_STRICT_CSP, +}); diff --git a/middleware/nonce.js b/middleware/nonce.js new file mode 100644 index 000000000..1181fac85 --- /dev/null +++ b/middleware/nonce.js @@ -0,0 +1,9 @@ +const uuid = require('uuid/v4'); + +// nonce is designed to create a random value that can be used in conjunction +// with the csp middleware. +module.exports = (req, res, next) => { + res.locals.nonce = uuid(); + + next(); +}; diff --git a/package.json b/package.json index 5dcb5c6ff..b21e2f78a 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "common-tags": "^1.4.0", "compression": "1.7.1", "compression-webpack-plugin": "^1.0.0", + "consolidate": "0.14.0", "cookie-parser": "^1.4.3", "copy-webpack-plugin": "^4.0.0", "cross-spawn": "^5.1.0", diff --git a/plugins/talk-plugin-notifications/server/router.js b/plugins/talk-plugin-notifications/server/router.js index 68919a936..9185c5aba 100644 --- a/plugins/talk-plugin-notifications/server/router.js +++ b/plugins/talk-plugin-notifications/server/router.js @@ -4,7 +4,7 @@ const { get, isEmpty, reduce } = require('lodash'); module.exports = router => { router.get('/account/unsubscribe-notifications', (req, res) => { - res.render(path.join(__dirname, 'views/unsubscribe-notifications')); + res.render(path.join(__dirname, 'views/unsubscribe-notifications.njk')); }); /** diff --git a/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.ejs b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.ejs deleted file mode 100644 index 129c3115c..000000000 --- a/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.ejs +++ /dev/null @@ -1,64 +0,0 @@ - - - - - <%= t('talk-plugin-notifications.unsubscribe_page.unsubscribe') %> - <%- include(root + '/partials/head') %> - - - - -
-
<%= t('talk-plugin-notifications.unsubscribe_page.token_invalid') %>
- -
- <%= t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') %> - -
-
- - - - \ No newline at end of file diff --git a/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk new file mode 100644 index 000000000..5080277f0 --- /dev/null +++ b/plugins/talk-plugin-notifications/server/views/unsubscribe-notifications.njk @@ -0,0 +1,68 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('talk-plugin-notifications.unsubscribe_page.unsubscribe') }}{% endblock %} + +{% block css %} +{{ super() }} + +{% endblock %} + +{% block html %} +
+
{{ t('talk-plugin-notifications.unsubscribe_page.token_invalid') }}
+
{{ t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') }}
+
+ {{ t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') }} + +
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/plugins/talk-plugin-profile-data/server/router.js b/plugins/talk-plugin-profile-data/server/router.js index 6d8f47d03..6bd165e64 100644 --- a/plugins/talk-plugin-profile-data/server/router.js +++ b/plugins/talk-plugin-profile-data/server/router.js @@ -102,7 +102,7 @@ async function loadComments(ctx, userID, archive, latestContentDate) { module.exports = router => { // /account/download will render the download page. router.get('/account/download', (req, res) => { - res.render(path.join(__dirname, 'views', 'download')); + res.render(path.join(__dirname, 'views', 'download.njk')); }); // /api/v1/account/download will send back a zipped archive of the users diff --git a/plugins/talk-plugin-profile-data/server/views/download.ejs b/plugins/talk-plugin-profile-data/server/views/download.ejs deleted file mode 100644 index 260badf3a..000000000 --- a/plugins/talk-plugin-profile-data/server/views/download.ejs +++ /dev/null @@ -1,56 +0,0 @@ - - - - <%= t('download_landing.download_your_account') %> - <%- include(root + '/partials/account') %> - - -
-
-

<%= t('download_landing.download_your_account') %>

-

<%= t('download_landing.download_details') %>

-

<%= t('download_landing.all_information_included') %>

- -
-
- -
-
-
- - - - diff --git a/plugins/talk-plugin-profile-data/server/views/download.njk b/plugins/talk-plugin-profile-data/server/views/download.njk new file mode 100644 index 000000000..ee9c94272 --- /dev/null +++ b/plugins/talk-plugin-profile-data/server/views/download.njk @@ -0,0 +1,56 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('download_landing.download_your_account') }}{% endblock %} + +{% block html %} +
+
+

{{ t('download_landing.download_your_account') }}

+

{{ t('download_landing.download_details') }}

+

{{ t('download_landing.all_information_included') }}

+ +
+
+ +
+
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/routes/account/index.js b/routes/account/index.js index 70e62accc..37b26e344 100644 --- a/routes/account/index.js +++ b/routes/account/index.js @@ -2,11 +2,11 @@ const express = require('express'); const router = express.Router(); router.get('/email/confirm', (req, res) => { - res.render('account/email/confirm'); + res.render('account/email/confirm.njk'); }); router.get('/password/reset', (req, res) => { - res.render('account/password/reset'); + res.render('account/password/reset.njk'); }); module.exports = router; diff --git a/routes/admin/index.js b/routes/admin/index.js index d00ef8642..1a9993cfd 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); router.get('*', (req, res) => { - res.render('admin'); + res.render('admin.njk'); }); module.exports = router; diff --git a/routes/api/v1/csp.js b/routes/api/v1/csp.js new file mode 100644 index 000000000..ad1101987 --- /dev/null +++ b/routes/api/v1/csp.js @@ -0,0 +1,33 @@ +const express = require('express'); +const Joi = require('joi'); +const { logger } = require('../../../services/logging'); +const router = express.Router(); + +const schema = Joi.object().keys({ + 'csp-report': Joi.object().keys({ + 'document-uri': Joi.string(), + referrer: Joi.string(), + 'blocked-uri': Joi.string(), + 'violated-directive': Joi.string(), + 'original-policy': Joi.string(), + }), +}); + +const json = express.json({ type: 'application/csp-report' }); + +router.post('/', json, async (req, res, next) => { + const { value, error: err } = Joi.validate(req.body, schema, { + stripUnknown: true, + presence: 'required', + }); + if (err) { + res.status(400).end(); + return; + } + + logger.error({ report: value }, 'csp violation reported'); + + res.status(202).end(); +}); + +module.exports = router; diff --git a/routes/api/v1/graph.js b/routes/api/v1/graph.js index 40e87415b..0660755ea 100644 --- a/routes/api/v1/graph.js +++ b/routes/api/v1/graph.js @@ -10,7 +10,7 @@ router.use('/ql', apollo.graphqlExpress(createGraphOptions)); if (process.env.NODE_ENV !== 'production') { // Interactive graphiql interface. router.use('/iql', staticTemplate, (req, res) => { - res.render('api/graphiql', { + res.render('api/graphiql.njk', { endpointURL: 'api/v1/graph/ql', }); }); diff --git a/routes/api/v1/index.js b/routes/api/v1/index.js index 46d5c0278..7f715c854 100644 --- a/routes/api/v1/index.js +++ b/routes/api/v1/index.js @@ -9,6 +9,7 @@ router.get('/', (req, res) => { }); router.use('/account', require('./account')); +router.use('/csp', require('./csp')); router.use('/assets', require('./assets')); router.use('/auth', require('./auth')); router.use('/graph', require('./graph')); diff --git a/routes/dev/assets.js b/routes/dev/assets.js index aea0628e3..df59f21a0 100644 --- a/routes/dev/assets.js +++ b/routes/dev/assets.js @@ -11,7 +11,7 @@ router.get('/id/:asset_id', async (req, res, next) => { throw new ErrNotFound(); } - res.render('dev/article', { + res.render('dev/article.njk', { title: asset.title, asset_id: asset.id, asset_url: asset.url, @@ -28,7 +28,7 @@ router.get('/random', (req, res) => { }); router.get('/title/:asset_title', (req, res) => { - res.render('dev/article', { + res.render('dev/article.njk', { title: req.params.asset_title.split('-').join(' '), asset_url: '', asset_id: null, @@ -48,7 +48,7 @@ router.get('/', async (req, res, next) => { Asset.count(), ]); - res.render('dev/articles', { + res.render('dev/articles.njk', { skip, limit, count, diff --git a/routes/dev/index.js b/routes/dev/index.js index 58daca583..580e702fd 100644 --- a/routes/dev/index.js +++ b/routes/dev/index.js @@ -12,7 +12,7 @@ router.get('/', staticTemplate, async (req, res) => { await SetupService.isAvailable(); return res.redirect(url.resolve(MOUNT_PATH, 'admin/install')); } catch (e) { - return res.render('dev/article', { + return res.render('dev/article.njk', { title: 'Coral Talk', asset_url: '', asset_id: '', diff --git a/routes/embed/index.js b/routes/embed/index.js index 695ffe609..7456a5b71 100644 --- a/routes/embed/index.js +++ b/routes/embed/index.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); router.use('/stream', (req, res) => { - res.render('embed/stream'); + res.render('embed/stream.njk'); }); module.exports = router; diff --git a/routes/index.js b/routes/index.js index 2dbfd51ff..5183e2881 100644 --- a/routes/index.js +++ b/routes/index.js @@ -9,7 +9,9 @@ const path = require('path'); const compression = require('compression'); const plugins = require('../services/plugins'); const staticTemplate = require('../middleware/staticTemplate'); -const staticMiddleware = require('express-static-gzip'); +const contentSecurityPolicy = require('../middleware/contentSecurityPolicy'); +const nonce = require('../middleware/nonce'); +const staticServer = require('express-static-gzip'); const { DISABLE_STATIC_SERVER } = require('../config'); const { passport } = require('../services/passport'); const { MOUNT_PATH } = require('../url'); @@ -48,7 +50,7 @@ if (!DISABLE_STATIC_SERVER) { if (process.env.NODE_ENV === 'production') { router.use( '/static', - staticMiddleware(dist, { + staticServer(dist, { indexFromEmptyFile: false, enableBrotli: true, customCompressions: [ @@ -74,10 +76,12 @@ router.use(compression()); // STATIC ROUTES //============================================================================== -router.use('/admin', staticTemplate, require('./admin')); -router.use('/account', staticTemplate, require('./account')); -router.use('/login', staticTemplate, require('./login')); -router.use('/embed', staticTemplate, require('./embed')); +const staticMiddleware = [staticTemplate, nonce, contentSecurityPolicy]; + +router.use('/admin', ...staticMiddleware, require('./admin')); +router.use('/account', ...staticMiddleware, require('./account')); +router.use('/login', ...staticMiddleware, require('./login')); +router.use('/embed', ...staticMiddleware, require('./embed')); //============================================================================== // PASSPORT MIDDLEWARE diff --git a/routes/login/index.js b/routes/login/index.js index b69cb7f5c..e03690dd0 100644 --- a/routes/login/index.js +++ b/routes/login/index.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); router.get('*', (req, res) => { - res.render('login'); + res.render('login.njk'); }); module.exports = router; diff --git a/routes/plugins.js b/routes/plugins.js index 647acdb75..fcc1125b9 100644 --- a/routes/plugins.js +++ b/routes/plugins.js @@ -2,15 +2,13 @@ const express = require('express'); const debug = require('debug')('talk:routes:plugins'); const plugins = require('../services/plugins'); const staticTemplate = require('../middleware/staticTemplate'); +const contentSecurityPolicy = require('../middleware/contentSecurityPolicy'); +const nonce = require('../middleware/nonce'); const router = express.Router(); -// Routes mounted from plugins won't have access to our internal partials -// directory, so we should make that available. -router.use(staticTemplate, (req, res, next) => { - res.locals.root = res.app.get('views'); - next(); -}); +// Apply the middleware. +router.use(staticTemplate, nonce, contentSecurityPolicy); // Inject server route plugins. plugins.get('server', 'router').forEach(plugin => { diff --git a/services/passport.js b/services/passport.js index e4813e419..4b96d2352 100644 --- a/services/passport.js +++ b/services/passport.js @@ -115,13 +115,13 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => { res.locals.encodeJSONForHTML = encodeJSONForHTML; if (err) { - return res.render('auth-callback', { + return res.render('auth-callback.njk', { auth: { err, data: null }, }); } if (!user) { - return res.render('auth-callback', { + return res.render('auth-callback.njk', { auth: { err: new ErrNotAuthorized(), data: null }, }); } @@ -132,7 +132,7 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => { SetTokenForSafari(req, res, token); // We logged in the user! Let's send back the user data. - res.render('auth-callback', { + res.render('auth-callback.njk', { auth: { err: null, data: { user, token } }, }); }; diff --git a/views/account/email/confirm.ejs b/views/account/email/confirm.ejs deleted file mode 100644 index 47bc7773b..000000000 --- a/views/account/email/confirm.ejs +++ /dev/null @@ -1,63 +0,0 @@ - - - - <%= t('confirm_email.email_confirmation') %> - <%- include ../../partials/account %> - - -
-
-

<%= t('confirm_email.email_confirmation') %>

-

<%= t('confirm_email.click_to_confirm') %>

-
-
- -
-
-
- - - - diff --git a/views/account/email/confirm.njk b/views/account/email/confirm.njk new file mode 100644 index 000000000..8e2c9ff05 --- /dev/null +++ b/views/account/email/confirm.njk @@ -0,0 +1,63 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('confirm_email.email_confirmation') }}{% endblock %} + +{% block html %} +
+
+

{{ t('confirm_email.email_confirmation') }}

+

{{ t('confirm_email.click_to_confirm') }}

+
+
+ +
+
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/account/password/reset.ejs b/views/account/password/reset.ejs deleted file mode 100644 index c8b106139..000000000 --- a/views/account/password/reset.ejs +++ /dev/null @@ -1,85 +0,0 @@ - - - - <%= t('password_reset.set_new_password') %> - <%- include ../../partials/account %> - - -
-
-

<%= t('password_reset.set_new_password') %>

-

<%= t('password_reset.change_password_help') %>

-
-
- - - -
-
-
- - - - diff --git a/views/account/password/reset.njk b/views/account/password/reset.njk new file mode 100644 index 000000000..6eee12dd0 --- /dev/null +++ b/views/account/password/reset.njk @@ -0,0 +1,85 @@ +{% extends "templates/account.njk" %} + +{% block title %}{{ t('password_reset.set_new_password') }}{% endblock %} + +{% block html %} +
+
+

{{ t('password_reset.set_new_password') }}

+

{{ t('password_reset.change_password_help') }}

+
+
+ + + +
+
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/admin.ejs b/views/admin.ejs deleted file mode 100644 index 89cc150f7..000000000 --- a/views/admin.ejs +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Talk - Coral Admin - <%- include partials/head %> - - - - <%- include partials/custom-css %> - - -
- - - - diff --git a/views/admin.njk b/views/admin.njk new file mode 100644 index 000000000..1b60f6e76 --- /dev/null +++ b/views/admin.njk @@ -0,0 +1,14 @@ +{% extends "templates/base.njk" %} + +{% block title %}Talk Admin{% endblock %} + +{% block css %} + + + +{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/api/graphiql.ejs b/views/api/graphiql.ejs deleted file mode 100644 index 8007569b3..000000000 --- a/views/api/graphiql.ejs +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - GraphiQL - - <%- include ../partials/dev %> - - - - - - - - - <%- include ../partials/dev-nav %> -
- - - diff --git a/views/api/graphiql.njk b/views/api/graphiql.njk new file mode 100644 index 000000000..b726918f3 --- /dev/null +++ b/views/api/graphiql.njk @@ -0,0 +1,126 @@ +{% extends "templates/development.njk" %} + +{% block title %}GraphiQL{% endblock %} + +{% block css %} +{# Include the base development pieces #} +{{ super() }} + + +{% endblock %} + +{% block js %} + + + + + +{% endblock %} + +{% block html %} +
+{% endblock %} diff --git a/views/auth-callback.ejs b/views/auth-callback.ejs deleted file mode 100644 index e4d2387b7..000000000 --- a/views/auth-callback.ejs +++ /dev/null @@ -1,10 +0,0 @@ - - - - <%- include partials/data %> - - - - - - diff --git a/views/auth-callback.njk b/views/auth-callback.njk new file mode 100644 index 000000000..7119a6255 --- /dev/null +++ b/views/auth-callback.njk @@ -0,0 +1,10 @@ + + + + {% include "partials/data.njk" %} + + + + + + diff --git a/views/dev/article.ejs b/views/dev/article.ejs deleted file mode 100644 index 008d3989e..000000000 --- a/views/dev/article.ejs +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - <%= title %> - <%- include ../partials/dev %> - - - <%- include ../partials/dev-nav %> -
-

<%= title %>

-
- - -
- - diff --git a/views/dev/article.njk b/views/dev/article.njk new file mode 100644 index 000000000..2fb8b3724 --- /dev/null +++ b/views/dev/article.njk @@ -0,0 +1,55 @@ +{% extends "templates/development.njk" %} + +{% block title %}{{ title }}{% endblock %} + +{% block meta %} + + + + + + + + +{% endblock %} + +{% block html %} +
+

{{ title }}

+
+
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/dev/articles.ejs b/views/dev/articles.ejs deleted file mode 100644 index 1b07ed116..000000000 --- a/views/dev/articles.ejs +++ /dev/null @@ -1,40 +0,0 @@ - - - All Assets - <%- include ../partials/dev %> - - - <%- include ../partials/dev-nav %> -
-
-

All Assets

- <%= skip + 1 %> - <%= skip + assets.length %> of <%= count %> Assets -
-
- <% if (skip === 0) { %> Create a random article<% } %> - <% assets.forEach(function (asset) { %> - -
-
<%= asset.title %>
- Created <%= asset.created_at.toLocaleString('en-US') %> -
- <%= asset.url %> -
- <% }) %> -
- <% if (count !== assets.length) { %> - - <% } %> -
- - diff --git a/views/dev/articles.njk b/views/dev/articles.njk new file mode 100644 index 000000000..e204c39fc --- /dev/null +++ b/views/dev/articles.njk @@ -0,0 +1,37 @@ +{% extends "templates/development.njk" %} + +{% block title %}All Assets{% endblock %} + +{% block html %} +
+
+

All Assets

+ {{ skip + 1 }} - {{ skip + assets.length }} of {{ count }} Assets +
+
+ {% if skip === 0 %} Create a random article{% endif %} + {% for asset in assets %} + +
+
{{ asset.title }}
+ Created {{ asset.created_at.toLocaleString('en-US') }} +
+ {{ asset.url }} +
+ {% endfor %} +
+ {% if count !== assets.length %} + + {% endif %} +
+{% endblock %} diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs deleted file mode 100644 index a1c708b55..000000000 --- a/views/embed/stream.ejs +++ /dev/null @@ -1,14 +0,0 @@ - - - - - <%- include ../partials/head %> - - - <%- include ../partials/custom-css %> - - -
- - - diff --git a/views/embed/stream.njk b/views/embed/stream.njk new file mode 100644 index 000000000..ddc620f8e --- /dev/null +++ b/views/embed/stream.njk @@ -0,0 +1,16 @@ +{% extends "templates/base.njk" %} + +{% block title %}Talk{% endblock %} + +{% block css %} + + +{% endblock %} + +{% block html %} +
+{% endblock %} + +{% block js %} + +{% endblock %} diff --git a/views/login.ejs b/views/login.ejs deleted file mode 100644 index 671b25c83..000000000 --- a/views/login.ejs +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - <%- include partials/head %> - - <%- include partials/custom-css %> - - -
- - - - diff --git a/views/login.njk b/views/login.njk new file mode 100644 index 000000000..6a3c1112f --- /dev/null +++ b/views/login.njk @@ -0,0 +1,16 @@ +{% extends "templates/base.njk" %} + +{% block title %}Talk - Login{% endblock %} + +{% block css %} + +{% endblock %} + +{% block html %} +
+{% endblock %} + +{% block js %} + + +{% endblock %} diff --git a/views/partials/account.ejs b/views/partials/account.ejs deleted file mode 100644 index ff2166816..000000000 --- a/views/partials/account.ejs +++ /dev/null @@ -1,4 +0,0 @@ -<%- include head %> - - -<%- include custom-css %> diff --git a/views/partials/custom-css.ejs b/views/partials/custom-css.ejs deleted file mode 100644 index d453c37e1..000000000 --- a/views/partials/custom-css.ejs +++ /dev/null @@ -1 +0,0 @@ -<% if (locals.customCssUrl) { %><% } %> diff --git a/views/partials/custom-css.njk b/views/partials/custom-css.njk new file mode 100644 index 000000000..4e5a494e6 --- /dev/null +++ b/views/partials/custom-css.njk @@ -0,0 +1,3 @@ +{% if customCssUrl %} + +{% endif %} diff --git a/views/partials/data.ejs b/views/partials/data.ejs deleted file mode 100644 index f0bbf5925..000000000 --- a/views/partials/data.ejs +++ /dev/null @@ -1,3 +0,0 @@ -<%_ if (data != null) { _%> - -<%_ } _%> \ No newline at end of file diff --git a/views/partials/data.njk b/views/partials/data.njk new file mode 100644 index 000000000..4edaf2e2e --- /dev/null +++ b/views/partials/data.njk @@ -0,0 +1,3 @@ +{% if data %} + +{% endif %} diff --git a/views/partials/dev-nav.ejs b/views/partials/dev-nav.ejs deleted file mode 100644 index 088c52c20..000000000 --- a/views/partials/dev-nav.ejs +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/views/partials/favicon.njk b/views/partials/favicon.njk new file mode 100644 index 000000000..5ec2aad8a --- /dev/null +++ b/views/partials/favicon.njk @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/views/partials/head.ejs b/views/partials/head.ejs deleted file mode 100644 index dc3df7103..000000000 --- a/views/partials/head.ejs +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -<%- include data %> - - diff --git a/views/partials/nav.njk b/views/partials/nav.njk new file mode 100644 index 000000000..ecea9018d --- /dev/null +++ b/views/partials/nav.njk @@ -0,0 +1,8 @@ + diff --git a/views/templates/account.njk b/views/templates/account.njk new file mode 100644 index 000000000..7e1b6e6c9 --- /dev/null +++ b/views/templates/account.njk @@ -0,0 +1,6 @@ +{% extends "templates/base.njk" %} + +{% block css %} + + +{% endblock %} diff --git a/views/templates/base.njk b/views/templates/base.njk new file mode 100644 index 000000000..23048151e --- /dev/null +++ b/views/templates/base.njk @@ -0,0 +1,39 @@ + + + + {# Meta tags #} + + + {% block meta %}{% endblock %} + + {# Favicon Configuration #} + {% include "partials/favicon.njk" %} + + {# CSP and security headers #} + {% block security %}{% endblock %} + + {# Title #} + {% block title %}{% endblock %} + + {# CSS #} + + + + {% include "partials/custom-css.njk" %} + {% block css %}{% endblock %} + + {# Static data injection #} + {% include "partials/data.njk" %} + + {# Configuration #} + + + + {% block body %} + {% block html %} +
+ {% endblock %} + {% endblock %} + {% block js %}{% endblock %} + + diff --git a/views/partials/dev.ejs b/views/templates/development.njk similarity index 54% rename from views/partials/dev.ejs rename to views/templates/development.njk index 691b548e5..f7fab6682 100644 --- a/views/partials/dev.ejs +++ b/views/templates/development.njk @@ -1,5 +1,17 @@ - +{% extends "templates/base.njk" %} + +{# Null out the security block, we don't want/need that in development #} +{% block security %}{% endblock %} + +{% block css %} - + +{% endblock %} + +{% block body %} + {% include "partials/nav.njk" %} + + {% block html %}{% endblock %} +{% endblock %} diff --git a/yarn.lock b/yarn.lock index dff8e4dab..a65a3524d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2392,6 +2392,12 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" +consolidate@0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.0.tgz#b03acd566a2565ca96e99f44fd1417486b4df88d" + dependencies: + bluebird "^3.1.1" + constantinople@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-3.1.0.tgz#7569caa8aa3f8d5935d62e1fa96f9f702cd81c79"