Re: Node App Custom Session Storage needs re-authenticating every 24 hours

Solved

Node App Custom Session Storage needs re-authenticating every 24 hours

Brandammo
Shopify Partner
7 0 5

I have been trying to get this to work for months and i can't find anything that makes any sense.

 

I have set up my node app to use CustomSessionStorage with the session being stored to FireBase Firestore DB.

 

I can see the session being stored in the DB but the app needs re-authenticating every 24 hours or so. I read that setting accessMode: 'offline' would remove the issue of any session expiry but it hasn't made a difference.

 

Any advice or if i am missing something would be greatly appreciated.

 

Session in DB

 

{
"id":"offline_jaques-of-london.myshopify.com",
"shop":"jaques-of-london.myshopify.com",
"state":"105619287389410",
"isOnline":false,
"accessToken":"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",
"scope":"read_products,write_discounts,write_script_tags"
}

 

 

server.js

 

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 gql from "graphql-tag";
import db from './firebase';
import moment from 'moment';
import FirebaseSessionHandler from "./firebase-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();
const sessionHandler = new FirebaseSessionHandler();
Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
  SCOPES: process.env.SCOPES.split(","),
  HOST_NAME: process.env.HOST.replace(/https:\/\//, ""),
  API_VERSION: ApiVersion.October20,
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
    sessionHandler.storeCallback,
    sessionHandler.loadCallback,
    sessionHandler.deleteCallback
  ),
});
const createDiscountCode = require('./createDiscountCode');

const QUERY_SCRIPT_TAGS = gql`
    query {
        scriptTags(first: 5) {
            edges {
                node {
                    id
                    src
                    displayScope
                }
            }
        }
    }
`;

