Code Monkey home page Code Monkey logo

simple-koa-shopify-auth's People

Contributors

deniz-lktr avatar regexj avatar thesecuritydev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

simple-koa-shopify-auth's Issues

Getting Context Initialize error

Hi I was using koa auth for shopify, now since its depreciated, tried this library, first i was running koa auth on node 10.18.0 , on that , installing it gave me a error on redis, so upgraded node to 16.x and npm run build and npm start works fine, but when accessing the app, it crashes with Error: Context has not been properly initialized. Please call the .initialize() method to setup your app context object.
I am very new to node js , pls help, below is my code in server.js

/**
require('isomorphic-fetch');
const dotenv = require('dotenv');
const Koa = require('koa');
const next = require('next');
//const {default: createShopifyAuth} = require('@shopify/koa-shopify-auth');
//const {verifyRequest} = require('@shopify/koa-shopify-auth');
const session = require('koa-session');

dotenv.config();

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

const {SHOPIFY_API_SECRET_KEY, SHOPIFY_API_KEY} = process.env;
const { createShopifyAuth } = require("simple-koa-shopify-auth");
const { verifyRequest } = require("simple-koa-shopify-auth");
const verifyApiRequest = verifyRequest({ returnHeader: true });
const verifyPageRequest = verifyRequest();
app.prepare().then(() => {
const server = new Koa();

server.use(session({ secure: true, sameSite: 'none' }, server));
server.keys = [SHOPIFY_API_SECRET_KEY];

server.use(
        createShopifyAuth({
            apiKey: SHOPIFY_API_KEY,
            secret: SHOPIFY_API_SECRET_KEY,
            scopes: ['read_orders,write_orders,read_shipping,read_products,write_products'],
             accessMode: 'offline',
               authPath: "/auth",  // Where to redirect if the session is invalid

            async afterAuth(ctx) {
                const {shop, accessToken} = ctx.state.shopify;
          //      ctx.cookies.set('shopOrigin', shop, {httpOnly: false});
          //       ctx.cookies.set('shopOrigin', shop, {httpOnly: false,secure: true,sameSite: 'none'});

                const axios = require('axios');
                const https = require('https');
                // return this promise
                const agent = new https.Agent({
                    rejectUnauthorized: false
                });
                await axios.get('https://qikink.com/erp2/index.php/login/shopify?json=1&shop=' + shop + "&accesstoken=" + accessToken, {httpsAgent: agent}).then((response) => {
                   
                  //  console.log('got response' + res);
                  
                    if (response['data'].error) {
                        console.log("Below is the error");
                     console.log(response['data'].error);
                       return   ctx.redirect("/");
                        // ctx.redirect("/");
                       // return  ctx.redirect('https://qikink.com/erp2/index.php/login/shopify?json=0&shop=' + shop + "&accesstoken=" + accessToken);
                    } else {
                              console.log("No Error");
                        console.log(response);
                        return   ctx.redirect("/");
                    }

                });

                console.log("why coming here");



            },
        }),
        );

server.use(verifyRequest());
server.use(async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
    return;
});

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

});

*//

LRUCache is not a constructor

What is the error?
When I'm upgrading shopify polaris version from 6.2.0 to latest it says lru-cache not found. After installing lru-cache package it gives me this LRUCache is not a constructor error.

See the image below -
image

System Specifications-
Windows 10
Node version - 14.20.0
NPM - 6.14.17
Shopify Polaris - 6.2.0
simple-koa-shopify-auth - 2.1.16

"Internal Server Error" message when first time installed an app, but app works fine after reload page.

When I install an app using this package for auth, it shows message "Internal Server Error"
MicrosoftTeams-image

But after this message, when reloaded the page, the app works fine. This problem is consistent accross any store when installed an app for the first time. I pulled up the logs from the server and this is what I found.
image

Not sure if this is an issue with this library, but I am not accessing any id property on my createShopifyAuth call.

Session creation is not working

Discussion started at: Shopify/koa-shopify-auth#134

When using simple-koa-shopify-auth the session is not created, as if my CustomSessionStorage is never called.
It was working fine before switching to simple-koa-shopify-auth

