@faststore/core 4.10.1 → 4.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +278 -235
- package/@generated/graphql.ts +1 -0
- package/@generated/schema.graphql +1 -1
- package/CHANGELOG.md +86 -0
- package/package.json +6 -6
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +3 -3
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +5 -7
- package/src/components/account/Drawer/OrganizationSignInButton/OrganizationSignInButton.tsx +4 -1
- package/src/components/account/Layout/Layout.tsx +5 -7
- package/src/components/account/Menu/Menu.tsx +1 -1
- package/src/components/navigation/Navbar/Navbar.tsx +3 -3
- package/src/components/navigation/Navbar/NavbarAccountArea.tsx +5 -5
- package/src/components/navigation/NavbarSlider/NavbarSlider.tsx +3 -3
- package/src/experimental/myAccountCmsServerSideProps.ts +3 -3
- package/src/experimental/myAccountServerSideProps.ts +4 -4
- package/src/pages/[slug]/p.tsx +18 -4
- package/src/pages/api/fs/logout.ts +18 -10
- package/src/pages/api/fs/switch-contract.ts +170 -21
- package/src/pages/api/graphql.ts +101 -60
- package/src/pages/pvt/account/403.tsx +5 -4
- package/src/pages/pvt/account/cards/index.tsx +5 -5
- package/src/pages/pvt/account/orders/[id].tsx +5 -5
- package/src/pages/pvt/account/orders/index.tsx +5 -5
- package/src/pages/pvt/account/profile.tsx +5 -5
- package/src/pages/pvt/account/quotes/index.tsx +5 -5
- package/src/pages/pvt/account/security.tsx +5 -5
- package/src/pages/pvt/account/user-details.tsx +6 -6
- package/src/sdk/account/getB2BSessionClaims.ts +1 -1
- package/src/sdk/account/getIsB2BBuyer.ts +28 -0
- package/src/sdk/account/refreshToken.ts +89 -11
- package/src/sdk/account/refreshTokenPaths.ts +25 -0
- package/src/sdk/account/useRefreshToken.ts +15 -11
- package/src/sdk/product/isProductNotFound.ts +13 -0
- package/src/sdk/product/usePageProductsQuery.ts +80 -21
- package/src/sdk/product/useProductNotFoundRedirect.ts +43 -0
- package/src/sdk/product/useProductQuery.ts +29 -0
- package/src/sdk/session/index.ts +46 -34
- package/src/server/cms/index.ts +59 -7
- package/src/server/cms/pdp.ts +10 -5
- package/src/server/cms/plp.ts +10 -5
- package/src/server/content/promiseCache.ts +68 -0
- package/src/server/content/service.ts +71 -51
- package/src/utils/getCheckoutParentDomain.ts +53 -0
- package/src/utils/isLocalHost.ts +2 -16
- package/src/utils/shouldBypassRefreshToken.ts +63 -0
- package/src/utils/trustedForwardedHost.ts +18 -0
- package/test/components/account/Layout.test.tsx +1 -1
- package/test/components/account/Menu.browser.test.tsx +7 -5
- package/test/components/account/OrganizationDrawer.browser.test.tsx +13 -13
- package/test/components/account/OrganizationDrawerBody.test.tsx +3 -3
- package/test/components/account/OrganizationSignInButton.browser.test.tsx +19 -4
- package/test/components/navigation/Navbar.browser.test.tsx +3 -3
- package/test/components/navigation/NavbarAccountArea.browser.test.tsx +5 -5
- package/test/components/navigation/NavbarSlider.browser.test.tsx +3 -3
- package/test/experimental/myAccountCmsServerSideProps.test.ts +5 -5
- package/test/pages/api/graphql.test.ts +175 -0
- package/test/pages/api/logout.test.ts +106 -0
- package/test/pages/api/switch-contract.test.ts +443 -15
- package/test/pages/pvt/account/403.test.ts +67 -0
- package/test/pages/pvt/account/cards/index.test.ts +3 -3
- package/test/sdk/account/getIsB2BBuyer.test.ts +52 -0
- package/test/sdk/account/refreshToken.browser.test.ts +10 -1
- package/test/sdk/account/refreshToken.test.ts +150 -18
- package/test/sdk/account/useRefreshToken.browser.test.ts +72 -0
- package/test/sdk/product/usePageProductsQuery.browser.test.ts +260 -0
- package/test/sdk/product/usePageProductsQuery.deliveryPromise.browser.test.ts +130 -0
- package/test/sdk/product/useProductNotFoundRedirect.browser.test.ts +83 -0
- package/test/sdk/product/useProductQuery.browser.test.ts +154 -0
- package/test/sdk/session/refreshTokenRoute.browser.test.ts +106 -0
- package/test/sdk/session/validateSession.browser.test.ts +111 -5
- package/test/server/cms/index.test.ts +102 -9
- package/test/server/content/promiseCache.test.ts +150 -0
- package/test/server/content/service.test.ts +277 -11
- package/test/utils/shouldBypassRefreshToken.browser.test.ts +38 -0
- package/test/utils/shouldBypassRefreshToken.test.ts +98 -0
- package/test/utils/trustedForwardedHost.test.ts +21 -0
- package/src/sdk/account/getIsRepresentative.ts +0 -20
- package/test/utils/isLocalHost.browser.test.ts +0 -37
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { parse } from 'cookie'
|
|
2
2
|
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
|
3
|
+
import { isIP } from 'node:net'
|
|
3
4
|
|
|
4
5
|
import storeConfig from 'discovery.config'
|
|
5
6
|
import { expireCookieServer, getCookieDomains } from 'src/utils/clearCookies'
|
|
7
|
+
import { getCheckoutParentDomain } from 'src/utils/getCheckoutParentDomain'
|
|
6
8
|
import { getRequestHostname } from 'src/utils/getRequestHostname'
|
|
9
|
+
import {
|
|
10
|
+
isCookieSafeHostname,
|
|
11
|
+
isHostAllowed,
|
|
12
|
+
removeCookieDomain,
|
|
13
|
+
singleForwardedHost,
|
|
14
|
+
} from 'src/utils/trustedForwardedHost'
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
17
|
* Cookies holding the checkout orderForm of the previous commercial context.
|
|
@@ -42,6 +50,23 @@ const isAuthCookie = (cookie: unknown): cookie is VtexAuthCookie => {
|
|
|
42
50
|
return Boolean(candidate?.Name?.trim() && candidate?.Value)
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
/**
|
|
54
|
+
* A non-positive `expiresIn` would turn into `Max-Age=0`, which expires the
|
|
55
|
+
* credential the response is supposed to install — logging the buyer out while
|
|
56
|
+
* the route reports success. Anything that is not a positive integer falls back
|
|
57
|
+
* to the default.
|
|
58
|
+
*/
|
|
59
|
+
const resolveMaxAge = (expiresIn: unknown): number =>
|
|
60
|
+
typeof expiresIn === 'number' && Number.isInteger(expiresIn) && expiresIn > 0
|
|
61
|
+
? expiresIn
|
|
62
|
+
: DEFAULT_AUTH_COOKIE_MAX_AGE
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Uses the forwarded protocol when a proxy sets it, otherwise the connection
|
|
66
|
+
* itself. A non-local hostname alone does not imply https: a `Secure` cookie
|
|
67
|
+
* sent over plain http is dropped by the browser while the route reports
|
|
68
|
+
* success.
|
|
69
|
+
*/
|
|
45
70
|
const isSecureRequest = (request: NextApiRequest): boolean => {
|
|
46
71
|
const forwarded = request.headers['x-forwarded-proto']
|
|
47
72
|
const proto = Array.isArray(forwarded) ? forwarded[0] : forwarded
|
|
@@ -50,41 +75,162 @@ const isSecureRequest = (request: NextApiRequest): boolean => {
|
|
|
50
75
|
return proto.split(',')[0].trim() === 'https'
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
const
|
|
78
|
+
const socket = request.socket
|
|
79
|
+
return Boolean(socket && 'encrypted' in socket && socket.encrypted)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Every parent of `hostname` with at least two labels, closest first:
|
|
84
|
+
* `a.b.store.com` gives `b.store.com` and `store.com`. A parent that is a
|
|
85
|
+
* public suffix (e.g. `com.br`) is rejected by the browser, so expiring a
|
|
86
|
+
* cookie there is a no-op.
|
|
87
|
+
*/
|
|
88
|
+
const getParentDomains = (hostname: string): string[] => {
|
|
89
|
+
const labels = hostname.split('.')
|
|
54
90
|
|
|
55
|
-
return
|
|
91
|
+
return labels
|
|
92
|
+
.slice(1, -1)
|
|
93
|
+
.map((_, index) => labels.slice(index + 1).join('.'))
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
97
|
+
* Hosts where the auth cookies stay host-only: WebOps previews, whose ingress
|
|
98
|
+
* does not pass the browser host in `Host`, and local development.
|
|
99
|
+
* `{account}.vtex.app` is a production domain that receives the browser host,
|
|
100
|
+
* so it gets a Domain like any custom domain. `/api/graphql` keeps its wider
|
|
101
|
+
* `.vtex.app` list for the cookies it relays from upstream.
|
|
63
102
|
*/
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
103
|
+
const HOST_ONLY_AUTH_COOKIE_SUFFIXES = [
|
|
104
|
+
'localhost',
|
|
105
|
+
'.localhost',
|
|
106
|
+
'.preview.vtex.app',
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
const isHostOnlyCookieHost = (hostname: string | null): boolean =>
|
|
110
|
+
!!hostname && isHostAllowed(hostname, HOST_ONLY_AUTH_COOKIE_SUFFIXES)
|
|
68
111
|
|
|
69
112
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
113
|
+
* Domain for the auth cookies, or `undefined` to write them host-only.
|
|
114
|
+
*
|
|
115
|
+
* Previews and local hosts stay host-only: behind the preview ingress `Host`
|
|
116
|
+
* is not the host the browser requested, and a mismatched Domain makes the browser drop
|
|
117
|
+
* the cookie while the route reports success. `x-forwarded-host` only decides
|
|
118
|
+
* that the cookie is host-only; it never chooses the Domain. IP literals and
|
|
119
|
+
* hosts that cannot be written verbatim into the attribute (including a
|
|
120
|
+
* trailing-dot FQDN) are host-only too.
|
|
121
|
+
*/
|
|
122
|
+
const resolveAuthCookieDomain = (
|
|
123
|
+
request: NextApiRequest
|
|
124
|
+
): string | undefined => {
|
|
125
|
+
const hostname = getRequestHostname(request.headers.host)
|
|
126
|
+
const forwardedHost = getRequestHostname(
|
|
127
|
+
singleForwardedHost(request.headers['x-forwarded-host'])
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
!hostname ||
|
|
132
|
+
isHostOnlyCookieHost(hostname) ||
|
|
133
|
+
isHostOnlyCookieHost(forwardedHost) ||
|
|
134
|
+
isIP(hostname) !== 0 ||
|
|
135
|
+
hostname.endsWith('.') ||
|
|
136
|
+
!isCookieSafeHostname(hostname)
|
|
137
|
+
) {
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return hostname
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Mirrors the attributes VTEX ID uses when it sets these cookies at login and
|
|
146
|
+
* refresh (`Domain=<host>; Path=/; Secure; SameSite=None; HttpOnly`).
|
|
147
|
+
*
|
|
148
|
+
* Browsers tell cookies apart by name, domain and path, and a cookie with a
|
|
149
|
+
* Domain attribute never matches a host-only one. Writing the contract token
|
|
150
|
+
* host-only would sit next to VTEX ID's cookie instead of replacing it, leaving
|
|
151
|
+
* two `VtexIdclientAutCookie_<account>` with different contracts. Each reader
|
|
152
|
+
* then picks a different one. Any host-only copy left by earlier versions is
|
|
153
|
+
* expired in the same batch.
|
|
154
|
+
*
|
|
155
|
+
* VTEX ID's refresh also writes the cookie on the parent domain (e.g.
|
|
156
|
+
* `Domain=store.com` for `www.store.com`). Parent copies are expired, so the
|
|
157
|
+
* only auth cookie left holds the switched contract. The exception is the
|
|
158
|
+
* parent shared with a checkout on a sibling host (`secure.store.com`): the
|
|
159
|
+
* token is written there too, with the same value as the host copy, so
|
|
160
|
+
* checkout keeps the session after the switch.
|
|
161
|
+
*
|
|
162
|
+
* The Domain-scoped cookie is always `Secure`, like VTEX ID's, because the
|
|
163
|
+
* forwarded protocol is not reliable behind the CDN. The host-only fallback
|
|
164
|
+
* follows the request protocol so plain-http local hosts keep working.
|
|
74
165
|
*/
|
|
75
166
|
const buildAuthCookies = (
|
|
76
167
|
payload: SwitchPropertiesResponse,
|
|
168
|
+
domain: string | undefined,
|
|
77
169
|
secure: boolean
|
|
78
170
|
): string[] => {
|
|
79
|
-
const
|
|
80
|
-
const
|
|
171
|
+
const checkoutParent = domain ? getCheckoutParentDomain(domain) : undefined
|
|
172
|
+
const staleScopes: Array<string | undefined> = domain
|
|
173
|
+
? [
|
|
174
|
+
undefined,
|
|
175
|
+
...getParentDomains(domain).filter(
|
|
176
|
+
(parent) => parent !== checkoutParent
|
|
177
|
+
),
|
|
178
|
+
]
|
|
179
|
+
: []
|
|
180
|
+
const writeScopes = checkoutParent ? [domain, checkoutParent] : [domain]
|
|
181
|
+
const attributes = (scope: string | undefined) =>
|
|
182
|
+
[
|
|
183
|
+
scope && `Domain=${scope}`,
|
|
184
|
+
'Path=/',
|
|
185
|
+
`Max-Age=${resolveMaxAge(payload.expiresIn)}`,
|
|
186
|
+
'HttpOnly',
|
|
187
|
+
(scope || secure) && 'Secure',
|
|
188
|
+
`SameSite=${scope ? 'None' : 'Lax'}`,
|
|
189
|
+
]
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
.join('; ')
|
|
81
192
|
|
|
82
193
|
return [payload.authCookie, payload.accountAuthCookie]
|
|
83
194
|
.filter(isAuthCookie)
|
|
84
|
-
.
|
|
85
|
-
(
|
|
86
|
-
|
|
87
|
-
|
|
195
|
+
.flatMap((cookie) => [
|
|
196
|
+
...staleScopes.map((staleDomain) =>
|
|
197
|
+
expireCookieServer({
|
|
198
|
+
name: cookie.Name,
|
|
199
|
+
path: '/',
|
|
200
|
+
domain: staleDomain,
|
|
201
|
+
})
|
|
202
|
+
),
|
|
203
|
+
...writeScopes.map(
|
|
204
|
+
(scope) => `${cookie.Name}=${cookie.Value}; ${attributes(scope)}`
|
|
205
|
+
),
|
|
206
|
+
])
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* `switch-properties` also issues `vid_rt` (the refresh token) on the
|
|
211
|
+
* Authenticator refresh paths. It is called server-side on
|
|
212
|
+
* `<account>.myvtex.com`, so its Domain points there and the browser would
|
|
213
|
+
* reject it. Re-scope it like the auth cookies so the next B2B refresh, which
|
|
214
|
+
* goes through the Authenticator route, finds it.
|
|
215
|
+
*
|
|
216
|
+
* Only relayed with `experimental.refreshToken` on: otherwise nothing uses it,
|
|
217
|
+
* and `/api/fs/logout` does not expire it either, so it would outlive logout.
|
|
218
|
+
*/
|
|
219
|
+
const relayRefreshTokenCookies = (
|
|
220
|
+
headers: Headers | undefined,
|
|
221
|
+
domain: string | undefined
|
|
222
|
+
): string[] => {
|
|
223
|
+
if (!storeConfig.experimental?.refreshToken) {
|
|
224
|
+
return []
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return (headers?.getSetCookie?.() ?? [])
|
|
228
|
+
.filter((cookie) => cookie.trimStart().startsWith('vid_rt='))
|
|
229
|
+
.map((cookie) => {
|
|
230
|
+
const hostOnly = removeCookieDomain(cookie)
|
|
231
|
+
|
|
232
|
+
return domain ? `${hostOnly}; Domain=${domain}` : hostOnly
|
|
233
|
+
})
|
|
88
234
|
}
|
|
89
235
|
|
|
90
236
|
const buildOrderFormExpiryCookies = (request: NextApiRequest): string[] => {
|
|
@@ -167,8 +313,11 @@ const handler: NextApiHandler<SwitchContractResponse> = async (
|
|
|
167
313
|
return
|
|
168
314
|
}
|
|
169
315
|
|
|
316
|
+
const domain = resolveAuthCookieDomain(request)
|
|
317
|
+
|
|
170
318
|
response.setHeader('Set-Cookie', [
|
|
171
|
-
...buildAuthCookies(payload, isSecureRequest(request)),
|
|
319
|
+
...buildAuthCookies(payload, domain, isSecureRequest(request)),
|
|
320
|
+
...relayRefreshTokenCookies(switchResponse.headers, domain),
|
|
172
321
|
...buildOrderFormExpiryCookies(request),
|
|
173
322
|
])
|
|
174
323
|
|
package/src/pages/api/graphql.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BadRequestError,
|
|
3
|
+
type CacheControl,
|
|
3
4
|
UnauthorizedError,
|
|
4
5
|
isFastStoreError,
|
|
5
6
|
stringifyCacheControl,
|
|
@@ -10,8 +11,9 @@ import type { NextApiHandler, NextApiRequest } from 'next'
|
|
|
10
11
|
import discoveryConfig from 'discovery.config'
|
|
11
12
|
import { getJWTAutCookie } from 'src/utils/getCookie'
|
|
12
13
|
import { getRequestHostname } from 'src/utils/getRequestHostname'
|
|
13
|
-
import {
|
|
14
|
+
import { shouldBypassRefreshTokenForRequest } from 'src/utils/shouldBypassRefreshToken'
|
|
14
15
|
import {
|
|
16
|
+
HOST_ONLY_COOKIE_HOST_SUFFIXES,
|
|
15
17
|
isHostAllowed,
|
|
16
18
|
removeCookieDomain,
|
|
17
19
|
singleForwardedHost,
|
|
@@ -24,7 +26,6 @@ const OTELLogger = logger('@faststore/core')
|
|
|
24
26
|
|
|
25
27
|
const DEFAULT_MAX_AGE = 5 * 60 // 5 minutes
|
|
26
28
|
const DEFAULT_STALE_WHILE_REVALIDATE = 60 * 60 // 1 hour
|
|
27
|
-
const ALLOWED_HOST_SUFFIXES = ['localhost', '.vtex.app', '.localhost']
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Recovers a FastStoreError from an execution error, whether it is the error
|
|
@@ -87,7 +88,8 @@ const normalizeSetCookieDomain = ({
|
|
|
87
88
|
OTELLogger('warn', 'Ignoring multi-value or malformed x-forwarded-host')
|
|
88
89
|
}
|
|
89
90
|
const host =
|
|
90
|
-
forwardedHost &&
|
|
91
|
+
forwardedHost &&
|
|
92
|
+
isHostAllowed(forwardedHost, HOST_ONLY_COOKIE_HOST_SUFFIXES)
|
|
91
93
|
? forwardedHost
|
|
92
94
|
: getRequestHostname(request.headers.host)
|
|
93
95
|
if (!host) {
|
|
@@ -96,7 +98,7 @@ const normalizeSetCookieDomain = ({
|
|
|
96
98
|
const cookieDomain = domainMatch[1]
|
|
97
99
|
|
|
98
100
|
if (
|
|
99
|
-
!isHostAllowed(host,
|
|
101
|
+
!isHostAllowed(host, HOST_ONLY_COOKIE_HOST_SUFFIXES) ||
|
|
100
102
|
!shouldReplaceCookieDomain({ cookieDomain, host })
|
|
101
103
|
) {
|
|
102
104
|
return setCookie
|
|
@@ -260,6 +262,87 @@ const respondToUnexpectedError = (
|
|
|
260
262
|
response.status(500).end()
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
/**
|
|
266
|
+
* With `experimental.refreshToken` on, rejects a ValidateSession with 401 when
|
|
267
|
+
* the token needs a refresh, so the client runs the refresh-token flow first.
|
|
268
|
+
*
|
|
269
|
+
* Skipped on localhost and WebOps previews: neither proxies VTEX ID, so the
|
|
270
|
+
* refresh would fail and log out a manually injected auth cookie.
|
|
271
|
+
*/
|
|
272
|
+
const assertValidateSessionTokenIsFresh = ({
|
|
273
|
+
request,
|
|
274
|
+
operationName,
|
|
275
|
+
sessionRefreshAfter,
|
|
276
|
+
}: {
|
|
277
|
+
request: NextApiRequest
|
|
278
|
+
operationName: string | undefined
|
|
279
|
+
sessionRefreshAfter: string | null | undefined
|
|
280
|
+
}) => {
|
|
281
|
+
if (
|
|
282
|
+
operationName !== 'ValidateSession' ||
|
|
283
|
+
!discoveryConfig.experimental?.refreshToken ||
|
|
284
|
+
shouldBypassRefreshTokenForRequest(request.headers)
|
|
285
|
+
) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const jwt = getJWTAutCookie({
|
|
290
|
+
headers: request.headers,
|
|
291
|
+
account: discoveryConfig.api.storeId,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
if (shouldForceRefreshTokenForValidateSession({ jwt, sessionRefreshAfter })) {
|
|
295
|
+
throw new UnauthorizedError(
|
|
296
|
+
'Unauthorized: Token expired. Please login again or refresh the page.'
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Cache-Control for a successful response: the directives collected while
|
|
303
|
+
* executing the operation when there are any, otherwise a default
|
|
304
|
+
* `s-maxage`/`stale-while-revalidate` for GET queries and `no-store` for
|
|
305
|
+
* everything else.
|
|
306
|
+
*/
|
|
307
|
+
const getSuccessCacheControl = ({
|
|
308
|
+
request,
|
|
309
|
+
operationName,
|
|
310
|
+
cacheControl,
|
|
311
|
+
hasAuthCookie,
|
|
312
|
+
}: {
|
|
313
|
+
request: NextApiRequest
|
|
314
|
+
operationName: string | undefined
|
|
315
|
+
cacheControl: CacheControl | undefined
|
|
316
|
+
hasAuthCookie: boolean
|
|
317
|
+
}) => {
|
|
318
|
+
if (cacheControl) {
|
|
319
|
+
return stringifyCacheControl(cacheControl, hasAuthCookie)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (
|
|
323
|
+
request.method !== 'GET' ||
|
|
324
|
+
!operationName?.toLowerCase()?.endsWith('query')
|
|
325
|
+
) {
|
|
326
|
+
return 'no-cache, no-store'
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const graphqlCacheControl = discoveryConfig?.experimental?.graphqlCacheControl
|
|
330
|
+
|
|
331
|
+
const maxAge =
|
|
332
|
+
graphqlCacheControl?.maxAge && graphqlCacheControl.maxAge > 0
|
|
333
|
+
? graphqlCacheControl.maxAge
|
|
334
|
+
: DEFAULT_MAX_AGE // 5 minutes
|
|
335
|
+
|
|
336
|
+
const staleWhileRevalidate =
|
|
337
|
+
graphqlCacheControl?.staleWhileRevalidate &&
|
|
338
|
+
graphqlCacheControl.staleWhileRevalidate > 0
|
|
339
|
+
? graphqlCacheControl.staleWhileRevalidate
|
|
340
|
+
: DEFAULT_STALE_WHILE_REVALIDATE // 1 hour
|
|
341
|
+
|
|
342
|
+
const scope = hasAuthCookie ? 'private' : 'public'
|
|
343
|
+
return `${scope}, s-maxage=${maxAge}, stale-while-revalidate=${staleWhileRevalidate}`
|
|
344
|
+
}
|
|
345
|
+
|
|
263
346
|
const handler: NextApiHandler = async (request, response) => {
|
|
264
347
|
if (request.method !== 'POST' && request.method !== 'GET') {
|
|
265
348
|
response.status(405).end()
|
|
@@ -286,29 +369,11 @@ const handler: NextApiHandler = async (request, response) => {
|
|
|
286
369
|
operation?.__meta__?.operationHash
|
|
287
370
|
)
|
|
288
371
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
discoveryConfig.experimental?.refreshToken
|
|
295
|
-
) {
|
|
296
|
-
const jwt = getJWTAutCookie({
|
|
297
|
-
headers: request.headers,
|
|
298
|
-
account: discoveryConfig.api.storeId,
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
const shouldRefreshToken = shouldForceRefreshTokenForValidateSession({
|
|
302
|
-
jwt,
|
|
303
|
-
sessionRefreshAfter: variables?.session?.refreshAfter,
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
if (shouldRefreshToken) {
|
|
307
|
-
throw new UnauthorizedError(
|
|
308
|
-
'Unauthorized: Token expired. Please login again or refresh the page.'
|
|
309
|
-
)
|
|
310
|
-
}
|
|
311
|
-
}
|
|
372
|
+
assertValidateSessionTokenIsFresh({
|
|
373
|
+
request,
|
|
374
|
+
operationName: operation.__meta__.operationName,
|
|
375
|
+
sessionRefreshAfter: variables?.session?.refreshAfter,
|
|
376
|
+
})
|
|
312
377
|
|
|
313
378
|
// Prevents to call ValidateSession or ValidateCartMutation without session (required) and get GraphQLError
|
|
314
379
|
const doNotRun =
|
|
@@ -338,39 +403,15 @@ const handler: NextApiHandler = async (request, response) => {
|
|
|
338
403
|
|
|
339
404
|
const hasAuthCookie = hasVtexIdclientAutCookie(request)
|
|
340
405
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
) {
|
|
351
|
-
const maxAge =
|
|
352
|
-
discoveryConfig?.experimental?.graphqlCacheControl?.maxAge &&
|
|
353
|
-
discoveryConfig?.experimental?.graphqlCacheControl?.maxAge > 0
|
|
354
|
-
? discoveryConfig.experimental.graphqlCacheControl.maxAge
|
|
355
|
-
: DEFAULT_MAX_AGE // 5 minutes
|
|
356
|
-
|
|
357
|
-
const staleWhileRevalidate =
|
|
358
|
-
discoveryConfig?.experimental?.graphqlCacheControl
|
|
359
|
-
?.staleWhileRevalidate &&
|
|
360
|
-
discoveryConfig?.experimental?.graphqlCacheControl
|
|
361
|
-
?.staleWhileRevalidate > 0
|
|
362
|
-
? discoveryConfig.experimental.graphqlCacheControl
|
|
363
|
-
.staleWhileRevalidate
|
|
364
|
-
: DEFAULT_STALE_WHILE_REVALIDATE // 1 hour
|
|
365
|
-
|
|
366
|
-
const scope = hasAuthCookie ? 'private' : 'public'
|
|
367
|
-
response.setHeader(
|
|
368
|
-
'cache-control',
|
|
369
|
-
`${scope}, s-maxage=${maxAge}, stale-while-revalidate=${staleWhileRevalidate}`
|
|
370
|
-
)
|
|
371
|
-
} else {
|
|
372
|
-
response.setHeader('cache-control', 'no-cache, no-store')
|
|
373
|
-
}
|
|
406
|
+
response.setHeader(
|
|
407
|
+
'cache-control',
|
|
408
|
+
getSuccessCacheControl({
|
|
409
|
+
request,
|
|
410
|
+
operationName: operation.__meta__.operationName,
|
|
411
|
+
cacheControl: extensions.cacheControl,
|
|
412
|
+
hasAuthCookie,
|
|
413
|
+
})
|
|
414
|
+
)
|
|
374
415
|
|
|
375
416
|
const setCookieValues = Array.from(extensions.cookies.values())
|
|
376
417
|
if (setCookieValues.length > 0 && !hasErrors) {
|
|
@@ -24,8 +24,7 @@ import PageProvider from 'src/sdk/overrides/PageProvider'
|
|
|
24
24
|
import { execute } from 'src/server'
|
|
25
25
|
import { injectGlobalSections } from 'src/server/cms/global'
|
|
26
26
|
import { fetchMyAccountPageContent } from 'src/server/cms/fetchMyAccountPageContent'
|
|
27
|
-
import {
|
|
28
|
-
import { isLocalHost } from 'src/utils/isLocalHost'
|
|
27
|
+
import { shouldBypassRefreshTokenForRequest } from 'src/utils/shouldBypassRefreshToken'
|
|
29
28
|
import { withLocaleValidationSSR } from 'src/utils/localization/withLocaleValidation'
|
|
30
29
|
import { getMyAccountRedirect } from 'src/utils/myAccountRedirect'
|
|
31
30
|
|
|
@@ -137,14 +136,16 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
137
136
|
|
|
138
137
|
const statusCode: number = (account.errors[0] as any)?.extensions?.status
|
|
139
138
|
|
|
140
|
-
const
|
|
139
|
+
const bypassRefreshToken = shouldBypassRefreshTokenForRequest(
|
|
140
|
+
context.req.headers
|
|
141
|
+
)
|
|
141
142
|
|
|
142
143
|
return {
|
|
143
144
|
props: {
|
|
144
145
|
globalSections: globalSectionsResult,
|
|
145
146
|
pageSections: pageContent.sections,
|
|
146
147
|
needsRefreshToken:
|
|
147
|
-
!
|
|
148
|
+
!bypassRefreshToken &&
|
|
148
149
|
(statusCode === 401 || statusCode === 403) &&
|
|
149
150
|
storeConfig.experimental?.refreshToken,
|
|
150
151
|
fromPage,
|
|
@@ -21,7 +21,7 @@ import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
|
|
|
21
21
|
import type { AccountCardsPageData } from 'src/sdk/account/accountPageContext'
|
|
22
22
|
import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
|
|
23
23
|
import { getB2BSessionClaims } from 'src/sdk/account/getB2BSessionClaims'
|
|
24
|
-
import {
|
|
24
|
+
import { getIsB2BBuyer } from 'src/sdk/account/getIsB2BBuyer'
|
|
25
25
|
import { execute } from 'src/server'
|
|
26
26
|
import { injectGlobalSections } from 'src/server/cms/global'
|
|
27
27
|
import { extractAccountNavigationData } from 'src/server/cms/myAccountDefaultSections'
|
|
@@ -72,7 +72,7 @@ export default function ListCardsPage({
|
|
|
72
72
|
navigationLabels,
|
|
73
73
|
accountPageData,
|
|
74
74
|
accountName,
|
|
75
|
-
|
|
75
|
+
isB2BBuyer,
|
|
76
76
|
}: ListCardsPageProps) {
|
|
77
77
|
const { sections: globalSections, settings: globalSettings } =
|
|
78
78
|
globalSectionsProp ?? {}
|
|
@@ -89,7 +89,7 @@ export default function ListCardsPage({
|
|
|
89
89
|
<NextSeo noindex nofollow />
|
|
90
90
|
|
|
91
91
|
<Layout
|
|
92
|
-
|
|
92
|
+
isB2BBuyer={isB2BBuyer}
|
|
93
93
|
accountName={accountName}
|
|
94
94
|
navigationLabels={navigationLabels}
|
|
95
95
|
>
|
|
@@ -109,7 +109,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
109
109
|
Record<string, string>,
|
|
110
110
|
Locator
|
|
111
111
|
> = async (context) => {
|
|
112
|
-
const
|
|
112
|
+
const isB2BBuyer = getIsB2BBuyer({
|
|
113
113
|
headers: context.req.headers as Record<string, string>,
|
|
114
114
|
account: storeConfig.api.storeId,
|
|
115
115
|
})
|
|
@@ -249,7 +249,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
249
249
|
hasError,
|
|
250
250
|
},
|
|
251
251
|
pageSections,
|
|
252
|
-
|
|
252
|
+
isB2BBuyer,
|
|
253
253
|
},
|
|
254
254
|
}
|
|
255
255
|
}
|
|
@@ -23,7 +23,7 @@ import storeConfig from 'discovery.config'
|
|
|
23
23
|
import { getGlobalSectionsData } from 'src/components/cms/GlobalSections'
|
|
24
24
|
import { default as AfterSection } from 'src/customizations/src/myAccount/extensions/orders/[id]/after'
|
|
25
25
|
import { default as BeforeSection } from 'src/customizations/src/myAccount/extensions/orders/[id]/before'
|
|
26
|
-
import {
|
|
26
|
+
import { getIsB2BBuyer } from 'src/sdk/account/getIsB2BBuyer'
|
|
27
27
|
import PageProvider from 'src/sdk/overrides/PageProvider'
|
|
28
28
|
import { execute } from 'src/server'
|
|
29
29
|
import { injectGlobalSections } from 'src/server/cms/global'
|
|
@@ -51,7 +51,7 @@ export default function OrderDetailsPage({
|
|
|
51
51
|
navigationLabels,
|
|
52
52
|
accountPageData,
|
|
53
53
|
accountName,
|
|
54
|
-
|
|
54
|
+
isB2BBuyer,
|
|
55
55
|
}: OrderDetailsPageProps) {
|
|
56
56
|
const { sections: globalSections, settings: globalSettings } =
|
|
57
57
|
globalSectionsProp ?? {}
|
|
@@ -68,7 +68,7 @@ export default function OrderDetailsPage({
|
|
|
68
68
|
<NextSeo noindex nofollow />
|
|
69
69
|
|
|
70
70
|
<Layout
|
|
71
|
-
|
|
71
|
+
isB2BBuyer={isB2BBuyer}
|
|
72
72
|
accountName={accountName}
|
|
73
73
|
navigationLabels={navigationLabels}
|
|
74
74
|
>
|
|
@@ -313,7 +313,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
313
313
|
Record<string, string>,
|
|
314
314
|
Locator
|
|
315
315
|
> = async (context) => {
|
|
316
|
-
const
|
|
316
|
+
const isB2BBuyer = getIsB2BBuyer({
|
|
317
317
|
headers: context.req.headers as Record<string, string>,
|
|
318
318
|
account: storeConfig.api.storeId,
|
|
319
319
|
})
|
|
@@ -415,7 +415,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
415
415
|
...(orderStatusLabels ? { orderStatusLabels } : {}),
|
|
416
416
|
},
|
|
417
417
|
pageSections,
|
|
418
|
-
|
|
418
|
+
isB2BBuyer,
|
|
419
419
|
},
|
|
420
420
|
}
|
|
421
421
|
}
|
|
@@ -22,7 +22,7 @@ import { default as BeforeSection } from 'src/customizations/src/myAccount/exten
|
|
|
22
22
|
import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
|
|
23
23
|
import type { AccountOrdersListPageData } from 'src/sdk/account/accountPageContext'
|
|
24
24
|
import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
|
|
25
|
-
import {
|
|
25
|
+
import { getIsB2BBuyer } from 'src/sdk/account/getIsB2BBuyer'
|
|
26
26
|
import { execute } from 'src/server'
|
|
27
27
|
import { injectGlobalSections } from 'src/server/cms/global'
|
|
28
28
|
import { extractAccountNavigationData } from 'src/server/cms/myAccountDefaultSections'
|
|
@@ -52,7 +52,7 @@ export default function ListOrdersPage({
|
|
|
52
52
|
navigationLabels,
|
|
53
53
|
accountPageData,
|
|
54
54
|
accountName,
|
|
55
|
-
|
|
55
|
+
isB2BBuyer,
|
|
56
56
|
}: ListOrdersPageProps) {
|
|
57
57
|
const { sections: globalSections, settings: globalSettings } =
|
|
58
58
|
globalSectionsProp ?? {}
|
|
@@ -69,7 +69,7 @@ export default function ListOrdersPage({
|
|
|
69
69
|
<NextSeo noindex nofollow />
|
|
70
70
|
|
|
71
71
|
<Layout
|
|
72
|
-
|
|
72
|
+
isB2BBuyer={isB2BBuyer}
|
|
73
73
|
accountName={accountName}
|
|
74
74
|
navigationLabels={navigationLabels}
|
|
75
75
|
>
|
|
@@ -132,7 +132,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
132
132
|
Record<string, string>,
|
|
133
133
|
Locator
|
|
134
134
|
> = async (context) => {
|
|
135
|
-
const
|
|
135
|
+
const isB2BBuyer = getIsB2BBuyer({
|
|
136
136
|
headers: context.req.headers as Record<string, string>,
|
|
137
137
|
account: storeConfig.api.storeId,
|
|
138
138
|
})
|
|
@@ -265,7 +265,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
265
265
|
},
|
|
266
266
|
},
|
|
267
267
|
pageSections,
|
|
268
|
-
|
|
268
|
+
isB2BBuyer,
|
|
269
269
|
},
|
|
270
270
|
}
|
|
271
271
|
}
|
|
@@ -22,7 +22,7 @@ import { default as BeforeSection } from 'src/customizations/src/myAccount/exten
|
|
|
22
22
|
import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
|
|
23
23
|
import type { AccountProfilePageData } from 'src/sdk/account/accountPageContext'
|
|
24
24
|
import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
|
|
25
|
-
import {
|
|
25
|
+
import { getIsB2BBuyer } from 'src/sdk/account/getIsB2BBuyer'
|
|
26
26
|
import { injectGlobalSections } from 'src/server/cms/global'
|
|
27
27
|
import { extractAccountNavigationData } from 'src/server/cms/myAccountDefaultSections'
|
|
28
28
|
import { fetchMyAccountPageContent } from 'src/server/cms/fetchMyAccountPageContent'
|
|
@@ -51,7 +51,7 @@ export default function Profile({
|
|
|
51
51
|
navigationLabels,
|
|
52
52
|
accountPageData,
|
|
53
53
|
accountName,
|
|
54
|
-
|
|
54
|
+
isB2BBuyer,
|
|
55
55
|
}: ProfilePageProps) {
|
|
56
56
|
const { sections: globalSections, settings: globalSettings } =
|
|
57
57
|
globalSectionsProp ?? {}
|
|
@@ -68,7 +68,7 @@ export default function Profile({
|
|
|
68
68
|
<NextSeo noindex nofollow />
|
|
69
69
|
|
|
70
70
|
<Layout
|
|
71
|
-
|
|
71
|
+
isB2BBuyer={isB2BBuyer}
|
|
72
72
|
accountName={accountName}
|
|
73
73
|
navigationLabels={navigationLabels}
|
|
74
74
|
>
|
|
@@ -108,7 +108,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
108
108
|
return { redirect }
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const
|
|
111
|
+
const isB2BBuyer = getIsB2BBuyer({
|
|
112
112
|
headers: context.req.headers as Record<string, string>,
|
|
113
113
|
account: storeConfig.api.storeId,
|
|
114
114
|
})
|
|
@@ -184,7 +184,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
184
184
|
},
|
|
185
185
|
pageSections,
|
|
186
186
|
navigationLabels: navigationData as AccountNavigationLabels,
|
|
187
|
-
|
|
187
|
+
isB2BBuyer,
|
|
188
188
|
},
|
|
189
189
|
}
|
|
190
190
|
}
|