Merge remote-tracking branch 'origin/support' into notifications

pull/1/head
ClemDee 5 years ago
commit a62f2ff003

@ -139,6 +139,11 @@
@colortheme_notifications-color: #FFF;
@colortheme_notifications-warn: #ffae00;
@colortheme_support-bg: #42d1f4;
@colortheme_support-color: #000;
@colortheme_support-warn: #9A37F7;
// Sidebar layout (profile / settings)
@colortheme_sidebar-active: #fff;
@colortheme_sidebar-left-bg: #eee;

@ -0,0 +1,26 @@
const Nacl = require('tweetnacl');
const Crypto = require('crypto');
const keyPair = Nacl.box.keyPair();
console.log(keyPair);
console.log("You've just generated a new key pair for your support mailbox.");
console.log("The public key should first be added to your config.js file ('supportMailboxPublicKey'), then save and restart the server.")
console.log("Once restarted, administrators (specified with 'adminKeys' in config.js too) will be able to add the private key into their account. This can be done using the administration panel.");
console.log("You will have to send the private key to each administrator manually so that they can add it to their account.");
console.log();
console.log("WARNING: the public and private keys must come from the same key pair to have a working encrypted support mailbox.");
console.log();
console.log("NOTE: You can change the key pair at any time if you want to revoke access to the support mailbox. You just have to generate a new key pair using this file, and replace the value in config.js, and then send the new private key to the administrators of your choice.");
console.log();
console.log();
console.log("Your public key (add it to config.js):");
console.log(Nacl.util.encodeBase64(keyPair.publicKey));
console.log();
console.log();
console.log("Your private key (store it in a safe place and send it to your instance's admins):");
console.log(Nacl.util.encodeBase64(keyPair.secretKey));

@ -193,6 +193,7 @@ app.get('/api/config', function(req, res){
httpUnsafeOrigin: config.httpUnsafeOrigin,
adminEmail: config.adminEmail,
adminKeys: admins,
supportMailbox: config.supportMailboxPublicKey
}, null, '\t'),
'obj.httpSafeOrigin = ' + (function () {
if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; }

@ -17,6 +17,6 @@ define(function () {
// Sub
plan: 'CryptPad_plan',
// Apps
criticalApps: ['profile', 'settings', 'debug', 'admin', 'notifications']
criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications']
};
});

@ -85,6 +85,11 @@ define([
return id;
};
Hash.getChannelIdFromKey = function (publicKey) {
if (!publicKey) { return; }
return uint8ArrayToHex(Hash.decodeBase64(publicKey).subarray(0,16));
};
Hash.createRandomHash = function (type, password) {
var cryptor;
if (type === 'file') {

@ -478,6 +478,7 @@ define([
settings: store.proxy.settings,
thumbnails: disableThumbnails === false,
isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])),
support: Util.find(store.proxy, ['mailboxes', 'support', 'channel']),
pendingFriends: store.proxy.friends_pending || {}
}
};

