Created
March 24, 2021 03:38
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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