DEV Community

Cover image for MANAGING A TEAM WITH NODE.JS (PART 1)
Candie
Candie

Posted on • Edited on

MANAGING A TEAM WITH NODE.JS (PART 1)

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
Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
  • 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);

Enter fullscreen mode Exit fullscreen mode
  • 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,
    });
  }
};

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

PAYLOAD:

{
    "email": "testing@gmail.com",
    "password": "test1234"
}
Enter fullscreen mode Exit fullscreen mode

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"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
};



Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
};

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

PAYLOAD:

{
    "name": "nodejs team"
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

STEP 9

Now let us create another user and send an invitation to join the nodejs team
PAYLOAD:

{
    "email": "member@gmail.com",
    "password": "test1234"
}
Enter fullscreen mode Exit fullscreen mode

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"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

PAYLOAD:

{
    "email": "testing@gmail.com"
}
Enter fullscreen mode Exit fullscreen mode

RESPONSE:

{
    "status": "success",
    "data": "invitation sent to testing@gmail.com"
}
Enter fullscreen mode Exit fullscreen mode

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"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
    });
  }
};

Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

RESPONSE:

{
    "status": "success",
    "message": "invitation to nodejs team accepted"
}
Enter fullscreen mode Exit fullscreen mode

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"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)