diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 1d6fb17..dc94ba7 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -63,6 +63,7 @@ "custom_connection_placeholder": "Eg: https://example.com:8317", "custom_connection_hint": "By default the current URL is used. Override it here if needed.", "use_current_address": "Use Current URL", + "remember_password_label": "Remember password", "management_key_label": "Management Key:", "management_key_placeholder": "Enter the management key", "connect_button": "Connect", @@ -745,7 +746,11 @@ "link_webui_repo": "WebUI Repository", "link_webui_repo_desc": "Management Center frontend source code", "link_docs": "Documentation", - "link_docs_desc": "Usage tutorials and configuration guides" + "link_docs_desc": "Usage tutorials and configuration guides", + "clear_login_title": "Local Login Data", + "clear_login_desc": "Clear locally saved login data and sign out. Usage stats pricing settings will remain untouched.", + "clear_login_button": "Clear login data", + "clear_login_confirm": "Clear local login data and sign out now?" }, "notification": { "debug_updated": "Debug settings updated", @@ -758,6 +763,7 @@ "logging_to_file_updated": "Logging settings updated", "request_log_updated": "Request logging setting updated", "ws_auth_updated": "WebSocket authentication setting updated", + "login_storage_cleared": "Local login data cleared", "api_key_added": "API key added successfully", "api_key_updated": "API key updated successfully", "api_key_deleted": "API key deleted successfully", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index edc8b75..84ad6d8 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -63,6 +63,7 @@ "custom_connection_placeholder": "例如: https://example.com:8317", "custom_connection_hint": "默认使用当前访问地址,若需要可手动输入其他地址。", "use_current_address": "使用当前地址", + "remember_password_label": "记住密码", "management_key_label": "管理密钥:", "management_key_placeholder": "请输入管理密钥", "connect_button": "连接", @@ -745,7 +746,11 @@ "link_webui_repo": "WebUI 仓库", "link_webui_repo_desc": "管理中心前端界面源代码", "link_docs": "使用教程", - "link_docs_desc": "配置指南和使用说明" + "link_docs_desc": "配置指南和使用说明", + "clear_login_title": "本地登录信息", + "clear_login_desc": "清理本地保存的登录信息并退出登录,不会影响使用统计中的价格设置。", + "clear_login_button": "清理登录信息", + "clear_login_confirm": "确认清理本地登录信息并退出登录?" }, "notification": { "debug_updated": "调试设置已更新", @@ -758,6 +763,7 @@ "logging_to_file_updated": "日志记录设置已更新", "request_log_updated": "请求日志设置已更新", "ws_auth_updated": "WebSocket 鉴权设置已更新", + "login_storage_cleared": "本地登录信息已清理", "api_key_added": "API密钥添加成功", "api_key_updated": "API密钥更新成功", "api_key_deleted": "API密钥删除成功", diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 140da2c..63a81d5 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -19,11 +19,13 @@ export function LoginPage() { const restoreSession = useAuthStore((state) => state.restoreSession); const storedBase = useAuthStore((state) => state.apiBase); const storedKey = useAuthStore((state) => state.managementKey); + const storedRememberPassword = useAuthStore((state) => state.rememberPassword); const [apiBase, setApiBase] = useState(''); const [managementKey, setManagementKey] = useState(''); const [showCustomBase, setShowCustomBase] = useState(false); const [showKey, setShowKey] = useState(false); + const [rememberPassword, setRememberPassword] = useState(false); const [loading, setLoading] = useState(false); const [autoLoading, setAutoLoading] = useState(true); const [error, setError] = useState(''); @@ -38,6 +40,7 @@ export function LoginPage() { if (!autoLoggedIn) { setApiBase(storedBase || detectedBase); setManagementKey(storedKey || ''); + setRememberPassword(storedRememberPassword || Boolean(storedKey)); } } finally { setAutoLoading(false); @@ -45,7 +48,7 @@ export function LoginPage() { }; init(); - }, [detectedBase, restoreSession, storedBase, storedKey]); + }, [detectedBase, restoreSession, storedBase, storedKey, storedRememberPassword]); if (isAuthenticated) { const redirect = (location.state as any)?.from?.pathname || '/'; @@ -62,7 +65,11 @@ export function LoginPage() { setLoading(true); setError(''); try { - await login({ apiBase: baseToUse, managementKey: managementKey.trim() }); + await login({ + apiBase: baseToUse, + managementKey: managementKey.trim(), + rememberPassword + }); showNotification(t('common.connected_status'), 'success'); navigate('/', { replace: true }); } catch (err: any) { @@ -148,6 +155,16 @@ export function LoginPage() { } /> +
+ setRememberPassword(e.target.checked)} + /> + +
+ diff --git a/src/pages/SystemPage.module.scss b/src/pages/SystemPage.module.scss index e5b72fd..5ae8435 100644 --- a/src/pages/SystemPage.module.scss +++ b/src/pages/SystemPage.module.scss @@ -34,6 +34,12 @@ margin: 0 0 $spacing-md 0; } +.clearLoginActions { + display: flex; + justify-content: flex-end; + align-items: center; +} + .infoGrid { display: grid; gap: $spacing-sm; diff --git a/src/pages/SystemPage.tsx b/src/pages/SystemPage.tsx index 491bfe9..445b34b 100644 --- a/src/pages/SystemPage.tsx +++ b/src/pages/SystemPage.tsx @@ -6,6 +6,7 @@ import { IconGithub, IconBookOpen, IconExternalLink, IconCode } from '@/componen import { useAuthStore, useConfigStore, useNotificationStore, useModelsStore } from '@/stores'; import { apiKeysApi } from '@/services/api/apiKeys'; import { classifyModels } from '@/utils/models'; +import { STORAGE_KEY_AUTH } from '@/utils/constants'; import styles from './SystemPage.module.scss'; export function SystemPage() { @@ -104,6 +105,15 @@ export function SystemPage() { } }; + const handleClearLoginStorage = () => { + if (!window.confirm(t('system_info.clear_login_confirm'))) return; + auth.logout(); + if (typeof localStorage === 'undefined') return; + const keysToRemove = [STORAGE_KEY_AUTH, 'isLoggedIn', 'apiBase', 'apiUrl', 'managementKey']; + keysToRemove.forEach((key) => localStorage.removeItem(key)); + showNotification(t('notification.login_storage_cleared'), 'success'); + }; + useEffect(() => { fetchConfig().catch(() => { // ignore @@ -248,6 +258,15 @@ export function SystemPage() { )} + + +

{t('system_info.clear_login_desc')}

+
+ +
+
); diff --git a/src/stores/useAuthStore.ts b/src/stores/useAuthStore.ts index 96a35b5..cf7905d 100644 --- a/src/stores/useAuthStore.ts +++ b/src/stores/useAuthStore.ts @@ -34,6 +34,7 @@ export const useAuthStore = create()( isAuthenticated: false, apiBase: '', managementKey: '', + rememberPassword: false, serverVersion: null, serverBuildDate: null, connectionStatus: 'disconnected', @@ -52,16 +53,25 @@ export const useAuthStore = create()( secureStorage.getItem('apiUrl', { encrypt: true }); const legacyKey = secureStorage.getItem('managementKey'); - const { apiBase, managementKey } = get(); + const { apiBase, managementKey, rememberPassword } = get(); const resolvedBase = normalizeApiBase(apiBase || legacyBase || detectApiBaseFromLocation()); const resolvedKey = managementKey || legacyKey || ''; + const resolvedRememberPassword = rememberPassword || Boolean(managementKey) || Boolean(legacyKey); - set({ apiBase: resolvedBase, managementKey: resolvedKey }); + set({ + apiBase: resolvedBase, + managementKey: resolvedKey, + rememberPassword: resolvedRememberPassword + }); apiClient.setConfig({ apiBase: resolvedBase, managementKey: resolvedKey }); if (wasLoggedIn && resolvedBase && resolvedKey) { try { - await get().login({ apiBase: resolvedBase, managementKey: resolvedKey }); + await get().login({ + apiBase: resolvedBase, + managementKey: resolvedKey, + rememberPassword: resolvedRememberPassword + }); return true; } catch (error) { console.warn('Auto login failed:', error); @@ -79,6 +89,7 @@ export const useAuthStore = create()( login: async (credentials) => { const apiBase = normalizeApiBase(credentials.apiBase); const managementKey = credentials.managementKey.trim(); + const rememberPassword = credentials.rememberPassword ?? get().rememberPassword ?? false; try { set({ connectionStatus: 'connecting' }); @@ -97,10 +108,15 @@ export const useAuthStore = create()( isAuthenticated: true, apiBase, managementKey, + rememberPassword, connectionStatus: 'connected', connectionError: null }); - localStorage.setItem('isLoggedIn', 'true'); + if (rememberPassword) { + localStorage.setItem('isLoggedIn', 'true'); + } else { + localStorage.removeItem('isLoggedIn'); + } } catch (error: any) { set({ connectionStatus: 'error', @@ -185,7 +201,8 @@ export const useAuthStore = create()( })), partialize: (state) => ({ apiBase: state.apiBase, - managementKey: state.managementKey, + ...(state.rememberPassword ? { managementKey: state.managementKey } : {}), + rememberPassword: state.rememberPassword, serverVersion: state.serverVersion, serverBuildDate: state.serverBuildDate }) diff --git a/src/types/auth.ts b/src/types/auth.ts index 8ac68bb..374bd9f 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -7,6 +7,7 @@ export interface LoginCredentials { apiBase: string; managementKey: string; + rememberPassword?: boolean; } // 认证状态 @@ -14,6 +15,7 @@ export interface AuthState { isAuthenticated: boolean; apiBase: string; managementKey: string; + rememberPassword: boolean; serverVersion: string | null; serverBuildDate: string | null; }