Skip to content

Commit f004019

Browse files
committed
Replace isStorefrontPasswordProtected with API call
We were previously determining whether or not a storefront was password protected by making a HTTP request to the storefront and checking the response status. This worked for us in the past but is a known fragile piece. We've now shipped the ability to query whether the storefront is password protected directly in the Admin API so we no longer need to guess. This replaces all instances of `isStorefrontPasswordProtected` with the updated API call.
1 parent 4266f60 commit f004019

File tree

9 files changed

+90
-90
lines changed

9 files changed

+90
-90
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@shopify/cli-kit': patch
3+
'@shopify/theme': patch
4+
'@shopify/app': patch
5+
---
6+
7+
Utilize Admin API to determine if a storefront is password protected

packages/app/src/cli/services/dev/processes/theme-app-extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export async function setupPreviewThemeAppExtensionsProcess(
5252
])
5353

5454
const storeFqdn = adminSession.storeFqdn
55-
const storefrontPassword = (await isStorefrontPasswordProtected(storeFqdn))
55+
const storefrontPassword = (await isStorefrontPasswordProtected(adminSession))
5656
? await ensureValidPassword('', storeFqdn)
5757
: undefined
5858

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* eslint-disable @typescript-eslint/consistent-type-definitions */
2+
import * as Types from './types.js'
3+
4+
import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'
5+
6+
export type OnlineStorePasswordProtectionQueryVariables = Types.Exact<{[key: string]: never}>
7+
8+
export type OnlineStorePasswordProtectionQuery = {onlineStore: {passwordProtection: {enabled: boolean}}}
9+
10+
export const OnlineStorePasswordProtection = {
11+
kind: 'Document',
12+
definitions: [
13+
{
14+
kind: 'OperationDefinition',
15+
operation: 'query',
16+
name: {kind: 'Name', value: 'OnlineStorePasswordProtection'},
17+
selectionSet: {
18+
kind: 'SelectionSet',
19+
selections: [
20+
{
21+
kind: 'Field',
22+
name: {kind: 'Name', value: 'onlineStore'},
23+
selectionSet: {
24+
kind: 'SelectionSet',
25+
selections: [
26+
{
27+
kind: 'Field',
28+
name: {kind: 'Name', value: 'passwordProtection'},
29+
selectionSet: {
30+
kind: 'SelectionSet',
31+
selections: [
32+
{kind: 'Field', name: {kind: 'Name', value: 'enabled'}},
33+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
34+
],
35+
},
36+
},
37+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
38+
],
39+
},
40+
},
41+
],
42+
},
43+
},
44+
],
45+
} as unknown as DocumentNode<OnlineStorePasswordProtectionQuery, OnlineStorePasswordProtectionQueryVariables>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
query OnlineStorePasswordProtection {
2+
onlineStore {
3+
passwordProtection {
4+
enabled
5+
}
6+
}
7+
}

packages/cli-kit/src/public/node/themes/api.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import {MetafieldDefinitionsByOwnerType} from '../../../cli/api/graphql/admin/generated/metafield_definitions_by_owner_type.js'
1919
import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js'
2020
import {GetTheme} from '../../../cli/api/graphql/admin/generated/get_theme.js'
21+
import {OnlineStorePasswordProtection} from '../../../cli/api/graphql/admin/generated/online_store_password_protection.js'
2122
import {restRequest, RestResponse, adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
2223
import {AdminSession} from '@shopify/cli-kit/node/session'
2324
import {AbortError} from '@shopify/cli-kit/node/error'
@@ -356,6 +357,17 @@ export async function metafieldDefinitionsByOwnerType(type: MetafieldOwnerType,
356357
}))
357358
}
358359

360+
export async function passwordProtected(session: AdminSession): Promise<boolean> {
361+
const {onlineStore} = await adminRequestDoc(OnlineStorePasswordProtection, session)
362+
if (!onlineStore) {
363+
unexpectedGraphQLError("Unable to get details about the storefront's password protection")
364+
}
365+
366+
const {passwordProtection} = onlineStore
367+
368+
return passwordProtection.enabled
369+
}
370+
359371
async function request<T>(
360372
method: string,
361373
path: string,

packages/theme/src/cli/services/console.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {consoleLog} from '@shopify/cli-kit/node/output'
99
export async function ensureReplEnv(adminSession: AdminSession, storePasswordFlag?: string) {
1010
const themeId = await findOrCreateReplTheme(adminSession)
1111

12-
const storePassword = (await isStorefrontPasswordProtected(adminSession.storeFqdn))
12+
const storePassword = (await isStorefrontPasswordProtected(adminSession))
1313
? await ensureValidPassword(storePasswordFlag, adminSession.storeFqdn)
1414
: undefined
1515

packages/theme/src/cli/services/dev.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ export async function dev(options: DevOptions) {
4242
return
4343
}
4444

45-
const storefrontPasswordPromise = isStorefrontPasswordProtected(options.adminSession.storeFqdn).then(
46-
(needsPassword) =>
47-
needsPassword ? ensureValidPassword(options.storePassword, options.adminSession.storeFqdn) : undefined,
45+
const storefrontPasswordPromise = await isStorefrontPasswordProtected(options.adminSession).then((needsPassword) =>
46+
needsPassword ? ensureValidPassword(options.storePassword, options.adminSession.storeFqdn) : undefined,
4847
)
4948

5049
const localThemeExtensionFileSystem = emptyThemeExtFileSystem()

packages/theme/src/cli/utilities/theme-environment/storefront-session.test.ts

Lines changed: 11 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -7,96 +7,29 @@ import {
77
import {describe, expect, test, vi} from 'vitest'
88
import {fetch} from '@shopify/cli-kit/node/http'
99
import {AbortError} from '@shopify/cli-kit/node/error'
10+
import {passwordProtected} from '@shopify/cli-kit/node/themes/api'
11+
import {type AdminSession} from '@shopify/cli-kit/node/session'
1012

1113
vi.mock('@shopify/cli-kit/node/http')
14+
vi.mock('@shopify/cli-kit/node/themes/api')
1215

1316
describe('Storefront API', () => {
1417
describe('isStorefrontPasswordProtected', () => {
15-
test('returns true when the request is redirected to the password page', async () => {
16-
// Given
17-
vi.mocked(fetch).mockResolvedValue(response({status: 200, url: 'https://store.myshopify.com/password'}))
18-
19-
// When
20-
const isProtected = await isStorefrontPasswordProtected('store.myshopify.com')
21-
22-
// Then
23-
expect(isProtected).toBe(true)
24-
expect(fetch).toBeCalledWith('https://store.myshopify.com', {
25-
method: 'GET',
26-
})
27-
})
28-
29-
test('returns false when request is not redirected', async () => {
30-
// Given
31-
vi.mocked(fetch).mockResolvedValue(response({status: 200, url: 'https://store.myshopify.com'}))
32-
33-
// When
34-
const isProtected = await isStorefrontPasswordProtected('store.myshopify.com')
35-
36-
// Then
37-
expect(isProtected).toBe(false)
38-
expect(fetch).toBeCalledWith('https://store.myshopify.com', {
39-
method: 'GET',
40-
})
41-
})
42-
43-
test('returns false when store redirects to a different domain', async () => {
44-
// Given
45-
vi.mocked(fetch).mockResolvedValue(response({status: 200, url: 'https://store.myshopify.se'}))
46-
47-
// When
48-
const isProtected = await isStorefrontPasswordProtected('store.myshopify.com')
49-
50-
// Then
51-
expect(isProtected).toBe(false)
52-
})
18+
const adminSession: AdminSession = {
19+
storeFqdn: 'example-store.myshopify.com',
20+
token: '123456',
21+
}
5322

54-
test('returns false when store redirects to a different URI', async () => {
23+
test('makes an API call to check if the storefront is password protected', async () => {
5524
// Given
56-
vi.mocked(fetch).mockResolvedValue(response({status: 200, url: 'https://store.myshopify.com/random'}))
25+
vi.mocked(passwordProtected).mockResolvedValueOnce(true)
5726

5827
// When
59-
const isProtected = await isStorefrontPasswordProtected('store.myshopify.com')
60-
61-
// Then
62-
expect(isProtected).toBe(false)
63-
})
64-
65-
test('return true when store redirects to /<locale>/password', async () => {
66-
// Given
67-
vi.mocked(fetch).mockResolvedValue(response({status: 200, url: 'https://store.myshopify.com/fr-CA/password'}))
68-
69-
// When
70-
const isProtected = await isStorefrontPasswordProtected('store.myshopify.com')
28+
const isProtected = await isStorefrontPasswordProtected(adminSession)
7129

7230
// Then
7331
expect(isProtected).toBe(true)
74-
})
75-
76-
test('returns false if response is not a 302', async () => {
77-
// Given
78-
vi.mocked(fetch).mockResolvedValue(response({status: 200, url: 'https://store.myshopify.com/random'}))
79-
80-
// When
81-
const isProtected = await isStorefrontPasswordProtected('store.myshopify.com')
82-
83-
// Then
84-
expect(isProtected).toBe(false)
85-
})
86-
87-
test('ignores query params', async () => {
88-
// Given
89-
vi.mocked(fetch)
90-
.mockResolvedValueOnce(response({status: 200, url: 'https://store.myshopify.com/random?a=b'}))
91-
.mockResolvedValueOnce(response({status: 200, url: 'https://store.myshopify.com/password?a=b'}))
92-
93-
// When
94-
const redirectToRandomPath = await isStorefrontPasswordProtected('store.myshopify.com')
95-
const redirectToPasswordPath = await isStorefrontPasswordProtected('store.myshopify.com')
96-
97-
// Then
98-
expect(redirectToRandomPath).toBe(false)
99-
expect(redirectToPasswordPath).toBe(true)
32+
expect(passwordProtected).toHaveBeenCalledWith(adminSession)
10033
})
10134
})
10235

packages/theme/src/cli/utilities/theme-environment/storefront-session.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@ import {defaultHeaders} from './storefront-utils.js'
33
import {fetch} from '@shopify/cli-kit/node/http'
44
import {AbortError} from '@shopify/cli-kit/node/error'
55
import {outputDebug} from '@shopify/cli-kit/node/output'
6+
import {type AdminSession} from '@shopify/cli-kit/node/session'
7+
import {passwordProtected} from '@shopify/cli-kit/node/themes/api'
68

79
export class ShopifyEssentialError extends Error {}
810

9-
export async function isStorefrontPasswordProtected(storeURL: string): Promise<boolean> {
10-
const response = await fetch(prependHttps(storeURL), {
11-
method: 'GET',
12-
})
13-
14-
const redirectLocation = new URL(response.url)
15-
return redirectLocation.pathname.endsWith('/password')
11+
export async function isStorefrontPasswordProtected(session: AdminSession): Promise<boolean> {
12+
return passwordProtected(session)
1613
}
1714

1815
/**

0 commit comments

Comments
 (0)