Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9f75362
installed bull
moffatethan May 27, 2021
97cccaf
feat(task-queue) setup queue initalizer, process file for deadline re…
moffatethan May 27, 2021
d43369a
refactor(task-queue) removed client side dependencies from root package
moffatethan May 27, 2021
00a18ef
refactor(task-queue) renamed initalizer.js to initialize.js
moffatethan May 28, 2021
61c0bd4
feat(tasks) setup deadline reminders on cards, added dueDate to Card …
moffatethan Jun 2, 2021
e99efe9
refactor(task) some logging and touch up
moffatethan Jun 2, 2021
c4d95a4
refactor(task) removed unnecessary return of SendGrid response
moffatethan Jun 2, 2021
7f28e1d
fix(task) fixed merge conflicts
moffatethan Jun 2, 2021
6df114b
feat(be-teams) initial back-end implementation and some model refactors
moffatethan Jun 3, 2021
703494a
feat(teams) setup invite model, added invite route structure
moffatethan Jun 4, 2021
d9d09eb
fix(teams) merge conflict from task-queue branch
moffatethan Jun 4, 2021
c993c59
feat(team) implemented initial invite
moffatethan Jun 4, 2021
f79b102
fix(team) merge conflicts
moffatethan Jun 4, 2021
7c83b77
feat(teams) implemented invite controller
moffatethan Jun 4, 2021
d4233ad
feat(teams) added emailing to create invit
moffatethan Jun 5, 2021
0baa14a
feat(teams) completed initial back-end implementation of teams
moffatethan Jun 5, 2021
a73ba73
feat(teams) api documentation markdown file, sender automatically is …
moffatethan Jun 5, 2021
5ac1d21
feat(teams) added quick view to api docs, remove team controller & route
moffatethan Jun 6, 2021
e32fad2
feat(teams) added invites to the team model, fixed create invite bug,…
moffatethan Jun 6, 2021
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
15 changes: 14 additions & 1 deletion server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ const { join } = require("path");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const cors = require("cors");
const initializeQueue = require("./queue/initialize");

const authRouter = require("./routes/auth");
const userRouter = require("./routes/user");
const fileRouter = require("./routes/file");
const teamRouter = require("./routes/team/");
const boardsRouter = require("./routes/board");

const { json, urlencoded } = express;
Expand All @@ -21,6 +23,16 @@ connectDB();
const app = express();
const server = http.createServer(app);

const queue = initializeQueue(
{
name: "rambo",
},
[{ name: "deadlineReminders", jobHandlerPath: "./queue/jobs/deadline.js" }]
);

// Passing null data as there is no need to track any data from database.
queue.add("deadlineReminders", null, { cron: "0 0 * * *" }); // everyday at 12am.

