From ac91638ff4412db9a2417502fd2dbf22bd4a3378 Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Fri, 28 Feb 2025 11:16:47 +0000 Subject: [PATCH 1/7] =?UTF-8?q?chore:=20=F0=9F=A4=96=20changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/long-lions-behave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/long-lions-behave.md diff --git a/.changeset/long-lions-behave.md b/.changeset/long-lions-behave.md new file mode 100644 index 0000000..a42b8d0 --- /dev/null +++ b/.changeset/long-lions-behave.md @@ -0,0 +1,5 @@ +--- +"@fleek-platform/login-button": minor +--- + +Extend session dismissal check with cookie mismatching locals storage session details From 073db2e7bacad811de70e85ef4fd7b36a6af6ad9 Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Fri, 28 Feb 2025 13:44:13 +0000 Subject: [PATCH 2/7] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20util=20for=20?= =?UTF-8?q?token=20truncate=20middle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/token.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/utils/token.ts b/src/utils/token.ts index 9e50be4..8a96dfc 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -15,3 +15,14 @@ export const decodeAccessToken = (accessToken: string) => { return projectId; }; + +export const truncateMiddle = ( + str: string, + numOfChars: number = 3, + ellipsis: string = '...' +): string => { + const start = str.substring(0, numOfChars); + const end = str.slice(-numOfChars); + + return `${start}${ellipsis}${end}`; +} From 113adda3958b5e70657b5e6cfef39362936cd467 Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Fri, 28 Feb 2025 13:44:40 +0000 Subject: [PATCH 3/7] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20set=20cookies=20?= =?UTF-8?q?closer=20to=20state=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/store/authStore.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 9d25fa3..f2f71e2 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -5,6 +5,7 @@ import { useConfigStore } from './configStore'; import { getStoreName } from '../utils/store'; import { decodeAccessToken } from '../utils/token'; import type { UserProfile } from '@dynamic-labs/sdk-react-core'; +import { cookies } from '../utils/cookies'; export type TriggerLoginModal = (open: boolean) => void; export type TriggerLogout = () => void; @@ -69,8 +70,15 @@ export const useAuthStore = create()( accessToken, projectId, }); + + cookies.set('accessToken', accessToken); + cookies.set('projectId', projectId); + }, + setAuthToken: (authToken: string) => { + set({ authToken }); + + cookies.set('authToken', authToken); }, - setAuthToken: (authToken: string) => set({ authToken }), setIsLoggingIn: (isLoggingIn: boolean) => set({ isLoggingIn }), setIsLoggedIn: (isLoggedIn: boolean) => set({ isLoggedIn }), reset: () => set(initialState), @@ -100,11 +108,15 @@ export const useAuthStore = create()( throw new Error('Failed to get access token'); } + const accessToken = res.data; + set({ - accessToken: res.data, + accessToken, projectId, isLoggingIn: false, }); + + cookies.set('accessToken', accessToken); } catch (err) { console.error('Failed to update access token:', err); From 068660491151d540e5c300379eaff15657b2f1a6 Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Fri, 28 Feb 2025 13:51:14 +0000 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20extend=20user=20?= =?UTF-8?q?session=20checkups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/providers/DynamicProvider.tsx | 56 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/providers/DynamicProvider.tsx b/src/providers/DynamicProvider.tsx index 8ed60d3..78d16d2 100644 --- a/src/providers/DynamicProvider.tsx +++ b/src/providers/DynamicProvider.tsx @@ -9,7 +9,7 @@ import { type TriggerLoginModal, type TriggerLogout, useAuthStore } from '../sto import { cookies } from '../utils/cookies'; import type { LoginProviderChildrenProps } from './LoginProvider'; import { clearStorageByMatchTerm } from '../utils/browser'; -import { decodeAccessToken } from '../utils/token'; +import { decodeAccessToken, truncateMiddle } from '../utils/token'; import cssOverrides from '../css/index.css'; type DynamicUtilsProps = { @@ -47,22 +47,27 @@ const validateUserSession = async ({ graphqlApiUrl, projectId, onAuthenticationFailure, + onAuthenticationSuccess, }: { accessToken: string; graphqlApiUrl: string; projectId: string; onAuthenticationFailure: () => void; + onAuthenticationSuccess: () => void; }): Promise => { try { const { success: meSuccess } = await me(graphqlApiUrl, accessToken); const { success: projectSuccess } = await project(graphqlApiUrl, accessToken, projectId); - if (!meSuccess || !projectSuccess) { - onAuthenticationFailure(); - return false; - } + if (!meSuccess || !projectSuccess) throw Error('Unexpected user session details'); + + const cookieAccessToken = cookies.get('accessToken'); + if (cookieAccessToken !== accessToken) throw Error(`Expected ${truncateMiddle(accessToken)} but got ${typeof cookieAccessToken === 'string' ? truncateMiddle(cookieAccessToken) : typeof cookieAccessToken}`); + + typeof onAuthenticationSuccess === 'function' && onAuthenticationSuccess(); + return true; } catch (error) { console.error('Authentication validation failed:', error); @@ -90,12 +95,13 @@ export const DynamicProvider: FC = ({ children, graphqlApi const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); - // TODO: Remove useCallback to inspect re-triggers const onLogout = useCallback(() => { cookies.reset(); // TODO: Make sure the reset is not clearing // the trigger callbacks resetStore(); + // TODO: Dashboard has a concurrent process + // that should also match these requirements // Clear critical stores for (const item of ['dynamic', 'wagmi', 'fleek-xyz']) { clearStorageByMatchTerm(item); @@ -141,24 +147,6 @@ export const DynamicProvider: FC = ({ children, graphqlApi [graphqlApiUrl, setAuthToken, setAccessToken, setIsLoggedIn, setUserProfile, setIsNewUser, onAuthenticationSuccess], ); - useEffect(() => { - if (!accessToken) return; - - cookies.set('accessToken', accessToken); - }, [accessToken]); - - useEffect(() => { - if (!authToken) return; - - cookies.set('authToken', authToken); - }, [authToken]); - - useEffect(() => { - if (!projectId) return; - - cookies.set('projectId', projectId); - }, [projectId]); - useEffect(() => { const authToken = cookies.get('authToken'); const accessToken = cookies.get('accessToken'); @@ -184,15 +172,27 @@ export const DynamicProvider: FC = ({ children, graphqlApi useEffect(() => { if (!accessToken || !graphqlApiUrl) return; - // Validates the user session sometime in the future - // if found faulty, it should clear the user session + // Validates the user session sometime in the future. + // If found faulty, it should clear the user session + // e.g. user session clear/logout by dashboard. + // On the other hand, an existing user session can + // persist (localStorage), but dashboard uses cookies + // e.g. user logins in website and expect cross session. + // This is more of a safe-guard due to Dashboard + // having the requirement to clear localStorage items + // that match prefix `fleek-xyz-login`, meaning + // we're only computing if that fails to happen validateUserSession({ accessToken, graphqlApiUrl, projectId, - onAuthenticationFailure: () => typeof triggerLogout === 'function' && triggerLogout(), + onAuthenticationFailure: () => onLogout(), + onAuthenticationSuccess: () => { + cookies.set('accessToken', accessToken); + cookies.set('projectId', projectId); + } }); - }, [accessToken, graphqlApiUrl, projectId, triggerLogout]); + }, [onLogout]); const settings = { environmentId: dynamicEnvironmentId, From 64fbae64f04e3fc96629c5a946cf1df63896c520 Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Fri, 28 Feb 2025 14:09:36 +0000 Subject: [PATCH 5/7] =?UTF-8?q?chore:=20=F0=9F=A4=96=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/providers/DynamicProvider.tsx | 9 ++++++--- src/store/authStore.ts | 2 +- src/utils/token.ts | 12 ++++-------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/providers/DynamicProvider.tsx b/src/providers/DynamicProvider.tsx index 78d16d2..b274d5b 100644 --- a/src/providers/DynamicProvider.tsx +++ b/src/providers/DynamicProvider.tsx @@ -64,10 +64,13 @@ const validateUserSession = async ({ const cookieAccessToken = cookies.get('accessToken'); - if (cookieAccessToken !== accessToken) throw Error(`Expected ${truncateMiddle(accessToken)} but got ${typeof cookieAccessToken === 'string' ? truncateMiddle(cookieAccessToken) : typeof cookieAccessToken}`); + if (cookieAccessToken !== accessToken) + throw Error( + `Expected ${truncateMiddle(accessToken)} but got ${typeof cookieAccessToken === 'string' ? truncateMiddle(cookieAccessToken) : typeof cookieAccessToken}`, + ); typeof onAuthenticationSuccess === 'function' && onAuthenticationSuccess(); - + return true; } catch (error) { console.error('Authentication validation failed:', error); @@ -190,7 +193,7 @@ export const DynamicProvider: FC = ({ children, graphqlApi onAuthenticationSuccess: () => { cookies.set('accessToken', accessToken); cookies.set('projectId', projectId); - } + }, }); }, [onLogout]); diff --git a/src/store/authStore.ts b/src/store/authStore.ts index f2f71e2..173e562 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -115,7 +115,7 @@ export const useAuthStore = create()( projectId, isLoggingIn: false, }); - + cookies.set('accessToken', accessToken); } catch (err) { console.error('Failed to update access token:', err); diff --git a/src/utils/token.ts b/src/utils/token.ts index 8a96dfc..8075292 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -16,13 +16,9 @@ export const decodeAccessToken = (accessToken: string) => { return projectId; }; -export const truncateMiddle = ( - str: string, - numOfChars: number = 3, - ellipsis: string = '...' -): string => { - const start = str.substring(0, numOfChars); +export const truncateMiddle = (str: string, numOfChars: number = 3, ellipsis: string = '...'): string => { + const start = str.substring(0, numOfChars); const end = str.slice(-numOfChars); - + return `${start}${ellipsis}${end}`; -} +}; From f48d13217ede0774674ea4273a7dee16e4df6d5c Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Fri, 28 Feb 2025 14:10:18 +0000 Subject: [PATCH 6/7] =?UTF-8?q?chore:=20=F0=9F=A4=96=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/token.ts b/src/utils/token.ts index 8075292..5ac9fe7 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -16,7 +16,7 @@ export const decodeAccessToken = (accessToken: string) => { return projectId; }; -export const truncateMiddle = (str: string, numOfChars: number = 3, ellipsis: string = '...'): string => { +export const truncateMiddle = (str: string, numOfChars = 3, ellipsis = '...'): string => { const start = str.substring(0, numOfChars); const end = str.slice(-numOfChars); From 1edaf72c69a4e022fea7f3a12625ab486fbe8c3a Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Fri, 28 Feb 2025 14:40:03 +0000 Subject: [PATCH 7/7] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/store/authStore.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/store/authStore.ts b/src/store/authStore.ts index 173e562..3d958a7 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -108,6 +108,10 @@ export const useAuthStore = create()( throw new Error('Failed to get access token'); } + // TODO: Make a projectId validation against + // the accessToken projectId as it should match. + // On failure, throw error. + const accessToken = res.data; set({