Transfer team ownership

pull/1/head
yflory 5 years ago
parent 3fb0cc38ec
commit 295a712942

@ -288,6 +288,7 @@ define([
} }
})); }));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
// Add one of our teams as an owner
if (toAddTeams.length) { if (toAddTeams.length) {
// Send the command // Send the command
sframeChan.query('Q_SET_PAD_METADATA', { sframeChan.query('Q_SET_PAD_METADATA', {
@ -320,6 +321,7 @@ define([
})); }));
} }
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
// Offer ownership to a friend
if (toAdd.length) { if (toAdd.length) {
// Send the command // Send the command
sframeChan.query('Q_SET_PAD_METADATA', { sframeChan.query('Q_SET_PAD_METADATA', {
@ -1293,7 +1295,7 @@ define([
var team = privateData.teams[config.teamId]; var team = privateData.teams[config.teamId];
if (!team) { return void UI.warn(Messages.error); } if (!team) { return void UI.warn(Messages.error); }
var module = config.module || common.makeUniversal('team', { onEvent: function () {} }); var module = config.module || common.makeUniversal('team');
var $div; var $div;
var refreshButton = function () { var refreshButton = function () {
@ -3785,6 +3787,130 @@ define([
UI.proposal(div, todo); UI.proposal(div, todo);
}; };
UIElements.displayAddTeamOwnerModal = function (common, data) {
var priv = common.getMetadataMgr().getPrivateData();
var user = common.getMetadataMgr().getUserData();
var sframeChan = common.getSframeChannel();
var msg = data.content.msg;
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
//var text = Messages._getKey('owner_team_add', [name, title]); // XXX
var text = name + ' wants you to be an owner of the team ' + title; // XXX
var div = h('div', [
UI.setHTML(h('p'), text),
]);
var answer = function (yes) {
common.mailbox.sendTo("ADD_OWNER_ANSWER", {
teamChannel: msg.content.teamChannel,
title: msg.content.title,
answer: yes,
user: {
displayName: user.name,
avatar: user.avatar,
profile: user.profile,
notifications: user.notifications,
curvePublic: user.curvePublic,
edPublic: priv.edPublic
}
}, {
channel: msg.content.user.notifications,
curvePublic: msg.content.user.curvePublic
});
common.mailbox.dismiss(data, function (err) {
if (err) { console.log(err); }
});
};
var module = common.makeUniversal('team');
var addOwner = function (chan, waitFor, cb) {
// Remove yourself from the pending owners
sframeChan.query('Q_SET_PAD_METADATA', {
channel: chan,
command: 'ADD_OWNERS',
value: [priv.edPublic]
}, function (err, res) {
err = err || (res && res.error);
if (!err) { return; }
waitFor.abort();
cb(err);
});
};
var removePending = function (chan, waitFor, cb) {
// Remove yourself from the pending owners
sframeChan.query('Q_SET_PAD_METADATA', {
channel: chan,
command: 'RM_PENDING_OWNERS',
value: [priv.edPublic]
}, waitFor(function (err, res) {
err = err || (res && res.error);
if (!err) { return; }
waitFor.abort();
cb(err);
}));
};
var changeAll = function (add, _cb) {
var f = add ? addOwner : removePending;
var cb = Util.once(_cb);
NThen(function (waitFor) {
f(msg.content.teamChannel, waitFor, cb);
f(msg.content.chatChannel, waitFor, cb);
f(msg.content.rosterChannel, waitFor, cb);
}).nThen(function () { cb(); });
};
var todo = function (yes) {
if (yes) {
// ACCEPT
changeAll(true, function (err) {
if (err) {
console.error(err);
var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden
: Messages.error;
return void UI.warn(text);
}
UI.log(Messages.saved);
// Send notification to the sender
answer(true);
// Mark ourselves as "owner" in our local team data
module.execCommand("ANSWER_OWNERSHIP", {
teamChannel: msg.content.teamChannel,
answer: true
}, function (obj) {
if (obj && obj.error) { console.error(obj.error); }
});
// Remove yourself from the pending owners
changeAll(false, function (err) {
if (err) { console.error(err); }
});
});
return;
}
// DECLINE
// Remove yourself from the pending owners
changeAll(false, function (err) {
if (err) { console.error(err); }
// Send notification to the sender
answer(false);
// Set our role back to ADMIN
module.execCommand("ANSWER_OWNERSHIP", {
teamChannel: msg.content.teamChannel,
answer: false
}, function (obj) {
if (obj && obj.error) { console.error(obj.error); }
});
});
};
UI.proposal(div, todo);
};
UIElements.getVerifiedFriend = function (common, curve, name) { UIElements.getVerifiedFriend = function (common, curve, name) {
var priv = common.getMetadataMgr().getPrivateData(); var priv = common.getMetadataMgr().getPrivateData();

@ -216,6 +216,9 @@ define([
// if not archived, add handlers // if not archived, add handlers
if (!content.archived) { if (!content.archived) {
content.handler = function () { content.handler = function () {
if (msg.content.teamChannel) {
return void UIElements.displayAddTeamOwnerModal(common, data);
}
UIElements.displayAddOwnerModal(common, data); UIElements.displayAddOwnerModal(common, data);
}; };
} }

@ -270,12 +270,12 @@ define([
var content = msg.content; var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); } if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (!content.href || !content.title || !content.channel) { if (!content.teamChannel && !(content.href && content.title && content.channel)) {
console.log('Remove invalid notification'); console.log('Remove invalid notification');
return void cb(true); return void cb(true);
} }
var channel = content.channel; var channel = content.channel || content.teamChannel;
if (addOwners[channel]) { return void cb(true); } if (addOwners[channel]) { return void cb(true); }
addOwners[channel] = { addOwners[channel] = {
@ -286,7 +286,7 @@ define([
cb(false); cb(false);
}; };
removeHandlers['ADD_OWNER'] = function (ctx, box, data) { removeHandlers['ADD_OWNER'] = function (ctx, box, data) {
var channel = data.content.channel; var channel = data.content.channel || data.content.teamChannel;
if (addOwners[channel]) { if (addOwners[channel]) {
delete addOwners[channel]; delete addOwners[channel];
} }
@ -297,12 +297,23 @@ define([
var content = msg.content; var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); } if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (!content.channel) { if (!content.channel && !content.teamChannel) {
console.log('Remove invalid notification'); console.log('Remove invalid notification');
return void cb(true); return void cb(true);
} }
var channel = content.channel; var channel = content.channel || content.teamChannel;
// If our ownership rights for a team have been removed, update the owner flag
if (content.teamChannel) {
var teams = ctx.store.proxy.teams || {};
Object.keys(teams).some(function (id) {
if (teams[id].channel === channel) {
teams[id].owner = false;
return true;
}
});
}
if (addOwners[channel] && content.pending) { if (addOwners[channel] && content.pending) {
return void cb(false, addOwners[channel]); return void cb(false, addOwners[channel]);

@ -100,18 +100,6 @@ define([
if (membersChannel) { list.push(membersChannel); } if (membersChannel) { list.push(membersChannel); }
if (mailboxChannel) { list.push(mailboxChannel); } if (mailboxChannel) { list.push(mailboxChannel); }
// XXX Add the team mailbox
/*
if (store.proxy.mailboxes) {
var mList = Object.keys(store.proxy.mailboxes).map(function (m) {
return store.proxy.mailboxes[m].channel;
});
list = list.concat(mList);
}
*/
list.sort(); list.sort();
return list; return list;
}; };
@ -186,7 +174,6 @@ define([
channel: secret.channel, channel: secret.channel,
secret: secret, secret: secret,
validateKey: secret.keys.validateKey validateKey: secret.keys.validateKey
// XXX owners: team owner + all admins?
}; };
}; };
@ -314,6 +301,11 @@ define([
userName: 'team', userName: 'team',
classic: true classic: true
}; };
cfg.onMetadataUpdate = function (md) {
var team = ctx.teams[id];
if (!team) { return; }
ctx.emit('ROSTER_CHANGE', id, team.clients);
};
lm = Listmap.create(cfg); lm = Listmap.create(cfg);
lm.proxy.on('ready', waitFor()); lm.proxy.on('ready', waitFor());
@ -463,10 +455,14 @@ define([
} }
})); }));
}).nThen(function () { }).nThen(function () {
var id = Util.createRandomInteger();
config.onMetadataUpdate = function (md) {
if (!team) { return; }
ctx.emit('ROSTER_CHANGE', id, team.clients);
};
var lm = Listmap.create(config); var lm = Listmap.create(config);
var proxy = lm.proxy; var proxy = lm.proxy;
proxy.on('ready', function () { proxy.on('ready', function () {
var id = Util.createRandomInteger();
// Store keys in our drive // Store keys in our drive
var keys = { var keys = {
drive: { drive: {
@ -617,12 +613,43 @@ define([
var getTeamRoster = function (ctx, data, cId, cb) { var getTeamRoster = function (ctx, data, cId, cb) {
var teamId = data.teamId; var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); } if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId]; var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); } if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var state = team.roster.getState() || {}; var state = team.roster.getState() || {};
var members = state.members || {}; var members = state.members || {};
// Get pending owners
var md = team.listmap.metadata || {};
if (Array.isArray(md.pending_owners)) {
// Get the members associated to the pending_owners' edPublic and mark them as such
md.pending_owners.forEach(function (ed) {
var member;
Object.keys(members).some(function (curve) {
if (members[curve].edPublic === ed) {
member = members[curve];
return true;
}
});
if ((!member || member.role !== 'OWNER') && teamData.owner) {
var removeOwnership = function (chan) {
ctx.Store.setPadMetadata(null, {
channel: chan,
command: 'RM_PENDING_OWNERS',
value: [ed],
}, function () {});
};
removeOwnership(teamData.channel);
removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel']));
removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel']));
return;
}
member.pendingOwner = true;
});
}
// Add online status (using messenger data) // Add online status (using messenger data)
var chatData = team.getChatData(); var chatData = team.getChatData();
var online = ctx.store.messenger.getOnlineList(chatData.channel) || []; var online = ctx.store.messenger.getOnlineList(chatData.channel) || [];
@ -661,6 +688,159 @@ define([
}); });
}; };
var offerOwnership = function (ctx, data, cId, _cb) {
var cb = Util.once(_cb);
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
if (!data.curvePublic) { return void cb({error: 'MISSING_DATA'}); }
var state = team.roster.getState();
var user = state.members[data.curvePublic];
nThen(function (waitFor) {
// Offer ownership to a friend
var onError = function (res) {
var err = res && res.error;
if (err) {
waitFor.abort();
return void cb({error:err});
}
};
var addPendingOwner = function (chan) {
ctx.Store.setPadMetadata(null, {
channel: chan,
command: 'ADD_PENDING_OWNERS',
value: [user.edPublic],
}, waitFor(onError));
};
// Team proxy
addPendingOwner(teamData.channel);
// Team roster
addPendingOwner(Util.find(teamData, ['keys', 'roster', 'channel']));
// Team chat
addPendingOwner(Util.find(teamData, ['keys', 'chat', 'channel']));
}).nThen(function (waitFor) {
var obj = {};
obj[user.curvePublic] = {
role: 'OWNER'
};
team.roster.describe(obj, waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function (waitFor) {
// Send mailbox to offer ownership
var myData = Messaging.createData(ctx.store.proxy, false);
ctx.store.mailbox.sendTo("ADD_OWNER", {
teamChannel: teamData.channel,
chatChannel: Util.find(teamData, ['keys', 'chat', 'channel']),
rosterChannel: Util.find(teamData, ['keys', 'roster', 'channel']),
title: teamData.metadata.name,
user: myData
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, waitFor());
}).nThen(function () {
cb();
});
};
var revokeOwnership = function (ctx, teamId, user, _cb) {
var cb = Util.once(_cb);
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
var md = team.listmap.metadata || {};
var isPendingOwner = (md.pending_owners || []).indexOf(user.edPublic) !== -1;
nThen(function (waitFor) {
var cmd = isPendingOwner ? 'RM_PENDING_OWNERS' : 'RM_OWNERS';
var onError = function (res) {
var err = res && res.error;
if (err) {
waitFor.abort();
return void cb(err);
}
};
var removeOwnership = function (chan) {
ctx.Store.setPadMetadata(null, {
channel: chan,
command: cmd,
value: [user.edPublic],
}, waitFor(onError));
};
// Team proxy
removeOwnership(teamData.channel);
// Team roster
removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel']));
// Team chat
removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel']));
}).nThen(function (waitFor) {
var obj = {};
obj[user.curvePublic] = {
role: 'ADMIN',
pendingOwner: false
};
team.roster.describe(obj, waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function (waitFor) {
// Send mailbox to offer ownership
var myData = Messaging.createData(ctx.store.proxy, false);
ctx.store.mailbox.sendTo("RM_OWNER", {
teamChannel: teamData.channel,
title: teamData.metadata.name,
pending: isPendingOwner,
user: myData
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, waitFor());
}).nThen(function () {
cb();
});
};
// We've received an offer to be an owner of the team.
// If we accept, we need to set the "owner" flag in our team data
// If we decline, we need to change our role back to "ADMIN"
var answerOwnership = function (ctx, data, cId, cb) {
var myTeams = ctx.store.proxy.teams;
var teamId;
Object.keys(myTeams).forEach(function (id) {
if (myTeams[id].channel === data.teamChannel) {
teamId = id;
return true;
}
});
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var obj = {};
// Accept
if (data.answer) {
teamData.owner = true;
return;
}
// Decline
obj[ctx.store.proxy.curvePublic] = {
role: 'ADMIN',
};
team.roster.describe(obj, function (err) {
if (err) { return void cb({error: err}); }
cb();
});
};
var describeUser = function (ctx, data, cId, cb) { var describeUser = function (ctx, data, cId, cb) {
var teamId = data.teamId; var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); } if (!teamId) { return void cb({error: 'EINVAL'}); }
@ -668,6 +848,21 @@ define([
if (!team) { return void cb ({error: 'ENOENT'}); } if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
if (!data.curvePublic || !data.data) { return void cb({error: 'MISSING_DATA'}); } if (!data.curvePublic || !data.data) { return void cb({error: 'MISSING_DATA'}); }
var state = team.roster.getState();
var user = state.members[data.curvePublic];
// It it is an ownership revocation, we have to set it in pad metadata first
console.log(user.role, data.data.role);
if (user.role === "OWNER" && data.data.role !== "OWNER") {
revokeOwnership(ctx, teamId, user, function (err) {
console.error(err);
if (!err) { return; }
waitFor.abort();
return void cb({error: err});
});
return;
}
var obj = {}; var obj = {};
obj[data.curvePublic] = data.data; obj[data.curvePublic] = data.data;
team.roster.describe(obj, function (err) { team.roster.describe(obj, function (err) {
@ -902,6 +1097,12 @@ define([
if (cmd === 'SET_TEAM_METADATA') { if (cmd === 'SET_TEAM_METADATA') {
return void setTeamMetadata(ctx, data, clientId, cb); return void setTeamMetadata(ctx, data, clientId, cb);
} }
if (cmd === 'OFFER_OWNERSHIP') {
return void offerOwnership(ctx, data, clientId, cb);
}
if (cmd === 'ANSWER_OWNERSHIP') {
return void answerOwnership(ctx, data, clientId, cb);
}
if (cmd === 'DESCRIBE_USER') { if (cmd === 'DESCRIBE_USER') {
return void describeUser(ctx, data, clientId, cb); return void describeUser(ctx, data, clientId, cb);
} }

@ -430,6 +430,9 @@ define([
common.displayAvatar($(avatar), data.avatar, data.displayName); common.displayAvatar($(avatar), data.avatar, data.displayName);
// Name // Name
var name = h('span.cp-team-member-name', data.displayName); var name = h('span.cp-team-member-name', data.displayName);
if (data.pendingOwner) {
$(name).append(h('em', " PENDING"));
}
// Status // Status
var status = h('span.cp-team-member-status'+(data.online ? '.online' : '')); var status = h('span.cp-team-member-status'+(data.online ? '.online' : ''));
// Actions // Actions
@ -438,6 +441,28 @@ define([
var isMe = me && me.curvePublic === data.curvePublic; var isMe = me && me.curvePublic === data.curvePublic;
var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1; var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1;
var theirRole = ROLES.indexOf(data.role) || 0; var theirRole = ROLES.indexOf(data.role) || 0;
// If they're an admin and I am an owner, I can promote them to owner
if (!isMe && myRole > theirRole && theirRole === 1 && !data.pending) {
var promote = h('span.fa.fa-angle-double-up', {
title: "Offer ownership" // XXX
});
$(promote).click(function () {
$(promote).hide();
UI.confirm("Are you sure???", function (yes) { // XXX
APP.module.execCommand('OFFER_OWNERSHIP', {
teamId: APP.team,
curvePublic: data.curvePublic
}, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
return void UI.warn(Messages.error);
}
UI.log("DONE"); // XXX
});
});
});
$actions.append(promote);
}
// If they're a member and I have a higher role than them, I can promote them to admin // If they're a member and I have a higher role than them, I can promote them to admin
if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) { if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) {
var promote = h('span.fa.fa-angle-double-up', { var promote = h('span.fa.fa-angle-double-up', {
@ -467,7 +492,8 @@ define([
$actions.append(demote); $actions.append(demote);
} }
// If I'm not a member and I have an equal or higher role than them, I can remove them // If I'm not a member and I have an equal or higher role than them, I can remove them
if (!isMe && myRole > 0 && myRole >= theirRole) { // Note: we can't remove owners, we have to demote them first
if (!isMe && myRole > 0 && myRole >= theirRole && theirRole !== 2) {
var remove = h('span.fa.fa-times', { var remove = h('span.fa.fa-times', {
title: Messages.team_rosterKick title: Messages.team_rosterKick
}); });
@ -514,7 +540,7 @@ define([
var me = roster[userData.curvePublic] || {}; var me = roster[userData.curvePublic] || {};
var owner = Object.keys(roster).filter(function (k) { var owner = Object.keys(roster).filter(function (k) {
if (roster[k].pending) { return; } if (roster[k].pending) { return; }
return roster[k].role === "OWNER"; return roster[k].role === "OWNER" || roster[k].pendingOwner;
}).map(function (k) { }).map(function (k) {
return makeMember(common, roster[k], me); return makeMember(common, roster[k], me);
}); });

Loading…
Cancel
Save