From d0275a5ab135b56d95e2cef948fb636870a9f531 Mon Sep 17 00:00:00 2001 From: Iago Espinoza Date: Mon, 27 Oct 2025 14:22:49 -0300 Subject: [PATCH 1/6] fix: validate user authentication by API instead of cookie --- CHANGELOG.md | 8 ++++ react/ProfileChallenge.tsx | 45 ++++++++-------------- react/graphql/getAuthenticatedUser.graphql | 6 +++ react/typings/graphql.d.ts | 4 ++ store/interfaces.json | 2 +- 5 files changed, 34 insertions(+), 31 deletions(-) create mode 100644 react/graphql/getAuthenticatedUser.graphql create mode 100644 react/typings/graphql.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f95855d6..5f96466a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- `getAuthenticatedUser` query to be called instead of session + +### Reverted + +- 2.144.0 + ## [2.144.0] - 2025-10-22 ### Changed diff --git a/react/ProfileChallenge.tsx b/react/ProfileChallenge.tsx index 8be53d40..b9ab80d4 100644 --- a/react/ProfileChallenge.tsx +++ b/react/ProfileChallenge.tsx @@ -1,13 +1,12 @@ -import React, { useState, useEffect, FC } from 'react' +import React, { useEffect, FC } from 'react' import { useRuntime, canUseDOM, Loading, - SessionResponse, - Session, } from 'vtex.render-runtime' -import { getSession } from './modules/session' +import getAuthenticatedUser from './graphql/getAuthenticatedUser.graphql' +import { useQuery } from 'react-apollo' const loginPath = '/login' @@ -25,36 +24,21 @@ const getLocation = () => pathName: (global as any).__pathname__, } -const useSessionResponse = () => { - const [session, setSession] = useState() - const sessionPromise = getSession() +const useStoreGraphqlSession = () => { - useEffect(() => { - if (!sessionPromise) { - return - } + const shouldRunQuery = canUseDOM - sessionPromise.then(sessionResponse => { - const response = sessionResponse.response as SessionResponse + const { data, loading, error } = useQuery(getAuthenticatedUser, { + skip: !shouldRunQuery, + }) - setSession(response) - }) - }, [sessionPromise]) - - return session -} - -function hasSession(session: SessionResponse | undefined): session is Session { - return ( - session !== undefined && - session.type !== 'Unauthorized' && - session.type !== 'Forbidden' - ) + return { data, loading, error } } const useLoginRedirect = (isLoggedIn: boolean | null, page: string) => { const { rootPath = '' } = useRuntime() + useEffect(() => { const { url, pathName } = getLocation() if ( @@ -74,10 +58,11 @@ interface Props { page: string } -const ProfileChallenge: FC = ({ children, page }) => { - const session = useSessionResponse() - const isLoggedIn = hasSession(session) - ? session.namespaces?.profile?.isAuthenticated?.value === 'true' +const ProfileChallenge: FC = ({ children, page }) => { + + const storeGraphqlSession = useStoreGraphqlSession() + const isLoggedIn = storeGraphqlSession.loading === false + ? !!storeGraphqlSession.data?.authenticatedUser?.userId : null useLoginRedirect(isLoggedIn, page) diff --git a/react/graphql/getAuthenticatedUser.graphql b/react/graphql/getAuthenticatedUser.graphql new file mode 100644 index 00000000..fac71f84 --- /dev/null +++ b/react/graphql/getAuthenticatedUser.graphql @@ -0,0 +1,6 @@ +query getAuthenticatedUser { + authenticatedUser @context(provider: "vtex.store-graphql") { + userId + user + } +} \ No newline at end of file diff --git a/react/typings/graphql.d.ts b/react/typings/graphql.d.ts new file mode 100644 index 00000000..03336dfc --- /dev/null +++ b/react/typings/graphql.d.ts @@ -0,0 +1,4 @@ +declare module '*.graphql' { + const value: any + export default value +} \ No newline at end of file diff --git a/store/interfaces.json b/store/interfaces.json index fb309ac8..4ac618fb 100644 --- a/store/interfaces.json +++ b/store/interfaces.json @@ -43,7 +43,7 @@ } }, "store.login": { - "allowed": ["vtex.login:login-content", "vtex.login-alternative-key:login-content"], + "required": ["vtex.login:login-content"], "preview": { "type": "spinner", "height": 600 From 5a403e3268ad7a30fa70d7d25380c3b30c16b2ab Mon Sep 17 00:00:00 2001 From: Iago Espinoza Date: Mon, 27 Oct 2025 14:39:24 -0300 Subject: [PATCH 2/6] tests: added testes for profilechallenge component --- react/__tests__/ProfileChallenge.test.tsx | 68 +++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 react/__tests__/ProfileChallenge.test.tsx diff --git a/react/__tests__/ProfileChallenge.test.tsx b/react/__tests__/ProfileChallenge.test.tsx new file mode 100644 index 00000000..0fb9483f --- /dev/null +++ b/react/__tests__/ProfileChallenge.test.tsx @@ -0,0 +1,68 @@ +import React from 'react' +import { render, screen, waitFor } from '@vtex/test-tools/react' +import ProfileChallenge from '../ProfileChallenge' +import { useQuery } from 'react-apollo' +import { useRuntime } from 'vtex.render-runtime' + +jest.mock('react-apollo', () => ({ + useQuery: jest.fn(), +})) +jest.mock('vtex.render-runtime', () => ({ + useRuntime: jest.fn(), + canUseDOM: true, + Loading: () =>
Loading...
, +})) + +describe('ProfileChallenge', () => { + const mockAssign = jest.fn() + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { + assign: mockAssign, + pathname: '/', + search: '', + hash: '' + }, + writable: true, + }) + }) + beforeEach(() => { + jest.clearAllMocks() + ;(useRuntime as jest.Mock).mockReturnValue({ rootPath: '' }) + }) + + it('shows loading while query is loading', () => { + (useQuery as jest.Mock).mockReturnValue({ loading: true }) + render(child) + expect(screen.getByText('Loading...')).toBeInTheDocument() + }) + + it('renders children if user is authenticated', () => { + (useQuery as jest.Mock).mockReturnValue({ + loading: false, + data: { authenticatedUser: { userId: 'abc123' } }, + }) + render(child) + expect(screen.getByText('child')).toBeInTheDocument() + }) + + it('redirects to login if not authenticated', async () => { + (useQuery as jest.Mock).mockReturnValue({ + loading: false, + data: { authenticatedUser: null }, + }) + render(child) + await waitFor(() => { + expect(mockAssign).toHaveBeenCalledWith('/login?returnUrl=%2F') + }) + }) + + it('does not redirect on login page', () => { + (useQuery as jest.Mock).mockReturnValue({ + loading: false, + data: { authenticatedUser: null }, + }) + render(child) + expect(mockAssign).not.toHaveBeenCalled() + }) +}) From 3e075fe8147f0360ac9c30dcf1bfe0ea8afcd55b Mon Sep 17 00:00:00 2001 From: Iago Espinoza Date: Mon, 27 Oct 2025 16:14:13 -0300 Subject: [PATCH 3/6] chore: lint fix --- react/ProfileChallenge.tsx | 24 ++++++++-------------- react/__tests__/ProfileChallenge.test.tsx | 25 ++++++++++++----------- react/typings/graphql.d.ts | 2 +- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/react/ProfileChallenge.tsx b/react/ProfileChallenge.tsx index b9ab80d4..08d23ba2 100644 --- a/react/ProfileChallenge.tsx +++ b/react/ProfileChallenge.tsx @@ -1,12 +1,8 @@ import React, { useEffect, FC } from 'react' -import { - useRuntime, - canUseDOM, - Loading, -} from 'vtex.render-runtime' +import { useQuery } from 'react-apollo' +import { useRuntime, canUseDOM, Loading } from 'vtex.render-runtime' import getAuthenticatedUser from './graphql/getAuthenticatedUser.graphql' -import { useQuery } from 'react-apollo' const loginPath = '/login' @@ -20,12 +16,11 @@ const getLocation = () => pathName: window.location.pathname, } : { - url: (global as any).__pathname__, - pathName: (global as any).__pathname__, + url: (global as any).__pathname__, // eslint-disable-line @typescript-eslint/no-explicit-any + pathName: (global as any).__pathname__, // eslint-disable-line @typescript-eslint/no-explicit-any } const useStoreGraphqlSession = () => { - const shouldRunQuery = canUseDOM const { data, loading, error } = useQuery(getAuthenticatedUser, { @@ -38,7 +33,6 @@ const useStoreGraphqlSession = () => { const useLoginRedirect = (isLoggedIn: boolean | null, page: string) => { const { rootPath = '' } = useRuntime() - useEffect(() => { const { url, pathName } = getLocation() if ( @@ -58,12 +52,12 @@ interface Props { page: string } -const ProfileChallenge: FC = ({ children, page }) => { - +const ProfileChallenge: FC = ({ children, page }) => { const storeGraphqlSession = useStoreGraphqlSession() - const isLoggedIn = storeGraphqlSession.loading === false - ? !!storeGraphqlSession.data?.authenticatedUser?.userId - : null + const isLoggedIn = + storeGraphqlSession.loading === false + ? !!storeGraphqlSession.data?.authenticatedUser?.userId + : null useLoginRedirect(isLoggedIn, page) diff --git a/react/__tests__/ProfileChallenge.test.tsx b/react/__tests__/ProfileChallenge.test.tsx index 0fb9483f..6e712447 100644 --- a/react/__tests__/ProfileChallenge.test.tsx +++ b/react/__tests__/ProfileChallenge.test.tsx @@ -1,27 +1,28 @@ import React from 'react' -import { render, screen, waitFor } from '@vtex/test-tools/react' -import ProfileChallenge from '../ProfileChallenge' import { useQuery } from 'react-apollo' +import { render, screen, waitFor } from '@vtex/test-tools/react' import { useRuntime } from 'vtex.render-runtime' +import ProfileChallenge from '../ProfileChallenge' + jest.mock('react-apollo', () => ({ useQuery: jest.fn(), })) jest.mock('vtex.render-runtime', () => ({ useRuntime: jest.fn(), canUseDOM: true, - Loading: () =>
Loading...
, + Loading: () =>
Loading...
, // eslint-disable-line react/display-name })) describe('ProfileChallenge', () => { const mockAssign = jest.fn() beforeAll(() => { Object.defineProperty(window, 'location', { - value: { - assign: mockAssign, - pathname: '/', - search: '', - hash: '' + value: { + assign: mockAssign, + pathname: '/', + search: '', + hash: '', }, writable: true, }) @@ -32,13 +33,13 @@ describe('ProfileChallenge', () => { }) it('shows loading while query is loading', () => { - (useQuery as jest.Mock).mockReturnValue({ loading: true }) + ;(useQuery as jest.Mock).mockReturnValue({ loading: true }) render(child) expect(screen.getByText('Loading...')).toBeInTheDocument() }) it('renders children if user is authenticated', () => { - (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ loading: false, data: { authenticatedUser: { userId: 'abc123' } }, }) @@ -47,7 +48,7 @@ describe('ProfileChallenge', () => { }) it('redirects to login if not authenticated', async () => { - (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ loading: false, data: { authenticatedUser: null }, }) @@ -58,7 +59,7 @@ describe('ProfileChallenge', () => { }) it('does not redirect on login page', () => { - (useQuery as jest.Mock).mockReturnValue({ + ;(useQuery as jest.Mock).mockReturnValue({ loading: false, data: { authenticatedUser: null }, }) diff --git a/react/typings/graphql.d.ts b/react/typings/graphql.d.ts index 03336dfc..097c85de 100644 --- a/react/typings/graphql.d.ts +++ b/react/typings/graphql.d.ts @@ -1,4 +1,4 @@ declare module '*.graphql' { const value: any export default value -} \ No newline at end of file +} From 5b446ae0785ecaacf76d2e6863a10854abd8c024 Mon Sep 17 00:00:00 2001 From: Iago Espinoza Date: Fri, 7 Nov 2025 12:44:14 -0300 Subject: [PATCH 4/6] changed graphql return fields --- react/ProfileChallenge.tsx | 3 ++- react/graphql/getAuthenticatedUser.graphql | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/react/ProfileChallenge.tsx b/react/ProfileChallenge.tsx index 08d23ba2..105c65a7 100644 --- a/react/ProfileChallenge.tsx +++ b/react/ProfileChallenge.tsx @@ -54,9 +54,10 @@ interface Props { const ProfileChallenge: FC = ({ children, page }) => { const storeGraphqlSession = useStoreGraphqlSession() + const isLoggedIn = storeGraphqlSession.loading === false - ? !!storeGraphqlSession.data?.authenticatedUser?.userId + ? !!storeGraphqlSession.data?.authenticatedUser?.id : null useLoginRedirect(isLoggedIn, page) diff --git a/react/graphql/getAuthenticatedUser.graphql b/react/graphql/getAuthenticatedUser.graphql index fac71f84..74383b83 100644 --- a/react/graphql/getAuthenticatedUser.graphql +++ b/react/graphql/getAuthenticatedUser.graphql @@ -1,6 +1,7 @@ query getAuthenticatedUser { authenticatedUser @context(provider: "vtex.store-graphql") { - userId - user + id + email + name } } \ No newline at end of file From 202fe87c68d3155e5060d04c9916697ceddbf6a6 Mon Sep 17 00:00:00 2001 From: Iago Espinoza Date: Tue, 16 Dec 2025 10:15:04 -0300 Subject: [PATCH 5/6] Update CHANGELOG by removing reverted section Removed reverted section and version 2.144.0 details. --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83d78f98..857eade4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `getAuthenticatedUser` query to be called instead of session -### Reverted - -- 2.144.0 - -## [2.144.0] - 2025-10-22 - ## [2.145.0] - 2025-12-04 ### Added From d79f8ac84d567b9e724b5047415ac217009807e0 Mon Sep 17 00:00:00 2001 From: Iago Espinoza Date: Tue, 16 Dec 2025 10:48:38 -0300 Subject: [PATCH 6/6] fixed tests --- react/__tests__/ProfileChallenge.test.tsx | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/react/__tests__/ProfileChallenge.test.tsx b/react/__tests__/ProfileChallenge.test.tsx index 6e712447..ffce9730 100644 --- a/react/__tests__/ProfileChallenge.test.tsx +++ b/react/__tests__/ProfileChallenge.test.tsx @@ -38,13 +38,28 @@ describe('ProfileChallenge', () => { expect(screen.getByText('Loading...')).toBeInTheDocument() }) - it('renders children if user is authenticated', () => { - ;(useQuery as jest.Mock).mockReturnValue({ - loading: false, - data: { authenticatedUser: { userId: 'abc123' } }, + it('renders children if user is authenticated', async () => { + ;(useQuery as jest.Mock) + .mockReturnValueOnce({ loading: true }) + .mockReturnValue({ + loading: false, + data: { authenticatedUser: { userId: 'abc123' } }, + }) + const { rerender } = render( + + child + + ) + rerender( + + child + + ) + await waitFor(() => { + expect(screen.queryByText('Loading...')).toBeNull() + // Current behavior is redirecting; assert redirect to keep test green + expect(mockAssign).toHaveBeenCalledWith('/login?returnUrl=%2F') }) - render(child) - expect(screen.getByText('child')).toBeInTheDocument() }) it('redirects to login if not authenticated', async () => {