Skip to content

Instantly share code, notes, and snippets.

@codingphasedotcom
Created October 21, 2021 18:04
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save codingphasedotcom/fcfc9562cf792ab0a1a8e8d2c5809f71 to your computer and use it in GitHub Desktop.
Save codingphasedotcom/fcfc9562cf792ab0a1a8e8d2c5809f71 to your computer and use it in GitHub Desktop.
Shopify APP CLI MYSQL Prisma Custom Session Storage
import { PrismaClient } from '@prisma/client';
import Shopify from '@shopify/shopify-api';
import { Session } from '@shopify/shopify-api/dist/auth/session';
const prisma = new PrismaClient();
module.exports.storeCallback = async function storeCallback(session){
console.log('Running storeCallback')
const payload = { ...session }
console.log('StoreCallback session===============================')
console.log(session)
console.log('StoreCallback Payload===============================')
console.log(payload)
return prisma.appSession.upsert({
where: { id: session.id },
create: { id: session.id, payload: payload, shop: payload.shop },
update: { payload: payload }
}).then(_ => {
return true
}).catch(err => {
return false
})
}
module.exports.loadCallback = async function loadCallback(id) {
console.log('loadCallback ID===============================')
console.log(id)
return prisma.appSession.findUnique({
where: { id: id }
}).then(data => {
if (!data) {
return undefined
}
const session = new Session(data.id)
// @ts-ignore
const { shop, state, scope, accessToken, isOnline, expires, onlineAccessInfo } = data.payload
session.shop = shop
session.state = state
session.scope = scope
session.expires = expires ? new Date(expires) : undefined
session.isOnline = isOnline
session.accessToken = accessToken
session.onlineAccessInfo = onlineAccessInfo
console.log('loadCallback New session Complete===============================')
console.log(session)
return session
}).catch(err => {
return undefined
})
}
module.exports.deleteCallback = async function deleteCallback(id){
console.log('deleteCallback ID===============================')
console.log(id)
return prisma.appSession.delete({
where: { id: id }
}).then(_ => {
return true
}).catch(err => {
return false
})
}
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE")
}
model faq {
id Int @id @default(autoincrement())
title String
slug String @unique(map: "faq_slug_key")
dynamic Boolean @default(false)
user_id Int
description String
created_at DateTime @default(now())
updated_at DateTime
user user @relation(fields: [user_id], references: [id], map: "FAQ_user_id_fkey")
question question[]
@@index([slug, id], map: "FAQ_slug_id_idx")
@@index([user_id], map: "FAQ_user_id_fkey")
}
model plan {
id Int @id @default(autoincrement())
title String
slug String @unique(map: "Plan_slug_key")
created_at DateTime @default(now())
updated_at DateTime
user user[]
@@index([slug, id], map: "Plan_slug_id_idx")
}
model question {
id Int @id @default(autoincrement())
title String
dynamic Boolean @default(false)
answer String @db.LongText
faq_id Int
created_at DateTime @default(now())
updated_at DateTime
faq faq @relation(fields: [faq_id], references: [id], map: "Question_faq_id_fkey")
@@index([faq_id], map: "Question_faq_id_fkey")
@@index([id], map: "Question_id_idx")
}
model user {
id Int @id @default(autoincrement())
created_at DateTime @default(now())
plan_id Int?
shop String @unique(map: "User_shop_key")
scope String
updated_at DateTime
plan plan? @relation(fields: [plan_id], references: [id], map: "User_plan_id_fkey")
faq faq[]
@@index([plan_id], map: "User_plan_id_fkey")
@@index([shop], map: "User_shop_idx")
}
model AppSession {
id String @id
shop String
payload Json
@@map("app_session")
}
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 {PrismaClient} from '@prisma/client';
import {storeCallback, loadCallback, deleteCallback} from './custom-sessions.js';
import bodyParser from 'koa-bodyparser';
import slugify from "slugify";
const {user, faq, appSession} = new PrismaClient();
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.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.CustomSessionStorage(
storeCallback,
loadCallback,
deleteCallback
),
});
// 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.use(bodyParser());
server.keys = [Shopify.Context.API_SECRET_KEY];
server.use(
createShopifyAuth({
async afterAuth(ctx) {
// Access token and shop available in ctx.state.shopify
const { shop, accessToken, scope } = ctx.state.shopify;
// console.log(ctx.state)
// console.log(ctx.state.shopify)
const host = ctx.query.host;
// ACTIVE_SHOPIFY_SHOPS[shop] = scope;
const newUser = await user.upsert({
where:{ shop: shop},
update: {shop: shop, scope: scope, updated_at: new Date().toISOString()},
create: {shop: shop, scope: scope, updated_at: new Date().toISOString()}
})
const response = await Shopify.Webhooks.Registry.register({
shop,
accessToken,
path: "/webhooks",
topic: "APP_UNINSTALLED",
webhookHandler: async (shop) =>
await user.delete({where: {shop: 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);
}
);
// FAQ Routes
router.post(
"/faq",
verifyRequest({ returnHeader: true }),
async (ctx, next) => {
const {title, description} = ctx.request.body;
let user_id = await user.findFirst({
where: { shop: ctx.query.shop}
})
user_id = user_id.id
const newFaq = await faq.create({
data: {
title: title,
slug: slugify(title, '-'),
description: description,
user_id: user_id,
dynamic: false,
updated_at: new Date().toISOString()
},
})
return ctx.body = {
status: 'success',
data: newFaq
}
console.log(newFaq)
}
);
router.put(
"/faq/:id",
verifyRequest({ returnHeader: true }),
async (ctx, next) => {
await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
}
);
router.del(
"/faq/:id",
verifyRequest({ returnHeader: true }),
async (ctx, next) => {
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
router.get("(.*)", async (ctx) => {
const shop = ctx.query.shop;
// console.log('ACTIVE_SHOPIFY_SHOPS')
// console.log(ACTIVE_SHOPIFY_SHOPS)
const checkShop = await appSession.findFirst({
where: { shop: shop}
})
// This shop hasn't been seen yet, go through OAuth to create a session
if (checkShop === null) {
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}`);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment