maticzav / graphql-middleware

Split up your GraphQL resolvers in middleware functions
MIT License
1.14k stars 56 forks source link

Use graphql-middleware in subscriptions like queries and mutations #366

Open jpbidal opened 3 years ago

jpbidal commented 3 years ago

Hello, I want to use graphql-middleware in subscriptions to validate if users are authenticated. In queries and mutations it works, but not in subscriptions. Here is my definition of middleware:

import { ApolloError } from 'apollo-server-express'

const isLoggedIn = async (
  resolve: any,
  parent: any,
  args: any,
  context: any,
  info: any,
) => {
  if (context.isUnauthenticated()) {
    throw new ApolloError(`User not authenticated.`)
  }
  return resolve()
}

export const middlewares = {
  Query: {
    myQuery: isLoggedIn
  },
  Mutation: {
    myMutation: isLoggedIn
  },
  Subscription: {
    mySubscription: isLoggedIn,
  },
}

Thanks!

maticzav commented 3 years ago

That's interesting!

I know there's a part of the code that should take care of the subscriptions wrapping. Could you compose a short reproduction so I can investigate this?

jcpage commented 2 years ago

Hey - I commented in a related issue in the graphql-shield project:

https://github.com/maticzav/graphql-shield/issues/27#issuecomment-986198813

It has a lot of detail but the summary is the middleware code that is supposed to wrap the 'subscribe' field ends up finding the subscription's 'resolve' field first and wrapping it instead. Swapping the order of the checks would let it wrap subscription instead (which I believe is preferrable) or some extra work could let it wrap both (which I'm not sure is that useful?)

jpbidal commented 2 years ago

Hi! I apologize for the delay of answering your request. I'm using an workaround into each subscription resolve, like @jcpage says.

The isLoggedIn method could be simulated as a console log, but the real action is validate the user data from context.

When I run queries and mutations all works ok, but isLoggedIn method is ignored when I run subscriptions.

I attach an example of subscription server. I hope it helps you. Thank you so much for you job and congrats for it.

import express from 'express';
import http from 'http';
import { ApolloServer } from 'apollo-server-express';
import {
  ApolloServerPluginLandingPageGraphQLPlayground,
  ApolloServerPluginLandingPageDisabled,
} from 'apollo-server-core';
import { execute, subscribe } from 'graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
const { graphqlUploadExpress } = require('graphql-upload');
import { schema } from './graphql/schema/schema';
import { createContext } from './context';
import { settings } from './settings';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

async function startApolloServer() {
  const app = express();

  const optionsRedis = {
    host: settings.REDIS_HOST,
    port: settings.REDIS_PORT,
  };

  const pubsub = new RedisPubSub({
    publisher: new Redis(optionsRedis),
    subscriber: new Redis(optionsRedis),
  });

  app.use(graphqlUploadExpress());

  const httpServer = http.createServer(app);

  const subscriptionServer = SubscriptionServer.create(
    {
      schema,
      execute,
      subscribe,
      onConnect: (connectionParams: any) => {
        if (connectionParams.Authorization) {
          return createContext(
            settings,
            pubsub,
            connectionParams.Authorization,
          );
        }
      },
    },
    {
      server: httpServer,
      path: settings.GRAPHQL_ENDPOINT,
    },
  );

  const playground = ApolloServerPluginLandingPageGraphQLPlayground()
  const subscriptions = {
    async serverWillStart() {
      return {
        async drainServer() {
          subscriptionServer.close();
        },
      };
    },
  };

  const apolloServer = new ApolloServer({
    schema,
    context: (expressCtx) =>
      createContext(
        settings,
        pubsub,
        expressCtx.req.headers.authorization,
      ),
    introspection: true,
    plugins: [playground, subscriptions],
  });
  await apolloServer.start();

  apolloServer.applyMiddleware({
    app,
    cors: {
      origin: settings.CORS_ORIGIN,
      methods: settings.CORS_METHODS,
    },
    path: settings.GRAPHQL_ENDPOINT,
  });

  httpServer.listen(settings.PORT, () =>
    console.log(
      `🚀  Server ready at ${settings.PROTOCOL}://${settings.HOSTNAME}:${settings.PORT}${settings.GRAPHQL_ENDPOINT} - mode: ${ENVIRONMENTS.NODE_ENV}`,
    ),
  );

}

startApolloServer().catch((err) => {
  console.log(err);
});

Some libraries installed:

{
  "apollo-server-core": "^3.6.3",
  "apollo-server-express": "^3.6.3",
  "express": "^4.17.2",
  "graphql": "15.8.0",
  "express-session": "^1.17.1",
  "graphql-middleware": "^6.1.13",
  "graphql-redis-subscriptions": "^2.4.2",
  "graphql-subscriptions": "^2.0.0",
  "graphql-upload": "^13.0.0",
  "graphql-passport": "^0.6.3",
  "subscriptions-transport-ws": "^0.11.0"
}
cuzzlor commented 1 year ago

Here's a suggested solution: https://github.com/dimatill/graphql-middleware/issues/561