import _ from 'lodash'; import { APIGatewayProxyEvent, Context } from 'aws-lambda'; import { ApolloError } from 'apollo-server-lambda'; import { AuthChecker, createMethodDecorator } from 'type-graphql'; import Firebase from '@seed/services/auth/FirebaseService'; import { ApolloContext } from '@seed/interfaces/context'; import AccountModel from 'src/accounts/account.model'; import Loaders from '@src/__indexes/__loaders'; import { AccountTypeEnum } from '@src/accounts/account.components'; import { simpleEstimator, getComplexity, fieldExtensionsEstimator } from 'graphql-query-complexity'; import { clog } from '@seed/helpers/Utils'; import { newError } from '@seed/helpers/Error'; import { SecurityLevelEnum } from '@services/module-accounts/account.components'; export const ctxMiddleware = async (ctx: { event?: APIGatewayProxyEvent; context?: Context; req?: any }): Promise => { const user: AccountModel = new AccountModel(); let headers: any = {}; let organisationId = null; try { if (ctx.event) headers = ctx.event.headers; else if (ctx.req) headers = ctx.req.headers; /* * Deal with Auth */ const authorization: string = headers['x-authorization'] || headers.authorization || headers.Authorization; if (authorization) { // Verify the token const firebaseRes = await Firebase.getInstance().tokenIdentify(authorization); // Verify that exists in DB await user.getOne({ _id: firebaseRes.uid }, null); /* * Deal with Security */ const securityCheck = user.securityCheck(); if (!securityCheck) { throw newError(2400, { security: user.security, securityLevel: process.env.SECURITY_LEVEL || SecurityLevelEnum.nothing, }); } /* * Deal with Organisation */ organisationId = headers.organisationId || headers.organisationid; if (organisationId) { // Check if admin if (user.types && user.types.includes(AccountTypeEnum.admin)) { console.log('[ADMIN BYPASS]'); } else if (!user._id || !user.organisationIds || !user.organisationIds?.includes(organisationId)) throw newError(2101, { accountOrganisationIds: user.organisationIds, organisationId: organisationId, }); console.log('[ORG]', organisationId); } console.log('[AUTH - ]', user.types, user?._id); } else { console.log('no auth'); } } catch (error) { throw error; } return { event: ctx.event || ctx.req || {}, context: ctx.context || ctx.req || {}, ctx: { organisationId: organisationId, user: user, loaders: new Loaders(), }, }; }; export const authMiddleware: AuthChecker = ({ root, args, context, info }, roles) => { const user = context.ctx.user; if (!user || !user._id) { throw newError(2004); } if (roles.length > 0) { roles.push(AccountTypeEnum.admin); const intersection = _.intersection(roles, user.types); if (intersection.length > 0) return true; // no roles matched, restrict access throw newError(2000, { you: user.types, allowed: roles }); } return true; }; export const checkOrganisation = (check = false): any => { return createMethodDecorator(async ({ root, args, context, info }, next) => { if (check && !(context as any).ctx.organisationId) throw newError(2102, '400'); return next(); }); }; export const totalPublic = (check = false): any => { return createMethodDecorator(async ({ root, args, context, info }, next) => { if (check) (context as any).ctx.noOrganisationCheck = true; return next(); }); }; export const errorMiddleware = (err): any => { // Don't give the specific errors to the client. if (err.extensions) { const { translations, ...data } = err.extensions; const errorToReturn = { code: err.extensions.code, message: err.extensions.message || err.message, translations, data: process.env.NODE_ENV == 'production' ? { message: 'see logs' } : data, }; clog(errorToReturn); return errorToReturn; } // Otherwise return the original error. The error can also // be manipulated in other ways, so long as it's returned. return err; }; export const complexityMiddleware = (schema: any, variables: any, query: any) => { const maximumComplexity = process.env.GRAPH_COMPLEXITY ? parseInt(process.env.GRAPH_COMPLEXITY) : 1000; const complexity = getComplexity({ schema: schema, // The maximum allowed query complexity, queries above this threshold will be rejected // The query variables. This is needed because the variables are not available // in the visitor of the graphql-js library variables: variables, // specify operation name only when pass multi-operation documents query: query, // Add any number of estimators. The estimators are invoked in order, the first // numeric value that is being returned by an estimator is used as the field complexity. // If no estimator returns a value, an exception is raised. estimators: [ // Add more estimators here... fieldExtensionsEstimator(), // This will assign each field a complexity of 1 if no other estimator // returned a value. simpleEstimator({ defaultComplexity: 1, }), ], }); // Here we can react to the calculated complexity, // like compare it with max and throw error when the threshold is reached. if (complexity >= maximumComplexity) { throw new Error(`Sorry, too complicated query! ${complexity} is over ${maximumComplexity} that is the max allowed complexity.`); } // And here we can e.g. subtract the complexity point from hourly API calls limit. console.log('Used query complexity points:', complexity); }; export const oneToManyComplexity = ({ args, childComplexity }): number => { if (args.pagination && args.pagination.limit) return childComplexity * args.pagination.limit; else return childComplexity * 10; }; export const oneToManyComplexityNoLoader = ({ args, childComplexity }): number => { if (args.pagination && args.pagination.limit) return childComplexity * args.pagination.limit * 100; else return childComplexity * 10 * 100; }; export const createApolloContext = async (accountId?: string, organisationId: string | null = null): Promise => { const user: AccountModel = new AccountModel(); if (accountId) { await user.getOne({ _id: accountId }, null); } return { event: {} as any, context: {} as any, ctx: { organisationId, user: user, loaders: new Loaders(), }, }; };