mirror of https://github.com/xwiki-labs/cryptpad
Team invitation link improvements
This commit is contained in:
parent
acd7d9654d
commit
fb079c49bf
|
@ -352,10 +352,35 @@ define([
|
|||
});
|
||||
|
||||
var linkName, linkPassword, linkMessage, linkError, linkSpinText;
|
||||
var linkForm, linkSpin, linkResult;
|
||||
var linkForm, linkSpin, linkResult, linkUses;
|
||||
var linkWarning;
|
||||
// Invite from link
|
||||
var dismissButton = h('span.fa.fa-times');
|
||||
|
||||
var options = [{
|
||||
tag: 'a',
|
||||
attributes: {'data-value': 'VIEWER', href:'#'},
|
||||
content: h('span', Messages.team_viewers),
|
||||
}, {
|
||||
tag: 'a',
|
||||
attributes: {'data-value': 'MEMBER', href:'#'},
|
||||
content: h('span', Messages.team_members),
|
||||
}];
|
||||
var dropdownConfig = {
|
||||
text: Messages.team_viewers, // Button initial text
|
||||
options: options, // Entries displayed in the menu
|
||||
isSelect: true,
|
||||
caretDown: true,
|
||||
common: common,
|
||||
buttonCls: 'btn'
|
||||
};
|
||||
var $block = UIElements.createDropdown(dropdownConfig);
|
||||
$block.setValue('VIEWER');
|
||||
Messages.team_inviteRole = "Initial role"; // XXX
|
||||
Messages.team_inviteUses = "Max uses (0 = infinite)"; // XXX
|
||||
|
||||
|
||||
|
||||
var linkContent = h('div.cp-share-modal', [
|
||||
h('p', Messages.team_inviteLinkTitle ),
|
||||
linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}),
|
||||
|
@ -387,7 +412,20 @@ define([
|
|||
linkMessage = h('textarea.cp-teams-invite-message', {
|
||||
placeholder: Messages.team_inviteLinkNoteMsg,
|
||||
rows: 3
|
||||
})
|
||||
}),
|
||||
h('div.cp-teams-invite-block.cp-teams-invite-role',
|
||||
h('span', Messages.team_inviteRole),
|
||||
$block[0]
|
||||
),
|
||||
h('div.cp-teams-invite-block.cp-teams-invite-uses',
|
||||
h('span', Messages.team_inviteUses),
|
||||
linkUses = h('input', {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 999,
|
||||
value: 1
|
||||
})
|
||||
),
|
||||
]),
|
||||
linkSpin = h('div.cp-teams-invite-spinner', {
|
||||
style: 'display: none;'
|
||||
|
@ -400,17 +438,18 @@ define([
|
|||
}, h('textarea', {
|
||||
readonly: 'readonly'
|
||||
})),
|
||||
linkWarning = h('div.cp-teams-invite-alert.alert.alert-warning.dismissable', {
|
||||
linkWarning = h('div.cp-teams-invite-alert.alert.alert-warning.dismissable', { // XXX remove warning?
|
||||
style: "display: none;"
|
||||
}, [
|
||||
h('span.cp-inline-alert-text', Messages.team_inviteLinkWarning),
|
||||
dismissButton
|
||||
])
|
||||
]);
|
||||
$(linkUses).on('change keyup', function(e) {
|
||||
if (e.target.value === '') { e.target.value = 0; }
|
||||
});
|
||||
$(linkMessage).keydown(function (e) {
|
||||
if (e.which === 13) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (e.which === 13) { e.stopPropagation(); }
|
||||
});
|
||||
var localStore = window.cryptpadStore;
|
||||
localStore.get('hide-alert-teamInvite', function (val) {
|
||||
|
@ -428,6 +467,11 @@ define([
|
|||
var $nav = $linkContent.closest('.alertify').find('nav');
|
||||
$(linkError).text('').hide();
|
||||
var name = $(linkName).val();
|
||||
|
||||
var uses = Number($(linkUses).val());
|
||||
if (isNaN(uses) || !uses) { uses = -1; }
|
||||
var role = $block.getValue() || 'VIEWER';
|
||||
|
||||
var pw = $(linkPassword).find('input').val();
|
||||
var msg = $(linkMessage).val();
|
||||
var hash = Hash.createRandomHash('invite', pw);
|
||||
|
@ -461,6 +505,8 @@ define([
|
|||
hash: hash,
|
||||
teamId: config.teamId,
|
||||
seeds: seeds,
|
||||
role: role,
|
||||
uses: uses
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) {
|
||||
waitFor.abort();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
(function () {
|
||||
var factory = function (Util, Cred, Nacl) {
|
||||
var factory = function (Util, Cred, Nacl, Crypto) {
|
||||
var Invite = {};
|
||||
|
||||
var encode64 = Nacl.util.encodeBase64;
|
||||
|
@ -49,6 +49,24 @@ var factory = function (Util, Cred, Nacl) {
|
|||
roster.invite(toInvite, cb);
|
||||
};
|
||||
|
||||
// Invite links should only be visible to members or above, so
|
||||
// we store them in the roster encrypted with a string only available
|
||||
// to users with edit rights
|
||||
var decodeUTF8 = Nacl.util.decodeUTF8;
|
||||
Invite.encryptHash = function (data, seedStr) {
|
||||
var array = decodeUTF8(seedStr);
|
||||
var bytes = Nacl.hash(array);
|
||||
var cryptKey = bytes.subarray(0, 32);
|
||||
return Crypto.encrypt(data, cryptKey);
|
||||
};
|
||||
Invite.decryptHash = function (encryptedStr, seedStr) {
|
||||
var array = decodeUTF8(seedStr);
|
||||
var bytes = Nacl.hash(array);
|
||||
var cryptKey = bytes.subarray(0, 32);
|
||||
return Crypto.decrypt(encryptedStr, cryptKey);
|
||||
};
|
||||
|
||||
|
||||
/* INPUTS
|
||||
|
||||
* password (for scrypt)
|
||||
|
@ -84,16 +102,17 @@ var factory = function (Util, Cred, Nacl) {
|
|||
module.exports = factory(
|
||||
require("../common-util"),
|
||||
require("../common-credential.js"),
|
||||
require("nthen"),
|
||||
require("tweetnacl/nacl-fast")
|
||||
require("tweetnacl/nacl-fast"),
|
||||
require("chainpad-crypto/crypto")
|
||||
);
|
||||
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
||||
define([
|
||||
'/common/common-util.js',
|
||||
'/common/common-credential.js',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
], function (Util, Cred) {
|
||||
return factory(Util, Cred, window.nacl);
|
||||
], function (Util, Cred, Crypto) {
|
||||
return factory(Util, Cred, window.nacl, Crypto);
|
||||
});
|
||||
}
|
||||
}());
|
||||
|
|
|
@ -463,9 +463,22 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback)
|
|||
if (typeof(members[curve]) !== 'undefined') { throw new Error("MEMBER_ALREADY_PRESENT"); }
|
||||
|
||||
// copy the new profile from the old one
|
||||
members[curve] = Util.clone(members[author]);
|
||||
// and erase the old one
|
||||
delete members[author];
|
||||
var clone = Util.clone(members[author]);
|
||||
delete clone.remaining;
|
||||
delete clone.totalUses;
|
||||
delete clone.inviteChannel;
|
||||
delete clone.previewChannel;
|
||||
members[curve] = clone;
|
||||
|
||||
// XXX
|
||||
var remaining = members[author].remaining || 1;
|
||||
if (remaining === -1) { return true; } // Infinite uses, keep the link
|
||||
if (remaining > 1) { // Remove 1 use
|
||||
members[author].remaining = remaining - 1;
|
||||
} else { // Disable link
|
||||
delete members[author];
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
|
@ -1038,6 +1038,16 @@ define([
|
|||
});
|
||||
}
|
||||
|
||||
// Decrypt hash for invite links
|
||||
Object.keys(members).forEach(function (curve) {
|
||||
var member = members[curve];
|
||||
if (!member.inviteChannel) { return; }
|
||||
if (!member.hash) { return; }
|
||||
try {
|
||||
member.hash = Invite.decryptHash(member.hash, teamData.hash);
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
cb(members);
|
||||
});
|
||||
};
|
||||
|
@ -1580,10 +1590,12 @@ define([
|
|||
var message = data.message;
|
||||
var name = data.name;
|
||||
|
||||
/*
|
||||
var password = data.password;
|
||||
//var password = data.password;
|
||||
var hash = data.hash;
|
||||
*/
|
||||
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
|
||||
try {
|
||||
var encryptedHash = Invite.encryptHash(hash, teamData.hash);
|
||||
} catch (e) { console.error(e); }
|
||||
|
||||
// derive { channel, cryptKey} for the preview content channel
|
||||
var previewKeys = Invite.derivePreviewKeys(seeds.preview);
|
||||
|
@ -1595,6 +1607,10 @@ define([
|
|||
// and a placeholder in the roster
|
||||
var ephemeralKeys = Invite.generateKeys();
|
||||
|
||||
// Initial role of the invited users
|
||||
var role = data.role || "VIEWER"; // XXX
|
||||
var uses = data.uses || 1;
|
||||
|
||||
nThen(function (w) {
|
||||
|
||||
|
||||
|
@ -1652,9 +1668,12 @@ define([
|
|||
};
|
||||
putOpts.metadata.validateKey = sign.validateKey;
|
||||
|
||||
|
||||
|
||||
|
||||
// available only with the link and the content
|
||||
var inviteContent = {
|
||||
teamData: getInviteData(ctx, teamId, false),
|
||||
teamData: getInviteData(ctx, teamId, role === "MEMBER"),
|
||||
ephemeral: {
|
||||
edPublic: ephemeralKeys.edPublic,
|
||||
edPrivate: ephemeralKeys.edPrivate,
|
||||
|
@ -1692,6 +1711,10 @@ define([
|
|||
curvePublic: ephemeralKeys.curvePublic,
|
||||
displayName: data.name,
|
||||
pending: true,
|
||||
remaining: uses,
|
||||
totalUses: uses,
|
||||
role: role,
|
||||
hash: encryptedHash,
|
||||
inviteChannel: inviteKeys.channel,
|
||||
previewChannel: previewKeys.channel,
|
||||
}
|
||||
|
@ -1821,20 +1844,22 @@ define([
|
|||
}));
|
||||
}).nThen(function () {
|
||||
var tempRpc = {};
|
||||
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
|
||||
if (err) { return; }
|
||||
var rpc = tempRpc.rpc;
|
||||
if (rosterState.inviteChannel) {
|
||||
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
}
|
||||
if (rosterState.previewChannel) {
|
||||
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!rosterState.remaining || rosterState.remaining === 1) {
|
||||
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
|
||||
if (err) { return; }
|
||||
var rpc = tempRpc.rpc;
|
||||
if (rosterState.inviteChannel) {
|
||||
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
}
|
||||
if (rosterState.previewChannel) {
|
||||
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add the team to our list and join...
|
||||
joinTeam(ctx, {
|
||||
team: inviteContent.teamData
|
||||
|
|
|
@ -285,6 +285,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.cp-teams-invite-uses { // XXX
|
||||
input {
|
||||
margin-left: 10px !important;
|
||||
margin-bottom: 0px !important;
|
||||
width: 75px !important;
|
||||
}
|
||||
|
||||
}
|
||||
.cp-teams-invite-role {
|
||||
span:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#cp-teams-roster-dialog {
|
||||
table {
|
||||
width: 100%;
|
||||
|
|
|
@ -17,6 +17,7 @@ define([
|
|||
'/customize/application_config.js',
|
||||
'/common/messenger-ui.js',
|
||||
'/common/inner/invitation.js',
|
||||
'/common/clipboard.js',
|
||||
'/common/make-backup.js',
|
||||
'/customize/messages.js',
|
||||
|
||||
|
@ -43,6 +44,7 @@ define([
|
|||
AppConfig,
|
||||
MessengerUI,
|
||||
InviteInner,
|
||||
Clipboard,
|
||||
Backup,
|
||||
Messages)
|
||||
{
|
||||
|
@ -714,6 +716,20 @@ define([
|
|||
title: Messages.team_pendingOwnerTitle
|
||||
}, ' ' + Messages.team_pendingOwner));
|
||||
}
|
||||
if (data.pending && data.inviteChannel && data.remaining === -1) { // Invite link
|
||||
Messages.team_linkUsesInfinite = "(infinite uses)"; // XXX
|
||||
$(name).append(h('em', ' ' + Messages.team_linkUsesInfinite));
|
||||
} else if (data.pending && data.inviteChannel) {
|
||||
Messages.team_linkUses = "({0}/{1} remaining)"; // XXX
|
||||
$(name).append(h('em', ' ' + Messages._getKey('team_linkUses', [
|
||||
data.remaining || 1,
|
||||
data.totalUses || 1
|
||||
])));
|
||||
}
|
||||
if (data.pending && data.inviteChannel) {
|
||||
var r = data.role === "MEMBER" ? Messages.team_members : Messages.team_viewers;
|
||||
$(name).append(h('em', ' (' + r + ')'));
|
||||
}
|
||||
// Status
|
||||
var status = h('span.cp-team-member-status'+(data.online ? '.online' : ''));
|
||||
// Actions
|
||||
|
@ -817,6 +833,21 @@ define([
|
|||
actions,
|
||||
status,
|
||||
];
|
||||
if (data.inviteChannel) {
|
||||
var copy = h('span.fa.fa-copy');
|
||||
$(copy).click(function () {
|
||||
var privateData = common.getMetadataMgr().getPrivateData();
|
||||
var origin = privateData.origin;
|
||||
var href = origin + Hash.hashToHref(data.hash, 'teams');
|
||||
var success = Clipboard.copy(href);
|
||||
if (success) { UI.log(Messages.shareSuccess); }
|
||||
}).prependTo(actions);
|
||||
content = [
|
||||
avatar,
|
||||
name,
|
||||
actions
|
||||
];
|
||||
}
|
||||
var div = h('div.cp-team-roster-member', content);
|
||||
if (data.profile) {
|
||||
$(div).dblclick(function (e) {
|
||||
|
@ -872,7 +903,7 @@ define([
|
|||
if (!roster[k].pending) { return; }
|
||||
if (!roster[k].inviteChannel) { return; }
|
||||
roster[k].curvePublic = k;
|
||||
return roster[k].role === "VIEWER" || !roster[k].role;
|
||||
return roster[k].role === "MEMBER" || roster[k].role === "VIEWER" || !roster[k].role;
|
||||
}).map(function (k) {
|
||||
return makeMember(common, roster[k], me);
|
||||
});
|
||||
|
@ -1524,12 +1555,12 @@ define([
|
|||
$div.empty().append(content);
|
||||
});
|
||||
}
|
||||
var $divLink = $('div.cp-team-link').empty();
|
||||
/*var $divLink = $('div.cp-team-link').empty();
|
||||
if ($divLink.length) {
|
||||
refreshLink(common, function (content) {
|
||||
$divLink.append(content);
|
||||
});
|
||||
}
|
||||
}*/
|
||||
var $divCreate = $('div.cp-team-create');
|
||||
if ($divCreate.length) {
|
||||
refreshCreate(common, function (content) {
|
||||
|
|
Loading…
Reference in New Issue