mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-14 11:05:41 +05:00
Merge branch 'team-onboarding' into 5.2-candidate
This commit is contained in:
commit
4434cdca17
@ -123,6 +123,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
&.cp-teams-invite-role {
|
||||
.cp-radio:not(:first-child) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-teams-invite-message {
|
||||
resize: none;
|
||||
|
||||
@ -352,10 +352,23 @@ define([
|
||||
});
|
||||
|
||||
var linkName, linkPassword, linkMessage, linkError, linkSpinText;
|
||||
var linkForm, linkSpin, linkResult;
|
||||
var linkForm, linkSpin, linkResult, linkUses, linkRole;
|
||||
var linkWarning;
|
||||
// Invite from link
|
||||
var dismissButton = h('span.fa.fa-times');
|
||||
|
||||
var roleViewer = UI.createRadio('cp-team-role', 'cp-team-role-viewer',
|
||||
Messages.team_viewers, true, {
|
||||
input: { value: 'VIEWER' },
|
||||
});
|
||||
var roleMember = UI.createRadio('cp-team-role', 'cp-team-role-member',
|
||||
Messages.team_members, false, {
|
||||
input: { value: 'MEMBER' },
|
||||
});
|
||||
|
||||
Messages.team_inviteRole = "Initial role"; // XXX
|
||||
Messages.team_inviteUses = "Use(s) allowed for this link (0 = no limit)"; // 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;'}),
|
||||
@ -372,7 +385,7 @@ define([
|
||||
h('div.cp-teams-invite-block', [
|
||||
h('span', Messages.team_inviteLinkSetPassword),
|
||||
h('a.cp-teams-help.fa.fa-question-circle', {
|
||||
href: origin + Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
|
||||
href: Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
|
||||
target: "_blank",
|
||||
'data-tippy-placement': "right"
|
||||
})
|
||||
@ -387,7 +400,21 @@ define([
|
||||
linkMessage = h('textarea.cp-teams-invite-message', {
|
||||
placeholder: Messages.team_inviteLinkNoteMsg,
|
||||
rows: 3
|
||||
})
|
||||
}),
|
||||
linkRole = h('div.cp-teams-invite-block.cp-teams-invite-role',
|
||||
h('span', Messages.team_inviteRole),
|
||||
roleViewer,
|
||||
roleMember
|
||||
),
|
||||
h('div.cp-teams-invite-block.cp-teams-invite-uses',
|
||||
linkUses = h('input', {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 999,
|
||||
value: 1
|
||||
}),
|
||||
h('span', Messages.team_inviteUses)
|
||||
),
|
||||
]),
|
||||
linkSpin = h('div.cp-teams-invite-spinner', {
|
||||
style: 'display: none;'
|
||||
@ -400,17 +427,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 +456,12 @@ 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 = $(linkRole).find("input[name='cp-team-role']:checked").val() || 'VIEWER';
|
||||
|
||||
var pw = $(linkPassword).find('input').val();
|
||||
var msg = $(linkMessage).val();
|
||||
var hash = Hash.createRandomHash('invite', pw);
|
||||
@ -461,6 +495,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,17 @@ define([
|
||||
});
|
||||
}
|
||||
|
||||
// Decrypt hash for invite links
|
||||
Object.keys(members).forEach(function (curve) {
|
||||
var member = members[curve];
|
||||
if (!member.inviteChannel) { return; }
|
||||
if (!member.hash) { return; }
|
||||
if (!teamData.hash) { delete member.hash; return; }
|
||||
try {
|
||||
member.hash = Invite.decryptHash(member.hash, teamData.hash);
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
cb(members);
|
||||
});
|
||||
};
|
||||
@ -1580,10 +1591,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 +1608,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 +1669,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 +1712,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 +1845,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,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cp-teams-invite-uses { // XXX
|
||||
input {
|
||||
margin-bottom: 0px !important;
|
||||
margin-right: 10px;
|
||||
width: 75px !important;
|
||||
}
|
||||
|
||||
}
|
||||
.cp-teams-invite-role {
|
||||
margin-bottom: 15px;
|
||||
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 DB done
|
||||
$(name).append(h('em', ' ' + Messages.team_linkUsesInfinite));
|
||||
} else if (data.pending && data.inviteChannel) {
|
||||
Messages.team_linkUses = "({0}/{1} remaining)"; // XXX DB done
|
||||
$(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,23 @@ define([
|
||||
actions,
|
||||
status,
|
||||
];
|
||||
if (data.inviteChannel) {
|
||||
if (data.hash) {
|
||||
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 +905,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);
|
||||
});
|
||||
@ -1349,7 +1382,7 @@ define([
|
||||
Messages._getKey('team_inviteFromMsg',
|
||||
[Util.fixHTML(getDisplayName(json.author.displayName)),
|
||||
Util.fixHTML(json.teamName)])));
|
||||
if (typeof(json.message) === 'string') {
|
||||
if (typeof(json.message) === 'string' && json.message) {
|
||||
var message = h('div.cp-teams-invite-message');
|
||||
json.message.split('\n').forEach(line => {
|
||||
if (line.trim()) {
|
||||
@ -1524,12 +1557,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
Block a user