app.prepare().then(async () => {
  const redisStoreAccessToken = await RedisStoreAccessToken();

  // Create a new instance of the custom storage class
  const sessionStorage = await RedisStoreSession();


  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,
    // This should be replaced with your preferred storage strategy
    // SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
    SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
      sessionStorage.storeCallback,
      sessionStorage.loadCallback,
      sessionStorage.deleteCallback
    )
  });

  const server = new Koa();
  const router = new Router();
  server.keys  = [Shopify.Context.API_SECRET_KEY];
  server.use(
    createShopifyAuth({
      async afterAuth(ctx) {
        console.log("inside afterAuth");

        // Access token and shop available in ctx.state.shopify
        const { shop, accessToken, scope } = ctx.state.shopify;
        const host                         = ctx.query.host;

        await redisStoreAccessToken.storeCallback({
          id: shop,
          shop,
          scope,
          accessToken
        });

        registerUninstallWebhooks(shop, accessToken, async (topic, shop, body) => redisStoreAccessToken.deleteCallback(shop)).then();

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

  router.post("/my-rest", verifyRequest({ returnHeader: true }), async (ctx) => {
    try {
      ctx.body = "response from myRest endpoint!";
    } catch (error) {
      ctx.body = "โœ˜ something wrong in myRest endpoint!";
    }
  });
  
});

package.json

"simple-koa-shopify-auth": "^1.0.4",
"@shopify/shopify-api": "^2.0.0",

It seems that the loadCallback is not called

Any clue?

App bridge error after redirect in pages

i have setup simple koa shopify auth first time its working properly after redirect in side page getting below error

TypeError: History.create is not a function

Here is my server.js code

require('isomorphic-fetch');

const dotenv = require('dotenv');
const next = require('next');

// Koa-related
const http = require('http');
const Koa = require('koa');
const cors = require('@koa/cors');
const socket = require('socket.io');
const bodyParser = require('koa-bodyparser');
const vhost = require('koa-virtual-host');


const { createShopifyAuth, verifyRequest } = require("simple-koa-shopify-auth");

const { default: Shopify, ApiVersion } = require('@shopify/shopify-api');


const session = require('koa-session');
const { default: graphQLProxy } = require('@shopify/koa-shopify-graphql-proxy');
const Router = require('koa-router');


// Server-related
const registerShopifyWebhooks = require('./server/registerShopifyWebhooks');
const getShopInfo = require('./server/getShopInfo');
const { configureURLForwarderApp } = require('./routes/urlForwarderApp');

// Mongoose-related
const mongoose = require('mongoose');
const Shop = require('./models/shop');

// Routes
const combinedRouters = require('./routes');
const jobRouter = require('./routes/jobs');

// Twilio-related
const twilio = require('twilio');

// Mixpanel
const Mixpanel = require('mixpanel');
const { eventNames } = require('./constants/mixpanel');
const { createDefaultSegments } = require('./modules/segments');
const { createDefaultTemplates } = require('./modules/templates');
const { createDefaultAutomations } = require('./modules/automations');

// Server Files
const { registerJobs } = require('./server/agenda');
const { EventsQueueConsumer } = require('./server/jobs/eventsQueueConsumer');
const getShopOrderCount = require('./server/getShopOrderCount');


const jwt_decode = require("jwt-decode");

// Access env variables
dotenv.config();

// Constants
const {
  APP_VERSION_UPDATE_DATE_RAW,
  MONGODB_URI,
  PROD_SHOPIFY_API_KEY,
  PROD_SHOPIFY_API_SECRET_KEY,
  STAGING_SHOPIFY_API_KEY,
  STAGING_SHOPIFY_API_SECRET_KEY,
  TWILIO_ACCOUNT_SID,
  TWILIO_AUTH_TOKEN,
  TWILIO_PHONE_NUMBER,
  TWILIO_TOLL_FREE_PHONE_NUMBER,
  URL_FORWARDER_HOST,
  QA_MIXPANEL_TOKEN,
  PROD_MIXPANEL_TOKEN,
  ENABLE_JOBS,
  ENABLE_QUEUE_CONSUMERS,
} = process.env;

// Server initialization
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();


const SHOPIFY_API_KEY = dev ? STAGING_SHOPIFY_API_KEY : PROD_SHOPIFY_API_KEY;
const SHOPIFY_API_SECRET_KEY = dev
  ? STAGING_SHOPIFY_API_SECRET_KEY
  : PROD_SHOPIFY_API_SECRET_KEY;
const APP_VERSION_UPDATE_DATE = new Date(APP_VERSION_UPDATE_DATE_RAW);
const MIXPANEL_TOKEN = dev ? QA_MIXPANEL_TOKEN : PROD_MIXPANEL_TOKEN;



// Mongo DB set up
mongoose.connect(MONGODB_URI, { useNewUrlParser: true });
mongoose.set('useFindAndModify', false);
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));


