mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 06:40:07 +08:00
Merge branch 'master' into karma
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
const express = require('express');
|
||||
const nunjucks = require('nunjucks');
|
||||
const cons = require('consolidate');
|
||||
const trace = require('./middleware/trace');
|
||||
const logging = require('./middleware/logging');
|
||||
const path = require('path');
|
||||
@@ -72,7 +74,26 @@ app.use(
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
// configure the default views directory.
|
||||
const views = path.join(__dirname, 'views');
|
||||
app.set('views', views);
|
||||
|
||||
// reconfigure nunjucks.
|
||||
cons.requires.nunjucks = nunjucks.configure(views, {
|
||||
autoescape: true,
|
||||
trimBlocks: true,
|
||||
lstripBlocks: true,
|
||||
watch: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
// assign the nunjucks engine to .njk files.
|
||||
app.engine('njk', cons.nunjucks);
|
||||
|
||||
// assign the ejs engine to .ejs and .html files.
|
||||
app.engine('ejs', cons.ejs);
|
||||
app.engine('html', cons.ejs);
|
||||
|
||||
// set .ejs as the default extension.
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
const helmet = require('helmet');
|
||||
const { WEBSOCKET_LIVE_URI, ENABLE_STRICT_CSP } = require('../config');
|
||||
const { BASE_PATH, BASE_URL, STATIC_URL } = require('../url');
|
||||
const { URL } = require('url');
|
||||
|
||||
// websocketUri represents the host where we can connect for websocket requests.
|
||||
const websocketUri = new URL(WEBSOCKET_LIVE_URI || BASE_URL);
|
||||
websocketUri.protocol = websocketUri.protocol.startsWith('https')
|
||||
? 'wss'
|
||||
: 'ws';
|
||||
const { origin: websocketSrc } = websocketUri;
|
||||
|
||||
// staticSrc represents any static asset hosted on the static host.
|
||||
const { host: staticSrc } = new URL(STATIC_URL);
|
||||
|
||||
// nonceSrc represents the nonce source that is used to indicate a safe resource
|
||||
// to load.
|
||||
const nonceSrc = (req, res) => `'nonce-${res.locals.nonce}'`;
|
||||
|
||||
module.exports = helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
reportUri: `${BASE_PATH}api/v1/csp`, // report all policy violations to our reporting uri
|
||||
defaultSrc: ["'none'"], // by default, do not allow anything at all
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
'https://ajax.googleapis.com', // for jquery
|
||||
staticSrc, // for any static files loaded from a cdn
|
||||
nonceSrc,
|
||||
],
|
||||
styleSrc: [
|
||||
"'self'",
|
||||
'https://maxcdn.bootstrapcdn.com', // for bootstrap css
|
||||
'https://fonts.googleapis.com', // for google fonts
|
||||
'https://code.getmdl.io', // for mdl css
|
||||
staticSrc, // for any static files loaded from a cdn
|
||||
nonceSrc,
|
||||
],
|
||||
connectSrc: ["'self'", websocketSrc],
|
||||
fontSrc: [
|
||||
"'self'",
|
||||
'https://maxcdn.bootstrapcdn.com', // for font-awesome
|
||||
'https://fonts.gstatic.com', // for google fonts
|
||||
staticSrc, // for any static files loaded from a cdn
|
||||
nonceSrc,
|
||||
],
|
||||
imgSrc: [
|
||||
"'self'",
|
||||
staticSrc, // for any static files loaded from a cdn
|
||||
nonceSrc,
|
||||
],
|
||||
},
|
||||
browserSniff: false,
|
||||
// Allow the configuration to disable strict enforcement of CSP.
|
||||
reportOnly: !ENABLE_STRICT_CSP,
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
const uuid = require('uuid/v4');
|
||||
|
||||
// nonce is designed to create a random value that can be used in conjunction
|
||||
// with the csp middleware.
|
||||
module.exports = (req, res, next) => {
|
||||
res.locals.nonce = uuid();
|
||||
|
||||
next();
|
||||
};
|
||||
+2
-1
@@ -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,11 +2,11 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/email/confirm', (req, res) => {
|
||||
res.render('account/email/confirm');
|
||||
res.render('account/email/confirm.njk');
|
||||
});
|
||||
|
||||
router.get('/password/reset', (req, res) => {
|
||||
res.render('account/password/reset');
|
||||
res.render('account/password/reset.njk');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -2,7 +2,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
res.render('admin');
|
||||
res.render('admin.njk');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const express = require('express');
|
||||
const Joi = require('joi');
|
||||
const { logger } = require('../../../services/logging');
|
||||
const router = express.Router();
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
'csp-report': Joi.object().keys({
|
||||
'document-uri': Joi.string(),
|
||||
referrer: Joi.string(),
|
||||
'blocked-uri': Joi.string(),
|
||||
'violated-directive': Joi.string(),
|
||||
'original-policy': Joi.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const json = express.json({ type: 'application/csp-report' });
|
||||
|
||||
router.post('/', json, async (req, res, next) => {
|
||||
const { value, error: err } = Joi.validate(req.body, schema, {
|
||||
stripUnknown: true,
|
||||
presence: 'required',
|
||||
});
|
||||
if (err) {
|
||||
res.status(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error({ report: value }, 'csp violation reported');
|
||||
|
||||
res.status(202).end();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -10,7 +10,7 @@ router.use('/ql', apollo.graphqlExpress(createGraphOptions));
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
// Interactive graphiql interface.
|
||||
router.use('/iql', staticTemplate, (req, res) => {
|
||||
res.render('api/graphiql', {
|
||||
res.render('api/graphiql.njk', {
|
||||
endpointURL: 'api/v1/graph/ql',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ router.get('/', (req, res) => {
|
||||
});
|
||||
|
||||
router.use('/account', require('./account'));
|
||||
router.use('/csp', require('./csp'));
|
||||
router.use('/assets', require('./assets'));
|
||||
router.use('/auth', require('./auth'));
|
||||
router.use('/graph', require('./graph'));
|
||||
|
||||
@@ -11,7 +11,7 @@ router.get('/id/:asset_id', async (req, res, next) => {
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
res.render('dev/article', {
|
||||
res.render('dev/article.njk', {
|
||||
title: asset.title,
|
||||
asset_id: asset.id,
|
||||
asset_url: asset.url,
|
||||
@@ -28,7 +28,7 @@ router.get('/random', (req, res) => {
|
||||
});
|
||||
|
||||
router.get('/title/:asset_title', (req, res) => {
|
||||
res.render('dev/article', {
|
||||
res.render('dev/article.njk', {
|
||||
title: req.params.asset_title.split('-').join(' '),
|
||||
asset_url: '',
|
||||
asset_id: null,
|
||||
@@ -48,7 +48,7 @@ router.get('/', async (req, res, next) => {
|
||||
Asset.count(),
|
||||
]);
|
||||
|
||||
res.render('dev/articles', {
|
||||
res.render('dev/articles.njk', {
|
||||
skip,
|
||||
limit,
|
||||
count,
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ router.get('/', staticTemplate, async (req, res) => {
|
||||
await SetupService.isAvailable();
|
||||
return res.redirect(url.resolve(MOUNT_PATH, 'admin/install'));
|
||||
} catch (e) {
|
||||
return res.render('dev/article', {
|
||||
return res.render('dev/article.njk', {
|
||||
title: 'Coral Talk',
|
||||
asset_url: '',
|
||||
asset_id: '',
|
||||
|
||||
@@ -2,7 +2,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/stream', (req, res) => {
|
||||
res.render('embed/stream');
|
||||
res.render('embed/stream.njk');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+10
-6
@@ -9,7 +9,9 @@ const path = require('path');
|
||||
const compression = require('compression');
|
||||
const plugins = require('../services/plugins');
|
||||
const staticTemplate = require('../middleware/staticTemplate');
|
||||
const staticMiddleware = require('express-static-gzip');
|
||||
const contentSecurityPolicy = require('../middleware/contentSecurityPolicy');
|
||||
const nonce = require('../middleware/nonce');
|
||||
const staticServer = require('express-static-gzip');
|
||||
const { DISABLE_STATIC_SERVER } = require('../config');
|
||||
const { passport } = require('../services/passport');
|
||||
const { MOUNT_PATH } = require('../url');
|
||||
@@ -48,7 +50,7 @@ if (!DISABLE_STATIC_SERVER) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
router.use(
|
||||
'/static',
|
||||
staticMiddleware(dist, {
|
||||
staticServer(dist, {
|
||||
indexFromEmptyFile: false,
|
||||
enableBrotli: true,
|
||||
customCompressions: [
|
||||
@@ -74,10 +76,12 @@ router.use(compression());
|
||||
// STATIC ROUTES
|
||||
//==============================================================================
|
||||
|
||||
router.use('/admin', staticTemplate, require('./admin'));
|
||||
router.use('/account', staticTemplate, require('./account'));
|
||||
router.use('/login', staticTemplate, require('./login'));
|
||||
router.use('/embed', staticTemplate, require('./embed'));
|
||||
const staticMiddleware = [staticTemplate, nonce, contentSecurityPolicy];
|
||||
|
||||
router.use('/admin', ...staticMiddleware, require('./admin'));
|
||||
router.use('/account', ...staticMiddleware, require('./account'));
|
||||
router.use('/login', ...staticMiddleware, require('./login'));
|
||||
router.use('/embed', ...staticMiddleware, require('./embed'));
|
||||
|
||||
//==============================================================================
|
||||
// PASSPORT MIDDLEWARE
|
||||
|
||||
@@ -2,7 +2,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
res.render('login');
|
||||
res.render('login.njk');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+4
-6
@@ -2,15 +2,13 @@ const express = require('express');
|
||||
const debug = require('debug')('talk:routes:plugins');
|
||||
const plugins = require('../services/plugins');
|
||||
const staticTemplate = require('../middleware/staticTemplate');
|
||||
const contentSecurityPolicy = require('../middleware/contentSecurityPolicy');
|
||||
const nonce = require('../middleware/nonce');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Routes mounted from plugins won't have access to our internal partials
|
||||
// directory, so we should make that available.
|
||||
router.use(staticTemplate, (req, res, next) => {
|
||||
res.locals.root = res.app.get('views');
|
||||
next();
|
||||
});
|
||||
// Apply the middleware.
|
||||
router.use(staticTemplate, nonce, contentSecurityPolicy);
|
||||
|
||||
// Inject server route plugins.
|
||||
plugins.get('server', 'router').forEach(plugin => {
|
||||
|
||||
+45
-31
@@ -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),
|
||||
};
|
||||
|
||||
@@ -115,13 +115,13 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
|
||||
res.locals.encodeJSONForHTML = encodeJSONForHTML;
|
||||
|
||||
if (err) {
|
||||
return res.render('auth-callback', {
|
||||
return res.render('auth-callback.njk', {
|
||||
auth: { err, data: null },
|
||||
});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.render('auth-callback', {
|
||||
return res.render('auth-callback.njk', {
|
||||
auth: { err: new ErrNotAuthorized(), data: null },
|
||||
});
|
||||
}
|
||||
@@ -132,7 +132,7 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
|
||||
SetTokenForSafari(req, res, token);
|
||||
|
||||
// We logged in the user! Let's send back the user data.
|
||||
res.render('auth-callback', {
|
||||
res.render('auth-callback.njk', {
|
||||
auth: { err: null, data: { user, token } },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('confirm_email.email_confirmation') %></title>
|
||||
<%- include ../../partials/account %>
|
||||
</head>
|
||||
<body class="confirm-email-page">
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1><%= t('confirm_email.email_confirmation') %></h1>
|
||||
<p><%= t('confirm_email.click_to_confirm') %></p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="verify-email-form">
|
||||
<button type="submit"><%= t('confirm_email.confirm') %></button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirectUri;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#verify-email-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,63 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('confirm_email.email_confirmation') }}{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1>{{ t('confirm_email.email_confirmation') }}</h1>
|
||||
<p>{{ t('confirm_email.click_to_confirm') }}</p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="verify-email-form">
|
||||
<button type="submit">{{ t('confirm_email.confirm') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirectUri;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/email/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#verify-email-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,85 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= t('password_reset.set_new_password') %></title>
|
||||
<%- include ../../partials/account %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1><%= t('password_reset.set_new_password') %></h1>
|
||||
<p><%= t('password_reset.change_password_help') %></p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="reset-password-form">
|
||||
<label for="password">
|
||||
<%= t('password_reset.new_password') %>
|
||||
<input type="password" name="password" placeholder="<%= t('password_reset.new_password') %>" />
|
||||
<small><%= t('password_reset.new_password_help') %></small>
|
||||
</label>
|
||||
<label for="confirm-password">
|
||||
<%= t('password_reset.confirm_new_password') %>
|
||||
<input type="password" name="confirm-password" placeholder="<%= t('password_reset.confirm_new_password') %>" />
|
||||
</label>
|
||||
<button type="submit"><%= t('password_reset.change_password') %></button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirm = $('[name="confirm-password"]').val();
|
||||
|
||||
if (password === '' || password.length < 8) {
|
||||
showError('Passwords must be at least 8 characters.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirm) {
|
||||
showError('New password and confirm password must match');
|
||||
return false;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirect;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#reset-password-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "templates/account.njk" %}
|
||||
|
||||
{% block title %}{{ t('password_reset.set_new_password') }}{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="root">
|
||||
<section class="container">
|
||||
<h1>{{ t('password_reset.set_new_password') }}</h1>
|
||||
<p>{{ t('password_reset.change_password_help') }}</p>
|
||||
<div class="error-console"><span></span></div>
|
||||
<form id="reset-password-form">
|
||||
<label for="password">
|
||||
{{ t('password_reset.new_password') }}
|
||||
<input type="password" name="password" placeholder="{{ t('password_reset.new_password') }}" />
|
||||
<small>{{ t('password_reset.new_password_help') }}</small>
|
||||
</label>
|
||||
<label for="confirm-password">
|
||||
{{ t('password_reset.confirm_new_password') }}
|
||||
<input type="password" name="confirm-password" placeholder="{{ t('password_reset.confirm_new_password') }}" />
|
||||
</label>
|
||||
<button type="submit">{{ t('password_reset.change_password') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script nonce="{{ nonce }}" type="text/javascript">
|
||||
$(function() {
|
||||
function showError(error) {
|
||||
try {
|
||||
let err = JSON.parse(error);
|
||||
$('.error-console span').text(err.message);
|
||||
$('.error-console').fadeIn();
|
||||
} catch (err) {
|
||||
$('.error-console span').text(error);
|
||||
$('.error-console').fadeIn();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit (e) {
|
||||
e.preventDefault();
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
var password = $('[name="password"]').val();
|
||||
var confirm = $('[name="confirm-password"]').val();
|
||||
|
||||
if (password === '' || password.length < 8) {
|
||||
showError('Passwords must be at least 8 characters.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirm) {
|
||||
showError('New password and confirm password must match');
|
||||
return false;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirect;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '{{ BASE_PATH }}api/v1/account/password/reset',
|
||||
contentType: 'application/json',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
|
||||
})
|
||||
.then(function () {
|
||||
$('#reset-password-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title>Talk - Coral Admin</title>
|
||||
<%- include partials/head %>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-admin/bundle.css') %>">
|
||||
<%- include partials/custom-css %>
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div id="root"></div>
|
||||
<script src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="<%= resolve('coral-admin/bundle.js') %>" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block title %}Talk Admin{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
|
||||
<link href="https://code.getmdl.io/1.2.1/material.min.css" rel="stylesheet">
|
||||
<link href="{{ resolve('coral-admin/bundle.css') }}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script nonce="{{ nonce }}" src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="{{ resolve('coral-admin/bundle.js') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,125 +0,0 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>GraphiQL</title>
|
||||
<meta name="robots" content="noindex" />
|
||||
<%- include ../partials/dev %>
|
||||
<style>
|
||||
html, body, #graphiql {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
*, ::after, ::before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
</style>
|
||||
<link href="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.css" rel="stylesheet" />
|
||||
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react-dom.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<main id="graphiql"></main>
|
||||
<script>
|
||||
// Collect the URL parameters
|
||||
var parameters = {};
|
||||
window.location.search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
// Produce a Location query string from a parameter object.
|
||||
function locationQuery(params, location) {
|
||||
return (location ? location: '') + '?' + Object.keys(params).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(params[key]);
|
||||
}).join('&');
|
||||
}
|
||||
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
||||
var graphqlParamNames = {
|
||||
query: true,
|
||||
variables: true,
|
||||
operationName: true
|
||||
};
|
||||
var otherParams = {};
|
||||
for (var k in parameters) {
|
||||
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
|
||||
otherParams[k] = parameters[k];
|
||||
}
|
||||
}
|
||||
// We don't use safe-serialize for location, because it's not client input.
|
||||
var fetchURL = locationQuery(otherParams, '<%= BASE_URL %><%= endpointURL %>');
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
var headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
try {
|
||||
let token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return fetch(fetchURL, {
|
||||
method: 'post',
|
||||
headers: headers,
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared.
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
function onEditOperationName(newOperationName) {
|
||||
parameters.operationName = newOperationName;
|
||||
updateURL();
|
||||
}
|
||||
function updateURL() {
|
||||
history.replaceState(null, null, locationQuery(parameters));
|
||||
}
|
||||
// Render <GraphiQL /> into the body.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName,
|
||||
query: null,
|
||||
response: null,
|
||||
variables: null,
|
||||
operationName: null,
|
||||
}),
|
||||
document.querySelector("#graphiql")
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,126 @@
|
||||
{% extends "templates/development.njk" %}
|
||||
|
||||
{% block title %}GraphiQL{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{# Include the base development pieces #}
|
||||
{{ super() }}
|
||||
<style type="text/css">
|
||||
html, body, #graphiql {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
*, ::after, ::before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
</style>
|
||||
<link href="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.css" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/react/15.0.0/react-dom.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/graphiql/0.9.1/graphiql.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
// Collect the URL parameters
|
||||
var parameters = {};
|
||||
window.location.search.substr(1).split('&').forEach(function (entry) {
|
||||
var eq = entry.indexOf('=');
|
||||
if (eq >= 0) {
|
||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
||||
decodeURIComponent(entry.slice(eq + 1));
|
||||
}
|
||||
});
|
||||
// Produce a Location query string from a parameter object.
|
||||
function locationQuery(params, location) {
|
||||
return (location ? location: '') + '?' + Object.keys(params).map(function (key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(params[key]);
|
||||
}).join('&');
|
||||
}
|
||||
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
||||
var graphqlParamNames = {
|
||||
query: true,
|
||||
variables: true,
|
||||
operationName: true
|
||||
};
|
||||
var otherParams = {};
|
||||
for (var k in parameters) {
|
||||
if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
|
||||
otherParams[k] = parameters[k];
|
||||
}
|
||||
}
|
||||
// We don't use safe-serialize for location, because it's not client input.
|
||||
var fetchURL = locationQuery(otherParams, '{{ BASE_URL }}{{ endpointURL }}');
|
||||
|
||||
// Defines a GraphQL fetcher using the fetch API.
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
var headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
try {
|
||||
let token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return fetch(fetchURL, {
|
||||
method: 'post',
|
||||
headers: headers,
|
||||
body: JSON.stringify(graphQLParams),
|
||||
credentials: 'include',
|
||||
}).then(function (response) {
|
||||
return response.text();
|
||||
}).then(function (responseBody) {
|
||||
try {
|
||||
return JSON.parse(responseBody);
|
||||
} catch (error) {
|
||||
return responseBody;
|
||||
}
|
||||
});
|
||||
}
|
||||
// When the query and variables string is edited, update the URL bar so
|
||||
// that it can be easily shared.
|
||||
function onEditQuery(newQuery) {
|
||||
parameters.query = newQuery;
|
||||
updateURL();
|
||||
}
|
||||
function onEditVariables(newVariables) {
|
||||
parameters.variables = newVariables;
|
||||
updateURL();
|
||||
}
|
||||
function onEditOperationName(newOperationName) {
|
||||
parameters.operationName = newOperationName;
|
||||
updateURL();
|
||||
}
|
||||
function updateURL() {
|
||||
history.replaceState(null, null, locationQuery(parameters));
|
||||
}
|
||||
// Render <GraphiQL /> into the body.
|
||||
ReactDOM.render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: graphQLFetcher,
|
||||
onEditQuery: onEditQuery,
|
||||
onEditVariables: onEditVariables,
|
||||
onEditOperationName: onEditOperationName,
|
||||
query: null,
|
||||
response: null,
|
||||
variables: null,
|
||||
operationName: null,
|
||||
}),
|
||||
document.querySelector("#graphiql")
|
||||
);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<main id="graphiql"></main>
|
||||
{% endblock %}
|
||||
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<%- include partials/data %>
|
||||
</head>
|
||||
<body>
|
||||
<script type="application/json" id="auth"><%- encodeJSONForHTML(auth) %></script>
|
||||
<script type="text/javascript" src="<%= resolve('coral-auth-callback/bundle.js') %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% include "partials/data.njk" %}
|
||||
</head>
|
||||
<body>
|
||||
<script type="application/json" id="auth">{{ encodeJSONForHTML(auth) }}</script>
|
||||
<script type="text/javascript" src="{{ resolve('coral-auth-callback/bundle.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,52 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta property="og:title" content="<%= title %>" />
|
||||
<meta property="og:description" content="A description of this article." />
|
||||
<meta property="article:author" content="A. J. Ournalist" />
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:image" content="https://coralproject.net/images/splash-md.jpg">
|
||||
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
|
||||
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
|
||||
<meta property="article:section" itemprop="articleSection" content="The Section!" />
|
||||
<title><%= title %></title>
|
||||
<%- include ../partials/dev %>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<div class="container">
|
||||
<h1 class="mt-3"><%= title %></h1>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script src="<%= resolve('embed.js') %>"></script>
|
||||
<script>
|
||||
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '<%= BASE_URL %>',
|
||||
asset_url: '<%= asset_url ? asset_url : '' %>',
|
||||
asset_id: '<%= asset_id ? asset_id : '' %>',
|
||||
auth_token: '',
|
||||
/**
|
||||
* You can listen to events using the example below.
|
||||
* The argument passed is the event emitter from
|
||||
* https://github.com/asyncly/EventEmitter2
|
||||
*
|
||||
* events: function(events) {
|
||||
* events.onAny(function(eventName, data) {
|
||||
* console.log(eventName, data);
|
||||
* });
|
||||
* },
|
||||
*/
|
||||
plugins_config: {
|
||||
/**
|
||||
* You can disable rendering slot components of a plugin by doing:
|
||||
*
|
||||
* 'talk-plugin-love': {
|
||||
* disable_components: true,
|
||||
* },
|
||||
*/
|
||||
test: 'data',
|
||||
debug: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "templates/development.njk" %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<meta property="og:title" content="{{ title }}" />
|
||||
<meta property="og:description" content="A description of this article." />
|
||||
<meta property="article:author" content="A. J. Ournalist" />
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:image" content="https://coralproject.net/images/splash-md.jpg">
|
||||
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
|
||||
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
|
||||
<meta property="article:section" itemprop="articleSection" content="The Section!" />
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div class="container">
|
||||
<h1 class="mt-3">{{ title }}</h1>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{{ resolve('embed.js') }}"></script>
|
||||
<script>
|
||||
window.TalkEmbed = Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
|
||||
talk: '{{ BASE_URL }}',
|
||||
asset_url: '{{ asset_url }}',
|
||||
asset_id: '{{ asset_id }}',
|
||||
auth_token: '',
|
||||
/**
|
||||
* You can listen to events using the example below.
|
||||
* The argument passed is the event emitter from
|
||||
* https://github.com/asyncly/EventEmitter2
|
||||
*
|
||||
* events: function(events) {
|
||||
* events.onAny(function(eventName, data) {
|
||||
* console.log(eventName, data);
|
||||
* });
|
||||
* },
|
||||
*/
|
||||
plugins_config: {
|
||||
/**
|
||||
* You can disable rendering slot components of a plugin by doing:
|
||||
*
|
||||
* 'talk-plugin-love': {
|
||||
* disable_components: true,
|
||||
* },
|
||||
*/
|
||||
test: 'data',
|
||||
debug: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,40 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>All Assets</title>
|
||||
<%- include ../partials/dev %>
|
||||
</head>
|
||||
<body>
|
||||
<%- include ../partials/dev-nav %>
|
||||
<div class="container">
|
||||
<div class="d-flex w-100 justify-content-between mt-3 mb-2">
|
||||
<h1 class="mb-0">All Assets</h1>
|
||||
<span class="text-muted"><%= skip + 1 %> - <%= skip + assets.length %> of <%= count %> Assets</span>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
<% if (skip === 0) { %><a href="<%= BASE_PATH %>dev/assets/random" class="list-group-item list-group-item-action list-group-item-primary"><i class="fa fa-plus" aria-hidden="true"></i> Create a random article</a><% } %>
|
||||
<% assets.forEach(function (asset) { %>
|
||||
<a href="<%= BASE_PATH %>dev/assets/id/<%= asset.id %>" class="list-group-item list-group-item-action flex-column align-items-start">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1"><%= asset.title %></h5>
|
||||
<small>Created <%= asset.created_at.toLocaleString('en-US') %></small>
|
||||
</div>
|
||||
<small><%= asset.url %></small>
|
||||
</a>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% if (count !== assets.length) { %>
|
||||
<nav aria-label="Page navigation example" class="mt-2">
|
||||
<ul class="pagination justify-content-center">
|
||||
<% let page = 1; for (let i = 0; i < count; i += limit) { %>
|
||||
<% if (i === skip) { %>
|
||||
<li class="page-item disabled"><a class="page-link" href="#"><%= page %></a></li>
|
||||
<% } else { %>
|
||||
<li class="page-item"><a class="page-link" href="?skip=<%= i %>&limit=<%= limit %>"><%= page %></a></li>
|
||||
<% } %>
|
||||
<% page++; } %>
|
||||
</ul>
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
{% extends "templates/development.njk" %}
|
||||
|
||||
{% block title %}All Assets{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div class="container">
|
||||
<div class="d-flex w-100 justify-content-between mt-3 mb-2">
|
||||
<h1 class="mb-0">All Assets</h1>
|
||||
<span class="text-muted">{{ skip + 1 }} - {{ skip + assets.length }} of {{ count }} Assets</span>
|
||||
</div>
|
||||
<div class="list-group">
|
||||
{% if skip === 0 %}<a href="{{ BASE_PATH }}dev/assets/random" class="list-group-item list-group-item-action list-group-item-primary"><i class="fa fa-plus" aria-hidden="true"></i> Create a random article</a>{% endif %}
|
||||
{% for asset in assets %}
|
||||
<a href="{{ BASE_PATH }}dev/assets/id/{{ asset.id }}" class="list-group-item list-group-item-action flex-column align-items-start">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ asset.title }}</h5>
|
||||
<small>Created {{ asset.created_at.toLocaleString('en-US') }}</small>
|
||||
</div>
|
||||
<small>{{ asset.url }}</small>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if count !== assets.length %}
|
||||
<nav aria-label="Page navigation example" class="mt-2">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% for i in range(0, count, limit) %}
|
||||
{% if i === skip %}
|
||||
<li class="page-item disabled"><a class="page-link" href="#">{{ i / limit + 1 | round }}</a></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{{ BASE_PATH }}dev/assets?skip={{ i }}&limit={{ limit }}">{{ i / limit + 1 | round }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<%- include ../partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/default.css') %>">
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('embed/stream/bundle.css') %>">
|
||||
<%- include ../partials/custom-css %>
|
||||
</head>
|
||||
<body class="embed-stream-page">
|
||||
<div id="talk-embed-stream-container"></div>
|
||||
<script src="<%= resolve('embed/stream/bundle.js') %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block title %}Talk{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{{ resolve('embed/stream/default.css')}}" rel="stylesheet">
|
||||
<link href="{{ resolve('embed/stream/bundle.css')}}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="talk-embed-stream-container"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{{ resolve('embed/stream/bundle.js')}}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<%- include partials/head %>
|
||||
<link rel="stylesheet" type="text/css" href="<%= resolve('coral-login/bundle.css') %>">
|
||||
<%- include partials/custom-css %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="talk-login-container"></div>
|
||||
<script src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="<%= resolve('coral-login/bundle.js') %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block title %}Talk - Login{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{{ resolve('coral-login/bundle.css')}}" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block html %}
|
||||
<div id="talk-login-container"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script nonce="{{ nonce }}" src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
|
||||
<script src="{{ resolve('coral-login/bundle.js')}}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,4 +0,0 @@
|
||||
<%- include head %>
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
|
||||
<%- include custom-css %>
|
||||
@@ -1 +0,0 @@
|
||||
<% if (locals.customCssUrl) { %><link href="<%= customCssUrl %>" rel="stylesheet" type="text/css"><% } %>
|
||||
@@ -0,0 +1,3 @@
|
||||
{% if customCssUrl %}
|
||||
<link nonce="{{ nonce }}" href="{{ customCssUrl }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
@@ -1,3 +0,0 @@
|
||||
<%_ if (data != null) { _%>
|
||||
<script id="data" type="application/json"><%- JSON.stringify(data) %></script>
|
||||
<%_ } _%>
|
||||
@@ -0,0 +1,3 @@
|
||||
{% if data %}
|
||||
<script id="data" type="application/json">{{ data | dump | safe }}</script>
|
||||
{% endif %}
|
||||
@@ -1,8 +0,0 @@
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="<%= BASE_PATH %>dev"><%= organizationName %> <span class="text-muted">Organization</span></a>
|
||||
<form class="form-inline mb-0">
|
||||
<a href="<%= BASE_PATH %>admin" class="btn btn-outline-primary mr-2"><i class="fa fa-lock" aria-hidden="true"></i> Admin</a>
|
||||
<a href="<%= BASE_PATH %>api/v1/graph/iql" class="btn btn-outline-secondary mr-2 graphiql"><i class="fa fa-terminal" aria-hidden="true"></i> Graph<em>i</em>QL</a>
|
||||
<a href="<%= BASE_PATH %>dev/assets" class="btn btn-outline-secondary"><i class="fa fa-list" aria-hidden="true"></i> All Assets</a>
|
||||
</form>
|
||||
</nav>
|
||||
@@ -0,0 +1,14 @@
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="{{ STATIC_URL }}public/img/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="{{ STATIC_URL }}public/img/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="{{ STATIC_URL }}public/img/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="{{ STATIC_URL }}public/img/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="{{ STATIC_URL }}public/img/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="{{ STATIC_URL }}public/img/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="{{ STATIC_URL }}public/img/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="{{ STATIC_URL }}public/img/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ STATIC_URL }}public/img/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ STATIC_URL }}public/img/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="{{ STATIC_URL }}public/img/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ STATIC_URL }}public/img/favicon-16x16.png">
|
||||
<link rel="manifest" href="{{ STATIC_URL }}public/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
@@ -1,23 +0,0 @@
|
||||
<meta charset="utf-8">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<%= STATIC_URL %>public/img/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<%= STATIC_URL %>public/img/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="<%= STATIC_URL %>public/img/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="<%= STATIC_URL %>public/img/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="<%= STATIC_URL %>public/img/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="<%= STATIC_URL %>public/img/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="<%= STATIC_URL %>public/img/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= STATIC_URL %>public/img/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= STATIC_URL %>public/img/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= STATIC_URL %>public/img/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="<%= STATIC_URL %>public/img/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= STATIC_URL %>public/img/favicon-16x16.png">
|
||||
<link rel="manifest" href="<%= STATIC_URL %>public/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
|
||||
|
||||
<%- include data %>
|
||||
|
||||
<base href="<%= BASE_URL %>"/>
|
||||
@@ -0,0 +1,8 @@
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="{{ BASE_PATH }}dev">{{ organizationName }} <span class="text-muted">Organization</span></a>
|
||||
<form class="form-inline mb-0">
|
||||
<a href="{{ BASE_PATH }}admin" class="btn btn-outline-primary mr-2"><i class="fa fa-lock" aria-hidden="true"></i> Admin</a>
|
||||
<a href="{{ BASE_PATH }}api/v1/graph/iql" class="btn btn-outline-secondary mr-2 graphiql"><i class="fa fa-terminal" aria-hidden="true"></i> Graph<em>i</em>QL</a>
|
||||
<a href="{{ BASE_PATH }}dev/assets" class="btn btn-outline-secondary"><i class="fa fa-list" aria-hidden="true"></i> All Assets</a>
|
||||
</form>
|
||||
</nav>
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="{{ BASE_PATH }}public/css/admin.css">
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{# Meta tags #}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
{% block meta %}{% endblock %}
|
||||
|
||||
{# Favicon Configuration #}
|
||||
{% include "partials/favicon.njk" %}
|
||||
|
||||
{# CSP and security headers #}
|
||||
{% block security %}{% endblock %}
|
||||
|
||||
{# Title #}
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
|
||||
{# CSS #}
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
|
||||
{% include "partials/custom-css.njk" %}
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
{# Static data injection #}
|
||||
{% include "partials/data.njk" %}
|
||||
|
||||
{# Configuration #}
|
||||
<base href="{{ BASE_URL }}"/>
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
{% block html %}
|
||||
<div id="root"></div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% block js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,17 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{% extends "templates/base.njk" %}
|
||||
|
||||
{# Null out the security block, we don't want/need that in development #}
|
||||
{% block security %}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600" rel="stylesheet">
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous" rel="stylesheet">
|
||||
<link href="<%= BASE_PATH %>public/css/dev.css" rel="stylesheet" >
|
||||
<link href="{{ BASE_PATH }}public/css/dev.css" rel="stylesheet" >
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% include "partials/nav.njk" %}
|
||||
|
||||
{% block html %}{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user