Team invitation link improvements

This commit is contained in:
yflory 2022-11-04 16:42:04 +01:00
parent acd7d9654d
commit fb079c49bf
6 changed files with 184 additions and 35 deletions

View File

@ -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();

View File

@ -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);
});
}
}());

View File

@ -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;
};

View File

@ -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

View File

@ -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%;

View File

@ -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) {