packages/deep-microservice-gateway/src/index.js
import {ApolloGateway, RemoteGraphQLDataSource} from '@apollo/gateway';
import {getPublicIP} from '@thinkdeep/get-public-ip';
import depthLimit from 'graphql-depth-limit';
import {ApolloServer} from 'apollo-server-express';
import express from 'express';
import jwt from 'express-jwt';
import {getLogger} from './get-logger.js';
import jwks from 'jwks-rsa';
const logger = getLogger();
/**
* Start the gateway.
*/
const startGatewayService = async () => {
const gateway = new ApolloGateway({
serviceList: [
{
name: 'analysis',
url: process.env.PREDECOS_MICROSERVICE_ANALYSIS_URL,
},
{
name: 'collection',
url: process.env.PREDECOS_MICROSERVICE_COLLECTION_URL,
},
{
name: 'configuration',
url: process.env.PREDECOS_MICROSERVICE_CONFIGURATION_URL,
},
],
buildService({url}) {
return new RemoteGraphQLDataSource({
url,
willSendRequest({request, context}) {
request.http.headers.set(
'permissions',
context?.req?.permissions
? JSON.stringify(context.req.permissions)
: null
);
request.http.headers.set(
'me',
context?.req?.me ? JSON.stringify(context.req.me) : null
);
},
});
},
logger,
});
const server = new ApolloServer({
gateway,
subscriptions: false,
context: ({req, res}) => ({req, res}),
csrfPrevention: true,
validationRules: [depthLimit(10)],
});
await server.start();
const extractPermissions = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: process.env.PREDECOS_AUTH_JWKS_URI,
}),
aud: process.env.PREDECOS_AUTH_AUDIENCE,
issuer: process.env.PREDECOS_AUTH_ISSUER,
algorithms: ['RS256'],
requestProperty: 'permissions',
});
const extractMe = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: process.env.PREDECOS_AUTH_JWKS_URI,
}),
aud: process.env.PREDECOS_AUTH_AUDIENCE,
issuer: process.env.PREDECOS_AUTH_ISSUER,
algorithms: ['RS256'],
requestProperty: 'me',
getToken: (req) => {
if (req.headers.me) {
return req.headers.me;
}
return '';
},
});
const app = express();
// NOTE: x-powered-by can allow attackers to determine what technologies are being used by software and
// therefore how to attack. Therefore, it's disabled here.
app.disable('x-powered-by');
// NOTE: This handler must be present here in order for authorization to correctly operate. If placed
// after server.applyMiddleWare(...) it simply doesn't execute. However, the presence of this handler
// before server.applyMiddleWare(...) breaks Apollo Explorer but applies authorization.
app.use(extractPermissions);
app.use(extractMe);
// NOTE: Placing a forward slash at the end of any allowed origin causes a preflight error.
let allowedOrigins = [
'https://predecos.com',
'https://www.predecos.com',
'https://thinkdeep-d4624.web.app',
'https://www.thinkdeep-d4624.web.app',
];
const isProduction = process.env.NODE_ENV.toLowerCase() === 'production';
if (!isProduction) {
allowedOrigins = allowedOrigins.concat([
/^https?:\/\/localhost:\d{1,5}/,
'https://studio.apollographql.com',
]);
}
const path = process.env.GRAPHQL_PATH;
if (!path) {
throw new Error(
`A path at which the application can be accessed is required (i.e, /graphql). Received: ${path}`
);
}
logger.debug(`Applying middleware.`);
server.applyMiddleware({
app,
path,
cors: {
origin: allowedOrigins,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS,CONNECT,TRACE',
credentials: true,
},
});
const port = Number(process.env.GRAPHQL_PORT);
if (!port) {
throw new Error(
`A port at which the application can be accessed is required. Received: ${port}`
);
}
app.listen({port}, () =>
logger.info(
`Server ready at http://${getPublicIP()}:${port}${server.graphqlPath}`
)
);
};
startGatewayService()
.then(() => {
/* Do nothing */
})
.catch((error) => {
logger.error(
`An Error Occurred: ${JSON.stringify(
error
)}, message: ${error.message.toString()}`
);
});