Created
October 21, 2021 18:04
-
-
Save codingphasedotcom/fcfc9562cf792ab0a1a8e8d2c5809f71 to your computer and use it in GitHub Desktop.
Shopify APP CLI MYSQL Prisma Custom Session Storage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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