WIP admin 'network' panel

This commit is contained in:
ansuz 2021-06-08 20:24:30 +05:30
parent 3965af1992
commit 41db88a360
7 changed files with 316 additions and 53 deletions

View file

@ -122,28 +122,6 @@ module.exports = {
],
*/
/* We're very proud that CryptPad is available to the public as free software!
* We do, however, still need to pay our bills as we develop the platform.
*
* By default CryptPad will prompt users to consider donating to
* our OpenCollective campaign. We publish the state of our finances periodically
* so you can decide for yourself whether our expenses are reasonable.
*
* You can disable any solicitations for donations by setting 'removeDonateButton' to true,
* but we'd appreciate it if you didn't!
*/
//removeDonateButton: false,
/*
* By default, CryptPad contacts one of our servers once a day.
* This check-in will also send some very basic information about your instance including its
* version and the adminEmail so we can reach you if we are aware of a serious problem.
* We will never sell it or send you marketing mail.
*
* If you want to block this check-in and remain set 'blockDailyCheck' to true.
*/
//blockDailyCheck: false,
/* =====================
* STORAGE
* ===================== */

View file

@ -10,6 +10,8 @@ module.exports.create = function (Env) {
nThen(function (w) {
Decrees.load(Env, w(function (err) {
Env.flushCache();
if (err) {
log.error('DECREES_LOADING', {
error: err.code || err,
@ -17,6 +19,36 @@ nThen(function (w) {
});
console.error(err);
}
var stats = {
restrictRegistration: Env.restrictRegistration,
supportMailbox: Env.supportMailbox,
defaultStorageLimit: Env.defaultStorageLimit,
maxUploadSize: Env.maxUploadSize,
premiumUploadSize: Env.premiumUploadSize,
adminEmail: Env.adminEmail,
inactiveTime: Env.inactiveTime,
accountRetentionTime: Env.accountRetentionTime,
archiveRetentionTime: Env.archiveRetentionTime,
httpUnsafeOrigin: Env.httpUnsafeOrigin,
httpSafeOrigin: Env.httpSafeOrigin,
adminKeys: Env.admins,
consentToContact: Env.consentToContact,
listMyInstance: Env.listMyInstance,
provideAggregateStatistics: Env.provideAggregateStatistics,
removeDonateButton: Env.removeDonateButton,
blockDailyCheck: Env.blockDailyCheck,
};
console.log(stats);
}));
}).nThen(function () {
// asynchronously create a historyKeeper and RPC together

View file

@ -317,6 +317,13 @@ var instanceStatus = function (Env, Server, cb) {
maxUploadSize: Env.maxUploadSize,
premiumUploadSize: Env.premiumUploadSize,
consentToContact: Env.consentToContact,
listMyInstance: Env.listMyInstance,
provideAggregateStatistics: Env.provideAggregateStatistics,
removeDonateButton: Env.removeDonateButton,
blockDailyCheck: Env.blockDailyCheck,
});
};

View file

@ -34,6 +34,13 @@ SET_MAINTENANCE
SET_ADMIN_EMAIL
SET_SUPPORT_MAILBOX
// COMMUNITY PARTICIPATION AND GOVERNANCE
CONSENT_TO_CONTACT
LIST_MY_INSTANCE
PROVIDE_AGGREGATE_STATISTICS
REMOVE_DONATE_BUTTON
BLOCK_DAILY_CHECK
NOT IMPLEMENTED:
// RESTRICTED REGISTRATION
@ -89,6 +96,21 @@ commands.DISABLE_INTEGRATED_EVICTION = makeBooleanSetter('disableIntegratedEvict
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['DISABLE_INTEGRATED_TASKS', [true]]], console.log)
commands.DISABLE_INTEGRATED_TASKS = makeBooleanSetter('disableIntegratedTasks');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['CONSENT_TO_CONTACT', [true]]], console.log)
commands.CONSENT_TO_CONTACT = makeBooleanSetter('consentToContact');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['LIST_MY_INSTANCE', [true]]], console.log)
commands.LIST_MY_INSTANCE = makeBooleanSetter('listMyInstance');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['PROVIDE_AGGREGATE_STATISTICS', [true]]], console.log)
commands.PROVIDE_AGGREGATE_STATISTICS = makeBooleanSetter('provideAggregateStatistics');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['REMOVE_DONATE_BUTTON', [true]]], console.log)
commands.REMOVE_DONATE_BUTTON = makeBooleanSetter('removeDonateButton');
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['BLOCK_DAILY_CHECK', [true]]], console.log)
commands.BLOCK_DAILY_CHECK = makeBooleanSetter('blockDailyCheck');
/*
var isNonNegativeNumber = function (n) {
return !(typeof(n) !== 'number' || isNaN(n) || n < 0);

View file

@ -11,9 +11,17 @@ const Core = require("./commands/core");
const Quota = require("./commands/quota");
const Util = require("./common-util");
module.exports.create = function (config) {
var canonicalizeOrigin = function (s) {
if (typeof(s) === 'undefined') { return; }
return (s || '').trim().replace(/\/+$/, '');
};
module.exports.create = function (config) {
const Env = {
httpUnsafeOrigin: canonicalizeOrigin(config.httpUnsafeOrigin),
httpSafeOrigin: canonicalizeOrigin(config.httpSafeOrigin),
removeDonateButton: config.removeDonateButton,
OFFLINE_MODE: false,
FRESH_KEY: '',
FRESH_MODE: true,
@ -97,6 +105,10 @@ module.exports.create = function (config) {
allowSubscriptions: config.allowSubscriptions === true,
blockDailyCheck: config.blockDailyCheck === true,
consentToContact: false,
listMyInstance: false,
provideAggregateStatistics: false,
myDomain: config.myDomain,
mySubdomain: config.mySubdomain, // only exists for the accounts integration
customLimits: {},

View file

@ -16,10 +16,6 @@ var Env = require("./lib/env").create(config);
var app = Express();
var canonicalizeOrigin = function (s) {
return (s || '').trim().replace(/\/+$/, '');
};
var fancyURL = function (domain, path) {
try {
if (domain && path) { return new URL(path, domain).href; }
@ -30,15 +26,10 @@ var fancyURL = function (domain, path) {
(function () {
// you absolutely must provide an 'httpUnsafeOrigin'
if (typeof(config.httpUnsafeOrigin) !== 'string') {
if (typeof(Env.httpUnsafeOrigin) !== 'string') {
throw new Error("No 'httpUnsafeOrigin' provided");
}
config.httpUnsafeOrigin = canonicalizeOrigin(config.httpUnsafeOrigin);
if (typeof(config.httpSafeOrigin) === 'string') {
config.httpSafeOrigin = canonicalizeOrigin(config.httpSafeOrigin);
}
// fall back to listening on a local address
// if httpAddress is not a string
if (typeof(config.httpAddress) !== 'string') {
@ -50,7 +41,7 @@ var fancyURL = function (domain, path) {
config.httpPort = 3000;
}
if (typeof(config.httpSafeOrigin) !== 'string') {
if (typeof(Env.httpSafeOrigin) !== 'string') {
Env.NO_SANDBOX = true;
if (typeof(config.httpSafePort) !== 'number') {
config.httpSafePort = config.httpPort + 1;
@ -76,7 +67,7 @@ var setHeaders = (function () {
}
// next define the base Content Security Policy (CSP) headers
if (typeof(config.contentSecurity) === 'string') {
if (typeof(config.contentSecurity) === 'string') { // XXX deprecate this
headers['Content-Security-Policy'] = config.contentSecurity;
if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' }
if (headers['Content-Security-Policy'].indexOf('frame-ancestors') === -1) {
@ -87,14 +78,14 @@ var setHeaders = (function () {
}
} else {
// use the default CSP headers constructed with your domain
headers['Content-Security-Policy'] = Default.contentSecurity(config.httpUnsafeOrigin);
headers['Content-Security-Policy'] = Default.contentSecurity(Env.httpUnsafeOrigin);
}
const padHeaders = Util.clone(headers);
if (typeof(config.padContentSecurity) === 'string') {
padHeaders['Content-Security-Policy'] = config.padContentSecurity;
} else {
padHeaders['Content-Security-Policy'] = Default.padContentSecurity(config.httpUnsafeOrigin);
padHeaders['Content-Security-Policy'] = Default.padContentSecurity(Env.httpUnsafeOrigin);
}
if (Object.keys(headers).length) {
return function (req, res) {
@ -151,7 +142,7 @@ app.head(/^\/common\/feedback\.html/, function (req, res, next) {
app.use('/blob', function (req, res, next) {
if (req.method === 'HEAD') {
Express.static(Path.join(__dirname, (config.blobPath || './blob')), {
Express.static(Path.join(__dirname, Env.paths.blob),
setHeaders: function (res, path, stat) {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Headers', 'Content-Length');
@ -191,13 +182,13 @@ var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$');
app.get(mainPagePattern, Express.static(__dirname + '/customize'));
app.get(mainPagePattern, Express.static(__dirname + '/customize.dist'));
app.use("/blob", Express.static(Path.join(__dirname, (config.blobPath || './blob')), {
app.use("/blob", Express.static(Path.join(__dirname, Env.paths.blob), {
maxAge: Env.DEV_MODE? "0d": "365d"
}));
app.use("/datastore", Express.static(Path.join(__dirname, (config.filePath || './datastore')), {
app.use("/datastore", Express.static(Path.join(__dirname, Env.paths.data), {
maxAge: "0d"
}));
app.use("/block", Express.static(Path.join(__dirname, (config.blockPath || '/block')), {
app.use("/block", Express.static(Path.join(__dirname, Env.paths.block), {
maxAge: "0d",
}));
@ -254,10 +245,10 @@ var serveConfig = makeRouteCache(function (host) {
waitSeconds: 600,
urlArgs: 'ver=' + Package.version + cacheString(),
},
removeDonateButton: (config.removeDonateButton === true),
allowSubscriptions: (config.allowSubscriptions === true),
removeDonateButton: (Env.removeDonateButton === true),
allowSubscriptions: (Env.allowSubscriptions === true),
websocketPath: config.externalWebsocketURL,
httpUnsafeOrigin: config.httpUnsafeOrigin,
httpUnsafeOrigin: Env.httpUnsafeOrigin,
adminEmail: Env.adminEmail,
adminKeys: Env.admins,
inactiveTime: Env.inactiveTime,
@ -265,10 +256,10 @@ var serveConfig = makeRouteCache(function (host) {
defaultStorageLimit: Env.defaultStorageLimit,
maxUploadSize: Env.maxUploadSize,
premiumUploadSize: Env.premiumUploadSize,
restrictRegistration: Env.restrictRegistration, // FIXME see the race condition in env.js
restrictRegistration: Env.restrictRegistration,
}, null, '\t'),
'obj.httpSafeOrigin = ' + (function () {
if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; }
if (Env.httpSafeOrigin) { return '"' + Env.httpSafeOrigin + '"'; }
if (config.httpSafePort) {
return "(function () { return window.location.origin.replace(/\:[0-9]+$/, ':" +
config.httpSafePort + "'); }())";
@ -332,16 +323,16 @@ nThen(function (w) {
var ps = port === 80? '': ':' + port;
var roughAddress = 'http://' + hostName + ps;
var betterAddress = fancyURL(config.httpUnsafeOrigin);
var betterAddress = fancyURL(Env.httpUnsafeOrigin);
if (betterAddress) {
console.log('Serving content for %s via %s.\n', betterAddress, roughAddress);
} else {
console.log('Serving content via %s.\n', roughAddress);
}
if (!Array.isArray(config.adminKeys)) {
if (!Env.admins.length) {
console.log("Your instance is not correctly configured for safe use in production.\nSee %s for more information.\n",
fancyURL(config.httpUnsafeOrigin, '/checkup/') || 'https://your-domain.com/checkup/'
fancyURL(Env.httpUnsafeOrigin, '/checkup/') || 'https://your-domain.com/checkup/'
);
}
});

View file

@ -85,15 +85,26 @@ define([
'performance': [ // Msg.admin_cat_performance
'cp-admin-refresh-performance',
'cp-admin-performance-profiling',
]
],
'network': [ // Msg.admin_cat_network
'cp-admin-checkup',
'cp-admin-block-daily-check',
'cp-admin-consent-to-contact',
'cp-admin-list-my-instance',
'cp-admin-provide-aggregate-statistics',
'cp-admin-remove-donate-button',
],
};
var create = {};
var keyToCamlCase = function (key) {
return key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
};
var makeBlock = function (key, addButton) { // Title, Hint, maybeButton
// Convert to camlCase for translation keys
var safeKey = key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
var safeKey = keyToCamlCase(key);
var $div = $('<div>', {'class': 'cp-admin-' + key + ' cp-sidebarlayout-element'});
$('<label>').text(Messages['admin_'+safeKey+'Title'] || key).appendTo($div);
$('<span>', {'class': 'cp-sidebarlayout-description'})
@ -260,7 +271,7 @@ define([
var $div = makeBlock(key); // Msg.admin_registrationHint, .admin_registrationTitle, .admin_registrationButton
var state = APP.instanceStatus.restrictRegistration;
var $cbox = $(UI.createCheckbox('cp-settings-userfeedback',
var $cbox = $(UI.createCheckbox('cp-settings-' + key,
Messages.admin_registrationTitle,
state, { label: { class: 'noTitle' } }));
var spinner = UI.makeSpinner($cbox);
@ -289,6 +300,55 @@ define([
return $div;
};
var makeAdminCheckbox = function (data) {
return function () {
var state = data.getState();
var key = data.key;
var $div = makeBlock(key);
var labelKey = 'admin_' + keyToCamlCase(key) + 'Label';
var titleKey = 'admin_' + keyToCamlCase(key) + 'Title';
var $cbox = $(UI.createCheckbox('cp-admin-' + key,
Messages[labelKey] || Messages[titleKey],
state, { label: { class: 'noTitle' } }));
var spinner = UI.makeSpinner($cbox);
var $checkbox = $cbox.find('input').on('change', function() {
spinner.spin();
var val = $checkbox.is(':checked') || false;
$checkbox.attr('disabled', 'disabled');
data.query(val, function (state) {
spinner.done();
$checkbox[0].checked = state;
$checkbox.removeAttr('disabled');
});
});
$cbox.appendTo($div);
return $div;
};
};
// Msg.admin_registrationHint, .admin_registrationTitle, .admin_registrationButton
create['registration'] = makeAdminCheckbox({
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);
});
});
},
});
create['email'] = function () {
var key = 'email';
var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle, Msg.admin_emailButton
@ -1663,6 +1723,166 @@ define([
return $div;
};
Messages.admin_cat_network = 'Network'; // XXX
Messages.admin_checkupTitle = 'admin_checkupTitle'; // XXX
Messages.admin_checkupHint = 'admin_checkupHint'; // XXX
Messages.admin_checkupButton = 'Run diagnostics'; // XXX
create['checkup'] = function () {
var $div = makeBlock('checkup', true);
$div.find('button').click(function () {
common.openURL('/checkup/');
});
return $div;
};
Messages.admin_consentToContactTitle = 'admin_consentToContactTitle'; // XXX
Messages.admin_consentToContactHint = 'admin_consentToContactHint'; // XXX
Messages.admin_consentToContactLabel = 'admin_consentToContactLabel'; // XXX
create['consent-to-contact'] = makeAdminCheckbox({
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_listMyInstanceTitle = 'admin_listMyInstanceTitle'; // XXX
Messages.admin_listMyInstanceHint = 'admin_listMyInstanceHint'; // XXX
Messages.admin_listMyInstanceLabel = 'admin_listMyInstanceLabel'; // XXX
create['list-my-instance'] = makeAdminCheckbox({
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_provideAggregateStatisticsTitle = 'admin_provideAggregateStatisticsTitle'; // XXX
Messages.admin_provideAggregateStatisticsHint = 'admin_provideAggregateStatisticsHint'; // XXX
Messages.admin_provideAggregateStatisticsLabel = 'admin_provideAggregateStatisticsLabel'; // XXX
create['provide-aggregate-statistics'] = makeAdminCheckbox({
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_removeDonateButtonTitle = 'admin_removeDonateButtonTitle'; // XXX
Messages.admin_removeDonateButtonHint = 'admin_removeDonateButtonHint'; // XXX
Messages.admin_removeDonateButtonLabel = 'admin_removeDonateButtonLabel'; // XXX
/* // XXX
* We're very proud that CryptPad is available to the public as free software!
* We do, however, still need to pay our bills as we develop the platform.
*
* By default CryptPad will prompt users to consider donating to
* our OpenCollective campaign. We publish the state of our finances periodically
* so you can decide for yourself whether our expenses are reasonable.
*
* You can disable any solicitations for donations by setting 'removeDonateButton' to true,
* but we'd appreciate it if you didn't!
*/
create['remove-donate-button'] = makeAdminCheckbox({
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);
});
});
},
});
Messages.admin_blockDailyCheckTitle = 'admin_blockDailyCheckTitle'; // XXX
Messages.admin_blockDailyCheckHint = 'admin_blockDailyCheckHint'; // XXX
Messages.admin_blockDailyCheckLabel = 'admin_blockDailyCheckLabel'; // XXX
/* // XXX
* By default, CryptPad contacts one of our servers once a day.
* This check-in will also send some very basic information about your instance including its
* version and the adminEmail so we can reach you if we are aware of a serious problem.
* We will never sell it or send you marketing mail.
*
* If you want to block this check-in and remain set 'blockDailyCheck' to true.
*/
create['block-daily-check'] = makeAdminCheckbox({
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);
});
});
},
});
var hideCategories = function () {
APP.$rightside.find('> div').hide();
};
@ -1680,6 +1900,7 @@ define([
support: 'fa fa-life-ring',
broadcast: 'fa fa-bullhorn',
performance: 'fa fa-heartbeat',
network: 'fa fa-sitemap', // or fa-university ?
};
var createLeftside = function () {