// 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();
  let setAccessToken;
  let setShop;
  server.keys = [Shopify.Context.API_SECRET_KEY];
  server.use(
    createShopifyAuth({
      accessMode: 'offline',
      async afterAuth(ctx) {
        // Access token and shop available in ctx.state.shopify
        const { shop, accessToken, scope } = ctx.state.shopify;
        setAccessToken = accessToken;
        setShop = shop;
        const host = ctx.query.host;
        ACTIVE_SHOPIFY_SHOPS[shop] = scope;

        const 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}`
          );
        }

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

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

  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, next) => {
      await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
    }
  );

  //Get the winning ratios
  router.get("/echo/prizes", async (ctx, next) => {
    try {
      const res = await db.collection("shopify").doc('config').get();
      const json = await res.data();
      ctx.body = {
        status: 'success',
        data: json
      }
    } catch (error) {
      console.log(`Failed to process webhook: ${error}`);
    }
  });

  //Check the code exists and the status
  router.get("/echo/check/:code", async (ctx, next) => {
    //console.log(ctx.params);
    try {
      const res = await db.collection("codes").doc(`${ctx.params.code}`).get();
      const json = await res.data();
      ctx.body = {
        status: 'success',
        params: ctx.params,
        data: json
      }
    } catch (error) {
      console.log(`Failed to process webhook: ${error}`);
    }
  });

  //Create the code
  router.get("/echo/create/:code/:prize", async (ctx, next) => {
    let codeCode = ctx.params.code;
    let codePrize = parseInt(ctx.params.prize);
    let startDate = moment().utc().format();
    let endDate = moment().add(48, 'hours').utc().format();
    try {
      const codeData = await createDiscountCode(setAccessToken, setShop, codeCode, codePrize, startDate, endDate);
      ctx.body = {
        status: 'success',
        data: codeData,
        end: moment().add(48, 'hours').format('YYYY-MM-DD HH:mm:ss')
      }
      //Add code to prizes
      const data = {
        prize: codePrize,
        claimed: moment().format("DD/MM/YYYY HH:mm:ss"),
        campaign: `${codeCode.slice(0, 2)}`,
      };
      const addPrize = await db.collection('prizes').doc(`${codeCode}`).set(data);
      //Update code status
      db.collection("codes").doc(`${codeCode}`).update({
        'redeemed': true,
        'used': moment().format("DD/MM/YYYY HH:mm:ss")
      });
    } catch (error) {
      console.log(`Failed to process webhook: ${error}`);
    }
  });

  //Email
  router.get("/echo/email/:email/:code/:prize", async (ctx, next) => {
    try {
      let sendEmail = ctx.params.email;
      let sendCode = ctx.params.code;
      let sendPrize = ctx.params.prize;

      const response = await fetch('https://hooks.zapier.com/hooks/catch/411924/bua3yph/', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email: `${sendEmail}`,
          latest_code: `${sendCode}`, 
          code_prize: `${sendPrize}`
        })
      }).then(function (response) {
        return response.json();
      });
      ctx.body = {
        status: 'success',
        data: response
      }
    } catch (error) {
      console.log(`Failed to process webhook: ${error}`);
    }
  });

  router.get("(/_next/static/.*)", handleRequest); // Static content is clear
  router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
  router.get("(.*)", async (ctx) => {
    const shop = ctx.query.shop;

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

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

});

 

 

firebase-session.js

 

import db from "./firebase";

class FirebaseSessionHandler {
  async storeCallback(session) {
    try {
      let doc = db.collection('session').doc(session.id);
      await doc.set({
        session: JSON.stringify(session)
      })

      return true
    } catch (err) {
      throw new Error(err)
    }
  };
  async loadCallback(id) {
    try {
      return await db.collection('session').doc(id).get().then(item => {
        return JSON.parse(item.data().session);
      });
    } catch (err) {
      throw new Error(err)
    }
  };
  async deleteCallback(id) {
    try {
      await db.collection('session').doc(id).delete();
      return true
    } catch (err) {
      throw new Error(err)
    }
  };
}

export default FirebaseSessionHandler

 

_app.js

 

import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
import App from "next/app";
import { AppProvider } from "@shopify/polaris";
import { Provider, useAppBridge } from "@shopify/app-bridge-react";
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import { Redirect } from "@shopify/app-bridge/actions";
import "@shopify/polaris/dist/styles.css";
import translations from "@shopify/polaris/locales/en.json";

function userLoggedInFetch(app) {
  const fetchFunction = authenticatedFetch(app);

  return async (uri, options) => {
    const response = await fetchFunction(uri, options);

    if (
      response.headers.get("X-Shopify-API-Request-Failure-Reauthorize") === "1"
    ) {
      const authUrlHeader = response.headers.get(
        "X-Shopify-API-Request-Failure-Reauthorize-Url"
      );

      const redirect = Redirect.create(app);
      redirect.dispatch(Redirect.Action.APP, authUrlHeader || `/auth`);
      return null;
    }

    return response;
  };
}

function MyProvider(props) {
  const app = useAppBridge();

  const client = new ApolloClient({
    fetch: userLoggedInFetch(app),
    fetchOptions: {
      credentials: "include",
    },
  });

  const Component = props.Component;

  return (
    <ApolloProvider client={client}>
      <Component {...props} />
    </ApolloProvider>
  );
}

class MyApp extends App {
  render() {
    const { Component, pageProps, host } = this.props;
    return (
      <AppProvider i18n={translations}>
        <Provider
          config={{
            apiKey: API_KEY,
            host: host,
            forceRedirect: true,
          }}
        >
          <MyProvider Component={Component} {...pageProps} />
        </Provider>
      </AppProvider>
    );
  }
}

MyApp.getInitialProps = async ({ ctx }) => {
  return {
    host: ctx.query.host,
  };
};

export default MyApp;

 

 

Accepted Solution (1)

bishpls
Shopify Partner
26 6 21

This is an accepted solution.

Did you see this comment in the template file? This is the root of your problem -- your underlying CustomSessionStorage needs to connect to a database, and you need an independent accounting of whether or not an app has gone through the oAuth process and generated a Session / access token -- not the locally-scoped ACTIVE_SHOPIFY_SHOPS (which, as you've noted, will be wiped out on server restart)

 

// 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 = {};

 

View solution in original post

Replies 4 (4)

bishpls
Shopify Partner
26 6 21

This is an accepted solution.

Did you see this comment in the template file? This is the root of your problem -- your underlying CustomSessionStorage needs to connect to a database, and you need an independent accounting of whether or not an app has gone through the oAuth process and generated a Session / access token -- not the locally-scoped ACTIVE_SHOPIFY_SHOPS (which, as you've noted, will be wiped out on server restart)

 

// 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 = {};

 

Brandammo
Shopify Partner
7 0 5

Thank you for your help with this, that makes sense and is somewhat obvious now you have pointed it out. I appreciate your time.

buildpath
Shopify Partner
59 11 20

@Brandammo  could you share any hints or code for how you persisted active shopify shops in the db and checked for them?

 

I'm able to persist in db and check, but I'm running my check in the same app.use("/*") where the active-shopify-shops object was being set before, and it always runs twice quickly and fails the second time due to req.query being undefined... I've been searching 2 days for a solution

Ecom entrepreneur since 2004 | Shopify App developer since 2021 | Shopify merchant and theme developer since 2016
Meltin-Bit
Shopify Partner
27 0 10

Hi, I have the same problem. I implemented the storeCallback, loadCallback and deleteCallback...but I can't figure out where and how to set the 

ACTIVE_SHOPIFY_SHOPS. I'm using the last github repo with node and express and no more Koa and Next.
Meltin Bit