diff --git a/.eslintignore b/.eslintignore index 8baaa3a8..5072732e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,3 +4,4 @@ node_modules /data/schema.sql /@app/graphql/index.* /@app/client/.next +**/build diff --git a/@app/lib/src/withApollo.tsx b/@app/lib/src/withApollo.tsx index 93858b81..97348484 100644 --- a/@app/lib/src/withApollo.tsx +++ b/@app/lib/src/withApollo.tsx @@ -1,17 +1,23 @@ import { getDataFromTree } from "@apollo/react-ssr"; -import { InMemoryCache } from "apollo-cache-inmemory"; +import { InMemoryCache, NormalizedCacheObject } from "apollo-cache-inmemory"; import { ApolloClient } from "apollo-client"; import { ApolloLink, split } from "apollo-link"; import { onError } from "apollo-link-error"; import { HttpLink } from "apollo-link-http"; import { WebSocketLink } from "apollo-link-ws"; import { getOperationAST } from "graphql"; -import withApolloBase from "next-with-apollo"; +import withApolloBase, { InitApolloOptions } from "next-with-apollo"; +import React from "react"; import { SubscriptionClient } from "subscriptions-transport-ws"; import ws from "ws"; import { GraphileApolloLink } from "./GraphileApolloLink"; +interface WithApolloOptions { + useNext?: boolean; + rootUrl?: string; +} + let wsClient: SubscriptionClient | null = null; export function resetWebsocketConnection(): void { @@ -29,18 +35,18 @@ function makeServerSideLink(req: any, res: any) { } function makeClientSideLink(ROOT_URL: string) { - const nextDataEl = document.getElementById("__NEXT_DATA__"); - if (!nextDataEl || !nextDataEl.textContent) { - throw new Error("Cannot read from __NEXT_DATA__ element"); + const nextDataEl = + typeof document !== "undefined" && document.getElementById("__NEXT_DATA__"); + const headers = {}; + if (nextDataEl && nextDataEl.textContent) { + const data = JSON.parse(nextDataEl.textContent); + headers["CSRF-Token"] = data.query.CSRF_TOKEN; } - const data = JSON.parse(nextDataEl.textContent); - const CSRF_TOKEN = data.query.CSRF_TOKEN; const httpLink = new HttpLink({ uri: `${ROOT_URL}/graphql`, - credentials: "same-origin", - headers: { - "CSRF-Token": CSRF_TOKEN, - }, + credentials: + process.env.NODE_ENV === "development" ? "include" : "same-origin", + headers, }); wsClient = new SubscriptionClient( `${ROOT_URL.replace(/^http/, "ws")}/graphql`, @@ -65,47 +71,61 @@ function makeClientSideLink(ROOT_URL: string) { return mainLink; } -export const withApollo = withApolloBase( - ({ initialState, ctx }) => { - const ROOT_URL = process.env.ROOT_URL; - if (!ROOT_URL) { - throw new Error("ROOT_URL envvar is not set"); - } +const getApolloClient = ( + { initialState, ctx }: InitApolloOptions, + withApolloOptions?: WithApolloOptions +): ApolloClient => { + const ROOT_URL = process.env.ROOT_URL || withApolloOptions?.rootUrl; + if (!ROOT_URL) { + throw new Error("ROOT_URL envvar is not set"); + } + + const onErrorLink = onError(({ graphQLErrors, networkError }) => { + if (graphQLErrors) + graphQLErrors.map(({ message, locations, path }) => + console.error( + `[GraphQL error]: message: ${message}, location: ${JSON.stringify( + locations + )}, path: ${JSON.stringify(path)}` + ) + ); + if (networkError) console.error(`[Network error]: ${networkError}`); + }); - const onErrorLink = onError(({ graphQLErrors, networkError }) => { - if (graphQLErrors) - graphQLErrors.map(({ message, locations, path }) => - console.error( - `[GraphQL error]: message: ${message}, location: ${JSON.stringify( - locations - )}, path: ${JSON.stringify(path)}` - ) - ); - if (networkError) console.error(`[Network error]: ${networkError}`); - }); + const { req, res }: any = ctx || {}; + const isServer = typeof window === "undefined"; + const mainLink = + isServer && req && res + ? makeServerSideLink(req, res) + : makeClientSideLink(ROOT_URL); - const { req, res }: any = ctx || {}; - const isServer = typeof window === "undefined"; - const mainLink = - isServer && req && res - ? makeServerSideLink(req, res) - : makeClientSideLink(ROOT_URL); + const client = new ApolloClient({ + link: ApolloLink.from([onErrorLink, mainLink]), + cache: new InMemoryCache({ + dataIdFromObject: (o) => + o.__typename === "Query" + ? "ROOT_QUERY" + : o.id + ? `${o.__typename}:${o.id}` + : null, + }).restore(initialState || {}), + }); - const client = new ApolloClient({ - link: ApolloLink.from([onErrorLink, mainLink]), - cache: new InMemoryCache({ - dataIdFromObject: (o) => - o.__typename === "Query" - ? "ROOT_QUERY" - : o.id - ? `${o.__typename}:${o.id}` - : null, - }).restore(initialState || {}), - }); + return client; +}; - return client; - }, - { - getDataFromTree, - } -); +const withApolloWithNext = withApolloBase(getApolloClient, { + getDataFromTree, +}); + +const withApolloWithoutNext = (Component: any, options?: WithApolloOptions) => ( + props: any +) => { + const apollo = getApolloClient({}, options); + return ; +}; + +export const withApollo = (Component: any, options?: WithApolloOptions) => + options?.useNext === false + ? withApolloWithoutNext(Component, options) + : withApolloWithNext(Component); diff --git a/@app/server/src/middleware/installCSRFProtection.ts b/@app/server/src/middleware/installCSRFProtection.ts index e26c2777..17c69695 100644 --- a/@app/server/src/middleware/installCSRFProtection.ts +++ b/@app/server/src/middleware/installCSRFProtection.ts @@ -1,5 +1,20 @@ import csrf from "csurf"; import { Express } from "express"; +import url from "url"; + +const skipList = process.env.CSRF_SKIP_REFERERS + ? process.env.CSRF_SKIP_REFERERS?.replace(/s\s/g, "") + .split(",") + .map((s) => { + // It is prefixed with a protocol + if (s.indexOf("//") !== -1) { + const { host: skipHost } = url.parse(s); + return skipHost; + } + + return s; + }) + : []; export default (app: Express) => { const csrfProtection = csrf({ @@ -21,6 +36,12 @@ export default (app: Express) => { ) { // Bypass CSRF for GraphiQL next(); + } else if ( + skipList && + skipList.includes(url.parse(req.headers.referer || "").host) + ) { + // Bypass CSRF for named referers + next(); } else { csrfProtection(req, res, next); } diff --git a/package.json b/package.json index 69a2250c..572838b3 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,10 @@ "proseWrap": "always", "overrides": [ { - "files": ["*.yml", "*.yaml"], + "files": [ + "*.yml", + "*.yaml" + ], "options": { "printWidth": 120 }