All files / utils token.ts

96.59% Statements 85/88
85.71% Branches 18/21
94.44% Functions 17/18
95.83% Lines 69/72

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 3044x 4x           4x 4x   4x 4x                               4x           4x             4x                                                           4x             9x 9x 9x 9x             8x   8x 4x   4x   4x   4x   5x                     4x           5x 5x 5x 4x           4x 3x 3x   3x     2x     2x   3x   2x       2x                   4x     3x       3x               4x 2x 2x       1x   1x                 4x     2x       2x                           4x 2x 2x         1x   1x               4x     2x     2x               4x   2x 2x 2x         1x   1x               4x     2x     2x               4x 2x 2x         1x   1x                           4x                      
import { INVALID_TOKEN_MSG } from "@app/constants";
import tokenModel from "@app/models/token";
import {
  OrganizationInvitationTokenPayload,
  TokenTypes,
} from "@app/types/token";
import { User } from "@app/types/user";
import jwt, { JwtPayload, SignOptions } from "jsonwebtoken";
import { Types, isValidObjectId } from "mongoose";
 
const DEFAULT_ACCESS_TOKEN_EXPIRE_IN = "24h";
const DEFAULT_REFRESH_TOKEN_EXPIRE_IN = "7d";
 
/**
 * This function saves a token with its type, value, user ID, and organization ID to the database.
 * @param {TokenTypes} type - TokenTypes is a custom type that specifies the type of token being saved
 * (e.g. "access token", "refresh token", etc.).
 * @param {string} token - The `token` parameter is a string that represents the actual token that
 * needs to be saved. It could be an access token, refresh token, or any other type of token used for
 * authentication or authorization purposes.
 * @param userId - userId is a parameter of type Types.ObjectId, which represents the unique identifier
 * of a user in the database. It is used to associate the token with a specific user.
 * @param organizationId - The `organizationId` parameter is of type `Types.ObjectId` and represents
 * the unique identifier of the organization associated with the token being saved.
 * @returns The `saveToken` function returns a promise that resolves to the newly created token object
 * after it has been saved to the database.
 */
const saveToken = (
  type: TokenTypes,
  token: string,
  userId: Types.ObjectId,
  organizationId: Types.ObjectId
) => {
  const newToken = new tokenModel({
    type,
    token,
    userId,
    organizationId,
  });
 
  return newToken.save();
};
 
type TokenPayload<T extends TokenTypes> =
  T extends "organizationInvitationToken"
    ? // organizationId will be required in payload for all organization related token
      { [key: string]: any; organizationId: Types.ObjectId }
    : Record<string, any>;
 
/**
 * This is a TypeScript function that generates a token with a specified type, payload, and expiration
 * time, and optionally saves it in a database.
 * @param {T} type - The type of token being generated. It is a generic type that extends the
 * TokenTypes enum.
 * @param payload - The payload parameter is an object that contains the data that will be encoded in
 * the token. The type of the payload object is determined by the type parameter, which is a generic
 * type that extends the TokenTypes enum. The payload object must match the shape of the TokenPayload
 * interface for the corresponding token type
 * @param [userId] - The ID of the user for whom the token is being generated. This is used to save the
 * token in the database for future authentication.
 * @param {boolean} [saveTokenInDB=true] - `saveTokenInDB` is a boolean parameter that determines
 * whether the generated token should be saved in the database or not. If set to `true`, the token will
 * be saved in the database, otherwise it won't be saved.
 * @param [expiresIn] - The expiresIn parameter is an optional parameter that specifies the expiration
 * time of the token. It can be expressed in seconds or a string describing a time span using the
 * [zeit/ms](https://github.com/zeit/ms.js) library. If this parameter is not provided, the token will
 * expire after the default
 * @returns The function `generateToken` returns a Promise that resolves to a string, which is the
 * generated token.
 */
const generateToken = async <T extends TokenTypes>(
  type: T,
  payload: TokenPayload<T>,
  userId?: Types.ObjectId,
  saveTokenInDB: boolean = true,
  /** expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js).  Eg: 60, "2 days", "10h", "7d" */
  expiresIn?: SignOptions["expiresIn"]
) => {
  const organizationId = payload.organizationId;
  try {
    const generatedToken = jwt.sign(
      payload,
      process.env.TOKEN_SECRET as string,
      {
        expiresIn: expiresIn || DEFAULT_ACCESS_TOKEN_EXPIRE_IN,
      }
    );
    if (saveTokenInDB) {
      // valid user id required to save token in database
      if (!userId || !isValidObjectId(userId))
        throw new Error("Unable to generate token. Invalid User Id.");
      // valid organization id also required to saved organization related token in database
      Iif (["organizationInvitationToken"].includes(type) && !organizationId)
        throw new Error("Unable to generate token. Invalid Organization Id.");
      await saveToken(type, generatedToken, userId, organizationId);
    }
    return generatedToken;
  } catch (error: any) {
    throw new Error(error.message || "Token not generated");
  }
};
 