@ -10,6 +10,7 @@ define([
var TYPES = [
'notifications',
'support'
];
var BLOCKING_TYPES = [
];
@ -25,6 +26,16 @@ define([
if (res.error) { console.error(res); }
});
}
if (!mailboxes['support']) {
mailboxes.support = {
channel: Hash.createChannelId(),
lastKnownHash: '',
viewed: []
};
ctx.pinPads([mailboxes.support.channel], function (res) {
if (res.error) { console.error(res); }
});
}
};
/*
@ -76,15 +87,30 @@ proxy.mailboxes = {
var crypto = Crypto.Mailbox.createEncryptor(keys);
var network = ctx.store.network;
var ciphertext = crypto.encrypt(JSON.stringify({
var text = JSON.stringify({
type: type,
content: msg
}), user.curvePublic);
});
var ciphertext = crypto.encrypt(text, user.curvePublic);
network.join(user.channel).then(function (wc) {
wc.bcast(ciphertext).then(function () {
cb();
wc.leave();
// If we've just sent a message to one of our mailboxes, we have to trigger the handler manually
// (the server won't send back our message to us)
var box;
if (Object.keys(ctx.boxes).some(function (t) {
var _box = ctx.boxes[t];
if (_box.channel === user.channel) {
box = _box;
return true;
}
})) {
var hash = ciphertext.slice(0, 64);
box.onMessage(text, null, null, null, hash, user.curvePublic);
}
});
}, function (err) {
cb({error: err});
@ -157,6 +183,7 @@ proxy.mailboxes = {
var openChannel = function (ctx, type, m, onReady) {
var box = ctx.boxes[type] = {
channel: m.channel,
queue: [], // Store the messages to send when the channel is ready
history: [], // All the hashes loaded from the server in corretc order
content: {}, // Content of the messages that should be displayed
@ -213,7 +240,7 @@ proxy.mailboxes = {
});
box.queue = [];
};
cfg.onMessage = function (msg, user, vKey, isCp, hash, author) {
box.onMessage = cfg.onMessage = function (msg, user, vKey, isCp, hash, author) {
if (hash === m.lastKnownHash) { return; }
try {
msg = JSON.parse(msg);

@ -90,8 +90,11 @@ define([
var pushMessage = function (data, handler) {
var todo = function (f) {
try {
var el = createElement(data);
var el;
if (data.type === 'notifications') {
el = createElement(data);
Notifications.add(Common, data, el);
}
f(data, el);
} catch (e) {
console.error(e);
@ -108,7 +111,9 @@ define([
onViewedHandlers.forEach(function (f) {
try {
f(data);
if (data.type === 'notifications') {
Notifications.remove(Common, data);
}
} catch (e) {
console.error(e);
}
@ -139,18 +144,25 @@ define([
var subscribed = false;
// Get all existing notifications + the new ones when they come
mailbox.subscribe = function (cfg) {
mailbox.subscribe = function (types, cfg) {
if (!subscribed) {
execCommand('SUBSCRIBE', null, function () {});
subscribed = true;
}
if (typeof(cfg.onViewed) === "function") {
onViewedHandlers.push(cfg.onViewed);
onViewedHandlers.push(function (data) {
if (types.indexOf(data.type) === -1) { return; }
cfg.onViewed(data);
});
}
if (typeof(cfg.onMessage) === "function") {
onMessageHandlers.push(cfg.onMessage);
onMessageHandlers.push(function (data, el) {
if (types.indexOf(data.type) === -1) { return; }
cfg.onMessage(data, el);
});
}
Object.keys(history).forEach(function (type) {
if (types.indexOf(type) === -1) { return; }
history[type].forEach(function (data) {
pushMessage({
type: type,

@ -983,7 +983,7 @@ MessengerUI, Messages) {
$button.addClass('fa-bell');
};
Common.mailbox.subscribe({
Common.mailbox.subscribe(['notifications'], {
onMessage: function (data, el) {
if (el) {
div.appendChild(el);

@ -0,0 +1,19 @@
@import (reference) '../../customize/src/less2/include/framework.less';
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
&.cp-app-support {
.framework_min_main(
@bg-color: @colortheme_support-bg,
@warn-color: @colortheme_support-warn,
@color: @colortheme_support-color
);
.sidebar-layout_main();
.cp-hidden {
display: none !important;
}
display: flex;
flex-flow: column;
}

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css" rel="stylesheet" type="text/css">
</head>
<body>
<iframe id="sbox-iframe">

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/support/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
</style>
</head>
<body class="cp-app-support">
<div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-sidebarlayout-container"></div>
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</body>
</html>

@ -0,0 +1,370 @@
define([
'jquery',
'/common/toolbar3.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/common-util.js',
'/common/common-hash.js',
'/customize/messages.js',
'/common/hyperscript.js',
'/api/config',
'/common/common-feedback.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/support/app-support.less',
], function (
$,
Toolbar,
nThen,
SFCommon,
UI,
UIElements,
Util,
Hash,
Messages,
h,
ApiConfig,
Feedback
)
{
var saveAs = window.saveAs;
var APP = window.APP = {};
var common;
var metadataMgr;
var privateData;
var sframeChan;
var categories = {
'tickets': [
'cp-support-list',
],
'new': [
'cp-support-form',
],
};
var supportKey = ApiConfig.supportMailbox; // XXX curvePublic
var supportChannel = Hash.getChannelIdFromKey(supportKey); // XXX
if (true || !supportKey || !supportChannel) {
categories = {
'tickets': [
'cp-support-disabled'
]
};
}
var create = {};
var makeBlock = function (key, addButton) {
// Convert to camlCase for translation keys
var safeKey = key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
var $div = $('<div>', {'class': 'cp-support-' + key + ' cp-sidebarlayout-element'});
$('<label>').text(Messages['support_'+safeKey+'Title'] || key).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages['support_'+safeKey+'Hint'] || 'Coming soon...').appendTo($div);
if (addButton) {
$('<button>', {
'class': 'btn btn-primary'
}).text(Messages['support_'+safeKey+'Button'] || safeKey).appendTo($div);
}
return $div;
};
var showError = function (form, msg) {
if (!msg) {
return void $(form).find('.cp-support-form-error').text('').hide();
}
$(form).find('.cp-support-form-error').text(msg).show();
};
var makeForm = function (cb, title) {
var button;
if (typeof(cb) === "function") {
button = h('button.btn.btn-primary.cp-support-list-send', Messages.support_send || 'Send'); // XXX
$(button).click(cb);
}
var content = [
h('hr'),
h('div.cp-support-form-error'),
h('label' + (title ? '.cp-hidden' : ''), Messages.support_formTitle || 'title...'), // XXX
h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), {
placeholder: Messages.support_formTitlePlaceholder || 'title here...', // XXX
value: title
}),
cb ? undefined : h('br'),
h('label', Messages.support_formMessage || 'content...'), // XXX
h('textarea.cp-support-form-msg', {
placeholder: Messages.support_formMessagePlaceholder || 'describe your problem here...' // XXX
}),
h('hr'),
button
];
return h('div.cp-support-form-container', content);
};
var sendForm = function (id, form) {
var user = metadataMgr.getUserData();
privateData = metadataMgr.getPrivateData();
var $title = $(form).find('.cp-support-form-title');
var $content = $(form).find('.cp-support-form-msg');
var title = $title.val();
if (!title) {
return void showError(form, Messages.support_formTitleError || 'title error'); // XXX
}
var content = $content.val();
if (!content) {
return void showError(form, Messages.support_formContentError || 'content error'); // XXX
}
// Success: hide any error
showError(form, null);
$content.val('');
$title.val('');
common.mailbox.sendTo('TICKET', {
sender: {
name: user.name,
channel: privateData.support,
curvePublic: user.curvePublic,
edPublic: privateData.edPublic
},
title: title,
message: content,
id: id
}, {
channel: supportChannel,
curvePublic: supportKey
});
common.mailbox.sendTo('TICKET', {
sender: {
name: user.name,
channel: privateData.support,
curvePublic: user.curvePublic,
edPublic: privateData.edPublic
},
title: title,
message: content,
id: id
}, {
channel: privateData.support,
curvePublic: user.curvePublic
});
return true;
};
// List existing (open?) tickets
create['list'] = function () {
var key = 'list';
var $div = makeBlock(key);
var makeTicket = function (content) {
var ticketTitle = content.id + ' - ' + content.title;
var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer || 'Answer'); // XXX
var $ticket = $(h('div.cp-support-list-ticket', {
'data-id': content.id
}, [
h('h2', ticketTitle),
h('div.cp-support-list-actions', answer)
]));
$(answer).click(function () {
$div.find('.cp-support-form-container').remove();
$div.find('.cp-support-answer').show();
$(answer).hide();
var form = makeForm(function () {
var sent = sendForm(content.id, form);
if (sent) {
$(answer).show();
$(form).remove();
}
}, content.title);
$ticket.append(form);
});
$div.append($ticket);
return $ticket;
};
var makeMessage = function (content, hash) {
// Check content.sender to see if it comes from us or from an admin
// XXX admins should send their personal public key?
var fromMe = content.sender && content.sender.edPublic === privateData.edPublic;
return h('div.cp-support-list-message', [
h('p.cp-support-message-from' + fromMe ? '.cp-support-fromme' : '',
//Messages._getKey('support_from', [content.sender.name])), // XXX
[h('b', 'From: '), content.sender.name]),
h('pre.cp-support-message-content', content.message)
]);
};
// Register to the "support" mailbox
common.mailbox.subscribe(['support'], {
onMessage: function (data) {
/*
Get ID of the ticket
If we already have a div for this ID
Push the message to the end of the ticket
If it's a new ticket ID
Make a new div for this ID
*/
var msg = data.content.msg;
var hash = data.content.hash;
if (msg.type === 'CLOSE') {
// A ticket has been closed by the admins...
// TODO: add a "closed" class to the ticket in the UI
}
if (msg.type !== 'TICKET') { return; }
var content = msg.content;
var id = content.id;
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]');
if (!$ticket.length) {
$ticket = makeTicket(content);
}
$ticket.append(makeMessage(content, hash));
},
onViewed: function (data) {
// Remove the ticket with this hash
// If the ticket div is empty, remove the ticket div
}
});
return $div;
};
// Create a new tickets
create['form'] = function () {
var key = 'form';
var $div = makeBlock(key, true);
var form = makeForm();
$div.find('button').before(form);
var id = Util.uid();
$div.find('button').click(function () {
var sent = sendForm(id, form);
if (sent) {
$('.cp-sidebarlayout-category[data-category="tickets"]').click();
}
});
return $div;
};
// Support is disabled...
create['disabled'] = function () {
var key = 'disabled';
var $div = makeBlock(key);
// XXX add text
return $div;
};
var hideCategories = function () {
APP.$rightside.find('> div').hide();
};
var showCategories = function (cat) {
hideCategories();
cat.forEach(function (c) {
APP.$rightside.find('.'+c).show();
});
};
var createLeftside = function () {
var $categories = $('<div>', {'class': 'cp-sidebarlayout-categories'})
.appendTo(APP.$leftside);
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var active = privateData.category || 'tickets';
common.setHash(active);
Object.keys(categories).forEach(function (key) {
var $category = $('<div>', {
'class': 'cp-sidebarlayout-category',
'data-category': key
}).appendTo($categories);
if (key === 'tickets') { $category.append($('<span>', {'class': 'fa fa-envelope-o'})); }
if (key === 'new') { $category.append($('<span>', {'class': 'fa fa-life-ring'})); }
if (key === active) {
$category.addClass('cp-leftside-active');
}
$category.click(function () {
if (!Array.isArray(categories[key]) && categories[key].onClick) {
categories[key].onClick();
return;
}
active = key;
common.setHash(key);
$categories.find('.cp-leftside-active').removeClass('cp-leftside-active');
$category.addClass('cp-leftside-active');
showCategories(categories[key]);
});
$category.append(Messages['support_cat_'+key] || key);
});
showCategories(categories[active]);
};
var createToolbar = function () {
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
var configTb = {
displayed: displayed,
sfCommon: common,
$container: APP.$toolbar,
pageTitle: Messages.supportPage || 'Support', // XXX
metadataMgr: common.getMetadataMgr(),
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.hide();
};
nThen(function (waitFor) {
$(waitFor(UI.addLoadingScreen));
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
}).nThen(function (waitFor) {
APP.$container = $('#cp-sidebarlayout-container');
APP.$toolbar = $('#cp-toolbar');
APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container);
sFrameChan = common.getSframeChannel();
sFrameChan.onReady(waitFor());
}).nThen(function (/*waitFor*/) {
createToolbar();
metadataMgr = common.getMetadataMgr();
privateData = metadataMgr.getPrivateData();
common.setTabTitle(Messages.supportPage || 'Support');
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
// Content
var $rightside = APP.$rightside;
var addItem = function (cssClass) {
var item = cssClass.slice(11); // remove 'cp-support-'
if (typeof (create[item]) === "function") {
$rightside.append(create[item]());
}
};
for (var cat in categories) {
if (!Array.isArray(categories[cat])) { continue; }
categories[cat].forEach(addItem);
}
createLeftside();
UI.removeLoadingScreen();
});
});

