mirror of
https://github.com/wassname/talk.git
synced 2026-07-16 11:22:16 +08:00
Merge pull request #1633 from coralproject/csp
Content Security Policy + Nunjacks
This commit is contained in:
@@ -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');
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title><%= t('talk-plugin-notifications.unsubscribe_page.unsubscribe') %></title>
|
||||
<%- include(root + '/partials/head') %>
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
|
||||
</head>
|
||||
<body class="confirm-email-page">
|
||||
<div id="root">
|
||||
<div class="error-console container"><%= t('talk-plugin-notifications.unsubscribe_page.token_invalid') %></div>
|
||||
<div id="success" style="display:none;" class="legend container"><%= t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') %></div>
|
||||
<form id="unsubscribe-form" class="container">
|
||||
<legend class="legend"><%= t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') %></legend>
|
||||
<button type="submit"><%= t('talk-plugin-notifications.unsubscribe_page.confirm') %></button>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
var submitting = false;
|
||||
var payload = JSON.stringify({token: location.hash.replace('#', '')});
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting = true;
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/unsubscribe-notifications',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
}).then(function (success) {
|
||||
$('#unsubscribe-form').fadeOut(function () {
|
||||
$('#success').fadeIn();
|
||||
});
|
||||
}).catch(function () {
|
||||
submitting = false;
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/unsubscribe-notifications/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
})
|
||||
.then(function () {
|
||||
$('#unsubscribe-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function () {
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('talk-plugin-notifications.unsubscribe_page.unsubscribe') }}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ super() }}
|
||||
<style nonce="{{ nonce }}" type="text/css">
|
||||
#success { display:none; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<div class="error-console container">{{ t('talk-plugin-notifications.unsubscribe_page.token_invalid') }}</div>
|
||||
<div id="success" class="legend container">{{ t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') }}</div>
|
||||
<form id="unsubscribe-form" class="container">
|
||||
<legend class="legend">{{ t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') }}</legend>
|
||||
<button type="submit">{{ t('talk-plugin-notifications.unsubscribe_page.confirm') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
var submitting = false;
|
||||
var payload = JSON.stringify({token: location.hash.replace('#', '')});
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting = true;
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/unsubscribe-notifications',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
}).then(function (success) {
|
||||
$('#unsubscribe-form').fadeOut(function () {
|
||||
$('#success').fadeIn();
|
||||
});
|
||||
}).catch(function () {
|
||||
submitting = false;
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/unsubscribe-notifications/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
})
|
||||
.then(function () {
|
||||
$('#unsubscribe-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function () {
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -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
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('download_landing.download_your_account') %></title>
|
||||
<%- include(root + '/partials/account') %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1><%= t('download_landing.download_your_account') %></h1>
|
||||
<p><%= t('download_landing.download_details') %></p>
|
||||
<p><%= t('download_landing.all_information_included') %></p>
|
||||
<ul class="check_list">
|
||||
<li><%= t('download_landing.information_included.date') %></li>
|
||||
<li><%= t('download_landing.information_included.url') %></li>
|
||||
<li><%= t('download_landing.information_included.body') %></li>
|
||||
<li><%= t('download_landing.information_included.asset_url') %></li>
|
||||
</ul>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="download-form" method="post" action="<%= BASE_PATH %>api/v1/account/download">
|
||||
<button type="submit"><%= t('download_landing.confirm') %></button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
var token = location.hash.replace('#', '');
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/download',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: token, check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#download-form').append('<input name="token" type="hidden" value="' + token + '"/>').fadeIn();
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('download_landing.download_your_account') }}{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1>{{ t('download_landing.download_your_account') }}</h1>
|
||||
<p>{{ t('download_landing.download_details') }}</p>
|
||||
<p>{{ t('download_landing.all_information_included') }}</p>
|
||||
<ul class="check_list">
|
||||
<li>{{ t('download_landing.information_included.date') }}</li>
|
||||
<li>{{ t('download_landing.information_included.url') }}</li>
|
||||
<li>{{ t('download_landing.information_included.body') }}</li>
|
||||
<li>{{ t('download_landing.information_included.asset_url') }}</li>
|
||||
</ul>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="download-form" method="post" action="{{ BASE_PATH }}api/v1/account/download">
|
||||
<button type="submit">{{ t('download_landing.confirm') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
var token = location.hash.replace('#', '');
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/download',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: token, check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#download-form').append('<input name="token" type="hidden" value="' + token + '"/>').fadeIn();
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -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: '',
|
||||
|
||||
@@ -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;
|
||||
|
||||
+10
-6
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+4
-6
@@ -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 => {
|
||||
|
||||
@@ -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 } },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('confirm_email.email_confirmation') %></title>
|
||||
<%- include ../../partials/account %>
|
||||
</head>
|
||||
<body class="confirm-email-page">
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1><%= t('confirm_email.email_confirmation') %></h1>
|
||||
<p><%= t('confirm_email.click_to_confirm') %></p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="verify-email-form">
|
||||
<button type="submit"><%= t('confirm_email.confirm') %></button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirectUri;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#verify-email-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('confirm_email.email_confirmation') }}{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1>{{ t('confirm_email.email_confirmation') }}</h1>
|
||||
<p>{{ t('confirm_email.click_to_confirm') }}</p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="verify-email-form">
|
||||
<button type="submit">{{ t('confirm_email.confirm') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirectUri;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#verify-email-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,85 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('password_reset.set_new_password') %></title>
|
||||
<%- include ../../partials/account %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1><%= t('password_reset.set_new_password') %></h1>
|
||||
<p><%= t('password_reset.change_password_help') %></p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="reset-password-form">
|
||||
<label for="password">
|
||||
<%= t('password_reset.new_password') %>
|
||||
<input type="password" name="password" placeholder="<%= t('password_reset.new_password') %>" />
|
||||
<small><%= t('password_reset.new_password_help') %></small>
|
||||
</label>
|
||||
<label for="confirm-password">
|
||||
<%= t('password_reset.confirm_new_password') %>
|
||||
<input type="password" name="confirm-password" placeholder="<%= t('password_reset.confirm_new_password') %>" />
|
||||
</label>
|
||||
<button type="submit"><%= t('password_reset.change_password') %></button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirm = $('[name="confirm-password"]').val();
|
||||
|
||||
if (password === '' || password.length < 8) {
|
||||
showError('Passwords must be at least 8 characters.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirm) {
|
||||
showError('New password and confirm password must match');
|
||||
return false;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirect;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#reset-password-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('password_reset.set_new_password') }}{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1>{{ t('password_reset.set_new_password') }}</h1>
|
||||
<p>{{ t('password_reset.change_password_help') }}</p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="reset-password-form">
|
||||
<label for="password">
|
||||
{{ t('password_reset.new_password') }}
|
||||
<input type="password" name="password" placeholder="{{ t('password_reset.new_password') }}" />
|
||||
<small>{{ t('password_reset.new_password_help') }}</small>
|
||||
</label>
|
||||
<label for="confirm-password">
|
||||
{{ t('password_reset.confirm_new_password') }}
|
||||
<input type="password" name="confirm-password" placeholder="{{ t('password_reset.confirm_new_password') }}" />
|
||||
</label>
|
||||
<button type="submit">{{ t('password_reset.change_password') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirm = $('[name="confirm-password"]').val();
|
||||
|
||||
if (password === '' || password.length < 8) {
|
||||
showError('Passwords must be at least 8 characters.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirm) {
|
||||
showError('New password and confirm password must match');
|
||||
return false;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirect;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#reset-password-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title>Talk - Coral Admin</title>
|
||||
<%- include partials/head %>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-admin/bundle.css') %>">
|
||||
<%- include partials/custom-css %>
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div id="root"></div>
|
||||
<script src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="<%= resolve('coral-admin/bundle.js') %>" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block title %}Talk Admin{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
|
||||
<link href="https://code.getmdl.io/1.2.1/material.min.css" rel="stylesheet">
|
||||
<link href="{{ resolve('coral-admin/bundle.css') }}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script nonce="{{ nonce }}" src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="{{ resolve('coral-admin/bundle.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,125 +0,0 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>GraphiQL</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
<%- include ../partials/dev %>
|
||||
<style>
|
||||
html, body, #graphiql {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
*, ::after, ::before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
</style>
|
||||
<link href="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.css" rel="stylesheet" />
|
||||
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react-dom.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<main id="graphiql"></main>
|
||||
<script>
|
||||
// Collect the URL parameters
|
||||
var parameters = {};
|
||||
window.location.search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
// Produce a Location query string from a parameter object.
|
||||
function locationQuery(params, location) {
|
||||
return (location ? location: '') + '?' + Object.keys(params).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(params[key]);
|
||||
}).join('&');
|
||||
}
|
||||
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
||||
var graphqlParamNames = {
|
||||
query: true,
|
||||
variables: true,
|
||||
operationName: true
|
||||
};
|
||||
var otherParams = {};
|
||||
for (var k in parameters) {
|
||||
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
|
||||
otherParams[k] = parameters[k];
|
||||
}
|
||||
}
|
||||
// We don't use safe-serialize for location, because it's not client input.
|
||||
var fetchURL = locationQuery(otherParams, '<%= BASE_URL %><%= endpointURL %>');
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
var headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
try {
|
||||
let token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return fetch(fetchURL, {
|
||||
method: 'post',
|
||||
headers: headers,
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared.
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
function onEditOperationName(newOperationName) {
|
||||
parameters.operationName = newOperationName;
|
||||
updateURL();
|
||||
}
|
||||
function updateURL() {
|
||||
history.replaceState(null, null, locationQuery(parameters));
|
||||
}
|
||||
// Render <GraphiQL /> into the body.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName,
|
||||
query: null,
|
||||
response: null,
|
||||
variables: null,
|
||||
operationName: null,
|
||||
}),
|
||||
document.querySelector("#graphiql")
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,126 @@
|
||||
{% extends "templates/development.njk" %}
|
||||
|
||||
{% block title %}GraphiQL{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{# Include the base development pieces #}
|
||||
{{ super() }}
|
||||
<style type="text/css">
|
||||
html, body, #graphiql {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
*, ::after, ::before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
</style>
|
||||
<link href="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.css" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react-dom.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
// Collect the URL parameters
|
||||
var parameters = {};
|
||||
window.location.search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
// Produce a Location query string from a parameter object.
|
||||
function locationQuery(params, location) {
|
||||
return (location ? location: '') + '?' + Object.keys(params).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(params[key]);
|
||||
}).join('&');
|
||||
}
|
||||
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
||||
var graphqlParamNames = {
|
||||
query: true,
|
||||
variables: true,
|
||||
operationName: true
|
||||
};
|
||||
var otherParams = {};
|
||||
for (var k in parameters) {
|
||||
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
|
||||
otherParams[k] = parameters[k];
|
||||
}
|
||||
}
|
||||
// We don't use safe-serialize for location, because it's not client input.
|
||||
var fetchURL = locationQuery(otherParams, '{{ BASE_URL }}{{ endpointURL }}');
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
var headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
try {
|
||||
let token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return fetch(fetchURL, {
|
||||
method: 'post',
|
||||
headers: headers,
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared.
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
function onEditOperationName(newOperationName) {
|
||||
parameters.operationName = newOperationName;
|
||||
updateURL();
|
||||
}
|
||||
function updateURL() {
|
||||
history.replaceState(null, null, locationQuery(parameters));
|
||||
}
|
||||
// Render <GraphiQL /> into the body.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName,
|
||||
query: null,
|
||||
response: null,
|
||||
variables: null,
|
||||
operationName: null,
|
||||
}),
|
||||
document.querySelector("#graphiql")
|
||||
);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<main id="graphiql"></main>
|
||||
{% endblock %}
|
||||
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<%- include partials/data %>
|
||||
</head>
|
||||
<body>
|
||||
<script type="application/json" id="auth"><%- encodeJSONForHTML(auth) %></script>
|
||||
<script type="text/javascript" src="<%= resolve('coral-auth-callback/bundle.js') %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% include "partials/data.njk" %}
|
||||
</head>
|
||||
<body>
|
||||
<script type="application/json" id="auth">{{ encodeJSONForHTML(auth) }}</script>
|
||||
<script type="text/javascript" src="{{ resolve('coral-auth-callback/bundle.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,52 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:title" content="<%= title %>" />
|
||||
<meta property="og:description" content="A description of this article." />
|
||||
<meta property="article:author" content="A. J. Ournalist" />
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:image" content="https://coralproject.net/images/splash-md.jpg">
|
||||
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
|
||||
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
|
||||
<meta property="article:section" itemprop="articleSection" content="The Section!" />
|
||||
<title><%= title %></title>
|
||||
<%- include ../partials/dev %>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<div class="container">
|
||||
<h1 class="mt-3"><%= title %></h1>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script src="<%= resolve('embed.js') %>"></script>
|
||||
<script>
|
||||
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '<%= BASE_URL %>',
|
||||
asset_url: '<%= asset_url ? asset_url : '' %>',
|
||||
asset_id: '<%= asset_id ? asset_id : '' %>',
|
||||
auth_token: '',
|
||||
/**
|
||||
* You can listen to events using the example below.
|
||||
* The argument passed is the event emitter from
|
||||
* https://github.com/asyncly/EventEmitter2
|
||||
*
|
||||
* events: function(events) {
|
||||
* events.onAny(function(eventName, data) {
|
||||
* console.log(eventName, data);
|
||||
* });
|
||||
* },
|
||||
*/
|
||||
plugins_config: {
|
||||
/**
|
||||
* You can disable rendering slot components of a plugin by doing:
|
||||
*
|
||||
* 'talk-plugin-love': {
|
||||
* disable_components: true,
|
||||
* },
|
||||
*/
|
||||
test: 'data',
|
||||
debug: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "templates/development.njk" %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<meta property="og:title" content="{{ title }}" />
|
||||
<meta property="og:description" content="A description of this article." />
|
||||
<meta property="article:author" content="A. J. Ournalist" />
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:image" content="https://coralproject.net/images/splash-md.jpg">
|
||||
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
|
||||
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
|
||||
<meta property="article:section" itemprop="articleSection" content="The Section!" />
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div class="container">
|
||||
<h1 class="mt-3">{{ title }}</h1>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{{ resolve('embed.js') }}"></script>
|
||||
<script>
|
||||
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '{{ BASE_URL }}',
|
||||
asset_url: '{{ asset_url }}',
|
||||
asset_id: '{{ asset_id }}',
|
||||
auth_token: '',
|
||||
/**
|
||||
* You can listen to events using the example below.
|
||||
* The argument passed is the event emitter from
|
||||
* https://github.com/asyncly/EventEmitter2
|
||||
*
|
||||
* events: function(events) {
|
||||
* events.onAny(function(eventName, data) {
|
||||
* console.log(eventName, data);
|
||||
* });
|
||||
* },
|
||||
*/
|
||||
plugins_config: {
|
||||
/**
|
||||
* You can disable rendering slot components of a plugin by doing:
|
||||
*
|
||||
* 'talk-plugin-love': {
|
||||
* disable_components: true,
|
||||
* },
|
||||
*/
|
||||
test: 'data',
|
||||
debug: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,40 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>All Assets</title>
|
||||
<%- include ../partials/dev %>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<div class="container">
|
||||
<div class="d-flex w-100 justify-content-between mt-3 mb-2">
|
||||
<h1 class="mb-0">All Assets</h1>
|
||||
<span class="text-muted"><%= skip + 1 %> - <%= skip + assets.length %> of <%= count %> Assets</span>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<% if (skip === 0) { %><a href="<%= BASE_PATH %>dev/assets/random" class="list-group-item list-group-item-action list-group-item-primary"><i class="fa fa-plus" aria-hidden="true"></i> Create a random article</a><% } %>
|
||||
<% assets.forEach(function (asset) { %>
|
||||
<a href="<%= BASE_PATH %>dev/assets/id/<%= asset.id %>" class="list-group-item list-group-item-action flex-column align-items-start">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1"><%= asset.title %></h5>
|
||||
<small>Created <%= asset.created_at.toLocaleString('en-US') %></small>
|
||||
</div>
|
||||
<small><%= asset.url %></small>
|
||||
</a>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% if (count !== assets.length) { %>
|
||||
<nav aria-label="Page navigation example" class="mt-2">
|
||||
<ul class="pagination justify-content-center">
|
||||
<% let page = 1; for (let i = 0; i < count; i += limit) { %>
|
||||
<% if (i === skip) { %>
|
||||
<li class="page-item disabled"><a class="page-link" href="#"><%= page %></a></li>
|
||||
<% } else { %>
|
||||
<li class="page-item"><a class="page-link" href="?skip=<%= i %>&limit=<%= limit %>"><%= page %></a></li>
|
||||
<% } %>
|
||||
<% page++; } %>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
{% extends "templates/development.njk" %}
|
||||
|
||||
{% block title %}All Assets{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div class="container">
|
||||
<div class="d-flex w-100 justify-content-between mt-3 mb-2">
|
||||
<h1 class="mb-0">All Assets</h1>
|
||||
<span class="text-muted">{{ skip + 1 }} - {{ skip + assets.length }} of {{ count }} Assets</span>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
{% if skip === 0 %}<a href="{{ BASE_PATH }}dev/assets/random" class="list-group-item list-group-item-action list-group-item-primary"><i class="fa fa-plus" aria-hidden="true"></i> Create a random article</a>{% endif %}
|
||||
{% for asset in assets %}
|
||||
<a href="{{ BASE_PATH }}dev/assets/id/{{ asset.id }}" class="list-group-item list-group-item-action flex-column align-items-start">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ asset.title }}</h5>
|
||||
<small>Created {{ asset.created_at.toLocaleString('en-US') }}</small>
|
||||
</div>
|
||||
<small>{{ asset.url }}</small>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if count !== assets.length %}
|
||||
<nav aria-label="Page navigation example" class="mt-2">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% for i in range(0, count, limit) %}
|
||||
{% if i === skip %}
|
||||
<li class="page-item disabled"><a class="page-link" href="#">{{ i / limit + 1 | round }}</a></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{{ BASE_PATH }}dev/assets?skip={{ i }}&limit={{ limit }}">{{ i / limit + 1 | round }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<%- include ../partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/default.css') %>">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/bundle.css') %>">
|
||||
<%- include ../partials/custom-css %>
|
||||
</head>
|
||||
<body class="embed-stream-page">
|
||||
<div id="talk-embed-stream-container"></div>
|
||||
<script src="<%= resolve('embed/stream/bundle.js') %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block title %}Talk{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{{ resolve('embed/stream/default.css')}}" rel="stylesheet">
|
||||
<link href="{{ resolve('embed/stream/bundle.css')}}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="talk-embed-stream-container"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{{ resolve('embed/stream/bundle.js')}}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<%- include partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-login/bundle.css') %>">
|
||||
<%- include partials/custom-css %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="talk-login-container"></div>
|
||||
<script src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="<%= resolve('coral-login/bundle.js') %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block title %}Talk - Login{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{{ resolve('coral-login/bundle.css')}}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="talk-login-container"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script nonce="{{ nonce }}" src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="{{ resolve('coral-login/bundle.js')}}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,4 +0,0 @@
|
||||
<%- include head %>
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
|
||||
<%- include custom-css %>
|
||||
@@ -1 +0,0 @@
|
||||
<% if (locals.customCssUrl) { %><link href="<%= customCssUrl %>" rel="stylesheet" type="text/css"><% } %>
|
||||
@@ -0,0 +1,3 @@
|
||||
{% if customCssUrl %}
|
||||
<link nonce="{{ nonce }}" href="{{ customCssUrl }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
@@ -1,3 +0,0 @@
|
||||
<%_ if (data != null) { _%>
|
||||
<script id="data" type="application/json"><%- JSON.stringify(data) %></script>
|
||||
<%_ } _%>
|
||||
@@ -0,0 +1,3 @@
|
||||
{% if data %}
|
||||
<script id="data" type="application/json">{{ data | dump | safe }}</script>
|
||||
{% endif %}
|
||||
@@ -1,8 +0,0 @@
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="<%= BASE_PATH %>dev"><%= organizationName %> <span class="text-muted">Organization</span></a>
|
||||
<form class="form-inline mb-0">
|
||||
<a href="<%= BASE_PATH %>admin" class="btn btn-outline-primary mr-2"><i class="fa fa-lock" aria-hidden="true"></i> Admin</a>
|
||||
<a href="<%= BASE_PATH %>api/v1/graph/iql" class="btn btn-outline-secondary mr-2 graphiql"><i class="fa fa-terminal" aria-hidden="true"></i> Graph<em>i</em>QL</a>
|
||||
<a href="<%= BASE_PATH %>dev/assets" class="btn btn-outline-secondary"><i class="fa fa-list" aria-hidden="true"></i> All Assets</a>
|
||||
</form>
|
||||
</nav>
|
||||
@@ -0,0 +1,14 @@
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="{{ STATIC_URL }}public/img/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="{{ STATIC_URL }}public/img/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="{{ STATIC_URL }}public/img/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_URL }}public/img/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="{{ STATIC_URL }}public/img/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_URL }}public/img/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="{{ STATIC_URL }}public/img/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_URL }}public/img/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_URL }}public/img/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_URL }}public/img/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="{{ STATIC_URL }}public/img/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_URL }}public/img/favicon-16x16.png">
|
||||
<link rel="manifest" href="{{ STATIC_URL }}public/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
@@ -1,23 +0,0 @@
|
||||
<meta charset="utf-8">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<%= STATIC_URL %>public/img/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<%= STATIC_URL %>public/img/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="<%= STATIC_URL %>public/img/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="<%= STATIC_URL %>public/img/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="<%= STATIC_URL %>public/img/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="<%= STATIC_URL %>public/img/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="<%= STATIC_URL %>public/img/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= STATIC_URL %>public/img/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= STATIC_URL %>public/img/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= STATIC_URL %>public/img/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="<%= STATIC_URL %>public/img/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= STATIC_URL %>public/img/favicon-16x16.png">
|
||||
<link rel="manifest" href="<%= STATIC_URL %>public/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
|
||||
|
||||
<%- include data %>
|
||||
|
||||
<base href="<%= BASE_URL %>"/>
|
||||
@@ -0,0 +1,8 @@
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="{{ BASE_PATH }}dev">{{ organizationName }} <span class="text-muted">Organization</span></a>
|
||||
<form class="form-inline mb-0">
|
||||
<a href="{{ BASE_PATH }}admin" class="btn btn-outline-primary mr-2"><i class="fa fa-lock" aria-hidden="true"></i> Admin</a>
|
||||
<a href="{{ BASE_PATH }}api/v1/graph/iql" class="btn btn-outline-secondary mr-2 graphiql"><i class="fa fa-terminal" aria-hidden="true"></i> Graph<em>i</em>QL</a>
|
||||
<a href="{{ BASE_PATH }}dev/assets" class="btn btn-outline-secondary"><i class="fa fa-list" aria-hidden="true"></i> All Assets</a>
|
||||
</form>
|
||||
</nav>
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="{{ BASE_PATH }}public/css/admin.css">
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{# Meta tags #}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
{% block meta %}{% endblock %}
|
||||
|
||||
{# Favicon Configuration #}
|
||||
{% include "partials/favicon.njk" %}
|
||||
|
||||
{# CSP and security headers #}
|
||||
{% block security %}{% endblock %}
|
||||
|
||||
{# Title #}
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
|
||||
{# CSS #}
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
|
||||
{% include "partials/custom-css.njk" %}
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{# Static data injection #}
|
||||
{% include "partials/data.njk" %}
|
||||
|
||||
{# Configuration #}
|
||||
<base href="{{ BASE_URL }}"/>
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
{% block html %}
|
||||
<div id="root"></div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% block js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,17 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{# Null out the security block, we don't want/need that in development #}
|
||||
{% block security %}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous" rel="stylesheet">
|
||||
<link href="<%= BASE_PATH %>public/css/dev.css" rel="stylesheet" >
|
||||
<link href="{{ BASE_PATH }}public/css/dev.css" rel="stylesheet" >
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include "partials/nav.njk" %}
|
||||
|
||||
{% block html %}{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user