Add admin panel
parent
5a629e8681
commit
bb5f03bd0f
|
@ -49,6 +49,20 @@ var baseCSP = [
|
|||
|
||||
|
||||
module.exports = {
|
||||
/* =====================
|
||||
* Admin
|
||||
* ===================== */
|
||||
|
||||
/*
|
||||
* CryptPad now contains an administration panel. Its access is restricted to specific
|
||||
* users using the following list.
|
||||
* To give access to the admin panel to a user account, just add their user id,
|
||||
* which can be found on the settings page for registered users.
|
||||
* Entries should be strings separated by a comma.
|
||||
*/
|
||||
adminKeys: [
|
||||
//"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=",
|
||||
],
|
||||
|
||||
/* =====================
|
||||
* Infra setup
|
||||
|
|
|
@ -131,6 +131,10 @@
|
|||
@colortheme_kanban-color: #000;
|
||||
@colortheme_kanban-warn: #e6385d;
|
||||
|
||||
@colortheme_admin-bg: #7c0404;
|
||||
@colortheme_admin-color: #FFF;
|
||||
@colortheme_admin-warn: #ffae00;
|
||||
|
||||
// Sidebar layout (profile / settings)
|
||||
@colortheme_sidebar-active: #fff;
|
||||
@colortheme_sidebar-left-bg: #eee;
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
.cp-icon-color-ooslide { color: @colortheme_ooslide-bg; }
|
||||
.cp-icon-color-sheet { color: @colortheme_oocell-bg; }
|
||||
.cp-icon-color-kanban { color: @colortheme_kanban-bg; }
|
||||
.cp-icon-color-admin { color: @colortheme_admin-bg; }
|
||||
|
||||
.cp-border-color-pad { border-color: @colortheme_pad-bg !important; }
|
||||
.cp-border-color-code { border-color: @colortheme_code-bg !important; }
|
||||
|
@ -37,5 +38,6 @@
|
|||
.cp-border-color-ooslide { border-color: @colortheme_ooslide-bg !important; }
|
||||
.cp-border-color-sheet { border-color: @colortheme_oocell-bg !important; }
|
||||
.cp-border-color-kanban { border-color: @colortheme_kanban-bg !important; }
|
||||
.cp-border-color-admin { border-color: @colortheme_admin-bg !important; }
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
"sortify": "^1.0.4",
|
||||
"stream-to-pull-stream": "^1.7.2",
|
||||
"tweetnacl": "~0.12.2",
|
||||
"ws": "^1.0.1"
|
||||
"ws": "^1.0.1",
|
||||
"get-folder-size": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flow-bin": "^0.59.0",
|
||||
|
|
106
rpc.js
106
rpc.js
|
@ -15,6 +15,7 @@ const Package = require('./package.json');
|
|||
const Pinned = require('./pinned');
|
||||
const Saferphore = require("saferphore");
|
||||
const nThen = require("nthen");
|
||||
const getFolderSize = require("get-folder-size");
|
||||
|
||||
var RPC = module.exports;
|
||||
|
||||
|
@ -1484,6 +1485,101 @@ var isNewChannel = function (Env, channel, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
var getDiskUsage = function (Env, cb) {
|
||||
var data = {};
|
||||
nThen(function (waitFor) {
|
||||
getFolderSize('./', waitFor(function(err, info) {
|
||||
data.total = info;
|
||||
}));
|
||||
getFolderSize(Env.paths.pin, waitFor(function(err, info) {
|
||||
data.pin = info;
|
||||
}));
|
||||
getFolderSize(Env.paths.blob, waitFor(function(err, info) {
|
||||
data.blob = info;
|
||||
}));
|
||||
getFolderSize(Env.paths.staging, waitFor(function(err, info) {
|
||||
data.blobstage = info;
|
||||
}));
|
||||
getFolderSize(Env.paths.block, waitFor(function(err, info) {
|
||||
data.block = info;
|
||||
}));
|
||||
getFolderSize(Env.paths.data, waitFor(function(err, info) {
|
||||
data.datastore = info;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
cb (void 0, data);
|
||||
});
|
||||
};
|
||||
var getRegisteredUsers = function (Env, cb) {
|
||||
var dir = Env.paths.pin;
|
||||
var folders;
|
||||
var users = 0;
|
||||
nThen(function (waitFor) {
|
||||
Fs.readdir(dir, waitFor(function (err, list) {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb(err);
|
||||
}
|
||||
folders = list;
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
folders.forEach(function (f) {
|
||||
var dir = Env.paths.pin + '/' + f;
|
||||
Fs.readdir(dir, waitFor(function (err, list) {
|
||||
if (err) { return; }
|
||||
users += list.length;
|
||||
}));
|
||||
});
|
||||
}).nThen(function () {
|
||||
cb(void 0, users);
|
||||
});
|
||||
};
|
||||
var getActiveSessions = function (Env, ctx, cb) {
|
||||
var total = ctx.users ? Object.keys(ctx.users).length : '?';
|
||||
|
||||
var ips = [];
|
||||
Object.keys(ctx.users).forEach(function (u) {
|
||||
var user = ctx.users[u];
|
||||
var socket = user.socket;
|
||||
var conn = socket.upgradeReq.connection;
|
||||
if (ips.indexOf(conn.remoteAddress) === -1) {
|
||||
ips.push(conn.remoteAddress);
|
||||
}
|
||||
});
|
||||
|
||||
cb (void 0, [total, ips.length]);
|
||||
};
|
||||
|
||||
var adminCommand = function (Env, ctx, publicKey, config, data, cb) {
|
||||
var admins = [];
|
||||
try {
|
||||
admins = (config.adminKeys || []).map(function (k) {
|
||||
k = k.replace(/\/+$/, '');
|
||||
var s = k.split('/');
|
||||
return s[s.length-1];
|
||||
});
|
||||
} catch (e) { console.error("Can't parse admin keys. Please update or fix your config.js file!"); }
|
||||
if (admins.indexOf(publicKey) === -1) {
|
||||
return void cb("FORBIDDEN");
|
||||
}
|
||||
// Handle commands here
|
||||
switch (data[0]) {
|
||||
case 'ACTIVE_SESSIONS':
|
||||
return getActiveSessions(Env, ctx, cb);
|
||||
case 'ACTIVE_PADS':
|
||||
return cb(void 0, ctx.channels ? Object.keys(ctx.channels).length : '?');
|
||||
case 'REGISTERED_USERS':
|
||||
return getRegisteredUsers(Env, cb);
|
||||
case 'DISK_USAGE':
|
||||
return getDiskUsage(Env, cb);
|
||||
case 'FLUSH_CACHE':
|
||||
config.flushCache();
|
||||
return cb(void 0, true);
|
||||
default:
|
||||
return cb('UNHANDLED_ADMIN_COMMAND');
|
||||
}
|
||||
};
|
||||
|
||||
var isUnauthenticatedCall = function (call) {
|
||||
return [
|
||||
'GET_FILE_SIZE',
|
||||
|
@ -1516,6 +1612,7 @@ var isAuthenticatedCall = function (call) {
|
|||
'REMOVE_PINS',
|
||||
'WRITE_LOGIN_BLOCK',
|
||||
'REMOVE_LOGIN_BLOCK',
|
||||
'ADMIN',
|
||||
].indexOf(call) !== -1;
|
||||
};
|
||||
|
||||
|
@ -1587,6 +1684,7 @@ RPC.create = function (
|
|||
var blobPath = paths.blob = keyOrDefaultString('blobPath', './blob');
|
||||
var blobStagingPath = paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
|
||||
paths.block = keyOrDefaultString('blockPath', './block');
|
||||
paths.data = keyOrDefaultString('filePath', './datastore');
|
||||
|
||||
var isUnauthenticateMessage = function (msg) {
|
||||
return msg && msg.length === 2 && isUnauthenticatedCall(msg[0]);
|
||||
|
@ -1872,6 +1970,14 @@ RPC.create = function (
|
|||
}
|
||||
Respond(e);
|
||||
});
|
||||
case 'ADMIN':
|
||||
return void adminCommand(Env, ctx, safeKey, config, msg[1], function (e, result) {
|
||||
if (e) {
|
||||
WARN(e, result);
|
||||
return void Respond(e);
|
||||
}
|
||||
Respond(void 0, result);
|
||||
});
|
||||
default:
|
||||
return void Respond('UNSUPPORTED_RPC_CALL', msg);
|
||||
}
|
||||
|
|
13
server.js
13
server.js
|
@ -54,6 +54,10 @@ if (FRESH_MODE) {
|
|||
console.log("FRESH MODE ENABLED");
|
||||
FRESH_KEY = +new Date();
|
||||
}
|
||||
config.flushCache = function () {
|
||||
FRESH_KEY = +new Date();
|
||||
};
|
||||
|
||||
|
||||
const clone = (x) => (JSON.parse(JSON.stringify(x)));
|
||||
|
||||
|
@ -167,6 +171,14 @@ if (config.privKeyAndCertFiles) {
|
|||
|
||||
app.get('/api/config', function(req, res){
|
||||
var host = req.headers.host.replace(/\:[0-9]+/, '');
|
||||
var admins = [];
|
||||
try {
|
||||
admins = (config.adminKeys || []).map(function (k) {
|
||||
k = k.replace(/\/+$/, '');
|
||||
var s = k.split('/');
|
||||
return s[s.length-1];
|
||||
});
|
||||
} catch (e) { console.error("Can't parse admin keys"); }
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.send('define(function(){\n' + [
|
||||
'var obj = ' + JSON.stringify({
|
||||
|
@ -180,6 +192,7 @@ app.get('/api/config', function(req, res){
|
|||
websocketURL:'ws' + ((useSecureWebsockets) ? 's' : '') + '://' + host + ':' +
|
||||
websocketPort + '/cryptpad_websocket',
|
||||
httpUnsafeOrigin: config.httpUnsafeOrigin,
|
||||
adminKeys: admins,
|
||||
}, null, '\t'),
|
||||
'obj.httpSafeOrigin = ' + (function () {
|
||||
if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; }
|
||||
|
|
|
@ -17,6 +17,6 @@ define(function () {
|
|||
// Sub
|
||||
plan: 'CryptPad_plan',
|
||||
// Apps
|
||||
criticalApps: ['profile', 'settings', 'debug']
|
||||
criticalApps: ['profile', 'settings', 'debug', 'admin']
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1634,6 +1634,14 @@ define([
|
|||
content: h('span', Messages.settingsButton)
|
||||
});
|
||||
}
|
||||
// Add administration panel link if the user is an admin
|
||||
if (priv.edPublic && Array.isArray(Config.adminKeys) && Config.adminKeys.indexOf(priv.edPublic) !== -1) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-toolbar-menu-admin fa fa-cogs'},
|
||||
content: h('span', Messages.adminPage || 'Admin')
|
||||
});
|
||||
}
|
||||
// Add login or logout button depending on the current status
|
||||
if (accountName) {
|
||||
options.push({
|
||||
|
@ -1729,6 +1737,13 @@ define([
|
|||
window.parent.location = origin+'/settings/';
|
||||
}
|
||||
});
|
||||
$userAdmin.find('a.cp-toolbar-menu-admin').click(function () {
|
||||
if (padType) {
|
||||
window.open(origin+'/admin/');
|
||||
} else {
|
||||
window.parent.location = origin+'/admin/';
|
||||
}
|
||||
});
|
||||
$userAdmin.find('a.cp-toolbar-menu-profile').click(function () {
|
||||
if (padType) {
|
||||
window.open(origin+'/profile/');
|
||||
|
|
|
@ -615,6 +615,11 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
// Admin
|
||||
common.adminRpc = function (data, cb) {
|
||||
postMessage("ADMIN_RPC", data, cb);
|
||||
};
|
||||
|
||||
// Network
|
||||
common.onNetworkDisconnect = Util.mkEvent();
|
||||
common.onNetworkReconnect = Util.mkEvent();
|
||||
|
|
|
@ -947,6 +947,14 @@ define([
|
|||
}
|
||||
};
|
||||
|
||||
// Admin
|
||||
Store.adminRpc = function (clientId, data, cb) {
|
||||
store.rpc.adminRpc(data, function (err, res) {
|
||||
if (err) { return void cb({error: err}); }
|
||||
cb(res);
|
||||
});
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
/////////////////////// PAD //////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -77,6 +77,8 @@ define([
|
|||
DRIVE_USEROBJECT: Store.userObjectCommand,
|
||||
// Settings,
|
||||
DELETE_ACCOUNT: Store.deleteAccount,
|
||||
// Admin
|
||||
ADMIN_RPC: Store.adminRpc,
|
||||
};
|
||||
|
||||
Rpc.query = function (cmd, data, cb) {
|
||||
|
|
|
@ -58,6 +58,18 @@ define([
|
|||
rpc.send('UNPIN', channels, cb);
|
||||
};
|
||||
|
||||
// Get data for the admin panel
|
||||
exp.adminRpc = function (obj, cb) {
|
||||
if (!obj.cmd) {
|
||||
setTimeout(function () {
|
||||
cb('[TypeError] admin rpc expects a command');
|
||||
});
|
||||
return;
|
||||
}
|
||||
var params = [obj.cmd, obj.data];
|
||||
rpc.send('ADMIN', params, cb);
|
||||
};
|
||||
|
||||
// ask the server what it thinks your hash is
|
||||
exp.getServerHash = function (cb) {
|
||||
rpc.send('GET_HASH', edPublic, function (e, hash) {
|
||||
|
|
|
@ -409,6 +409,10 @@ define([
|
|||
setDocumentTitle();
|
||||
});
|
||||
|
||||
sframeChan.on('EV_SET_HASH', function (hash) {
|
||||
window.location.hash = hash;
|
||||
});
|
||||
|
||||
Cryptpad.autoStore.onStoreRequest.reg(function (data) {
|
||||
sframeChan.event("EV_AUTOSTORE_DISPLAY_POPUP", data);
|
||||
});
|
||||
|
|
|
@ -228,6 +228,10 @@ define([
|
|||
ctx.sframeChan.event('EV_SET_TAB_TITLE', newTitle);
|
||||
};
|
||||
|
||||
funcs.setHash = function (hash) {
|
||||
ctx.sframeChan.event('EV_SET_HASH', hash);
|
||||
};
|
||||
|
||||
funcs.setLoginRedirect = function (cb) {
|
||||
cb = cb || $.noop;
|
||||
ctx.sframeChan.query('Q_SET_LOGIN_REDIRECT', null, cb);
|
||||
|
|
|
@ -1511,6 +1511,7 @@ define([
|
|||
return;
|
||||
}
|
||||
active = key;
|
||||
common.setHash(key);
|
||||
$categories.find('.cp-leftside-active').removeClass('cp-leftside-active');
|
||||
$category.addClass('cp-leftside-active');
|
||||
showCategories(categories[key]);
|
||||
|
|
Loading…
Reference in New Issue