thesecuritydev / simple-koa-shopify-auth Goto Github PK
View Code? Open in Web Editor NEWAn unofficial, simplified version of the @Shopify/koa-shopify-auth middleware library.
License: MIT License
An unofficial, simplified version of the @Shopify/koa-shopify-auth middleware library.
License: MIT License
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}`);
});
});
*//
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).
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.
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.
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
Are there plans to update simple-koa-shopify-auth
to support the latest version of @shopify/shopify-api
(version 6)? There have been quite a few breaking changes as part of this upgrade: https://github.com/Shopify/shopify-api-js/blob/main/docs/migrating-to-v6.md.
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
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}`);
});
});
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;
When I install an app using this package for auth, it shows message "Internal Server Error"
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.
Not sure if this is an issue with this library, but I am not accessing any id property on my createShopifyAuth
call.
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>
);
}
}
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?
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,
};
};
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.
Shopify.utils.validateShop has been deprecated. Is it possible to update the package to support shopify node api v5.0.0?
Noticed that this was solved with f7aff2a
Can you please say when you going to create a new release?
Shopify required us to adapt to the new admin.shopify.com host scheme and it looks like this issue is a blocker for us atm.
Thank you very much!
I am following the example mentioned in the readme, but I get this when trying to install or access the app
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
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.
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?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.