I just had to implement something like this for interfacing with ApolloGraphQL, because the calls to Apollo come in via a WebApp listener. Here’s the code in case it may be helfpul.
SERVER SIDE
import {ApolloServer} from "@apollo/server";
import {expressMiddleware} from "@apollo/server/express4";
import {ApolloServerPluginDrainHttpServer} from "@apollo/server/plugin/drainHttpServer";
// import express from "express";
import http from "http";
import cors from "cors";
import {PubSub, withFilter} from "graphql-subscriptions-continued";
import {WebSocketServer} from "ws";
import {useServer} from "graphql-ws/lib/use/ws";
import {makeExecutableSchema} from "@graphql-tools/schema";
import {MongodbPubSub} from 'graphql-mongodb-subscriptions';
import {getUserIdByLoginToken} from "./utils_server/meteor-apollo-utils";
import typeDefs from "./api/schema";
import {resolvers} from "./api/resolvers";
import {array_of_cors_origins} from "../both";
const requestIp = require('request-ip');
const schema = makeExecutableSchema({typeDefs, resolvers});
// https://www.youtube.com/watch?v=AcZ5dcYMwA4
// for non-Meteor users, use this:
// const app = express();
// https://forums.meteor.com/t/working-apollo-client-server-setup-with-context-for-both-queries-and-subscriptions/61770/2?u=vikr00001
const meteorInternalVersionOfExpress = WebAppInternals.NpmModules.express.module;
const app = meteorInternalVersionOfExpress()
const httpServer = http.createServer(app);
const wsServer = new WebSocketServer({
path: "/graphql",
noServer: true,
});
//https://forums.meteor.com/t/apollo-server-express-server-setup-for-subscriptions/57871/2?u=vikr00001
WebApp.httpServer.on("upgrade", function upgrade(request, socket, head) {
if (!request.url) {
return;
}
switch (request.url) {
case "/graphql":
return wsServer.handleUpgrade(request, socket, head, function done(ws) {
wsServer.emit("connection", ws, request);
});
default:
break;
}
});
const serverCleanUp =useServer({
schema,
context: async (ctx, msg, args) => {
let context = await getDynamicContext(ctx, msg, args, true)
return context;
},
}, wsServer);
// https://forums.meteor.com/t/solved-check-ip-address-on-api-applications/55443
function parseIp(req) {
let response =
(typeof req?.headers['x-forwarded-for'] === 'string'
&& req?.headers['x-forwarded-for'].split(',').shift())
|| req?.connection?.remoteAddress
|| req?.socket?.remoteAddress
|| req?.connection?.socket?.remoteAddress;
return response;
}
async function getDynamicContext(ctx, msg, args, isSubscription){
let token = null;
if (isSubscription){
token = ctx.connectionParams.authorization
}
else{
token = ctx.req.headers['token']
}
let user = null;
let userId = null;
try {
if ((!!token) && (token !== "null")) {
[user, userId] = await getUserIdByLoginToken(token);
}
} catch (error) {
console.log('context: ', error)
}
let clientIp = '';
try {
let request = ctx?.req || ctx.extra.request;
clientIp = request?.clientIp || parseIp(request) || request?.ip || request?.headers['x-forwarded-for'] || request?.connection?.remoteAddress || ctx?.res?.req?.headers["x-forwarded-for"] || request?.client?.remoteAddress;
console.log("in getDynamicContext. clientIp:", clientIp);
} catch {
console.log("in getDynamicContext. Couldn't get clientIp")
}
return {
user: user,
userId: userId,
clientIp: clientIp
};
}
const apolloServer = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({httpServer}),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanUp.dispose();
}
}
}
}
],
});
await apolloServer.start();
WebApp.handlers.use(
"/graphql",
cors({
origin: function (origin, callback) {
const allowedOrigins = array_of_cors_origins;
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
console.log('origin: ', origin)
console.log('array_of_cors_origins: ', array_of_cors_origins)
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}),
meteorInternalVersionOfExpress.json(),
requestIp.mw(),
expressMiddleware(apolloServer, {
context: async (ctx, msg, args) => {
const context = await getDynamicContext(ctx, msg, args, false);
return context;
}
})
);
CLIENT SIDE
import {ApolloClient, split, HttpLink, InMemoryCache, ApolloLink, ApolloProvider, createHttpLink} from "@apollo/client";
import {GraphQLWsLink} from "@apollo/client/link/subscriptions";
import {setContext} from "@apollo/client/link/context";
import {getMainDefinition} from "@apollo/client/utilities";
import {createClient} from "graphql-ws";
import {graphQLQueryEndpoint, graphQLSubscriptionEndpoint} from "../both";
const wsLink = graphQLSubscriptionEndpoint
? new GraphQLWsLink(
createClient({
url: graphQLSubscriptionEndpoint,
connectionParams: () => {
return {
authorization: localStorage.getItem("Meteor.loginToken") || "",
};
},
on: {
error: props => {
console.log("wsLink error", props);
},
},
}),
)
: null;
const httpLink = createHttpLink({
uri: graphQLQueryEndpoint,
fetchOptions: {
rejectUnauthorized: false,
},
});
// Middleware to add custom headers
const customHeadersMiddleware = new ApolloLink((operation, forward) => {
// Define your custom headers
const customHeaders = {
"token": localStorage.getItem("Meteor.loginToken")
};
// Use operation.setContext to add the custom headers to the request
operation.setContext(({ headers }) => ({
headers: {
...headers,
...customHeaders,
},
}));
return forward(operation);
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
// console.log('definition.kind: ', definition.kind)
// console.log('definition.operation: ', definition?.operation)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
customHeadersMiddleware.concat(wsLink),
customHeadersMiddleware.concat(httpLink)
)
const apolloClient = new ApolloClient({
link: splitLink,
cache: new InMemoryCache({}),
});
export {apolloClient};
You can see that on the client side, it grabs the token of the logged-in user, and adds it to the header, here:
const customHeaders = {
"token": localStorage.getItem("Meteor.loginToken")
};
// Use operation.setContext to add the custom headers to the request
operation.setContext(({ headers }) => ({
headers: {
...headers,
...customHeaders,
},
}));
Then on the server it gets the current Meteor user by a call to:
getUserIdByLoginToken()
Which comes from:
import {getUserIdByLoginToken} from "./utils_server/meteor-apollo-utils";