cryptpad/www/common/outer/team.js

365 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/common-realtime.js',
'/common/proxy-manager.js',
'/common/outer/sharedfolder.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
'/bower_components/nthen/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, Constants, Realtime,
ProxyManager, SF,
Listmap, Crypto, ChainPad, nThen) {
var Team = {};
var Nacl = window.nacl;
var initializeTeams = function (ctx, cb) {
// XXX ?
cb();
};
var handleSharedFolder = function (ctx, id, sfId, rt) {
var t = ctx.teams[id];
if (!t) { return; }
t.sharedFolders[sfId] = rt;
// XXX register events
// rt.proxy.on('change',... emit change event
// TODO: pin or unpin document added to a shared folder from someone who is not a member of the team
};
var initRpc = function (ctx, team, data, cb) {
if (team.rpc) { return void cb(); }
if (!data.edPrivate || !data.edPublic) { return void cb('EFORBIDDEN'); }
require(['/common/pinpad.js'], function (Pinpad) {
Pinpad.create(ctx.store.network, data, function (e, call) {
if (e) { return void cb(e); }
team.rpc = call;
cb();
// XXX get pin limit?
});
});
};
var onReady = function (ctx, id, lm, keys, cb) {
// XXX
// sanity check: do we have all the required keys?
// [x] initialize team rpc with pin, unpin, ...
// [x] team.rpc = rpc
// [x] load manager with userObject
// team.manager =... team.userObject = ....
// [ ] load members pad
// [ ] load shared folders
// [ ] register listmap 'change' events for the drive and the shared folders
// [ ] ~resetPins for the team?
// [ ] getPinLimit
var proxy = lm.proxy;
var team = {
id: id,
proxy: proxy,
listmap: lm,
clients: [],
realtime: lm.realtime,
handleSharedFolder: function (sfId, rt) { handleSharedFolder(ctx, id, sfId, rt); },
sharedFolders: {}, // equivalent of store.sharedFolders in async-store
};
team.sendEvent = function (q, data, sender) {
ctx.emit(q, data, team.clients.filter(function (cId) {
return cId !== sender;
}));
};
var pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
var unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
nThen(function (waitFor) {
if (!keys.edPrivate) { return; }
initRpc(ctx, team, keys, waitFor(function (err) {
if (err) { return; }
pin = function (data, cb) {
if (!team.rpc) { return void cb({error: 'TEAM_RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') { console.error('expected a callback'); }
team.rpc.pin(data, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
unpin = function (data, cb) {
if (!team.rpc) { return void cb({error: 'TEAM_RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') { console.error('expected a callback'); }
team.rpc.unpin(data, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
}));
}).nThen(function () {
var loadSharedFolder = function (id, data, cb) {
SF.load({
network: ctx.store.network,
store: team
}, id, data, cb);
};
var manager = team.manager = ProxyManager.create(proxy.drive, {
onSync: function (cb) { ctx.Store.onSync(id, cb); },
edPublic: proxy.edPublic,
pin: pin,
unpin: unpin,
loadSharedFolder: loadSharedFolder,
settings: {
drive: Util.find(ctx.store, ['proxy', 'settings', 'drive'])
}
}, {
outer: true,
removeOwnedChannel: function (channel, cb) {
var data;
if (typeof(channel) === "object") {
channel.teamId = id;
data = channel;
} else {
data = {
channel: channel,
teamId: id
};
}
ctx.Store.removeOwnedChannel('', data, cb);
},
edPublic: proxy.edPublic,
loggedIn: true,
log: function (msg) {
// broadcast to all drive apps
team.sendEvent("DRIVE_LOG", msg);
}
});
team.userObject = manager.user.userObject;
team.userObject.fixFiles();
}).nThen(function (waitFor) {
// XXX
// Load shared folders
// Load members pad
console.log('ok', waitFor);
}).nThen(function () {
ctx.teams[id] = team;
if (ctx.onReadyHandlers[id]) {
ctx.onReadyHandlers[id].forEach(function (obj) {
// Callback and subscribe the client to new notifications
if (typeof (obj.cb) === "function") { obj.cb(); }
if (!obj.cId) { return; }
var idx = team.clients.indexOf(obj.cId);
if (idx === -1) {
team.clients.push(obj.cId);
}
});
}
delete ctx.onReadyHandlers[id];
cb();
});
};
var openChannel = function (ctx, teamData, id, cb) {
var secret = Hash.getSecrets('team', teamData.hash, teamData.password);
var crypto = Crypto.createEncryptor(secret.keys);
var cfg = {
data: {},
readOnly: !Boolean(secret.keys.signKey),
network: ctx.store.network,
channel: secret.channel,
crypto: crypto,
ChainPad: ChainPad,
metadata: {
validateKey: secret.keys.validateKey || undefined,
},
userName: 'team',
classic: true
};
var lm = Listmap.create(cfg);
lm.proxy.on('create', function () {
}).on('ready', function () {
onReady(ctx, id, lm, teamData.keys, cb);
});
};
var createTeam = function (ctx, data, cId, _cb) {
var cb = Util.once(_cb);
var password = Hash.createChannelId();
var hash = Hash.createRandomHash('team', password);
var secret = Hash.getSecrets('team', hash, password);
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
var membersSecret = Hash.getSecrets('members');
var membersHashes = Hash.getHashes(membersSecret);
var config = {
network: ctx.store.network,
channel: secret.channel,
data: {},
validateKey: secret.keys.validateKey, // derived validation key
crypto: Crypto.createEncryptor(secret.keys),
logLevel: 1,
classic: true,
ChainPad: ChainPad,
owners: [ctx.store.proxy.edPublic]
};
nThen(function (waitFor) {
console.log('pin..');
ctx.pinPads([secret.channel, membersSecret.channel], waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
}));
// XXX initialize the members channel with yourself, and mark it as owned!
}).nThen(function () {
console.log('init proxy');
var lm = Listmap.create(config);
var proxy = lm.proxy;
proxy.on('ready', function () {
console.log('ready');
// Store keys in our drive
var id = Util.createRandomInteger();
var keys = {
edPrivate: Nacl.util.encodeBase64(keyPair.secretKey),
edPublic: Nacl.util.encodeBase64(keyPair.publicKey)
};
ctx.store.proxy.teams[id] = {
hash: hash,
password: password,
keys: keys,
members: membersHashes.editHash,
name: data.name
};
// Initialize the team drive
proxy.drive = {};
// Create metadata
proxy.metadata = {
name: name,
members: membersHashes.viewHash,
};
// Add rpc key
proxy.edPublic = Nacl.util.encodeBase64(keyPair.publicKey);
onReady(ctx, id, lm, {
edPrivate: keyPair.secretKey,
edPublic: keyPair.publicKey
}, function () {
cb();
});
}).on('error', function (info) {
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
cb({error:'ECONNECT'});
}
});
});
};
var subscribe = function (ctx, id, cId, cb) {
// If the team is loading, as ourselves in the list
if (ctx.onReadyHandlers[id]) {
var _idx = ctx.onReadyHandlers[id].indexOf(cId);
if (_idx === -1) {
ctx.onReadyHandlers[id].push({
cId: cId,
cb: cb
});
}
return;
}
// Otherwise, subscribe to new notifications
if (!id || !ctx.teams[id]) {
return void cb({error: 'EINVAL'});
}
var clients = ctx.teams[id].clients;
var idx = clients.indexOf(cId);
if (idx === -1) {
clients.push(cId);
}
cb();
};
// Remove a client from all the team they're subscribed to
var removeClient = function (ctx, cId) {
Object.keys(ctx.teams).forEach(function (id) {
// Remove from the subscribers
var clients = ctx.teams[id].clients;
var idx = clients.indexOf(cId);
if (idx !== -1) { clients.splice(idx, 1); }
// And remove from the onReady handlers in case they haven't finished loading
if (ctx.onReadyHandlers[id]) {
var idx2 = ctx.onReadyHandlers.indexOf(cId);
if (idx2 !== -1) { ctx.onReadyHandlers.splice(idx2, 1); }
}
});
};
Team.init = function (cfg, waitFor, emit) {
var team = {};
var store = cfg.store;
if (!store.loggedIn || !store.proxy.edPublic) { return; }
var ctx = {
store: store,
Store: cfg.Store,
pinPads: cfg.pinPads,
emit: emit,
onReadyHandlers: {},
teams: {}
};
var teams = store.proxy.teams = store.proxy.teams || {};
initializeTeams(ctx, waitFor(function (err) {
if (err) { return; }
}));
Object.keys(teams).forEach(function (id) {
ctx.onReadyHandlers[id] = [];
openChannel(ctx, teams[id], id, waitFor(function () {
console.error('team '+id+' ready');
}));
});
team.getTeam = function (id) {
return ctx.teams[id];
};
team.getTeams = function () {
return Object.keys(ctx.teams);
};
team.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
team.execCommand = function (clientId, obj, cb) {
console.log(obj);
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SUBSCRIBE') {
// Only the team app will subscribe to events?
return void subscribe(ctx, data, clientId, cb);
}
if (cmd === 'LIST_TEAMS') {
return void cb(store.proxy.teams);
}
if (cmd === 'CREATE_TEAM') {
return void createTeam(ctx, data, clientId, cb);
}
};
return team;
};
return Team;
});