@ -0,0 +1,55 @@
// Load #1, load as little as possible because we are in a race to get the loading screen up.
define([
'/bower_components/nthen/index.js',
'/api/config',
'/common/dom-ready.js',
'/common/requireconfig.js',
'/common/sframe-common-outer.js'
], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) {
var requireConfig = RequireConfig();
// Loaded in load #2
nThen(function (waitFor) {
DomReady.onReady(waitFor());
}).nThen(function (waitFor) {
var req = {
cfg: requireConfig,
req: [ '/common/loading.js' ],
pfx: window.location.origin
};
window.rc = requireConfig;
window.apiconf = ApiConfig;
document.getElementById('sbox-iframe').setAttribute('src',
ApiConfig.httpSafeOrigin + '/support/inner.html?' + requireConfig.urlArgs +
'#' + encodeURIComponent(JSON.stringify(req)));
// This is a cheap trick to avoid loading sframe-channel in parallel with the
// loading screen setup.
var done = waitFor();
var onMsg = function (msg) {
var data = JSON.parse(msg.data);
if (data.q !== 'READY') { return; }
window.removeEventListener('message', onMsg);
var _done = done;
done = function () { };
_done();
};
window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) {
var addRpc = function (sframeChan, Cryptpad, Utils) {
};
var category;
if (window.location.hash) {
category = window.location.hash.slice(1);
window.location.hash = '';
}
var addData = function (obj) {
if (category) { obj.category = category; }
};
SFCommonO.start({
noRealtime: true,
addRpc: addRpc,
addData: addData
});
});
});
Loading…
Cancel
Save