@agnostack/next-shopify
Version: 
Please contact agnoStack via info@agnostack.com for any questions
145 lines • 9.1 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.protectRoute = void 0;
const chalk_1 = __importDefault(require("chalk"));
const utils_1 = require("../../utils");
const shared_1 = require("../../../shared");
const processRedirect = (url, params, permanent = false) => ({
    redirect: {
        destination: (0, shared_1.appendQuery)(url, new URLSearchParams((0, shared_1.ensureObject)(params)).toString()),
        permanent,
    },
});
const protectRoute = (serverRuntimeConfig, _props) => {
    const { PAGE_PATHS, API_PATHS, APP_CONFIG, PAYMENT_CONFIG } = serverRuntimeConfig;
    const { isSessionTokenEnabled } = APP_CONFIG;
    const exitPath = PAGE_PATHS[shared_1.PAGE_ROUTE_NAMES.EXIT];
    const errorPath = PAGE_PATHS[shared_1.PAGE_ROUTE_NAMES.ERROR];
    return (...args_1) => __awaiter(void 0, [...args_1], void 0, function* ({ req, res, query, locale, resolvedUrl, // NOTE: this comes in from nextJS
     } = {}, data) {
        var _a, _b, _c, _d;
        const [shop] = (0, shared_1.ensureArray)((_a = query === null || query === void 0 ? void 0 : query.shop) !== null && _a !== void 0 ? _a : (_b = req.query) === null || _b === void 0 ? void 0 : _b.shop);
        if ((0, shared_1.stringEmpty)(shop)) {
            return processRedirect(errorPath, Object.assign(Object.assign({}, query), { code: 'no_shop_provided' }));
        }
        const props = (0, shared_1.cleanObject)(Object.assign(Object.assign({ locale }, _props), data), false, shared_1.stringEmptyOnly);
        // NOTE: this should handle running in next to test non-shopify functionality
        const shouldBypassProtect = (0, utils_1.allowLocalBypass)(serverRuntimeConfig);
        if (shouldBypassProtect) {
            console.info(`${chalk_1.default.yellowBright('next-shopify')} - [${chalk_1.default.bold('protectRoute')}]: ${`Bypassing local protect - should ONLY happen when running in next to test non-shopify functionality!`}`);
            return { props };
        }
        const { shopify, loadAppData, loadSession, storeSession, findSessionIds, validateSessionToken, exchangeSessionToken, ensureBilling: handleEnsureBilling, } = yield (0, utils_1.getShopifyHelpers)(serverRuntimeConfig, { shop });
        if (shopify === null || shopify === void 0 ? void 0 : shopify.config) {
            res.setHeader('Content-Security-Policy', (0, utils_1.getSecurityHeader)(shop, shopify.config));
        }
        // TODO: delete this for legacy auth
        // #region SESSION TOKEN FLOW
        if (isSessionTokenEnabled) {
            const idToken = (_c = query === null || query === void 0 ? void 0 : query[shared_1.SESSION_TOKEN_PARAM]) !== null && _c !== void 0 ? _c : (_d = req.query) === null || _d === void 0 ? void 0 : _d[shared_1.SESSION_TOKEN_PARAM];
            const { isValid: isValidToken } = yield validateSessionToken({ idToken });
            let isValid = isValidToken;
            let offlineSession;
            if (isValid) {
                const { isValid: isValidExchange, accessToken: { session, } = {}, } = yield exchangeSessionToken({ idToken });
                isValid = isValidExchange;
                offlineSession = session;
            }
            // NOTE: this handles an invalid session token
            if (!isValid || !offlineSession) {
                const [resolvedPath, _resolvedParams] = (0, shared_1.ensureString)(resolvedUrl).split('?');
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
                const _e = (0, shared_1.querystringToObject)(_resolvedParams), _f = shared_1.SESSION_TOKEN_PARAM, _oldToken = _e[_f], resolvedParams = __rest(_e, [typeof _f === "symbol" ? _f : _f + ""]);
                return processRedirect(exitPath, Object.assign(Object.assign({}, query), { shop, redirectUri: (0, shared_1.appendQuery)(resolvedPath, new URLSearchParams(resolvedParams).toString()) }));
            }
            const existingSessionIds = yield findSessionIds(shop);
            // NOTE: this handles first time installs
            if (!existingSessionIds.includes(offlineSession.id)) {
                try {
                    yield storeSession(offlineSession.toObject());
                }
                catch (error) {
                    console.error('Error storing session', error);
                    // TODO add custom 'code=' param for this use case
                    return processRedirect(errorPath, Object.assign(Object.assign({}, query), { shop }));
                }
                let appId;
                try {
                    const { appInfo } = yield loadAppData();
                    appId = (0, utils_1.normalizeShopifyId)(appInfo === null || appInfo === void 0 ? void 0 : appInfo.id).id;
                    // eslint-disable-next-line no-empty
                }
                catch (_ignore) {
                    console.info('Ignoring error loading appData', _ignore);
                }
                // Register webhooks
                const prepareWebhookHandlers = (0, utils_1.getPrepareWebhookHandlers)(serverRuntimeConfig);
                const webhookHandlers = prepareWebhookHandlers({ appId });
                shopify.webhooks.addHandlers(webhookHandlers);
                // NOTE: webhooks cannot use an online session to make API calls to Shopify (but not all webooks may be doing that for all apps)
                yield shopify.webhooks.register({ session: offlineSession });
                if (PAYMENT_CONFIG === null || PAYMENT_CONFIG === void 0 ? void 0 : PAYMENT_CONFIG.required) {
                    // NOTE: handleEnsureBilling handles calling encryptStoreSession for offline session when shouldCheckBilling
                    const callbackUrl = yield handleEnsureBilling(offlineSession, Object.assign({}, (PAYMENT_CONFIG === null || PAYMENT_CONFIG === void 0 ? void 0 : PAYMENT_CONFIG.debugMode) && {
                        forceBilling: true,
                    }));
                    if ((0, shared_1.stringNotEmpty)(callbackUrl)) {
                        return processRedirect(exitPath, Object.assign(Object.assign({}, query), { shop, redirectUri: callbackUrl }));
                    }
                }
            }
            return { props };
        }
        // #endregion SESSION TOKEN FLOW
        // TODO: delete this for legacy auth
        // NOTE: (NON-EMBEDDED APP or NO SESSION TOKEN SUPPORT - typically via SHOPIFY_EMBEDDED_LEGACY)
        // #region NON-SESSION TOKEN FLOW
        const authPath = API_PATHS[shared_1.API_ROUTE_NAMES.AUTH_ONLINE];
        const existingSessionIds = (0, shared_1.ensureArray)(yield findSessionIds(shop)).filter((sessionId) => (!sessionId.startsWith(shared_1.STATE_PREFIX)));
        if ((0, shared_1.arrayEmpty)(existingSessionIds)) {
            return processRedirect(authPath, Object.assign(Object.assign({}, query), { shop }));
        }
        try {
            // NOTE: this does not work if called INSIDE of embedded
            yield loadSession({ req, res });
            return processRedirect(yield shopify.auth.getEmbeddedAppUrl({
                rawRequest: req,
                rawResponse: res,
            }));
        }
        catch (error) {
            if (error instanceof shared_1.ShopifyAuthenticationError) {
                const reAuth = (error instanceof shared_1.ShopifySessionAuthenticationError);
                return processRedirect(authPath, Object.assign(Object.assign({}, query), { shop, reAuth }));
            }
            // TODO add custom 'code=' param for this use case
            return processRedirect(errorPath, Object.assign(Object.assign({}, query), { shop }));
        }
        // #endregion NON-SESSION TOKEN FLOW
    });
};
exports.protectRoute = protectRoute;
//# sourceMappingURL=protect.js.map