-
-
Notifications
You must be signed in to change notification settings - Fork 11.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
♻️ refactor: user store add an auth slice (#2214)
* ♻️ refactor: refactor the user store with auth slice * ♻️ refactor: separate common and sync slice * 🧑💻 chore: add an isMobile selector * ♻️ refactor: refactor the auth action and common action * 🎨 chore: clean code
- Loading branch information
Showing
23 changed files
with
513 additions
and
369 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,13 @@ | ||
import { UserCommonState, initialCommonState } from './slices/common/initialState'; | ||
import { UserAuthState, initialAuthState } from './slices/auth/initialState'; | ||
import { UserPreferenceState, initialPreferenceState } from './slices/preference/initialState'; | ||
import { UserSettingsState, initialSettingsState } from './slices/settings/initialState'; | ||
import { UserSyncState, initialSyncState } from './slices/sync/initialState'; | ||
|
||
export type UserState = UserCommonState & UserSettingsState & UserPreferenceState; | ||
export type UserState = UserSyncState & UserSettingsState & UserPreferenceState & UserAuthState; | ||
|
||
export const initialState: UserState = { | ||
...initialCommonState, | ||
...initialSyncState, | ||
...initialSettingsState, | ||
...initialPreferenceState, | ||
...initialAuthState, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { act, renderHook, waitFor } from '@testing-library/react'; | ||
import { mutate } from 'swr'; | ||
import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
import { withSWR } from '~test-utils'; | ||
|
||
import { userService } from '@/services/user'; | ||
import { useUserStore } from '@/store/user'; | ||
import { switchLang } from '@/utils/client/switchLang'; | ||
|
||
vi.mock('zustand/traditional'); | ||
|
||
vi.mock('@/utils/client/switchLang', () => ({ | ||
switchLang: vi.fn(), | ||
})); | ||
|
||
vi.mock('swr', async (importOriginal) => { | ||
const modules = await importOriginal(); | ||
return { | ||
...(modules as any), | ||
mutate: vi.fn(), | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
vi.restoreAllMocks(); | ||
}); | ||
|
||
describe('createAuthSlice', () => { | ||
describe('refreshUserConfig', () => { | ||
it('should refresh user config', async () => { | ||
const { result } = renderHook(() => useUserStore()); | ||
|
||
await act(async () => { | ||
await result.current.refreshUserConfig(); | ||
}); | ||
|
||
expect(mutate).toHaveBeenCalledWith(['fetchUserConfig', true]); | ||
}); | ||
}); | ||
|
||
describe('useFetchUserConfig', () => { | ||
it('should not fetch user config if initServer is false', async () => { | ||
const mockUserConfig: any = undefined; // 模拟未初始化服务器的情况 | ||
vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig); | ||
|
||
const { result } = renderHook(() => useUserStore().useFetchUserConfig(false), { | ||
wrapper: withSWR, | ||
}); | ||
|
||
// 因为 initServer 为 false,所以不会触发 getUserConfig 的调用 | ||
expect(userService.getUserConfig).not.toHaveBeenCalled(); | ||
// 确保状态未改变 | ||
expect(result.current.data).toBeUndefined(); | ||
}); | ||
|
||
it('should fetch user config correctly when initServer is true', async () => { | ||
const mockUserConfig: any = { | ||
avatar: 'new-avatar-url', | ||
settings: { | ||
language: 'en', | ||
}, | ||
}; | ||
vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig); | ||
|
||
const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), { | ||
wrapper: withSWR, | ||
}); | ||
|
||
// 等待 SWR 完成数据获取 | ||
await waitFor(() => expect(result.current.data).toEqual(mockUserConfig)); | ||
|
||
// 验证状态是否正确更新 | ||
expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar); | ||
expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings); | ||
|
||
// 验证是否正确处理了语言设置 | ||
expect(switchLang).not.toHaveBeenCalledWith('auto'); | ||
}); | ||
it('should call switch language when language is auto', async () => { | ||
const mockUserConfig: any = { | ||
avatar: 'new-avatar-url', | ||
settings: { | ||
language: 'auto', | ||
}, | ||
}; | ||
vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(mockUserConfig); | ||
|
||
const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), { | ||
wrapper: withSWR, | ||
}); | ||
|
||
// 等待 SWR 完成数据获取 | ||
await waitFor(() => expect(result.current.data).toEqual(mockUserConfig)); | ||
|
||
// 验证状态是否正确更新 | ||
expect(useUserStore.getState().avatar).toBe(mockUserConfig.avatar); | ||
expect(useUserStore.getState().settings).toEqual(mockUserConfig.settings); | ||
|
||
// 验证是否正确处理了语言设置 | ||
expect(switchLang).toHaveBeenCalledWith('auto'); | ||
}); | ||
|
||
it('should handle the case when user config is null', async () => { | ||
vi.spyOn(userService, 'getUserConfig').mockResolvedValueOnce(null as any); | ||
|
||
const { result } = renderHook(() => useUserStore().useFetchUserConfig(true), { | ||
wrapper: withSWR, | ||
}); | ||
|
||
// 等待 SWR 完成数据获取 | ||
await waitFor(() => expect(result.current.data).toBeNull()); | ||
|
||
// 验证状态未被错误更新 | ||
expect(useUserStore.getState().avatar).toBeUndefined(); | ||
expect(useUserStore.getState().settings).toEqual({}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import useSWR, { SWRResponse, mutate } from 'swr'; | ||
import { StateCreator } from 'zustand/vanilla'; | ||
|
||
import { UserConfig, userService } from '@/services/user'; | ||
import { switchLang } from '@/utils/client/switchLang'; | ||
import { setNamespace } from '@/utils/storeDebug'; | ||
|
||
import { UserStore } from '../../store'; | ||
import { settingsSelectors } from '../settings/selectors'; | ||
|
||
const n = setNamespace('auth'); | ||
const USER_CONFIG_FETCH_KEY = 'fetchUserConfig'; | ||
|
||
export interface UserAuthAction { | ||
getUserConfig: () => void; | ||
/** | ||
* universal login method | ||
*/ | ||
login: () => Promise<void>; | ||
/** | ||
* universal logout method | ||
*/ | ||
logout: () => Promise<void>; | ||
refreshUserConfig: () => Promise<void>; | ||
|
||
useFetchUserConfig: (initServer: boolean) => SWRResponse<UserConfig | undefined>; | ||
} | ||
|
||
export const createAuthSlice: StateCreator< | ||
UserStore, | ||
[['zustand/devtools', never]], | ||
[], | ||
UserAuthAction | ||
> = (set, get) => ({ | ||
getUserConfig: () => { | ||
console.log(n('userconfig')); | ||
}, | ||
login: async () => { | ||
// TODO: 针对开启 next-auth 的场景,需要在这里调用登录方法 | ||
console.log(n('login')); | ||
}, | ||
logout: async () => { | ||
// TODO: 针对开启 next-auth 的场景,需要在这里调用登录方法 | ||
console.log(n('logout')); | ||
}, | ||
refreshUserConfig: async () => { | ||
await mutate([USER_CONFIG_FETCH_KEY, true]); | ||
|
||
// when get the user config ,refresh the model provider list to the latest | ||
get().refreshModelProviderList(); | ||
}, | ||
|
||
useFetchUserConfig: (initServer) => | ||
useSWR<UserConfig | undefined>( | ||
[USER_CONFIG_FETCH_KEY, initServer], | ||
async () => { | ||
if (!initServer) return; | ||
return userService.getUserConfig(); | ||
}, | ||
{ | ||
onSuccess: (data) => { | ||
if (!data) return; | ||
|
||
set( | ||
{ avatar: data.avatar, settings: data.settings, userId: data.uuid }, | ||
false, | ||
n('fetchUserConfig', data), | ||
); | ||
|
||
// when get the user config ,refresh the model provider list to the latest | ||
get().refreshDefaultModelProviderList({ trigger: 'fetchUserConfig' }); | ||
|
||
const { language } = settingsSelectors.currentSettings(get()); | ||
if (language === 'auto') { | ||
switchLang('auto'); | ||
} | ||
}, | ||
revalidateOnFocus: false, | ||
}, | ||
), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
export interface LobeUser { | ||
avatar?: string; | ||
firstName?: string | null; | ||
fullName?: string | null; | ||
id: string; | ||
latestName?: string | null; | ||
username?: string | null; | ||
} | ||
|
||
export interface UserAuthState { | ||
/** | ||
* @deprecated | ||
*/ | ||
avatar?: string; | ||
isSignedIn?: boolean; | ||
user?: LobeUser; | ||
userId?: string; | ||
} | ||
|
||
export const initialAuthState: UserAuthState = {}; |
Oops, something went wrong.