-
Notifications
You must be signed in to change notification settings - Fork 1
feat(teams) Created initial teams back-end controller (invites, collaborators, creating teams) #91
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
Open
moffatethan
wants to merge
19
commits into
main
Choose a base branch
from
feat-be-teams
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
9f75362
installed bull
moffatethan 97cccaf
feat(task-queue) setup queue initalizer, process file for deadline re…
moffatethan d43369a
refactor(task-queue) removed client side dependencies from root package
moffatethan 00a18ef
refactor(task-queue) renamed initalizer.js to initialize.js
moffatethan 61c0bd4
feat(tasks) setup deadline reminders on cards, added dueDate to Card …
moffatethan e99efe9
refactor(task) some logging and touch up
moffatethan c4d95a4
refactor(task) removed unnecessary return of SendGrid response
moffatethan 7f28e1d
fix(task) fixed merge conflicts
moffatethan 6df114b
feat(be-teams) initial back-end implementation and some model refactors
moffatethan 703494a
feat(teams) setup invite model, added invite route structure
moffatethan d9d09eb
fix(teams) merge conflict from task-queue branch
moffatethan c993c59
feat(team) implemented initial invite
moffatethan f79b102
fix(team) merge conflicts
moffatethan 7c83b77
feat(teams) implemented invite controller
moffatethan d4233ad
feat(teams) added emailing to create invit
moffatethan 0baa14a
feat(teams) completed initial back-end implementation of teams
moffatethan a73ba73
feat(teams) api documentation markdown file, sender automatically is …
moffatethan 5ac1d21
feat(teams) added quick view to api docs, remove team controller & route
moffatethan e32fad2
feat(teams) added invites to the team model, fixed create invite bug,…
moffatethan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"), | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * 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"); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 withboardsThere was a problem hiding this comment.
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.