From 757f30d4a1637bdcd7c8a390726ef13bdfdf3024 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 22 Feb 2024 18:46:26 +0100 Subject: [PATCH] New support: add moderator role --- lib/commands/admin-rpc.js | 29 +++ lib/commands/moderators.js | 42 +++++ lib/env.js | 8 + lib/http-worker.js | 1 + lib/storage/basic.js | 4 + lib/storage/moderator.js | 87 +++++++++ www/admin/inner.js | 261 ++++++++++++++++++++++----- www/common/notifications.js | 14 ++ www/common/outer/async-store.js | 2 +- www/common/outer/mailbox-handlers.js | 21 ++- www/common/outer/support.js | 52 +++++- www/moderation/inner.js | 3 +- www/support/ui.js | 6 +- 13 files changed, 472 insertions(+), 58 deletions(-) create mode 100644 lib/commands/moderators.js create mode 100644 lib/storage/moderator.js diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index cd71475db..f78f720a7 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -14,6 +14,7 @@ const Core = require("./core"); const Channel = require("./channel"); const Invitation = require("./invitation"); const Users = require("./users"); +const Moderators = require("./moderators"); const BlockStore = require("../storage/block"); const MFA = require("../storage/mfa"); const ArchiveAccount = require('../archive-account'); @@ -918,6 +919,30 @@ var deleteInvitation = (Env, Server, cb, data) => { Invitation.delete(Env, id, cb); }; +var getModerators = (Env, Server, cb) => { + Moderators.getAll(Env, cb); +}; +var addModerator = (Env, Server, cb, data, unsafeKey) => { + const obj = Array.isArray(data) && data[1]; + const name = obj.name; + const edPublic = obj.edPublic; + const curvePublic = obj.curvePublic; + const mailbox = obj.mailbox; + const profile = obj.profile; + const userData = { + name, + edPublic, + curvePublic, + mailbox, + profile + }; + Moderators.add(Env, edPublic, userData, unsafeKey, cb); +}; +var removeModerator = (Env, Server, cb, data) => { + const id = Array.isArray(data) && data[1]; + Moderators.delete(Env, id, cb); +}; + var commands = { ACTIVE_SESSIONS: getActiveSessions, ACTIVE_PADS: getActiveChannelCount, @@ -985,6 +1010,10 @@ var commands = { ADD_KNOWN_USER: addKnownUser, DELETE_KNOWN_USER: deleteKnownUser, UPDATE_KNOWN_USER: updateKnownUser, + + GET_MODERATORS: getModerators, + ADD_MODERATOR: addModerator, + REMOVE_MODERATOR: removeModerator, }; // addFirstAdmin is an anon_rpc command diff --git a/lib/commands/moderators.js b/lib/commands/moderators.js new file mode 100644 index 000000000..3a90ad2ee --- /dev/null +++ b/lib/commands/moderators.js @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +/*jshint esversion: 6 */ +const Moderators = module.exports; + +const Moderator = require('../storage/moderator'); +const Util = require("../common-util"); + +Moderators.getAll = (Env, cb) => { + Moderator.getAll(Env, (err, data) => { + if (err) { return void cb(err); } + cb(null, data); + }); +}; +Moderators.getKeysSync = (Env) => { + return Moderator.getAllKeys(Env); +}; + +Moderators.add = (Env, edPublic, data, adminKey, _cb) => { + const cb = Util.once(Util.mkAsync(_cb)); + data.createdBy = adminKey; + data.time = +new Date(); + const safeKey = Util.escapeKeyCharacters(edPublic); + Moderator.write(Env, safeKey, data, (err) => { + if (err) { return void cb(err); } + Env.moderators.push(edPublic); + Env.envUpdated.fire(); + cb(); + }); +}; + +Moderators.delete = (Env, id, _cb) => { + const cb = Util.once(Util.mkAsync(_cb)); + Moderator.delete(Env, id, (err) => { + if (err && err !== 'ENOENT') { return void cb(err); } + // XXX update Env.moderators + cb(void 0, true); + }); +}; + diff --git a/lib/env.js b/lib/env.js index 49a53963d..5b84b36f6 100644 --- a/lib/env.js +++ b/lib/env.js @@ -371,6 +371,14 @@ module.exports.create = function (config) { console.error("Can't parse admin keys. Please update or fix your config.js file!"); } + try { + let moderators = require('./commands/moderators').getKeysSync(Env); + Env.moderators = moderators || []; + } catch (e) { + console.log(e); + console.error("Can't parse support keys."); + } + Env.envUpdated = Util.mkEvent(); Env.cacheFlushed = Util.mkEvent(); diff --git a/lib/http-worker.js b/lib/http-worker.js index 9be982193..27587ca90 100644 --- a/lib/http-worker.js +++ b/lib/http-worker.js @@ -569,6 +569,7 @@ var serveConfig = makeRouteCache(function () { httpUnsafeOrigin: Env.httpUnsafeOrigin, adminEmail: Env.adminEmail, adminKeys: Env.admins, + moderatorKeys: Env.moderators, inactiveTime: Env.inactiveTime, supportMailbox: Env.supportMailbox, supportMailboxKey: Env.supportMailboxKey, diff --git a/lib/storage/basic.js b/lib/storage/basic.js index 3de79f7ec..bcc529cbb 100644 --- a/lib/storage/basic.js +++ b/lib/storage/basic.js @@ -45,6 +45,10 @@ Basic.readDir = function (Env, path, cb) { if (!path) { return void pathError(cb); } Fs.readdir(path, cb); }; +Basic.readDirSync = function (Env, path, cb) { + if (!path) { return []; } + return Fs.readdirSync(path); +}; Basic.write = function (Env, path, data, cb) { if (!path) { return void pathError(cb); } diff --git a/lib/storage/moderator.js b/lib/storage/moderator.js new file mode 100644 index 000000000..0251e7fa9 --- /dev/null +++ b/lib/storage/moderator.js @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +const Basic = require("./basic.js"); +const Path = require("node:path"); +const nThen = require('nthen'); +const Util = require('../common-util'); + +const Moderator = module.exports; +/* This module manages storage used to implement user management. "Known users" can + be added here in order to store their public key, their block ID and an alias + used to recognize them. +*/ + +const pathFromId = function (Env, id) { + if (!id || typeof(id) !== 'string') { return void console.error('KNWONUSER_BAD_ID', id); } + return Path.join(Env.paths.base, "support", id.slice(0, 2), id); +}; + +Moderator.read = function (Env, id, cb) { + var path = pathFromId(Env, id); + Basic.read(Env, path, (err, data) => { + if (err) { return void cb(err.code); } + cb(void 0, Util.tryParse(data)); + }); +}; + +Moderator.getAllKeys = function (Env) { + let keys = []; + let dirPath = Path.join(Env.paths.base, "support"); + try { + let prefixes = Basic.readDirSync(Env, dirPath); + prefixes.forEach((prefix) => { + let dirPath2 = Path.join(Env.paths.base, "support", prefix); + try { + let newKeys = Basic.readDirSync(Env, dirPath2); + keys.push(...newKeys); + } catch (e) {} + }); + return keys; + } catch (e) { + return; + } +}; +Moderator.getAll = function (Env, cb) { + let users = {}; + nThen((waitFor) => { + let dirPath = Path.join(Env.paths.base, "support"); + Basic.readDir(Env, dirPath, waitFor((err, prefixes) => { + if (err && err.code === 'ENOENT') { return void cb(void 0, {}); } + if (err) { waitFor.abort(); return void cb(err.code); } + prefixes.forEach((prefix) => { + var dirPath2 = Path.join(Env.paths.base, "support", prefix); + Basic.readDir(Env, dirPath2, waitFor((err, files) => { + if (err) { waitFor.abort(); return void cb(err.code); } + files.forEach((id) => { + Moderator.read(Env, id, waitFor((err, data) => { + users[id] = data || { error: err }; + })); + }); + })); + }); + })); + }).nThen(() => { + cb(null, users); + }); +}; + +Moderator.write = function (Env, id, data, cb) { + var path = pathFromId(Env, id); + Basic.write(Env, path, JSON.stringify(data), (err) => { + if (err) { return void cb(err.code); } + cb(); + }); +}; + +Moderator.delete = function (Env, id, cb) { + var path = pathFromId(Env, id); + Basic.delete(Env, path, (err) => { + if (err) { return void cb(err.code); } + cb(); + }); +}; + + + diff --git a/www/admin/inner.js b/www/admin/inner.js index b17d0405e..5b5bd5623 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -2923,22 +2923,28 @@ Example Messages.admin_supportNewDisabled = "Modern support system is disabled."; Messages.admin_supportNewInit = "Initialize support page on this instance"; Messages.admin_supportNewDelete = "Disable support"; - Messages.admin_supportNewConfirm = "Are you sure? This will remove access to all current moderators."; + Messages.admin_supportNewConfirm = "Are you sure? This will remove access to all current moderators and delete all existing tickets."; + Messages.admin_supportMembers = "Current support team"; + Messages.admin_supportAdd = "Add a contact to the support team"; create['support-new'] = function () { - var $div = makeBlock('support-new'); // Msg.admin_supportNewHint, .admin_supportNewTitle - var newSupportKey = ApiConfig.supportMailboxKey; - (function () { - var state = h('div'); - var $state = $(state).appendTo($div); - var button = h('button.btn.btn-primary', Messages.admin_supportNewInit); - var $button = $(button).appendTo($div); - var delButton = h('button.btn.btn-danger', Messages.admin_supportNewDelete); - var $delButton = $(delButton).appendTo($div).hide(); - var spinner = UI.makeSpinner($div); + const $div = makeBlock('support-new'); // Msg.admin_supportNewHint, .admin_supportNewTitle + let supportKey = ApiConfig.supportMailboxKey; + let edPublic = common.getMetadataMgr().getPrivateData().edPublic; // My edPublic + let refresh = function () {}; + const redraw = function (membersData, oldPrivKey) { + $div.empty(); - var setState = function () { + const state = h('div'); + const $state = $(state).appendTo($div); + const button = h('button.btn.btn-primary', Messages.admin_supportNewInit); + const $button = $(button).appendTo($div); + const delButton = h('button.btn.btn-danger', Messages.admin_supportNewDelete); + const $delButton = $(delButton).appendTo($div).hide(); + const spinner = UI.makeSpinner($div); + + const setState = function () { $state.html(''); - if (newSupportKey) { + if (supportKey) { $button.hide(); $delButton.show(); return $state.append([ @@ -2954,6 +2960,7 @@ Example }; setState(); + // XXX TODO add/remove access // XXX when removing access, send new private keys to everybody who should keep access AND move chainpad doc to the new one // ==> we'll need to delete the old chainpad doc using admin commands @@ -2967,7 +2974,7 @@ Example $delButton.attr('disabled', 'disabled'); sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', - data: ['SET_SUPPORT_MAILBOX2', ['']] + data: ['SET_SUPPORT_MAILBOX2', ['', '']] }, function (e, response) { $delButton.removeAttr('disabled'); if (e || response.error) { @@ -2978,61 +2985,218 @@ Example } spinner.done(); UI.log(Messages.saved); - newSupportKey = undefined; + supportKey = undefined; setState(); }); }); }); - var next = function () { + + /* + const getEncryptor = (curvePrivate) => { + const seed = curvePrivate.slice(0,24); + const hash = Hash.getEditHashFromKeys({ + version: 2, + type: 'support', + keys: { + editKeyStr: seed + } + }); + const secret = Hash.getSecrets('support', hash); + return Crypto.createEncryptor(secret.keys); + }; + console.log(oldPrivKey, getEncryptor(oldPrivKey)); + */ + const getMemberData = (curve) => { + let friends = common.getFriends(true); + let f = friends[curve || 'me']; + return { + name: f.displayName, + edPublic: f.edPublic, + curvePublic: f.curvePublic, + mailbox: f.notifications, + profile: f.profile + // XXX add avatar? + }; + }; + const generateKey = function () { + if (supportKey && !membersData[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; + } spinner.spin(); $button.attr('disabled', 'disabled'); - var keyPair = Nacl.box.keyPair(); - var pub = Nacl.util.encodeBase64(keyPair.publicKey); - var priv = Nacl.util.encodeBase64(keyPair.secretKey); - var ed = Nacl.sign.keyPair.fromSeed(keypair.secretKey); - var edPub = Nacl.util.encodeBase64(ed.publicKey); - // Store the private key first. It won't be used until the decree is accepted. - sFrameChan.query("Q_ADMIN_MAILBOX", { - version: 2, - priv: priv - }, function (err, obj) { - if (err || (obj && obj.error)) { - console.error(err || obj.error); - UI.warn(Messages.error); - spinner.hide(); - return; - } - // Then send the decree + const keyPair = Nacl.box.keyPair(); + const pub = Nacl.util.encodeBase64(keyPair.publicKey); + const priv = Nacl.util.encodeBase64(keyPair.secretKey); + const ed = Nacl.sign.keyPair.fromSeed(keypair.secretKey); + const edPub = Nacl.util.encodeBase64(ed.publicKey); + const onError = (waitFor, err, res) => { + if (waitFor) { waitFor.abort(); } + console.error(err, res); + spinner.hide(); + UI.warn(Messages.error); + }; + + //const oldCrypto = supportKey ? getEncryptor(oldPrivKey) : undefined; + //const newCrypto = getEncryptor(priv); + + nThen((waitFor) => { + // Send new key to server sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_SUPPORT_MAILBOX2', [pub, edPub]] - }, function (e, response) { + }, waitFor((e, response) => { $button.removeAttr('disabled'); - if (e || response.error) { - UI.warn(Messages.error); - console.error(e, response); - spinner.hide(); - return; - } + if (e || response.error) { return void onError(waitFor, e, response); } + })); + }).nThen((waitFor) => { + // Add to own mailbox + sFrameChan.query("Q_ADMIN_MAILBOX", { + version: 2, + priv: priv + }, waitFor((err, obj) => { + if (err || (obj && obj.error)) { return void onError(waitFor, err, obj); } + spinner.done(); UI.log(Messages.saved); - newSupportKey = pub; + supportKey = pub; setState(); - //$('.cp-admin-support-init').hide(); - //APP.$rightside.append(create['support-list']()); - //APP.$rightside.append(create['support-priv']()); + })); + }).nThen(() => { + // Add myself to moderator role if not already there + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADD_MODERATOR', + data: getMemberData() + }, (e, response) => { + console.error(e, response); + }); + }).nThen(() => { + // XXX migrate chainpad doc + // XXX delete old pin log + // XXX send new keys to members + console.error('XXX TODO send to members'); + }); + }; + + const addSupportMember = (curve, _cb) => { + let cb = Util.mkAsync(_cb); + let userData = getMemberData(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, (obj) => { + // XXX IF ERROR, (can't notify) remove from DB? + cb(); }); }); }; + Util.onClickEnter($button, function () { - if (newSupportKey) { + /* + if (supportKey) { return void UI.confirm(Messages.admin_supportNewConfirm, function (yes) { - if (yes) { next(); } + if (yes) { generateKey(); } }); } - next(); + */ + generateKey(); }); - })(); + + const drawMembers = () => { + if (!supportKey) { return; } + const members = {}; + const friends = Util.clone(common.getFriends(false)) + Object.keys(membersData).forEach((ed) => { + let m = membersData[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) => { + console.error('REMOVE', el); + } + }); + let contactsGrid = UIElements.getUserGrid(Messages.admin_supportAdd, { + common: common, + list: true, + large: true, + data: friends + }, function () {}); + + let addBtn = h('button.btn.btn-primary', '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); + } + addSupportMember(curve, waitFor()); + }); + }).nThen(() => { + refresh(); + }); + }); + // Only moderators can add new moderators + if (!membersData[edPublic]) { + contactsGrid.div = undefined; + addBtn = undefined; + } + + const list = h('div', [ + currentList.div, + contactsGrid.div, + h('nev', addBtn) + ]); + $div.append(list); + }; + drawMembers(); + }; + refresh = () => { + let oldKey, members; + nThen((waitFor) => { + APP.supportModule.execCommand('GET_PRIVATE_KEY', {}, waitFor((obj) => { + oldKey = obj && obj.curvePrivate; + })); + }).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; + } + members = response[0]; + })); + }).nThen(() => { + redraw(members, oldKey); + }); + }; + refresh(); return $div; }; create['support-init'] = function () { @@ -4096,6 +4260,7 @@ Example APP.origin = privateData.origin; APP.readOnly = privateData.readOnly; APP.support = Support.create(common, true); + APP.supportModule = common.makeUniversal('support'); // Content diff --git a/www/common/notifications.js b/www/common/notifications.js index f00a33970..10b760bdc 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -486,6 +486,7 @@ define([ // XXX Messages.support_userNotification = "New support ticket or response: {0}"; + Messages.support_moderatorNotification = "You have been added to the moderators list"; handlers['NOTIF_TICKET'] = function (common, data) { var content = data.content; var msg = content.msg.content; @@ -504,6 +505,19 @@ define([ content.dismissHandler = defaultDismiss(common, data); } }; + handlers['ADD_MODERATOR'] = function (common, data) { + var content = data.content; + content.getFormatText = function () { + return Messages.support_moderatorNotification; + }; + content.handler = function () { + common.openURL('/moderation/'); + defaultDismiss(common, data)(); + }; + if (!content.archived) { + content.dismissHandler = defaultDismiss(common, data); + } + }; handlers['BROADCAST_CUSTOM'] = function (common, data) { var content = data.content; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index d62a33d8e..5eda66eac 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1627,7 +1627,7 @@ define([ var box = mailboxes[key] = { channel: channel, viewed: [], - lastKnownHash: '', + lastKnownHash: data.lastKnownHash || '', keys: { curvePublic: pub, curvePrivate: priv diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 7acd4cfe6..05aa23ebb 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -3,11 +3,12 @@ // SPDX-License-Identifier: AGPL-3.0-or-later define([ + '/api/config', '/common/common-messaging.js', '/common/common-hash.js', '/common/common-util.js', '/components/chainpad-crypto/crypto.js', -], function (Messaging, Hash, Util, Crypto) { +], function (ApiConfig, Messaging, Hash, Util, Crypto) { // Random timeout between 10 and 30 times your sync time (lag + chainpad sync) var getRandomTimeout = function (ctx) { @@ -908,6 +909,24 @@ define([ if (adminSupportNotif && adminSupportNotif.channel === id) { adminSupportNotif = undefined; } }; + handlers['ADD_MODERATOR'] = function (ctx, box, data, cb) { + var msg = data.msg; + var content = msg.content; + var newKey = content.supportKey; + // check if it matches the server key + var pub = Hash.getBoxPublicFromSecret(newKey); + if (pub !== ApiConfig.supportMailboxKey) { return void cb(true); } + // We have a correct key: add support mailbox + ctx.Store.addAdminMailbox(null, { + version: 2, + priv: newKey, + lastKnownHash: content.lastKnownHash + }, function (err) { + if (err) { return void cb(true); } + cb(false); + }); + }; + return { add: function (ctx, box, data, cb) { /** diff --git a/www/common/outer/support.js b/www/common/outer/support.js index 31d41c8b9..e3e2cac1b 100644 --- a/www/common/outer/support.js +++ b/www/common/outer/support.js @@ -22,7 +22,7 @@ define([ var cb = Util.mkAsync(_cb); if (isAdmin && !ctx.adminRdyEvt) { return void cb('EFORBIDDEN'); } require(['/api/config?' + (+new Date())], function (NewConfig) { - ctx.adminKeys = NewConfig.adminKeys; // Update admin keys // XXX MODERATOR + ctx.moderatorKeys = NewConfig.moderatorKeys; // Update admin keys // XXX MODERATOR var supportKey = NewConfig.supportMailboxKey; if (!supportKey) { return void cb('E_NOT_INIT'); } @@ -324,7 +324,7 @@ define([ entry.premium = premium; if (senderKey) { - entry.lastAdmin = ctx.adminKeys.indexOf(senderKey) !== -1 + entry.lastAdmin = ctx.moderatorKeys.indexOf(senderKey) !== -1 } } } @@ -544,7 +544,7 @@ define([ if (err) { console.error('Support RPC not ready', err); } })); }).nThen((waitFor) => { - let seed = privateKey.slice(0,24); // XXX better way to get seed? + let seed = privateKey.slice(0,24); // XXX better way to get seed? also in admin/inner.js let hash = Hash.getEditHashFromKeys({ version: 2, type: 'support', @@ -559,6 +559,44 @@ define([ }; + var getAdminKey = function (ctx, data, cId, cb) { + let proxy = ctx.store.proxy; + let supportKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePublic']); + let privateKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePrivate']); + getKeys(ctx, false, {}, (err, obj) => { + if (err) { return void cb({error: err}); } + if (obj.theirPublic !== supportKey) { return void cb({ error: 'EFORBIDDEN' }); } + cb({ + curvePrivate: privateKey + }); + }); + }; + var addModerator = function (ctx, data, cId, cb) { + let proxy = ctx.store.proxy; + var mailbox = Util.find(ctx, [ 'store', 'mailbox' ]); + + let supportKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePublic']); + let privateKey = Util.find(proxy, ['mailboxes', 'supportteam', 'keys', 'curvePrivate']); + let lastKnownHash = Util.find(proxy, ['mailboxes', 'supportteam', 'lastKnownHash']); + let edPublic = proxy.edPublic; + + // Confirm that I know the latest private key + getKeys(ctx, false, {}, (err, obj) => { + if (err) { return void cb({error: err}); } + if (obj.theirPublic !== supportKey) { return void cb({ error: 'EFORBIDDEN' }); } + if (!ctx.moderatorKeys.includes(edPublic)) { return void cb({ error: 'EFORBIDDEN' }); } + // Send this private key to the selected user + mailbox.sendTo('ADD_MODERATOR', { + supportKey: privateKey, + lastKnownHash + }, { + channel: data.mailbox, + curvePublic: data.curvePublic + }, () => { + cb(); + }); + }); + }; Support.init = function (cfg, waitFor, emit) { var support = {}; @@ -572,7 +610,7 @@ define([ var proxy = store.proxy.support = store.proxy.support || {}; var ctx = { - adminKeys: ApiConfig.adminKeys, + moderatorKeys: ApiConfig.moderatorKeys, supportData: proxy, store: cfg.store, Store: cfg.Store, @@ -621,6 +659,12 @@ define([ if (cmd === 'CLOSE_TICKET_ADMIN') { return void closeTicketAdmin(ctx, data, clientId, cb); } + if (cmd === 'GET_PRIVATE_KEY') { + return void getAdminKey(ctx, data, clientId, cb); + } + if (cmd === 'ADD_MODERATOR') { + return void addModerator(ctx, data, clientId, cb); + } if (cmd === 'GET_MY_TICKETS') { return void getMyTickets(ctx, data, clientId, cb); } diff --git a/www/moderation/inner.js b/www/moderation/inner.js index 379ceda80..6514789bf 100644 --- a/www/moderation/inner.js +++ b/www/moderation/inner.js @@ -301,7 +301,8 @@ define([ var privateData = metadataMgr.getPrivateData(); common.setTabTitle(Messages.supportPage); - if (!common.isAdmin()) { + if (!Array.isArray(ApiConfig.moderatorKeys) || + !ApiConfig.moderatorKeys.includes(privateData.edPublic)) { return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden'); } diff --git a/www/support/ui.js b/www/support/ui.js index 44459c7cf..76a6308e5 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -423,7 +423,7 @@ define([ // Check content.sender to see if it comes from us or from an admin var senderKey = content.sender && content.sender.edPublic; var fromMe = senderKey === privateData.edPublic; - var fromAdmin = ctx.adminKeys.indexOf(senderKey) !== -1 + var fromAdmin = ctx.moderatorKeys.indexOf(senderKey) !== -1 || (!senderKey && content.sender.accountName === 'support'); // XXX anon key? var fromPremium = Boolean(content.sender.plan || Util.find(content, ['sender', 'quota', 'plan'])); @@ -498,7 +498,7 @@ define([ var senderKey = content.sender && content.sender.edPublic; var fromMe = senderKey === privateData.edPublic; - var fromAdmin = ctx.adminKeys.indexOf(senderKey) !== -1; + var fromAdmin = ctx.moderatorKeys.indexOf(senderKey) !== -1; var adminClass = (fromAdmin? '.cp-support-fromadmin': ''); var name = Util.fixHTML(content.sender.name) || Messages.anonymous; @@ -518,7 +518,7 @@ define([ isAdmin: isAdmin, pinUsage: pinUsage || false, teamsUsage: teamsUsage || false, - adminKeys: Array.isArray(ApiConfig.adminKeys)? ApiConfig.adminKeys.slice(): [], + moderatorKeys: Array.isArray(ApiConfig.moderatorKeys)? ApiConfig.moderatorKeys.slice(): [], }; ctx.supportModule = common.makeUniversal('support');