Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for deleting decks and transferring ownership #149

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions application/controllers/deckTrees.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,19 +525,58 @@ const self = module.exports = {

},

removeDeckTreeNode: function(request, reply) {
removeDeckTreeNode: async function(request, reply) {
let userId = request.auth.credentials.userid;
let rootId = request.payload.selector.id;

// determine the info for the node to remove
let node = parseNodeToRemove(request.payload.selector);
try {
// check permissions for root deck
let perms = await deckDB.userPermissions(rootId, userId);
if (!perms) throw boom.badData(`could not find deck tree ${rootId}`);
if (!perms.edit) throw boom.forbidden();

// determine the info for the node to remove
let node = parseNodeToRemove(request.payload.selector);
if (node.kind !== 'deck' || !request.payload.purge) {
// just remove it!
return reply(await treeDB.removeDeckTreeNode(rootId, node, userId));
}

// purging
// find the node from database
let treeNode = await treeDB.findDeckTreeNode(rootId, node.id, node.kind);
if (!treeNode) throw boom.badData(`could not find ${node.kind}: ${node.id} in deck tree: ${rootId}`);

let deckId = util.toIdentifier(treeNode.ref);

// just remove it!
return treeDB.removeDeckTreeNode(rootId, node, userId).then(reply).catch((err) => {
// check permissions for deck as well
perms = await deckDB.userPermissions(deckId, userId);
// perms should exist unless concurrent edits, let's own this error (5xx instead of 4xx)
if (!perms.admin) throw boom.forbidden(`cannot purge deck: ${deckId}, user ${userId} is not deck admin`);

let deck = await deckDB.getDeck(deckId);
// check if deck is used elsewhere
let otherUsage = _.reject(deck.usage, util.parseIdentifier(treeNode.parentId));
if (_.size(otherUsage)) {
throw boom.badData(`cannot purge deck: ${deckId} from deck tree: ${rootId}, as it is also used in decks [${otherUsage.map(util.toIdentifier).join(',')}]`);
}

// also check if deck has subdecks
if (_.find(deck.contentItems, { kind: 'deck' })) {
throw boom.badData(`cannot purge deck: ${deckId}, as it has subdecks`);
}

// do the remove and then purge
let removed = await treeDB.removeDeckTreeNode(rootId, node, userId);
await deckDB.adminUpdate(deckId, { user: -1 });

reply(removed);

} catch (err) {
if (err.isBoom) return reply(err);
request.log('error', err);
reply(boom.badImplementation());
});
}
},

getDeckTreeVariants: function(request, reply) {
Expand Down
52 changes: 52 additions & 0 deletions application/controllers/decks.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,58 @@ let self = module.exports = {

},

// TODO support full deck updates
updateDeck: async function(request, reply) {
let userId = request.auth.credentials.userid;
let deckId = request.params.id;

try {
let perms = await deckDB.userPermissions(deckId, userId);
if (!perms) throw boom.notFound();
if (!perms.admin) throw boom.forbidden(`user:${userId} is not authorized to delete deck:${deckId}`);

await deckDB.adminUpdate(deckId, request.payload);

reply(await deckDB.getDeck(deckId));
} catch (err) {
if (err.isBoom) return reply(err);
request.log('error', err);
reply(boom.badImplementation());
}

},

deleteDeck: async function(request, reply) {
let userId = request.auth.credentials.userid;
let deckId = request.params.id;

try {
let perms = await deckDB.userPermissions(deckId, userId);
if (!perms) throw boom.notFound();
if (!perms.admin) throw boom.forbidden(`user:${userId} is not authorized to delete deck:${deckId}`);

let deck = await deckDB.getDeck(deckId);
if (!deck) throw boom.notFound();

// check if deck is not root
if (_.size(deck.usage)) throw boom.methodNotAllowed(`cannot delete deck ${deckId} as it is shared in other deck trees`);

// also check if deck has subdecks
if (_.find(deck.contentItems, { kind: 'deck' })) throw boom.methodNotAllowed(`cannot delete deck ${deckId} as it includes subdecks`);

let result = await deckDB.adminUpdate(deckId, { user: -1 });
if (!result) throw boom.notFound();

// no content retured, it's a DELETE op
reply();
} catch (err) {
if (err.isBoom) return reply(err);
request.log('error', err);
reply(boom.badImplementation());
}

},

};

async function countAndList(query, options){
Expand Down
35 changes: 19 additions & 16 deletions application/database/deckDatabase.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ let self = module.exports = {
let {id: deckId, revision: revisionId} = util.parseIdentifier(identifier) || {};

let col = await helper.getCollection('decks');
let found = await col.findOne({ _id: deckId });
let found = await col.findOne({ _id: deckId, user: { $gt: 0 } });
if (!found) return;

if (revisionId) {
Expand Down Expand Up @@ -507,24 +507,25 @@ let self = module.exports = {
return result.ops[0];
},

// TODO only used for accessLevel tests right now, should be removed or properly integrated
// once a decision re: access level is made
_adminUpdate: function(id, deckPatch) {
return helper.connectToDatabase()
.then((db) => db.collection('decks'))
.then((col) => {
return col.findOne({_id: parseInt(id)})
.then((existingDeck) => {
if (!_.isEmpty(deckPatch.accessLevel)) {
existingDeck.accessLevel = deckPatch.accessLevel;
}
// updates admin properties in patch object
adminUpdate: async function(deckId, patch) {
deckId = parseInt(deckId);

return col.findOneAndReplace({ _id: parseInt(id) }, existingDeck, { returnOriginal: false } )
.then((updated) => updated.value);
});
let decks = await helper.getCollection('decks');
let update = _.pick(patch, 'accessLevel', 'user');

});
if (update.user) {
// we also make it hidden:
// 1) when deleting in order to be removed from the search index automatically
// 2) when transferring owner, so that it doesn't display in the new owner's public decks right away

// TODO alert user they own a new deck
update.hidden = true;
}

return decks.findOneAndUpdate({ _id: deckId }, {
$set: update,
}, { returnOriginal: false }).then((result) => result.value);
},

// returns the new or existing request, with isNew set to true if it was new
Expand Down Expand Up @@ -1870,6 +1871,8 @@ let self = module.exports = {
let item = util.parseIdentifier(itemId);
if (keepVisibleOnly && itemKind === 'deck' && item.revision) {
return self.get(item.id).then((existingDeck) => {
if (!existingDeck) return [];

let [latestRevision] = existingDeck.revisions.slice(-1);
if (latestRevision.id === item.revision) {
return self._getRootDecks(itemId, itemKind, keepVisibleOnly);
Expand Down
43 changes: 43 additions & 0 deletions application/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,48 @@ module.exports = function(server) {
}
});

// new route to eventually replace the PUT method
server.route({
method: 'PATCH',
path: '/deck/{id}',
handler: decks.updateDeck,
config: {
validate: {
params: {
id: Joi.number().integer().description('Identifier of the deck (without revision)'),
},
payload: {
user: Joi.number().integer().description('Identifier of the new deck owner').required(),
},
headers: Joi.object({
'----jwt----': Joi.string().required().description('JWT header provided by /login')
}).unknown(),
},
tags: ['api'],
auth: 'jwt',
description: 'Update metadata of a deck',
},
});

server.route({
method: 'DELETE',
path: '/deck/{id}',
handler: decks.deleteDeck,
config: {
validate: {
params: {
id: Joi.number().integer().description('Identifier of the deck to delete'),
},
headers: Joi.object({
'----jwt----': Joi.string().required().description('JWT header provided by /login')
}).unknown(),
},
tags: ['api'],
auth: 'jwt',
description: 'Delete a deck',
},
});

server.route({
method: 'GET',
path: '/legacy/{oldId}',
Expand Down Expand Up @@ -947,6 +989,7 @@ module.exports = function(server) {
stype: Joi.string(),
sid: Joi.string()
}),
purge: Joi.boolean().default(false),
}).requiredKeys('selector'),
headers: Joi.object({
'----jwt----': Joi.string().required().description('JWT header provided by /login')
Expand Down
Loading