<file_sep>import * as fbadmin from 'firebase-admin';
import * as functions from 'firebase-functions';
import * as fireorm from 'fireorm';

fbadmin.initializeApp(functions.config().firebase)

const firestore = fbadmin.firestore()
const storage = fbadmin.storage().bucket()
fireorm.initialize(firestore);

export const db = firestore;
export const fbStorage = storage

export const getRepository = fireorm.getRepository;
<file_sep>import { logger } from "../utilities/logger";
import * as querystring from "querystring";
import * as functions from "firebase-functions";
import * as crypto from "crypto";


export const verifyHmac = function (providedHmacString: string, queryObj: any): boolean {
  const LOG = logger('shopify-verifyHmac');
  const conf = functions.config().shopify;
  const map = Object.assign({}, queryObj);
  delete map["signature"];
  delete map["hmac"];
  const message = querystring.stringify(map);
  const providedHmac = Buffer.from(providedHmacString, "utf-8");
  const generatedHash = Buffer.from(
      crypto
          .createHmac("sha256", conf.api_secret)
          .update(message)
          .digest("hex"),
      "utf-8"
  );
  let hashEquals = false;
  // timingSafeEqual will prevent any timing attacks. Arguments must be buffers
  try {
    hashEquals = crypto.timingSafeEqual(generatedHash, providedHmac);
    // timingSafeEqual will return an error if the input buffers are not the same length.
  } catch (e) {
    LOG.error(e);
    hashEquals = false;
  }
  if (!hashEquals) {
    LOG.warn(`HMAC validation failed - provided hmac: ${providedHmacString} - calculated: ${generatedHash}\n
      API secret: ${conf.shopify.api_secret}\n
      Message: ${message}`);
  }
  return hashEquals;

}<file_sep>import Shopify = require("shopify-api-node");
import { Store } from "../models/Store";
import { constants } from "./constants";


//this way we uniformize the Shopify version
export const getNewShopifyClient = function (mp: Store): Shopify {
  if(mp.accessToken == null) {
    throw new Error(`AccessToken for MP: ${mp.id} is null - aborting`);
  }
  return new Shopify({
    shopName: mp.id,
    accessToken: mp.accessToken,
    apiVersion: constants.shopifyApiVersion
  })
};

export const getNewShopifyClientWithToken = function (mp:string,token:string): Shopify {
  return new Shopify({
    shopName: mp,
    accessToken: token,
    apiVersion: constants.shopifyApiVersion
  })
};



<file_sep>
import * as express from "express";
import { Store } from "../models/Store";
import { logger } from "../utilities/logger";
import * as functions from "firebase-functions";
import { verifyHmac } from './verifyHmac';
import * as uaParser from "useragent";
import { constants } from "../utilities/constants";

export const shopifyLoginRequiredMiddleware = async function (
  req: express.Request,
  resp: express.Response,
  next: Function
) {
  const LOG = logger('shopifyLoginRequiredMiddleware');
  const { shop, hmac } = req.query as any
  const shopDomain = req.signedCookies ? req.signedCookies.shopifyStore : null

  if (shopDomain != null && shop != shopDomain && hmac != null) {
    const hashEquals = verifyHmac(hmac, req.query);
    if (hashEquals) {
      const mp = await Store.getRepository().findById(shop);
      if (mp != null) {
        innerLogin(mp, req, resp);
        return next();
      } else {
        LOG.warn(`shop and valid hmac found - but shop wasn't found locally - redirecting to connect`)
        return redirectToConnect(shop, resp);
      }
    }
  }

  if (shopDomain == null) {
    return redirectToConnect(shop, resp);
  }
  try {
    innerLogin(await getStorePartner(shopDomain), req, resp);

  } catch (e) {
    LOG.error(`Couldn't log in to ${shopDomain} redirecting to connect`, e);
    return redirectToConnect(shop, resp);
  }

  return next();
};


