Shopify / koa-shopify-auth

DEPRECATED Middleware to authenticate a Koa application with Shopify
MIT License
80 stars 63 forks source link

403 Forbidden response when trying to process webhook #136

Closed evan4747 closed 1 year ago

evan4747 commented 2 years ago

Issue summary

I'm sure this is an error on my end but I am having trouble getting my Shopify app to process webhooks for the orders/paid event after the app has not been opened through my Shopify dashboard for a day. I used Shopify's example product-reviews-sample-app as the skeleton for my server.js. The app works exactly as intended and processes my graphql requests as long as the app has recently been opened.

Actual behavior

I am getting a 403 response "Failed to process webhook: Error: No webhook is registered for topic orders/paid." I have registered the webhook using 'accessMode: "offline"' and have setup a custom storage method to retrieve the offline access token from a postgresql database so I am not quite sure what the issue is.

Server.js code

import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "@shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
import { enqueueInvLineItemUpdateJob } from "./jobs/inventory-update-by-line-item";

import { SqlSessionStorage } from "./app-session";

dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
  dev,
});
const handle = app.getRequestHandler();

Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
  SCOPES: process.env.SCOPES
    ? process.env.SCOPES.split(",")
    : "read_orders",
  HOST_NAME: process.env.HOST.replace(/https:\/\//, ""),
  API_VERSION: ApiVersion.October20,
  IS_EMBEDDED_APP: true,
  // This should be replaced with your preferred storage strategy
  SESSION_STORAGE: SqlSessionStorage,
});

// Storing the currently active shops in memory will force them to re-login when your server restarts. You should
// persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};

app.prepare().then(async () => {
  const server = new Koa();
  const router = new Router();
  server.keys = [Shopify.Context.API_SECRET_KEY];

  server.use(
    createShopifyAuth({
      accessMode: "online",
      prefix: "/online",
      async afterAuth(ctx) {
        // Online access mode access token and shop available in ctx.state.shopify
        const { shop } = ctx.state.shopify;

        // Redirect to app with shop parameter upon auth
        // ctx.redirect(`/?shop=${shop}&host=${host}`);
        ctx.redirect(
          `https://${shop}/admin/apps/${process.env.SHOPIFY_API_KEY}`
        );
      },
    })
  );

  server.use(
    createShopifyAuth({
      accessMode: "offline",
      prefix: "/offline",
      async afterAuth(ctx) {
        // Access token and shop available in ctx.state.shopify
        const { shop, accessToken, scope } = ctx.state.shopify;

        ACTIVE_SHOPIFY_SHOPS[shop] = shop;

        let response = await Shopify.Webhooks.Registry.register({
          shop,
          accessToken,
          path: "/webhooks",
          topic: "APP_UNINSTALLED",
          webhookHandler: async (topic, shop, body) =>
            delete ACTIVE_SHOPIFY_SHOPS[shop],
        });

        if (!response.success) {
          console.log(
            `Failed to register APP_UNINSTALLED webhook: ${response.result}`
          );
        }

        response = await Shopify.Webhooks.Registry.register({
          shop,
          accessToken,
          path: "/webhooks",
          topic: "ORDERS_PAID",
          webhookHandler: async (topic, shop, body) => {
            enqueueInvLineItemUpdateJob(shop, JSON.parse(body));
          }
        });

        if (!response.success) {
          console.log(
            `Failed to register ORDERS_PAID webhook: ${response.result}`
          );
        }

        // Redirect to app with shop parameter upon auth
        ctx.redirect(`/online/auth/?shop=${shop}`);
      },
    })
  );

  const handleRequest = async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  };

  const verifyIfActiveShopifyShop = (ctx, next) => {
    const { shop } = ctx.query;

    // This shop hasn't been seen yet, go through OAuth to create a session
    if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
      ctx.redirect(`/offline/auth?shop=${shop}`);
      return;
    }

    return next();
  };

  router.post("/webhooks", async (ctx) => {
    try {
      await Shopify.Webhooks.Registry.process(ctx.req, ctx.res);
      console.log(`Webhook processed, returned status code 200`);
    } catch (error) {
      console.log(`Failed to process webhook: ${error}`);
    }
  });

  router.post(
    "/graphql",
    verifyRequest({ returnHeader: true }),
    async (ctx) => {
      await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
    }
  );

  router.get("(/_next/static/.*)", handleRequest); // Static content is clear
  router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
  // Embedded app Next.js entry point
  router.get("(.*)", verifyIfActiveShopifyShop, handleRequest);

  server.use(router.allowedMethods());
  server.use(router.routes());
  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

Any help is greatly appreciated and I can provide additional information if needed.

github-actions[bot] commented 1 year ago

Note that this repo is no longer maintained and this issue will not be reviewed. Prefer the official JavaScript API library. If you still want to use Koa, see simple-koa-shopify-auth for a potential community solution.