As a backend developer, have you ever wondered the concept behind
- Creating a team
- Sending Team invitation to members
- Accepting team invitation
- Rejecting team invitation
- Leaving team
- Removing member from team and
- managing roles in a team
In this article, I will walk you through how I was able to achieve all of these with Javascript
.
REQUIREMENTS
Knowledge of Javascript, Nodejs, and RESTful APIs
LET'S GET STARTED
STEP 1
Let us start with initializing our project in vscode
editor using the npm init
command
STEP 2
Install necessary dependencies
npm install express body-parser mongoose dotenv jsonwebtoken cors nodemon
STEP 3
Create an express application and connect to your mongodb database
const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const mongoose = require("mongoose");
const dotenv = require("dotenv");
dotenv.config();
const app = express()
const corsOptions = {
origin: "*",
credentials: true,
optionSuccessStatus: 200,
};
app.use(cors(corsOptions));
app.use(express.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(express.json());
const db = process.env.DATABASE;
async function startServer() {
const port = process.env.PORT || 8080;
try {
const response = await mongoose.connect(db, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log("database connection successful");
app.listen(port, () => {
console.log(`app running on port ${port}`);
});
} catch (error) {
console.log(error);
}
}
startServer();
STEP 4
Add this command to the scripts object inside your package.json file
and npm start
"start": "nodemon app.js"
Node.js v18.16.0
[nodemon] starting `node app.js`
database connection successful
app running on port 8000
If you got something like this in your terminal, then we are on the same page, if not, look through your codes.
STEP 5
Let us create our models for users and teams and export them
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema(
{
name: {
type: String,
default: "",
trim: true,
},
email: {
type: String,
required: [true, "email is required"],
unique: true,
trim: true,
},
password: {
type: String,
required: [true, "password is required"],
trim: true,
},
teams: [
{
type: mongoose.Schema.ObjectId,
ref: "Team",
},
],
invitations: [
{
type: mongoose.Schema.ObjectId,
ref: "Team",
},
],
createdAt: {
type: Date,
default: Date.now(),
},
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true },
}
);
module.exports = mongoose.model("User", userSchema);
- Name: name of the user
- Email: email of the user,
- password: user's password,
- teams: A reference to the teams this user belongs to
- invitations: A reference to pending team invitations
const mongoose = require("mongoose");
const teamSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
required: [true, "team must have a name"],
},
members: {
type: Array,
},
userId: {
type: String,
required: [true, "team must belong to a user"],
},
createdAt: {
type: Date,
default: Date.now(),
},
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true },
}
);
module.exports = mongoose.model("Team", teamSchema);
- members: members in a team
- userId: identifier for the creator of the team
STEP 6
Let us quickly create an authentication controller to manage signup and login so we can authenticate users
const User = require("../models/user-model");
const jwt = require("jsonwebtoken");
// CREATE JWT TOKEN
const createJWTToken = (id) =>
jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN,
});
// CREATE AND SEND JWT TOKEN
const createSendToken = (user, statusCode, res) => {
const token = createJWTToken(user.id);
user.password = undefined;
return res.status(statusCode).json({
status: "success",
token,
data: {
user,
},
});
};
exports.signup = async (req, res, next) => {
try {
// check if user exists
const userExist = await User.findOne({ email: req.body.email });
if (userExist) {
return res.status(400).json({
status: "failed",
message: "user with this email exists",
});
}
// create user
const user = await User.create(req.body);
// send response data
createSendToken(user, 201, res);
} catch (error) {
res.status(400).json({
status: "failed",
error: error.message,
});
}
};
exports.login = async (req, res, next) => {
const { email, password } = req.body;
try {
// find user
const user = await User.findOne({ email
}).populate("teams").populate({
path: "invitations",
select: "name",
});
// check if user exist
if (!user) {
return res.status(404).json({
status: "failed",
message: "user not found",
});
}
//check if password match
if (user.password !== password) {
return res.status(401).json({
status: "failed",
message: "invalid login credentials",
});
}
// send response
createSendToken(user, 201, res);
} catch (error) {
res.status(400).json({
status: "failed",
error: error.message,
});
}
};
STEP 7
Create express route for signup and login then send a request from postman
const express = require("express");
const authController = require("../controllers/authController");
const router = express.Router();
router.post("/register", authController.signup);
router.post("/login", authController.login);
module.exports = router;
PAYLOAD:
{
"email": "testing@gmail.com",
"password": "test1234"
}
RESPONSE:
{
"status": "success",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NzMyYWQzMGNhMjk0MzRkMDRiODQ2YyIsImlhdCI6MTY4NTI2OTIwMywiZXhwIjoxNjkzMDQ1MjAzfQ.Q8aDSekSVKK5zBVkRaJOdqs3JT3G5xniRnpu6zjHO8U",
"data": {
"user": {
"name": "",
"email": "testing@gmail.com",
"teams": [],
"invitations": [],
"createdAt": "2023-05-28T10:19:47.491Z",
"_id": "64732ad30ca29434d04b846c",
"__v": 0,
"id": "64732ad30ca29434d04b846c"
}
}
}
STEP 8
Now let us create our team controller and start creating and managing our team.
Before that, let us quickly create a middleware to protect our endpoint and ensure only authenticated users can access certain resources
NOTE: Use this command to make your JWT available in postman
pm.environment.set("jwt", pm.response.json().token);
const jwt = require("jsonwebtoken");
const User = require("./models/user-model");
const jwt = require("jsonwebtoken");
const User = require("./models/user-model");
const jwt = require("jsonwebtoken");
const User = require("./models/user-model");
// PROTECT ROUTE
exports.protect = async (req, res, next) => {
// check if token is available on authorization header
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer")
) {
token = req.headers.authorization.split(" ")[1];
}
// if not, restrict access
if (!token) {
return res.status(401).json({
status: "failed",
message: "unauthenticated",
});
}
// decode the token to extract the user identifier
let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
return res.status(401).json({
status: "failed",
message: "your login token is invalid",
});
}
// use identifier to find user in database
try {
const user = await User.findById(decoded.id);
// if user does not exist, throw an error and abort operation
if (!user) {
return res.status(404).json({
status: "failed",
message: "user no longer exists",
});
}
// if user is available, store the user data on the request object
req.user = user;
// call the next() middleware
next();
} catch (error) {
return res.status(400).json({
status: "failed",
message: error.message,
});
}
};
We will be using this middleware to check restrict certain resources to only authenticated users.
Back to our team controller, let us create a team
const Team = require("../models/team-model");
const User = require("../models/user-model");
exports.createTeam = async (req, res, next) => {
const reqBody = {
name: req.body.name,
// remember we stored this user's data on the req object inside the middleware we created.
userId: req.user.id,
};
try {
// while creating a team, we want to immediately add the creator of the team as an admin of the team.
const team = await Team.create(reqBody);
// let us create a member data
const user = req.user;
const memberDetail = {
name: user.name || "",
email: user.email,
id: req.user.id,
role: "admin",
};
// let us push this member detail inside the member array on the team model
team.members.push(memberDetail);
// let us push the team id into the team array on the user model
user.teams.push(team.id);
// let us save these changes
await team.save();
await user.save();
return res.status(201).json({
status: "success",
data: team,
});
} catch (error) {
return res.status(400).json({
status: "failed",
message: error.message,
});
}
};
STEP 8
Let us create a team route and run the request in postman
const express = require("express");
const middleware = require("../middleware");
const teamController = require("../controllers/teamController");
const router = express.Router();
router.route("/").post(middleware.protect, teamController.createTeam);
module.exports = router;
PAYLOAD:
{
"name": "nodejs team"
}
RESPONSE:
{
"status": "success",
"data": {
"name": "nodejs team",
"members": [
{
"name": "",
"email": "member@gmail.com",
"id": "6473372575f23d8038180c71",
"role": "admin"
}
],
"userId": "6473372575f23d8038180c71",
"createdAt": "2023-05-28T11:20:31.553Z",
"_id": "64733913dff5e9f5e31f4850",
"__v": 1,
"id": "64733913dff5e9f5e31f4850"
}
}
STEP 9
Now let us create another user and send an invitation to join the nodejs team
PAYLOAD:
{
"email": "member@gmail.com",
"password": "test1234"
}
RESPONSE:
{
"status": "success",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NzMzNzI1NzVmMjNkODAzODE4MGM3MSIsImlhdCI6MTY4NTI3MjM1NywiZXhwIjoxNjkzMDQ4MzU3fQ.cCZywLZekCUPAh_ZNKsLZ0bVrgd-gSm71qFUBIE8jos",
"data": {
"user": {
"name": "",
"email": "member@gmail.com",
"teams": [],
"invitations": [],
"createdAt": "2023-05-28T11:04:25.981Z",
"_id": "6473372575f23d8038180c71",
"__v": 0,
"id": "6473372575f23d8038180c71"
}
}
}
Now let us create the sendInvitation
controller
exports.sendInvitation = async (req, res, next) => {
// pass the team id as a parameter
const { teamId } = req.params;
// use the member email to send the invitation,
// so the member email will be the payload
const { email } = req.body;
try {
// find this user in the database
const member = await User.findOne({ email });
// if user doesnt exist
if (!member) {
return res.status(404).json({
status: "failed",
message: "member not found",
});
}
// if member exist, check if member is already in the team
// 1. find the team
const team = await Team.findById(teamId);
// 2. check if team exists
if (!team) {
return res.status(404).json({
status: "failed",
message: "team not found",
});
}
// if team exists, check if the member is in the team already
const memberExist = team.members.find((item) => item.id === member.id);
if (memberExist) {
return res.status(400).json({
status: "failed",
message: "member is already in the team",
});
}
// if not, send the invitation by pushing the teamID inside the member.invitations array
// push the team ID into the invitations array on the user model
member.invitations.push(team.id);
// save changes
await member.save();
return res.status(200).json({
status: "success",
data: `invitation sent to ${member.email}`,
});
} catch (error) {
return res.status(400).json({
status: "failed",
message: error.message,
});
}
};
STEP 10
Create a send invitation route and send the request in postman;
NOTE: Don't forget to pass the teamID as a parameter
router.post(
"/send-invitation/:teamId",
middleware.protect,
teamController.sendInvitation
);
PAYLOAD:
{
"email": "testing@gmail.com"
}
RESPONSE:
{
"status": "success",
"data": "invitation sent to testing@gmail.com"
}
STEP 11
Login to testing@gmail.com account and let us accept the invitation sent by member@gmail.com
If you are wondering, I created a team with the member@gmail.com endpoint and sent an invitation to testing@gmail.com.
Logging in to the testing@gmail.com account, the invitation array will contain the data we sent
{
"status": "success",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NzMyYWQzMGNhMjk0MzRkMDRiODQ2YyIsImlhdCI6MTY4NTI4MDU0NywiZXhwIjoxNjkzMDU2NTQ3fQ.FWf3-xYrF3uxnp572KAS7ZsG-F4XUeSwgJwLY2PFtK8",
"data": {
"user": {
"_id": "64732ad30ca29434d04b846c",
"name": "",
"email": "testing@gmail.com",
"teams": [],
"invitations": [
{
"_id": "64733913dff5e9f5e31f4850",
"name": "nodejs team",
"id": "64733913dff5e9f5e31f4850"
}
],
"createdAt": "2023-05-28T10:19:47.491Z",
"__v": 2,
"id": "64732ad30ca29434d04b846c"
}
}
}
ACCEPT INVITATION
exports.acceptInvitation = async (req, res, next) => {
try {
// pass the teamID as a parameter to accept invitation
const { teamId } = req.params;
// find user in database
const user = await User.findById(req.user.id);
// find team in database
const team = await Team.findById(teamId);
// check if team still exists
if (!team) {
return res.status(404).json({
status: "success",
message: "team not found",
});
}
// check if the user is already a member of the team
const memberExist = team.members.find((item) => item.id === user.id);
if (memberExist) {
return res.status(400).json({
status: "failed",
message: "you are already a member of this group",
});
}
// if user is not a member of the team, now let us accept invitation and add user to the team
// 1. create user detail
const userDetail = {
name: user.name || "",
email: user.email,
id: user.id,
role: "member",
};
// 2. add member to the team
team.members.push(userDetail);
// 3. remove the team from the invitation array
const teamIndex = user.invitations.indexOf(team.id);
user.invitations.splice(teamIndex);
// 4. add team to the user team array
user.teams.push(team.id);
// 5. save changes
await user.save();
await team.save();
return res.status(200).json({
status: "success",
message: `invitation to ${team.name} accepted`,
});
} catch (error) {
return res.status(400).json({
status: "failed",
message: error.message,
});
}
};
STEP 12
Let us create an accept invitation endpoint and run this request in postman.
NOTE: This request needs no payload, justpass the teamID as a parameter on the request
router.post(
"/accept-invitation/:teamId",
middleware.protect,
teamController.acceptInvitation
);
RESPONSE:
{
"status": "success",
"message": "invitation to nodejs team accepted"
}
If we login to testing@gmail.com account, we should see the teams object in the teams array and the invitation array should be empty
{
"status": "success",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY0NzMyYWQzMGNhMjk0MzRkMDRiODQ2YyIsImlhdCI6MTY4NTI4MTk3NiwiZXhwIjoxNjkzMDU3OTc2fQ.tQF301JkgBDcDPuyUde0_i4QcjeFm0KP4cX5S9Bikuc",
"data": {
"user": {
"_id": "64732ad30ca29434d04b846c",
"name": "",
"email": "testing@gmail.com",
"teams": [
{
"_id": "64733913dff5e9f5e31f4850",
"name": "nodejs team",
"members": [
{
"name": "",
"email": "member@gmail.com",
"id": "6473372575f23d8038180c71",
"role": "admin"
},
{
"name": "",
"email": "testing@gmail.com",
"id": "64732ad30ca29434d04b846c",
"role": "member"
}
],
"userId": "6473372575f23d8038180c71",
"createdAt": "2023-05-28T11:20:31.553Z",
"__v": 2,
"id": "64733913dff5e9f5e31f4850"
}
],
"invitations": [],
"createdAt": "2023-05-28T10:19:47.491Z",
"__v": 3,
"id": "64732ad30ca29434d04b846c"
}
}
}
Now we have 2 members inside the nodejs team.
I am going to stop here for now, in my next article, I will talk about
- Leaving team,
- Removing member from team
- Rejecting invitation
- Managing roles in a team.
Meanwhile, let me know your thought on this method in the comments section.
Top comments (0)