Frontend Serverless with React and GraphQL

Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.


43. Add Verify User Check

Objective: Create a function that will verify that the user can only execute a mutation with a user variable that matches their current user id in the GraphCMS server.

We need to write a function that will fetch the userId from the current auth0 session and ensure that it will match any user parameters that are passed as variables.

First, we need to create a new function called getUserObject that will recursively search through an object for any keys that match 'user' or 'owner'. If it finds them, it will return the corresponding key.

utils/getUserObject.ts

import * as _ from 'lodash';
export const getUserObject = (currentItem) => {
let result = null;
if (_.isArray(currentItem)) {
for (const item of currentItem) {
result = getUserObject(item);
if (result) {
break;
}
}
} else {
for (const prop in currentItem) {
if (prop === 'user') {
return currentItem.user;
} else if (prop === 'owner') {
return currentItem.owner;
}
if (_.isObject(currentItem[prop]) || _.isArray(currentItem[prop])) {
result = getUserObject(currentItem[prop]);
if (result) {
break;
}
}
}
}
return result;
};

The currentItem being passed in will be a variables object that may have a user or owner property within side it. An effective way to find these nested properties is to have the function be recursive.

We will iterate over the items in the array or object to look for properties that match and in the event that we find a nested array or object, we call the same function again and search through those properties.

If we ever find a matching property we will return the value of that property which we can compare against the user in a new function called verifyUserMatches which will call the getUserObject function. Add the verifyUserMatches function to the verify file so that it looks like this:

utils/verify.ts

import * as _ from 'lodash';
import { getUserObject } from './getUserObject';
import auth0 from './auth0';
export const verifyNotABannedMutation = async (req, res) => {
const isBannedMutation = req.body.query.match(
/deleteMany|updateMany|publishMany/g,
);
if (!_.isNil(isBannedMutation)) {
throw new Error('Invalid Mutation Requested');
}
};
export const verifyUserMutation = async (req, res) => {
const requestedUserId = getUserObject(req.body.variables);
if (!_.isNil(requestedUserId)) {
const { user } = await auth0.getSession(req);
const actualUserId: string = _.get(user, 'sub');
if (actualUserId !== requestedUserId) {
throw new Error('Invalid User Requested');
}
}
};

Finally, update the graphql api function to call verifyUserMatches along with verifyNotABannedMutation that we created in the previous step.

pages/api/graphql.ts

import getConfig from 'next/config';
import { GraphQLClient } from 'graphql-request';
import {
verifyNotABannedMutation,
verifyUserMutation,
} from '../../utils/verify';
const { serverRuntimeConfig } = getConfig();
const {
BRANCH,
GRAPHCMSURL,
GRAPHCMSPROJECTID,
GRAPHCMS_TOKEN,
} = serverRuntimeConfig.graphcms;
const graphqlEndpoint = `${GRAPHCMSURL}/${GRAPHCMSPROJECTID}/${BRANCH}`;
export const graphQLClient = new GraphQLClient(graphqlEndpoint, {
headers: {
authorization: `Bearer ${GRAPHCMS_TOKEN}`,
},
});
async function proxyGraphql(req, res) {
try {
await verifyNotABannedMutation(req, res);
await verifyUserMutation(req, res);
const { variables, query } = req.body;
const data = await graphQLClient.rawRequest(query, variables);
res.json(data);
} catch (e) {
res.json({ data: {}, errors: [{ message: e.message }] });
}
}
export default proxyGraphql;