const innerLogin = function (mp: Store, req: express.Request, resp: express.Response) {
  //TODO - take care of the sameSite flag based on
  // the UA on the request
  applyLoginCookie(mp.id, req, resp);
  resp.locals.SHOPIFY_API_KEY = functions.config().shopify.api_key;
  resp.locals.SHOP_DOMAIN = mp.id;

  resp.locals.mp = mp;

}

const redirectToConnect = function (shop: string | null, resp: express.Response): void {
  const redirectUri =
    shop != null
      ? `${constants.basePath}/shopify/connect?shop=${encodeURIComponent(
        shop
      )}`
      : `${constants.basePath}/shopify/connect`;
  return resp.redirect(redirectUri);

}

export async function getStorePartner(
  shopDomain: string
): Promise<Store> {
  // const LOG = logger('shopifyController#getMerchantPartner');
  const repo = Store.getRepository();
  let mp = await repo.findById(shopDomain);
  if (mp == null) {
    throw new Error(`Couldn't find MP for shopDomain: ${shopDomain}`);
  }
  return mp;
}

export const applyLoginCookie = function (mpId: String, req: express.Request, resp: express.Response) {

  resp.cookie("shopifyStore", mpId, secureCookieOptions(req, resp));
}

export const secureCookieOptions = function (req: express.Request, resp: express.Response): express.CookieOptions {
  let options: express.CookieOptions = {
    signed: true,
    secure: true,
  };

  if (_shouldApplySameSiteNone(req.headers["user-agent"] || "")) {
    options = {
      signed: true,
      secure: true,
      sameSite: 'none'
    };
  }

  return options;

}
const _shouldApplySameSiteNone = function (uaString: string) {
  const LOG = logger("_shouldApplySameSiteNone");
  try {

    const userAgent = uaParser.parse(uaString);

    if (userAgent.family.toLowerCase().indexOf('chrom') != -1 && parseFloat(userAgent.major) < 67) {
      return false;
    }
    const os = userAgent.os;
    if (os.family.toLowerCase().indexOf("max os x") != -1 && os.major == "10" && os.minor == "14") {
      return false;
    }

    if (os.family.toLowerCase().indexOf("ios") != -1 && os.major == "12") {
      return false;
    }
    return true;
  } catch (ex) {
    LOG.error("caught error - returning should apply", ex)
    return true;
  }

}<file_sep>import { Collection , IEntity } from 'fireorm';
import {getRepository} from './../utilities/database';

@Collection()
export class Store implements IEntity {
  id!: string;
  shop?: string;
  accessToken?: string | null;
  shopifyScopes: string = "";
  creationDate?: Date;
  webhooksVerifiedAt?: Date | null;
  disabled: boolean = false;

  public static getRepository() {
    return getRepository(Store);
  }

  public toString(): string {
    return `MerchantPartner Id: ${this.id}`;
  }

  public constructor(init?: Partial<Store>) {
    Object.assign(this, init);
  }

}


<file_sep>import * as winston from "winston";
import {LoggingWinston} from "@google-cloud/logging-winston";

const format = winston.format;

const loggingWinston = new LoggingWinston({
  level: 'debug'
});

export const logger = function (label: string): winston.Logger {
  return winston.createLogger({
    level: 'debug',
    transports: [
      new winston.transports.Console({
        format: format.combine(
            format.label({label: label, message:true}),
            format.colorize(),
            format.simple()
        )
      }),
        loggingWinston
    ]
  });
}





<file_sep>import { Store } from "../models/Store";
import { constants } from "./constants";
import { logger } from "./logger";
import { getNewShopifyClient } from "./shopifyClient";

