@faststore/core 4.10.1-dev.5 → 4.10.1-dev.7
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 +169 -157
- package/@generated/graphql.ts +1 -0
- package/@generated/schema.graphql +1 -1
- package/CHANGELOG.md +39 -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 +11 -2
- 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 +4 -3
- 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 +10 -5
- 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 +4 -4
- package/src/utils/getCheckoutParentDomain.ts +53 -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/logout.test.ts +106 -0
- package/test/pages/api/switch-contract.test.ts +443 -15
- 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 +4 -1
- 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 +4 -1
- package/test/utils/trustedForwardedHost.test.ts +21 -0
- package/src/sdk/account/getIsRepresentative.ts +0 -20
|
@@ -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
|
@@ -13,6 +13,7 @@ import { getJWTAutCookie } from 'src/utils/getCookie'
|
|
|
13
13
|
import { getRequestHostname } from 'src/utils/getRequestHostname'
|
|
14
14
|
import { shouldBypassRefreshTokenForRequest } from 'src/utils/shouldBypassRefreshToken'
|
|
15
15
|
import {
|
|
16
|
+
HOST_ONLY_COOKIE_HOST_SUFFIXES,
|
|
16
17
|
isHostAllowed,
|
|
17
18
|
removeCookieDomain,
|
|
18
19
|
singleForwardedHost,
|
|
@@ -25,7 +26,6 @@ const OTELLogger = logger('@faststore/core')
|
|
|
25
26
|
|
|
26
27
|
const DEFAULT_MAX_AGE = 5 * 60 // 5 minutes
|
|
27
28
|
const DEFAULT_STALE_WHILE_REVALIDATE = 60 * 60 // 1 hour
|
|
28
|
-
const ALLOWED_HOST_SUFFIXES = ['localhost', '.vtex.app', '.localhost']
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Recovers a FastStoreError from an execution error, whether it is the error
|
|
@@ -88,7 +88,8 @@ const normalizeSetCookieDomain = ({
|
|
|
88
88
|
OTELLogger('warn', 'Ignoring multi-value or malformed x-forwarded-host')
|
|
89
89
|
}
|
|
90
90
|
const host =
|
|
91
|
-
forwardedHost &&
|
|
91
|
+
forwardedHost &&
|
|
92
|
+
isHostAllowed(forwardedHost, HOST_ONLY_COOKIE_HOST_SUFFIXES)
|
|
92
93
|
? forwardedHost
|
|
93
94
|
: getRequestHostname(request.headers.host)
|
|
94
95
|
if (!host) {
|
|
@@ -97,7 +98,7 @@ const normalizeSetCookieDomain = ({
|
|
|
97
98
|
const cookieDomain = domainMatch[1]
|
|
98
99
|
|
|
99
100
|
if (
|
|
100
|
-
!isHostAllowed(host,
|
|
101
|
+
!isHostAllowed(host, HOST_ONLY_COOKIE_HOST_SUFFIXES) ||
|
|
101
102
|
!shouldReplaceCookieDomain({ cookieDomain, host })
|
|
102
103
|
) {
|
|
103
104
|
return setCookie
|
|
@@ -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
|
}
|
|
@@ -27,7 +27,7 @@ import BeforeSection from 'src/customizations/src/myAccount/extensions/quotes/be
|
|
|
27
27
|
import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
|
|
28
28
|
import type { AccountQuotesPageData } from 'src/sdk/account/accountPageContext'
|
|
29
29
|
import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
|
|
30
|
-
import {
|
|
30
|
+
import { getIsB2BBuyer } from 'src/sdk/account/getIsB2BBuyer'
|
|
31
31
|
import PageProvider from 'src/sdk/overrides/PageProvider'
|
|
32
32
|
import { execute } from 'src/server'
|
|
33
33
|
import { injectGlobalSections } from 'src/server/cms/global'
|
|
@@ -56,7 +56,7 @@ export default function ListQuotesPage({
|
|
|
56
56
|
navigationLabels,
|
|
57
57
|
accountPageData,
|
|
58
58
|
accountName,
|
|
59
|
-
|
|
59
|
+
isB2BBuyer,
|
|
60
60
|
}: ListQuotesPageProps) {
|
|
61
61
|
const { sections: globalSections, settings: globalSettings } =
|
|
62
62
|
globalSectionsProp ?? {}
|
|
@@ -73,7 +73,7 @@ export default function ListQuotesPage({
|
|
|
73
73
|
<NextSeo noindex nofollow />
|
|
74
74
|
|
|
75
75
|
<Layout
|
|
76
|
-
|
|
76
|
+
isB2BBuyer={isB2BBuyer}
|
|
77
77
|
accountName={accountName}
|
|
78
78
|
navigationLabels={navigationLabels}
|
|
79
79
|
>
|
|
@@ -122,7 +122,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
122
122
|
Record<string, string>,
|
|
123
123
|
Locator
|
|
124
124
|
> = async (context) => {
|
|
125
|
-
const
|
|
125
|
+
const isB2BBuyer = getIsB2BBuyer({
|
|
126
126
|
headers: context.req.headers as Record<string, string>,
|
|
127
127
|
account: storeConfig.api.storeId,
|
|
128
128
|
})
|
|
@@ -273,7 +273,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
273
273
|
},
|
|
274
274
|
},
|
|
275
275
|
pageSections,
|
|
276
|
-
|
|
276
|
+
isB2BBuyer,
|
|
277
277
|
},
|
|
278
278
|
}
|
|
279
279
|
}
|
|
@@ -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 { AccountSecurityPageData } 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'
|
|
@@ -51,7 +51,7 @@ export default function Page({
|
|
|
51
51
|
navigationLabels,
|
|
52
52
|
accountPageData,
|
|
53
53
|
accountName,
|
|
54
|
-
|
|
54
|
+
isB2BBuyer,
|
|
55
55
|
}: SecurityPageProps) {
|
|
56
56
|
const { sections: globalSections, settings: globalSettings } =
|
|
57
57
|
globalSectionsProp ?? {}
|
|
@@ -68,7 +68,7 @@ export default function Page({
|
|
|
68
68
|
<NextSeo noindex nofollow />
|
|
69
69
|
|
|
70
70
|
<Layout
|
|
71
|
-
|
|
71
|
+
isB2BBuyer={isB2BBuyer}
|
|
72
72
|
accountName={accountName}
|
|
73
73
|
navigationLabels={navigationLabels}
|
|
74
74
|
>
|
|
@@ -101,7 +101,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
101
101
|
Record<string, string>,
|
|
102
102
|
Locator
|
|
103
103
|
> = async (context) => {
|
|
104
|
-
const
|
|
104
|
+
const isB2BBuyer = getIsB2BBuyer({
|
|
105
105
|
headers: context.req.headers as Record<string, string>,
|
|
106
106
|
account: storeConfig.api.storeId,
|
|
107
107
|
})
|
|
@@ -185,7 +185,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
185
185
|
},
|
|
186
186
|
globalSections: globalSectionsResult,
|
|
187
187
|
pageSections,
|
|
188
|
-
|
|
188
|
+
isB2BBuyer,
|
|
189
189
|
},
|
|
190
190
|
}
|
|
191
191
|
}
|
|
@@ -23,7 +23,7 @@ import { default as BeforeSection } from 'src/customizations/src/myAccount/exten
|
|
|
23
23
|
import type { MyAccountProps } from 'src/experimental/myAccountServerSideProps'
|
|
24
24
|
import type { AccountUserDetailsPageData } from 'src/sdk/account/accountPageContext'
|
|
25
25
|
import type { AccountNavigationLabels } from 'src/sdk/account/getMyAccountRoutes'
|
|
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'
|
|
@@ -50,7 +50,7 @@ export default function Page({
|
|
|
50
50
|
navigationLabels,
|
|
51
51
|
accountPageData,
|
|
52
52
|
accountName,
|
|
53
|
-
|
|
53
|
+
isB2BBuyer,
|
|
54
54
|
}: UserDetailsPageProps) {
|
|
55
55
|
const { sections: globalSections, settings: globalSettings } =
|
|
56
56
|
globalSectionsProp ?? {}
|
|
@@ -67,7 +67,7 @@ export default function Page({
|
|
|
67
67
|
<NextSeo noindex nofollow />
|
|
68
68
|
|
|
69
69
|
<Layout
|
|
70
|
-
|
|
70
|
+
isB2BBuyer={isB2BBuyer}
|
|
71
71
|
accountName={accountName}
|
|
72
72
|
navigationLabels={navigationLabels}
|
|
73
73
|
>
|
|
@@ -105,7 +105,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
105
105
|
Record<string, string>,
|
|
106
106
|
Locator
|
|
107
107
|
> = async (context) => {
|
|
108
|
-
const
|
|
108
|
+
const isB2BBuyer = getIsB2BBuyer({
|
|
109
109
|
headers: context.req.headers as Record<string, string>,
|
|
110
110
|
account: storeConfig.api.storeId,
|
|
111
111
|
})
|
|
@@ -118,7 +118,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
118
118
|
return { redirect }
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
if (!
|
|
121
|
+
if (!isB2BBuyer) {
|
|
122
122
|
return {
|
|
123
123
|
redirect: {
|
|
124
124
|
destination: localizeRedirectDestination('/pvt/account', context),
|
|
@@ -209,7 +209,7 @@ const getServerSidePropsBase: GetServerSideProps<
|
|
|
209
209
|
},
|
|
210
210
|
},
|
|
211
211
|
pageSections,
|
|
212
|
-
|
|
212
|
+
isB2BBuyer,
|
|
213
213
|
},
|
|
214
214
|
}
|
|
215
215
|
}
|
|
@@ -10,7 +10,7 @@ export type B2BSessionClaims = {
|
|
|
10
10
|
/**
|
|
11
11
|
* Whether the buyer is associated with a Unit/Contract. Per the Personal
|
|
12
12
|
* Cards PRD (REQ-2), this is detected by the presence of a `unitId` claim in
|
|
13
|
-
* the VTEX ID JWT — the same cookie `
|
|
13
|
+
* the VTEX ID JWT — the same cookie `getIsB2BBuyer` already decodes.
|
|
14
14
|
* Gates the Shared tab (FR-5).
|
|
15
15
|
*
|
|
16
16
|
* Note this is the only part of the gate that lives in the token. The
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parse } from 'cookie'
|
|
2
|
+
import { parseJwt } from 'src/utils/getCookie'
|
|
3
|
+
|
|
4
|
+
type GetIsB2BBuyerParams = {
|
|
5
|
+
headers?: Record<string, string>
|
|
6
|
+
account: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether the buyer is a B2B member: the VTEX ID token carries both an
|
|
11
|
+
* organizational unit and a contract, the rule Buyer Portal uses. The
|
|
12
|
+
* `isRepresentative` claim is not read, since it can be true outside B2B.
|
|
13
|
+
*/
|
|
14
|
+
export function getIsB2BBuyer({
|
|
15
|
+
headers,
|
|
16
|
+
account,
|
|
17
|
+
}: GetIsB2BBuyerParams): boolean {
|
|
18
|
+
const authCookie = parse(headers?.cookie ?? '')?.[
|
|
19
|
+
'VtexIdclientAutCookie_' + account
|
|
20
|
+
]
|
|
21
|
+
const jwt = parseJwt(authCookie)
|
|
22
|
+
|
|
23
|
+
return isNonBlankString(jwt?.unitId) && isNonBlankString(jwt?.customerId)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isNonBlankString(value: unknown): boolean {
|
|
27
|
+
return typeof value === 'string' && value.trim() !== ''
|
|
28
|
+
}
|