Skip to content

Instantly share code, notes, and snippets.

@sofyan-ahmad
Created March 24, 2021 03:38
Show Gist options
  • Save sofyan-ahmad/a12355aed17e91f1422fc6e8a15a870b to your computer and use it in GitHub Desktop.
Save sofyan-ahmad/a12355aed17e91f1422fc6e8a15a870b to your computer and use it in GitHub Desktop.
Strapi.io - cache graphql query request with custom api token and check user permission before return value from cache
// strapi/extensions/users-permissions/config/policies/permissions.js
const _ = require('lodash');
const crypto = require('crypto');
const redisClient = require('../../../../cache/redis');
const handleErrors = (ctx, err = undefined, type) => {
throw strapi.errors[type](err);
};
const cachePrefix = 'cms:token:';
module.exports = async (ctx, next) => {
let role;
if (ctx.state.user) {
// request is already authenticated in a different way
return next();
}
// add the detection of `token` query parameter
if (
ctx.request &&
ctx.request.header &&
(ctx.request.header.authorization || ctx.request.header.token)
) {
try {
// init `id` and `isAdmin` outside of validation blocks
let id;
// let isAdmin;
if (ctx.request.header.token) {
const reqToken = ctx.request.header.token;
const redisKey = `${cachePrefix}${crypto.createHash('sha1').update(reqToken).digest('base64')}`;
let token = null;
// find the token entry on cache
const fromCache = await redisClient.get(redisKey);
if (fromCache) {
token = JSON.parse(fromCache);
} else {
// find the token entry that match the token from the request on DB
const [fromDb] = await strapi.query('token').find({ token: reqToken });
if (fromDb) {
await redisClient.set(redisKey, JSON.stringify(fromDb));
}
token = fromDb;
}
if (!token) {
return handleErrors(ctx, 'Invalid token: This token doesn\'t exist', 'unauthorized');
}
if (token.user && typeof token.token === 'string') {
id = token.user.id;
// isAdmin = false;
}
delete ctx.request.query.token;
} else if (ctx.request.header.authorization) {
// use the current system with JWT in the header
const decrypted = await strapi.plugins['users-permissions'].services.jwt.getToken(ctx);
id = decrypted.id;
// isAdmin = decrypted.isAdmin || false;
}
if (id === undefined) {
throw new Error('Invalid token: Token did not contain required fields');
}
// fetch authenticated user
ctx.state.user = await strapi.plugins[
'users-permissions'
].services.user.fetchAuthenticatedUser(id);
} catch (err) {
return handleErrors(ctx, err, 'unauthorized');
}
if (!ctx.state.user) {
return handleErrors(ctx, 'User Not Found', 'unauthorized');
}
role = ctx.state.user.role;
if (role.type === 'root') {
return await next();
}
const store = await strapi.store({
environment: '',
type: 'plugin',
name: 'users-permissions',
});
if (
_.get(await store.get({ key: 'advanced' }), 'email_confirmation') &&
!ctx.state.user.confirmed
) {
return handleErrors(ctx, 'Your account email is not confirmed.', 'unauthorized');
}
if (ctx.state.user.blocked) {
return handleErrors(
ctx,
'Your account has been blocked by the administrator.',
'unauthorized',
);
}
}
// Retrieve `public` role.
if (!role) {
role = await strapi.query('role', 'users-permissions').findOne({ type: 'public' }, []);
}
const { route } = ctx.request;
const permission = await strapi.query('permission', 'users-permissions').findOne(
{
role: role.id,
type: route && route.plugin ? route.plugin : 'application',
controller: route.controller,
action: route && route.action ? route.action : 'find',
enabled: true,
},
[],
);
if (!permission) {
return handleErrors(ctx, undefined, 'forbidden');
}
// Execute the policies.
if (permission.policy) {
return await strapi.plugins['users-permissions'].config.policies[permission.policy](ctx, next);
}
// Execute the action.
return next();
};
// strapi/extensions/graphql/config/settings.js
const apolloServerPluginResponseCache = require('apollo-server-plugin-response-cache');
const { RedisCache } = require('apollo-server-cache-redis');
const _ = require('lodash');
const pluralize = require('pluralize');
const permission = require('../../users-permissions/config/policies/permissions');
// set this to whatever you believe should be the max age for your cache control
const MAX_AGE = 60 * 60 * 24; // 1 day
async function checkCachePermission(requestContext) {
try {
if (requestContext && requestContext.operation && requestContext.operation.operation !== 'query') {
return false;
}
// resolve selection set
if (requestContext && requestContext.operation && requestContext.operation.kind === 'OperationDefinition') {
if (requestContext.operation.selectionSet.selections.length) {
const findField = _.find(requestContext.operation.selectionSet.selections, { kind: 'Field' });
if (findField && findField.name && findField.name.value) {
const ctx = requestContext.context.context;
let controller = findField.name.value;
let action = 'findone';
if (pluralize.isPlural(controller)) {
controller = pluralize.singular(controller);
action = 'find';
}
ctx.request.route = {
controller,
action,
};
await permission(ctx, () => true);
return true;
}
}
}
return false;
} catch (e) {
console.log('Cache Permissions', e);
return false;
}
}
async function sessionId(requestContext) {
// return a session ID here, if there is one for this request
return null;
}
// decide if we should write to the cache in this request
async function shouldReadFromCache(requestContext) {
return checkCachePermission(requestContext);
}
// decide if we should write to the cache in this request
async function shouldWriteToCache(requestContext) {
return checkCachePermission(requestContext);
}
async function extraCacheKeyData(requestContext) {
// use this to create any extra data that can be used for the cache key
}
function injectCacheControl() {
return {
requestDidStart(requestContext) {
requestContext.overallCachePolicy = {
scope: 'PUBLIC', // or 'PRIVATE'
maxAge: MAX_AGE,
};
},
};
}
module.exports = {
federation: false,
apolloServer: {
tracing: false,
persistedQueries: { ttl: 10 * MAX_AGE }, // we set this to be a factor of 10, somewhat arbitrary
cacheControl: { defaultMaxAge: MAX_AGE },
plugins: [
apolloServerPluginResponseCache({
shouldReadFromCache,
shouldWriteToCache,
extraCacheKeyData,
sessionId,
}),
injectCacheControl(),
],
},
};
if (process.env.CACHE_HOST && process.env.CACHE_PORT) {
const cache = new RedisCache(`${process.env.CACHE_HOST}:${process.env.CACHE_PORT}`);
module.exports.apolloServer.cache = cache;
module.exports.apolloServer.persistedQueries.cache = cache;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment