Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1093 from Shopify/support-unified-admin-url
Browse files Browse the repository at this point in the history
Support unified admin url when using `sanitizeShop`
  • Loading branch information
zzooeeyy authored Dec 15, 2023
2 parents 49beccb + b49453e commit 28fe927
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/young-news-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopify/shopify-api": minor
---

Add helpers to convert between shop admin URLs and legacy URLs. `sanitizeShop` utility method can now support shop admin URLs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {shopifyApi} from '../..';
import {testConfig} from '../../__tests__/test-config';

const VALID_URLS = [
{
adminUrl: 'admin.shopify.com/store/my-shop',
legacyAdminUrl: 'my-shop.myshopify.com',
},
{
adminUrl: 'admin.web.abc.def-gh.ij.spin.dev/store/my-shop',
legacyAdminUrl: 'my-shop.shopify.abc.def-gh.ij.spin.dev',
},
];

const INVALID_ADMIN_URLS = [
'not-admin.shopify.com/store/my-shop',
'adminisnotthis.shopify.com/store/my-shop',
'adminisnot.web.abc.def-gh.ij.spin.dev/store/my-shop',
'admin.what.abc.def-gh.ij.spin.dev/store/my-shop',
];

const INVALID_LEGACY_URLS = [
'notshopify.com',
'my-shop.myshopify.com.nope',
'my-shop.myshopify.com/admin',
];

describe.each(VALID_URLS)(
'For valid shop URL: %s',
({adminUrl, legacyAdminUrl}) => {
it('can convert from shop admin URL to legacy URL', () => {
const shopify = shopifyApi(testConfig());
const actual = shopify.utils.shopAdminUrlToLegacyUrl(adminUrl);
expect(actual).toEqual(legacyAdminUrl);
});

it('can convert from legacy URL to shop admin URL', () => {
const shopify = shopifyApi(testConfig());
const actual = shopify.utils.legacyUrlToShopAdminUrl(legacyAdminUrl);
expect(actual).toEqual(adminUrl);
});

it('can strip protocol before converting from shop admin URL to legacy URL', () => {
const shopify = shopifyApi(testConfig());
const urlWithProtocol = `https://${adminUrl}`;
const actual = shopify.utils.shopAdminUrlToLegacyUrl(urlWithProtocol);
expect(actual).toEqual(legacyAdminUrl);
});

it('can strip protocol before converting from legacy URL to shop admin URL', () => {
const shopify = shopifyApi(testConfig());
const urlWithProtocol = `https://${legacyAdminUrl}`;
const actual = shopify.utils.legacyUrlToShopAdminUrl(urlWithProtocol);
expect(actual).toEqual(adminUrl);
});
},
);

describe.each(INVALID_ADMIN_URLS)(
'For invalid shop admin URL: %s',
(invalidUrl) => {
it('returns null when trying to convert from shop admin url to legacy url', () => {
const shopify = shopifyApi(testConfig());

expect(shopify.utils.shopAdminUrlToLegacyUrl(invalidUrl)).toBe(null);
});
},
);

describe.each(INVALID_LEGACY_URLS)(
'For invalid legacy shop URL: %s',
(invalidUrl) => {
it('returns null when trying to convert from legacy url to shop admin url', () => {
const shopify = shopifyApi(testConfig());

expect(shopify.utils.legacyUrlToShopAdminUrl(invalidUrl)).toBe(null);
});
},
);
45 changes: 42 additions & 3 deletions packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const VALID_SHOP_URL_4 = 'dev-shop-.myshopify.io';

const INVALID_SHOP_URL_1 = 'notshopify.com';
const INVALID_SHOP_URL_2 = '-invalid.myshopify.io';
const VALID_SHOPIFY_HOST_BUT_NOT_VALID_ADMIN_URL =
'not-admin.shopify.com/store/my-shop';

