|
|
|
@ -81,6 +81,7 @@ define([ |
|
|
|
try { team.listmap.stop(); } catch (e) {} |
|
|
|
try { team.roster.stop(); } catch (e) {} |
|
|
|
team.proxy = {}; |
|
|
|
team.stopped = true; |
|
|
|
delete ctx.teams[teamId]; |
|
|
|
delete ctx.store.proxy.teams[teamId]; |
|
|
|
ctx.emit('LEAVE_TEAM', teamId, team.clients); |
|
|
|
@ -140,8 +141,10 @@ define([ |
|
|
|
roster: roster |
|
|
|
}; |
|
|
|
|
|
|
|
// Subscribe to events
|
|
|
|
if (cId) { team.clients.push(cId); } |
|
|
|
|
|
|
|
// Listen for roster changes
|
|
|
|
roster.on('change', function () { |
|
|
|
var state = roster.getState(); |
|
|
|
var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']); |
|
|
|
@ -158,16 +161,19 @@ define([ |
|
|
|
rosterData.lastKnownHash = hash; |
|
|
|
}); |
|
|
|
|
|
|
|
// Update metadata
|
|
|
|
var state = roster.getState(); |
|
|
|
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]); |
|
|
|
if (teamData) { teamData.metadata = state.metadata; } |
|
|
|
|
|
|
|
// Broadcast an event to all the tabs displaying this team
|
|
|
|
team.sendEvent = function (q, data, sender) { |
|
|
|
ctx.emit(q, data, team.clients.filter(function (cId) { |
|
|
|
return cId !== sender; |
|
|
|
})); |
|
|
|
}; |
|
|
|
|
|
|
|
// Provide team chat keys to the messenger app
|
|
|
|
team.getChatData = function () { |
|
|
|
var chatKeys = keys.chat || {}; |
|
|
|
var hash = chatKeys.edit || chatKeys.view; |
|
|
|
@ -177,7 +183,7 @@ define([ |
|
|
|
teamId: id, |
|
|
|
channel: secret.channel, |
|
|
|
secret: secret, |
|
|
|
validateKey: secret.keys.validateKey |
|
|
|
validateKey: chatKeys.validateKey |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
@ -185,6 +191,7 @@ define([ |
|
|
|
team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; |
|
|
|
team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; |
|
|
|
nThen(function (waitFor) { |
|
|
|
// Init Team RPC
|
|
|
|
if (!keys.drive.edPrivate) { return; } |
|
|
|
initRpc(ctx, team, keys.drive, waitFor(function (err) { |
|
|
|
if (err) { return; } |
|
|
|
@ -208,6 +215,7 @@ define([ |
|
|
|
}; |
|
|
|
})); |
|
|
|
}).nThen(function () { |
|
|
|
// Create the proxy manager
|
|
|
|
var loadSharedFolder = function (id, data, cb) { |
|
|
|
SF.load({ |
|
|
|
network: ctx.store.network, |
|
|
|
@ -217,9 +225,8 @@ define([ |
|
|
|
}); |
|
|
|
}; |
|
|
|
var teamData = ctx.store.proxy.teams[team.id]; |
|
|
|
if (teamData) { |
|
|
|
secret = Hash.getSecrets('team', teamData.hash, teamData.password); |
|
|
|
} |
|
|
|
var hash = teamData.hash || teamData.roHash; |
|
|
|
secret = Hash.getSecrets('team', hash, teamData.password); |
|
|
|
var manager = team.manager = ProxyManager.create(proxy.drive, { |
|
|
|
onSync: function (cb) { ctx.Store.onSync(id, cb); }, |
|
|
|
edPublic: keys.drive.edPublic, |
|
|
|
@ -251,12 +258,14 @@ define([ |
|
|
|
team.sendEvent("DRIVE_LOG", msg); |
|
|
|
}, |
|
|
|
rt: team.realtime, |
|
|
|
editKey: secret && secret.keys.secondaryKey |
|
|
|
editKey: secret.keys.secondaryKey, |
|
|
|
readOnly: Boolean(!secret.keys.secondaryKey) |
|
|
|
}); |
|
|
|
team.secondaryKey = secret && secret.keys.secondaryKey; |
|
|
|
team.userObject = manager.user.userObject; |
|
|
|
team.userObject.fixFiles(); |
|
|
|
}).nThen(function (waitFor) { |
|
|
|
// Load the shared folders
|
|
|
|
ctx.teams[id] = team; |
|
|
|
registerChangeEvents(ctx, team, proxy); |
|
|
|
SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor()); |
|
|
|
@ -296,10 +305,20 @@ define([ |
|
|
|
var openChannel = function (ctx, teamData, id, _cb) { |
|
|
|
var cb = Util.once(_cb); |
|
|
|
|
|
|
|
var secret = Hash.getSecrets('team', teamData.hash, teamData.password); |
|
|
|
var hash = teamData.hash || teamData.roHash; |
|
|
|
var secret = Hash.getSecrets('team', hash, teamData.password); |
|
|
|
var crypto = Crypto.createEncryptor(secret.keys); |
|
|
|
|
|
|
|
if (!teamData.roHash) { |
|
|
|
teamData.roHash = Hash.getViewHashFromKeys(secret); |
|
|
|
} |
|
|
|
|
|
|
|
var keys = teamData.keys; |
|
|
|
if (!keys.chat.validateKey && keys.chat.edit) { |
|
|
|
var chatSecret = Hash.getSecrets('chat', keys.chat.edit); |
|
|
|
keys.chat.validateKey = chatSecret.keys.validateKey; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var roster; |
|
|
|
var lm; |
|
|
|
@ -373,6 +392,7 @@ define([ |
|
|
|
}, waitFor(function (err, _roster) { |
|
|
|
if (err) { |
|
|
|
waitFor.abort(); |
|
|
|
console.error(err); |
|
|
|
return void cb({error: 'ROSTER_ERROR'}); |
|
|
|
} |
|
|
|
roster = _roster; |
|
|
|
@ -421,6 +441,7 @@ define([ |
|
|
|
var password = Hash.createChannelId(); |
|
|
|
var hash = Hash.createRandomHash('team', password); |
|
|
|
var secret = Hash.getSecrets('team', hash, password); |
|
|
|
var roHash = Hash.getViewHashFromKeys(secret); |
|
|
|
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
|
|
|
|
|
|
|
|
var rosterSeed = Crypto.Team.createSeed(); |
|
|
|
@ -520,6 +541,7 @@ define([ |
|
|
|
chat: { |
|
|
|
edit: chatHashes.editHash, |
|
|
|
view: chatHashes.viewHash, |
|
|
|
validateKey: chatSecret.keys.validateKey, |
|
|
|
channel: chatSecret.channel |
|
|
|
}, |
|
|
|
roster: { |
|
|
|
@ -532,6 +554,7 @@ define([ |
|
|
|
owner: true, |
|
|
|
channel: secret.channel, |
|
|
|
hash: hash, |
|
|
|
roHash: roHash, |
|
|
|
password: password, |
|
|
|
keys: keys, |
|
|
|
//members: membersHashes.editHash,
|
|
|
|
@ -665,7 +688,7 @@ define([ |
|
|
|
|
|
|
|
var joinTeam = function (ctx, data, cId, cb) { |
|
|
|
var team = data.team; |
|
|
|
if (!team.hash || !team.channel || !team.password |
|
|
|
if (!(team.hash || team.roHash) || !team.channel || !team.password |
|
|
|
|| !team.keys || !team.metadata) { return void cb({error: 'EINVAL'}); } |
|
|
|
var id = Util.createRandomInteger(); |
|
|
|
ctx.store.proxy.teams[id] = team; |
|
|
|
@ -924,6 +947,92 @@ define([ |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
var getInviteData = function (ctx, teamId, edit) { |
|
|
|
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); |
|
|
|
if (!teamData) { return {}; } |
|
|
|
var data = Util.clone(teamData); |
|
|
|
if (!edit) { |
|
|
|
// Delete edit keys
|
|
|
|
delete data.hash; |
|
|
|
delete data.keys.drive.edPrivate; |
|
|
|
delete data.keys.chat.edit; |
|
|
|
} |
|
|
|
// Delete owner key
|
|
|
|
delete data.owner; |
|
|
|
return data; |
|
|
|
}; |
|
|
|
|
|
|
|
// Update my edit rights in listmap (only upgrade) and userObject (upgrade and downgrade)
|
|
|
|
// We also need to propagate the changes to the shared folders
|
|
|
|
var updateMyRights = function (ctx, teamId, hash) { |
|
|
|
if (!teamId) { return true; } |
|
|
|
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); |
|
|
|
if (!teamData) { return true; } |
|
|
|
var team = ctx.teams[teamId]; |
|
|
|
|
|
|
|
var secret = Hash.getSecrets('team', hash || teamData.roHash, teamData.password); |
|
|
|
// Upgrade the listmap if we can
|
|
|
|
SF.upgrade(teamData.channel, secret); |
|
|
|
// Set the new readOnly value in userObject
|
|
|
|
if (team.userObject) { |
|
|
|
team.userObject.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey); |
|
|
|
} |
|
|
|
|
|
|
|
// Upgrade the shared folders
|
|
|
|
var folders = Util.find(team, ['proxy', 'drive', 'sharedFolders']); |
|
|
|
Object.keys(folders || {}).forEach(function (sfId) { |
|
|
|
var data = team.manager.getSharedFolderData(sfId); |
|
|
|
var parsed = Hash.parsePadUrl(data.href || data.roHref); |
|
|
|
var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password); |
|
|
|
SF.upgrade(secret.channel, secret); |
|
|
|
var uo = Util.find(team, ['manager', 'folders', sfId, 'userObject']); |
|
|
|
if (uo) { |
|
|
|
uo.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey); |
|
|
|
} |
|
|
|
}); |
|
|
|
ctx.emit('ROSTER_CHANGE_RIGHTS', teamId, team.clients); |
|
|
|
}; |
|
|
|
|
|
|
|
var changeMyRights = function (ctx, teamId, state, data) { |
|
|
|
if (!teamId) { return true; } |
|
|
|
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); |
|
|
|
if (!teamData) { return true; } |
|
|
|
var team = ctx.teams[teamId]; |
|
|
|
if (!team) { return true; } |
|
|
|
|
|
|
|
if (teamData.channel !== data.channel || teamData.password !== data.password) { return true; } |
|
|
|
|
|
|
|
if (state) { |
|
|
|
teamData.hash = data.hash; |
|
|
|
teamData.keys.drive.edPrivate = data.keys.drive.edPrivate; |
|
|
|
teamData.keys.chat.edit = data.keys.chat.edit; |
|
|
|
} else { |
|
|
|
delete teamData.hash; |
|
|
|
delete teamData.keys.drive.edPrivate; |
|
|
|
delete teamData.keys.chat.edit; |
|
|
|
} |
|
|
|
|
|
|
|
updateMyRights(ctx, teamId, data.hash); |
|
|
|
}; |
|
|
|
var changeEditRights = function (ctx, teamId, user, state, 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'}); } |
|
|
|
|
|
|
|
// Send mailbox to offer ownership
|
|
|
|
var myData = Messaging.createData(ctx.store.proxy, false); |
|
|
|
ctx.store.mailbox.sendTo("TEAM_EDIT_RIGHTS", { |
|
|
|
state: state, |
|
|
|
teamData: getInviteData(ctx, teamId, state), |
|
|
|
user: myData |
|
|
|
}, { |
|
|
|
channel: user.notifications, |
|
|
|
curvePublic: user.curvePublic |
|
|
|
}, cb); |
|
|
|
}; |
|
|
|
|
|
|
|
var describeUser = function (ctx, data, cId, cb) { |
|
|
|
var teamId = data.teamId; |
|
|
|
if (!teamId) { return void cb({error: 'EINVAL'}); } |
|
|
|
@ -937,13 +1046,27 @@ define([ |
|
|
|
// It it is an ownership revocation, we have to set it in pad metadata first
|
|
|
|
if (user.role === "OWNER" && data.data.role !== "OWNER") { |
|
|
|
revokeOwnership(ctx, teamId, user, function (err) { |
|
|
|
if (!err) { return; } |
|
|
|
if (!err) { return void cb(); } |
|
|
|
console.error(err); |
|
|
|
return void cb({error: err}); |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Viewer to editor
|
|
|
|
if (user.role === "VIEWER" && data.data.role !== "VIEWER") { |
|
|
|
return void changeEditRights(ctx, teamId, user, true, function (err) { |
|
|
|
return void cb({error: err}); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Editor to viewer
|
|
|
|
if (user.role !== "VIEWER" && data.data.role === "VIEWER") { |
|
|
|
return void changeEditRights(ctx, teamId, user, false, function (err) { |
|
|
|
return void cb({error: err}); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
var obj = {}; |
|
|
|
obj[data.curvePublic] = data.data; |
|
|
|
team.roster.describe(obj, function (err) { |
|
|
|
@ -952,15 +1075,6 @@ define([ |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// TODO send guest keys only in the future
|
|
|
|
var getInviteData = function (ctx, teamId) { |
|
|
|
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); |
|
|
|
if (!teamData) { return {}; } |
|
|
|
var data = Util.clone(teamData); |
|
|
|
delete data.owner; |
|
|
|
return data; |
|
|
|
}; |
|
|
|
|
|
|
|
var inviteToTeam = function (ctx, data, cId, cb) { |
|
|
|
var teamId = data.teamId; |
|
|
|
if (!teamId) { return void cb({error: 'EINVAL'}); } |
|
|
|
@ -975,6 +1089,7 @@ define([ |
|
|
|
|
|
|
|
var obj = {}; |
|
|
|
obj[user.curvePublic] = user; |
|
|
|
obj[user.curvePublic].role = 'VIEWER'; |
|
|
|
team.roster.add(obj, function (err) { |
|
|
|
if (err && err !== 'NO_CHANGE') { return void cb({error: err}); } |
|
|
|
ctx.store.mailbox.sendTo('INVITE_TO_TEAM', { |
|
|
|
@ -1100,9 +1215,21 @@ define([ |
|
|
|
if (err) { return; } |
|
|
|
})); |
|
|
|
|
|
|
|
// Listen for changes in our access rights (if another worker receives edit access)
|
|
|
|
ctx.store.proxy.on('change', ['teams'], function (o, n, p) { |
|
|
|
if (p[2] !== 'hash') { return; } |
|
|
|
updateMyRights(ctx, p[1], n); |
|
|
|
}); |
|
|
|
ctx.store.proxy.on('remove', ['teams'], function (o, p) { |
|
|
|
if (p[2] !== 'hash') { return; } |
|
|
|
updateMyRights(ctx, p[1]); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
Object.keys(teams).forEach(function (id) { |
|
|
|
ctx.onReadyHandlers[id] = []; |
|
|
|
openChannel(ctx, teams[id], id, waitFor(function () { |
|
|
|
openChannel(ctx, teams[id], id, waitFor(function (err) { |
|
|
|
if (err) { return void console.error(err); } |
|
|
|
console.debug('Team '+id+' ready'); |
|
|
|
})); |
|
|
|
}); |
|
|
|
@ -1121,7 +1248,7 @@ define([ |
|
|
|
edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']), |
|
|
|
avatar: Util.find(teams[id], ['metadata', 'avatar']) |
|
|
|
}; |
|
|
|
if (safe) { |
|
|
|
if (safe && ctx.teams[id]) { |
|
|
|
t[id].secondaryKey = ctx.teams[id].secondaryKey; |
|
|
|
} |
|
|
|
}); |
|
|
|
@ -1147,6 +1274,9 @@ define([ |
|
|
|
}); |
|
|
|
|
|
|
|
}; |
|
|
|
team.changeMyRights = function (id, edit, teamData) { |
|
|
|
changeMyRights(ctx, id, edit, teamData); |
|
|
|
}; |
|
|
|
team.updateMyData = function (data) { |
|
|
|
Object.keys(ctx.teams).forEach(function (id) { |
|
|
|
var team = ctx.teams[id]; |
|
|
|
|