Merge pull request #1633 from coralproject/csp

Content Security Policy + Nunjacks
This commit is contained in:
Wyatt Johnson
2018-05-23 12:08:45 -06:00
committed by GitHub
55 changed files with 801 additions and 611 deletions
+22 -1
View File
@@ -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');
//==============================================================================
+4
View File
@@ -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
+55
View File
@@ -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,
});
+9
View File
@@ -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();
};
+1
View File
@@ -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 -2
View File
@@ -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;
+1 -1
View File
@@ -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;
+33
View File
@@ -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;
+1 -1
View File
@@ -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',
});
});
+1
View File
@@ -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'));
+3 -3
View File
@@ -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
View File
@@ -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: '',
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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 => {
+3 -3
View File
@@ -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 } },
});
};
-63
View File
@@ -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>
+63
View File
@@ -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 %}
-85
View File
@@ -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>
+85
View File
@@ -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 %}
-17
View File
@@ -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>
+14
View File
@@ -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 %}
-125
View File
@@ -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>
+126
View File
@@ -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 %}
-10
View File
@@ -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>
+10
View File
@@ -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>
-52
View File
@@ -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>
+55
View File
@@ -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 %}
-40
View File
@@ -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 %>&amp;limit=<%= limit %>"><%= page %></a></li>
<% } %>
<% page++; } %>
</ul>
</nav>
<% } %>
</div>
</body>
</html>
+37
View File
@@ -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 }}&amp;limit={{ limit }}">{{ i / limit + 1 | round }}</a></li>
{% endif %}
{% endfor %}
</ul>
</nav>
{% endif %}
</div>
{% endblock %}
-14
View File
@@ -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>
+16
View File
@@ -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 %}
-16
View File
@@ -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>
+16
View File
@@ -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 %}
-4
View File
@@ -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
View File
@@ -1 +0,0 @@
<% if (locals.customCssUrl) { %><link href="<%= customCssUrl %>" rel="stylesheet" type="text/css"><% } %>
+3
View File
@@ -0,0 +1,3 @@
{% if customCssUrl %}
<link nonce="{{ nonce }}" href="{{ customCssUrl }}" rel="stylesheet">
{% endif %}
-3
View File
@@ -1,3 +0,0 @@
<%_ if (data != null) { _%>
<script id="data" type="application/json"><%- JSON.stringify(data) %></script>
<%_ } _%>
+3
View File
@@ -0,0 +1,3 @@
{% if data %}
<script id="data" type="application/json">{{ data | dump | safe }}</script>
{% endif %}
-8
View File
@@ -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>
+14
View File
@@ -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">
-23
View File
@@ -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 %>"/>
+8
View File
@@ -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>
+6
View File
@@ -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 %}
+39
View File
@@ -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 %}
+6
View File
@@ -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"