2
\$\begingroup\$

This is something I've done a few times, but I've found it to feel a bit error-prone with so many conditions, and am wondering if anyone can point me in the direction of a cleaner way. This is a PATCH route for editing a user. Super admin and admin users are both able to change other users (with some limitations) while other types of users can only edit themselves.

router.patch('/:userId', async (req, res) => {
  const patcher = (req as AuthRequest).user;
  const otherUser = await database.getUserById(req.params.userId);
  const requestedUpdate = req.body;

  // 404 if user is not found.
  if (!otherUser) {
    return sendCannotFind(res);
  }

  // Basic validation of requestedUpdate
  if (requestedUpdate.userType && !isUserTypeValid(requestedUpdate.userType)) {
    return sendInvalidUserType(res);
  }

  if (patcher.userType === 'superAdmin') {
    // Super admin cannot demote self.
    if (otherUser.id === patcher.id && requestedUpdate.userType) {
      return sendCannotSetUserType(res);
    }

    // Super admin cannot edit other super admins
    if (otherUser.userType === 'superAdmin' && otherUser.id !== patcher.id) {
      return sendCannotEdit(res);
    }
  } else if (patcher.userType === 'admin') {
    // Admin cannot edit super admins
    if (otherUser.userType === 'superAdmin') {
      return sendCannotEdit(res);
    }

    // Admin cannot edit other admins
    if (otherUser.userType === 'admin' && otherUser.id !== patcher.id) {
      return sendCannotEdit(res);
    }

    // Admin cannot promote or demote themselves
    if (otherUser.id === patcher.id && requestedUpdate.userType) {
      return sendCannotSetUserType(res);
    }

    // Admin cannot promote anyone to admin or superAdmin
    if (requestedUpdate.userType === 'admin' || requestedUpdate.userType === 'superAdmin') {
      return sendCannotSetUserType(res);
    }
  } else {
    // Non-admins cannot edit anyone but themselves
    if (otherUser.id !== patcher.id) {
      return sendCannotEdit(res);
    }

    // Non-admins cannot promote or demote themselves
    if (requestedUpdate.userType && requestedUpdate.userType !== otherUser.userType) {
      return sendCannotSetUserType(res);
    }
  }

  await doEdit(otherUser, requestedUpdate);
  return res.json(otherUser);
});
```
\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

I think I came up with a better and more declarative way using Joi schemas in nested dictionaries.

The idea is that we first figure out these three things:

  1. Is the user editing themselves? (bool)
  2. What type of user is the editor? (admin, etc)
  3. What type of user is being edited? (admin, etc)

And based on those three things, we can lookup the correct Joi schema to use.

So the schema dictionary looks like this for the PATCH route:

// No one is allowed to edit their own user type, everyone can edit their own name.
const selfEditSchemas: { [key in UserType]?: Joi.Schema } = {
  [UserType.superAdmin]: Joi.object({
    userType: Joi.forbidden(),
    name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(),
  }),
  [UserType.admin]: Joi.object({
    userType: Joi.forbidden(),
    name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(),
  }),
  [UserType.user]: Joi.object({
    userType: Joi.forbidden(),
    name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(),
  }),
};

// We only allow admins and super admins to edit others, and we use different schemas
// depending on what type of user they are trying to edit.
const otherEditSchemas: { [key in UserType]: { [key in UserType]?: Joi.Schema } } = {
  [UserType.superAdmin]: {
    [UserType.admin]: Joi.object({ // (superAdmin can edit admins according to this schema)
      userType: Joi.string().valid(...Object.values(UserType)).optional(),
      name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(),
    }),
    [UserType.user]: Joi.object({ // (superAdmin can edit regular users according to this chema)
      userType: Joi.string().valid(...Object.values(UserType)).optional(),
      name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(),
    }),
  },
  [UserType.admin]: {
    [UserType.user]: Joi.object({ // (admin can edit regular users according to this schema)
      userType: Joi.forbidden(), // (admin cannot promote regular users)
      name: Joi.string().pattern(/^[a-zA-Z0-9_\-. ]{3,100}$/).optional(),
    }),
  },
  [UserType.user]: {}, // (user is not allowed to edit anyone else)
};

And now the logic for validating edits becomes a lot simpler and less error-prone IMO:

function validateUserEdit(req: express.Request, res: express.Response, next: express.NextFunction) {
  const userReq = req as UserRequest;

  const isSelf = userReq.user.id === userReq.otherUser.id;
  const schema = isSelf
    ? selfEditSchemas[userReq.user.userType]
    : otherEditSchemas[userReq.user.userType][userReq.otherUser.userType];

  if (!schema) {
    return res.status(403).json({
      message: 'Not allowed to edit that user.',
      code: ErrorCodes.FORBIDDEN_TO_EDIT_USER,
    });
  }

  const validateRes = schema.validate(req.body, { stripUnknown: true });
  if (validateRes.error) {
    res.status(400).json({
      message: `Invalid arguments: ${validateRes.error}`,
      code: ErrorCodes.INVALID_ARGUMENTS,
    });
  } else {
    req.body = validateRes.value;
    next();
  }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.