/**
 * This will verify token and return payload,
 * @param token
 * @param deleteToken `default:true`
 * @param checkInDB `default:true`
 * @returns
 */
export const useToken = async (
  token: string,
  /** Should delete token from database after token verified successfully */
  deleteToken: boolean = true,
  /** Should token exist in database */
  checkInDB: boolean = true
) => {
  try {
    if (token.split(".").length !== 3) throw new Error(INVALID_TOKEN_MSG);
    const tokenPayload = jwt.verify(
      token,
      process.env.TOKEN_SECRET as string
    ) as JwtPayload;
 
    // if param value of checkInDB is true then this token should exist in database
    if (checkInDB) {
      const tokenDocument = await tokenModel.findOne({ token });
      const isTokenExistInDB = !!tokenDocument;
 
      if (!isTokenExistInDB) throw new Error(INVALID_TOKEN_MSG);
 
      // add token type in token payload
      tokenPayload["type"] = tokenDocument.type;
 
      // delete token from database if param value of 'deleteToken' is true
      if (deleteToken) await tokenModel.deleteOne({ token });
    }
    return tokenPayload;
  } catch (error: any) {
    if (
      ["JsonWebTokenError", "TokenExpiredError"].includes(error.name) ||
      error.message === INVALID_TOKEN_MSG
    )
      throw new Error(INVALID_TOKEN_MSG);
    throw error;
  }
};
 
/**
 * This will generate jwt token with user details in payload
 * @param user user object
 * @returns token
 */
export const generateAccessToken = (
  user: Pick<User, "name" | "email" | "_id">
) => {
  const payload = {
    name: user.name,
    email: user.email,
  };
  return generateToken("accessToken", payload, user._id);
};
 
/**
 * This will delete all access tokens of user
 * @param user user object
 * @returns token
 */
export const deleteAccessTokens = async (user: Pick<User, "_id">) => {
  try {
    await tokenModel.deleteMany({
      type: "accessToken",
      userId: user._id,
    });
    return true;
  } catch (_) {
    return false;
  }
};
 
/**
 * This will generate jwt token
 * @param user user object
 * @returns token
 */
export const generateRefreshToken = (
  user: Pick<User, "name" | "email" | "_id">
) => {
  const payload = {
    name: user.name,
    email: user.email,
  };
  return generateToken(
    "refreshToken",
    payload,
    user._id,
    true,
    DEFAULT_REFRESH_TOKEN_EXPIRE_IN
  );
};
 
/**
 * This will delete all refresh tokens of user
 * @param user user object
 * @returns token
 */
export const deleteRefreshTokens = async (user: Pick<User, "_id">) => {
  try {
    await tokenModel.deleteMany({
      type: "refreshToken",
      userId: user._id,
    });
  } catch (_error) {
    return false;
  }
  return true;
};
 
/**
 * Use this to generate email verification token
 * @param user
 * @returns token
 */
export const generateEmailVerificationToken = (
  user: Pick<User, "email" | "_id">
) => {
  const payload = {
    email: user.email,
  };
  return generateToken("emailVerificationToken", payload, user._id);
};
 
/**
 * This will delete all email verification tokens of user
 * @param user user object
 * @returns token
 */
export const deleteEmailVerificationTokens = async (
  user: Pick<User, "_id">
) => {
  try {
    await tokenModel.deleteMany({
      type: "emailVerificationToken",
      userId: user._id,
    });
  } catch (_error) {
    return false;
  }
  return true;
};
 
/**
 * Use this to generate reset password token
 * @param user
 * @returns token
 */
export const generateResetPasswordToken = (
  user: Pick<User, "email" | "_id">
) => {
  const payload = {
    email: user.email,
  };
  return generateToken("resetPasswordToken", payload, user._id);
};
 
/**
 * This will delete all reset password tokens of user
 * @param user user object
 * @returns token
 */
export const deleteResetPasswordTokens = async (user: Pick<User, "_id">) => {
  try {
    await tokenModel.deleteMany({
      type: "resetPasswordToken",
      userId: user._id,
    });
  } catch (_error) {
    return false;
  }
  return true;
};
 
/**
 * This TypeScript function generates an organization invitation token with a payload and a 7-day
 * expiration time.
 * @param payload - The payload parameter is an object of type UserOrganizationInvitation, with the
 * "_id", "status", and "token" properties omitted. This object contains the data that will be used to
 * generate the organization invitation token.
 * @returns The function `generateOrganizationInvitationToken` is returning a token generated using the
 * `generateToken` function. The token is of type "organizationInvitationToken" and is generated using
 * the `payload` object passed as an argument to the function. The token does not have an expiration
 * date and is not signed. The function returns the generated token.
 */
export const generateOrganizationInvitationToken = (
  payload: OrganizationInvitationTokenPayload
) => {
  return generateToken(
    "organizationInvitationToken",
    payload,
    payload.invitedByUserId,
    true,
    "7d"
  );
};