mirror of https://github.com/xwiki-labs/cryptpad
Support V2 prototype
This commit is contained in:
parent
93a285baa5
commit
070d8dc450
|
@ -40,6 +40,7 @@ SET_MAINTENANCE
|
|||
// EASIER CONFIG
|
||||
SET_ADMIN_EMAIL
|
||||
SET_SUPPORT_MAILBOX
|
||||
SET_SUPPORT_MAILBOX2
|
||||
|
||||
// COMMUNITY PARTICIPATION AND GOVERNANCE
|
||||
CONSENT_TO_CONTACT
|
||||
|
@ -209,6 +210,10 @@ commands.SET_ADMIN_EMAIL = makeGenericSetter('adminEmail', args_isString);
|
|||
commands.SET_SUPPORT_MAILBOX = makeGenericSetter('supportMailbox', function (args) {
|
||||
return args_isString(args) && Core.isValidPublicKey(args[0]);
|
||||
});
|
||||
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_SUPPORT_MAILBOX2', ["Tdz6+fE9N9XXBY93rW5qeNa/k27yd40c0vq7EJyt7jA="]]], console.log)
|
||||
commands.SET_SUPPORT_MAILBOX2 = makeGenericSetter('newSupportMailbox', function (args) {
|
||||
return args_isString(args) && (Core.isValidPublicKey(args[0]) || !args[0]);
|
||||
});
|
||||
|
||||
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['SET_INSTANCE_PURPOSE', ["development"]]], console.log)
|
||||
commands.SET_INSTANCE_PURPOSE = makeGenericSetter('instancePurpose', args_isString);
|
||||
|
|
|
@ -147,6 +147,7 @@ module.exports.create = function (config) {
|
|||
|
||||
adminEmail: config.adminEmail,
|
||||
supportMailbox: config.supportMailboxPublicKey,
|
||||
newSupportMailbox: undefined,
|
||||
|
||||
metadata_cache: {},
|
||||
channel_cache: {},
|
||||
|
|
|
@ -571,6 +571,7 @@ var serveConfig = makeRouteCache(function () {
|
|||
adminKeys: Env.admins,
|
||||
inactiveTime: Env.inactiveTime,
|
||||
supportMailbox: Env.supportMailbox,
|
||||
newSupportMailbox: Env.newSupportMailbox,
|
||||
defaultStorageLimit: Env.defaultStorageLimit,
|
||||
maxUploadSize: Env.maxUploadSize,
|
||||
premiumUploadSize: Env.premiumUploadSize,
|
||||
|
|
|
@ -45,6 +45,7 @@ Stats.instanceData = function (Env) {
|
|||
|
||||
// we expect that you enable your support mailbox
|
||||
data.supportMailbox = Boolean(Env.supportMailbox);
|
||||
data.newSupportMailbox = Boolean(Env.newSupportMailbox);
|
||||
|
||||
// do you allow registration?
|
||||
data.restrictRegistration = Boolean(Env.restrictRegistration);
|
||||
|
|
|
@ -98,6 +98,7 @@ define([
|
|||
'cp-admin-disk-usage',
|
||||
],
|
||||
'support': [ // Msg.admin_cat_support
|
||||
'cp-admin-support-new',
|
||||
'cp-admin-support-list',
|
||||
'cp-admin-support-init',
|
||||
'cp-admin-support-priv',
|
||||
|
@ -2914,51 +2915,125 @@ Example
|
|||
});
|
||||
return $div;
|
||||
};
|
||||
|
||||
// XXX
|
||||
Messages.admin_supportNewHint = "Create or update the current support keys.";
|
||||
Messages.admin_supportNewTitle = "Initialize support";
|
||||
Messages.admin_supportNewEnabled = "Modern support system is enabled.";
|
||||
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.";
|
||||
create['support-new'] = function () {
|
||||
var $div = makeBlock('support-new'); // Msg.admin_supportNewHint, .admin_supportNewTitle
|
||||
var newSupportKey = ApiConfig.newSupportMailbox;
|
||||
(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);
|
||||
|
||||
var setState = function () {
|
||||
$state.html('');
|
||||
if (newSupportKey) {
|
||||
$button.hide();
|
||||
$delButton.show();
|
||||
return $state.append([
|
||||
h('i.fa.fa-check'),
|
||||
h('span', Messages.admin_supportNewEnabled)
|
||||
]);
|
||||
}
|
||||
$button.show();
|
||||
$state.append([
|
||||
h('i.fa.fa-times'),
|
||||
h('span', Messages.admin_supportNewDisabled)
|
||||
]);
|
||||
};
|
||||
setState();
|
||||
|
||||
Util.onClickEnter($delButton, function () {
|
||||
UI.confirm(Messages.admin_supportNewConfirm, function (yes) {
|
||||
if (!yes) { return; }
|
||||
// Send the decree, don't delete data locally, we just want to remove
|
||||
// the support UI for the clients
|
||||
spinner.spin();
|
||||
$delButton.attr('disabled', 'disabled');
|
||||
sFrameChan.query('Q_ADMIN_RPC', {
|
||||
cmd: 'ADMIN_DECREE',
|
||||
data: ['SET_SUPPORT_MAILBOX2', ['']]
|
||||
}, function (e, response) {
|
||||
$delButton.removeAttr('disabled');
|
||||
if (e || response.error) {
|
||||
UI.warn(Messages.error);
|
||||
console.error(e, response);
|
||||
spinner.hide();
|
||||
return;
|
||||
}
|
||||
spinner.done();
|
||||
UI.log(Messages.saved);
|
||||
newSupportKey = undefined;
|
||||
setState();
|
||||
});
|
||||
});
|
||||
});
|
||||
var next = function () {
|
||||
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);
|
||||
// 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
|
||||
sFrameChan.query('Q_ADMIN_RPC', {
|
||||
cmd: 'ADMIN_DECREE',
|
||||
data: ['SET_SUPPORT_MAILBOX2', [pub]]
|
||||
}, function (e, response) {
|
||||
$button.removeAttr('disabled');
|
||||
if (e || response.error) {
|
||||
UI.warn(Messages.error);
|
||||
console.error(e, response);
|
||||
spinner.hide();
|
||||
return;
|
||||
}
|
||||
spinner.done();
|
||||
UI.log(Messages.saved);
|
||||
newSupportKey = pub;
|
||||
setState();
|
||||
//$('.cp-admin-support-init').hide();
|
||||
//APP.$rightside.append(create['support-list']());
|
||||
//APP.$rightside.append(create['support-priv']());
|
||||
});
|
||||
});
|
||||
};
|
||||
Util.onClickEnter($button, function () {
|
||||
if (newSupportKey) {
|
||||
return void UI.confirm(Messages.admin_supportNewConfirm, function (yes) {
|
||||
if (yes) { next(); }
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
})();
|
||||
return $div;
|
||||
};
|
||||
create['support-init'] = function () {
|
||||
var $div = makeBlock('support-init'); // Msg.admin_supportInitHint, .admin_supportInitTitle
|
||||
if (!supportKey) {
|
||||
(function () {
|
||||
$div.append(h('p', Messages.admin_supportInitHelp));
|
||||
var button = h('button.btn.btn-primary', Messages.admin_supportInitGenerate);
|
||||
var $button = $(button).appendTo($div);
|
||||
$div.append($button);
|
||||
var spinner = UI.makeSpinner($div);
|
||||
$button.click(function () {
|
||||
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);
|
||||
// Store the private key first. It won't be used until the decree is accepted.
|
||||
sFrameChan.query("Q_ADMIN_MAILBOX", 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
|
||||
sFrameChan.query('Q_ADMIN_RPC', {
|
||||
cmd: 'ADMIN_DECREE',
|
||||
data: ['SET_SUPPORT_MAILBOX', [pub]]
|
||||
}, function (e, response) {
|
||||
$button.removeAttr('disabled');
|
||||
if (e || response.error) {
|
||||
UI.warn(Messages.error);
|
||||
console.error(e, response);
|
||||
spinner.hide();
|
||||
return;
|
||||
}
|
||||
spinner.done();
|
||||
UI.log(Messages.saved);
|
||||
supportKey = pub;
|
||||
APP.privateKey = priv;
|
||||
$('.cp-admin-support-init').hide();
|
||||
APP.$rightside.append(create['support-list']());
|
||||
APP.$rightside.append(create['support-priv']());
|
||||
});
|
||||
});
|
||||
});
|
||||
return; // XXX old support can't be created anymore
|
||||
})();
|
||||
return $div;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ define(['/customize/application_config.js'], function (AppConfig) {
|
|||
MAX_PREMIUM_TEAMS_SLOTS: Math.max(AppConfig.maxTeamsSlots || 0, AppConfig.maxPremiumTeamsSlots || 0) || 5,
|
||||
MAX_PREMIUM_TEAMS_OWNED: Math.max(AppConfig.maxTeamsOwned || 0, AppConfig.maxPremiumTeamsOwned || 0) || 5,
|
||||
// Apps
|
||||
criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications', 'calendar'],
|
||||
criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications', 'calendar', 'moderation'],
|
||||
earlyAccessApps: ['doc', 'presentation']
|
||||
};
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ define([
|
|||
'/common/outer/cache-store.js',
|
||||
'/common/outer/sharedfolder.js',
|
||||
'/common/outer/cursor.js',
|
||||
'/common/outer/support.js',
|
||||
'/common/outer/integration.js',
|
||||
'/common/outer/onlyoffice.js',
|
||||
'/common/outer/mailbox.js',
|
||||
|
@ -39,7 +40,7 @@ define([
|
|||
'/components/saferphore/index.js',
|
||||
], function (ApiConfig, Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback,
|
||||
Realtime, Messaging, Pinpad, Cache,
|
||||
SF, Cursor, Integration, OnlyOffice, Mailbox, Profile, Team, Messenger, History,
|
||||
SF, Cursor, Support, Integration, OnlyOffice, Mailbox, Profile, Team, Messenger, History,
|
||||
Calendar, Block, NetConfig, AppConfig,
|
||||
Crypto, ChainPad, CpNetflux, Listmap, Netflux, nThen, Saferphore) {
|
||||
|
||||
|
@ -1616,12 +1617,14 @@ define([
|
|||
});
|
||||
};
|
||||
Store.addAdminMailbox = function (clientId, data, cb) {
|
||||
var priv = data;
|
||||
var priv = data && data.priv;
|
||||
var pub = Hash.getBoxPublicFromSecret(priv);
|
||||
var isNewSupport = data && data.version === 2;
|
||||
if (!priv || !pub) { return void cb({error: 'EINVAL'}); }
|
||||
var channel = Hash.getChannelIdFromKey(pub);
|
||||
var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {};
|
||||
var box = mailboxes.supportadmin = {
|
||||
var key = isNewSupport ? 'support2' : 'supportadmin';
|
||||
var box = mailboxes['supportadmin'] = {
|
||||
channel: channel,
|
||||
viewed: [],
|
||||
lastKnownHash: '',
|
||||
|
@ -1631,7 +1634,7 @@ define([
|
|||
}
|
||||
};
|
||||
Store.pinPads(null, [channel], function () {});
|
||||
store.mailbox.open('supportadmin', box, function () {
|
||||
store.mailbox.open(key, box, function () {
|
||||
console.log('ready');
|
||||
});
|
||||
onSync(null, cb);
|
||||
|
@ -2826,6 +2829,7 @@ define([
|
|||
postMessage(clientId, 'LOADING_DRIVE', data);
|
||||
});
|
||||
loadUniversal(Cursor, 'cursor', waitFor);
|
||||
loadUniversal(Support, 'support', waitFor);
|
||||
loadUniversal(Integration, 'integration', waitFor);
|
||||
loadOnlyOffice();
|
||||
loadUniversal(Messenger, 'messenger', waitFor);
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
define([
|
||||
'/common/common-util.js',
|
||||
'/common/common-hash.js',
|
||||
'/components/nthen/index.js',
|
||||
'/components/chainpad-crypto/crypto.js',
|
||||
'chainpad-netflux'
|
||||
], function (Util, Hash, nThen, Crypto, CpNetflux) {
|
||||
var Support = {};
|
||||
|
||||
var getKeys = function (ctx, cb) {
|
||||
require(['/api/config?' + (+new Date())], function (NewConfig) {
|
||||
supportKey = NewConfig.newSupportMailbox;
|
||||
if (!supportKey) { return void cb('E_NOT_INIT'); }
|
||||
var myCurve = ctx.store.proxy.curvePrivate;
|
||||
cb(null, {
|
||||
supportKey,
|
||||
myCurve
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Get the content of a mailbox and close it
|
||||
// Used for support tickets
|
||||
var getContent = function (ctx, data, _cb) {
|
||||
var supportKey, myCurve;
|
||||
nThen((waitFor) => {
|
||||
// Send ticket to the admins and call back
|
||||
getKeys(ctx, waitFor((err, obj) => {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb({error: err});
|
||||
}
|
||||
supportKey = obj.supportKey;
|
||||
myCurve = obj.myCurve;
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
var keys = Crypto.Curve.deriveKeys(supportKey, myCurve);
|
||||
var crypto = Crypto.Curve.createEncryptor(keys);
|
||||
var cfg = {
|
||||
network: ctx.store.network,
|
||||
channel: data.channel,
|
||||
noChainPad: true,
|
||||
crypto: crypto,
|
||||
owners: []
|
||||
};
|
||||
var all = [];
|
||||
var cpNf;
|
||||
var close = function () {
|
||||
if (!cpNf) { return; }
|
||||
if (typeof(cpNf.stop) !== "function") { return; }
|
||||
cpNf.stop();
|
||||
};
|
||||
var cb = Util.once(Util.both(close, Util.mkAsync(_cb)));
|
||||
cfg.onMessage = function (msg, user, vKey, isCp, hash, author, data) {
|
||||
var time = data && data.time;
|
||||
try {
|
||||
msg = JSON.parse(msg);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
if (author) { msg.author = author; }
|
||||
console.log(msg);
|
||||
all.push(msg);
|
||||
};
|
||||
cfg.onError = cb;
|
||||
cfg.onChannelError = cb;
|
||||
cfg.onReady = function () {
|
||||
cb(null, all);
|
||||
};
|
||||
cpNf = CpNetflux.start(cfg);
|
||||
});
|
||||
};
|
||||
|
||||
var makeTicket = function (ctx, data, cId, cb) {
|
||||
var mailbox = Util.find(ctx, [ 'store', 'mailbox' ]);
|
||||
var anonRpc = Util.find(ctx, [ 'store', 'anon_rpc' ]);
|
||||
if (!mailbox) { return void cb({error: 'E_NOT_READY'}); }
|
||||
if (!anonRpc) { return void cb({error: "anonymous rpc session not ready"}); }
|
||||
|
||||
var channel = data.channel;
|
||||
var title = data.title;
|
||||
var ticket = data.ticket;
|
||||
var supportKey, myCurve;
|
||||
nThen((waitFor) => {
|
||||
// Send ticket to the admins and call back
|
||||
getKeys(ctx, waitFor((err, obj) => {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb({error: err});
|
||||
}
|
||||
supportKey = obj.supportKey;
|
||||
myCurve = obj.myCurve;
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
// Create ticket mailbox
|
||||
var keys = Crypto.Curve.deriveKeys(supportKey, myCurve);
|
||||
var crypto = Crypto.Curve.createEncryptor(keys);
|
||||
var text = JSON.stringify(ticket);
|
||||
var ciphertext = crypto.encrypt(text);
|
||||
anonRpc.send("WRITE_PRIVATE_MESSAGE", [
|
||||
channel,
|
||||
ciphertext
|
||||
], waitFor((err) => {
|
||||
if (err) {
|
||||
waitFor.abort();
|
||||
return void cb({error: err});
|
||||
}
|
||||
}));
|
||||
}).nThen((waitFor) => {
|
||||
// Store in our worker
|
||||
console.error(channel);
|
||||
ctx.supportData[channel] = {
|
||||
time: +new Date(),
|
||||
title: title,
|
||||
curvePublic: supportKey // XXX Should we store this value or delete the ticket on key rotation?
|
||||
};
|
||||
ctx.Store.onSync(null, waitFor());
|
||||
}).nThen(() => {
|
||||
var supportChannel = Hash.getChannelIdFromKey(supportKey);
|
||||
console.error(supportChannel);
|
||||
mailbox.sendTo('NEW_TICKET', {
|
||||
title: title,
|
||||
channel: channel
|
||||
}, {
|
||||
channel: supportChannel,
|
||||
curvePublic: supportKey
|
||||
}, (obj) => {
|
||||
// Don't store the ticket in case of error
|
||||
if (obj && obj.error) { delete ctx.supportData[channel]; }
|
||||
cb(obj);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var getMyTickets = function (ctx, data, cId, cb) {
|
||||
var all = [];
|
||||
var n = nThen;
|
||||
Object.keys(ctx.supportData).forEach(function (ticket) {
|
||||
n = n((waitFor) => {
|
||||
var t = Util.clone(ctx.supportData[ticket]);
|
||||
getContent(ctx, {
|
||||
channel: ticket,
|
||||
}, waitFor((err, messages) => {
|
||||
if (err) { t.error = err; }
|
||||
else { t.messages = messages; }
|
||||
all.push(t);
|
||||
}));
|
||||
}).nThen;
|
||||
});
|
||||
n(() => {
|
||||
cb({tickets: all});
|
||||
});
|
||||
};
|
||||
|
||||
Support.init = function (cfg, waitFor, emit) {
|
||||
var support = {};
|
||||
|
||||
// Already initialized by a "noDrive" tab?
|
||||
if (cfg.store && cfg.store.modules && cfg.store.modules['support']) {
|
||||
return cfg.store.modules['support'];
|
||||
}
|
||||
|
||||
var store = cfg.store;
|
||||
var proxy = store.proxy.support = store.proxy.support || {};
|
||||
|
||||
var ctx = {
|
||||
supportData: proxy,
|
||||
store: cfg.store,
|
||||
Store: cfg.Store,
|
||||
emit: emit,
|
||||
channels: {},
|
||||
clients: {}
|
||||
};
|
||||
|
||||
support.removeClient = function (clientId) {
|
||||
removeClient(ctx, clientId);
|
||||
};
|
||||
support.leavePad = function (padChan) {
|
||||
leaveChannel(ctx, padChan);
|
||||
};
|
||||
support.execCommand = function (clientId, obj, cb) {
|
||||
var cmd = obj.cmd;
|
||||
var data = obj.data;
|
||||
if (cmd === 'MAKE_TICKET') {
|
||||
return void makeTicket(ctx, data, clientId, cb);
|
||||
}
|
||||
if (cmd === 'GET_MY_TICKETS') {
|
||||
return void getMyTickets(ctx, data, clientId, cb);
|
||||
}
|
||||
};
|
||||
|
||||
return support;
|
||||
};
|
||||
|
||||
return Support;
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
@import (reference) '../../customize/src/less2/include/framework.less';
|
||||
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
|
||||
@import (reference) '../../customize/src/less2/include/support.less';
|
||||
@import (reference) '../../customize/src/less2/include/charts.less';
|
||||
&.cp-app-moderation {
|
||||
|
||||
.framework_min_main();
|
||||
.sidebar-layout_main();
|
||||
.support_main();
|
||||
.charts_main();
|
||||
|
||||
.cp-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
label{
|
||||
margin-top:0.5rem;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
.cp-sidebarlayout-element {
|
||||
label:not(.cp-admin-label) {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
input {
|
||||
max-width: 25rem;
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CryptPad</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<link href="/customize/src/outer.css?ver=1.3.2" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<iframe-placeholder>
|
|
@ -0,0 +1,22 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="cp-app-noscroll">
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script src="/customize/pre-loading.js?ver=1.1"></script>
|
||||
<link href="/customize/src/pre-loading.css?ver=1.0" rel="stylesheet" type="text/css">
|
||||
<script async data-bootload="/moderation/inner.js" data-main="/common/sframe-boot.js?ver=1.11" src="/components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
.loading-hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="cp-app-moderation">
|
||||
<div id="cp-toolbar" class="cp-toolbar-container"></div>
|
||||
<div id="cp-content-container"></div>
|
||||
</body>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'/api/config',
|
||||
'/customize/application_config.js',
|
||||
'/components/chainpad-crypto/crypto.js',
|
||||
'/common/toolbar.js',
|
||||
'/components/nthen/index.js',
|
||||
'/common/sframe-common.js',
|
||||
'/common/hyperscript.js',
|
||||
'/customize/messages.js',
|
||||
'/common/common-interface.js',
|
||||
'/common/common-ui-elements.js',
|
||||
'/common/common-util.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/common-signing-keys.js',
|
||||
'/support/ui.js',
|
||||
'/common/clipboard.js',
|
||||
'json.sortify',
|
||||
|
||||
'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!/moderation/app-moderation.less',
|
||||
], function (
|
||||
$,
|
||||
ApiConfig,
|
||||
AppConfig,
|
||||
Crypto,
|
||||
Toolbar,
|
||||
nThen,
|
||||
SFCommon,
|
||||
h,
|
||||
Messages,
|
||||
UI,
|
||||
UIElements,
|
||||
Util,
|
||||
Hash,
|
||||
Keys,
|
||||
Support,
|
||||
Clipboard,
|
||||
Sortify,
|
||||
)
|
||||
{
|
||||
var APP = {
|
||||
'instanceStatus': {}
|
||||
};
|
||||
|
||||
var Nacl = window.nacl;
|
||||
var common;
|
||||
var sFrameChan;
|
||||
|
||||
// XXX
|
||||
|
||||
var andThen = function () {
|
||||
|
||||
|
||||
};
|
||||
|
||||
var createToolbar = function () {
|
||||
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
|
||||
var configTb = {
|
||||
displayed: displayed,
|
||||
sfCommon: common,
|
||||
$container: APP.$toolbar,
|
||||
pageTitle: Messages.supportPage,
|
||||
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-content-container');
|
||||
APP.$toolbar = $('#cp-toolbar');
|
||||
sFrameChan = common.getSframeChannel();
|
||||
sFrameChan.onReady(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
if (!common.isAdmin()) { return; } // XXX moderator
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
createToolbar();
|
||||
var metadataMgr = common.getMetadataMgr();
|
||||
var privateData = metadataMgr.getPrivateData();
|
||||
common.setTabTitle(Messages.supportPage);
|
||||
|
||||
if (!common.isAdmin()) {
|
||||
return void UI.errorLoadingScreen(Messages.admin_authError || '403 Forbidden');
|
||||
}
|
||||
|
||||
APP.privateKey = privateData.supportPrivateKey;
|
||||
APP.origin = privateData.origin;
|
||||
APP.readOnly = privateData.readOnly;
|
||||
APP.support = Support.create(common, true);
|
||||
|
||||
UI.removeLoadingScreen();
|
||||
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
// Load #1, load as little as possible because we are in a race to get the loading screen up.
|
||||
define([
|
||||
'/components/nthen/index.js',
|
||||
'/api/config',
|
||||
'/common/dom-ready.js',
|
||||
'/common/sframe-common-outer.js',
|
||||
], function (nThen, ApiConfig, DomReady, SFCommonO) {
|
||||
|
||||
// Loaded in load #2
|
||||
nThen(function (waitFor) {
|
||||
DomReady.onReady(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
SFCommonO.initIframe(waitFor);
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
var addRpc = function (sframeChan, Cryptpad/*, Utils*/) {
|
||||
// Adding a new avatar from the profile: pin it and store it in the object
|
||||
sframeChan.on('Q_ADMIN_MAILBOX', function (data, cb) {
|
||||
Cryptpad.addAdminMailbox(data, cb);
|
||||
});
|
||||
sframeChan.on('Q_ADMIN_RPC', function (data, cb) {
|
||||
Cryptpad.adminRpc(data, cb);
|
||||
});
|
||||
sframeChan.on('Q_UPDATE_LIMIT', function (data, cb) {
|
||||
Cryptpad.updatePinLimit(function (e) {
|
||||
cb({error: e});
|
||||
});
|
||||
});
|
||||
};
|
||||
var category;
|
||||
if (window.location.hash) {
|
||||
category = window.location.hash.slice(1);
|
||||
window.location.hash = '';
|
||||
}
|
||||
var addData = function (obj) {
|
||||
if (category) { obj.category = category; }
|
||||
};
|
||||
SFCommonO.start({
|
||||
noRealtime: true,
|
||||
addRpc: addRpc,
|
||||
addData: addData
|
||||
});
|
||||
});
|
||||
});
|
|
@ -47,6 +47,7 @@ define([
|
|||
var categories = {
|
||||
'tickets': [ // Msg.support_cat_tickets
|
||||
'cp-support-list',
|
||||
'cp-support-listnew',
|
||||
],
|
||||
'new': [ // Msg.support_cat_new
|
||||
'cp-support-subscribe',
|
||||
|
@ -151,6 +152,29 @@ define([
|
|||
return $div;
|
||||
};
|
||||
|
||||
create['listnew'] = function () {
|
||||
var key = 'listnew';
|
||||
var $div = makeBlock(key); // Msg.support_listHint, .support_listTitle
|
||||
let refresh = function () {
|
||||
APP.supportModule.execCommand('GET_MY_TICKETS', {}, function (obj) {
|
||||
console.error(obj);
|
||||
if (obj && obj.error) {
|
||||
console.error(obj.error);
|
||||
return void UI.warn(Messages.error);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
var button = h('btn.btn-primary', 'refresh'); // XXX
|
||||
Util.onClickEnter($(button), function () {
|
||||
refresh();
|
||||
});
|
||||
$div.append([
|
||||
button
|
||||
]);
|
||||
return $div;
|
||||
};
|
||||
|
||||
create['language'] = function () {
|
||||
if (!Array.isArray(AppConfig.supportLanguages)) { return $(h('div')); }
|
||||
var languages = AppConfig.supportLanguages;
|
||||
|
@ -209,17 +233,19 @@ define([
|
|||
var id = Util.uid();
|
||||
|
||||
$div.find('button').click(function () {
|
||||
var metadataMgr = common.getMetadataMgr();
|
||||
var privateData = metadataMgr.getPrivateData();
|
||||
var user = metadataMgr.getUserData();
|
||||
var sent = APP.support.sendForm(id, form, {
|
||||
channel: privateData.support,
|
||||
curvePublic: user.curvePublic
|
||||
});
|
||||
id = Util.uid();
|
||||
if (sent) {
|
||||
var data = APP.support.getFormData(form);
|
||||
APP.supportModule.execCommand('MAKE_TICKET', {
|
||||
channel: Hash.createChannelId(),
|
||||
title: data.title,
|
||||
ticket: data
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) {
|
||||
console.error(obj.error);
|
||||
return void UI.warn(Messages.error);
|
||||
}
|
||||
id = Util.uid();
|
||||
$('.cp-sidebarlayout-category[data-category="tickets"]').click();
|
||||
}
|
||||
});
|
||||
});
|
||||
$div.find('button').before(form);
|
||||
return $div;
|
||||
|
@ -345,6 +371,7 @@ define([
|
|||
APP.origin = privateData.origin;
|
||||
APP.readOnly = privateData.readOnly;
|
||||
APP.support = Support.create(common, false, APP.pinUsage, APP.teamsUsage);
|
||||
APP.supportModule = common.makeUniversal('support');
|
||||
|
||||
// Content
|
||||
var $rightside = APP.$rightside;
|
||||
|
|
|
@ -34,7 +34,7 @@ define([
|
|||
};
|
||||
SFCommonO.start({
|
||||
noRealtime: true,
|
||||
addData: addData
|
||||
addData: addData,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ define([
|
|||
var user = metadataMgr.getUserData();
|
||||
var teams = privateData.teams || {};
|
||||
data = data || {};
|
||||
if (data.sender) { return data; }
|
||||
|
||||
data.sender = {
|
||||
name: user.name,
|
||||
|
@ -101,7 +102,7 @@ define([
|
|||
};
|
||||
|
||||
|
||||
var sendForm = function (ctx, id, form, dest) {
|
||||
var getFormData = function (ctx, form) {
|
||||
var $form = $(form);
|
||||
var $cat = $form.find('.cp-support-form-category');
|
||||
var $title = $form.find('.cp-support-form-title');
|
||||
|
@ -110,14 +111,6 @@ define([
|
|||
var $attachments = $form.find('.cp-support-attachments');
|
||||
|
||||
var category = $cat.val().trim();
|
||||
/*
|
||||
Messages.support_formCategoryError = "Please select a ticket category from the dropdown menu"; // TODO
|
||||
if (!category) {
|
||||
console.log($cat);
|
||||
return void UI.alert(Messages.support_formCategoryError);
|
||||
}
|
||||
*/
|
||||
|
||||
var title = $title.val().trim();
|
||||
if (!title) {
|
||||
return void UI.alert(Messages.support_formTitleError);
|
||||
|
@ -140,13 +133,15 @@ Messages.support_formCategoryError = "Please select a ticket category from the d
|
|||
});
|
||||
$attachments.html('');
|
||||
|
||||
send(ctx, id, 'TICKET', {
|
||||
return getDebuggingData(ctx, {
|
||||
category: category,
|
||||
title: title,
|
||||
attachments: attachments,
|
||||
message: content,
|
||||
}, dest);
|
||||
|
||||
});
|
||||
};
|
||||
var sendForm = function (ctx, id, form, dest) {
|
||||
send(ctx, id, 'TICKET', getFormData(ctx, form), dest, isNew);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
@ -167,9 +162,7 @@ Messages.support_formCategoryError = "Please select a ticket category from the d
|
|||
return {
|
||||
tag: 'a',
|
||||
content: h('span', Messages['support_cat_'+key]),
|
||||
action: function () {
|
||||
onChange(key);
|
||||
}
|
||||
attributes: { 'data-value': key }
|
||||
};
|
||||
});
|
||||
var dropdownCfg = {
|
||||
|
@ -181,6 +174,7 @@ Messages.support_formCategoryError = "Please select a ticket category from the d
|
|||
};
|
||||
var $select = UIElements.createDropdown(dropdownCfg);
|
||||
$select.find('button').addClass('btn');
|
||||
$select.onChange.reg(onChange);
|
||||
return $select;
|
||||
};
|
||||
|
||||
|
@ -221,7 +215,7 @@ Messages.support_formCategoryError = "Please select a ticket category from the d
|
|||
ctx.common.openUnsafeURL(href);
|
||||
};
|
||||
|
||||
makeCategoryDropdown(ctx, catContainer, function (key) {
|
||||
makeCategoryDropdown(ctx, catContainer, function (text, key) {
|
||||
$(category).val(key);
|
||||
if (!notice) { return; }
|
||||
//console.log(key);
|
||||
|
@ -532,6 +526,8 @@ Messages.support_formCategoryError = "Please select a ticket category from the d
|
|||
adminKeys: Array.isArray(ApiConfig.adminKeys)? ApiConfig.adminKeys.slice(): [],
|
||||
};
|
||||
|
||||
ctx.supportModule = common.makeUniversal('support');
|
||||
|
||||
var fmConfig = {
|
||||
body: $('body'),
|
||||
noStore: true, // Don't store attachments into our drive
|
||||
|
@ -543,8 +539,8 @@ Messages.support_formCategoryError = "Please select a ticket category from the d
|
|||
};
|
||||
ctx.FM = common.createFileManager(fmConfig);
|
||||
|
||||
ui.sendForm = function (id, form, dest) {
|
||||
return sendForm(ctx, id, form, dest);
|
||||
ui.getFormData = function (form) {
|
||||
return getFormData(ctx, form);
|
||||
};
|
||||
ui.makeForm = function (cb, title, hideNotice) {
|
||||
return makeForm(ctx, cb, title, hideNotice);
|
||||
|
|
Loading…
Reference in New Issue