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);
});
```
asked Aug 28, 2020 at 1:52
\$\endgroup\$

1 Answer 1

1
\$\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();
 }
}
answered Sep 8, 2020 at 16:25
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.