// initializes the library
Shopify.Context.initialize({
  API_KEY: process.env.PROD_SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.PROD_SHOPIFY_API_SECRET_KEY,
  SCOPES: [
    'read_orders',
    'write_orders',
    'read_products',
    'write_products',
    'read_customers',
    'write_customers',
    'write_draft_orders',
    'read_draft_orders',
    'read_script_tags',
    'write_script_tags',
  ],
  HOST_NAME: process.env.PROD_SERVER_URL.replace(/https:\/\//, ''),
  API_VERSION: ApiVersion.April22,
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});

const ACTIVE_SHOPIFY_SHOPS = {};

app.prepare().then(() => {
  const router = new Router();
  const server = new Koa();

  //const {shop, accessToken} = ctx.state.shopify;

  server.use(session({ secure: true, sameSite: 'none' }, server));
  const httpServer = http.createServer(server.callback());
  const io = socket(httpServer);
  server.context.io = io;

  // Start queue consumers if required
  if (ENABLE_QUEUE_CONSUMERS == 'true') {
    EventsQueueConsumer.run().catch((err) => {
      console.log(`Error running Events Queue Consumer: ${err}`);
    });
  }

  // Bind mixpanel to server context
  const mixpanel = Mixpanel.init(MIXPANEL_TOKEN);
  server.context.mixpanel = mixpanel;

  // Twilio config
  const twilioClient = new twilio(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);
  const twilioSend = (body, to, from, mediaUrl = []) => {
    return twilioClient.messages
      .create({
        body,
        to,
        from: from || TWILIO_PHONE_NUMBER,
        mediaUrl,
      })
      .then((twilioMessagesRes) => {
        return { delivered: true };
      })
      .catch((twilioMessagesErr) => {
        console.log('Error sending twilio message from API: twilioMessagesErr');
        return { delivered: false, error: twilioMessagesErr };
      });
  };
  server.context.twilioSend = twilioSend;

  io.on('connection', function (socket) {
    socket.on('registerShop', () => {
      let ctx = server.createContext(
        socket.request,
        new http.OutgoingMessage()
      );
      //const { shop } = ctx.session;

      const shop = ctx.query.shop;

      //const {shop, accessToken} = ctx.state.shopify;


      Shop.findOne({ shopifyDomain: shop }).then((userShop) => {
        if (userShop) {
          socket.join(`shop:${userShop._id}`);

          // Initialize mixpanel user
          ctx.mixpanel.people.set(userShop._id, {
            $first_name: userShop.shopifyDomain,
            $last_name: '',
            shopifyDomain: userShop.shopifyDomain,
          });
        }
      });
    });
  });

  server.keys = [SHOPIFY_API_SECRET_KEY];

  if (!URL_FORWARDER_HOST) {
    console.warn('URL_FORWARDER_HOST is not set and will not function.');
  } else {
    server.use(vhost(URL_FORWARDER_HOST, configureURLForwarderApp()));
  }

  // Initiate agenda for jobs
  if (ENABLE_JOBS === 'true') {
    (async function () {
      await registerJobs();
    })();
  }

  server.use(cors());


  console.log('step1', SHOPIFY_API_KEY, 'step2', SHOPIFY_API_SECRET_KEY);

   server.use(
    createShopifyAuth({
        apiKey: SHOPIFY_API_KEY,
        secret: SHOPIFY_API_SECRET_KEY,
        accessMode: 'offline',
        scopes: [
          'read_orders',
          'write_orders',
          'read_products',
          'write_products',
          'read_customers',
          'write_customers',
          'write_draft_orders',
          'read_draft_orders',
          'read_script_tags',
          'write_script_tags',
        ],
        async afterAuth(ctx) {

        const {shop, accessToken} = ctx.state.shopify;




       ACTIVE_SHOPIFY_SHOPS[shop] = true;


        // Register Shopify webhooks
        await registerShopifyWebhooks(accessToken, shop);

        // Gather base shop info on shop
        const [shopInfo, orderCount] = await Promise.all([
          getShopInfo(accessToken, shop),
          getShopOrderCount(accessToken, shop),
        ]);

        // If user doesn't already exist, hasn't approved a subscription,
        // or does not have the latest app version, force them to approve
        // the app payment/usage pricing screen
        let userShop = await Shop.findOne({ shopifyDomain: shop });
        const shouldUpdateApp =
          !userShop ||
          !userShop.appLastUpdatedAt ||
          new Date(userShop.appLastUpdatedAt) < APP_VERSION_UPDATE_DATE;

        const existingShopName =
          userShop && userShop.shopName
            ? userShop.shopName
            : shopInfo && shopInfo.name;

        // Load 25 cents into new accounts
        if (!userShop) {
          // Track new install
          userShop = await Shop.findOneAndUpdate(
            { shopifyDomain: shop },
            {
              accessToken: accessToken,
              appLastUpdatedAt: new Date(),
              shopSmsNumber: TWILIO_PHONE_NUMBER,
              tollFreeNumber: TWILIO_TOLL_FREE_PHONE_NUMBER,
              smsNumberIsPrivate: false,
              loadedFunds: 0.5,
              shopName: existingShopName,
              quarterlyOrderCount: orderCount,
              guidedToursCompletion: {
                dashboard: null,
                conversations: null,
                templates: null,
                subscribers: null,
                segments: null,
                campaigns: null,
                analytics: null,
                automations: null,
              },
            },
            { new: true, upsert: true, setDefaultsOnInsert: true }
          ).exec();
          ctx.mixpanel.track(eventNames.INSTALLED_APP, {
            distinct_id: userShop._id,
            shopifyDomain: shop,
            accessToken,
            shopSmsNumber: TWILIO_PHONE_NUMBER,
            tollFreeNumber: TWILIO_TOLL_FREE_PHONE_NUMBER,
          });

          // Create default templates and segments
          createDefaultSegments(userShop);
          createDefaultTemplates(userShop);
        } else if (shouldUpdateApp) {
          const existingShopSmsNumber =
            userShop.shopSmsNumber || TWILIO_PHONE_NUMBER;
          const existingTollFreeSmsNumber =
            userShop.tollFreeNumber || TWILIO_TOLL_FREE_PHONE_NUMBER;
          const existingNumberIsPrivate = !!userShop.smsNumberIsPrivate;
          userShop = await Shop.findOneAndUpdate(
            { shopifyDomain: shop },
            {
              accessToken: accessToken,
              appLastUpdatedAt: new Date(),
              shopSmsNumber: existingShopSmsNumber,
              tollFreeNumber: existingTollFreeSmsNumber,
              smsNumberIsPrivate: existingNumberIsPrivate,
              shopName: existingShopName,
              quarterlyOrderCount: orderCount,
            },
            { new: true, upsert: true, setDefaultsOnInsert: true }
          ).exec();
        } else {
          userShop = await Shop.findOneAndUpdate(
            { shopifyDomain: shop },
            {
              accessToken: accessToken,
              shopName: existingShopName,
              quarterlyOrderCount: orderCount,
            },
            { new: true, upsert: true, setDefaultsOnInsert: true }
          ).exec();
        }

        ctx.mixpanel &&
          ctx.mixpanel.people.set(shop._id, {
            quarterlyOrders: orderCount,
          });

        // Redirect user to app home page
        //ctx.redirect('/');
        ctx.redirect(`/?shop=${shop}`);
      },
    })
  );
  // Milind Changes
  server.use(async (ctx, next) => {
    const shop = ctx.query.shop;
    console.log('Milind', 'step1', shop, `frame-ancestors https://${shop} https://admin.shopify.com;`);
    ctx.set('Content-Security-Policy', `frame-ancestors https://${shop} https://admin.shopify.com;`);
    await next();
  });

  server.use(graphQLProxy({ version: ApiVersion.April22 }));
  server.use(bodyParser());

  server.use(jobRouter.routes());




  router.get('/healthcheck', async (ctx) => {


    var decoded = jwt_decode(ctx.req.headers.authorization.replace('Bearer ', ''));



    //const { shop } = decoded.dest.replace('https://', '');


    ctx.res.statusCode = 200;
    ctx.body = { decoded };
    return { decoded };
  });

  router.get('/', async (ctx) => {
     const shop = ctx.query.shop;

    //const {shop, accessToken} = ctx.state.shopify;

    const userShop = await Shop.findOne({ shopifyDomain: shop });
  
    // If 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 {
      // Load app skeleton. Don't include sensitive information here!
     // ctx.body = '๐ŸŽ‰';


      //ctx.body = { userShop };
    

          if (!userShop.email || !userShop.country || !userShop.adminPhoneNumber) {
            
            //ctx.body = '๐ŸŽ‰';
            app.render(ctx.req, ctx.res, '/welcome', ctx.query);
          } else if (
            !userShop.onboardingVersionCompleted ||
            userShop.onboardingVersionCompleted < 1.0
          ) {
            app.render(ctx.req, ctx.res, '/onboarding/get-started', ctx.query);
          } else {
            await handle(ctx.req, ctx.res);
          }


    }
  });


  router.get('*', async (ctx) => {
    //router.get('*', async (ctx) => {
    if (ctx.url.includes('/?')) {
      if (ctx.url.substring(0, 2) != '/?') {
        ctx.url = ctx.url.replace('/?', '?'); // Remove trailing slash before params
        ctx.url = ctx.url.replace(/\/\s*$/, ''); // Remove trailing slash at the end
      }
    }

    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  });

  server.use(combinedRouters());

  server.use(router.allowedMethods());
  server.use(router.routes());

  httpServer.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

Page code

import {
  Banner,
  Button,
  Card,
  FormLayout,
  Layout,
  Page,
  Select,
  Spinner,
  TextField,
} from '@shopify/polaris';
import { Context } from '@shopify/app-bridge-react';
import { History } from '@shopify/app-bridge/actions';
import countryList from 'country-list';
import {
  getAreaCodeForCountry,
  isValidEmail,
  isValidPhoneNumber,
} from '../modules/utils';
import mixpanel from '../modules/mixpanel';
import { eventNames, eventPages } from '../constants/mixpanel';

import createApp from "@shopify/app-bridge";
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import { Redirect } from "@shopify/app-bridge/actions";


import userLoggedInFetch from '../utils/userLoggedInFetch';


class Welcome extends React.Component {
  static contextType = Context;

  state = {
    emailAddress: '',
    country: '',
    adminPhoneNumber: '',
    formError: false,
    screenWidth: 0,
    loading: false,
  };

  componentDidMount() {
    console.log("Milind", this.context);
    const app = this.context;
    const history = History.create(app);
    history.dispatch(History.Action.PUSH, `/welcome`);

    this.setState({
      app: app,
    });

    //this.getShopifyStoreInfo();
    this.updateWindowDimensions();
    window.addEventListener('resize', this.updateWindowDimensions);
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.updateWindowDimensions);
  }

  updateWindowDimensions = () => {
    this.setState({ screenWidth: window.innerWidth });
  };

  getShopifyStoreInfo = () => {

    fetch(`${SERVER_URL}/get-shopify-store-info`)
      .then(response => {
        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;
          }else{
    
            return response.json();
    
    
          }

      })
      .then((data) => {
        this.setState({
          emailAddress: data.email || '',
          adminPhoneNumber: data.phone || '',
          country: data.country || '',
        });
      });
  };

  handleEmailAddressChange = (emailAddress) => {
    this.setState({ emailAddress });
  };

  handleCountryChange = (country) => {
    this.setState({ country });
  };

  handleAdminPhoneNumber = (adminPhoneNumber) => {
    this.setState({ adminPhoneNumber });
  };

  saveAdminInformation = () => {
    if (
      !isValidEmail(this.state.emailAddress) ||
      !this.state.country ||
      !isValidPhoneNumber(this.state.adminPhoneNumber, this.state.country)
    ) {
      this.setState({ formError: true });
      return;
    }

    this.setState({ loading: true });

    const fetch = userLoggedInFetch(this.state.app);

    // Save admin information
    fetch(`${SERVER_URL}/save-admin-information`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: this.state.emailAddress,
        country: this.state.country,
        adminPhoneNumber: this.state.adminPhoneNumber,
      }),
    })
    .then((response) => {
      return response.json();
    })
      .then((data) => {


        if (data.email && data.country && data.adminPhoneNumber) {
          mixpanel.track(eventNames.CONFIRMED_ADMIN_INFORMATION, {
            page: eventPages.ONBOARDING,
            email: data.email,
            country: data.country,
            adminPhoneNumber: data.adminPhoneNumber,
            shopifyDomain: data.shopifyDomain,
          });
          this.setState({ loading: false });
          // Redirect to next page
         // window.location.assign(`/`);

         const redirect = Redirect.create(this.state.app);
         redirect.dispatch(Redirect.Action.APP, `/?shop=`+data.shopifyDomain);



        } else {
          this.setState({ formError: true, loading: false });
          return;
        }
      });
  };

  render() {
    return (
      <Page>
         {this.state.formError && (
          <Banner title="Error - missing fields" status="critical">
            <p>
              Please make sure all fields are completed correctly before
              proceeding.
            </p>
          </Banner>
        )}
<Layout>
          <Layout.Section>
            <div
              style={{
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
              }}
            >
              <div
                style={{
                  textAlign: 'center',
                  margin: '50px 0px',
                }}
              >
                <div
                  style={{
                    fontWeight: '600',
                    fontSize: '24px',
                    marginBottom: '16px',
                    lineHeight: '30px',
                  }}
                >
                  <div style={{ fontSize: '48px' }}>๐Ÿ‘‹</div>
                  <br />
          
                </div>
                <div
                  style={{
                    fontSize: '16px',
                  }}
                >
               
       
                </div>
              </div>
              <div
                style={{
                  display: 'flex',
                  alignItems: 'center',
                }}
              >
                {this.state.screenWidth > 1098 && (
                  <div
                    style={{
                      width: '250px',
                      marginRight: '50px',
                      lineHeight: '24px',
                      fontWeight: '600',
                    }}
                  >
                    <span style={{ fontSize: '36px' }}>๐Ÿ’Œ</span>
                    <br />
                
                  </div>
                )}
                <div
                  style={{
                    minWidth: '400px',
                    maxWidth: '400px',
                  }}
                >
                  <Card sectioned>
                    <FormLayout onSubmit={() => {}}>
                      <TextField
                        label="Email address"
                        labelHidden
                        placeholder="Personal email address"
                        inputMode="email"
                        type="email"
                        onChange={this.handleEmailAddressChange}
                        value={this.state.emailAddress}
                      />
                      <Select
                        label="Country"
                        labelHidden
                        options={countryList.getNames().map((c) => {
                          return { label: c, value: c };
                        })}
                        onChange={this.handleCountryChange}
                        value={this.state.country}
                      />
                      <TextField
                        label="Phone number"
                        labelHidden
                        placeholder="Personal mobile number"
                        inputMode="tel"
                        type="tel"
                        prefix={
                          this.state.country
                            ? `+${getAreaCodeForCountry(this.state.country)}`
                            : ''
                        }
                        onChange={this.handleAdminPhoneNumber}
                        value={this.state.adminPhoneNumber}
                      />
                      {this.state.loading ? (
                        <div style={{ textAlign: 'center' }}>
                          <Spinner
                            accessibilityLabel="Send text spinner"
                            size="small"
                            color="teal"
                          />
                        </div>
                      ) : (
                        <Button
                          fullWidth
                          primary
                          onClick={this.saveAdminInformation}
                        >
                          Confirm
                        </Button>
                      )}
                    </FormLayout>
                  </Card>
                </div>
                {this.state.screenWidth > 1098 && (
                  <div
                    style={{
                      width: '250px',
                      marginLeft: '50px',
                      textAlign: 'left',
                      lineHeight: '24px',
                      fontWeight: '600',
                    }}
                  >
                
                    <br />
     
                    <br />
                    <br />
                    <br />

                    <br />
            
                  </div>
                )}
              </div>
              {this.state.screenWidth <= 1098 && (
                <div
                  style={{
                    width: '400px',
                    textAlign: 'center',
                    lineHeight: '24px',
                    fontWeight: '600',
                    margin: '50px 0px',
                  }}
                >

             
                  <br />
       
                  <br />
         
                  <br />
                  <br />

                  <br />
          
                </div>
              )}
            </div>
          </Layout.Section>
        </Layout>
      </Page>
    );
  }
}

