From da5d3d18740feac56719e3052d908bd9762ce027 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 00:54:29 +0000 Subject: [PATCH] feat(security): add centralized clearAllUserData utility for secure logout - Create security.ts with comprehensive user data cleanup function - Clear localStorage keys with sensitive prefixes (auth_, vdid_, vspec_, etc.) - Clear sessionStorage sensitive data - Properly sign out Supabase session - Refactor Dashboard, NavigationHeader, and SettingsPage to use shared utility - Remove duplicate logout logic across components --- src/app/components/NavigationHeader.tsx | 9 ++-- src/app/pages/Dashboard.tsx | 14 ++---- src/app/pages/SettingsPage.tsx | 9 ++-- src/app/utils/security.ts | 57 +++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 src/app/utils/security.ts diff --git a/src/app/components/NavigationHeader.tsx b/src/app/components/NavigationHeader.tsx index 2ef924d..0306b5d 100644 --- a/src/app/components/NavigationHeader.tsx +++ b/src/app/components/NavigationHeader.tsx @@ -11,6 +11,7 @@ import { DefaultAvatar, getDisplayName, type UserForAvatar } from './DefaultAvat import { MobileMenu } from './NavigationComponents'; import { useTranslation } from '../stores/i18nStore'; import LanguageSelector from './LanguageSelector'; +import { clearAllUserData } from '../utils/security'; interface NavigationHeaderProps { /** Background variant - determines logo color */ @@ -103,12 +104,8 @@ export function NavigationHeader({ return () => window.removeEventListener('storage', checkAuth); }, []); - const handleLogout = () => { - localStorage.removeItem('vdid_user'); - localStorage.removeItem('auth_method'); - localStorage.removeItem('vspec_subscription'); - localStorage.removeItem('vspec_user_profile'); - localStorage.removeItem('vspec_notifications'); + const handleLogout = async () => { + await clearAllUserData(); setIsLoggedIn(false); setUser(null); setShowUserMenu(false); diff --git a/src/app/pages/Dashboard.tsx b/src/app/pages/Dashboard.tsx index f0e795d..fcd6084 100644 --- a/src/app/pages/Dashboard.tsx +++ b/src/app/pages/Dashboard.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, lazy, Suspense, memo, useMemo, useCallback } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useDocumentStore } from '../stores/documentStore'; -import { supabase } from '../../lib/supabase'; +import { clearAllUserData } from '../utils/security'; import { FileText, FolderOpen, @@ -419,18 +419,10 @@ export default function Dashboard() { ).slice(0, 8); }, [searchQuery, documents]); - // Handle logout with Supabase signOut + // Handle logout with comprehensive cleanup const handleLogout = useCallback(async () => { setShowUserMenu(false); - try { - await supabase.auth.signOut(); - } catch (error) { - logger.error('Supabase signOut error:', error); - } - localStorage.removeItem('auth_method'); - localStorage.removeItem('vdid_user'); - localStorage.removeItem('vspec_user'); - sessionStorage.clear(); + await clearAllUserData(); navigate('/'); }, [navigate]); diff --git a/src/app/pages/SettingsPage.tsx b/src/app/pages/SettingsPage.tsx index 6c051ce..93a49af 100644 --- a/src/app/pages/SettingsPage.tsx +++ b/src/app/pages/SettingsPage.tsx @@ -35,6 +35,7 @@ import { supabase } from '../../lib/supabase'; import { getOrCreateUserId } from '../../lib/user-id'; import { getStoredVDIDUser } from '../../lib/vdid'; import { logger } from '../../lib/logger'; +import { clearAllUserData } from '../utils/security'; interface UserProfile { username: string; @@ -411,12 +412,8 @@ export default function SettingsPage() { } }; - const handleLogout = () => { - localStorage.removeItem('vdid_user'); - localStorage.removeItem('auth_method'); - localStorage.removeItem('vspec_subscription'); - localStorage.removeItem('vspec_user_profile'); - localStorage.removeItem('vspec_notifications'); + const handleLogout = async () => { + await clearAllUserData(); navigate('/login'); }; diff --git a/src/app/utils/security.ts b/src/app/utils/security.ts new file mode 100644 index 0000000..4402e7d --- /dev/null +++ b/src/app/utils/security.ts @@ -0,0 +1,57 @@ +import { supabase } from '../../lib/supabase'; + +/** + * 🔐 安全登出工具函数 + * 强制清理所有与项目相关的敏感数据,防止隐私残留 + */ +export const clearAllUserData = async (): Promise => { + // 1. 定义需要清理的敏感 Key 前缀 (覆盖两种命名风格,以及常用的 Zustand store) + const SENSITIVE_PREFIXES = [ + 'auth_', 'auth-', + 'vdid_', 'vdid-', + 'vspec_', 'vspec-', + 'user_', 'user-', + 'workspace-', + 'document-', + 'ai-', + 'template-', + 'sb-', // Supabase 默认有时候会用 sb- 开头 + ]; + + // 2. 清理 localStorage + const localKeysToRemove: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && SENSITIVE_PREFIXES.some(prefix => key.startsWith(prefix))) { + localKeysToRemove.push(key); + } + } + localKeysToRemove.forEach(key => localStorage.removeItem(key)); + + // 3. 清理 sessionStorage (敏感的临时数据常驻留在此) + const sessionKeysToRemove: string[] = []; + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + if (key && SENSITIVE_PREFIXES.some(prefix => key.startsWith(prefix))) { + sessionKeysToRemove.push(key); + } + } + sessionKeysToRemove.forEach(key => sessionStorage.removeItem(key)); + + // 4. 清理 Supabase session (服务端/Edge状态) + try { + await supabase.auth.signOut(); + } catch (error) { + // 静默失败,通常是因为 session 已经过期或网络问题,不影响本地清理 + if (import.meta.env.DEV) console.warn('[Security] Supabase signOut warning:', error); + } + + const totalCleared = localKeysToRemove.length + sessionKeysToRemove.length; + + // 仅在开发环境打印日志 + if (import.meta.env.DEV) { + console.log(`[Security] Cleared ${totalCleared} sensitive keys (Local + Session).`); + } + + return totalCleared; +};