MANAGING A TEAM WITH NODE.JS (PART 1)
Tags: javascript, node, mongodb
Published: 2023-05-29 07:59:15

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

```js
npm install express body-parser mongoose dotenv jsonwebtoken cors nodemon
```

## STEP 3
Create an express application and connect to your mongodb database

```js
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"`

```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

```js
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

```js
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

```js
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

```js
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:

```js
{
    "email": "testing@gmail.com",
    "password": "test1234"
}
```
RESPONSE:

```js
{
    "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);`

```js
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

```js
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

```js
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:

```js
{
    "name": "nodejs team"
}
```
RESPONSE:

```js
{
    "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:

```js
{
    "email": "member@gmail.com",
    "password": "test1234"
}
```
RESPONSE: 

```js
{
    "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

```js
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

```js
router.post(
  "/send-invitation/:teamId",
  middleware.protect,
  teamController.sendInvitation
);
```
PAYLOAD:

```js
{
    "email": "testing@gmail.com"
}
```
RESPONSE: 

```js
{
    "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

```js
{
    "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

```js
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

```js
router.post(
  "/accept-invitation/:teamId",
  middleware.protect,
  teamController.acceptInvitation
);
```

RESPONSE: 

```js
{
    "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

```js
{
    "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.