GraphQL Federation 上传文件 - 此操作已被阻止为潜在的跨站点请求伪造 (CSRF)

问题描述 投票:0回答:0

我尝试将图像上传到 Cloudflare R2 存储桶。但是当我在客户端上传文件时,该文件成功到达网关。完成此步骤后,它不会转到子图并在 devtools 上给出此错误;

{
    "errors": [
        {
            "message": "This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Please either specify a 'content-type' header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain) or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight\n",
            "extensions": {
                "code": "BAD_REQUEST",
                "stacktrace": [
                    "GraphQLError: This operation has been blocked as a potential Cross-Site Request Forgery (CSRF). Please either specify a 'content-type' header (with a type that is not one of application/x-www-form-urlencoded, multipart/form-data, text/plain) or provide a non-empty value for one of the following headers: x-apollo-operation-name, apollo-require-preflight",
                    "",
                ],
                "serviceName": "userService"
            }
        }
    ],
    "data": {
        "uploadUserPhoto": null
    }
}

我将

apollo-require-preflight:true
添加到客户端的标题中。但是我不太确定在 Gateway 和 Subgraphs 之间添加相同的标头。

客户端

上传组件

import { gql, useMutation } from '@apollo/client';

const UPLOAD = gql`
  mutation UploadUserPhoto($photo: Upload) {
    uploadUserPhoto(photo: $photo)
  }
`;
function FileUploadSingle() {
  const [upload] = useMutation(UPLOAD);

  const handleFileUpload = async (event: any) => {
    const file = event.target.files[0];
    try {
      await upload({
        variables: {
          photo: file,
        },
      });
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div>
      <input type="file" onChange={(e: any) => handleFileUpload(e)} />
    </div>
  );
}

export default FileUploadSingle;

index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { createUploadLink } from 'apollo-upload-client';
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  from,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

const uploadLink = createUploadLink({
  uri: 'http://localhost:4000/graphql',
  headers: { 'apollo-require-preflight': 'true' },
});

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('token');

  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : '',
    },
  };
});

const client = new ApolloClient({
  link: from([authLink, uploadLink]),
  cache: new InMemoryCache(),
});

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>
);

服务器端

网关/index.ts

import { getOriginsAccordingToEnvironment } from "./helpers";
import { ApolloGateway, IntrospectAndCompose } from "@apollo/gateway";
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import express from "express";
import { json } from "body-parser";
import cors from "cors";
import { ENVIRONMENTS } from "./constants";
import { verifyToken } from "./helpers";
import  graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.js";
import waitOn from "wait-on";
require("dotenv").config();
const app = express();

import FileUploadDataSource from "@profusion/apollo-federation-upload";

const startServer = async () => {
  const environment = process.env.NODE_ENV as string;

  let gateway;

  if (environment === ENVIRONMENTS.DEVELOPMENT) {
    gateway = new ApolloGateway({
      supergraphSdl: new IntrospectAndCompose({
        subgraphs: [
          { name: "userService", url: process.env.USER_SERVICE_URL },
          { name: "paymentService", url: process.env.PAYMENT_SERVICE_URL },
          { name: "productService", url: process.env.PRODUCT_SERVICE_URL },
        ],
      }),
      buildService({ name, url }) {
        return new FileUploadDataSource({
          url,
          useChunkedTransfer: true,
          willSendRequest({
            request,
            context,
          }: {
            request: any;
            context: any;
          }) {

            console.log(request.variables) // THIS GIVES UPLOADED FILE'S INFORMATIONS
        
            request.http.headers.set(
              "user",
              context.id && context.fullName && context.email && context.isAuth
                ? JSON.stringify(context)
                : null
            );
          },
        });
      },
    });
  }

  const server = new ApolloServer({
    gateway,
  });

  await server.start();
  app.use(graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));

  app.use(
    "/graphql",
    cors({
      origin: getOriginsAccordingToEnvironment(environment),
    }),
    json(),
    expressMiddleware(server, {
      context: async ({ req }: { req: any }) => {
        interface JwtPayload {
          id: string;
          iat: number;
          exp: number;
        }
        const verifiedToken = verifyToken(
          req.headers.authorization
        ) as JwtPayload;

        const user = verifiedToken || null;
        return user;
      },
    })
  );

  app.listen({ port: 4000 }, () => {
    console.log(`🚀 Server ready at http://localhost:4000/graphql`);
  });
};

const startGateWayWithAllServices = () => {
  const waitOnOptions = {
    resources: ["tcp:4001", "tcp:4002", "tcp:4003"],
  };
  waitOn(waitOnOptions)
    .then(() => {
      startServer();
    })
    .catch((err) => {
      console.error("ERR:", err);
    });
};

startGateWayWithAllServices();

用户子图 / resolver.ts

import UserController from "../../controllers/user";
import { IUser } from "../../types";
import { uploadToStorage } from "../../helpers/image-upload";
import { nanoid } from "nanoid";

const resolvers = {

  Mutation: {
 
    async uploadUserPhoto(_: any, { photo }: { photo: any }, ctx: any) {
       if (!ctx || !ctx.id || !ctx.isAuth) {
         throw new Error("You have to login!");
       }


      const uploadedImage = await uploadToStorage({
        filename: `${ctx.id}`,
        file: photo,
      });

      await UserController.updateUserAvatarById({
        userId: ctx.id,
        avatarURL: uploadedImage.Location,
      });
    },
  },

  User: {
    async __resolveReference(user: any) {
      return await UserController.findUserById({ id: user.id });
    },
  },
};

export { resolvers };

用户子图/index.ts

require("dotenv").config();
import { ApolloServer } from "@apollo/server";
import { buildSubgraphSchema } from "@apollo/subgraph";
import gql from "graphql-tag";
import { resolvers } from "./graphql/resolvers/user";
import { startStandaloneServer } from "@apollo/server/standalone";
import { readFileSync } from "fs";
const mongoose = require("mongoose");

const typeDefs = gql(
  readFileSync(`${__dirname}/graphql/types/user.graphql`).toString("utf-8")
);

import { DB_URI } from "../database-config";

const createDatabaseConnection = async () => {
  try {
    mongoose.set("strictQuery", false);
    await mongoose.connect(DB_URI);
    console.log("✅ User Service: Connected to DB");
  } catch (error) {
    console.log("⛔", error);
  }
};

const startUserServiceServer = async () => {
  const server = new ApolloServer({
    schema: buildSubgraphSchema({ typeDefs, resolvers }),
  });

  await createDatabaseConnection();

  const { url } = await startStandaloneServer(server, {
    context: async ({ req }: { req: any }) => {
      const user = req.headers.user ? JSON.parse(req.headers.user) : null;

      return user;
    },
    listen: { port: 4001 },
  });

  console.log(`🚀 User Service: Server ready at ${url}`);
};

startUserServiceServer();

javascript graphql apollo apollo-server apollo-federation
© www.soinside.com 2019 - 2024. All rights reserved.