export const addScriptTag = async function (store: Store) {
  const LOG = logger('addScriptTag');
  try {
    const scriptUrl = `https://${constants.defaultRequestHost}${constants.basePath}/assets/script.js`
    LOG.info(`ScriptUrl :${scriptUrl}`)

    const shopify = getNewShopifyClient(store)
    await shopify.scriptTag.create({
      event: "onload",
      src: scriptUrl
    })
  }
  catch (e) {
    LOG.error('Script tag not loaded')
    LOG.error(JSON.stringify(e))
  }
}<file_sep>import * as functions from 'firebase-functions';
import * as express from "express";
import { shopifyController } from './controllers/shopifyController';
import { testController } from './controllers/testController';
import { shopifyLoginRequiredMiddleware } from './middlewares/shopifyLoginMiddleware';
import exphbs = require("express-handlebars");
import { constants } from './utilities/constants';
const STATIC_PATH = "assets";
import cookieParser = require("cookie-parser");

const app = express();

app.use(cookieParser(constants.jwtSecret));

app.engine("handlebars", exphbs.create({
  helpers:{
    static: function (options: any) {
      return `${constants.basePath}/${STATIC_PATH}/${options.fn()}`;
    },
  }
}).engine);


app.set("view engine", "handlebars");
app.use(express.json());

//serve static assets on dir assets
app.use(`/${STATIC_PATH}`, express.static("assets"));

export const indexGETRoute = app.get("/", shopifyLoginRequiredMiddleware, async (req, res) => {
  res.render("home")
});

app.use("/test", testController);
app.use("/shopify", shopifyController);

exports.appv2 = functions.runWith({
  memory: '256MB',
  timeoutSeconds: 500,
}).https.onRequest(app);

<file_sep>import * as express from "express";
import { constants, getRandomArbitrary } from "../utilities/constants";
import * as functions from 'firebase-functions';
import {secureCookieOptions,applyLoginCookie} from "../middlewares/shopifyLoginMiddleware";
import {verifyHmac} from "../middlewares/verifyHmac";
import * as request from "request-promise-native";
import { Store } from "../models/Store";
import { addScriptTag } from "../utilities/scriptTag";
import { subscribeToWebhooks } from "../utilities/webhooks";

const router = express.Router();

export const shopifyController = router;

const SHOPIFY_SCOPES = "read_products,read_script_tags,write_script_tags";

//connect and install route
export const connectGETRoute = router.get("/connect", (req, res) => {
  const shopDomain: string = req.query.shop as any

  if (shopDomain) {
    const state = getRandomArbitrary(8, 13);
    const redirectUri = `${constants.schema}://${constants.defaultRequestHost}${constants.basePath}/shopify/callback`;
    const shopUrl = shopDomain.endsWith("myshopify.com")
        ? shopDomain
        : `${shopDomain}.myshopify.com`;
    const installUri =
        `${constants.schema}://${shopUrl}/admin/oauth/authorize?client_id=${
            functions.config().shopify.api_key
            }` +
        `&scope=${SHOPIFY_SCOPES}` +
        `&state=${state}` +
        `&redirect_uri=${redirectUri}`;
    res.cookie("state", state, secureCookieOptions(req, res));
   
    return res.render('redirect', {
      redirectUrl: installUri,
      forceParentRedirect: true
    });
  }
  return res.render("shopify/connect");

});

//verify connect and save details to db or update db
export const callbackGETRoute = router.get("/callback", (req, res) => {
  const {shop, hmac, code, state} = req.query as any
  const stateCookie = req.signedCookies.state;

  const conf = functions.config().shopify;
  if (state !== stateCookie) {
    console.warn(`State: ${state} stateCookie: ${stateCookie}`);
    return res.status(403).send("Request origin cannot be verified");
  }
  if (shop && hmac && code) {
    const hashEquals = verifyHmac(hmac, req.query);

    if (!hashEquals) {
      return res.status(400).send("HMAC validation failed");
    }
    // DONE: Exchange temporary code for a permanent access token
    const accessTokenRequestUrl =
        "https://" + shop + "/admin/oauth/access_token";
    const accessTokenPayload = {
      client_id: conf.api_key,
      client_secret: conf.api_secret,
      code
    };

    return request
        .post(accessTokenRequestUrl, {json: accessTokenPayload})
        .then(accessTokenResponse => {
          const mp = new Store({
            id: shop,
            accessToken: accessTokenResponse.access_token
          });

          const mpRepo = Store.getRepository();

          return mpRepo.findById(shop).then(async existingMp => {
            if (existingMp != null) {
              existingMp.accessToken = accessTokenResponse.access_token;
              await mpRepo.update(existingMp);
            } else {
              await mpRepo.create(mp);
              await addScriptTag(mp)
              await subscribeToWebhooks(mp)
            }
            applyLoginCookie(shop, req, res);

            return res.redirect(`${constants.basePath}/`);
          });
        })
        .catch(error => {
          console.error(JSON.stringify(error));
          return res.status(error.statusCode).send(error?.error?.error_description);
        });

  } else {
    return res.status(400).send("Required parameters missing");
  }

});
<file_sep>import * as express from "express";
import { constants } from "../utilities/constants";
import { getNewShopifyClientWithToken } from "../utilities/shopifyClient";
const router = express.Router();


