Add new placeholder data and account script to admin panel

This commit is contained in:
yflory 2023-09-14 17:49:16 +02:00
parent 399d50e941
commit f282db9121
8 changed files with 244 additions and 77 deletions

View File

@ -122,6 +122,9 @@ define([
.on('ready', function () {
setTimeout(function () { cb(void 0, rt); });
})
.on('error', function (info) {
cb(info.type, {reason: info.message});
})
.on('disconnect', function (info) {
cb('E_DISCONNECT', info);
});
@ -211,10 +214,13 @@ define([
return void console.log("Block requires 2FA");
}
if (err === 404 && response && response.reason
&& response.reason !== 'PASSWORD_CHANGE') {
if (err === 404 && response && response.reason) {
waitFor.abort();
w.abort();
/*
// the following block prevent users from re-using an old password
if (isRegister) { return void cb('HAS_PLACEHOLDER'); }
*/
return void cb('DELETED_USER', response);
}
@ -299,6 +305,7 @@ define([
loadUserObject(opt, waitFor(function (err, rt) {
if (err) {
waitFor.abort();
if (err === 'EDELETED') { return void cb('DELETED_USER', rt); }
return void cb(err);
}
@ -396,6 +403,7 @@ define([
loadUserObject(opt, waitFor(function (err, rt) {
if (err) {
waitFor.abort();
if (err === 'EDELETED') { return void cb('DELETED_USER', rt); }
return void cb('MODERN_REGISTRATION_INIT');
}
@ -603,8 +611,16 @@ define([
});
});
break;
/*
case 'HAS_PLACEHOLDER':
UI.errorLoadingScreen('UNAVAILABLE', true, true);
break;
*/
case 'DELETED_USER':
UI.errorLoadingScreen(UI.getDestroyedPlaceholder(result.reason, true));
UI.errorLoadingScreen(
UI.getDestroyedPlaceholder(result.reason, true), true, () => {
window.location.reload();
});
break;
case 'INVAL_PASS':
UI.removeLoadingScreen(function () {

View File

@ -1,7 +1,6 @@
/* jshint esversion: 6, node: true */
const nThen = require('nthen');
const Pins = require('./pins');
const Environment = require("./env");
const Util = require("./common-util");
const Store = require('./storage/file.js');
const BlobStore = require("./storage/blob");
@ -16,23 +15,9 @@ const Fse = require("fs-extra");
const { parentPort } = require('node:worker_threads');
const config = require('./load-config');
const Env = Environment.create(config);
const COMMANDS = {};
let Log;
Env.computeMetadata = function (channel, cb) {
const ref = {};
const lineHandler = Meta.createLineHandler(ref, (err) => { console.log(err); });
return void Env.store.readChannelMetadata(channel, lineHandler, function (err) {
if (err) {
// stream errors?
return void cb(err);
}
cb(void 0, ref.meta);
});
};
const mkReportPath = function (Env, safeKey) {
return Path.join(Env.paths.archive, 'accounts', safeKey);
};
@ -56,6 +41,21 @@ const deleteReport = (Env, key, cb) => {
};
const init = (cb) => {
const Environment = require("./env");
const config = require('./load-config');
const Env = Environment.create(config);
Env.computeMetadata = function (channel, cb) {
const ref = {};
const lineHandler = Meta.createLineHandler(ref, (err) => { console.log(err); });
return void Env.store.readChannelMetadata(channel, lineHandler, function (err) {
if (err) {
// stream errors?
return void cb(err);
}
cb(void 0, ref.meta);
});
};
nThen((waitFor) => {
Logger.create(config, waitFor(function (_) {
Log = Env.Log = _;
@ -91,7 +91,9 @@ const init = (cb) => {
}
Env.blobStore = blob;
}));
}).nThen(cb);
}).nThen(() => {
cb(Env);
});
};
COMMANDS.start = (edPublic, blockId, reason) => {
@ -106,8 +108,11 @@ COMMANDS.start = (edPublic, blockId, reason) => {
let channelsToArchive = [];
let deletedChannels = [];
let deletedBlobs = [];
let Env;
nThen((waitFor) => {
init(waitFor());
init(waitFor((_Env) => {
Env = _Env;
}));
}).nThen((waitFor) => {
let lineHandler = Pins.createLineHandler(ref, (err) => { console.log(err); });
Env.pinStore.readMessagesBin(safeKey, 0, (msgObj, readMore) => {
@ -197,7 +202,8 @@ COMMANDS.start = (edPublic, blockId, reason) => {
key: safeKey,
channels: deletedChannels,
blobs: deletedBlobs,
blockId: blockId
blockId: blockId,
reason: reason
};
storeReport(Env, report, waitFor((err) => {
if (err) {
@ -216,8 +222,11 @@ COMMANDS.restore = (edPublic) => {
let pads, blobs;
let blockId;
let errors = [];
let Env;
nThen((waitFor) => {
init(waitFor());
init(waitFor((_Env) => {
Env = _Env;
}));
}).nThen((waitFor) => {
Log.info('MODERATION_ACCOUNT_RESTORE_START', edPublic, waitFor());
readReport(Env, safeKey, waitFor((err, report) => {
@ -290,14 +299,28 @@ COMMANDS.restore = (edPublic) => {
};
parentPort.on('message', (message) => {
let parsed = message; //JSON.parse(message);
let command = parsed.command;
let content = parsed.content;
let block = parsed.block;
let reason = parsed.reason;
COMMANDS[command](content, block, reason);
});
const getStatus = (Env, edPublic, cb) => {
const safeKey = Util.escapeKeyCharacters(edPublic);
readReport(Env, safeKey, (err, report) => {
if (err) { return void cb(err); }
cb(void 0, report);
});
};
parentPort.postMessage('READY');
if (parentPort) {
parentPort.on('message', (message) => {
let parsed = message; //JSON.parse(message);
let command = parsed.command;
let content = parsed.content;
let block = parsed.block;
let reason = parsed.reason;
COMMANDS[command](content, block, reason);
});
parentPort.postMessage('READY');
} else {
module.exports = {
getStatus: getStatus
};
}

View File

@ -10,6 +10,7 @@ const Core = require("./core");
const Channel = require("./channel");
const BlockStore = require("../storage/block");
const MFA = require("../storage/mfa");
const ArchiveAccount = require('../archive-account');
/* jshint ignore:start */
const { Worker } = require('node:worker_threads');
/* jshint ignore:end */
@ -288,22 +289,28 @@ var restoreArchivedDocument = function (Env, Server, cb, data) {
}
};
// CryptPad_AsyncStore.rpc.send('ADMIN', ['ARCHIVE_ACCOUNT', edPublic], console.log)
// CryptPad_AsyncStore.rpc.send('ADMIN', ['ARCHIVE_ACCOUNT', {key, block, reason}], console.log)
var archiveAccount = function (Env, Server, _cb, data) {
const cb = Util.once(_cb);
const worker = new Worker('./lib/archive-account.js');
const args = Array.isArray(data) && data[1];
if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); }
worker.on('message', message => {
if (message === 'READY') {
return worker.postMessage({
command: 'start',
content: data && data[1],
block: data && data[2],
reason: data && data[3]
content: args.key,
block: args.block, // optional, may be including in pin log
reason: args.reason
});
}
// DONE: disconnect all users from these channels
const reason = `MODERATION_ACCOUNT:${data && data[3]}`;
Env.Log.info('ARCHIVE_ACCOUNT_BY_ADMIN', {
safeKey: args.key,
reason: args.reason,
});
const reason = `MODERATION_ACCOUNT:${args.reason}`;
var deletedChannels = Util.tryParse(message);
if (Array.isArray(deletedChannels)) {
let n = nThen;
@ -326,14 +333,20 @@ var archiveAccount = function (Env, Server, _cb, data) {
var restoreAccount = function (Env, Server, _cb, data) {
const cb = Util.once(_cb);
const worker = new Worker('./lib/archive-account.js');
const args = Array.isArray(data) && data[1];
if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); }
worker.on('message', message => {
if (message === 'READY') {
return worker.postMessage({
command: 'restore',
content: data && data[1]
content: args.key
});
}
// Response
Env.Log.info('RESTORE_ACCOUNT_BY_ADMIN', {
safeKey: args.key,
reason: args.reason,
});
cb(void 0, {
state: true,
errors: Util.tryParse(message)
@ -344,7 +357,11 @@ var restoreAccount = function (Env, Server, _cb, data) {
cb(err);
});
worker.on('exit', () => { worker.unref(); });
};
var getAccountArchiveStatus = function (Env, Server, cb, data) {
const args = Array.isArray(data) && data[1];
if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); }
ArchiveAccount.getStatus(Env, args.key, cb);
};
// CryptPad_AsyncStore.rpc.send('ADMIN', ['CLEAR_CACHED_CHANNEL_INDEX', documentID], console.log)
@ -567,6 +584,10 @@ var getDocumentStatus = function (Env, Server, cb, data) {
}
response.archived = result;
}));
BlockStore.readPlaceholder(Env, id, w((result) => {
if (!result) { return; }
response.placeholder = result;
}));
MFA.read(Env, id, w(function (err, v) {
if (err === 'ENOENT') {
response.totp = 'DISABLED';
@ -598,6 +619,10 @@ var getDocumentStatus = function (Env, Server, cb, data) {
}
response.archived = result;
}));
Env.blobStore.getPlaceholder(id, w((result) => {
if (!result) { return; }
response.placeholder = result;
}));
}).nThen(function () {
cb(void 0, response);
});
@ -616,6 +641,10 @@ var getDocumentStatus = function (Env, Server, cb, data) {
}
response.archived = result;
}));
Env.store.getPlaceholder(id, w((result) => {
if (!result) { return; }
response.placeholder = result;
}));
}).nThen(function () {
cb(void 0, response);
});
@ -646,6 +675,8 @@ var getPinHistory = function (Env, Server, cb, data) {
cb("NOT_IMPLEMENTED");
};
/*
// NOTE: Deprecated, archive whole account now
var archivePinLog = function (Env, Server, cb, data) {
var args = Array.isArray(data) && data[1];
if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); }
@ -671,6 +702,7 @@ var archivePinLog = function (Env, Server, cb, data) {
cb(err);
});
};
*/
var archiveBlock = function (Env, Server, cb, data) {
var args = Array.isArray(data) && data[1];
@ -708,6 +740,8 @@ var restoreArchivedBlock = function (Env, Server, cb, data) {
});
};
/*
// NOTE: Deprecated, archive whole account now
var restoreArchivedPinLog = function (Env, Server, cb, data) {
var args = Array.isArray(data) && data[1];
if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); }
@ -732,6 +766,7 @@ var restoreArchivedPinLog = function (Env, Server, cb, data) {
cb(err);
});
};
*/
var archiveOwnedDocuments = function (Env, Server, cb, data) {
Env.Log.debug('ARCHIVE_OWNED_DOCUMENTS', data);
@ -842,9 +877,9 @@ var commands = {
GET_PIN_LIST: getPinList,
GET_PIN_HISTORY: getPinHistory,
ARCHIVE_PIN_LOG: archivePinLog,
//ARCHIVE_PIN_LOG: archivePinLog,
ARCHIVE_OWNED_DOCUMENTS: archiveOwnedDocuments,
RESTORE_ARCHIVED_PIN_LOG: restoreArchivedPinLog,
//RESTORE_ARCHIVED_PIN_LOG: restoreArchivedPinLog,
ARCHIVE_BLOCK: archiveBlock,
RESTORE_ARCHIVED_BLOCK: restoreArchivedBlock,
@ -854,6 +889,7 @@ var commands = {
ARCHIVE_ACCOUNT: archiveAccount,
RESTORE_ACCOUNT: restoreAccount,
GET_ACCOUNT_ARCHIVE_STATUS: getAccountArchiveStatus,
CLEAR_CACHED_CHANNEL_INDEX: clearChannelIndex,
GET_CACHED_CHANNEL_INDEX: getChannelIndex,

View File

@ -689,6 +689,9 @@ BlobStore.create = function (config, _cb) {
var path = prependArchive(Env, makeBlobPath(Env, blobId));
isFile(path, cb);
},
getPlaceholder: function (blobId, cb) {
readPlaceholder(Env, blobId, cb);
},
closeBlobstage: function (safeKey) {
closeBlobstage(Env, safeKey);

View File

@ -1429,6 +1429,9 @@ module.exports.create = function (conf, _cb) {
channelBytes(env, channelName, Util.both(cb, next));
});
},
getPlaceholder: function (channelName, cb) {
readPlaceholder(env, channelName, cb);
},
// OTHER DATABASE FUNCTIONALITY
// remove a particular channel from the cache
closeChannel: function (channelName, cb) {

View File

@ -53,6 +53,20 @@ define([
var common;
var sFrameChan;
// XXX
// TO DELETE
// Messages.admin_archivePinLog
// Messages.admin_restoreArchivedPins
// TO ADD
Messages.admin_archiveAccount = "Archive this account";
Messages.admin_archiveAccountInfo = "Including its owned documents";
Messages.admin_archiveAccountConfirm = "Please specify the reason for archival, this will be shown to the user.";
Messages.admin_restoreAccount = "Restore this account";
Messages.admin_accountSuspended = "Account archived by admin";
Messages.admin_accountReport = "Account archive report";
Messages.admin_accountReportFull = "Get detailed report";
Messages.admin_channelPlaceholder = "Destroyed document placeholder";
var categories = {
'general': [ // Msg.admin_cat_general
'cp-admin-flush-cache',
@ -226,6 +240,7 @@ define([
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);
@ -236,6 +251,7 @@ define([
}
}));
}).nThen(function (w) {
if (!data.first) { return; }
// storage used
sframeCommand('GET_USER_TOTAL_SIZE', key, w((err, response) => {
if (err || !Array.isArray(response)) {
@ -246,6 +262,7 @@ define([
}
}));
}).nThen(function (w) {
if (!data.first) { return; }
// channels pinned
// files pinned
sframeCommand('GET_USER_STORAGE_STATS', key, w((err, response) => {
@ -268,6 +285,17 @@ define([
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 {
@ -281,6 +309,12 @@ define([
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);
}
@ -391,11 +425,13 @@ define([
]));
}
// First pin activity time
row(Messages.admin_firstPinTime, maybeDate(data.first));
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));
// last pin activity time
row(Messages.admin_lastPinTime, maybeDate(data.latest));
}
// currently online
row(Messages.admin_currentlyOnline, data.currentlyOnline);
@ -407,27 +443,46 @@ define([
row(Messages.admin_note, data.note || Messages.ui_none);
// storage limit
row(Messages.admin_planlimit, getPrettySize(data.limit));
if (data.limit) { row(Messages.admin_planlimit, getPrettySize(data.limit)); }
// data stored
row(Messages.admin_storageUsage, getPrettySize(data.usage));
if (data.usage) { row(Messages.admin_storageUsage, getPrettySize(data.usage)); }
// number of channels
row(Messages.admin_channelCount, data.channels);
if (typeof(data.channel) === "number") {
row(Messages.admin_channelCount, data.channels);
}
// number of files pinned
row(Messages.admin_fileCount, data.files);
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 = primary(Messages.admin_accountReportFull, () => {
$(button).remove();
$(pre).html(JSON.stringify(data.archiveReport, 0, 2));
})
]));
}
// actions
if (data.archived && data.live === false) {
row(Messages.admin_restoreArchivedPins, primary(Messages.ui_restore, function () {
if (data.archived && data.live === false && data.archiveReport) {
row(Messages.admin_restoreAccount, primary(Messages.ui_restore, function () {
justifyRestorationDialog('', reason => {
sframeCommand('RESTORE_ARCHIVED_PIN_LOG', {
sframeCommand('RESTORE_ACCOUNT', {
key: data.key,
reason: reason,
}, function (err) {
@ -497,9 +552,10 @@ define([
// archive pin log
var archiveHandler = () => {
justifyArchivalDialog(Messages.admin_archivePinLogConfirm, reason => {
sframeCommand('ARCHIVE_PIN_LOG', {
justifyArchivalDialog(Messages.admin_archiveAccountConfirm, reason => {
sframeCommand('ARCHIVE_ACCOUNT', {
key: data.key,
block: data.blockId,
reason: reason,
}, (err /*, response */) => {
console.error(err);
@ -512,7 +568,12 @@ define([
});
};
row(Messages.admin_archivePinLog, danger(Messages.admin_archiveButton, archiveHandler));
var archiveAccountLabel = h('span', [
Messages.admin_archiveAccount,
h('br'),
h('small', Messages.archiveAccountInfo)
]);
row(archiveAccountLabel, danger(Messages.admin_archiveButton, archiveHandler));
// archive owned documents
/* // TODO not implemented
@ -678,6 +739,7 @@ define([
}
data.live = res[0].live;
data.archived = res[0].archived;
data.placeholder = res[0].placeholder;
//console.error("get channel status", err, res);
}));
}).nThen(function () {
@ -696,7 +758,6 @@ define([
/* FIXME
Messages.admin_getFullPinHistory = 'Pin history';
Messages.admin_archivePinLogConfirm = "All content in this user's drive will be un-listed, meaning it may be deleted if it is not in any other drive.";
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.";
*/
@ -791,6 +852,11 @@ define([
}
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 = danger(Messages.admin_unarchiveButton, function () {
@ -1075,6 +1141,7 @@ define([
data.live = res[0].live;
data.archived = res[0].archived;
data.totp = res[0].totp;
data.placeholder = res[0].placeholder;
}));
}).nThen(function () {
try {
@ -1120,6 +1187,10 @@ define([
});
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 = danger(Messages.ui_restore, function () {
justifyRestorationDialog('', reason => {

View File

@ -1969,6 +1969,42 @@ define([
return void cb({ error: 'INVALID_CODE' });
}
}));
}).nThen(function (waitFor) {
var blockUrl = Block.getBlockUrl(blockKeys);
// Check whether there is a block at that new location
Util.getBlock(blockUrl, {}, waitFor(function (err, response) {
// If there is no block or the block is invalid, continue.
// error 401 means protected block
/*
// the following block prevent users from re-using an old password
if (err === 404 && response && response.reason) {
waitFor.abort();
return void cb({
error: 'EDELELED',
reason: response.reason
});
}
*/
if (err && err !== 401) {
console.log("no block found");
return;
}
response.arrayBuffer().then(waitFor(arraybuffer => {
var block = new Uint8Array(arraybuffer);
var decryptedBlock = Block.decrypt(block, blockKeys);
if (!decryptedBlock) {
console.error("Found a login block but failed to decrypt");
return;
}
// If there is already a valid block, abort! We risk overriding another user's data
waitFor.abort();
cb({ error: 'EEXISTS' });
}));
}));
}).nThen(function (waitFor) {
// Create a new user hash
// Get the current content, store it in the new user file
@ -1997,27 +2033,6 @@ define([
}
}), optsPut);
}));
}).nThen(function (waitFor) {
var blockUrl = Block.getBlockUrl(blockKeys);
// Check whether there is a block at that new location
Util.fetch(blockUrl, waitFor(function (err, block) {
// If there is no block or the block is invalid, continue.
// error 401 means protected block
if (err && err !== 401) {
console.log("no block found");
return;
}
var decryptedBlock = Block.decrypt(block, blockKeys);
if (!decryptedBlock) {
console.error("Found a login block but failed to decrypt");
return;
}
// If there is already a valid block, abort! We risk overriding another user's data
waitFor.abort();
cb({ error: 'EEXISTS' });
}));
}).nThen(function (waitFor) {
// Write the new login block
var content = {

View File

@ -247,7 +247,7 @@ define([
list.push(userChannel);
if (store.data && store.data.blockId) {
list.push(`${store.data.blockId}#block`); // XXX 5.5.0?
//list.push(`${store.data.blockId}#block`); // XXX 5.5.0?
}
list.sort();