export default Welcome;

Add the ability to specify the host of the redirect url instead of ctx.host

I'm having the same issue as this person here on the original koa-shopify-auth and so am suggesting the exact same thing: Shopify/koa-shopify-auth#11

Overview
The developer should be able to define the host of the redirect URL to be https://{Host} instead of taking the host from context (ctx).

export function createTopLevelRedirect(apiKey: string, path: string) {

When running createShopifyAuth() there could be an extra optional parameter, host, which if set the above function will use the provided host instead of ctx.host, which can be wrong when using proxies.

Validate the request from Frontend

In my Koajs backend, i have setup a route which is invoked from the embedded app (ex. a button click).

Koajs backend route

import Router from 'koa-router';
import { verifyRequest } from 'simple-koa-shopify-auth';

const verifyApiRequest = verifyRequest({ returnHeader: true });
const appRouter = new Router();

appRouter.get('/api/app/test', verifyApiRequest, async (ctx) => {
  ctx.body = { message: 'Request recevied' };
});

export default appRouter;

And in the embedded app, I used the axios client to make the request and i have added the session token in the request header as follow.

import React from 'react';
import { Page, Layout, Card, Button } from '@shopify/polaris';
import { useAppBridge } from '@shopify/app-bridge-react';
import { getSessionToken } from '@shopify/app-bridge-utils';
import axios from 'axios';

const Home = () => {
  const app = useAppBridge();

  const onClick = async () => {
    // Create axios instance for authenticated request
    const authAxios = axios.create();
    // intercept all requests on this axios instance
    authAxios.interceptors.request.use((config) => {
      return getSessionToken(app)
        .then((token) => {
          // append your request headers with an authenticated token
          console.log(`token: ${ token }`);
          config.headers["Authorization"] = `Bearer ${ token }`;
          config.headers["Content-Type"] = `application/json`;
          return config;
        });
    });
    authAxios.get('/api/app/test')
             .then(result => console.log(result))
             .catch(error => console.log(error))
  };

  return (
    <Page title="Home">
      <Layout>
        <Layout.AnnotatedSection
          title="Home page"
          description="This is the home page."
        >
          <Card sectioned>
            <p>Home</p>
            <Button onClick={ onClick }>Click me</Button>
          </Card>
        </Layout.AnnotatedSection>
      </Layout>
    </Page>
  );
};

export default Home;

I still got 401 bad request when clicking the btn from the frontend. But it works if i remove the verifyApiRequest in the route. i.e.

...
appRouter.get('/api/app/test', async (ctx) => {
  ctx.body = { message: 'Request recevied' };
});
...

Anything i have missed? Thanks.

"The app couldn't be loaded" error

I am following the example mentioned in the readme, but I get this when trying to install or access the app

Screenshot 2022-03-09 at 22 38 54

I can see that the online auth path is hit, and it redirects with

ctx.redirect(/install/auth/?shop=${shop});

From web server logs, I can observe that this is then hit :
"GET /auth/callback?code=aeccdc[...]"

Any clue to what that above error indicates? Do I need to set some specific allowed redirect urls in the Shopify app setup web ui?

thanks for any hints

How to make it work with koa-mount?

First of all thx for building this module.

I have been using the koa-shopify-auth library to build an shopify embedded app and i would like to update some of the packages as they are going to be deprecated.

In the past, i serve the react js code by the koa-mount package. Basically i can get it working but i have no idea how to authenticate the route using the verifyRequest(). Here is my server.js.

import 'isomorphic-fetch';
import path from 'path';
import { fileURLToPath } from 'url';
import Koa from 'koa';
import Router from 'koa-router';
import mount from 'koa-mount';
import koaStatic from 'koa-static';
import { createShopifyAuth, verifyRequest } from "simple-koa-shopify-auth";
import { Shopify, ApiVersion } from '@shopify/shopify-api';
import dotenv from 'dotenv';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

dotenv.config();
const {
  SHOPIFY_API_SECRET_KEY,
  SHOPIFY_API_KEY
} = process.env;

const PORT = 3002;

// initializes the library
Shopify.Context.initialize({
  API_KEY: SHOPIFY_API_KEY,
  API_SECRET_KEY: SHOPIFY_API_SECRET_KEY,
  SCOPES: [
    'read_products'
  ],
  HOST_NAME: 'ykyuen.ngrok.io',
  API_VERSION: ApiVersion.January23,
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});

const app = new Koa();
const router = new Router();
app.keys = [Shopify.Context.API_SECRET_KEY];

const ACTIVE_SHOPIFY_SHOPS = {};

app.use(
  createShopifyAuth({
    accessMode: 'offline',
    authPath: '/install/auth',
    async afterAuth(ctx) {
      const { shop, accessToken } = ctx.state.shopify;
      const { host } = ctx.query;
      if (!accessToken) {
        ctx.response.status = 500;
        ctx.response.body = 'Failed to get access token! Please try again.';
        return;
      }
      ctx.redirect(`/auth?shop=${ shop }&host=${ host }`);
    },
  })
);

app.use(
  createShopifyAuth({
    accessMode: 'online',
    authPath: '/auth',
    async afterAuth(ctx) {
      const { shop } = ctx.state.shopify;
      const { host } = ctx.query;
      ACTIVE_SHOPIFY_SHOPS[shop] = true;

      if (ACTIVE_SHOPIFY_SHOPS[shop]) {
        ctx.redirect(`/?shop=${ shop }&host=${ host }`);
      } else {
        ctx.redirect(`/install/auth/?shop=${ shop }&host=${ host }`);
      }
    },
  })
);

app.use(mount('/', koaStatic(__dirname + '/../dist')));
app.use(mount('/test', koaStatic(__dirname + '/../dist')));

app.listen(PORT, () => {
  console.log(`> Ready on http://localhost:${ PORT }/`);
});

actually i found a way to make the koa-mount supporting multiple middleware.
https://github.com/koajs/mount/pull/52/files

so i can modify my server.js as follow.

...
app.use(mount('/test', [verifyRequest(), koaStatic(__dirname + '/../dist')]));
...

But the result will make the "/test" path always redirect to the "/" path.

The following are the versions of the server packages

    "@shopify/shopify-api": "^5.3.0",
    "dotenv": "^16.0.3",
    "isomorphic-fetch": "^3.0.0",
    "koa": "^2.14.1",
    "koa-mount": "^4.0.0",
    "koa-router": "^12.0.0",
    "koa-static": "^5.0.0",
    "simple-koa-shopify-auth": "^2.1.10"

And for the reactjs project

    "@shopify/app-bridge-react": "3.2.6",
    "@shopify/polaris": "^10.19.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.6.2"

Appreciate any help or suggestion. Thanks.

Shopify graphQL proxy

Thanks so much for keeping the library going! Looks like it works but failing to get the graphqlProxy working with it.

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

But get 404 with this method.

Have tried the latest version of https://www.npmjs.com/package/@shopify/koa-shopify-graphql-proxy

Error forwarding web request: Error: connect ECONNREFUSED ::1:53462

Any ideas?

Reauth on Client Side

In the docs there is this line: For requests from the frontend, we want to return headers, so we can check if we need to reauth on the client side

Do you have an example of what reauthing on the client side would look like?

All our reauths happen server side: ctx.redirect(/online/auth/?shop=${shop}). However, I'd love to figure out how reauth client side as well.

EDIT:

For a little more clarity, I recognize that authenticatedFetch from the node boilerplate should assist with redirects. However, I'm having trouble getting it to work with the Axios interceptor example from the docs. I'm able to successfully make API calls using authAxios. However, I'm having trouble when the online access token expires (no reauth). The main goal is to force a redirect client side if the online access token is expired.

_App.js

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",
   },
 });

 // Create axios instance for authenticated request
 const authAxios = axios.create();
 // intercept all requests on this axios instance
 authAxios.interceptors.request.use(function (config) {
   return getSessionToken(app).then((token) => {
     let decoded = jwt_decode(token);

     // append your request headers with an authenticated token
     config.headers["Authorization"] = `Bearer ${token}`;

     config.params["shopId"] = decoded.dest.replace(/(^\w+:|^)\/\//, "");

     return config;
   });
 });

 const Component = props.Component;

 return (
   <ApolloProvider client={client}>
     <Component {...props} authAxios={authAxios} app={app} />
   </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,
         }}
       >
         <RoutePropagator />
         <MyProvider Component={Component} {...pageProps} />
       </Provider>
     </AppProvider>
   );
 }
}

The host is undefined

Since moving to simple-koa-shopify-auth the host is undefined in _app.js which is create an infinite loop in the console (hangs the tab)

MyApp.getInitialProps = async ({ ctx }) => {
  let host = ctx.query.host;
  console.log(`โ†’ ctx.query.host is ${host}`); //undefined
  return {
    host,
  };
};

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.