cryptpad/www/admin/inner.js

713 lines
27 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

define([
'jquery',
'/api/config',
'/bower_components/chainpad-crypto/crypto.js',
'/common/toolbar.js',
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/hyperscript.js',
'/customize/messages.js',
'/common/common-interface.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-signing-keys.js',
'/support/ui.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/admin/app-admin.less',
], function (
$,
ApiConfig,
Crypto,
Toolbar,
nThen,
SFCommon,
h,
Messages,
UI,
Util,
Hash,
Keys,
Support
)
{
var APP = {
'instanceStatus': {}
};
var common;
var sFrameChan;
var categories = {
'general': [
'cp-admin-flush-cache',
'cp-admin-update-limit',
// 'cp-admin-registration',
],
'quota': [
'cp-admin-defaultlimit',
'cp-admin-setlimit',
'cp-admin-getlimits',
],
'stats': [
'cp-admin-active-sessions',
'cp-admin-active-pads',
'cp-admin-open-files',
'cp-admin-registered',
'cp-admin-disk-usage',
],
'support': [
'cp-admin-support-list',
'cp-admin-support-init'
]
};
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-admin-' + key + ' cp-sidebarlayout-element'});
$('<label>').text(Messages['admin_'+safeKey+'Title'] || key).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
.text(Messages['admin_'+safeKey+'Hint'] || 'Coming soon...').appendTo($div);
if (addButton) {
$('<button>', {
'class': 'btn btn-primary'
}).text(Messages['admin_'+safeKey+'Button'] || safeKey).appendTo($div);
}
return $div;
};
create['update-limit'] = function () {
var key = 'update-limit';
var $div = makeBlock(key, true);
$div.find('button').click(function () {
sFrameChan.query('Q_UPDATE_LIMIT', null, function (e, res) {
if (e || (res && res.error)) { return void console.error(e || res.error); }
UI.alert(Messages.admin_updateLimitDone || 'done');
});
});
return $div;
};
create['flush-cache'] = function () {
var key = 'flush-cache';
var $div = makeBlock(key, true);
var called = false;
$div.find('button').click(function () {
if (called) { return; }
called = true;
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'FLUSH_CACHE',
}, function (e, data) {
called = false;
UI.alert(data ? Messages.admin_flushCacheDone || 'done' : 'error' + e);
});
});
return $div;
};
create['registration'] = function () {
var key = 'registration';
var $div = makeBlock(key, true);
var $button = $div.find('button');
var state = APP.instanceStatus.restrictRegistration;
if (state) {
$button.text(Messages.admin_registrationAllow);
} else {
$button.removeClass('btn-primary').addClass('btn-danger');
}
var called = false;
$div.find('button').click(function () {
called = true;
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['RESTRICT_REGISTRATION', [!state]]
}, function (e) {
if (e) { UI.warn(Messages.error); console.error(e); }
APP.updateStatus(function () {
called = false;
state = APP.instanceStatus.restrictRegistration;
if (state) {
console.log($button);
$button.text(Messages.admin_registrationAllow);
$button.addClass('btn-primary').removeClass('btn-danger');
} else {
$button.text(Messages.admin_registrationButton);
$button.removeClass('btn-primary').addClass('btn-danger');
}
});
});
});
return $div;
};
var getPrettySize = function (bytes) {
var unit = Util.magnitudeOfBytes(bytes);
var value = unit === 'GB' ? Util.bytesToGigabytes(bytes) : Util.bytesToMegabytes(bytes);
return unit === 'GB' ? Messages._getKey('formattedGB', [value])
: Messages._getKey('formattedMB', [value]);
};
create['defaultlimit'] = function () {
var key = 'defaultlimit';
var $div = makeBlock(key);
var _limit = APP.instanceStatus.defaultStorageLimit;
var _limitMB = Util.bytesToMegabytes(_limit);
var limit = getPrettySize(_limit);
var newLimit = h('input', {type: 'number', min: 0, value: _limitMB});
var set = h('button.btn.btn-primary', Messages.admin_setlimitButton);
$div.append(h('div', [
h('span.cp-admin-defaultlimit-value', Messages._getKey('admin_limit', [limit])),
h('div.cp-admin-setlimit-form', [
h('label', Messages.admin_defaultLimitMB),
newLimit,
h('nav', [set])
])
]));
UI.confirmButton(set, {
classes: 'btn-primary',
multiple: true,
validate: function () {
var l = parseInt($(newLimit).val());
if (isNaN(l)) { return false; }
return true;
}
}, function () {
var lMB = parseInt($(newLimit).val()); // Megabytes
var l = lMB * 1024 * 1024; // Bytes
var data = [l];
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['UPDATE_DEFAULT_STORAGE', data]
}, function (e) {
if (e) { UI.warn(Messages.error); return void console.error(e); }
var limit = getPrettySize(l);
$div.find('.cp-admin-defaultlimit-value').text(Messages._getKey('admin_limit', [limit]));
});
});
return $div;
};
create['getlimits'] = function () {
var key = 'getlimits';
var $div = makeBlock(key);
APP.refreshLimits = function () {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'GET_LIMITS',
}, function (e, data) {
if (e) { return; }
if (!Array.isArray(data) || !data[0]) { return; }
$div.find('.cp-admin-all-limits').remove();
var obj = data[0];
if (obj && (obj.message || obj.location)) {
delete obj.message;
delete obj.location;
}
var list = Object.keys(obj).sort(function (a, b) {
return obj[a].limit > obj[b].limit;
});
var compact = list.length > 10;
var content = list.map(function (key) {
var user = obj[key];
var limit = getPrettySize(user.limit);
var title = Messages._getKey('admin_limit', [limit]) + ', ' +
Messages._getKey('admin_limitPlan', [user.plan]) + ', ' +
Messages._getKey('admin_limitNote', [user.note]);
var keyEl = h('code.cp-limit-key', key);
$(keyEl).click(function () {
$('.cp-admin-setlimit-form').find('.cp-setlimit-key').val(key);
$('.cp-admin-setlimit-form').find('.cp-setlimit-quota').val(Math.floor(user.limit/1024));
$('.cp-admin-setlimit-form').find('.cp-setlimit-note').val(user.note);
});
if (compact) {
return h('tr.cp-admin-limit', {
title: title
}, [
h('td', keyEl),
h('td.limit', Messages._getKey('admin_limit', [limit])),
h('td.plan', Messages._getKey('admin_limitPlan', [user.plan])),
h('td.note', Messages._getKey('admin_limitNote', [user.note]))
]);
}
return h('li.cp-admin-limit', [
keyEl,
h('ul.cp-limit-data', [
h('li.limit', Messages._getKey('admin_limit', [limit])),
h('li.plan', Messages._getKey('admin_limitPlan', [user.plan])),
h('li.note', Messages._getKey('admin_limitNote', [user.note]))
])
]);
});
if (compact) { return $div.append(h('table.cp-admin-all-limits', content)); }
$div.append(h('ul.cp-admin-all-limits', content));
});
};
APP.refreshLimits();
return $div;
};
create['setlimit'] = function () {
var key = 'setlimit';
var $div = makeBlock(key);
var user = h('input.cp-setlimit-key');
var $key = $(user);
var limit = h('input.cp-setlimit-quota', {type: 'number', min: 0, value: 0});
var note = h('input.cp-setlimit-note');
var remove = h('button.btn.btn-danger', Messages.fc_remove);
var set = h('button.btn.btn-primary', Messages.admin_setlimitButton);
var form = h('div.cp-admin-setlimit-form', [
h('label', Messages.admin_limitUser),
user,
h('label', Messages.admin_limitMB),
limit,
h('label', Messages.admin_limitSetNote),
note,
h('nav', [set, remove])
]);
var getValues = function () {
var key = $key.val();
var _limit = parseInt($(limit).val());
var _note = $(note).val();
if (key.length !== 44) {
try {
var u = Keys.parseUser(key);
if (!u.domain || !u.user || !u.pubkey) {
return void UI.warn(Messages.admin_invalKey);
}
} catch (e) {
return void UI.warn(Messages.admin_invalKey);
}
}
if (isNaN(_limit) || _limit < 0) {
return void UI.warn(Messages.admin_invalLimit);
}
return {
key: key,
data: {
limit: _limit * 1024 * 1024,
note: _note,
plan: 'custom'
}
};
};
UI.confirmButton(remove, {
classes: 'btn-danger',
multiple: true,
validate: function () {
var obj = getValues();
if (!obj || !obj.key) { return false; }
return true;
}
}, function () {
var obj = getValues();
var data = [obj.key];
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['RM_QUOTA', data]
}, function (e) {
if (e) { UI.warn(Messages.error); console.error(e); }
APP.refreshLimits();
$key.val('');
});
});
$(set).click(function () {
var obj = getValues();
if (!obj || !obj.key) { return; }
var data = [obj.key, obj.data];
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ADMIN_DECREE',
data: ['SET_QUOTA', data]
}, function (e) {
if (e) { UI.warn(Messages.error); console.error(e); }
APP.refreshLimits();
$key.val('');
});
});
$div.append(form);
return $div;
};
create['active-sessions'] = function () {
var key = 'active-sessions';
var $div = makeBlock(key);
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ACTIVE_SESSIONS',
}, function (e, data) {
var total = data[0];
var ips = data[1];
$div.append(h('pre', total + ' (' + ips + ')'));
});
return $div;
};
create['active-pads'] = function () {
var key = 'active-pads';
var $div = makeBlock(key);
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'ACTIVE_PADS',
}, function (e, data) {
console.log(e, data);
$div.append(h('pre', String(data)));
});
return $div;
};
create['open-files'] = function () {
var key = 'open-files';
var $div = makeBlock(key);
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'GET_FILE_DESCRIPTOR_COUNT',
}, function (e, data) {
console.log(e, data);
$div.append(h('pre', String(data)));
});
return $div;
};
create['registered'] = function () {
var key = 'registered';
var $div = makeBlock(key);
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'REGISTERED_USERS',
}, function (e, data) {
console.log(e, data);
$div.append(h('pre', String(data)));
});
return $div;
};
create['disk-usage'] = function () {
var key = 'disk-usage';
var $div = makeBlock(key, true);
var called = false;
$div.find('button').click(function () {
$div.find('button').hide();
if (called) { return; }
called = true;
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'DISK_USAGE',
}, function (e, data) {
console.log(e, data);
if (e) { return void console.error(e); }
var obj = data[0];
Object.keys(obj).forEach(function (key) {
var val = obj[key];
var unit = Util.magnitudeOfBytes(val);
if (unit === 'GB') {
obj[key] = Util.bytesToGigabytes(val) + ' GB';
} else if (unit === 'MB') {
obj[key] = Util.bytesToMegabytes(val) + ' MB';
} else {
obj[key] = Util.bytesToKilobytes(val) + ' KB';
}
});
$div.append(h('ul', Object.keys(obj).map(function (k) {
return h('li', [
h('strong', k === 'total' ? k : '/' + k),
' : ',
obj[k]
]);
})));
});
});
return $div;
};
var supportKey = ApiConfig.supportMailbox;
create['support-list'] = function () {
if (!supportKey || !APP.privateKey) { return; }
var $container = makeBlock('support-list');
var $div = $(h('div.cp-support-container')).appendTo($container);
var catContainer = h('div.cp-dropdown-container');
$div.append(catContainer);
var category = 'all';
var $drop = APP.support.makeCategoryDropdown(catContainer, function (key) {
category = key;
if (key === 'all') {
$div.find('.cp-support-list-ticket').show();
return;
}
$div.find('.cp-support-list-ticket').hide();
$div.find('.cp-support-list-ticket[data-cat="'+key+'"]').show();
}, true);
$drop.setValue('all');
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var cat = privateData.category || '';
var linkedId = cat.indexOf('-') !== -1 && cat.slice(8);
var hashesById = {};
var reorder = function () {
var order = Object.keys(hashesById);
order.sort(function (id1, id2) {
var t1 = hashesById[id1];
var t2 = hashesById[id2];
if (!Array.isArray(t1)) { return 1; }
if (!Array.isArray(t2)) { return -1; }
var lastMsg1 = t1[t1.length - 1];
var lastMsg2 = t2[t2.length - 1];
var time1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'time']);
var time2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'time']);
var authorEd1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'sender', 'edPublic']);
var authorEd2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'sender', 'edPublic']);
var admin1 = ApiConfig.adminKeys.indexOf(authorEd1) !== -1;
var admin2 = ApiConfig.adminKeys.indexOf(authorEd2) !== -1;
// If one is answered and not the other, put the unanswered first
if (admin1 && !admin2) { return 1; }
if (!admin1 && admin2) { return -1; }
// Otherwise, sort them by time
return time2 - time1;
});
order.forEach(function (id, i) {
$div.find('[data-id="'+id+'"]').css('order', i);
});
};
var to = Util.throttle(function () {
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+linkedId+'"]');
$ticket[0].scrollIntoView();
linkedId = undefined;
}, 100);
// Register to the "support" mailbox
common.mailbox.subscribe(['supportadmin'], {
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;
var content = msg.content;
var id = content.id;
var $ticket = $div.find('.cp-support-list-ticket[data-id="'+id+'"]');
hashesById[id] = hashesById[id] || [];
if (hashesById[id].indexOf(hash) === -1) {
hashesById[id].push(data);
}
if (msg.type === 'CLOSE') {
// A ticket has been closed by the admins...
if (!$ticket.length) { return; }
$ticket.addClass('cp-support-list-closed');
$ticket.append(APP.support.makeCloseMessage(content, hash));
return;
}
if (msg.type !== 'TICKET') { return; }
if (!$ticket.length) {
$ticket = APP.support.makeTicket($div, content, function (hideButton) {
// the ticket will still be displayed until the worker confirms its deletion
// so make it unclickable in the meantime
hideButton.setAttribute('disabled', true);
var error = false;
nThen(function (w) {
hashesById[id].forEach(function (d) {
common.mailbox.dismiss(d, w(function (err) {
if (err) {
error = true;
console.error(err);
}
}));
});
}).nThen(function () {
if (!error) { return void $ticket.remove(); }
// if deletion failed then reactivate the button and warn
hideButton.removeAttribute('disabled');
// and show a generic error message
UI.alert(Messages.error);
});
});
if (category !== 'all' && $ticket.attr('data-cat') !== category) {
$ticket.hide();
}
}
$ticket.append(APP.support.makeMessage(content, hash));
reorder();
if (linkedId) { to(); }
}
});
return $container;
};
var checkAdminKey = function (priv) {
if (!supportKey) { return; }
return Hash.checkBoxKeyPair(priv, supportKey);
};
create['support-init'] = function () {
var $div = makeBlock('support-init');
if (!supportKey) {
$div.append(h('p', Messages.admin_supportInitHelp));
return $div;
}
if (!APP.privateKey || !checkAdminKey(APP.privateKey)) {
$div.append(h('p', Messages.admin_supportInitPrivate));
var error = h('div.cp-admin-support-error');
var input = h('input.cp-admin-add-private-key');
var button = h('button.btn.btn-primary', Messages.admin_supportAddKey);
if (APP.privateKey && !checkAdminKey(APP.privateKey)) {
$(error).text(Messages.admin_supportAddError);
}
$div.append(h('div', [
error,
input,
button
]));
$(button).click(function () {
var key = $(input).val();
if (!checkAdminKey(key)) {
$(input).val('');
return void $(error).text(Messages.admin_supportAddError);
}
sFrameChan.query("Q_ADMIN_MAILBOX", key, function () {
APP.privateKey = key;
$('.cp-admin-support-init').hide();
APP.$rightside.append(create['support-list']());
});
});
return $div;
}
return;
};
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 || 'general';
if (active.indexOf('-') !== -1) {
active = active.split('-')[0];
}
common.setHash(active);
Object.keys(categories).forEach(function (key) {
var $category = $('<div>', {'class': 'cp-sidebarlayout-category'}).appendTo($categories);
if (key === 'general') { $category.append($('<span>', {'class': 'fa fa-user-o'})); }
if (key === 'stats') { $category.append($('<span>', {'class': 'fa fa-line-chart'})); }
if (key === 'quota') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); }
if (key === 'support') { $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['admin_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.adminPage || 'Admin',
metadataMgr: common.getMetadataMgr(),
};
APP.toolbar = Toolbar.create(configTb);
APP.toolbar.$rightside.hide();
};
var updateStatus = APP.updateStatus = function (cb) {
sFrameChan.query('Q_ADMIN_RPC', {
cmd: 'INSTANCE_STATUS',
}, function (e, data) {
if (e) { console.error(e); return void cb(e); }
if (!Array.isArray(data)) { return void cb('EINVAL'); }
APP.instanceStatus = data[0];
console.log("Status", APP.instanceStatus);
cb();
});
};
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) {
updateStatus(waitFor());
}).nThen(function (/*waitFor*/) {
createToolbar();
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
common.setTabTitle(Messages.adminPage || 'Administration');
if (!privateData.edPublic || !ApiConfig.adminKeys || !Array.isArray(ApiConfig.adminKeys)
|| ApiConfig.adminKeys.indexOf(privateData.edPublic) === -1) {
return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
}
APP.privateKey = privateData.supportPrivateKey;
APP.origin = privateData.origin;
APP.readOnly = privateData.readOnly;
APP.support = Support.create(common, true);
// Content
var $rightside = APP.$rightside;
var addItem = function (cssClass) {
var item = cssClass.slice(9); // remove 'cp-settings-'
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();
});
});