Skip to content

Commit

Permalink
♻️ refactor: Move next-auth hooks to user store actions (#2364)
Browse files Browse the repository at this point in the history
* ♻️ refactor: move next-auth hooks to store  acitons

* 🔥 refactor: remove redundant files

* ♻️ refactor: add next-auth hooks

* ♻️ refactor: add env `NEXT_PUBLIC_ENABLE_NEXT_AUTH`

* 📝 docs: update docs for new NextAuth env

* ♻️ refactor: remove `login` in auth actions

* 🐛fix: login buttion not shown on settings

* ✅ test: hooks in auth actions

* ♻️ refactor: change button show condition

* ♻️ refactor: use server config to show oauth button

* 🐛fix: context error in settings

* ♻️ refactor: use server config store to enable auth

* 💬 refactor: remove tips

* ♻️ refactor: remove env `NEXT_PUBLIC_ENABLE_NEXT_AUTH`

* 🔥 refactor: remove open profile in store auth actions

* ✅ test(useMenu): change states
  • Loading branch information
cy948 committed May 14, 2024
1 parent 8264d5d commit 6dbcd70
Show file tree
Hide file tree
Showing 17 changed files with 174 additions and 79 deletions.
9 changes: 9 additions & 0 deletions docs/self-hosting/advanced/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ By setting the environment variables NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK

## Next Auth

Before using NextAuth, please set the following variables in LobeChat's environment variables:

| Environment Variable | Type | Description |
| --- | --- | --- |
| `NEXT_AUTH_SECRET` | Required | The key used to encrypt Auth.js session tokens. You can use the following command: `openssl rand -base64 32`, or visit `https://generate-secret.vercel.app/32` to generate the key. |
| `ACCESS_CODE` | Required | Add a password to access this service. You can set a sufficiently long random password to "disable" access code authorization. |
| `NEXTAUTH_URL` | Optional | This URL specifies the callback address for Auth.js when performing OAuth verification. Set this only if the default generated redirect address is incorrect. `https://example.com/api/auth` |
| `NEXT_AUTH_SSO_PROVIDERS` | Optional | This environment variable is used to enable multiple identity verification sources simultaneously, separated by commas, for example, `auth0,azure-ad,authentik`. |

Currently supported identity verification services include:

<Cards>
Expand Down
9 changes: 9 additions & 0 deletions docs/self-hosting/advanced/authentication.zh-CN.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ LobeChat 与 Clerk 做了深度集成,能够为用户提供一个更加安全

## Next Auth

在使用 NextAuth 之前,请先在 LobeChat 的环境变量中设置以下变量:

| 环境变量 | 类型 | 描述 |
| --- | --- | --- |
| `NEXT_AUTH_SECRET` | 必选 | 用于加密 Auth.js 会话令牌的密钥。您可以使用以下命令: `openssl rand -base64 32`,或者访问 `https://generate-secret.vercel.app/32` 生成秘钥。 |
| `ACCESS_CODE` | 必选 | 添加访问此服务的密码,你可以设置一个足够长的随机密码以 “禁用” 访问码授权 |
| `NEXTAUTH_URL` | 可选 | 该 URL 用于指定 Auth.js 在执行 OAuth 验证时的回调地址,当默认生成的重定向地址发生不正确时才需要设置。`https://example.com/api/auth` |
| `NEXT_AUTH_SSO_PROVIDERS` | 可选 | 该环境变量用于同时启用多个身份验证源,以逗号 `,` 分割,例如 `auth0,azure-ad,authentik`|

目前支持的身份验证服务有:

<Cards>
Expand Down
19 changes: 11 additions & 8 deletions src/app/(main)/settings/common/features/Common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { Form, type ItemGroup } from '@lobehub/ui';
import { App, Button, Input } from 'antd';
import isEqual from 'fast-deep-equal';
import { signIn, signOut } from 'next-auth/react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';

Expand All @@ -12,23 +11,22 @@ import { FORM_STYLE } from '@/const/layoutTokens';
import { DEFAULT_SETTINGS } from '@/const/settings';
import { useChatStore } from '@/store/chat';
import { useFileStore } from '@/store/file';
import { useServerConfigStore } from '@/store/serverConfig';
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
import { useSessionStore } from '@/store/session';
import { useToolStore } from '@/store/tool';
import { useUserStore } from '@/store/user';
import { settingsSelectors, userProfileSelectors } from '@/store/user/selectors';

type SettingItemGroup = ItemGroup;

export interface SettingsCommonProps {
showAccessCodeConfig: boolean;
showOAuthLogin?: boolean;
}

const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig, showOAuthLogin }) => {
const Common = memo(() => {
const { t } = useTranslation('setting');
const [form] = Form.useForm();

const isSignedIn = useUserStore((s) => s.isSignedIn);
const showAccessCodeConfig = useServerConfigStore(serverConfigSelectors.enabledAccessCode);
const showOAuthLogin = useServerConfigStore(serverConfigSelectors.enabledOAuthSSO);
const user = useUserStore(userProfileSelectors.userProfile, isEqual);

const [clearSessions, clearSessionGroups] = useSessionStore((s) => [
Expand All @@ -42,7 +40,12 @@ const Common = memo<SettingsCommonProps>(({ showAccessCodeConfig, showOAuthLogin
const [removeAllFiles] = useFileStore((s) => [s.removeAllFiles]);
const removeAllPlugins = useToolStore((s) => s.removeAllPlugins);
const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
const [setSettings, resetSettings] = useUserStore((s) => [s.setSettings, s.resetSettings]);
const [setSettings, resetSettings, signIn, signOut] = useUserStore((s) => [
s.setSettings,
s.resetSettings,
s.openLogin,
s.logout,
]);

const { message, modal } = App.useApp();

Expand Down
10 changes: 1 addition & 9 deletions src/app/(main)/settings/common/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { authEnv } from '@/config/auth';
import { getServerConfig } from '@/config/server';

import Common from './features/Common';
import Theme from './features/Theme';

const Page = () => {
const { SHOW_ACCESS_CODE_CONFIG } = getServerConfig();

return (
<>
<Theme />
<Common
showAccessCodeConfig={SHOW_ACCESS_CODE_CONFIG}
showOAuthLogin={authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH}
/>
<Common />
</>
);
};
Expand Down
10 changes: 6 additions & 4 deletions src/features/Conversation/Error/OAuthForm.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { Icon } from '@lobehub/ui';
import { App, Button } from 'antd';
import { ScanFace } from 'lucide-react';
import { signIn, signOut } from 'next-auth/react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Center, Flexbox } from 'react-layout-kit';

import { useOAuthSession } from '@/hooks/useOAuthSession';
import { useChatStore } from '@/store/chat';
import { useUserStore } from '@/store/user';
import { authSelectors, userProfileSelectors } from '@/store/user/selectors';

import { FormAction } from './style';

const OAuthForm = memo<{ id: string }>(({ id }) => {
const { t } = useTranslation('error');

const { user, isOAuthLoggedIn } = useOAuthSession();
const [signIn, signOut] = useUserStore((s) => [s.openLogin, s.logout]);
const user = useUserStore(userProfileSelectors.userProfile);
const isOAuthLoggedIn = useUserStore(authSelectors.isLoginWithAuth);

const [resend, deleteMessage] = useChatStore((s) => [s.regenerateMessage, s.deleteMessage]);

Expand All @@ -38,7 +40,7 @@ const OAuthForm = memo<{ id: string }>(({ id }) => {
avatar={isOAuthLoggedIn ? '✅' : '🕵️‍♂️'}
description={
isOAuthLoggedIn
? `${t('unlock.oauth.welcome')} ${user?.name}`
? `${t('unlock.oauth.welcome')} ${user?.fullName || ''}`
: t('unlock.oauth.description')
}
title={isOAuthLoggedIn ? t('unlock.oauth.success') : t('unlock.oauth.title')}
Expand Down
11 changes: 8 additions & 3 deletions src/features/User/__tests__/UserAvatar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('UserAvatar', () => {

act(() => {
useUserStore.setState({
enableAuth: () => true,
isSignedIn: true,
user: { avatar: mockAvatar, id: 'abc', username: mockUsername },
});
Expand All @@ -45,7 +46,11 @@ describe('UserAvatar', () => {
const mockUsername = 'testuser';

act(() => {
useUserStore.setState({ isSignedIn: true, user: { id: 'bbb', username: mockUsername } });
useUserStore.setState({
enableAuth: () => true,
isSignedIn: true,
user: { id: 'bbb', username: mockUsername },
});
});

render(<UserAvatar />);
Expand All @@ -54,7 +59,7 @@ describe('UserAvatar', () => {

it('should show LobeChat and default avatar when the user is not logged in and enable auth', () => {
act(() => {
useUserStore.setState({ isSignedIn: false, user: undefined });
useUserStore.setState({ enableAuth: () => true, isSignedIn: false, user: undefined });
});

render(<UserAvatar />);
Expand All @@ -67,7 +72,7 @@ describe('UserAvatar', () => {
it('should show LobeChat and default avatar when the user is not logged in and disabled auth', () => {
enableAuth = false;
act(() => {
useUserStore.setState({ isSignedIn: false, user: undefined });
useUserStore.setState({ enableAuth: () => false, isSignedIn: false, user: undefined });
});

render(<UserAvatar />);
Expand Down
6 changes: 3 additions & 3 deletions src/features/User/__tests__/useMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ afterEach(() => {
describe('useMenu', () => {
it('should provide correct menu items when user is logged in with auth', () => {
act(() => {
useUserStore.setState({ isSignedIn: true });
useUserStore.setState({ isSignedIn: true, enableAuth: () => true });
});
enableAuth = true;
enableClerk = false;
Expand Down Expand Up @@ -104,7 +104,7 @@ describe('useMenu', () => {

it('should provide correct menu items when user is logged in without auth', () => {
act(() => {
useUserStore.setState({ isSignedIn: false });
useUserStore.setState({ isSignedIn: false, enableAuth: () => false });
});
enableAuth = false;

Expand All @@ -123,7 +123,7 @@ describe('useMenu', () => {

it('should provide correct menu items when user is not logged in', () => {
act(() => {
useUserStore.setState({ isSignedIn: false });
useUserStore.setState({ isSignedIn: false, enableAuth: () => true });
});
enableAuth = true;

Expand Down
24 changes: 0 additions & 24 deletions src/hooks/useOAuthSession.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/libs/next-auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,3 @@ export const {
handlers: { GET, POST },
auth,
} = nextAuth;

declare module '@auth/core/jwt' {
// Returned by the `jwt` callback and `auth`, when using JWT sessions
interface JWT {
userId?: string;
}
}
2 changes: 2 additions & 0 deletions src/server/globalConfig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { parseAgentConfig } from './parseDefaultAgent';

export const getServerGlobalConfig = () => {
const {
ACCESS_CODES,
ENABLE_LANGFUSE,

DEFAULT_AGENT_CONFIG,
Expand Down Expand Up @@ -49,6 +50,7 @@ export const getServerGlobalConfig = () => {
config: parseAgentConfig(DEFAULT_AGENT_CONFIG),
},

enabledAccessCode: ACCESS_CODES?.length > 0,
enabledOAuthSSO: enableNextAuth,
languageModel: {
anthropic: {
Expand Down
1 change: 1 addition & 0 deletions src/store/serverConfig/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const featureFlagsSelectors = (s: ServerConfigStore) =>
mapFeatureFlagsEnvToState(s.featureFlags);

export const serverConfigSelectors = {
enabledAccessCode: (s: ServerConfigStore) => !!s.serverConfig?.enabledAccessCode,
enabledOAuthSSO: (s: ServerConfigStore) => s.serverConfig.enabledOAuthSSO,
enabledTelemetryChat: (s: ServerConfigStore) => s.serverConfig.telemetry.langfuse || false,
isMobile: (s: ServerConfigStore) => s.isMobile || false,
Expand Down
61 changes: 61 additions & 0 deletions src/store/user/slices/auth/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ afterEach(() => {
enableClerk = false;
});

/**
* Mock nextauth 库相关方法
*/
vi.mock('next-auth/react', async () => {
return {
signIn: vi.fn(),
signOut: vi.fn(),
};
});

describe('createAuthSlice', () => {
describe('refreshUserConfig', () => {
it('should refresh user config', async () => {
Expand Down Expand Up @@ -162,6 +172,32 @@ describe('createAuthSlice', () => {

expect(clerkSignOutMock).not.toHaveBeenCalled();
});

it('should call next-auth signOut when NextAuth is enabled', async () => {
useUserStore.setState({ enabledNextAuth: () => true });

const { result } = renderHook(() => useUserStore());

await act(async () => {
await result.current.logout();
});

const { signOut } = await import('next-auth/react');

expect(signOut).toHaveBeenCalled();
});

it('should not call next-auth signOut when NextAuth is disabled', async () => {
const { result } = renderHook(() => useUserStore());

await act(async () => {
await result.current.logout();
});

const { signOut } = await import('next-auth/react');

expect(signOut).not.toHaveBeenCalled();
});
});

describe('openLogin', () => {
Expand Down Expand Up @@ -190,6 +226,31 @@ describe('createAuthSlice', () => {

expect(clerkSignInMock).not.toHaveBeenCalled();
});

it('should call next-auth signIn when NextAuth is enabled', async () => {
useUserStore.setState({ enabledNextAuth: () => true });

const { result } = renderHook(() => useUserStore());

await act(async () => {
await result.current.openLogin();
});

const { signIn } = await import('next-auth/react');

expect(signIn).toHaveBeenCalled();
});
it('should not call next-auth signIn when NextAuth is disabled', async () => {
const { result } = renderHook(() => useUserStore());

await act(async () => {
await result.current.openLogin();
});

const { signIn } = await import('next-auth/react');

expect(signIn).not.toHaveBeenCalled();
});
});

describe('openUserProfile', () => {
Expand Down

0 comments on commit 6dbcd70

Please sign in to comment.