backend/lib/seed/graphql/Middleware.ts
2025-05-14 21:45:16 +02:00

205 lines
7.2 KiB
TypeScript

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<ApolloContext> => {
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<ApolloContext> = ({ 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<ApolloContext> => {
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(),
},
};
};