From 5fd7f1d7dda949b2e734661a9f9030d69889cdd0 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 3 Apr 2026 05:07:28 +0530 Subject: [PATCH 1/3] perf: optimize twitter header parsing in browser extension (#819) --- apps/browser-extension/utils/twitter-auth.ts | 33 ++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/browser-extension/utils/twitter-auth.ts b/apps/browser-extension/utils/twitter-auth.ts index bb918cad7..8a791fa27 100644 --- a/apps/browser-extension/utils/twitter-auth.ts +++ b/apps/browser-extension/utils/twitter-auth.ts @@ -23,15 +23,30 @@ export async function captureTwitterTokens( return false } - const authHeader = details.requestHeaders?.find( - (header) => header.name.toLowerCase() === "authorization", - ) - const cookieHeader = details.requestHeaders?.find( - (header) => header.name.toLowerCase() === "cookie", - ) - const csrfHeader = details.requestHeaders?.find( - (header) => header.name.toLowerCase() === "x-csrf-token", - ) + let authHeader: chrome.webRequest.HttpHeader | undefined + let cookieHeader: chrome.webRequest.HttpHeader | undefined + let csrfHeader: chrome.webRequest.HttpHeader | undefined + + if (details.requestHeaders) { + for (const header of details.requestHeaders) { + if (!header.name) continue + const name = header.name.toLowerCase() + + switch (name) { + case "authorization": + authHeader = header + break + case "cookie": + cookieHeader = header + break + case "x-csrf-token": + csrfHeader = header + break + } + + if (authHeader && cookieHeader && csrfHeader) break + } + } if (authHeader?.value && cookieHeader?.value && csrfHeader?.value) { const tokensAlreadyLogged = await getTokensLogged() From c012f3b5c454121a547fe54e6ecbd6e11b465730 Mon Sep 17 00:00:00 2001 From: Prasanna721 <106952318+Prasanna721@users.noreply.github.com> Date: Fri, 3 Apr 2026 01:55:24 +0000 Subject: [PATCH 2/3] fix stale session cookie (#823) - redirect to login when session is gone instead of blank screen - show cached username while session restores so header doesn't flicker - cleaned up redundant type casts and unused vars --- apps/web/components/ensure-workspace.tsx | 10 ++++++++-- apps/web/components/header.tsx | 14 ++++++++------ packages/lib/auth-context.tsx | 8 +++----- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/web/components/ensure-workspace.tsx b/apps/web/components/ensure-workspace.tsx index a9bd88230..370d5ca0a 100644 --- a/apps/web/components/ensure-workspace.tsx +++ b/apps/web/components/ensure-workspace.tsx @@ -10,8 +10,14 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) { const { session, organizations, isRestoring } = useAuth() useEffect(() => { - if (!session) return - if (isRestoring || organizations === null) return + if (isRestoring) return + if (!session) { + router.replace( + `/login?redirect=${encodeURIComponent(window.location.href)}`, + ) + return + } + if (organizations === null) return if (organizations.length > 0) return if (pathname.startsWith("/onboarding")) return router.replace("/onboarding/welcome?step=input") diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx index 38c5c68c4..a66626b07 100644 --- a/apps/web/components/header.tsx +++ b/apps/web/components/header.tsx @@ -34,7 +34,7 @@ import { useIsMobile } from "@hooks/use-mobile" import { useLocalStorageUsername } from "@hooks/use-local-storage-username" import { UserProfileMenu } from "@/components/user-profile-menu" import { FeedbackModal } from "./feedback-modal" -import { useViewMode } from "@/lib/view-mode-context" +import { useViewMode, type ViewMode } from "@/lib/view-mode-context" import { useQueryState } from "nuqs" import { feedbackParam } from "@/lib/search-params" @@ -45,7 +45,7 @@ interface HeaderProps { } export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) { - const { user } = useAuth() + const { user, isRestoring } = useAuth() const { selectedProjects, setSelectedProjects } = useProject() const router = useRouter() const isMobile = useIsMobile() @@ -53,14 +53,16 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) { "feedback", feedbackParam, ) - const isFeedbackOpen = feedbackOpen ?? false const { viewMode, setViewMode } = useViewMode() const handleFeedback = () => setFeedbackOpen(true) const localStorageUsername = useLocalStorageUsername() const displayName = - user?.displayUsername || localStorageUsername || user?.name || "" + user?.displayUsername || + (isRestoring ? localStorageUsername : "") || + user?.name || + "" const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My" return (
@@ -141,7 +143,7 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) { - setViewMode(v === "grid" ? "list" : (v as "graph" | "integrations")) + setViewMode(v === "grid" ? "list" : (v as ViewMode)) } > @@ -307,7 +309,7 @@ export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
setFeedbackOpen(false)} /> diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx index cfb9b9ab7..83f65f463 100644 --- a/packages/lib/auth-context.tsx +++ b/packages/lib/auth-context.tsx @@ -42,7 +42,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { isPending: orgsPending, } = authClient.useListOrganizations() - const organizations: OrganizationListItem[] | null = + const organizations = session?.session == null ? null : orgsPending ? null : (orgsData ?? []) const refetchOrganizations = useCallback( @@ -74,9 +74,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { }, []) useEffect(() => { - if (isSessionPending) { - return - } + if (isSessionPending) return if (!session?.session) { setIsRestoring(false) @@ -150,7 +148,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { return () => { cancelled = true } - }, [session, isSessionPending, orgsData, orgsPending, setActiveOrg]) + }, [isSessionPending, session, orgsData, orgsPending, setActiveOrg]) useEffect(() => { if (typeof window === "undefined") return From 8405305c505b9efde9be867239cfdd995929e306 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:29:24 +0000 Subject: [PATCH 3/3] feat(nova): delete account with all orgs (#821) --- apps/web/components/settings/account.tsx | 254 ++++++++++++++++++++--- apps/web/hooks/use-account-settings.ts | 103 +++++++++ packages/lib/auth-context.tsx | 9 + 3 files changed, 339 insertions(+), 27 deletions(-) create mode 100644 apps/web/hooks/use-account-settings.ts diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index b8b51e1ec..4ca5a9cb1 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -3,6 +3,10 @@ import { dmSans125ClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import { useAuth } from "@lib/auth-context" +import { + useAccountMemberships, + useDeleteUserAccount, +} from "@/hooks/use-account-settings" import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" import { useTokenUsage } from "@/hooks/use-token-usage" import { formatUsageNumber, tokensToCredits } from "@/lib/billing-utils" @@ -24,7 +28,8 @@ import { ChevronDown, Building2, } from "lucide-react" -import { useState } from "react" +import { useMemo, useState } from "react" +import { toast } from "sonner" function SectionTitle({ children }: { children: React.ReactNode }) { return ( @@ -86,14 +91,53 @@ function PlanFeatureRow({ ) } +function formatOrgRole(role: string): string { + const r = role.toLowerCase() + if (r === "owner") return "Owner" + if (r === "admin") return "Admin" + if (r === "member") return "Member" + return role + ? role.charAt(0).toUpperCase() + role.slice(1).toLowerCase() + : "Member" +} + export default function Account() { - const { user, org, setActiveOrg } = useAuth() + const { user, org, setActiveOrg, clearActiveOrg } = useAuth() const autumn = useCustomer() const [isUpgrading, setIsUpgrading] = useState(false) - const [deleteConfirmText, setDeleteConfirmText] = useState("") + const [emailConfirm, setEmailConfirm] = useState("") + const [notifyWhenDeleted, setNotifyWhenDeleted] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) + const [isClosingAccount, setIsClosingAccount] = useState(false) const [switchingOrgId, setSwitchingOrgId] = useState(null) const { data: allOrgs } = authClient.useListOrganizations() + const { data: memberships, isPending: membershipsPending } = + useAccountMemberships() + + const sortedMemberships = useMemo(() => { + if (!memberships?.length) return [] + return [...memberships].sort((a, b) => a.name.localeCompare(b.name)) + }, [memberships]) + + const ownedOrgs = useMemo( + () => memberships?.filter((m) => m.role === "owner") ?? [], + [memberships], + ) + + const hasOwnedOrgWithTeammates = useMemo( + () => ownedOrgs.some((m) => m.memberCount > 1), + [ownedOrgs], + ) + + const showMembershipsOverview = + !membershipsPending && + (sortedMemberships.length > 1 || hasOwnedOrgWithTeammates) + + const deleteUserAccount = useDeleteUserAccount() + + const emailMatches = user?.email + ? emailConfirm.trim().toLowerCase() === user.email.trim().toLowerCase() + : false const handleOrgSwitch = async (orgSlug: string, orgId: string) => { if (orgId === org?.id) return @@ -142,16 +186,33 @@ export default function Account() { } } - const handleDeleteAccount = () => { - if (deleteConfirmText !== "DELETE") return - // TODO: Implement account deletion API call - console.log("Delete account requested") - setIsDeleteDialogOpen(false) - setDeleteConfirmText("") + const handleDeleteAccount = async () => { + if (!user?.email || !emailMatches || membershipsPending) return + setIsClosingAccount(true) + try { + await deleteUserAccount.mutateAsync({ + confirmation: user.email, + notifyOnComplete: notifyWhenDeleted, + }) + clearActiveOrg() + try { + await authClient.signOut() + } catch { + window.location.assign("/login/new") + return + } + setIsDeleteDialogOpen(false) + setEmailConfirm("") + setNotifyWhenDeleted(false) + window.location.assign("/login/new") + } catch (e) { + const msg = e instanceof Error ? e.message : "Something went wrong" + toast.error(msg) + } finally { + setIsClosingAccount(false) + } } - const isDeleteEnabled = deleteConfirmText === "DELETE" - // Format member since date const memberSince = user?.createdAt ? new Date(user.createdAt).toLocaleDateString("en-US", { @@ -722,7 +783,10 @@ export default function Account() { open={isDeleteDialogOpen} onOpenChange={(open) => { setIsDeleteDialogOpen(open) - if (!open) setDeleteConfirmText("") + if (!open) { + setEmailConfirm("") + setNotifyWhenDeleted(false) + } }} > @@ -755,7 +819,7 @@ export default function Account() { {/* Header */}
-
+

- This will permanently delete your memories, - conversations, settings and cancel any active - subscriptions. + This cannot be undone.

+ {hasOwnedOrgWithTeammates && ( +

+ You own at least one organization that still has + other members. Those organizations will be deleted + for everyone when you confirm. +

+ )} +
+ + What happens next? + + +
+

+ Your account is locked immediately; data removal + runs in the background. +

+
    +
  • + Removes memories, conversations, and settings; + cancels active subscriptions. +
  • +
  • + Orgs where you're only a member: + you're removed; the org continues. +
  • +
  • Orgs you own: deleted for all members.
  • +
+
+
+ {showMembershipsOverview && ( +
+

+ Your organizations +

+
+ {sortedMemberships.map((m) => ( +
+
+

+ {m.name} +

+ {m.slug ? ( +

+ {m.slug} +

+ ) : null} +
+
+ + {formatOrgRole(m.role)} + + + {m.memberCount} member + {m.memberCount === 1 ? "" : "s"} + +
+
+ ))} +
+
+ )} + {/* Confirmation input */}

- Type DELETE to - confirm: + Type your account email to confirm:

setDeleteConfirmText(e.target.value)} - placeholder="DELETE" + autoComplete="off" + value={emailConfirm} + onChange={(e) => setEmailConfirm(e.target.value)} + placeholder={user?.email ?? "you@example.com"} className={cn( "w-full px-4 py-3 bg-transparent", "text-[#FAFAFA] placeholder:text-[#737373]", @@ -822,6 +993,26 @@ export default function Account() { />
+
@@ -841,20 +1032,29 @@ export default function Account() {
diff --git a/apps/web/hooks/use-account-settings.ts b/apps/web/hooks/use-account-settings.ts new file mode 100644 index 000000000..b6eea5954 --- /dev/null +++ b/apps/web/hooks/use-account-settings.ts @@ -0,0 +1,103 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { useAuth } from "@lib/auth-context" + +const API_BASE = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + +export type AccountMembership = { + orgId: string + name: string + slug: string + role: string + memberCount: number +} + +export function useAccountMemberships() { + const { user } = useAuth() + + return useQuery({ + queryKey: ["account", "memberships"], + queryFn: async () => { + const res = await fetch(`${API_BASE}/v3/auth/account/memberships`, { + credentials: "include", + }) + if (!res.ok) { + throw new Error("Failed to load organizations") + } + const data = (await res.json()) as { + organizations: AccountMembership[] + } + return data.organizations + }, + enabled: !!user?.id, + }) +} + +export function useLeaveNonOwnerMemberships() { + return useMutation({ + mutationFn: async () => { + const res = await fetch( + `${API_BASE}/v3/auth/account/leave-non-owner-memberships`, + { + method: "POST", + credentials: "include", + }, + ) + if (!res.ok) { + const body = (await res.json().catch(() => ({}))) as { + message?: string + } + throw new Error(body.message ?? "Failed to leave organizations") + } + return (await res.json()) as { + success: boolean + removedOrgIds: string[] + } + }, + }) +} + +export type DeleteUserAccountInput = { + confirmation: string + notifyOnComplete?: boolean +} + +export function useDeleteUserAccount() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ + confirmation, + notifyOnComplete, + }: DeleteUserAccountInput) => { + const res = await fetch(`${API_BASE}/v3/auth/account/delete`, { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + confirmation, + ...(notifyOnComplete === true ? { notifyOnComplete: true } : {}), + }), + }) + if (res.status === 202) { + return (await res.json().catch(() => ({}))) as { + status?: string + alreadyPending?: boolean + } + } + if (!res.ok) { + const body = (await res.json().catch(() => ({}))) as { + message?: string + error?: string + } + throw new Error( + body.message ?? body.error ?? "Failed to start account deletion", + ) + } + return {} + }, + onSuccess: () => { + queryClient.clear() + }, + }) +} diff --git a/packages/lib/auth-context.tsx b/packages/lib/auth-context.tsx index 83f65f463..f1456c960 100644 --- a/packages/lib/auth-context.tsx +++ b/packages/lib/auth-context.tsx @@ -26,6 +26,7 @@ interface AuthContextType { isRestoring: boolean isSessionPending: boolean setActiveOrg: (orgSlug: string) => Promise + clearActiveOrg: () => void updateOrgMetadata: (partial: Record) => void refetchOrganizations: () => Promise } @@ -60,6 +61,13 @@ export function AuthProvider({ children }: { children: ReactNode }) { localStorage.setItem(STORAGE_KEY, slug) }, []) + const clearActiveOrg = useCallback(() => { + setOrg(null) + try { + localStorage.removeItem(STORAGE_KEY) + } catch {} + }, []) + const updateOrgMetadata = useCallback((partial: Record) => { setOrg((prev) => { if (!prev) return prev @@ -188,6 +196,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { session: session?.session ?? null, user: session?.user ?? null, setActiveOrg, + clearActiveOrg, updateOrgMetadata, refetchOrganizations, }}