Skip to content

Commit

Permalink
feat(general): added join requests
Browse files Browse the repository at this point in the history
  • Loading branch information
serge1peshcoff committed Feb 24, 2020
1 parent 6c89004 commit bf6c135
Show file tree
Hide file tree
Showing 13 changed files with 480 additions and 38 deletions.
8 changes: 7 additions & 1 deletion lib/permissions-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ class PermissionsManager {
}
}

// This function should be called with a string, which is the `combined` field
// of a permission, either with a scope (like `global:edit:user`) or without it
// (like `edit:user`). If scope is provided, only permission with scope is searched
// for, if it's not provided, all scopes are iterated through, with the following
// priority: global, local, join_request (so if a person has both `global:edit:user` and
// `local:edit:user` permissions, the first one would be chosen.
static getPermissionKeys(combined) {
const combinedSplit = combined.split(':');
if (combinedSplit.length === 2) {
Expand All @@ -51,7 +57,7 @@ class PermissionsManager {
return keys.some((key) => this.permissionsMap[key]);
}

getPermissionFilter(permission) {
getPermissionFilters(permission) {
const keys = this.getPermissionKeys(permission);
for (const key of keys) {
if (this.permissionsMap[key]) {
Expand Down
16 changes: 13 additions & 3 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ const members = require('../middlewares/members');
const bodies = require('../middlewares/bodies');
const circles = require('../middlewares/circles');
const permissions = require('../middlewares/permissions');
const memberships = require('../middlewares/memberships');
const joinRequests = require('../middlewares/join-requests');

const GeneralRouter = router({ mergeParams: true });
const MemberRouter = router({ mergeParams: true });
const BodiesRouter = router({ mergeParams: true });
const MembershipsRouter = router({ mergeParams: true });
const JoinRequestsRouter = router({ mergeParams: true });
const CirclesRouter = router({ mergeParams: true });
const PermissionsRouter = router({ mergeParams: true });
const CampaignsRouter = router({ mergeParams: true });
Expand Down Expand Up @@ -78,16 +81,22 @@ MemberRouter.put('/', members.updateUser);

// Everything related to a specific body. Auth only (except for body details).
BodiesRouter.use(fetch.fetchBody);
BodiesRouter.get('/members', bodies.listAllMemberships);
BodiesRouter.get('/', bodies.getBody);
BodiesRouter.use(middlewares.maybeAuthorize, middlewares.ensureAuthorized);
BodiesRouter.get('/members', memberships.listAllMemberships);
BodiesRouter.get('/join-requests', joinRequests.listAllJoinRequests);
BodiesRouter.post('/join-requests', joinRequests.createJoinRequest);
BodiesRouter.put('/status', bodies.setBodyStatus);
BodiesRouter.put('/', bodies.updateBody);

// Everything related to a specific body membership. Auth only.
MembershipsRouter.use(middlewares.maybeAuthorize, middlewares.ensureAuthorized, fetch.fetchBody, fetch.fetchMembership);
MembershipsRouter.put('/', bodies.updateMembership);
MembershipsRouter.delete('/', bodies.deleteMembership);
MembershipsRouter.put('/', memberships.updateMembership);
MembershipsRouter.delete('/', memberships.deleteMembership);

// Everything related to a specific body membership. Auth only.
JoinRequestsRouter.use(middlewares.maybeAuthorize, middlewares.ensureAuthorized, fetch.fetchBody, fetch.fetchJoinRequest);
JoinRequestsRouter.put('/status', joinRequests.changeRequestStatus);

// Everything related to a specific circle. Auth only.
CirclesRouter.use(middlewares.maybeAuthorize, middlewares.ensureAuthorized, fetch.fetchCircle);
Expand All @@ -110,6 +119,7 @@ CampaignsRouter.delete('/', campaigns.deleteCampaign);

server.use('/members/:user_id', MemberRouter);
server.use('/bodies/:body_id/members/:membership_id', MembershipsRouter);
server.use('/bodies/:body_id/join-requests/:request_id', JoinRequestsRouter);
server.use('/bodies/:body_id', BodiesRouter);
server.use('/circles/:circle_id', CirclesRouter);
server.use('/permissions/:permission_id', PermissionsRouter);
Expand Down
32 changes: 1 addition & 31 deletions middlewares/bodies.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { Body, BodyMembership, User } = require('../models');
const { Body } = require('../models');

exports.listAllBodies = async (req, res) => {
const bodies = await Body.findAll({});
Expand Down Expand Up @@ -45,33 +45,3 @@ exports.setBodyStatus = async (req, res) => {
data: req.currentBody
});
};

exports.listAllMemberships = async (req, res) => {
// TODO: check permissions
// TOOD: add pagination
const members = await BodyMembership.findAll({
where: { body_id: req.currentBody.id },
include: [User]
});

return res.json({
success: true,
data: members
});
};

exports.updateMembership = async (req, res) => {
await req.currentBodyMembership.update({ comment: req.body.comment });
return res.json({
success: true,
data: req.currentBodyMembership
});
};

exports.deleteMembership = async (req, res) => {
await req.currentBodyMembership.destroy();
return res.json({
success: true,
message: 'Membership is deleted.'
});
};
23 changes: 22 additions & 1 deletion middlewares/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const {
Circle,
Permission,
Campaign,
BodyMembership
BodyMembership,
JoinRequest
} = require('../models');

const { Sequelize } = require('../lib/sequelize');
Expand Down Expand Up @@ -144,3 +145,23 @@ exports.fetchMembership = async (req, res, next) => {
req.currentBodyMembership = membership;
return next();
};

exports.fetchJoinRequest = async (req, res, next) => {
// searching the campaign by id if it's numeric
if (!helpers.isNumber(req.params.request_id)) {
return errors.makeBadRequestError(res, 'Join request ID is invalid.');
}

const request = await JoinRequest.findOne({
where: {
id: Number(req.params.request_id),
body_id: Number(req.params.body_id)
}
});
if (!request) {
return errors.makeNotFoundError(res, 'Join request is not found.');
}

req.currentJoinRequest = request;
return next();
};
57 changes: 57 additions & 0 deletions middlewares/join-requests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { JoinRequest, BodyMembership } = require('../models');
const errors = require('../lib/errors');
const { sequelize } = require('../lib/sequelize');

exports.listAllJoinRequests = async (req, res) => {
// TODO: check permissions
const requests = await JoinRequest.findAll({
where: { body_id: req.currentBody.id }
});

return res.json({
success: true,
data: requests
});
};

exports.createJoinRequest = async (req, res) => {
const request = await JoinRequest.create({
user_id: req.user.id,
body_id: req.currentBody.id,
motivation: req.body.motivation
});
return res.json({
success: true,
data: request
});
};

exports.changeRequestStatus = async (req, res) => {
// TODO: check permissions
if (!['accepted', 'rejected'].includes(req.body.status)) {
return errors.makeBadRequestError(res, 'The status is invalid.');
}

if (req.currentJoinRequest.status !== 'pending') {
return errors.makeBadRequestError(res, 'The join request was processed already.');
}

await sequelize.transaction(async (t) => {
// if a join request is accepted, then create a new body membership
// but store the join request (for history)
// if a join request is rejected, just delete it so a person can reapply.
if (req.body.status === 'accepted') {
await BodyMembership.create({
user_id: req.currentJoinRequest.user_id,
body_id: req.currentBody.id
}, { transaction: t });
} else {
await req.currentJoinRequest.destroy();
}
});

return res.json({
success: true,
message: `The join request was ${req.body.status}.`
});
};
43 changes: 43 additions & 0 deletions middlewares/memberships.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const {
BodyMembership,
JoinRequest,
User
} = require('../models');

exports.listAllMemberships = async (req, res) => {
// TODO: check permissions
// TOOD: add pagination
const members = await BodyMembership.findAll({
where: { body_id: req.currentBody.id },
include: [User]
});

return res.json({
success: true,
data: members
});
};

exports.updateMembership = async (req, res) => {
await req.currentBodyMembership.update({ comment: req.body.comment });
return res.json({
success: true,
data: req.currentBodyMembership
});
};

exports.deleteMembership = async (req, res) => {
// delete all join requests if any, so a person can reapply
await JoinRequest.destroy({
where: {
user_id: req.currentBodyMembership.user_id,
body_id: req.currentBody.id
}
});
await req.currentBodyMembership.destroy();

return res.json({
success: true,
message: 'Membership is deleted.'
});
};
52 changes: 52 additions & 0 deletions migrations/20200221131411-create-join-requests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('join_requests', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
body_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'bodies',
key: 'id'
},
onDelete: 'CASCADE'
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
},
onDelete: 'CASCADE'
},
motivation: {
type: Sequelize.TEXT,
allowNull: true
},
status: {
allowNull: false,
type: Sequelize.STRING,
defaultValue: 'pending'
},
created_at: {
allowNull: false,
type: Sequelize.DATE
},
updated_at: {
allowNull: false,
type: Sequelize.DATE
}
}, {
uniqueKeys: {
body_permission_unique: {
fields: ['body_id', 'user_id']
}
}
}),
down: (queryInterface) => queryInterface.dropTable('join_requests')
};
38 changes: 38 additions & 0 deletions models/JoinRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const { Sequelize, sequelize } = require('../lib/sequelize');