const CUSTOM_DOMAIN = 'my-custom-domain.com';
const VALID_SHOP_WITH_CUSTOM_DOMAIN = `my-shop.${CUSTOM_DOMAIN}`;
Expand All @@ -28,6 +30,13 @@ const VALID_HOSTS = [
return {testhost, base64host: Buffer.from(testhost).toString('base64')};
});

const VALID_SHOP_ADMIN_URLS = [
{
adminUrl: 'admin.shopify.com/store/my-shop',
legacyAdminUrl: 'my-shop.myshopify.com',
},
];

const INVALID_HOSTS = [
{
testhost: 'plain-string-is-not-base64',
Expand Down Expand Up @@ -65,11 +74,14 @@ describe('sanitizeShop', () => {
);
});

test('returns null for invalid URLs', () => {
test.each([
INVALID_SHOP_URL_1,
INVALID_SHOP_URL_2,
VALID_SHOPIFY_HOST_BUT_NOT_VALID_ADMIN_URL,
])('returns null for invalid URL - %s', (invalidUrl) => {
const shopify = shopifyApi(testConfig());

expect(shopify.utils.sanitizeShop(INVALID_SHOP_URL_1)).toBe(null);
expect(shopify.utils.sanitizeShop(INVALID_SHOP_URL_2)).toBe(null);
expect(shopify.utils.sanitizeShop(invalidUrl)).toBe(null);
});

test('throws for invalid URLs if set to', () => {
Expand Down Expand Up @@ -104,6 +116,33 @@ describe('sanitizeShop', () => {
shopify.utils.sanitizeShop(INVALID_SHOP_WITH_CUSTOM_DOMAIN_REGEX),
).toBe(null);
});

test.each(VALID_SHOP_ADMIN_URLS)(
'accepts new format of shop admin URLs and converts to legacy admin URLs - %s',
({adminUrl, legacyAdminUrl}) => {
const shopify = shopifyApi(testConfig());
const actual = shopify.utils.sanitizeShop(adminUrl);

expect(actual).toEqual(legacyAdminUrl);
},
);

test('Accepts new format of spin admin URL and converts to legacy admin URL', () => {
const expectedLegacyAdminUrl = 'my-shop.shopify.abc.def-gh.ij.spin.dev';
const spinAdminUrl = 'admin.web.abc.def-gh.ij.spin.dev/store/my-shop';

const shopify = shopifyApi(
testConfig({
customShopDomains: [
'web\\.abc\\.def-gh\\.ij\\.spin\\.dev',
'shopify\\.abc\\.def-gh\\.ij\\.spin\\.dev',
],
}),
);
const actual = shopify.utils.sanitizeShop(spinAdminUrl);

expect(actual).toEqual(expectedLegacyAdminUrl);
});
});

describe('sanitizeHost', () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/shopify-api/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import {ConfigInterface} from '../base-types';
import {sanitizeShop, sanitizeHost} from './shop-validator';
import {validateHmac} from './hmac-validator';
import {versionCompatible, versionPriorTo} from './version-compatible';
import {
shopAdminUrlToLegacyUrl,
legacyUrlToShopAdminUrl,
} from './shop-admin-url-helper';

export function shopifyUtils(config: ConfigInterface) {
return {
Expand All @@ -11,6 +15,8 @@ export function shopifyUtils(config: ConfigInterface) {
validateHmac: validateHmac(config),
versionCompatible: versionCompatible(config),
versionPriorTo: versionPriorTo(config),
shopAdminUrlToLegacyUrl,
legacyUrlToShopAdminUrl,
};
}

Expand Down
76 changes: 76 additions & 0 deletions packages/shopify-api/lib/utils/shop-admin-url-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Converts admin.shopify.com/store/my-shop to my-shop.myshopify.com
export function shopAdminUrlToLegacyUrl(shopAdminUrl: string): string | null {
const shopUrl = removeProtocol(shopAdminUrl);

const isShopAdminUrl = shopUrl.split('.')[0] === 'admin';

if (!isShopAdminUrl) {
return null;
}

const regex = new RegExp(`admin\\..+/store/([^/]+)`);
const matches = shopUrl.match(regex);

if (matches && matches.length === 2) {
const shopName = matches[1];
const isSpinUrl = shopUrl.includes('spin.dev/store/');

if (isSpinUrl) {
return spinAdminUrlToLegacyUrl(shopUrl);
} else {
return `${shopName}.myshopify.com`;
}
} else {
return null;
}
}

// Converts my-shop.myshopify.com to admin.shopify.com/store/my-shop
export function legacyUrlToShopAdminUrl(legacyAdminUrl: string): string | null {
const shopUrl = removeProtocol(legacyAdminUrl);
const regex = new RegExp(`(.+)\\.myshopify\\.com$`);
const matches = shopUrl.match(regex);

if (matches && matches.length === 2) {
const shopName = matches[1];
return `admin.shopify.com/store/${shopName}`;
} else {
const isSpinUrl = shopUrl.endsWith('spin.dev');

if (isSpinUrl) {
return spinLegacyUrlToAdminUrl(shopUrl);
} else {
return null;
}
}
}

function spinAdminUrlToLegacyUrl(shopAdminUrl: string) {
const spinRegex = new RegExp(`admin\\.web\\.(.+\\.spin\\.dev)/store/(.+)`);
const spinMatches = shopAdminUrl.match(spinRegex);

if (spinMatches && spinMatches.length === 3) {
const spinUrl = spinMatches[1];
const shopName = spinMatches[2];
return `${shopName}.shopify.${spinUrl}`;
} else {
return null;
}
}

function spinLegacyUrlToAdminUrl(legacyAdminUrl: string) {
const spinRegex = new RegExp(`(.+)\\.shopify\\.(.+\\.spin\\.dev)`);
const spinMatches = legacyAdminUrl.match(spinRegex);

if (spinMatches && spinMatches.length === 3) {
const shopName = spinMatches[1];
const spinUrl = spinMatches[2];
return `admin.web.${spinUrl}/store/${shopName}`;
} else {
return null;
}
}

function removeProtocol(url: string): string {
return url.replace(/^https?:\/\//, '').replace(/\/$/, '');
}
14 changes: 13 additions & 1 deletion packages/shopify-api/lib/utils/shop-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import {ConfigInterface} from '../base-types';
import {InvalidHostError, InvalidShopError} from '../error';
import {decodeHost} from '../auth/decode-host';

import {shopAdminUrlToLegacyUrl} from './shop-admin-url-helper';

export function sanitizeShop(config: ConfigInterface) {
return (shop: string, throwOnInvalid = false): string | null => {
let shopUrl = shop;
const domainsRegex = ['myshopify\\.com', 'shopify\\.com', 'myshopify\\.io'];
if (config.customShopDomains) {
domainsRegex.push(
Expand All @@ -17,7 +20,16 @@ export function sanitizeShop(config: ConfigInterface) {
`^[a-zA-Z0-9][a-zA-Z0-9-_]*\\.(${domainsRegex.join('|')})[/]*$`,
);

const sanitizedShop = shopUrlRegex.test(shop) ? shop : null;
const shopAdminRegex = new RegExp(
`^admin\\.(${domainsRegex.join('|')})/store/([a-zA-Z0-9][a-zA-Z0-9-_]*)$`,
);

const isShopAdminUrl = shopAdminRegex.test(shopUrl);
if (isShopAdminUrl) {
shopUrl = shopAdminUrlToLegacyUrl(shopUrl) || '';
}

const sanitizedShop = shopUrlRegex.test(shopUrl) ? shopUrl : null;
if (!sanitizedShop && throwOnInvalid) {
throw new InvalidShopError('Received invalid shop argument');
}
Expand Down

0 comments on commit 28fe927

Please sign in to comment.