Merge branch 'master' into karma

This commit is contained in:
Kim Gardner
2018-05-23 15:19:32 -04:00
committed by GitHub
60 changed files with 915 additions and 724 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');
//==============================================================================
+57 -74
View File
@@ -1,7 +1,10 @@
import ta from 'timeago.js';
import { negotiateLanguages } from 'fluent-langneg/compat';
import has from 'lodash/has';
import get from 'lodash/get';
import merge from 'lodash/merge';
import first from 'lodash/first';
import isUndefined from 'lodash/isUndefined';
import moment from 'moment';
import 'moment/locale/ar';
@@ -12,8 +15,8 @@ import 'moment/locale/fr';
import 'moment/locale/nl';
import 'moment/locale/pt-br';
import { createStorage } from 'coral-framework/services/storage';
// timeago
import ta from 'timeago.js';
import arTA from 'timeago.js/locales/ar';
import daTA from 'timeago.js/locales/da';
import deTA from 'timeago.js/locales/de';
@@ -24,6 +27,7 @@ import pt_BRTA from 'timeago.js/locales/pt_BR';
import zh_CNTA from 'timeago.js/locales/zh_CN';
import zh_TWTA from 'timeago.js/locales/zh_TW';
// locales
import ar from '../../../locales/ar.yml';
import en from '../../../locales/en.yml';
import da from '../../../locales/da.yml';
@@ -35,8 +39,9 @@ import pt_BR from '../../../locales/pt_BR.yml';
import zh_CN from '../../../locales/zh_CN.yml';
import zh_TW from '../../../locales/zh_TW.yml';
const defaultLanguage = process.env.TALK_DEFAULT_LANG;
const translations = {
export const defaultLocale = process.env.TALK_DEFAULT_LANG.replace(/-/g, '_');
export const translations = {
...ar,
...en,
...da,
@@ -49,84 +54,62 @@ const translations = {
...zh_TW,
};
let lang;
let timeagoInstance;
export const supportedLocales = Object.keys(translations);
function setLocale(storage, locale) {
storage.setItem('locale', locale);
}
let LOCALE;
let TIMEAGO_INSTANCE;
// detectLanguage will try to get the locale from storage if available,
// otherwise will try to get it from the navigator, otherwise, it will fallback
// to the default language.
function detectLanguage(storage) {
try {
const lang = storage.getItem('locale') || navigator.language;
if (lang) {
return lang;
}
} catch (err) {
console.warn(
'Error while trying to detect language, will fallback to',
err
);
}
console.warn('Could not detect language, will fallback to', defaultLanguage);
return defaultLanguage;
}
// getLocale will get the users locale from the local detector and parse it to a
// format we can work with.
function getLocale(storage) {
// Get the language from the local detector.
const lang = detectLanguage(storage);
// Some language strings come with additional subtags as defined in:
//
// https://www.ietf.org/rfc/bcp/bcp47.txt
//
// So we should strip that off if we find it.
return lang.split('-')[0];
}
const detectLanguage = () =>
first(
negotiateLanguages(navigator.languages, supportedLocales, {
defaultLocale,
strategy: 'lookup',
})
);
export function setupTranslations() {
// Setup the translation framework with the storage.
const storage = createStorage('localStorage');
// locale
LOCALE = detectLanguage();
const locale = getLocale(storage);
setLocale(storage, locale);
// Setting moment
moment.locale(locale);
// Extract language key.
lang = locale.split('-')[0];
// Check if we have a translation in this language.
if (!(lang in translations)) {
lang = defaultLanguage;
}
// moment
moment.locale(LOCALE);
// timeago
ta.register('ar', arTA);
ta.register('es', esTA);
ta.register('da', daTA);
ta.register('de', deTA);
ta.register('fr', frTA);
ta.register('nl_NL', nlTA);
ta.register('pt_BR', pt_BRTA);
ta.register('zh_CN', zh_CNTA);
ta.register('zh_TW', zh_TWTA);
timeagoInstance = ta();
ta.register('nl-NL', nlTA);
ta.register('pt-BR', pt_BRTA);
ta.register('zh-CN', zh_CNTA);
ta.register('zh-TW', zh_TWTA);
TIMEAGO_INSTANCE = ta();
}
/**
* loadTranslations will load the new language pack into the existing ones.
*
* @param {Object} newTranslations translation object to merge into the existing
* languages.
*/
export function loadTranslations(newTranslations) {
// Merge the new translations into the existing translations.
merge(translations, newTranslations);
// Push new languages into the supportedLocales array.
Object.keys(newTranslations).forEach(language => {
if (!supportedLocales.includes(language)) {
supportedLocales.push(language);
}
});
}
export function timeago(time) {
return timeagoInstance.format(new Date(time), lang);
return TIMEAGO_INSTANCE.format(new Date(time), LOCALE);
}
/**
@@ -140,24 +123,24 @@ export function timeago(time) {
*/
export function t(key, ...replacements) {
let translation;
if (has(translations[lang], key)) {
translation = get(translations[lang], key);
if (has(translations[LOCALE], key)) {
translation = get(translations[LOCALE], key);
} else if (has(translations['en'], key)) {
translation = get(translations['en'], key);
console.warn(`${lang}.${key} language key not set`);
console.warn(`${LOCALE}.${key} language key not set`);
}
if (translation) {
// replace any {n} with the arguments passed to this method
replacements.forEach((str, i) => {
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
});
return translation;
} else {
console.warn(`${lang}.${key} and en.${key} language key not set`);
if (!translation) {
console.warn(`${LOCALE}.${key} and en.${key} language key not set`);
return key;
}
// Handle replacements in the translation string.
return translation.replace(
/{(\d+)}/g,
(match, number) =>
!isUndefined(replacements[number]) ? replacements[number] : match
);
}
export default t;
+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
+3 -3
View File
@@ -11,9 +11,9 @@ can enable the following plugins:
- [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth) - to facilitate email changes and email association
- [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data) - to facilitate account download and deletion
Even if you don't reside in a location where GDPR will apply, it is recommended
to enable these features as a best practice to provide your users with control over their
own data.
Even if GDPR will not apply to you, it is recommended to enable these
features as a best practice to provide your users with control over their own
data.
## GPDR Feature Overview
+1 -1
View File
@@ -81,5 +81,5 @@ example issuer and Talk must match:
reference, the basic takeaway is that the secret used to sign the tokens issued
by the issuer must be able to be verified by Talk.
For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/reference/server/#tokenUserNotFound)
For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/api/server/#tokenusernotfound)
reference.
+2 -2
View File
@@ -413,7 +413,7 @@ fi_FI:
title_reject: "Huomasimme sinun hylänneen käyttäjänimen"
suspend_user: "Aseta väliaikainen käyttökielto"
yes_suspend: "Kyllä, sulje väliaikaisesti"
email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää."
email_message_reject: "Toinen yhteisön jäsen on ilmiantanut käyttäjänimesi ja sen perusteella nimi on hylätty. Et voi enää osallistua keskusteluun. Ole ystävällisesti yhteydessä meihin, jos sinulla on asiasta kysyttävää."
write_message: "Kirjoita viesti"
send: Lähetä
thank_you: "Arvostamme palautettasi. Moderaattorimme käy läpi tekemäsi ilmiannon."
@@ -462,4 +462,4 @@ fi_FI:
close: "Sulje asennusnäkymä"
admin_sidebar:
view_options: "Näytä asetukset"
sort_comments: "Järjestä kommentit"
sort_comments: "Järjestä kommentit"
+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();
};
+2 -1
View File
@@ -58,7 +58,6 @@
"dependencies": {
"@coralproject/gql-merge": "^0.1.0",
"@coralproject/graphql-anywhere-optimized": "^0.1.0",
"accepts": "^1.3.4",
"apollo-client": "^1.9.1",
"apollo-engine": "^0.8.1",
"apollo-server-express": "^1.2.0",
@@ -91,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",
@@ -108,6 +108,7 @@
"express-static-gzip": "^0.3.1",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^0.11.2",
"fluent-langneg": "^0.1.0",
"form-data": "^2.3.1",
"fs-extra": "^4.0.1",
"graphql": "^0.10.1",
@@ -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 => {
+45 -31
View File
@@ -1,8 +1,11 @@
const fs = require('fs');
const path = require('path');
const debug = require('debug')('talk:services:i18n');
const accepts = require('accepts');
const { get, has, merge } = require('lodash');
const {
acceptedLanguages,
negotiateLanguages,
} = require('fluent-langneg/compat');
const { first, get, has, merge, isUndefined } = require('lodash');
const yaml = require('yamljs');
const plugins = require('./plugins');
const { DEFAULT_LANG } = require('../config');
@@ -11,7 +14,7 @@ const resolve = (...paths) =>
path.resolve(path.join(__dirname, '..', 'locales', ...paths));
// Load all the translations.
let translations = fs
const translations = fs
.readdirSync(resolve())
// Resolve all the filenames relative the the locales directory.
@@ -23,26 +26,22 @@ let translations = fs
// Load the translation files from disk.
.map(filename => fs.readFileSync(filename, 'utf8'))
// Load the translation files.
.reduce((packs, contents) => {
const pack = yaml.parse(contents);
return merge(packs, pack);
}, {});
// Load the translation files and merge the yaml into the existing packs.
.reduce((packs, contents) => merge(packs, yaml.parse(contents)), {});
// Create a list of all supported translations.
const languages = Object.keys(translations);
const supportedLocales = Object.keys(translations);
// Move the default language to the front.
if (languages.includes(DEFAULT_LANG)) {
const from = languages.indexOf(DEFAULT_LANG);
languages.splice(from, 1);
languages.splice(0, 0, DEFAULT_LANG);
if (supportedLocales.includes(DEFAULT_LANG)) {
const from = supportedLocales.indexOf(DEFAULT_LANG);
supportedLocales.splice(from, 1);
supportedLocales.splice(0, 0, DEFAULT_LANG);
}
debug(`loaded language sets for ${languages}`);
debug(`loaded language sets for ${supportedLocales}`);
let loadedPluginTranslations = false;
const loadPluginTranslations = () => {
const lazyLoadPluginTranslations = () => {
if (loadedPluginTranslations) {
return;
}
@@ -55,7 +54,15 @@ const loadPluginTranslations = () => {
const pack = yaml.parse(fs.readFileSync(filename, 'utf8'));
translations = merge(translations, pack);
// Merge the translations into the system translations.
merge(translations, pack);
// Push new languages into the supportedLocales array.
Object.keys(pack).forEach(language => {
if (!supportedLocales.includes(language)) {
supportedLocales.push(language);
}
});
});
loadedPluginTranslations = true;
@@ -64,7 +71,7 @@ const loadPluginTranslations = () => {
const t = lang => (key, ...replacements) => {
// Loads the translations into the translations array from plugins. This is
// done lazily to ensure that we don't have an import cycle.
loadPluginTranslations();
lazyLoadPluginTranslations();
let translation;
if (has(translations[lang], key)) {
@@ -74,16 +81,17 @@ const t = lang => (key, ...replacements) => {
console.warn(`${lang}.${key} language key not set`);
}
if (translation) {
// replace any {n} with the arguments passed to this method
replacements.forEach((str, i) => {
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
});
return translation;
} else {
if (!translation) {
console.warn(`${lang}.${key} and en.${key} language key not set`);
return key;
}
// Handle replacements in the translation string.
return translation.replace(
/{(\d+)}/g,
(match, number) =>
!isUndefined(replacements[number]) ? replacements[number] : match
);
};
/**
@@ -92,13 +100,19 @@ const t = lang => (key, ...replacements) => {
*/
const i18n = {
request(req) {
debug(`possible languages given request '${accepts(req).languages()}'`);
const lang = accepts(req).language(languages);
debug(`parsed request language as '${lang}'`);
const language = lang ? lang : DEFAULT_LANG;
debug(`decided language as '${language}'`);
const acceptsLanguages = acceptedLanguages(req.headers['accept-language']);
debug(`possible languages given request '${acceptsLanguages}'`);
return t(language);
// negotiate the language.
const lang = first(
negotiateLanguages(acceptsLanguages, supportedLocales, {
defaultLocale: DEFAULT_LANG,
strategy: 'lookup',
})
);
debug(`decided language as '${lang}'`);
return t(lang);
},
t: t(DEFAULT_LANG),
};
+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 %}
+11 -1
View File
@@ -167,7 +167,7 @@ abbrev@1, abbrev@^1.0.7:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
accepts@^1.3.4, accepts@~1.3.4:
accepts@~1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
dependencies:
@@ -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"
@@ -3975,6 +3981,10 @@ flexbuffer@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/flexbuffer/-/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30"
fluent-langneg@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/fluent-langneg/-/fluent-langneg-0.1.0.tgz#aa12054fbfa4b728daec38331efc12f01faae93a"
flush-write-stream@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"