const JoinRequest = sequelize.define('join_request', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
motivation: {
type: Sequelize.TEXT,
allowNull: true
},
status: {
type: Sequelize.ENUM('pending', 'accepted'),
allowNull: false,
defaultValue: 'pending',
validate: {
isIn: {
args: [['pending', 'accepted']],
msg: 'Satus should be one of these: "pending", "accepted".'
}
}
},
}, {
underscored: true,
tableName: 'join_requests',
createdAt: 'created_at',
updatedAt: 'updated_at',
});


JoinRequest.beforeValidate(async (request) => {
// skipping these fields if they are unset, will catch it later.
if (typeof request.motivation === 'string') request.motivation = request.motivation.trim();
});

module.exports = JoinRequest;
10 changes: 9 additions & 1 deletion models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const Permission = require('./Permission');
const CirclePermission = require('./CirclePermission');
const CircleMembership = require('./CircleMembership');
const BodyMembership = require('./BodyMembership');
const JoinRequest = require('./JoinRequest');

Campaign.hasMany(User, { foreignKey: 'campaign_id' });
User.belongsTo(Campaign, { foreignKey: 'campaign_id' });
Expand Down Expand Up @@ -58,6 +59,12 @@ User.hasMany(BodyMembership, { foreignKey: 'user_id' });
BodyMembership.belongsTo(Body, { foreignKey: 'body_id' });
Body.hasMany(BodyMembership, { foreignKey: 'body_id' });

JoinRequest.belongsTo(User, { foreignKey: 'user_id' });
User.hasMany(JoinRequest, { foreignKey: 'user_id' });

JoinRequest.belongsTo(Body, { foreignKey: 'body_id' });
Body.hasMany(JoinRequest, { foreignKey: 'body_id' });

module.exports = {
User,
Campaign,
Expand All @@ -69,5 +76,6 @@ module.exports = {
Permission,
CirclePermission,
CircleMembership,
BodyMembership
BodyMembership,
JoinRequest
};
Loading

0 comments on commit bf6c135

Please sign in to comment.