From d36cb6095630482ed1f89d8d93e60e58c8e182b5 Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Thu, 5 Mar 2026 20:47:31 +0900 Subject: [PATCH] refactor: use fingerprinted auth headers for API calls Replace getHeaders with getHeadersWithFingerprint across billing, calendar, onboarding, and settings components to ensure device fingerprint is always included in API requests. Add new async method that fetches fingerprint if not cached and improve auth event tracking to prevent duplicate user identification and sign-in events. --- apps/desktop/src/auth/billing.tsx | 2 +- apps/desktop/src/auth/context.tsx | 69 ++++++++++++++----- apps/desktop/src/auth/useConnections.ts | 2 +- .../components/oauth/calendar-selection.tsx | 2 +- apps/desktop/src/onboarding/account/trial.tsx | 2 +- apps/desktop/src/settings/general/account.tsx | 4 +- 6 files changed, 57 insertions(+), 24 deletions(-) diff --git a/apps/desktop/src/auth/billing.tsx b/apps/desktop/src/auth/billing.tsx index 132cd6f914..6466e9dbc7 100644 --- a/apps/desktop/src/auth/billing.tsx +++ b/apps/desktop/src/auth/billing.tsx @@ -63,7 +63,7 @@ export function BillingProvider({ children }: { children: ReactNode }) { enabled: !!auth?.session && !billing.isPro, queryKey: [auth?.session?.user.id ?? "", "canStartTrial"], queryFn: async () => { - const headers = auth?.getHeaders(); + const headers = await auth?.getHeadersWithFingerprint(); if (!headers) { return false; } diff --git a/apps/desktop/src/auth/context.tsx b/apps/desktop/src/auth/context.tsx index 4771086208..7385024b7e 100644 --- a/apps/desktop/src/auth/context.tsx +++ b/apps/desktop/src/auth/context.tsx @@ -50,6 +50,7 @@ type AuthTokenHandlers = { type AuthUtils = { getHeaders: () => Record | null; + getHeadersWithFingerprint: () => Promise | null>; getAvatarUrl: () => Promise; }; @@ -105,37 +106,41 @@ async function initSession( } let trackedUserId: string | null = null; +let trackedSignedInEventUserId: string | null = null; async function trackAuthEvent( event: AuthChangeEvent, session: Session | null, ): Promise { if ((event === "SIGNED_IN" || event === "INITIAL_SESSION") && session) { - if (session.user.id === trackedUserId) { - return; + if (session.user.id !== trackedUserId) { + trackedUserId = session.user.id; + + const appVersion = await getVersion(); + await analyticsCommands.identify(session.user.id, { + email: session.user.email, + set: { + account_created_date: session.user.created_at, + is_signed_up: true, + app_version: appVersion, + os_version: osVersion(), + platform: platform(), + }, + }); } - trackedUserId = session.user.id; - - const appVersion = await getVersion(); - void analyticsCommands.identify(session.user.id, { - email: session.user.email, - set: { - account_created_date: session.user.created_at, - is_signed_up: true, - app_version: appVersion, - os_version: osVersion(), - platform: platform(), - }, - }); - - if (event === "SIGNED_IN") { - void analyticsCommands.event({ event: "user_signed_in" }); + if ( + event === "SIGNED_IN" && + session.user.id !== trackedSignedInEventUserId + ) { + trackedSignedInEventUserId = session.user.id; + await analyticsCommands.event({ event: "user_signed_in" }); } } if (event === "SIGNED_OUT") { trackedUserId = null; + trackedSignedInEventUserId = null; } } @@ -338,6 +343,32 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { return headers; }, [session, fingerprint]); + const getHeadersWithFingerprint = useCallback(async () => { + if (!session) { + return null; + } + + let resolvedFingerprint = fingerprint; + + if (!resolvedFingerprint) { + const result = await miscCommands.getFingerprint(); + if (result.status === "ok") { + resolvedFingerprint = result.data; + setFingerprint((prev) => prev ?? result.data); + } + } + + const headers: Record = { + Authorization: `${session.token_type} ${session.access_token}`, + }; + + if (resolvedFingerprint) { + headers[DEVICE_FINGERPRINT_HEADER] = resolvedFingerprint; + } + + return headers; + }, [session, fingerprint]); + const getAvatarUrl = useCallback(async () => { const email = session?.user.email; @@ -366,6 +397,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { handleAuthCallback, setSessionFromTokens, getHeaders, + getHeadersWithFingerprint, getAvatarUrl, }), [ @@ -377,6 +409,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { handleAuthCallback, setSessionFromTokens, getHeaders, + getHeadersWithFingerprint, getAvatarUrl, ], ); diff --git a/apps/desktop/src/auth/useConnections.ts b/apps/desktop/src/auth/useConnections.ts index 7c8da3a7c8..1e122d4471 100644 --- a/apps/desktop/src/auth/useConnections.ts +++ b/apps/desktop/src/auth/useConnections.ts @@ -14,7 +14,7 @@ export function useConnections(enabled = true) { return useQuery({ queryKey: ["integration-status", userId], queryFn: async () => { - const headers = auth?.getHeaders(); + const headers = await auth?.getHeadersWithFingerprint(); if (!headers) { return []; } diff --git a/apps/desktop/src/calendar/components/oauth/calendar-selection.tsx b/apps/desktop/src/calendar/components/oauth/calendar-selection.tsx index 8b083f8c66..c8f641cba3 100644 --- a/apps/desktop/src/calendar/components/oauth/calendar-selection.tsx +++ b/apps/desktop/src/calendar/components/oauth/calendar-selection.tsx @@ -46,7 +46,7 @@ export function useOAuthCalendarSelection(config: CalendarProvider) { } = useQuery({ queryKey: ["oauthCalendars", config.id], queryFn: async () => { - const headers = auth?.getHeaders(); + const headers = await auth?.getHeadersWithFingerprint(); if (!headers) return []; const client = createClient({ baseUrl: env.VITE_API_URL, headers }); const { data, error } = await googleListCalendars({ client }); diff --git a/apps/desktop/src/onboarding/account/trial.tsx b/apps/desktop/src/onboarding/account/trial.tsx index aca2108268..b346a6adea 100644 --- a/apps/desktop/src/onboarding/account/trial.tsx +++ b/apps/desktop/src/onboarding/account/trial.tsx @@ -28,7 +28,7 @@ export function useTrialFlow(onContinue: () => void) { const mutation = useMutation({ mutationFn: async () => { - const headers = auth.getHeaders(); + const headers = await auth.getHeadersWithFingerprint(); if (!headers) throw new Error("no headers"); const client = createClient({ baseUrl: env.VITE_API_URL, headers }); const { data, error } = await startTrial({ diff --git a/apps/desktop/src/settings/general/account.tsx b/apps/desktop/src/settings/general/account.tsx index 07b3f638f5..af731f60f0 100644 --- a/apps/desktop/src/settings/general/account.tsx +++ b/apps/desktop/src/settings/general/account.tsx @@ -306,7 +306,7 @@ function BillingButton() { enabled: !!auth?.session && !isPro, queryKey: [auth?.session?.user.id ?? "", "canStartTrial"], queryFn: async () => { - const headers = auth?.getHeaders(); + const headers = await auth?.getHeadersWithFingerprint(); if (!headers) { return false; } @@ -322,7 +322,7 @@ function BillingButton() { const startTrialMutation = useMutation({ mutationFn: async () => { - const headers = auth?.getHeaders(); + const headers = await auth?.getHeadersWithFingerprint(); if (!headers) { throw new Error("Not authenticated"); }