mirror of https://github.com/xwiki-labs/cryptpad
3982 lines
155 KiB
JavaScript
3982 lines
155 KiB
JavaScript
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||
//
|
||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
||
define([
|
||
'jquery',
|
||
'/common/toolbar.js',
|
||
'/common/pad-types.js',
|
||
'/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',
|
||
'/common/inner/sidebar-layout.js',
|
||
'/customize/messages.js',
|
||
'/common/common-signing-keys.js',
|
||
'/common/hyperscript.js',
|
||
'/common/clipboard.js',
|
||
'json.sortify',
|
||
'/api/config',
|
||
'/api/instance',
|
||
'/lib/datepicker/flatpickr.js',
|
||
'/install/onboardscreen.js',
|
||
|
||
'css!/lib/datepicker/flatpickr.min.css',
|
||
'css!/components/bootstrap/dist/css/bootstrap.min.css',
|
||
'css!/components/components-font-awesome/css/font-awesome.min.css',
|
||
'less!/admin/app-admin.less',
|
||
], function(
|
||
$,
|
||
Toolbar,
|
||
PadTypes,
|
||
nThen,
|
||
SFCommon,
|
||
UI,
|
||
UIElements,
|
||
Util,
|
||
Hash,
|
||
Sidebar,
|
||
Messages,
|
||
Keys,
|
||
h,
|
||
Clipboard,
|
||
Sortify,
|
||
ApiConfig,
|
||
Instance,
|
||
Flatpickr,
|
||
Onboarding,
|
||
) {
|
||
|
||
var APP = window.APP = {};
|
||
|
||
var Nacl = window.nacl;
|
||
var common;
|
||
var sFrameChan;
|
||
|
||
var andThen = function (common, $container) {
|
||
const sidebar = Sidebar.create(common, 'admin', $container);
|
||
// UNUSED-TRANSLATIONS:START
|
||
var categories = {
|
||
'general': { // Msg.admin_cat_general
|
||
icon: 'fa fa-user-o',
|
||
content: [
|
||
'instance-info-notice',
|
||
'name',
|
||
'description',
|
||
'email',
|
||
'jurisdiction',
|
||
'flush-cache'
|
||
]
|
||
},
|
||
'customize': { // Msg.admin_cat_customize
|
||
icon: 'fa fa-paint-brush',
|
||
content: [
|
||
'logo',
|
||
'color',
|
||
]
|
||
},
|
||
'broadcast' : { // Msg.admin_cat_broadcast
|
||
icon: 'fa fa-bullhorn',
|
||
content : [
|
||
'notice',
|
||
'maintenance',
|
||
'survey',
|
||
'broadcast'
|
||
]
|
||
},
|
||
'security': { // Msg.admin_cat_security
|
||
icon: 'fa fa-lock',
|
||
content: [
|
||
'enableembeds',
|
||
'forcemfa',
|
||
]
|
||
},
|
||
'apps': { // Msg.admin_cat_apps
|
||
icon: 'fa fa-wrench',
|
||
content: [
|
||
'apps',
|
||
]
|
||
},
|
||
'users' : { // Msg.admin_cat_users
|
||
icon : 'fa fa-address-card-o',
|
||
content : [
|
||
'registration',
|
||
'invitation',
|
||
'users'
|
||
]
|
||
},
|
||
'quota': { // Msg.admin_cat_quota
|
||
icon: 'fa fa-hdd-o',
|
||
content: [
|
||
'defaultlimit',
|
||
'setlimit',
|
||
'getlimits',
|
||
]
|
||
},
|
||
'database' : { // Msg.admin_cat_database
|
||
icon : 'fa fa-database',
|
||
content : [
|
||
'account-metadata',
|
||
'document-metadata',
|
||
'block-metadata',
|
||
'totp-recovery',
|
||
|
||
]
|
||
},
|
||
'support' : { // Msg.admin_cat_support
|
||
icon : 'fa fa-ambulance',
|
||
content : [
|
||
'support-setup',
|
||
'support-team',
|
||
]
|
||
},
|
||
'stats' : { // Msg.admin_cat_stats
|
||
icon : 'fa fa-line-chart',
|
||
content : [
|
||
'refresh-stats',
|
||
'uptime',
|
||
'active-sessions',
|
||
'active-pads',
|
||
'open-files',
|
||
'registered',
|
||
'disk-usage',
|
||
]
|
||
},
|
||
'performance' : { // Msg.admin_cat_performance
|
||
icon : 'fa fa-heartbeat',
|
||
content : [
|
||
'refresh-performance',
|
||
'performance-profiling',
|
||
'enable-disk-measurements',
|
||
'bytes-written',
|
||
]
|
||
},
|
||
'network' : { // Msg.admin_cat_network
|
||
icon : 'fa fa-sitemap',
|
||
content : [
|
||
'update-available',
|
||
'checkup',
|
||
'block-daily-check',
|
||
'provide-aggregate-statistics',
|
||
'list-my-instance',
|
||
|
||
'consent-to-contact',
|
||
'remove-donate-button',
|
||
'instance-purpose',
|
||
]
|
||
}
|
||
};
|
||
|
||
const blocks = sidebar.blocks;
|
||
|
||
// EXTENSION_POINT:ADMIN_CATEGORY
|
||
common.getExtensions('ADMIN_CATEGORY').forEach(ext => {
|
||
if (!ext || !ext.id || !ext.name || !ext.content) {
|
||
return console.error('Invalid extension point', 'ADMIN_CATEGORY', ext);
|
||
}
|
||
if (categories[ext.id]) {
|
||
return console.error('Extension point ID already used', ext);
|
||
}
|
||
console.error(ext);
|
||
categories[ext.id] = {
|
||
icon: ext.icon,
|
||
name: ext.name,
|
||
content: ext.content
|
||
};
|
||
});
|
||
|
||
const flushCache = (cb) => {
|
||
cb = cb || function () {};
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'FLUSH_CACHE',
|
||
}, cb);
|
||
};
|
||
/*
|
||
var flushCacheNotice = function () {
|
||
var notice = UIElements.setHTML(h('p'), Messages.admin_reviewCheckupNotice);
|
||
$(notice).find('a').attr({
|
||
href: new URL('/checkup/', ApiConfig.httpUnsafeOrigin).href,
|
||
}).click(function (ev) {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
common.openURL('/checkup/');
|
||
});
|
||
var content = h('span', [
|
||
UIElements.setHTML(h('p'), Messages.admin_cacheEvictionRequired),
|
||
notice,
|
||
]);
|
||
UI.alert(content);
|
||
};
|
||
*/
|
||
|
||
//general blocks
|
||
|
||
// Msg.admin_flushCacheHint, .admin_flushCacheTitle, .admin_flushCacheButton
|
||
sidebar.addItem('flush-cache', function (cb) {
|
||
var button = blocks.activeButton('primary', '',
|
||
Messages.admin_flushCacheButton, done => {
|
||
flushCache(function (e, data) {
|
||
done(!!data);
|
||
UI.alert(data ? Messages.admin_flushCacheDone || 'done' : 'error' + e);
|
||
});
|
||
});
|
||
cb(button);
|
||
});
|
||
|
||
var isHex = s => !/[^0-9a-f]/.test(s);
|
||
|
||
var sframeCommand = function (command, data, cb) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: command,
|
||
data: data,
|
||
}, function (err, response) {
|
||
if (err) { return void cb(err); }
|
||
if (response && response.error) { return void cb(response.error); }
|
||
try {
|
||
cb(void 0, response);
|
||
} catch (err2) {
|
||
console.error(err2);
|
||
}
|
||
});
|
||
};
|
||
|
||
// XXX make this use blocks
|
||
var makeMetadataTable = function (cls) {
|
||
var table = h(`table.${cls || 'cp-account-stats'}`);
|
||
var row = (label, value) => {
|
||
table.appendChild(h('tr', [
|
||
h('td', h('strong', label)),
|
||
h('td', value)
|
||
]));
|
||
};
|
||
|
||
return {
|
||
row: row,
|
||
table: table,
|
||
};
|
||
};
|
||
|
||
var getPrettySize = UIElements.prettySize;
|
||
|
||
var localizeState = state => {
|
||
var o = {
|
||
'true': Messages.ui_true,
|
||
'false': Messages.ui_false,
|
||
'undefined': Messages.ui_undefined,
|
||
};
|
||
return o[state] || Messages.error;
|
||
};
|
||
|
||
var disable = $el => $el.attr('disabled', 'disabled');
|
||
var enable = $el => $el.removeAttr('disabled');
|
||
|
||
var maybeDate = function (d) {
|
||
return d? new Date(d): Messages.ui_undefined;
|
||
};
|
||
|
||
var justifyDialog = (message, suggestion, implicit, explicit) => {
|
||
UI.prompt(message, suggestion, result => {
|
||
if (result === null) { return; }
|
||
if (typeof(result) !== 'string') { result = ''; }
|
||
else { result = result.trim(); }
|
||
implicit(result); // remember the justification for next time
|
||
explicit(result); // follow up with the action
|
||
}, {
|
||
ok: Messages.ui_confirm,
|
||
inputOpts: {
|
||
placeholder: Messages.admin_archiveNote || '',
|
||
},
|
||
});
|
||
};
|
||
|
||
var archiveReason = "";
|
||
var justifyArchivalDialog = (customMessage, action) => {
|
||
var message = customMessage || Messages.admin_archiveReason;
|
||
justifyDialog(message, archiveReason, reason => { archiveReason = reason; }, action);
|
||
};
|
||
|
||
var restoreReason = "";
|
||
var justifyRestorationDialog = (customMessage, action) => {
|
||
var message = customMessage || Messages.admin_restoreReason;
|
||
justifyDialog(message, restoreReason, reason => { restoreReason = reason; }, action);
|
||
};
|
||
|
||
var copyToClipboard = (content) => {
|
||
var button = blocks.activeButton('primary','', Messages.copyToClipboard, () => {
|
||
var toCopy = JSON.stringify(content, null, 2);
|
||
Clipboard.copy(toCopy, (err) => {
|
||
if (err) { return UI.warn(Messages.error); }
|
||
UI.log(Messages.genericCopySuccess);
|
||
});
|
||
}, true);
|
||
return button;
|
||
};
|
||
|
||
var reportContentLabel = () => {
|
||
return h('span', [
|
||
Messages.admin_reportContent,
|
||
' (JSON) ',
|
||
h('br'),
|
||
h('small', Messages.ui_experimental),
|
||
]);
|
||
};
|
||
|
||
var DOCUMENT_TYPES = {
|
||
32: 'channel',
|
||
48: 'file',
|
||
33: 'ephemeral',
|
||
34: 'broadcast',
|
||
};
|
||
|
||
var inferDocumentType = id => {
|
||
return DOCUMENT_TYPES[typeof(id) === 'string' && id.length] || 'unknown';
|
||
};
|
||
|
||
var renderAccountData = function (data) {
|
||
var tableObj = makeMetadataTable('cp-account-stats');
|
||
var row = tableObj.row;
|
||
|
||
// info
|
||
row(Messages.admin_generatedAt, new Date(data.generated));
|
||
|
||
// signing key
|
||
if (data.key === data.safeKey) {
|
||
row(Messages.settings_publicSigningKey, h('code', data.key));
|
||
} else {
|
||
row(Messages.settings_publicSigningKey, h('span', [
|
||
h('code', data.key),
|
||
', ',
|
||
h('br'),
|
||
h('code', data.safeKey),
|
||
]));
|
||
}
|
||
|
||
if (data.first || data.latest) {
|
||
// First pin activity time
|
||
row(Messages.admin_firstPinTime, maybeDate(data.first));
|
||
|
||
// last pin activity time
|
||
row(Messages.admin_lastPinTime, maybeDate(data.latest));
|
||
}
|
||
|
||
// currently online
|
||
row(Messages.admin_currentlyOnline, localizeState(data.currentlyOnline));
|
||
|
||
// plan name
|
||
row(Messages.admin_planName, data.plan || Messages.ui_none);
|
||
|
||
// plan note
|
||
row(Messages.admin_note, data.note || Messages.ui_none);
|
||
|
||
// storage limit
|
||
if (data.limit) { row(Messages.admin_planlimit, getPrettySize(data.limit)); }
|
||
|
||
// data stored
|
||
if (data.usage) { row(Messages.admin_storageUsage, getPrettySize(data.usage)); }
|
||
|
||
// number of channels
|
||
if (typeof(data.channel) === "number") {
|
||
row(Messages.admin_channelCount, data.channels);
|
||
}
|
||
|
||
// number of files pinned
|
||
if (typeof(data.channel) === "number") {
|
||
row(Messages.admin_fileCount, data.files);
|
||
}
|
||
|
||
row(Messages.admin_pinLogAvailable, localizeState(data.live));
|
||
|
||
// pin log archived
|
||
row(Messages.admin_pinLogArchived, localizeState(data.archived));
|
||
|
||
if (data.archiveReport) {
|
||
row(Messages.admin_accountSuspended, localizeState(Boolean(data.archiveReport)));
|
||
}
|
||
if (data.archiveReport_formatted) {
|
||
let button, pre;
|
||
row(Messages.admin_accountReport, h('div', [
|
||
pre = h('pre', data.archiveReport_formatted),
|
||
button = blocks.activeButton('primary', '',
|
||
Messages.admin_accountReportFull, () => {
|
||
$(button).remove();
|
||
$(pre).html(JSON.stringify(data.archiveReport, 0, 2));
|
||
}, true)
|
||
]));
|
||
}
|
||
|
||
|
||
// actions
|
||
if (data.archived && data.live === false && data.archiveReport) {
|
||
let button = blocks.activeButton('primary', '',
|
||
Messages.ui_restore, () => {
|
||
justifyRestorationDialog('', reason => {
|
||
sframeCommand('RESTORE_ACCOUNT', {
|
||
key: data.key,
|
||
reason: reason,
|
||
}, function (err) {
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
UI.log(Messages.ui_success);
|
||
});
|
||
});
|
||
}, true);
|
||
row(Messages.admin_restoreAccount, button);
|
||
}
|
||
|
||
if (data.live === true) {
|
||
var getPins = (done) => {
|
||
sframeCommand('GET_PIN_LIST', data.key, (err, pins) => {
|
||
done(!err && Array.isArray(pins));
|
||
if (err || !Array.isArray(pins)) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
|
||
var table = makeMetadataTable('cp-pin-list').table;
|
||
var row = id => {
|
||
var type = inferDocumentType(id);
|
||
table.appendChild(h('tr', [
|
||
h('td', h('code', id)),
|
||
h('td', type),
|
||
]));
|
||
};
|
||
|
||
var P = pins.slice().sort((a, b) => a.length - b.length);
|
||
P.map(row);
|
||
|
||
UI.confirm(table, yes => {
|
||
if (!yes) { return; }
|
||
var content = P.join('\n');
|
||
Clipboard.copy(content, (err) => {
|
||
if (err) { return UI.warn(Messages.error); }
|
||
UI.log(Messages.genericCopySuccess);
|
||
});
|
||
}, {
|
||
wide: true,
|
||
ok: Messages.copyToClipboard,
|
||
});
|
||
});
|
||
};
|
||
|
||
// get full pin list
|
||
row(Messages.admin_getPinList, blocks.activeButton('primary', '', Messages.ui_fetch, getPins));
|
||
|
||
// get full pin history
|
||
var getHistoryHandler = (done) => {
|
||
sframeCommand('GET_PIN_HISTORY', data.key, (err, history) => {
|
||
done(!err);
|
||
if (err) {
|
||
console.error('Error retrieving pin history:', err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
UI.alert(history); // TODO NOT_IMPLEMENTED
|
||
});
|
||
};
|
||
var pinHistoryButton = blocks.activeButton('primary', '', Messages.ui_fetch, getHistoryHandler);
|
||
disable($(pinHistoryButton));
|
||
|
||
// TODO pin history is not implemented
|
||
//row(Messages.admin_getFullPinHistory, pinHistoryButton);
|
||
|
||
// archive pin log
|
||
var archiveHandler = () => {
|
||
justifyArchivalDialog(Messages.admin_archiveAccountConfirm, reason => {
|
||
sframeCommand('ARCHIVE_ACCOUNT', {
|
||
key: data.key,
|
||
block: data.blockId,
|
||
reason: reason,
|
||
}, (err /*, response */) => {
|
||
//console.error(err);
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
UI.log(Messages.ui_success);
|
||
});
|
||
});
|
||
};
|
||
|
||
var archiveAccountLabel = h('span', [
|
||
Messages.admin_archiveAccount,
|
||
h('br'),
|
||
h('small', Messages.admin_archiveAccountInfo)
|
||
]);
|
||
let archiveAccountButton = blocks.activeButton('danger', '',
|
||
Messages.admin_archiveButton, archiveHandler, true);
|
||
row(archiveAccountLabel, archiveAccountButton);
|
||
|
||
// archive owned documents
|
||
/* // TODO not implemented
|
||
var archiveDocuments = () => {
|
||
justifyRestorationDialog(Messages.admin_archiveDocumentsConfirm, reason => {
|
||
sframeCommand('ARCHIVE_OWNED_DOCUMENTS', {
|
||
key: data.key,
|
||
reason: reason,
|
||
}, (err, response) => {
|
||
if (err) { return void UI.warn(err); }
|
||
UI.log(response);
|
||
});
|
||
});
|
||
};
|
||
|
||
var archiveDocumentsButton = danger(Messages.admin_archiveButton, archiveDocuments);
|
||
disable($(archiveDocumentsButton));
|
||
row(Messages.admin_archiveOwnedAccountDocuments, archiveDocumentsButton);
|
||
*/
|
||
}
|
||
|
||
row(reportContentLabel, copyToClipboard(data));
|
||
|
||
return tableObj.table;
|
||
};
|
||
|
||
// Msg.admin_updateLimitHint, .admin_updateLimitTitle, .admin_updateLimitButton
|
||
sidebar.addItem('update-limit', function (cb) {
|
||
var button = blocks.activeButton('primary', '',
|
||
Messages.admin_updateLimitButton, done => {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'Q_UPDATE_LIMIT',
|
||
}, function (e, data) {
|
||
done(!!data);
|
||
UI.alert(data ? Messages.admin_updateLimitDone || 'done' : 'error' + e);
|
||
});
|
||
});
|
||
cb(button);
|
||
});
|
||
|
||
// Msg.admin_enableembedsHint, .admin_enableembedsTitle
|
||
sidebar.addCheckboxItem({
|
||
key: 'enableembeds',
|
||
getState: function () {
|
||
return APP.instanceStatus.enableEmbedding;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['ENABLE_EMBEDDING', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.enableEmbedding);
|
||
flushCache();
|
||
});
|
||
});
|
||
},
|
||
});
|
||
|
||
// Msg.admin_forcemfaHint, .admin_forcemfaTitle
|
||
sidebar.addCheckboxItem({
|
||
key: 'forcemfa',
|
||
getState: function () {
|
||
return APP.instanceStatus.enforceMFA;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['ENFORCE_MFA', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.enforceMFA);
|
||
flushCache();
|
||
});
|
||
});
|
||
},
|
||
});
|
||
|
||
|
||
var getInstanceString = function (attr) {
|
||
var val = APP.instanceStatus[attr];
|
||
var type = typeof(val);
|
||
switch (type) {
|
||
case 'string':
|
||
return val || '';
|
||
case 'object':
|
||
return val.default || '';
|
||
default:
|
||
return '';
|
||
}
|
||
};
|
||
|
||
// Msg.admin_emailHint, Msg.admin_emailTitle
|
||
sidebar.addItem('email', function (cb){
|
||
var input = blocks.input({
|
||
type: 'email',
|
||
value: ApiConfig.adminEmail || '',
|
||
'aria-labelledby': 'cp-admin-email'
|
||
});
|
||
var $input = $(input);
|
||
|
||
var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['SET_ADMIN_EMAIL', [$input.val().trim()]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
$input.val('');
|
||
console.error(e, response);
|
||
done(false);
|
||
return;
|
||
}
|
||
flushCache();
|
||
done(true);
|
||
UI.log(Messages._getKey('ui_saved', [Messages.admin_emailTitle]));
|
||
});
|
||
});
|
||
var nav = blocks.nav([button]);
|
||
|
||
var form = blocks.form([
|
||
input,
|
||
], nav);
|
||
|
||
$(nav).append(button.spinner);
|
||
|
||
cb(form);
|
||
});
|
||
|
||
sidebar.addItem('apps', function (cb) {
|
||
const appsToDisable = ApiConfig.appsToDisable || [];
|
||
const grid = Onboarding.createAppsGrid(appsToDisable);
|
||
|
||
var save = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['DISABLE_APPS', appsToDisable]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
done(false);
|
||
return;
|
||
}
|
||
flushCache();
|
||
done(true);
|
||
UI.log(Messages._getKey('ui_saved', [Messages.admin_appSelection]));
|
||
});
|
||
});
|
||
|
||
let form = blocks.form([
|
||
grid
|
||
], blocks.nav([save]));
|
||
|
||
cb(form);
|
||
});
|
||
|
||
|
||
sidebar.addItem('instance-info-notice', function(cb){
|
||
var key = 'instance-info-notice';
|
||
var notice = blocks.alert('info', key, [Messages.admin_infoNotice1, ' ', Messages.admin_infoNotice2]);
|
||
cb(notice);
|
||
}, {
|
||
noTitle: true,
|
||
noHint: true
|
||
});
|
||
|
||
// Msg.admin_nameHint, Msg.admin_nameTitle, Msg.admin_nameButton
|
||
sidebar.addItem('name', function (cb){
|
||
var input = blocks.input({
|
||
type: 'text',
|
||
value: getInstanceString('instanceName')|| ApiConfig.httpUnsafeOrigin || '',
|
||
placeholder: ApiConfig.httpUnsafeOrigin,
|
||
'aria-labelledby': 'cp-admin-name'
|
||
});
|
||
var $input = $(input);
|
||
|
||
var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['SET_INSTANCE_NAME', [$input.val().trim()]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
$input.val('');
|
||
console.error(e, response);
|
||
done(false);
|
||
return;
|
||
}
|
||
flushCache();
|
||
done(true);
|
||
UI.log(Messages._getKey('ui_saved', [Messages.admin_nameTitle]));
|
||
});
|
||
});
|
||
|
||
var nav = blocks.nav([button]);
|
||
var form = blocks.form([
|
||
input,
|
||
], nav);
|
||
|
||
$(nav).append(button.spinner);
|
||
|
||
cb(form);
|
||
});
|
||
|
||
// Msg.admin_descriptionHint, Msg.admin_descriptionTitle
|
||
sidebar.addItem('description', function (cb){
|
||
var textarea = blocks.textarea({
|
||
placeholder: Messages.home_host || '',
|
||
'aria-labelledby': 'cp-admin-description'
|
||
}, getInstanceString('instanceDescription'));
|
||
var $input = $(textarea);
|
||
var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['SET_INSTANCE_DESCRIPTION', [$input.val().trim()]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
$input.val('');
|
||
console.error(e, response);
|
||
done(false);
|
||
return;
|
||
}
|
||
flushCache();
|
||
done(true);
|
||
UI.log(Messages._getKey('ui_saved', [Messages.admin_descriptionTitle]));
|
||
});
|
||
});
|
||
|
||
var nav = blocks.nav([button]);
|
||
$(nav).append(button.spinner);
|
||
|
||
var form = blocks.form([
|
||
textarea,
|
||
], nav);
|
||
|
||
cb(form);
|
||
});
|
||
|
||
// Msg.admin_jurisdictionHint, Msg.admin_jurisdictionTitle
|
||
sidebar.addItem('jurisdiction', function (cb){
|
||
var input = blocks.input({
|
||
type: 'text',
|
||
value: getInstanceString('instanceJurisdiction'),
|
||
placeholder: Messages.owner_unknownUser || '',
|
||
'aria-labelledby': 'cp-admin-jurisdiction'
|
||
});
|
||
var $input = $(input);
|
||
|
||
var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['SET_INSTANCE_JURISDICTION', [$input.val().trim()]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
$input.val('');
|
||
console.error(e, response);
|
||
done(false);
|
||
return;
|
||
}
|
||
flushCache();
|
||
done(true);
|
||
UI.log(Messages._getKey('ui_saved', [Messages.admin_jurisdictionTitle]));
|
||
});
|
||
});
|
||
|
||
var nav = blocks.nav([button]);
|
||
$(nav).append(button.spinner);
|
||
|
||
var form = blocks.form([
|
||
input,
|
||
], nav);
|
||
|
||
cb(form);
|
||
});
|
||
|
||
// Messages.admin_noticeHint, Messages.admin_noticeTitle
|
||
sidebar.addItem('notice', function (cb){
|
||
var input = blocks.input({
|
||
type: 'text',
|
||
value: getInstanceString('instanceNotice'),
|
||
placeholder: '',
|
||
'aria-labelledby': 'cp-admin-notice'
|
||
});
|
||
var $input = $(input);
|
||
|
||
var button = blocks.activeButton('primary', '', Messages.settings_save, function (done) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['SET_INSTANCE_NOTICE', [$input.val().trim()]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
$input.val('');
|
||
console.error(e, response);
|
||
done(false);
|
||
return;
|
||
}
|
||
flushCache();
|
||
done(true);
|
||
UI.log(Messages._getKey('ui_saved', [Messages.admin_noticeTitle]));
|
||
});
|
||
});
|
||
|
||
var nav = blocks.nav([button]);
|
||
$(nav).append(button.spinner);
|
||
|
||
var form = blocks.form([
|
||
input,
|
||
], nav);
|
||
|
||
cb(form);
|
||
});
|
||
|
||
// Msg.admin_logoHint, Msg.admin_logoTitle
|
||
sidebar.addItem('logo', (cb) => {
|
||
|
||
let input = blocks.input({
|
||
type: 'file',
|
||
accept: 'image/*',
|
||
'aria-labelledby': 'cp-admin-logo'
|
||
});
|
||
|
||
var currentContainer = blocks.block([], 'cp-admin-customize-logo');
|
||
let redraw = () => {
|
||
var current = h('img', {src: '/api/logo?'+(+new Date()),alt:'Custom logo'}); // XXX
|
||
$(currentContainer).empty().append(current);
|
||
};
|
||
redraw();
|
||
|
||
var upload = blocks.button('primary', '', Messages.admin_logoButton);
|
||
var remove = blocks.button('danger', '', Messages.admin_logoRemoveButton);
|
||
|
||
let spinnerBlock = blocks.inline();
|
||
let spinner = UI.makeSpinner($(spinnerBlock));
|
||
let form = blocks.form([
|
||
currentContainer,
|
||
blocks.block(input),
|
||
blocks.nav([upload, remove, spinnerBlock])
|
||
]);
|
||
|
||
let $button = $(upload);
|
||
let $remove = $(remove);
|
||
|
||
Util.onClickEnter($button, function () {
|
||
let files = input.files;
|
||
if (files.length !== 1) {
|
||
UI.warn(Messages.error);
|
||
return;
|
||
}
|
||
spinner.spin();
|
||
$button.attr('disabled', 'disabled');
|
||
let reader = new FileReader();
|
||
reader.onloadend = function () {
|
||
let dataURL = this.result;
|
||
sframeCommand('UPLOAD_LOGO', {dataURL}, (err, response) => {
|
||
$button.removeAttr('disabled');
|
||
if (err) {
|
||
UI.warn(Messages.error);
|
||
$(input).val('');
|
||
console.error(err, response);
|
||
spinner.hide();
|
||
return;
|
||
}
|
||
redraw();
|
||
spinner.done();
|
||
UI.log(Messages.saved);
|
||
});
|
||
};
|
||
reader.readAsDataURL(files[0]);
|
||
});
|
||
UI.confirmButton($remove, {
|
||
classes: 'btn-danger',
|
||
multiple: true
|
||
}, function () {
|
||
spinner.spin();
|
||
$remove.attr('disabled', 'disabled');
|
||
sframeCommand('REMOVE_LOGO', {}, (err, response) => {
|
||
$remove.removeAttr('disabled');
|
||
if (err) {
|
||
UI.warn(Messages.error);
|
||
console.error(err, response);
|
||
spinner.hide();
|
||
return;
|
||
}
|
||
redraw();
|
||
spinner.done();
|
||
UI.log(Messages.saved);
|
||
});
|
||
});
|
||
|
||
cb(form);
|
||
});
|
||
|
||
// Msg.admin_colorHint, Msg.admin_colorTitle
|
||
sidebar.addItem('color', cb => {
|
||
let input = blocks.input({
|
||
type: 'color',
|
||
value: (Instance && Instance.color) || '#0087FF'
|
||
});
|
||
let label = blocks.labelledInput(Messages.admin_colorPick, input);
|
||
let current = blocks.block([], 'cp-admin-color-current');
|
||
let labelCurrent = blocks.labelledInput(Messages.admin_colorCurrent, current);
|
||
let preview = blocks.block([
|
||
blocks.block([
|
||
blocks.link('CryptPad', '/admin/#customize'),
|
||
blocks.button('primary', 'fa-floppy-o', Messages.settings_save),
|
||
blocks.button('secondary', 'fa-floppy-o', Messages.settings_save)
|
||
], 'cp-admin-color-preview-dark cp-sidebar-flex-block'),
|
||
blocks.block([
|
||
blocks.link('CryptPad', '/admin/#customize'),
|
||
blocks.button('primary', 'fa-floppy-o', Messages.settings_save),
|
||
blocks.button('secondary', 'fa-floppy-o', Messages.settings_save)
|
||
], 'cp-admin-color-preview-light cp-sidebar-flex-block')
|
||
], 'cp-admin-color-preview');
|
||
let labelPreview = blocks.labelledInput(Messages.admin_colorPreview, preview);
|
||
let $preview = $(preview);
|
||
|
||
let remove = blocks.button('danger', '', Messages.admin_logoRemoveButton);
|
||
let $remove = $(remove);
|
||
|
||
let setColor = (color, done) => {
|
||
sframeCommand('CHANGE_COLOR', {color}, (err, response) => {
|
||
if (err) {
|
||
UI.warn(Messages.error);
|
||
console.error(err, response);
|
||
done(false);
|
||
return;
|
||
}
|
||
done(true);
|
||
UI.log(Messages.saved);
|
||
});
|
||
};
|
||
|
||
let btn = blocks.activeButton('primary', '',
|
||
Messages.admin_colorChange, (done) => {
|
||
let color = $input.val();
|
||
setColor(color, done);
|
||
});
|
||
|
||
let onColorPicked = () => {
|
||
require(['/lib/less.min.js'], (Less) => {
|
||
let color = $input.val();
|
||
let lColor = Less.color(color.slice(1));
|
||
let lighten = Less.functions.functionRegistry._data.lighten;
|
||
let lightColor = lighten(lColor, {value:30}).toRGB();
|
||
$preview.find('.btn-primary').css({
|
||
'background-color': color
|
||
});
|
||
$preview.find('.cp-admin-color-preview-dark .btn-secondary').css({
|
||
'border-color': lightColor,
|
||
'color': lightColor,
|
||
});
|
||
$preview.find('.cp-admin-color-preview-light .btn-secondary').css({
|
||
'border-color': color,
|
||
'color': color,
|
||
});
|
||
$preview.find('.cp-admin-color-preview-dark a').attr('style', `color: ${lightColor} !important`);
|
||
$preview.find('.cp-admin-color-preview-light a').attr('style', `color: ${color} !important`);
|
||
});
|
||
};
|
||
let $input = $(input).on('change', onColorPicked).addClass('cp-admin-color-picker');
|
||
|
||
UI.confirmButton($remove, {
|
||
classes: 'btn-danger',
|
||
multiple: true
|
||
}, function () {
|
||
$remove.attr('disabled', 'disabled');
|
||
setColor('', () => {});
|
||
});
|
||
|
||
var colors = UIElements.makePalette(4, (color, $color) => {
|
||
// onselect
|
||
let rgb = $color.css('background-color');
|
||
let hex = Util.rgbToHex(rgb);
|
||
$input.val(hex);
|
||
onColorPicked();
|
||
});
|
||
|
||
$(label).append(colors);
|
||
let form = blocks.form([
|
||
labelCurrent,
|
||
label,
|
||
], blocks.nav([btn, remove, btn.spinner]));
|
||
|
||
cb([form, labelPreview]);
|
||
});
|
||
|
||
// Msg.admin_registrationHint, .admin_registrationTitle
|
||
// Msg.admin_registrationSsoTitle
|
||
sidebar.addItem('registration', function(cb){
|
||
var refresh = function () {};
|
||
|
||
var restrict = blocks.activeCheckbox({
|
||
key: 'registration',
|
||
getState: function () {
|
||
return APP.instanceStatus.restrictRegistration;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['RESTRICT_REGISTRATION', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.restrictRegistration);
|
||
refresh();
|
||
flushCache();
|
||
});
|
||
});
|
||
},
|
||
});
|
||
|
||
var restrictSSO = blocks.activeCheckbox({
|
||
key: 'registration-sso',
|
||
getState: function () {
|
||
return APP.instanceStatus.restrictSsoRegistration;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['RESTRICT_SSO_REGISTRATION', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.restrictSsoRegistration);
|
||
flushCache();
|
||
});
|
||
});
|
||
}
|
||
});
|
||
var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;
|
||
if (!ssoEnabled) { restrictSSO = undefined; }
|
||
|
||
var $sso = $(restrictSSO);
|
||
refresh = () => {
|
||
var closed = APP.instanceStatus.restrictRegistration;
|
||
if (closed) {
|
||
$sso.show();
|
||
} else {
|
||
$sso.hide();
|
||
}
|
||
};
|
||
refresh();
|
||
|
||
cb(blocks.form([restrict, restrictSSO], []));
|
||
});
|
||
|
||
// Msg.admin_invitationHint, admin_invitationTitle
|
||
sidebar.addItem('invitation', function(cb){
|
||
var button = blocks.button('primary', '', Messages.admin_invitationCreate);
|
||
var $b = $(button);
|
||
|
||
var inputAlias = blocks.input({
|
||
type: 'text'
|
||
});
|
||
var blockAlias = blocks.labelledInput(Messages.admin_invitationAlias, inputAlias);
|
||
|
||
var inputEmail = blocks.input({
|
||
type: 'email'
|
||
});
|
||
var blockEmail = blocks.labelledInput(Messages.admin_invitationEmail, inputEmail);
|
||
|
||
var refreshInvite = function () {};
|
||
var refreshButton = blocks.button('secondary', '', Messages.oo_refresh);
|
||
Util.onClickEnter($(refreshButton), function () {
|
||
refreshInvite();
|
||
});
|
||
|
||
var header = [
|
||
Messages.admin_invitationLink,
|
||
Messages.admin_invitationAlias,
|
||
Messages.admin_invitationEmail,
|
||
Messages.admin_documentCreationTime,
|
||
""
|
||
];
|
||
var list = blocks.table(header, []);
|
||
|
||
var nav = blocks.nav([button, refreshButton]);
|
||
var form = blocks.form([
|
||
blockAlias,
|
||
blockEmail
|
||
], nav);
|
||
|
||
var metadataMgr = common.getMetadataMgr();
|
||
var privateData = metadataMgr.getPrivateData();
|
||
|
||
var deleteInvite = function (id) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'DELETE_INVITATION',
|
||
data: id
|
||
}, function (e, response) {
|
||
$b.prop('disabled', false);
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(e, response);
|
||
}
|
||
refreshInvite();
|
||
});
|
||
};
|
||
|
||
let sort = all => {
|
||
return (k1, k2) => {
|
||
return all[k1].time - all[k2].time;
|
||
};
|
||
};
|
||
refreshInvite = function () {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'GET_ALL_INVITATIONS',
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
|
||
console.error(e, response);
|
||
return;
|
||
}
|
||
if (!Array.isArray(response)) { return; }
|
||
var all = response[0];
|
||
var newEntries = [];
|
||
|
||
Object.keys(all).sort(sort(all)).forEach(function (key) {
|
||
var data = all[key];
|
||
var url = privateData.origin + Hash.hashToHref(key, 'register');
|
||
|
||
var del = blocks.button('danger', 'fa-trash', Messages.kanban_delete );
|
||
var $del = $(del);
|
||
Util.onClickEnter($del, function () {
|
||
$del.attr('disabled', 'disabled');
|
||
UI.confirm(Messages.admin_invitationDeleteConfirm, function (yes) {
|
||
$del.attr('disabled', '');
|
||
if (!yes) { return; }
|
||
deleteInvite(key);
|
||
});
|
||
});
|
||
var copy = blocks.button('secondary', 'fa-clipboard', Messages.admin_invitationCopy);
|
||
Util.onClickEnter($(copy), function () {
|
||
Clipboard.copy(url, () => {
|
||
UI.log(Messages.genericCopySuccess);
|
||
});
|
||
});
|
||
|
||
newEntries.push([
|
||
UI.dialog.selectable(url),
|
||
data.alias,
|
||
data.email,
|
||
new Date(data.time).toLocaleString(),
|
||
[copy, del]
|
||
]);
|
||
});
|
||
list.updateContent(newEntries);
|
||
|
||
});
|
||
|
||
};
|
||
refreshInvite();
|
||
|
||
Util.onClickEnter($b, function () {
|
||
var alias = $(inputAlias).val().trim();
|
||
if (!alias) { return void UI.warn(Messages.error); } // FIXME better error message
|
||
$b.prop('disabled', true);
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'CREATE_INVITATION',
|
||
data: {
|
||
alias,
|
||
email: $(inputEmail).val()
|
||
}
|
||
}, function (e, response) {
|
||
$b.prop('disabled', false);
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(e, response);
|
||
}
|
||
$(inputAlias).val('').focus();
|
||
$(inputEmail).val('');
|
||
refreshInvite();
|
||
});
|
||
});
|
||
|
||
cb([form, list]);
|
||
});
|
||
|
||
var getBlockId = (val) => {
|
||
var url;
|
||
try {
|
||
url = new URL(val, ApiConfig.httpUnsafeOrigin);
|
||
} catch (err) { }
|
||
var getKey = function () {
|
||
var parts = val.split('/');
|
||
return parts[parts.length - 1];
|
||
};
|
||
var isValidBlockURL = function (url) {
|
||
if (!url) { return; }
|
||
return /* url.origin === ApiConfig.httpUnsafeOrigin && */ /^\/block\/.*/.test(url.pathname) && getKey().length === 44;
|
||
};
|
||
if (isValidBlockURL(url)) {
|
||
return getKey();
|
||
}
|
||
return;
|
||
};
|
||
|
||
var getAccountData = function (key, _cb) {
|
||
var cb = Util.once(Util.mkAsync(_cb));
|
||
var data = {
|
||
generated: +new Date(),
|
||
key: key,
|
||
safeKey: Util.escapeKeyCharacters(key),
|
||
};
|
||
|
||
return void nThen(function (w) {
|
||
sframeCommand('GET_PIN_ACTIVITY', key, w((err, response) => {
|
||
if (err === 'ENOENT') { return; }
|
||
if (err || !response || !response[0]) {
|
||
console.error(err);
|
||
console.error(response);
|
||
UI.warn(Messages.error);
|
||
} else {
|
||
data.first = response[0].first;
|
||
data.latest = response[0].latest;
|
||
console.info(err, response);
|
||
}
|
||
}));
|
||
}).nThen(function (w) {
|
||
sframeCommand('IS_USER_ONLINE', key, w((err, response) => {
|
||
console.log('online', err, response);
|
||
if (!Array.isArray(response) || typeof(response[0]) !== 'boolean') { return; }
|
||
data.currentlyOnline = response[0];
|
||
}));
|
||
}).nThen(function (w) {
|
||
if (!data.first) { return; }
|
||
sframeCommand('GET_USER_QUOTA', key, w((err, response) => {
|
||
if (err || !response) {
|
||
return void console.error('quota', err, response);
|
||
} else {
|
||
data.plan = response[1];
|
||
data.note = response[2];
|
||
data.limit = response[0];
|
||
}
|
||
}));
|
||
}).nThen(function (w) {
|
||
if (!data.first) { return; }
|
||
// storage used
|
||
sframeCommand('GET_USER_TOTAL_SIZE', key, w((err, response) => {
|
||
if (err || !Array.isArray(response)) {
|
||
//console.error('size', err, response);
|
||
} else {
|
||
//console.info('size', response);
|
||
data.usage = response[0];
|
||
}
|
||
}));
|
||
}).nThen(function (w) {
|
||
if (!data.first) { return; }
|
||
// channels pinned
|
||
// files pinned
|
||
sframeCommand('GET_USER_STORAGE_STATS', key, w((err, response) => {
|
||
if (err || !Array.isArray(response) || !response[0]) {
|
||
UI.warn(Messages.error);
|
||
return void console.error('storage stats', err, response);
|
||
} else {
|
||
data.channels = response[0].channels;
|
||
data.files = response[0].files;
|
||
}
|
||
}));
|
||
}).nThen(function (w) { // pin log status (live, archived, unknown)
|
||
sframeCommand('GET_PIN_LOG_STATUS', key, w((err, response) => {
|
||
if (err || !Array.isArray(response) || !response[0]) {
|
||
console.error('pin log status', err, response);
|
||
return void UI.warn(Messages.error);
|
||
} else {
|
||
console.info('pin log status', response);
|
||
data.live = response[0].live;
|
||
data.archived = response[0].archived;
|
||
}
|
||
}));
|
||
}).nThen(function (w) {
|
||
if (data.first) { return; }
|
||
// Account is probably deleted
|
||
sframeCommand('GET_ACCOUNT_ARCHIVE_STATUS', {key}, w((err, response) => {
|
||
if (err || !Array.isArray(response) || !response[0]) {
|
||
console.error('account status', err, response);
|
||
} else {
|
||
console.info('account status', response);
|
||
data.archiveReport = response[0];
|
||
}
|
||
}));
|
||
}).nThen(function () {
|
||
//console.log(data);
|
||
try {
|
||
['generated', 'first', 'latest'].forEach(k => {
|
||
var val = data[k];
|
||
if (typeof(val) !== 'number') { return; }
|
||
data[`${k}_formatted`] = new Date(val);
|
||
});
|
||
['limit', 'usage'].forEach(k => {
|
||
var val = data[k];
|
||
if (typeof(val) !== 'number') { return; }
|
||
data[`${k}_formatted`] = getPrettySize(val);
|
||
});
|
||
if (data.archiveReport) {
|
||
let formatted = Util.clone(data.archiveReport);
|
||
formatted.channels = data.archiveReport.channels.length;
|
||
formatted.blobs = data.archiveReport.blobs.length;
|
||
data['archiveReport_formatted'] = JSON.stringify(formatted, 0, 2);
|
||
}
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
|
||
cb(void 0, data);
|
||
});
|
||
};
|
||
|
||
// Msg.admin_usersHint, admin_usersTitle
|
||
// Msg.admin_storeInvitedLabel
|
||
// Msg.admin_storeSsoLabel
|
||
sidebar.addItem('users', function(cb){
|
||
|
||
var invited = blocks.activeCheckbox({
|
||
key: 'store-invited',
|
||
getState: function () {
|
||
return !APP.instanceStatus.dontStoreInvitedUsers;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['DISABLE_STORE_INVITED_USERS', [!val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(!APP.instanceStatus.dontStoreInvitedUsers);
|
||
flushCache();
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
var sso = blocks.activeCheckbox({
|
||
key: 'store-sso',
|
||
getState: function () {
|
||
return !APP.instanceStatus.dontStoreSSOUsers;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['DISABLE_STORE_SSO_USERS', [!val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(!APP.instanceStatus.dontStoreSSOUsers);
|
||
flushCache();
|
||
});
|
||
});
|
||
}
|
||
});
|
||
var ssoEnabled = ApiConfig.sso && ApiConfig.sso.list && ApiConfig.sso.list.length;
|
||
|
||
var button = blocks.button('primary', '', Messages.admin_usersAdd);
|
||
var $b = $(button);
|
||
|
||
var userAlias = blocks.input({ type: 'text' });
|
||
var blockAlias = blocks.labelledInput(Messages.admin_invitationAlias, userAlias);
|
||
|
||
var userEmail = blocks.input({ type: 'email' });
|
||
var blockEmail = blocks.labelledInput(Messages.admin_invitationEmail, userEmail);
|
||
|
||
var userEdPublic = blocks.input({ type: 'key' });
|
||
var blockEdPublic = blocks.labelledInput(Messages.admin_limitUser, userEdPublic);
|
||
|
||
var userBlock = blocks.input({ type: 'text' });
|
||
var blockUser = blocks.labelledInput(Messages.admin_usersBlock, userBlock);
|
||
|
||
var refreshUsers = function () {};
|
||
var refreshButton = blocks.button('secondary', '', Messages.oo_refresh);
|
||
Util.onClickEnter($(refreshButton), function () {
|
||
refreshUsers();
|
||
});
|
||
|
||
var header = [
|
||
Messages.admin_invitationAlias,
|
||
Messages.admin_invitationEmail,
|
||
Messages.admin_limitUser,
|
||
Messages.admin_documentCreationTime,
|
||
""
|
||
];
|
||
var list = blocks.table(header, []);
|
||
|
||
var nav = blocks.nav([button, refreshButton]);
|
||
|
||
if (!ssoEnabled) { sso = undefined; }
|
||
var form = blocks.form([
|
||
invited,
|
||
sso,
|
||
blockAlias,
|
||
blockEmail,
|
||
blockEdPublic,
|
||
blockUser
|
||
], nav);
|
||
|
||
var deleteUser = function (id) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'DELETE_KNOWN_USER',
|
||
data: id
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(e, response);
|
||
}
|
||
refreshUsers();
|
||
});
|
||
};
|
||
|
||
var updateUser = function (key, changes) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'UPDATE_KNOWN_USER',
|
||
data: {
|
||
edPublic: key,
|
||
changes: changes
|
||
}
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(e, response);
|
||
}
|
||
refreshUsers();
|
||
});
|
||
};
|
||
let sort = all => {
|
||
return (k1, k2) => {
|
||
return all[k2].time - all[k1].time;
|
||
};
|
||
};
|
||
refreshUsers = function () {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'GET_ALL_USERS',
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
if (!response || response.error !== "ENOENT") { UI.warn(Messages.error); }
|
||
console.error(e, response);
|
||
return;
|
||
}
|
||
if (!Array.isArray(response)) { return; }
|
||
var all = response[0];
|
||
var newEntries = [];
|
||
Object.keys(all).sort(sort(all)).forEach(function (key) {
|
||
var data = all[key];
|
||
var editUser = () => {};
|
||
var del = blocks.button('danger', 'fa fa-trash', Messages.admin_usersRemove);
|
||
var $del = $(del);
|
||
Util.onClickEnter($del, function () {
|
||
$del.attr('disabled', 'disabled');
|
||
UI.confirm(Messages.admin_usersRemoveConfirm, function (yes) {
|
||
$del.attr('disabled', '');
|
||
if (!yes) { return; }
|
||
deleteUser(key);
|
||
});
|
||
});
|
||
var edit = blocks.activeButton('secondary', 'fa fa-pencil',
|
||
Messages.tag_edit, () => { editUser(); }, true);
|
||
|
||
let aliasCell = blocks.inline(data.alias);
|
||
let emailCell = blocks.inline(data.email);
|
||
var actions = blocks.nav([edit, del]);
|
||
|
||
let $alias = $(aliasCell);
|
||
let $email = $(emailCell);
|
||
var $actions = $(actions);
|
||
|
||
editUser = () => {
|
||
var aliasInput = h('input');
|
||
var emailInput = h('input');
|
||
$(aliasInput).val(data.alias);
|
||
$(emailInput).val(data.email);
|
||
var save = blocks.button('primary', '', Messages.settings_save);
|
||
var cancel = blocks.button('secondary', '', Messages.cancel);
|
||
Util.onClickEnter($(save), function () {
|
||
var aliasVal = $(aliasInput).val().trim();
|
||
if (!aliasVal) { return void UI.warn(Messages.error); }
|
||
var changes = {
|
||
alias: aliasVal,
|
||
email: $(emailInput).val().trim()
|
||
};
|
||
updateUser(key, changes);
|
||
});
|
||
Util.onClickEnter($(cancel), function () {
|
||
refreshUsers();
|
||
});
|
||
$alias.html('').append(aliasInput);
|
||
$email.html('').append(emailInput);
|
||
$actions.html('').append([save, cancel]);
|
||
};
|
||
|
||
let infoBtn = blocks.activeButton('primary', 'fa fa-database',
|
||
Messages.admin_diskUsageButton, function (done) {
|
||
getAccountData(key, (err, data) => {
|
||
done(!err);
|
||
if (err) { return void console.error(err); }
|
||
var table = renderAccountData(data);
|
||
UI.alert(table, () => {}, {
|
||
wide: true,
|
||
});
|
||
});
|
||
});
|
||
newEntries.push([
|
||
aliasCell,
|
||
emailCell,
|
||
[blocks.code(key), infoBtn],
|
||
new Date(data.time).toLocaleString(),
|
||
actions
|
||
]);
|
||
});
|
||
list.updateContent(newEntries);
|
||
});
|
||
};
|
||
refreshUsers();
|
||
Util.onClickEnter($b, function () {
|
||
var alias = $(userAlias).val().trim();
|
||
if (!alias) { return void UI.warn(Messages.error); }
|
||
$b.prop('disabled', true);
|
||
|
||
var done = () => { $b.prop('disabled', false); };
|
||
// TODO Get "block" from pin log?
|
||
|
||
var keyStr = $(userEdPublic).val().trim();
|
||
var edPublic = keyStr && Keys.canonicalize(keyStr);
|
||
if (!edPublic) {
|
||
done();
|
||
return void UI.warn(Messages.admin_invalKey);
|
||
}
|
||
var block = getBlockId($(userBlock).val());
|
||
|
||
var obj = {
|
||
alias,
|
||
email: $(userEmail).val(),
|
||
block: block,
|
||
edPublic: edPublic,
|
||
};
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADD_KNOWN_USER',
|
||
data: obj
|
||
}, function (e, response) {
|
||
done();
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(e, response);
|
||
}
|
||
$(userAlias).val('').focus();
|
||
$(userEmail).val('');
|
||
$(userBlock).val('');
|
||
$(userEdPublic).val('');
|
||
refreshUsers();
|
||
});
|
||
});
|
||
|
||
cb([form, list]);
|
||
});
|
||
|
||
// Msg.admin_defaultlimitHint, .admin_defaultlimitTitle
|
||
sidebar.addItem('defaultlimit', function (cb) {
|
||
var _limit = APP.instanceStatus.defaultStorageLimit;
|
||
var _limitMB = Util.bytesToMegabytes(_limit);
|
||
var limit = getPrettySize(_limit);
|
||
|
||
var newLimit = blocks.input({
|
||
type: 'number',
|
||
min: 0,
|
||
value: _limitMB,
|
||
'aria-labelledby': 'cp-admin-defaultlimit'
|
||
});
|
||
var button = blocks.button('primary', '', Messages.admin_setlimitButton);
|
||
var nav = blocks.nav([button]);
|
||
var text = blocks.inline(Messages._getKey('admin_limit', [limit]));
|
||
|
||
var form = blocks.form([
|
||
text,
|
||
newLimit
|
||
], nav);
|
||
|
||
UI.confirmButton(button, {
|
||
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, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(e, response);
|
||
}
|
||
var limit = getPrettySize(l);
|
||
$(text).text(Messages._getKey('admin_limit', [limit]));
|
||
});
|
||
});
|
||
|
||
cb(form);
|
||
});
|
||
|
||
// Msg.admin_setlimitHint, .admin_setlimitTitle
|
||
sidebar.addItem('setlimit', function(cb){
|
||
var user = blocks.input({ type:'text', class: 'cp-setlimit-user'});
|
||
var userBlock = blocks.labelledInput(Messages.admin_limitUser, user);
|
||
var $key = $(user);
|
||
var limit = blocks.input({
|
||
type: 'number',
|
||
min: 0,
|
||
value: 0,
|
||
class: 'cp-setlimit-limit'
|
||
});
|
||
var limitBlock = blocks.labelledInput(Messages.admin_limitMB, limit);
|
||
var note = blocks.input({ type: 'text', class: 'cp-setlimit-note' });
|
||
var noteBlock = blocks.labelledInput(Messages.admin_limitSetNote, note);
|
||
var $note = $(note);
|
||
|
||
var remove = blocks.button('danger', '', Messages.fc_remove );
|
||
var set = blocks.button('primary', '', Messages.admin_setlimitButton);
|
||
|
||
var nav = blocks.nav([set, remove]);
|
||
var form = blocks.form([
|
||
userBlock,
|
||
limitBlock,
|
||
noteBlock
|
||
], nav);
|
||
|
||
var getValues = function () {
|
||
var key = $key.val();
|
||
var _limit = parseInt($(limit).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);
|
||
}
|
||
var _note = ($note.val() || "").trim();
|
||
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, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
return;
|
||
}
|
||
APP.refreshLimits();
|
||
$key.val('');
|
||
});
|
||
});
|
||
|
||
Util.onClickEnter($(set), 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, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
return;
|
||
}
|
||
APP.refreshLimits();
|
||
$key.val('');
|
||
$note.val('');
|
||
});
|
||
});
|
||
|
||
cb(form);
|
||
});
|
||
|
||
// Msg.admin_getlimitsHint, .admin_getlimitsTitle
|
||
sidebar.addItem('getlimits', function(cb){
|
||
var header = [
|
||
Messages.settings_publicSigningKey,
|
||
Messages.admin_planlimit,
|
||
Messages.admin_planName,
|
||
Messages.admin_note
|
||
];
|
||
var table = blocks.table(header, []);
|
||
let $table = $(table).hide();
|
||
|
||
APP.refreshLimits = function () {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'GET_LIMITS',
|
||
}, function (e, data) {
|
||
$table.hide();
|
||
if (e) { return; }
|
||
if (!Array.isArray(data) || !data[0]) { return; }
|
||
$table.show();
|
||
|
||
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 content = list.map(function (key) {
|
||
var user = obj[key];
|
||
var limit = getPrettySize(user.limit);
|
||
var infoButton = blocks.button('primary','', Messages.admin_diskUsageButton);
|
||
Util.onClickEnter($(infoButton), function () {
|
||
getAccountData(key, (err, data) => {
|
||
if (err) { return void console.error(err); }
|
||
var table = renderAccountData(data);
|
||
UI.alert(table, () => {
|
||
|
||
}, {
|
||
wide: true,
|
||
});
|
||
});
|
||
});
|
||
|
||
var keyEl = h('code.cp-limit-key', key);
|
||
$(keyEl).click(function () {
|
||
$('[data-item="setlimit"]').find('.cp-setlimit-user').val(key);
|
||
$('[data-item="setlimit"]').find('.cp-setlimit-limit').val(Math.floor(user.limit / 1024 / 1024));
|
||
$('[data-item="setlimit"]').find('.cp-setlimit-note').val(user.note);
|
||
});
|
||
var title = Messages._getKey('admin_limit', [limit]) + ', ' +
|
||
Messages._getKey('admin_limitPlan', [user.plan]) + ', ' +
|
||
Messages._getKey('admin_limitNote', [user.note]);
|
||
var attr = { title: title };
|
||
return [
|
||
[keyEl, infoButton],
|
||
{attr, content: limit},
|
||
{attr, content: user.plan},
|
||
{attr, content: user.note}
|
||
];
|
||
});
|
||
table.updateContent(content);
|
||
});
|
||
};
|
||
APP.refreshLimits();
|
||
cb(table);
|
||
});
|
||
|
||
// Msg.admin_accountMetadataHint.admin_accountMetadataTitle
|
||
sidebar.addItem('account-metadata', function(cb) {
|
||
var input = blocks.input({
|
||
type: 'text',
|
||
placeholder: Messages.admin_accountMetadataPlaceholder,
|
||
value: '',
|
||
});
|
||
var $input = $(input);
|
||
|
||
var btn = blocks.button('primary', '', Messages.ui_generateReport);
|
||
var $btn = $(btn);
|
||
|
||
var nav = blocks.nav([btn]);
|
||
var results = blocks.inline([]);
|
||
|
||
var form = blocks.form([
|
||
input
|
||
], nav);
|
||
|
||
form.append(results);
|
||
|
||
var pending = false;
|
||
var getInputState = function () {
|
||
var val = $input.val().trim();
|
||
var key = Keys.canonicalize(val);
|
||
var state = {
|
||
value: val,
|
||
key: key,
|
||
valid: Boolean(key),
|
||
pending: pending,
|
||
};
|
||
|
||
return state;
|
||
};
|
||
|
||
disable($btn);
|
||
|
||
var setInterfaceState = function (state) {
|
||
state = state || getInputState();
|
||
var both = [$input, $btn];
|
||
if (state.pending) {
|
||
both.forEach(disable);
|
||
} else if (state.valid) {
|
||
both.forEach(enable);
|
||
} else {
|
||
enable($input);
|
||
disable($btn);
|
||
}
|
||
};
|
||
|
||
$input.on('keypress keyup change paste', function () {
|
||
setTimeout(setInterfaceState);
|
||
});
|
||
|
||
Util.onClickEnter($btn, function () {
|
||
if (pending) { return; }
|
||
var state = getInputState();
|
||
if (!state.valid) {
|
||
results.innerHTML = '';
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
var key = state.key;
|
||
pending = true;
|
||
setInterfaceState();
|
||
|
||
getAccountData(key, (err, data) => {
|
||
pending = false;
|
||
setInterfaceState();
|
||
if (!data) {
|
||
results.innerHTML = '';
|
||
return UI.warn(Messages.error);
|
||
}
|
||
var table = renderAccountData(data);
|
||
results.innerHTML = '';
|
||
results.appendChild(table);
|
||
});
|
||
});
|
||
|
||
cb(form);
|
||
|
||
});
|
||
|
||
var getDocumentData = function (id, cb) {
|
||
var data = {
|
||
generated: +new Date(),
|
||
id: id,
|
||
};
|
||
data.type = inferDocumentType(id);
|
||
|
||
nThen(function (w) {
|
||
if (data.type !== 'channel') { return; }
|
||
sframeCommand('GET_STORED_METADATA', id, w(function (err, res) {
|
||
if (err) { return void console.error(err); }
|
||
if (!(Array.isArray(res) && res[0])) { return void console.error("NO_METADATA"); }
|
||
var metadata = res[0];
|
||
data.metadata = metadata;
|
||
data.created = Util.find(data, ['metadata', 'created']);
|
||
}));
|
||
}).nThen(function (w) {
|
||
sframeCommand("GET_DOCUMENT_SIZE", id, w(function (err, res) {
|
||
if (err) { return void console.error(err); }
|
||
if (!(Array.isArray(res) && typeof(res[0]) === 'number')) {
|
||
return void console.error("NO_SIZE");
|
||
}
|
||
data.size = res[0];
|
||
}));
|
||
}).nThen(function (w) {
|
||
if (data.type !== 'channel') { return; }
|
||
sframeCommand('GET_LAST_CHANNEL_TIME', id, w(function (err, res) {
|
||
if (err) { return void console.error(err); }
|
||
if (!Array.isArray(res) || typeof(res[0]) !== 'number') { return void console.error(res); }
|
||
data.lastModified = res[0];
|
||
}));
|
||
}).nThen(function (w) {
|
||
// whether currently open
|
||
if (data.type !== 'channel') { return; }
|
||
sframeCommand('GET_CACHED_CHANNEL_METADATA', id, w(function (err, res) {
|
||
//console.info("cached channel metadata", err, res);
|
||
if (err === 'ENOENT') {
|
||
data.currentlyOpen = false;
|
||
return;
|
||
}
|
||
|
||
if (err) { return void console.error(err); }
|
||
if (!Array.isArray(res) || !res[0]) { return void console.error(res); }
|
||
data.currentlyOpen = true;
|
||
}));
|
||
}).nThen(function (w) {
|
||
// status (live, archived, unknown)
|
||
if (!['channel', 'file'].includes(data.type)) { return; }
|
||
sframeCommand('GET_DOCUMENT_STATUS', id, w(function (err, res) {
|
||
if (err) { return void console.error(err); }
|
||
if (!Array.isArray(res) || !res[0]) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(err, res);
|
||
}
|
||
data.live = res[0].live;
|
||
data.archived = res[0].archived;
|
||
data.placeholder = res[0].placeholder;
|
||
//console.error("get channel status", err, res);
|
||
}));
|
||
}).nThen(function () {
|
||
// for easy readability when copying to clipboard
|
||
try {
|
||
['generated', 'created', 'lastModified'].forEach(k => {
|
||
data[`${k}_formatted`] = new Date(data[k]);
|
||
});
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
|
||
cb(void 0, data);
|
||
});
|
||
};
|
||
|
||
/* FIXME
|
||
Messages.admin_getFullPinHistory = 'Pin history';
|
||
Messages.admin_archiveOwnedAccountDocuments = "Archive this account's owned documents (not implemented)";
|
||
Messages.admin_archiveOwnedDocumentsConfirm = "All content owned exclusively by this user will be archived. This means their documents, drive, and accounts will be made inaccessible. This action cannot be undone. Please save the full pin list before proceeding to ensure individual documents can be restored.";
|
||
*/
|
||
|
||
var localizeType = function (type) {
|
||
var o = {
|
||
channel: Messages.type.doc,
|
||
file: Messages.type.file,
|
||
};
|
||
return o[type] || Messages.ui_undefined;
|
||
};
|
||
|
||
var renderDocumentData = function (data) {
|
||
var tableObj = makeMetadataTable('cp-document-stats');
|
||
var row = tableObj.row;
|
||
|
||
row(Messages.admin_generatedAt, maybeDate(data.generated));
|
||
row(Messages.documentID, h('code', data.id));
|
||
row(Messages.admin_documentType, localizeType(data.type));
|
||
row(Messages.admin_documentSize, data.size? getPrettySize(data.size): Messages.ui_undefined);
|
||
|
||
if (data.type === 'channel') {
|
||
try {
|
||
row(Messages.admin_documentMetadata, h('pre', JSON.stringify(data.metadata || {}, null, 2)));
|
||
} catch (err2) {
|
||
UI.warn(Messages.error);
|
||
console.error(err2);
|
||
}
|
||
|
||
// actions
|
||
// get raw metadata history
|
||
var metadataHistoryButton = blocks.activeButton('primary', '', Messages.ui_fetch, done => {
|
||
sframeCommand('GET_METADATA_HISTORY', data.id, (err, result) => {
|
||
done(!err);
|
||
if (err) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(err);
|
||
}
|
||
if (!Array.isArray(result)) {
|
||
UI.warn(Messages.error);
|
||
return void console.error("Expected an array");
|
||
}
|
||
var tableObj = makeMetadataTable('cp-metadata-history');
|
||
var row = items => {
|
||
tableObj.table.appendChild(h('tr', items.map(item => {
|
||
return h('td', item);
|
||
})));
|
||
};
|
||
var scroll = el => h('div.scroll', el);
|
||
result.forEach(item => {
|
||
var raw = JSON.stringify(item);
|
||
var time;
|
||
var last;
|
||
if (Array.isArray(item)) {
|
||
last = item[item.length - 1];
|
||
if (typeof(last) === 'number') { time = last; }
|
||
} else if (item && typeof(item) === 'object') {
|
||
time = item.created;
|
||
}
|
||
row([
|
||
h('small', maybeDate(time)), // time
|
||
scroll(h('code', raw)), // Raw
|
||
]);
|
||
});
|
||
|
||
UI.confirm(tableObj.table, (yes) => {
|
||
if (!yes) { return; }
|
||
var content = result.map(line => JSON.stringify(line)).join('\n');
|
||
Clipboard.copy(content, (err) => {
|
||
if (err) { return UI.warn(Messages.error); }
|
||
UI.log(Messages.genericCopySuccess);
|
||
});
|
||
}, {
|
||
wide: true,
|
||
ok: Messages.copyToClipboard,
|
||
});
|
||
});
|
||
});
|
||
row(Messages.admin_getRawMetadata, metadataHistoryButton);
|
||
|
||
row(Messages.admin_documentCreationTime, maybeDate(data.created));
|
||
row(Messages.admin_documentModifiedTime, maybeDate(data.lastModified));
|
||
row(Messages.admin_currentlyOpen, localizeState(data.currentlyOpen));
|
||
}
|
||
if (['file', 'channel'].includes(data.type)) {
|
||
row(Messages.admin_channelAvailable, localizeState(data.live));
|
||
row(Messages.admin_channelArchived, localizeState(data.archived));
|
||
}
|
||
|
||
if (data.type === 'file') {
|
||
// TODO what to do for files?
|
||
|
||
}
|
||
|
||
if (data.placeholder) {
|
||
console.warn('Placeholder code', data.placeholder);
|
||
row(Messages.admin_channelPlaceholder, UI.getDestroyedPlaceholderMessage(data.placeholder));
|
||
}
|
||
|
||
if (data.live && data.archived) {
|
||
let disableButtons;
|
||
let restoreButton = blocks.activeButton('danger', '', Messages.admin_unarchiveButton, function () {
|
||
justifyRestorationDialog('', reason => {
|
||
nThen(function (w) {
|
||
sframeCommand('REMOVE_DOCUMENT', {
|
||
id: data.id,
|
||
reason: reason,
|
||
}, w(err => {
|
||
if (err) {
|
||
w.abort();
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
}));
|
||
}).nThen(function () {
|
||
sframeCommand("RESTORE_ARCHIVED_DOCUMENT", {
|
||
id: data.id,
|
||
reason: reason,
|
||
}, (err /*, response */) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
UI.log(Messages.restoredFromServer);
|
||
disableButtons();
|
||
});
|
||
});
|
||
});
|
||
}, true);
|
||
|
||
let archiveButton = blocks.activeButton('danger', '',Messages.admin_archiveButton, function () {
|
||
justifyArchivalDialog('', result => {
|
||
sframeCommand('ARCHIVE_DOCUMENT', {
|
||
id: data.id,
|
||
reason: result,
|
||
}, (err /*, response */) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
UI.log(Messages.archivedFromServer);
|
||
disableButtons();
|
||
});
|
||
});
|
||
}, true);
|
||
|
||
disableButtons = function () {
|
||
[archiveButton, restoreButton].forEach(el => {
|
||
disable($(el));
|
||
});
|
||
};
|
||
|
||
row(h('span', [
|
||
Messages.admin_documentConflict,
|
||
h('br'),
|
||
h('small', Messages.ui_experimental),
|
||
]), h('span', [
|
||
h('div.alert.alert-danger.cp-admin-bigger-alert', [
|
||
Messages.admin_conflictExplanation,
|
||
]),
|
||
h('p', [
|
||
restoreButton,
|
||
archiveButton,
|
||
]),
|
||
]));
|
||
} else if (data.live) {
|
||
// archive
|
||
var archiveDocumentButton = blocks.activeButton('danger', '' ,Messages.admin_archiveButton, function () {
|
||
justifyArchivalDialog('', result => {
|
||
sframeCommand('ARCHIVE_DOCUMENT', {
|
||
id: data.id,
|
||
reason: result,
|
||
}, (err /*, response */) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
UI.log(Messages.archivedFromServer);
|
||
disable($(archiveDocumentButton));
|
||
});
|
||
});
|
||
}, true);
|
||
row(Messages.admin_archiveDocument, h('span', [
|
||
archiveDocumentButton,
|
||
h('small', Messages.admin_archiveHint),
|
||
]));
|
||
} else if (data.archived) {
|
||
var restoreDocumentButton = blocks.activeButton('primary', '',Messages.admin_unarchiveButton, function () {
|
||
justifyRestorationDialog('', reason => {
|
||
sframeCommand("RESTORE_ARCHIVED_DOCUMENT", {
|
||
id: data.id,
|
||
reason: reason,
|
||
}, (err /*, response */) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
UI.log(Messages.restoredFromServer);
|
||
disable($(restoreDocumentButton));
|
||
});
|
||
});
|
||
}, true);
|
||
row(Messages.admin_restoreDocument, h('span', [
|
||
restoreDocumentButton,
|
||
h('small', Messages.admin_unarchiveHint),
|
||
]));
|
||
}
|
||
|
||
row(reportContentLabel, copyToClipboard(data));
|
||
|
||
return tableObj.table;
|
||
};
|
||
|
||
|
||
// Msg.admin_documentMetadataHint.admin_documentMetadataTitle
|
||
sidebar.addItem('document-metadata', function(cb){
|
||
var input = blocks.input({
|
||
type: 'text',
|
||
placeholder: Messages.admin_documentMetadataPlaceholder,
|
||
value: ''
|
||
});
|
||
var $input = $(input);
|
||
var passwordContainer = UI.passwordInput({
|
||
id: 'cp-database-document-pw',
|
||
placeholder: Messages.admin_archiveInput2,
|
||
});
|
||
var $passwordContainer = $(passwordContainer);
|
||
var $password = $(passwordContainer).find('input');
|
||
|
||
var getBlobId = pathname => {
|
||
var parts;
|
||
try {
|
||
if (typeof(pathname) !== 'string') { return; }
|
||
parts = pathname.split('/').filter(Boolean);
|
||
if (parts.length !== 3) { return; }
|
||
if (parts[0] !== 'blob') { return; }
|
||
if (parts[1].length !== 2) { return; }
|
||
if (parts[2].length !== 48) { return; }
|
||
if (!parts[2].startsWith(parts[1])) { return; }
|
||
} catch (err) { return false; }
|
||
return parts[2];
|
||
};
|
||
|
||
var pending = false;
|
||
var getInputState = function () {
|
||
var val = $input.val().trim();
|
||
var state = {
|
||
valid: false,
|
||
passwordRequired: false,
|
||
id: undefined,
|
||
input: val,
|
||
password: $password.val().trim(),
|
||
pending: false,
|
||
};
|
||
|
||
if (!val) { return state; }
|
||
if (isHex(val) && [32, 48].includes(val.length)) {
|
||
state.valid = true;
|
||
state.id = val;
|
||
return state;
|
||
}
|
||
|
||
var url;
|
||
try {
|
||
url = new URL(val, ApiConfig.httpUnsafeOrigin);
|
||
} catch (err) {}
|
||
|
||
if (!url) { return state; } // invalid
|
||
|
||
// recognize URLs of the form: /blob/f1/f1338921fe8a73ed5401780d2147f725deeb9e3329f0f01e
|
||
var blobId = getBlobId(url.pathname);
|
||
if (blobId) {
|
||
state.valid = true;
|
||
state.id = blobId;
|
||
return state;
|
||
}
|
||
|
||
var parsed = Hash.isValidHref(val);
|
||
if (!parsed || !parsed.hashData) { return state; }
|
||
if (parsed.hashData.version === 3) {
|
||
state.id = parsed.hashData.channel;
|
||
state.valid = true;
|
||
return state;
|
||
}
|
||
|
||
var secret;
|
||
if (parsed.hashData.password) {
|
||
state.passwordRequired = true;
|
||
secret = Hash.getSecrets(parsed.type, parsed.hash, state.password);
|
||
} else {
|
||
secret = Hash.getSecrets(parsed.type, parsed.hash);
|
||
}
|
||
if (secret && secret.channel) {
|
||
state.id = secret.channel;
|
||
state.valid = true;
|
||
return state;
|
||
}
|
||
return state;
|
||
};
|
||
|
||
var results = blocks.inline([]);
|
||
|
||
var btn = blocks.button('primary', '', Messages.ui_generateReport);
|
||
var $btn = $(btn);
|
||
|
||
var nav = blocks.nav([btn]);
|
||
var form = blocks.form([
|
||
input,
|
||
passwordContainer
|
||
], nav);
|
||
form.append(results);
|
||
$passwordContainer.hide();
|
||
disable($btn);
|
||
|
||
var setInterfaceState = function () {
|
||
var state = getInputState();
|
||
var all = [ $btn, $password, $input ];
|
||
var text = [$password, $input];
|
||
|
||
if (state.pending) {
|
||
all.forEach(disable);
|
||
} else if (state.valid) {
|
||
all.forEach(enable);
|
||
} else {
|
||
text.forEach(enable);
|
||
disable($btn);
|
||
}
|
||
if (state.passwordRequired) {
|
||
$passwordContainer.show();
|
||
} else {
|
||
$passwordContainer.hide();
|
||
}
|
||
};
|
||
|
||
$input.on('keypress keyup change paste', function () {
|
||
setTimeout(setInterfaceState);
|
||
});
|
||
|
||
Util.onClickEnter($btn, function () {
|
||
if (pending) { return; }
|
||
pending = true;
|
||
var state = getInputState();
|
||
setInterfaceState(state);
|
||
getDocumentData(state.id, function (err, data) {
|
||
pending = false;
|
||
setInterfaceState();
|
||
if (err) {
|
||
results.innerHTML = '';
|
||
return void UI.warn(err);
|
||
}
|
||
var table = renderDocumentData(data);
|
||
results.innerHTML = '';
|
||
results.appendChild(table);
|
||
});
|
||
});
|
||
|
||
cb(form);
|
||
});
|
||
|
||
var getBlockData = function (key, _cb) {
|
||
var cb = Util.once(Util.mkAsync(_cb));
|
||
var data = {
|
||
generated: +new Date(),
|
||
key: key,
|
||
};
|
||
|
||
nThen(function (w) {
|
||
sframeCommand('GET_DOCUMENT_STATUS', key, w((err, res) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
if (!Array.isArray(res) || !res[0]) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(err, res);
|
||
}
|
||
data.live = res[0].live;
|
||
data.archived = res[0].archived;
|
||
data.totp = res[0].totp;
|
||
data.placeholder = res[0].placeholder;
|
||
}));
|
||
}).nThen(function () {
|
||
try {
|
||
['generated'].forEach(k => {
|
||
data[`${k}_formatted`] = new Date(data[k]);
|
||
});
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
|
||
cb(void 0, data);
|
||
});
|
||
};
|
||
|
||
var renderBlockData = function (data) {
|
||
var tableObj = makeMetadataTable('cp-block-stats');
|
||
var row = tableObj.row;
|
||
|
||
row(Messages.admin_generatedAt, maybeDate(data.generated));
|
||
row(Messages.admin_blockKey, h('code', data.key));
|
||
row(Messages.admin_blockAvailable, localizeState(data.live));
|
||
row(Messages.admin_blockArchived, localizeState(data.archived));
|
||
|
||
row(Messages.admin_totpEnabled, localizeState(Boolean(data.totp.enabled)));
|
||
row(Messages.admin_totpRecoveryMethod, data.totp.recovery);
|
||
|
||
if (data.live) {
|
||
var archiveButton = blocks.activeButton('danger', '', Messages.ui_archive, function () {
|
||
justifyArchivalDialog('', reason => {
|
||
sframeCommand('ARCHIVE_BLOCK', {
|
||
key: data.key,
|
||
reason: reason,
|
||
}, (err, res) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
disable($(archiveButton));
|
||
UI.log(Messages.ui_success);
|
||
console.log('archive block', err, res);
|
||
});
|
||
});
|
||
}, true);
|
||
row(Messages.admin_archiveBlock, archiveButton);
|
||
}
|
||
if (data.placeholder) {
|
||
console.warn('Placeholder code', data.placeholder);
|
||
row(Messages.admin_channelPlaceholder, UI.getDestroyedPlaceholderMessage(data.placeholder, true));
|
||
}
|
||
if (data.archived && !data.live) {
|
||
var restoreButton = blocks.activeButton('danger', '', Messages.ui_restore, function () {
|
||
justifyRestorationDialog('', reason => {
|
||
sframeCommand('RESTORE_ARCHIVED_BLOCK', {
|
||
key: data.key,
|
||
reason: reason,
|
||
}, (err, res) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
disable($(restoreButton));
|
||
console.log('restore archived block', err, res);
|
||
UI.log(Messages.ui_success);
|
||
});
|
||
});
|
||
}, true);
|
||
row(Messages.admin_restoreBlock, restoreButton);
|
||
}
|
||
|
||
row(reportContentLabel, copyToClipboard(data));
|
||
|
||
return tableObj.table;
|
||
};
|
||
|
||
// Msg.admin_blockMetadataHint.admin_blockMetadataTitle
|
||
sidebar.addItem('block-metadata', function(cb){
|
||
var input = blocks.input({
|
||
type: 'text',
|
||
placeholder: Messages.admin_blockMetadataPlaceholder,
|
||
value: ''
|
||
});
|
||
var $input = $(input);
|
||
var btn = blocks.button('primary', '', Messages.ui_generateReport);
|
||
var $btn = $(btn);
|
||
disable($btn);
|
||
|
||
var results = blocks.inline([]);
|
||
var nav = blocks.nav([btn]);
|
||
var form = blocks.form([
|
||
input
|
||
], nav);
|
||
form.append(results);
|
||
|
||
var pending = false;
|
||
var getInputState = function () {
|
||
var val = $input.val().trim();
|
||
var state = {
|
||
pending: pending,
|
||
valid: false,
|
||
value: val,
|
||
key: '',
|
||
};
|
||
|
||
var key = getBlockId(val);
|
||
if (key) {
|
||
state.valid = true;
|
||
state.key = key;
|
||
}
|
||
return state;
|
||
};
|
||
var setInterfaceState = function () {
|
||
var state = getInputState();
|
||
var all = [$btn, $input];
|
||
|
||
if (state.pending) {
|
||
all.forEach(disable);
|
||
} else if (state.valid) {
|
||
all.forEach(enable);
|
||
} else {
|
||
enable($input);
|
||
disable($btn);
|
||
}
|
||
};
|
||
|
||
$input.on('keypress keyup change paste', function () {
|
||
setTimeout(setInterfaceState);
|
||
});
|
||
|
||
Util.onClickEnter($btn, function () {
|
||
if (pending) { return; }
|
||
var state = getInputState();
|
||
pending = true;
|
||
setInterfaceState();
|
||
getBlockData(state.key, (err, data) => {
|
||
pending = false;
|
||
setInterfaceState();
|
||
if (err || !data) {
|
||
results.innerHTML = '';
|
||
console.log(err, data);
|
||
return UI.warn(Messages.error);
|
||
}
|
||
var table = renderBlockData(data);
|
||
results.innerHTML = '';
|
||
results.appendChild(table);
|
||
});
|
||
});
|
||
|
||
cb(form);
|
||
|
||
});
|
||
|
||
var renderTOTPData = function (data) {
|
||
var tableObj = makeMetadataTable('cp-block-stats');
|
||
var row = tableObj.row;
|
||
|
||
row(Messages.admin_generatedAt, maybeDate(data.generated));
|
||
row(Messages.admin_blockKey, h('code', data.key));
|
||
row(Messages.admin_blockAvailable, localizeState(data.live));
|
||
|
||
if (!data.live || !data.totp) { return tableObj.table; }
|
||
|
||
row(Messages.admin_totpCheck, localizeState(data.totpCheck));
|
||
|
||
if (!data.totpCheck) { return tableObj.table; }
|
||
|
||
row(Messages.admin_totpEnabled, localizeState(Boolean(data.totp.enabled)));
|
||
if (data.totp && data.totp.enabled) {
|
||
row(Messages.admin_totpRecoveryMethod, data.totp.recovery);
|
||
}
|
||
|
||
if (!data.totpCheck || !data.totp.enabled) { return tableObj.table; }
|
||
|
||
// TOTP is enabled and the signature is correct: display "disable TOTP" button
|
||
var disableButton = blocks.button('danger', '', Messages.admin_totpDisableButton);
|
||
UI.confirmButton(disableButton, { classes: 'btn-danger' }, () => {
|
||
sframeCommand('DISABLE_MFA', data.key, (err, res) => {
|
||
if (err) {
|
||
console.error(err);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
if (!Array.isArray(res) || !res[0] || !res[0].success) {
|
||
return UI.warn(Messages.error);
|
||
}
|
||
UI.log(Messages.ui_success);
|
||
});
|
||
|
||
});
|
||
row(Messages.admin_totpDisable, disableButton);
|
||
|
||
return tableObj.table;
|
||
};
|
||
|
||
var checkTOTPRequest = function (json) {
|
||
var clone = Util.clone(json);
|
||
delete clone.proof;
|
||
|
||
var msg = Nacl.util.decodeUTF8(Sortify(clone));
|
||
var sig = Nacl.util.decodeBase64(json.proof);
|
||
var pub = Nacl.util.decodeBase64(json.blockId);
|
||
return Nacl.sign.detached.verify(msg, sig, pub);
|
||
};
|
||
|
||
// Msg.admin_totpRecoveryHint.admin_totpRecoveryTitle
|
||
sidebar.addItem('totp-recovery', function(cb){
|
||
var textarea = blocks.textarea({
|
||
id: 'textarea-input',
|
||
'aria-labelledby': 'cp-admin-totp-recovery'
|
||
});
|
||
var $input = $(textarea);
|
||
var btn = blocks.button('primary','', Messages.admin_totpDisable);
|
||
var $btn = $(btn);
|
||
var results = blocks.inline([]);
|
||
|
||
var nav = blocks.nav([btn]);
|
||
var form = blocks.form([
|
||
textarea
|
||
], nav);
|
||
form.append(results);
|
||
disable($btn);
|
||
|
||
var pending = false;
|
||
var getInputState = function () {
|
||
var val = $input.val().trim();
|
||
var state = {
|
||
pending: pending,
|
||
value: undefined,
|
||
key: '',
|
||
};
|
||
|
||
var json;
|
||
try { json = JSON.parse(val); } catch (err) { }
|
||
if (!json || json.intent !== "Disable TOTP" || !json.blockId || json.blockId.length !== 44 ||
|
||
!json.date || !json.proof) { return state; }
|
||
|
||
state.value = json;
|
||
state.key = json.blockId.replace(/\//g, '-');
|
||
return state;
|
||
};
|
||
var setInterfaceState = function () {
|
||
var state = getInputState();
|
||
var all = [$btn, $input];
|
||
|
||
if (state.pending) {
|
||
all.forEach(disable);
|
||
} else {
|
||
all.forEach(enable);
|
||
}
|
||
};
|
||
|
||
setInterfaceState();
|
||
Util.onClickEnter($btn, function () {
|
||
if (pending) { return; }
|
||
var state = getInputState();
|
||
if (!state.value) { return; }
|
||
pending = true;
|
||
setInterfaceState();
|
||
getBlockData(state.key, (err, data) => {
|
||
pending = false;
|
||
setInterfaceState();
|
||
if (err || !data) {
|
||
results.innerHTML = '';
|
||
console.log(err, data);
|
||
return UI.warn(Messages.error);
|
||
}
|
||
var check = checkTOTPRequest(state.value);
|
||
if (!check) { UI.warn(Messages.admin_totpFailed); }
|
||
data.totpCheck = check;
|
||
var table = renderTOTPData(data);
|
||
results.innerHTML = '';
|
||
results.appendChild(table);
|
||
});
|
||
});
|
||
|
||
cb(form);
|
||
});
|
||
|
||
var onRefreshStats = Util.mkEvent();
|
||
|
||
sidebar.addItem('refresh-stats', function(cb){
|
||
var btn = blocks.button('primary', '', Messages.oo_refresh);
|
||
var $btn = $(btn);
|
||
Util.onClickEnter($btn, function () {
|
||
onRefreshStats.fire();
|
||
});
|
||
cb(btn);
|
||
}, {
|
||
noTitle: true,
|
||
noHint: true
|
||
});
|
||
|
||
// Msg.admin_uptimeTitle, .admin_uptimeHint
|
||
sidebar.addItem('uptime', function(cb){
|
||
var pre = blocks.pre(Messages.admin_uptimeTitle);
|
||
var set = function () {
|
||
var uptime = APP.instanceStatus.launchTime;
|
||
if (typeof(uptime) !== 'number') { return; }
|
||
pre.innerText = '';
|
||
pre.innerText = new Date(uptime);
|
||
};
|
||
|
||
set();
|
||
onRefreshStats.reg(function () {
|
||
APP.updateStatus(set);
|
||
});
|
||
|
||
cb(pre);
|
||
});
|
||
|
||
// Msg.admin_activeSessionsHint, .admin_activeSessionsTitle
|
||
sidebar.addItem('active-sessions', function(cb){
|
||
var pre = blocks.pre('');
|
||
var onRefresh = function () {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ACTIVE_SESSIONS',
|
||
}, function (e, data) {
|
||
pre.innerText = '';
|
||
var total = data[0];
|
||
var ips = data[1];
|
||
pre.append(total + ' (' + ips + ')');
|
||
});
|
||
};
|
||
onRefresh();
|
||
onRefreshStats.reg(onRefresh);
|
||
|
||
cb(pre);
|
||
});
|
||
|
||
// Msg.admin_activePadsHint, .admin_activePadsTitle
|
||
sidebar.addItem('active-pads', function(cb){
|
||
var pre = blocks.pre('');
|
||
var onRefresh = function () {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ACTIVE_PADS',
|
||
}, function (e, data) {
|
||
pre.innerText = '';
|
||
pre.append(String(data));
|
||
});
|
||
};
|
||
onRefresh();
|
||
onRefreshStats.reg(onRefresh);
|
||
|
||
cb(pre);
|
||
});
|
||
|
||
// Msg.admin_openFilesHint, .admin_openFilesTitle
|
||
sidebar.addItem('open-files', function(cb){
|
||
var pre = blocks.pre('');
|
||
var onRefresh = function () {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'GET_FILE_DESCRIPTOR_COUNT',
|
||
}, function (e, data) {
|
||
if (e || (data && data.error)) {
|
||
console.error(e, data);
|
||
pre.innerText = '';
|
||
pre.append(String(e || data.error));
|
||
return;
|
||
}
|
||
pre.innerText='';
|
||
pre.append(String(data));
|
||
});
|
||
};
|
||
onRefresh();
|
||
onRefreshStats.reg(onRefresh);
|
||
|
||
cb(pre);
|
||
});
|
||
|
||
// Msg.admin_registeredHint, .admin_registeredTitle
|
||
sidebar.addItem('registered', function(cb){
|
||
var pre = blocks.pre('');
|
||
var onRefresh = function () {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'REGISTERED_USERS',
|
||
}, function (e, arr) {
|
||
pre.innerText = '';
|
||
let data = arr[0];
|
||
pre.append(String(data.users));
|
||
});
|
||
};
|
||
onRefresh();
|
||
onRefreshStats.reg(onRefresh);
|
||
|
||
cb(pre);
|
||
});
|
||
|
||
// Msg.admin_diskUsageHint, .admin_diskUsageTitle, .admin_diskUsageButton
|
||
sidebar.addItem('disk-usage', function(cb){
|
||
var button = blocks.button('primary', '', Messages.admin_diskUsageButton);
|
||
var $button = $(button);
|
||
var called = false;
|
||
var nav = blocks.nav([button]);
|
||
var content = blocks.table([], []);
|
||
var form = blocks.form([
|
||
content
|
||
], nav);
|
||
|
||
Util.onClickEnter($button, function() {
|
||
UI.confirm(Messages.admin_diskUsageWarning, function (yes) {
|
||
if (!yes) { return; }
|
||
$button.hide();
|
||
if (called) { return; }
|
||
called = true;
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'DISK_USAGE',
|
||
}, function (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';
|
||
}
|
||
});
|
||
let attr = {'class': 'cp-strong'};
|
||
let entries = Object.keys(obj).map(function (k) {
|
||
return [
|
||
{attr, content: (k === 'total' ? k : '/' + k)},
|
||
obj[k]
|
||
];
|
||
});
|
||
content.updateContent(entries);
|
||
});
|
||
});
|
||
});
|
||
cb(form);
|
||
});
|
||
|
||
let onRefreshSupportEvt = Util.mkEvent();
|
||
let refreshSupport = () => {
|
||
let moderators, supportKey;
|
||
nThen((waitFor) => {
|
||
APP.supportModule.execCommand('GET_PRIVATE_KEY', {}, waitFor((obj) => {
|
||
supportKey = obj && obj.curvePublic;
|
||
}));
|
||
}).nThen((waitFor) => {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'GET_MODERATORS',
|
||
data: {}
|
||
}, waitFor((e, response) => {
|
||
if (e || response.error) {
|
||
console.error(e || response.error);
|
||
UI.warn(Messages.error);
|
||
return;
|
||
}
|
||
moderators = response[0];
|
||
}));
|
||
}).nThen(() => {
|
||
onRefreshSupportEvt.fire({moderators, supportKey});
|
||
});
|
||
};
|
||
const getMyData = () => {
|
||
let metadataMgr = common.getMetadataMgr();
|
||
let priv = metadataMgr.getPrivateData();
|
||
let user = metadataMgr.getUserData();
|
||
return {
|
||
name: user.name,
|
||
edPublic: priv.edPublic,
|
||
curvePublic: user.curvePublic,
|
||
mailbox: user.notifications,
|
||
profile: user.profile
|
||
};
|
||
};
|
||
const getContactData = (curve) => {
|
||
if (!curve) { return getMyData(); }
|
||
let friends = common.getFriends(true);
|
||
let f = friends[curve];
|
||
return {
|
||
name: f.displayName,
|
||
edPublic: f.edPublic,
|
||
curvePublic: f.curvePublic,
|
||
mailbox: f.notifications,
|
||
profile: f.profile
|
||
};
|
||
};
|
||
let generateSupportKey = (moderatorsData, supportKey, cb) => {
|
||
let edPublic = common.getMetadataMgr().getPrivateData().edPublic;
|
||
if (supportKey && !moderatorsData[edPublic]) {
|
||
UI.alert("A support key already exists. You must be a moderator to generate a new one or delete the existing support data.");
|
||
return;
|
||
}
|
||
cb = cb || function () {};
|
||
const onError = (waitFor, err, res) => {
|
||
if (waitFor) { waitFor.abort(); }
|
||
console.error(err, res);
|
||
UI.warn(Messages.error);
|
||
cb(err);
|
||
};
|
||
|
||
nThen((waitFor) => {
|
||
// Add myself to moderator role if not already there
|
||
let me = getContactData();
|
||
if (moderatorsData[me.edPublic]) { return; }
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADD_MODERATOR',
|
||
data: me
|
||
}, waitFor((e, response) => {
|
||
if (e) { console.error(e, response); }
|
||
}));
|
||
}).nThen((waitFor) => {
|
||
// Copy chainpad doc and pin
|
||
APP.supportModule.execCommand('ROTATE_KEYS', {}, waitFor((obj) => {
|
||
if (obj && obj.error) { return void onError(waitFor, obj.error); }
|
||
if (obj && obj.success && obj.noNotify) {
|
||
UI.alert(Messages.admin_supportRotateNotify);
|
||
onError(waitFor, 'NOTIFY_ERROR', obj);
|
||
return;
|
||
}
|
||
UI.log(Messages.saved);
|
||
}));
|
||
}).nThen(() => {
|
||
refreshSupport();
|
||
});
|
||
};
|
||
// Msg.admin_supportSetupHint, .admin_supportSetupTitle
|
||
sidebar.addItem('support-setup', function (cb) {
|
||
let content = blocks.block();
|
||
const $div = $(content);
|
||
const redraw = function (moderatorsData, supportKey) {
|
||
$div.empty();
|
||
|
||
const state = blocks.block([], 'cp-admin-support-state');
|
||
const $state = $(state).appendTo($div);
|
||
const button = blocks.activeButton('primary', '',
|
||
Messages.admin_supportInit, done => {
|
||
generateSupportKey(moderatorsData, supportKey, (err) => {
|
||
done(!err);
|
||
});
|
||
});
|
||
const $button = $(button).appendTo($div);
|
||
|
||
const delButton = blocks.activeButton('danger', '',
|
||
Messages.admin_supportDelete, done => {
|
||
UI.confirm(Messages.admin_supportConfirm, function (yes) {
|
||
if (!yes) { return void done(false); }
|
||
// Send the decree, don't delete data locally, we just want to remove
|
||
// the support UI for the clients
|
||
APP.supportModule.execCommand('DISABLE_SUPPORT', {}, (obj) => {
|
||
done(!(obj && obj.error));
|
||
if (obj && obj.error) {
|
||
console.error(obj.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
UI.log(Messages.saved);
|
||
refreshSupport();
|
||
});
|
||
});
|
||
});
|
||
const $delButton = $(delButton).hide();
|
||
|
||
const openButton = blocks.button('primary', '', Messages.admin_supportOpen);
|
||
const $openButton = $(openButton).hide();
|
||
Util.onClickEnter($(openButton), () => {
|
||
common.openURL('/moderation/');
|
||
});
|
||
|
||
let nav = blocks.nav([button, openButton, delButton,
|
||
button.spinner, delButton.spinner]);
|
||
$div.append(nav);
|
||
|
||
const setState = function () {
|
||
$state.html('');
|
||
if (supportKey) {
|
||
$button.hide();
|
||
$delButton.show();
|
||
$openButton.show();
|
||
return $state.append([
|
||
blocks.icon('fa-check'),
|
||
blocks.inline(Messages.admin_supportEnabled)
|
||
]);
|
||
}
|
||
$state.append([
|
||
blocks.icon('fa-times'),
|
||
blocks.inline(Messages.admin_supportDisabled)
|
||
]);
|
||
};
|
||
setState();
|
||
};
|
||
onRefreshSupportEvt.reg(obj => {
|
||
redraw(obj.moderators, obj.supportKey);
|
||
});
|
||
cb(content);
|
||
});
|
||
|
||
// Msg.admin_supportTeamHint, .admin_supportTeamTitle
|
||
sidebar.addItem('support-team', function (cb) {
|
||
let edPublic = common.getMetadataMgr().getPrivateData().edPublic; // My edPublic
|
||
let content = blocks.block();
|
||
const $div = $(content);
|
||
const redraw = function (moderatorsData, supportKey) {
|
||
$div.empty();
|
||
|
||
const addModerator = (curve, _cb) => {
|
||
let cb = Util.mkAsync(_cb);
|
||
let userData = getContactData(curve);
|
||
if (!userData) { return void cb('INVALID_USER'); }
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADD_MODERATOR',
|
||
data: userData
|
||
}, (e, response) => {
|
||
if (e || (response && response.error)) {
|
||
return void cb(e || response.error);
|
||
}
|
||
|
||
// User added to support team in database, send them the keys
|
||
APP.supportModule.execCommand('ADD_MODERATOR', userData, cb);
|
||
});
|
||
};
|
||
const removeModerator = (ed) => {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'REMOVE_MODERATOR',
|
||
data: ed
|
||
}, (e, response) => {
|
||
if (e || (response && response.error)) {
|
||
console.error(e || response.error);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
generateSupportKey(moderatorsData, supportKey);
|
||
});
|
||
};
|
||
|
||
const drawModerators = () => {
|
||
if (!supportKey) {
|
||
const list = blocks.block([
|
||
blocks.icon('fa-times'),
|
||
blocks.inline(Messages.admin_supportDisabled)
|
||
], 'cp-admin-support-state');
|
||
$div.append(list);
|
||
return;
|
||
}
|
||
const members = {};
|
||
const friends = Util.clone(common.getFriends(false));
|
||
Object.keys(moderatorsData).forEach((ed) => {
|
||
let m = moderatorsData[ed];
|
||
members[m.curvePublic] = {
|
||
displayName: m.name,
|
||
edPublic: m.edPublic,
|
||
profile: m.profile,
|
||
curvePublic: m.curvePublic,
|
||
notificatons: m.mailbox
|
||
};
|
||
});
|
||
Object.keys(friends).forEach((curve) => {
|
||
if (members[curve]) { delete friends[curve]; }
|
||
});
|
||
let currentList = UIElements.getUserGrid(Messages.admin_supportMembers, {
|
||
common: common,
|
||
list: true,
|
||
large: true,
|
||
noSelect: true,
|
||
data: members,
|
||
remove: (el) => {
|
||
let ed = $(el).attr('data-ed');
|
||
removeModerator(ed);
|
||
}
|
||
});
|
||
let contactsGrid = UIElements.getUserGrid(Messages.admin_supportAdd, {
|
||
common: common,
|
||
list: true,
|
||
large: true,
|
||
data: friends
|
||
}, function () {});
|
||
|
||
let addBtn = blocks.button('primary', '', Messages.tag_add);
|
||
Util.onClickEnter($(addBtn), () => {
|
||
var $sel = $(contactsGrid.div).find('.cp-usergrid-user.cp-selected');
|
||
nThen((waitFor) => {
|
||
$sel.each((i, el) => {
|
||
let curve = $(el).attr('data-curve');
|
||
if (!curve) {
|
||
console.error('Missing data on selected user', el);
|
||
return void UI.warn(Messages.error);
|
||
}
|
||
addModerator(curve, waitFor());
|
||
});
|
||
}).nThen(() => {
|
||
refreshSupport();
|
||
});
|
||
});
|
||
// Only moderators can add new moderators
|
||
if (!moderatorsData[edPublic]) {
|
||
contactsGrid.div = undefined;
|
||
addBtn = undefined;
|
||
}
|
||
|
||
const list = blocks.block([
|
||
currentList.div,
|
||
contactsGrid.div,
|
||
blocks.nav([addBtn])
|
||
]);
|
||
$div.append(list);
|
||
};
|
||
drawModerators();
|
||
};
|
||
onRefreshSupportEvt.reg(obj => {
|
||
redraw(obj.moderators, obj.supportKey);
|
||
});
|
||
cb(content);
|
||
});
|
||
setTimeout(refreshSupport);
|
||
|
||
var getApi = function (cb) {
|
||
return function () {
|
||
require(['/api/broadcast?'+ (+new Date())], function (Broadcast) {
|
||
cb(Broadcast);
|
||
setTimeout(function () {
|
||
try {
|
||
var ctx = require.s.contexts._;
|
||
var defined = ctx.defined;
|
||
Object.keys(defined).forEach(function (href) {
|
||
if (/^\/api\/broadcast\?[0-9]{13}/.test(href)) {
|
||
delete defined[href];
|
||
return;
|
||
}
|
||
});
|
||
} catch (e) {}
|
||
});
|
||
});
|
||
};
|
||
};
|
||
var checkLastBroadcastHash = function (cb) {
|
||
var deleted = [];
|
||
|
||
require(['/api/broadcast?'+ (+new Date())], function (BCast) {
|
||
var hash = BCast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash
|
||
common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) {
|
||
if (e) { console.error(e); return void cb(e); }
|
||
|
||
// No history, nothing to change
|
||
if (!Array.isArray(msgs)) { return void cb(); }
|
||
if (!msgs.length) { return void cb(); }
|
||
|
||
var lastHash;
|
||
var next = false;
|
||
|
||
// Start from the most recent messages until you find a CUSTOM message and
|
||
// check if it has been deleted
|
||
msgs.reverse().some(function (data) {
|
||
var c = data.content;
|
||
|
||
// This is the hash we want to keep
|
||
if (next) {
|
||
if (!c || !c.hash) { return; }
|
||
lastHash = c.hash;
|
||
next = false;
|
||
return true;
|
||
}
|
||
|
||
// initialize with the most recent hash
|
||
if (!lastHash && c && c.hash) { lastHash = c.hash; }
|
||
|
||
var msg = c && c.msg;
|
||
if (!msg) { return; }
|
||
|
||
// Remember all deleted messages
|
||
if (msg.type === "BROADCAST_DELETE") {
|
||
deleted.push(Util.find(msg, ['content', 'uid']));
|
||
}
|
||
|
||
// Only check custom messages
|
||
if (msg.type !== "BROADCAST_CUSTOM") { return; }
|
||
|
||
// If the most recent CUSTOM message has been deleted, it means we don't
|
||
// need to keep any message and we can continue with lastHash as the most
|
||
// recent broadcast message.
|
||
if (deleted.indexOf(msg.uid) !== -1) { return true; }
|
||
|
||
// We just found the oldest message we want to keep, move one iteration
|
||
// further into the loop to get the next message's hash.
|
||
// If this is the end of the loop, don't bump lastBroadcastHash at all.
|
||
next = true;
|
||
});
|
||
|
||
// If we don't have to bump our lastBroadcastHash, abort
|
||
if (next) { return void cb(); }
|
||
|
||
// Otherwise, bump to lastHash
|
||
console.warn('Updating last broadcast hash to', lastHash);
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['SET_LAST_BROADCAST_HASH', [lastHash]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
return;
|
||
}
|
||
console.log('lastBroadcastHash updated');
|
||
if (typeof(cb) === "function") { cb(); }
|
||
});
|
||
});
|
||
});
|
||
|
||
};
|
||
// Msg.admin_maintenanceHint, admin_maintenanceTitle
|
||
sidebar.addItem('maintenance', function(cb){
|
||
var button = blocks.button('primary', '', Messages.admin_maintenanceButton);
|
||
var $button = $(button);
|
||
var start = blocks.input({
|
||
type: 'date',
|
||
id: 'cp-admin-start-input',
|
||
class: 'flatpickr-input'
|
||
});
|
||
var end = blocks.input({
|
||
type: 'date',
|
||
id: 'cp-admin-end-input',
|
||
class: 'flatpickr-input'
|
||
});
|
||
var labelStart = blocks.labelledInput(Messages.broadcast_start, start);
|
||
var labelEnd = blocks.labelledInput(Messages.broadcast_end, end);
|
||
let active = blocks.block([], 'cp-broadcast-active');
|
||
let $active = $(active);
|
||
|
||
var form = blocks.form([
|
||
active,
|
||
labelStart,
|
||
labelEnd,
|
||
], blocks.nav([button]));
|
||
|
||
var refresh = getApi(function (Broadcast) {
|
||
$active.empty();
|
||
var removeButton = blocks.button('danger', '', Messages.admin_maintenanceCancel);
|
||
|
||
if (Broadcast && Broadcast.maintenance) {
|
||
var m = Broadcast.maintenance;
|
||
if (m.start && m.end && m.end >= (+new Date())) {
|
||
$active.append([
|
||
UI.setHTML(h('div'), Messages._getKey('broadcast_maintenance', [
|
||
new Date(m.start).toLocaleString(),
|
||
new Date(m.end).toLocaleString(),
|
||
])),
|
||
blocks.nav([removeButton])
|
||
]);
|
||
}
|
||
}
|
||
UI.confirmButton(removeButton, {
|
||
classes: 'btn-danger',
|
||
}, function () {
|
||
send("");
|
||
});
|
||
|
||
});
|
||
refresh();
|
||
|
||
var $start = $(start);
|
||
var $end = $(end);
|
||
var is24h = UIElements.is24h();
|
||
var dateFormat = "Y-m-d H:i";
|
||
if (!is24h) { dateFormat = "Y-m-d h:i K"; }
|
||
|
||
var endPickr = Flatpickr(end, {
|
||
enableTime: true,
|
||
time_24hr: is24h,
|
||
dateFormat: dateFormat,
|
||
minDate: new Date()
|
||
});
|
||
Flatpickr(start, {
|
||
enableTime: true,
|
||
time_24hr: is24h,
|
||
minDate: new Date(),
|
||
dateFormat: dateFormat,
|
||
onChange: function () {
|
||
endPickr.set('minDate', new Date($start.val()));
|
||
}
|
||
});
|
||
|
||
// Extract form data
|
||
var getData = function () {
|
||
var start = +new Date($start.val());
|
||
var end = +new Date($end.val());
|
||
if (isNaN(start) || isNaN(end)) {
|
||
console.error('Invalid dates');
|
||
return false;
|
||
}
|
||
return {
|
||
start: start,
|
||
end: end
|
||
};
|
||
};
|
||
|
||
var send = function (data) {
|
||
disable($button);
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['SET_MAINTENANCE', [data]]
|
||
}, function (e, response) {
|
||
enable($button);
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
$button.prop('disabled', '');
|
||
return;
|
||
}
|
||
// Maintenance applied, send notification
|
||
common.mailbox.sendTo('BROADCAST_MAINTENANCE', {}, {}, function () {
|
||
checkLastBroadcastHash(function () {
|
||
setTimeout(refresh, 300);
|
||
});
|
||
});
|
||
});
|
||
|
||
};
|
||
Util.onClickEnter($(button), function () {
|
||
var data = getData();
|
||
if (data === false) { return void UI.warn(Messages.error); }
|
||
send(data);
|
||
});
|
||
|
||
common.makeUniversal('broadcast', {
|
||
onEvent: function (obj) {
|
||
var cmd = obj.ev;
|
||
if (cmd !== "MAINTENANCE") { return; }
|
||
refresh();
|
||
}
|
||
});
|
||
|
||
cb(form);
|
||
|
||
});
|
||
|
||
// Msg.admin_surveyHint, admin_surveyTitle
|
||
sidebar.addItem('survey', function(cb){
|
||
var button = blocks.button('primary', '', Messages.admin_surveyButton);
|
||
var $button = $(button);
|
||
let nav = blocks.nav([button]);
|
||
let active = blocks.block([], 'cp-sidebar-flex-block');
|
||
let $active = $(active);
|
||
|
||
var input = blocks.input({
|
||
type: 'text',
|
||
id: 'cp-admin-survey-url-input'
|
||
});
|
||
var labelledInput = blocks.labelledInput(Messages.broadcast_surveyURL, input);
|
||
var $input = $(input);
|
||
|
||
let send = function () {};
|
||
var refresh = getApi(function (Broadcast) {
|
||
var removeButton = blocks.button('danger', '', Messages.admin_surveyCancel);
|
||
UI.confirmButton(removeButton, {
|
||
classes: 'btn-danger',
|
||
}, function () {
|
||
send("");
|
||
});
|
||
|
||
$active.empty();
|
||
if (Broadcast && Broadcast.surveyURL) {
|
||
var a = blocks.link(Messages.admin_surveyActive, Broadcast.surveyURL);
|
||
$active.append([a, removeButton]);
|
||
}
|
||
});
|
||
refresh();
|
||
|
||
send = function (data) {
|
||
$button.prop('disabled', 'disabled');
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['SET_SURVEY_URL', [data]]
|
||
}, function (e, response) {
|
||
$button.prop('disabled', '');
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
return;
|
||
}
|
||
// Maintenance applied, send notification
|
||
common.mailbox.sendTo('BROADCAST_SURVEY', {
|
||
url: data
|
||
}, {}, function () {
|
||
checkLastBroadcastHash(function () {
|
||
setTimeout(refresh, 300);
|
||
});
|
||
});
|
||
});
|
||
};
|
||
// Extract form data
|
||
var getData = function () {
|
||
var url = $input.val();
|
||
if (!Util.isValidURL(url)) {
|
||
console.error('Invalid URL', url);
|
||
return false;
|
||
}
|
||
return url;
|
||
};
|
||
Util.onClickEnter($(button), function () {
|
||
var data = getData();
|
||
if (data === false) { return void UI.warn(Messages.error); }
|
||
send(data);
|
||
});
|
||
|
||
common.makeUniversal('broadcast', {
|
||
onEvent: function (obj) {
|
||
var cmd = obj.ev;
|
||
if (cmd !== "SURVEY") { return; }
|
||
refresh();
|
||
}
|
||
});
|
||
|
||
var form = blocks.form([active, labelledInput], nav);
|
||
cb(form);
|
||
});
|
||
|
||
// Msg.admin_broadcastHint, admin_broadcastTitle
|
||
sidebar.addItem('broadcast', function(cb) {
|
||
var form = blocks.block([], 'cp-admin-broadcast-form');
|
||
var $form = $(form);
|
||
var refresh = getApi(function(Broadcast) {
|
||
var button = blocks.button('primary', '', Messages.admin_broadcastButton);
|
||
var $button = $(button);
|
||
var removeButton = blocks.button('danger', '', Messages.admin_broadcastCancel);
|
||
var activeContent = Messages.admin_broadcastActive;
|
||
var active = blocks.block(blocks.inline(activeContent), 'cp-broadcast-active');
|
||
var $active = $(active);
|
||
var activeUid;
|
||
var deleted = [];
|
||
|
||
// Render active message (if there is one)
|
||
var hash = Broadcast.lastBroadcastHash || '1'; // Truthy value if no lastKnownHash
|
||
common.mailbox.getNotificationsHistory('broadcast', null, hash, function (e, msgs) {
|
||
if (e) { return void console.error(e); }
|
||
if (!Array.isArray(msgs)) { return; }
|
||
if (!msgs.length) {
|
||
$active.hide();
|
||
}
|
||
msgs.reverse().some(function (data) {
|
||
var c = data.content;
|
||
var msg = c && c.msg;
|
||
if (!msg) { return; }
|
||
if (msg.type === "BROADCAST_DELETE") {
|
||
deleted.push(Util.find(msg, ['content', 'uid']));
|
||
}
|
||
if (msg.type !== "BROADCAST_CUSTOM") { return; }
|
||
if (deleted.indexOf(msg.uid) !== -1) { return true; }
|
||
|
||
// We found an active custom message, show it
|
||
var el = common.mailbox.createElement(data);
|
||
|
||
var uid = Util.find(data, ['content', 'msg', 'uid']);
|
||
var time = Util.find(data, ['content', 'msg', 'content', 'time']);
|
||
|
||
var formattedTime = new Date(time || 0).toLocaleString();
|
||
var rowContent = [
|
||
'ID: ' + uid,
|
||
formattedTime,
|
||
$(el).find('.cp-notification-content').text(),
|
||
removeButton
|
||
];
|
||
var table = blocks.table([], [rowContent]);
|
||
$active.append(table);
|
||
activeUid = uid;
|
||
return true;
|
||
});
|
||
if (!activeUid) { $active.hide(); }
|
||
});
|
||
|
||
// Custom message
|
||
var container = blocks.block([], 'cp-broadcast-container');
|
||
var $container = $(container);
|
||
var languages = Messages._languages;
|
||
var keys = Object.keys(languages).sort();
|
||
|
||
// Always keep the textarea ordered by language code
|
||
var reorder = function () {
|
||
$container.find('.cp-broadcast-lang').each(function (i, el) {
|
||
var $el = $(el);
|
||
var l = $el.attr('data-lang');
|
||
var index = keys.indexOf(l);
|
||
if (index !== -1) {
|
||
$el.css('order', index * 2);
|
||
} else {
|
||
console.error('Language key not found:', l);
|
||
}
|
||
});
|
||
};
|
||
// Remove a textarea
|
||
var removeLang = function (l) {
|
||
$container.find('.cp-broadcast-lang[data-lang="'+l+'"]').remove();
|
||
|
||
var hasDefault = $container.find('.cp-broadcast-lang .cp-checkmark input:checked').length;
|
||
if (!hasDefault) {
|
||
$container.find('.cp-broadcast-lang').first().find('.cp-checkmark input').prop('checked', 'checked');
|
||
}
|
||
};
|
||
|
||
var getData = function () { return false; };
|
||
var onPreview = function (l) {
|
||
var data = getData();
|
||
if (data === false) { return void UI.warn(Messages.error); }
|
||
|
||
var msg = {
|
||
uid: Util.uid(),
|
||
type: 'BROADCAST_CUSTOM',
|
||
content: data
|
||
};
|
||
common.mailbox.onMessage({
|
||
lang: l,
|
||
type: 'broadcast',
|
||
content: {
|
||
msg: msg,
|
||
hash: 'LOCAL|' + JSON.stringify(msg).slice(0,58)
|
||
}
|
||
}, function () {
|
||
UI.log(Messages.saved);
|
||
});
|
||
};
|
||
|
||
// Add a textarea
|
||
var addLang = function (l) {
|
||
if ($container.find('.cp-broadcast-lang[data-lang="'+l+'"]').length) { return; }
|
||
var preview = blocks.button('secondary', '', Messages.broadcast_preview);
|
||
$(preview).click(function () {
|
||
onPreview(l);
|
||
});
|
||
var bcastDefault = Messages.broadcast_defaultLanguage;
|
||
var first = !$container.find('.cp-broadcast-lang').length;
|
||
var radio = UI.createRadio('broadcastDefault', null, bcastDefault, first, {
|
||
'data-lang': l,
|
||
label: {class: 'noTitle'}
|
||
});
|
||
|
||
var textarea = blocks.textarea();
|
||
var label = blocks.labelledInput(Messages.kanban_body, textarea);
|
||
|
||
$container.append(h('div.cp-broadcast-lang', { 'data-lang': l }, [
|
||
h('h4', languages[l]),
|
||
label,
|
||
radio,
|
||
preview
|
||
]));
|
||
|
||
reorder();
|
||
};
|
||
// Checkboxes to select translations
|
||
var boxes = keys.map(function (l) {
|
||
var $cbox = $(UI.createCheckbox('cp-broadcast-custom-lang-'+l,
|
||
languages[l], false, { label: { class: 'noTitle' } }));
|
||
var $check = $cbox.find('input').on('change', function () {
|
||
var c = $check.is(':checked');
|
||
if (c) { return void addLang(l); }
|
||
removeLang(l);
|
||
});
|
||
if (l === 'en') {
|
||
setTimeout(function () {
|
||
$check.click();
|
||
});
|
||
}
|
||
return $cbox[0];
|
||
});
|
||
|
||
// Extract form data
|
||
getData = function () {
|
||
var map = {};
|
||
var defaultLanguage;
|
||
var error = false;
|
||
$container.find('.cp-broadcast-lang').each(function (i, el) {
|
||
var $el = $(el);
|
||
var l = $el.attr('data-lang');
|
||
if (!l) { error = true; return; }
|
||
var text = $el.find('textarea').val();
|
||
if (!text.trim()) { error = true; return; }
|
||
if ($el.find('.cp-checkmark input').is(':checked')) {
|
||
defaultLanguage = l;
|
||
}
|
||
map[l] = text;
|
||
});
|
||
if (!Object.keys(map).length) {
|
||
console.error('You must select at least one language');
|
||
return false;
|
||
}
|
||
if (error) {
|
||
console.error('One of the selected languages has no data');
|
||
return false;
|
||
}
|
||
return {
|
||
defaultLanguage: defaultLanguage,
|
||
content: map
|
||
};
|
||
};
|
||
|
||
var send = function (data) {
|
||
$button.prop('disabled', 'disabled');
|
||
//data.time = +new Date(); // FIXME not used anymore?
|
||
common.mailbox.sendTo('BROADCAST_CUSTOM', data, {}, function (err) {
|
||
if (err) {
|
||
$button.prop('disabled', '');
|
||
console.error(err);
|
||
return UI.warn(Messages.error);
|
||
}
|
||
UI.log(Messages.saved);
|
||
checkLastBroadcastHash(function () {
|
||
setTimeout(refresh, 300);
|
||
});
|
||
});
|
||
};
|
||
|
||
$button.click(function () {
|
||
var data = getData();
|
||
if (data === false) { return void UI.warn(Messages.error); }
|
||
send(data);
|
||
});
|
||
|
||
UI.confirmButton(removeButton, {
|
||
classes: 'btn-danger',
|
||
}, function () {
|
||
if (!activeUid) { return; }
|
||
common.mailbox.sendTo('BROADCAST_DELETE', {
|
||
uid: activeUid
|
||
}, {}, function (err) {
|
||
if (err) { return UI.warn(Messages.error); }
|
||
UI.log(Messages.saved);
|
||
checkLastBroadcastHash(function () {
|
||
setTimeout(refresh, 300);
|
||
});
|
||
});
|
||
});
|
||
|
||
// Make the form
|
||
$form.empty().append([
|
||
active,
|
||
h('div', Messages.broadcast_translations),
|
||
h('div.cp-broadcast-languages', boxes),
|
||
container,
|
||
h('div.cp-broadcast-form-submit', [
|
||
button
|
||
])
|
||
]);
|
||
});
|
||
refresh();
|
||
cb(form);
|
||
|
||
});
|
||
|
||
var onRefreshPerformance = Util.mkEvent();
|
||
|
||
sidebar.addItem('refresh-performance', function(cb){
|
||
var btn = blocks.button('primary', '', Messages.oo_refresh);
|
||
Util.onClickEnter($(btn), function () {
|
||
onRefreshPerformance.fire();
|
||
});
|
||
cb(btn);
|
||
}, {
|
||
noTitle: true,
|
||
noHint: true
|
||
});
|
||
|
||
// Msg.admin_performanceProfilingHint, .admin_performanceProfilingTitle
|
||
sidebar.addItem('performance-profiling', function(cb){
|
||
var header = [
|
||
Messages.admin_performanceKeyHeading,
|
||
Messages.admin_performanceTimeHeading,
|
||
Messages.admin_performancePercentHeading
|
||
];
|
||
|
||
var table = blocks.table(header, []);
|
||
|
||
const onRefresh = function () {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'GET_WORKER_PROFILES',
|
||
}, function (e, data) {
|
||
if (e || data.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, data);
|
||
return;
|
||
}
|
||
|
||
var o = data[0];
|
||
var sorted = Object.keys(o).sort(function (a, b) {
|
||
if (o[b] - o[a] <= 0) { return -1; }
|
||
return 1;
|
||
});
|
||
|
||
var total = 0;
|
||
sorted.forEach(function (key) { total += o[key]; });
|
||
|
||
const newRows = sorted.map(function (key) {
|
||
var percent = Math.floor((o[key] / total) * 1000) / 10;
|
||
return [key, o[key], percent + '%'];
|
||
});
|
||
|
||
table.updateContent(newRows);
|
||
});
|
||
};
|
||
|
||
onRefresh();
|
||
onRefreshPerformance.reg(onRefresh);
|
||
|
||
cb(table);
|
||
});
|
||
|
||
|
||
// Msg.admin_enableDiskMeasurementsTitle.admin_enableDiskMeasurementsHint
|
||
sidebar.addCheckboxItem({
|
||
getState: function () {
|
||
return APP.instanceStatus.enableProfiling;
|
||
},
|
||
key: 'enable-disk-measurements',
|
||
options: { htmlHint: true },
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['ENABLE_PROFILING', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.enableProfiling);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
var isPositiveInteger = function (n) {
|
||
return n && typeof(n) === 'number' && n % 1 === 0 && n > 0;
|
||
};
|
||
|
||
// Msg.admin_bytesWrittenTitle.admin_bytesWrittenHint
|
||
sidebar.addItem('bytes-written', function(cb){
|
||
var duration = APP.instanceStatus.profilingWindow;
|
||
if (!isPositiveInteger(duration)) { duration = 10000; }
|
||
var newDuration = blocks.input({
|
||
type:'number',
|
||
min: 0,
|
||
value: duration
|
||
});
|
||
var set = blocks.button('primary', '', Messages.admin_setDuration);
|
||
var label = blocks.labelledInput( Messages.ui_ms, newDuration);
|
||
var nav = blocks.nav([set]);
|
||
var form = blocks.form([
|
||
label
|
||
], nav);
|
||
UI.confirmButton(set, {
|
||
classes: 'btn-primary',
|
||
multiple: true,
|
||
validate: function () {
|
||
var l = parseInt($(newDuration).val());
|
||
if (isNaN(l)) { return false; }
|
||
return true;
|
||
}
|
||
}, function () {
|
||
var d = parseInt($(newDuration).val());
|
||
if (!isPositiveInteger(d)) { return void UI.warn(Messages.error); }
|
||
|
||
var data = [d];
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['SET_PROFILING_WINDOW', data]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
return void console.error(e, response);
|
||
}
|
||
$(form).find('.cp-admin-bytes-written-duration').text(Messages._getKey('admin_bytesWrittenDuration', [d]));
|
||
});
|
||
});
|
||
cb(form);
|
||
});
|
||
|
||
// Messages.admin_updateAvailableTitle.admin_updateAvailableHint.admin_updateAvailableLabel.admin_updateAvailableButton
|
||
sidebar.addItem('update-available', function(cb){
|
||
if (!APP.instanceStatus.updateAvailable) { return; }
|
||
|
||
var updateURL = 'https://github.com/cryptpad/cryptpad/releases/latest';
|
||
if (typeof(APP.instanceStatus.updateAvailable) === 'string') {
|
||
updateURL = APP.instanceStatus.updateAvailable;
|
||
}
|
||
var button = blocks.button('primary', '', Messages.admin_updateAvailableButton);
|
||
Util.onClickEnter($(button), function () {
|
||
common.openURL(updateURL);
|
||
});
|
||
|
||
cb(button);
|
||
});
|
||
|
||
// Messages.admin_checkupButton.admin_checkupHint.admin_checkupTitle
|
||
sidebar.addItem('checkup', function(cb){
|
||
var button = blocks.button('primary', '', Messages.admin_checkupButton);
|
||
Util.onClickEnter($(button), function () {
|
||
common.openURL('/checkup/');
|
||
});
|
||
|
||
cb(button);
|
||
});
|
||
|
||
|
||
// Messages.admin_blockDailyCheckTitle.admin_blockDailyCheckHint.admin_blockDailyCheckLabel
|
||
sidebar.addCheckboxItem({
|
||
key: 'block-daily-check',
|
||
getState: function () {
|
||
return APP.instanceStatus.blockDailyCheck;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['BLOCK_DAILY_CHECK', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.blockDailyCheck);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
// Messages.admin_provideAggregateStatisticsTitle.admin_provideAggregateStatisticsHint.admin_provideAggregateStatisticsLabel
|
||
sidebar.addCheckboxItem({
|
||
key: 'provide-aggregate-statistics',
|
||
getState: function () {
|
||
return APP.instanceStatus.provideAggregateStatistics;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['PROVIDE_AGGREGATE_STATISTICS', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.provideAggregateStatistics);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
// Messages.admin_listMyInstanceTitle.admin_listMyInstanceHint.admin_listMyInstanceLabel
|
||
sidebar.addCheckboxItem({
|
||
key: 'list-my-instance',
|
||
getState: function () {
|
||
return APP.instanceStatus.listMyInstance;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['LIST_MY_INSTANCE', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.listMyInstance);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
// Messages.admin_consentToContactTitle.admin_consentToContactHint.admin_consentToContactLabel
|
||
sidebar.addCheckboxItem({
|
||
key: 'consent-to-contact',
|
||
getState: function () {
|
||
return APP.instanceStatus.consentToContact;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['CONSENT_TO_CONTACT', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.consentToContact);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
// Messages.admin_removeDonateButtonTitle.admin_removeDonateButtonHint.admin_removeDonateButtonLabel
|
||
sidebar.addCheckboxItem({
|
||
key: 'remove-donate-button',
|
||
getState: function () {
|
||
return APP.instanceStatus.removeDonateButton;
|
||
},
|
||
query: function (val, setState) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: ['REMOVE_DONATE_BUTTON', [val]]
|
||
}, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
console.error(e, response);
|
||
}
|
||
APP.updateStatus(function () {
|
||
setState(APP.instanceStatus.removeDonateButton);
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
var sendDecree = function (data, cb) {
|
||
sFrameChan.query('Q_ADMIN_RPC', {
|
||
cmd: 'ADMIN_DECREE',
|
||
data: data,
|
||
}, cb);
|
||
};
|
||
|
||
// Messages.admin_instancePurposeTitle.admin_instancePurposeHint
|
||
sidebar.addItem('instance-purpose', function(cb){
|
||
var values = [
|
||
'noanswer', // Messages.admin_purpose_noanswer
|
||
'experiment', // Messages.admin_purpose_experiment
|
||
'personal', // Messages.admin_purpose_personal
|
||
'education', // Messages.admin_purpose_education
|
||
'org', // Messages.admin_purpose_org
|
||
'business', // Messages.admin_purpose_business
|
||
'public', // Messages.admin_purpose_public
|
||
];
|
||
var defaultPurpose = 'noanswer';
|
||
var purpose = APP.instanceStatus.instancePurpose || defaultPurpose;
|
||
|
||
var opts = values.map(function (key) {
|
||
var full_key = 'admin_purpose_' + key;
|
||
return UI.createRadio('cp-instance-purpose-radio', 'cp-instance-purpose-radio-'+key,
|
||
Messages[full_key] || Messages._getKey(full_key, [defaultPurpose]),
|
||
key === purpose, {
|
||
input: { value: key },
|
||
//label: { class: 'noTitle' }
|
||
});
|
||
});
|
||
|
||
var $opts = $(opts);
|
||
|
||
var setPurpose = function (value, cb) {
|
||
sendDecree([
|
||
'SET_INSTANCE_PURPOSE',
|
||
[ value]
|
||
], cb);
|
||
};
|
||
|
||
$opts.on('change', function () {
|
||
var val = $opts.find('input:radio:checked').val();
|
||
console.log(val);
|
||
//spinner.spin();
|
||
setPurpose(val, function (e, response) {
|
||
if (e || response.error) {
|
||
UI.warn(Messages.error);
|
||
//spinner.hide();
|
||
return;
|
||
}
|
||
//spinner.done();
|
||
UI.log(Messages.saved);
|
||
});
|
||
});
|
||
|
||
cb(opts);
|
||
});
|
||
|
||
// EXTENSION_POINT:ADMIN_ITEM
|
||
let utils = {
|
||
h, Util, Hash
|
||
};
|
||
common.getExtensions('ADMIN_ITEM').forEach(ext => {
|
||
if (!ext || !ext.id || typeof(ext.getContent) !== "function") {
|
||
return console.error('Invalid extension point', 'ADMIN_CATEGORY', ext);
|
||
}
|
||
if (sidebar.hasItem(ext.id)) {
|
||
return console.error('Extension point ID already used', ext);
|
||
}
|
||
|
||
sidebar.addItem(ext.id, cb => {
|
||
ext.getContent(common, blocks, utils, content => {
|
||
cb(content);
|
||
});
|
||
}, {
|
||
noTitle: !ext.title,
|
||
noHint: !ext.description,
|
||
title: ext.title,
|
||
hint: ext.description
|
||
});
|
||
});
|
||
|
||
|
||
|
||
sidebar.makeLeftside(categories);
|
||
};
|
||
|
||
|
||
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();
|
||
});
|
||
};
|
||
|
||
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();
|
||
};
|
||
|
||
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');
|
||
sFrameChan = common.getSframeChannel();
|
||
sFrameChan.onReady(waitFor());
|
||
}).nThen(function (waitFor) {
|
||
if (!common.isAdmin()) { return; }
|
||
updateStatus(waitFor());
|
||
}).nThen(function( /*waitFor*/ ) {
|
||
common.setTabTitle(Messages.adminPage || 'Administration');
|
||
|
||
if (!common.isAdmin()) {
|
||
return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
|
||
}
|
||
|
||
// Add toolbar
|
||
createToolbar();
|
||
|
||
APP.supportModule = common.makeUniversal('support');
|
||
|
||
// Content
|
||
andThen(common, APP.$container);
|
||
|
||
UI.removeLoadingScreen();
|
||
});
|
||
});
|