const io = socketio(server, {
cors: {
origin: "*",
Expand Down Expand Up @@ -48,6 +60,7 @@ app.use((req, res, next) => {
app.use("/auth", authRouter);
app.use("/users", userRouter);
app.use("/files", fileRouter);
app.use("/team", teamRouter);
app.use("/boards", boardsRouter);

if (process.env.NODE_ENV === "production") {
Expand All @@ -72,4 +85,4 @@ process.on("unhandledRejection", (err, promise) => {
server.close(() => process.exit(1));
});

module.exports = { app, server };
module.exports = { app, server, queue };
2 changes: 1 addition & 1 deletion server/controllers/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const Board = require("../models/Board");
const { Board } = require("../models/Board");
const { User } = require("../models/User");

const asyncHandler = require("express-async-handler");
Expand Down
51 changes: 51 additions & 0 deletions server/controllers/teamController/collaborator/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const asyncHandler = require("express-async-handler");
const { validationResult } = require("express-validator");
const { Team } = require("../../../models/Team");

/**
* Get all collaborators for the team.
* @route GET /team/:teamId/collaborators
* @returns {Object} A message and payload
*/
exports.getCollaborators = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return next(errors);

const { collaborators } = await Team.findOne({ _id: req.team._id })
.populate("collaborators")
.select("-password");

return res.status(200).json(collaborators);
});

/**
* Removes a collaborator from the team.
* @route DELETE /team/:teamId/collaborators/:userId
* @returns {Object} A message and payload
*/
exports.removeCollaborator = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return next(errors);
const { collaboratorId } = req.params;
const { team } = req;

const isOwner = team.owner.toString() === req.user.id;

if (isOwner || collaboratorId === req.user.id) {
const collaboratorIndex = team.collaboratorIndexPosition(collaboratorId);
if (collaboratorIndex > -1) {
team.collaborators.splice(collaboratorIndex, 1);
await team.save();

return res.status(200).json({
message: "Collaborator removed",
});
}

res.status(404);
throw new Error("Collaborator not in team");
}

res.status(401);
throw new Error("You cannot perform this task");
});
5 changes: 5 additions & 0 deletions server/controllers/teamController/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
teamController: require("./team"),
inviteController: require("./invite"),
collaboratorController: require("./collaborator"),
};
175 changes: 175 additions & 0 deletions server/controllers/teamController/invite/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
const asyncHandler = require("express-async-handler");
const { validationResult } = require("express-validator");
const { Invite } = require("../../../models/Invite");
const { User } = require("../../../models/User");
const sendEmail = require("../../../utils/sendEmail");

const invitationTemplate = (team, inviteId) => `
<img src="https://res.cloudinary.com/dpepwhv11/image/upload/v1622628335/logo_hgrobp.png" />
<div>
<h2>You've been invited to ${team.name}</h2>
<p>
${team.name} would like you to join as a collaborator.
</p>
<hr />
<div>
<a style="padding: 8px 12px;background-color: #759CFC;color: white; text-decoration: none;border-radius: 8px;" href="http://localhost:3000/team/${team._id}/invite/${inviteId}/accept">Join ${team.name}</a>
</div>
<div>
`;

/**
* Create a new invite and add the task to the queue to process emailing the recipient.
* @route POST /team/:teamId/invite
* @returns {Object} A message and payload
*/
exports.createInvite = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return next(errors);
const { team } = req;
const { recipient } = req.body;

const recipientEmail = await User.getEmail(recipient);
if (!recipientEmail) {
res.status(400);
throw new Error("Recipient does not exist");
}

const invite = await Invite.findOne({ recipient });

if (invite) {
res.status(400);
throw new Error("Recipient already has been invited");
}

if (recipient === req.user.id) {
res.status(400);
throw new Error("You cannot send an invite to yourself");
}

if (team.collaboratorIndexPosition(recipient) > -1) {
res.status(400);
throw new Error("That person is already in your team");
}

const isOwner = team.owner.toString() === req.user.id;

if (isOwner) {
const invite = await Invite.create({
team: req.team.id,
recipient,
sender: req.user.id,
});

const emailOptions = {
subject: "Your invited to join a team",
html: invitationTemplate(req.team, invite._id),
};
sendEmail(recipientEmail, emailOptions);

team.invites.push(invite.id);
await team.save();

return res.status(200).json({
message: "Invite sent",
payload: invite,
});
}

res.status(400);
throw new Error("You cannot invite to this team");
});

/**
* Accepting a team invitation
* @route GET /team/:teamId/:inviteId/accept
*/
exports.acceptInvite = asyncHandler(async (req, res, next) => {
const { inviteId } = req.params;
const { team } = req;
const invite = await Invite.findOne({ _id: inviteId });
if (!invite) {
res.status(404);
throw new Error("That invite does not exist");
}

if (invite.recipient.toString() !== req.user.id) {
res.status(400);
throw new Error("This invite is not for you");
}

const user = await User.findOne({ _id: invite.recipient });

if (!user) {
res.status(404);
await invite.remove(); // no need to keep the invite anymore if the recipient doesn't exist.
throw new Error("That user no longer exists");
}

team
.addCollaborator(invite.recipient)
.then(() => team.removeInvite(invite.id));

await invite.remove();

return res.status(200).json({
message: `Added you to ${team.name}`,
});
});