export const scriptGetRoute = router.get(
  "/script/:id",
  async (req, res, next) => {
    const id = req.params.id !== "1" ? req.params.id : null
    const domain = ""
    const token = ""

    const shopify = getNewShopifyClientWithToken(domain, token)
    id ? await shopify.scriptTag.delete(parseInt(id)) : ""
    console.log(await shopify.scriptTag.list())
    
    return res.sendStatus(200)

  });

  export const addScriptGetRoute = router.get(
    "/add-script/",
    async (req, res, next) => {
      
      const domain = "aero-apps-sandbox.myshopify.com"
      const token = "sh<PASSWORD> <KEY>"
  
      const shopify = getNewShopifyClientWithToken(domain, token)
     
      
      //create
      const scriptUrl = `https://${constants.defaultRequestHost}${constants.basePath}/assets/script.js`
      await shopify.scriptTag.create({
        event: "onload",
        src: scriptUrl
      })
  
      return res.sendStatus(200)
  
    });




export const testController = router;
<file_sep>ngrok http -subdomain=suggestr-shopify 8080<file_sep>import { Store } from "../models/Store";
import { logger } from "./logger";
import { getNewShopifyClient } from "./shopifyClient";



export const subscribeToWebhooks = async function (store: Store) {
  const LOG = logger('subscribeToWebhooks')
  try {
    LOG.info('Products subscribed')
    const shopify = getNewShopifyClient(store)
    await shopify.webhook.create({
      address: "https://example.com",
      topic: "products/update"
    })
  }
  catch (e) {
    LOG.error("Subsciption to webhooks unsucessfull")
    LOG.error(JSON.stringify(e))
  }
}<file_sep>
firebase functions:config:set shopify.api_key="" shopify.api_secret="" default.req_host=""
<file_sep>import * as express from "express";
import * as functions from "firebase-functions";

export const constants = {
  basePath: process.env.NODEMON_DEBUG ? '' : '/appv2',
  port: null,
  shopifyApiVersion: '2019-07',
  schema: process.env.FUNCTIONS_EMULATOR ? (process.env.MDEBUG_COMMAND ? "https" : "https") : "https",
  project: process.env.GCLOUD_PROJECT || "",
  allowedHosts: [
    "localhost",
    "suggestr-shopify.ngrok.io",
    "us-central1-pohoda-sandbox-dev.cloudfunctions.net"
  ],
  defaultRequestHost: process.env.RUNNING_SCRIPT ? "" : functions.config().default.req_host,
  functionLocation: 'us-central1',
  jwtSecret: process.env.JWT_SECRET || "different JWT asdl kjalkjklasd kjla123 123123 ",
 
};

export const requestHost = function (request: express.Request): string {
  if (constants.allowedHosts.indexOf(request.hostname) == -1) {
    throw new Error(`Host: ${request.hostname} not in allowedHosts`);
  }
  return request.hostname;
};

export const getRandomArbitrary = function (min: number, max: number) {
  return Math.round(Math.random() * (max - min) + min);
};