/**
* Get a list of all active invites a team has.
*
* @route GET /team/:teamId/invites/
* @returns {Array} A list of invite resources
*/
exports.getActiveInvites = asyncHandler(async (req, res, next) => {
const { team } = req;
const invites = await Invite.find({ team: team.id })
.populate("recipient")
.select("-password");

return res.status(200).json(invites);
});

/**
* Get a invite resource by it's id.
*
* Mongo ObjectId's are used as the codes for the invites.
* @route GET /team/:teamId/invite/:inviteId
* @returns {Object} An invite resource
*/
exports.getInvite = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return next(errors);
const { inviteId } = req.params;
const invite = await Invite.findOne({ _id: inviteId });

if (!invite) {
res.status(404);
throw new Error("Invite could not be found");
}

return res.status(200).json(invite);
});

/**
* Delete an invite resource by it's ID.
*
* @route DELETE /team/:teamId/invite/:inviteId/revoke
* @returns {Object} A message
*/
exports.revokeInvite = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return next(errors);
const { inviteId } = req.params;
const invite = await Invite.findOne({ _id: inviteId });

if (!invite) {
res.status(404);
throw new Error("Invite could not be found");
}

await invite.remove();
return res.status(200).json({ message: "Invite deleted" });
});
103 changes: 103 additions & 0 deletions server/controllers/teamController/team/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const asyncHandler = require("express-async-handler");
const { validationResult } = require("express-validator");
const { Team } = require("../../../models/Team");

/**
* Create a new Team resource and store in the database.
* @route POST /team
* @returns {Object} A message and payload
*/
exports.createTeam = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return next(errors);

const { name } = req.body;
const team = await Team.create({
name,
owner: req.user.id,
});

return res.status(200).json({
message: "Team created successfully!",
payload: team,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change this to be namespaced under team? so { message: "Team created successfully!", team: team} to maintain consistency with boards

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With how teams context is setup the actions accept payloads conventionally this makes more sense to pass the payload to the reducer.

});
});

/**
* Gets the Team by ID, otherwise returns null.
* @route GET /team
* @returns {Array} A list of users team resources
*/
exports.getUsersTeams = asyncHandler(async (req, res, next) => {
const teams = await Team.find({ owner: req.user.id })
.populate("collaborator")
.populate("invites")
.select("-password");

return res.status(200).json(teams);
});

/**
* Gets the Team by ID, otherwise returns null.
* @route GET /team/:teamId
* @returns {Object} The team
*/
exports.getTeam = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return next(errors);

return res.status(200).json(req.team);
});

/**
* Update teams (only name)
*
* We only allow updating name for Team here because edits to linked resources must happen with their relationship modal methods.
* @route PATCH /team/:teamId
* @returns {Object} The updated team
*/
exports.updateTeam = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return next(errors);

const { name } = req.body;
if (!name) {
res.status(400);
throw new Error("Name is required");
}

const team = await Team.findOneAndUpdate(
{ _id: req.params.teamId, owner: req.user.id },
{ name }
);

// middleware handles if a team doesn't exist, so if team is null then user is not the owner.
if (!team) {
res.status(400);
throw new Error("You cannot perform this task");
}

return res.status(200).json(team);
});

/**
* Deletes a team from the database.
*
* @route DELETE /team/:teamId
* @returns {Object} The updated team
*/
exports.deleteTeam = asyncHandler(async (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return next(errors);
const { team } = req;

const isOwner = team.owner.toString() === req.user.id;

if (isOwner) {
await team.remove();
return res.status(200).json({ message: "Team deleted" });
}

res.status(400);
throw new Error("You cannot perform this task");
});
Loading