@lobehub/lobehub 2.0.0-next.355 → 2.0.0-next.357
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.desktop +0 -1
- package/.env.example +16 -20
- package/.env.example.development +1 -4
- package/.github/workflows/e2e.yml +10 -11
- package/CHANGELOG.md +60 -0
- package/Dockerfile +28 -4
- package/changelog/v1.json +18 -0
- package/docker-compose/local/docker-compose.yml +2 -2
- package/docker-compose/local/grafana/docker-compose.yml +2 -2
- package/docker-compose/local/logto/docker-compose.yml +2 -2
- package/docker-compose/local/zitadel/.env.example +2 -2
- package/docker-compose/local/zitadel/.env.zh-CN.example +2 -2
- package/docker-compose/production/grafana/docker-compose.yml +2 -2
- package/docker-compose/production/logto/.env.example +2 -2
- package/docker-compose/production/logto/.env.zh-CN.example +2 -2
- package/docker-compose/production/zitadel/.env.example +2 -2
- package/docker-compose/production/zitadel/.env.zh-CN.example +2 -2
- package/docs/development/basic/add-new-authentication-providers.mdx +144 -136
- package/docs/development/basic/add-new-authentication-providers.zh-CN.mdx +146 -136
- package/docs/self-hosting/advanced/auth/legacy.mdx +4 -0
- package/docs/self-hosting/advanced/auth/legacy.zh-CN.mdx +4 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.mdx +326 -0
- package/docs/self-hosting/advanced/auth/nextauth-to-betterauth.zh-CN.mdx +323 -0
- package/docs/self-hosting/advanced/auth.mdx +43 -16
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +44 -16
- package/docs/self-hosting/advanced/redis/upstash.mdx +69 -0
- package/docs/self-hosting/advanced/redis/upstash.zh-CN.mdx +69 -0
- package/docs/self-hosting/advanced/redis.mdx +128 -0
- package/docs/self-hosting/advanced/redis.zh-CN.mdx +126 -0
- package/docs/self-hosting/environment-variables/auth.mdx +15 -1
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +15 -1
- package/docs/self-hosting/environment-variables/basic.mdx +13 -0
- package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +13 -0
- package/docs/self-hosting/environment-variables/redis.mdx +68 -0
- package/docs/self-hosting/environment-variables/redis.zh-CN.mdx +67 -0
- package/docs/self-hosting/migration/v2/breaking-changes.mdx +23 -23
- package/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx +23 -23
- package/docs/self-hosting/server-database/docker-compose.mdx +4 -4
- package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +4 -4
- package/e2e/CLAUDE.md +5 -6
- package/e2e/docs/local-setup.md +9 -12
- package/e2e/scripts/setup.ts +9 -15
- package/e2e/src/support/webServer.ts +6 -5
- package/package.json +4 -6
- package/packages/database/src/schemas/nextauth.ts +7 -2
- package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +370 -0
- package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +18 -5
- package/packages/utils/src/server/__tests__/auth.test.ts +1 -63
- package/packages/utils/src/server/auth.ts +8 -24
- package/scripts/_shared/checkDeprecatedAuth.js +99 -0
- package/scripts/clerk-to-betterauth/index.ts +8 -3
- package/scripts/nextauth-to-betterauth/_internal/config.ts +41 -0
- package/scripts/nextauth-to-betterauth/_internal/db.ts +32 -0
- package/scripts/nextauth-to-betterauth/_internal/env.ts +6 -0
- package/scripts/nextauth-to-betterauth/index.ts +226 -0
- package/scripts/nextauth-to-betterauth/verify.ts +188 -0
- package/scripts/prebuild.mts +66 -13
- package/scripts/serverLauncher/startServer.js +5 -5
- package/src/app/(backend)/api/auth/[...all]/route.ts +5 -23
- package/src/app/(backend)/api/webhooks/casdoor/route.ts +5 -5
- package/src/app/(backend)/api/webhooks/logto/route.ts +8 -8
- package/src/app/(backend)/middleware/auth/index.test.ts +8 -1
- package/src/app/(backend)/middleware/auth/index.ts +6 -15
- package/src/app/(backend)/middleware/auth/utils.test.ts +0 -32
- package/src/app/(backend)/middleware/auth/utils.ts +3 -8
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +8 -1
- package/src/app/(backend)/webapi/create-image/comfyui/route.ts +0 -1
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +8 -1
- package/src/app/[variants]/(auth)/signin/SignInEmailStep.tsx +1 -1
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +4 -17
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
- package/src/app/[variants]/(main)/settings/profile/features/SSOProvidersList/index.tsx +12 -19
- package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -14
- package/src/components/{NextAuth/AuthIcons.tsx → AuthIcons.tsx} +8 -10
- package/src/envs/auth.ts +12 -51
- package/src/envs/email.ts +3 -0
- package/src/envs/redis.ts +12 -54
- package/src/features/ChatInput/ChatInputProvider.tsx +22 -2
- package/src/features/ChatInput/InputEditor/index.tsx +14 -3
- package/src/features/ChatInput/store/initialState.ts +2 -0
- package/src/features/EditorCanvas/DiffAllToolbar.tsx +4 -5
- package/src/features/EditorCanvas/DocumentIdMode.tsx +21 -1
- package/src/features/User/__tests__/PanelContent.test.tsx +0 -11
- package/src/features/User/__tests__/UserAvatar.test.tsx +1 -16
- package/src/layout/AuthProvider/index.tsx +1 -6
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -4
- package/src/libs/better-auth/define-config.ts +2 -0
- package/src/libs/better-auth/plugins/email-whitelist.test.ts +120 -0
- package/src/libs/better-auth/plugins/email-whitelist.ts +62 -0
- package/src/libs/next/config/define-config.ts +13 -1
- package/src/libs/next/proxy/define-config.ts +2 -75
- package/src/libs/oidc-provider/provider.test.ts +0 -4
- package/src/libs/redis/index.ts +0 -1
- package/src/libs/redis/manager.test.ts +9 -45
- package/src/libs/redis/manager.ts +2 -16
- package/src/libs/redis/redis.test.ts +2 -4
- package/src/libs/redis/redis.ts +2 -4
- package/src/libs/redis/types.ts +2 -24
- package/src/libs/redis/utils.test.ts +0 -10
- package/src/libs/redis/utils.ts +0 -19
- package/src/libs/trpc/lambda/context.test.ts +0 -13
- package/src/libs/trpc/lambda/context.ts +21 -59
- package/src/libs/trpc/middleware/userAuth.ts +1 -7
- package/src/libs/trusted-client/getSessionUser.ts +15 -35
- package/src/server/globalConfig/index.ts +1 -3
- package/src/server/routers/lambda/__tests__/user.test.ts +0 -48
- package/src/server/routers/lambda/user.ts +1 -12
- package/src/server/services/email/impls/nodemailer/index.ts +2 -2
- package/src/server/services/webhookUser/index.ts +88 -0
- package/src/services/user/index.test.ts +0 -14
- package/src/services/user/index.ts +0 -4
- package/src/store/document/slices/document/action.ts +1 -0
- package/src/store/user/slices/auth/action.test.ts +22 -126
- package/src/store/user/slices/auth/action.ts +32 -65
- package/src/store/user/slices/auth/initialState.ts +0 -3
- package/src/store/user/slices/auth/selectors.ts +0 -3
- package/tests/setup.ts +10 -0
- package/scripts/_shared/checkDeprecatedClerkEnv.js +0 -42
- package/src/app/(backend)/api/auth/adapter/route.ts +0 -137
- package/src/app/[variants]/(auth)/next-auth/error/AuthErrorPage.tsx +0 -40
- package/src/app/[variants]/(auth)/next-auth/error/page.tsx +0 -11
- package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +0 -167
- package/src/app/[variants]/(auth)/next-auth/signin/page.tsx +0 -11
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/signin/layout.tsx +0 -12
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +0 -12
- package/src/envs/auth.test.ts +0 -47
- package/src/layout/AuthProvider/NextAuth/UserUpdater.tsx +0 -44
- package/src/layout/AuthProvider/NextAuth/index.tsx +0 -17
- package/src/libs/next-auth/adapter/index.ts +0 -177
- package/src/libs/next-auth/auth.config.ts +0 -64
- package/src/libs/next-auth/index.ts +0 -20
- package/src/libs/next-auth/sso-providers/auth0.ts +0 -24
- package/src/libs/next-auth/sso-providers/authelia.ts +0 -39
- package/src/libs/next-auth/sso-providers/authentik.ts +0 -25
- package/src/libs/next-auth/sso-providers/casdoor.ts +0 -50
- package/src/libs/next-auth/sso-providers/cloudflare-zero-trust.ts +0 -34
- package/src/libs/next-auth/sso-providers/cognito.ts +0 -8
- package/src/libs/next-auth/sso-providers/feishu.ts +0 -83
- package/src/libs/next-auth/sso-providers/generic-oidc.ts +0 -38
- package/src/libs/next-auth/sso-providers/github.ts +0 -23
- package/src/libs/next-auth/sso-providers/google.ts +0 -18
- package/src/libs/next-auth/sso-providers/index.ts +0 -35
- package/src/libs/next-auth/sso-providers/keycloak.ts +0 -22
- package/src/libs/next-auth/sso-providers/logto.ts +0 -48
- package/src/libs/next-auth/sso-providers/microsoft-entra-id-helper.ts +0 -29
- package/src/libs/next-auth/sso-providers/microsoft-entra-id.ts +0 -19
- package/src/libs/next-auth/sso-providers/okta.ts +0 -22
- package/src/libs/next-auth/sso-providers/sso.config.ts +0 -8
- package/src/libs/next-auth/sso-providers/wechat.ts +0 -36
- package/src/libs/next-auth/sso-providers/zitadel.ts +0 -21
- package/src/libs/redis/upstash.test.ts +0 -158
- package/src/libs/redis/upstash.ts +0 -136
- package/src/server/services/nextAuthUser/index.ts +0 -318
- package/src/server/services/nextAuthUser/utils.ts +0 -62
- package/src/types/next-auth.d.ts +0 -26
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { TRPCError } from '@trpc/server';
|
|
2
2
|
|
|
3
|
-
import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
|
|
4
|
-
|
|
5
3
|
import { trpc } from '../lambda/init';
|
|
6
4
|
|
|
7
5
|
export const userAuth = trpc.middleware(async (opts) => {
|
|
@@ -9,11 +7,7 @@ export const userAuth = trpc.middleware(async (opts) => {
|
|
|
9
7
|
|
|
10
8
|
// `ctx.user` is nullable
|
|
11
9
|
if (!ctx.userId) {
|
|
12
|
-
|
|
13
|
-
console.log('better auth: no session found in context');
|
|
14
|
-
} else if (enableNextAuth) {
|
|
15
|
-
console.log('next auth:', ctx.nextAuth);
|
|
16
|
-
}
|
|
10
|
+
console.log('better auth: no session found in context');
|
|
17
11
|
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
18
12
|
}
|
|
19
13
|
|
|
@@ -1,50 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { headers } from 'next/headers';
|
|
2
2
|
|
|
3
3
|
import type { TrustedClientUserInfo } from './index';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Get user info from the current session for trusted client authentication
|
|
7
|
-
* This works with different authentication providers (BetterAuth, NextAuth)
|
|
8
7
|
*
|
|
9
8
|
* @returns User info or undefined if not authenticated
|
|
10
9
|
*/
|
|
11
10
|
export const getSessionUser = async (): Promise<TrustedClientUserInfo | undefined> => {
|
|
12
11
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
email: session.user.email,
|
|
27
|
-
name: session.user.name || undefined,
|
|
28
|
-
userId: session.user.id,
|
|
29
|
-
};
|
|
12
|
+
// Dynamic import to avoid validator ESM/CJS issue during sitemap generation
|
|
13
|
+
const { auth } = await import('@/auth');
|
|
14
|
+
const headersList = await headers();
|
|
15
|
+
const session = await auth.api.getSession({
|
|
16
|
+
headers: headersList,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!session?.user?.id || !session?.user?.email) {
|
|
20
|
+
return undefined;
|
|
30
21
|
}
|
|
31
22
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
email: session.user.email,
|
|
42
|
-
name: session.user.name || undefined,
|
|
43
|
-
userId: session.user.id,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
email: session.user.email,
|
|
25
|
+
name: session.user.name || undefined,
|
|
26
|
+
userId: session.user.id,
|
|
27
|
+
};
|
|
48
28
|
} catch {
|
|
49
29
|
return undefined;
|
|
50
30
|
}
|
|
@@ -90,9 +90,7 @@ export const getServerGlobalConfig = async () => {
|
|
|
90
90
|
memory: {
|
|
91
91
|
userMemory: cleanObject(getPublicMemoryExtractionConfig()),
|
|
92
92
|
},
|
|
93
|
-
oAuthSSOProviders:
|
|
94
|
-
? getBetterAuthSSOProviders()
|
|
95
|
-
: authEnv.NEXT_AUTH_SSO_PROVIDERS.trim().split(/[,,]/),
|
|
93
|
+
oAuthSSOProviders: getBetterAuthSSOProviders(),
|
|
96
94
|
systemAgent: parseSystemAgent(appEnv.SYSTEM_AGENT),
|
|
97
95
|
telemetry: {
|
|
98
96
|
langfuse: langfuseEnv.ENABLE_LANGFUSE,
|
|
@@ -6,7 +6,6 @@ import { SessionModel } from '@/database/models/session';
|
|
|
6
6
|
import { UserModel, UserNotFoundError } from '@/database/models/user';
|
|
7
7
|
import { serverDB } from '@/database/server';
|
|
8
8
|
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
|
9
|
-
import { NextAuthUserService } from '@/server/services/nextAuthUser';
|
|
10
9
|
import { UserService } from '@/server/services/user';
|
|
11
10
|
|
|
12
11
|
import { userRouter } from '../user';
|
|
@@ -22,11 +21,6 @@ vi.mock('@/database/models/user');
|
|
|
22
21
|
vi.mock('@/server/modules/KeyVaultsEncrypt');
|
|
23
22
|
vi.mock('@/server/modules/S3');
|
|
24
23
|
vi.mock('@/server/services/user');
|
|
25
|
-
vi.mock('@/server/services/nextAuthUser');
|
|
26
|
-
vi.mock('@/envs/auth', () => ({
|
|
27
|
-
enableBetterAuth: false,
|
|
28
|
-
enableNextAuth: false,
|
|
29
|
-
}));
|
|
30
24
|
|
|
31
25
|
describe('userRouter', () => {
|
|
32
26
|
const mockUserId = 'test-user-id';
|
|
@@ -122,7 +116,6 @@ describe('userRouter', () => {
|
|
|
122
116
|
userId: mockUserId,
|
|
123
117
|
});
|
|
124
118
|
});
|
|
125
|
-
|
|
126
119
|
});
|
|
127
120
|
|
|
128
121
|
describe('makeUserOnboarded', () => {
|
|
@@ -140,47 +133,6 @@ describe('userRouter', () => {
|
|
|
140
133
|
});
|
|
141
134
|
});
|
|
142
135
|
|
|
143
|
-
describe('unlinkSSOProvider', () => {
|
|
144
|
-
it('should unlink SSO provider successfully', async () => {
|
|
145
|
-
const mockInput = {
|
|
146
|
-
provider: 'google',
|
|
147
|
-
providerAccountId: '123',
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const mockAccount = {
|
|
151
|
-
userId: mockUserId,
|
|
152
|
-
provider: 'google',
|
|
153
|
-
providerAccountId: '123',
|
|
154
|
-
type: 'oauth',
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
vi.mocked(NextAuthUserService).mockReturnValue({
|
|
158
|
-
getAccount: vi.fn().mockResolvedValue(mockAccount),
|
|
159
|
-
unlinkAccount: vi.fn().mockResolvedValue(undefined),
|
|
160
|
-
} as any);
|
|
161
|
-
|
|
162
|
-
await expect(
|
|
163
|
-
userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
|
|
164
|
-
).resolves.not.toThrow();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('should throw error if account does not exist', async () => {
|
|
168
|
-
const mockInput = {
|
|
169
|
-
provider: 'google',
|
|
170
|
-
providerAccountId: '123',
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
vi.mocked(NextAuthUserService).mockReturnValue({
|
|
174
|
-
getAccount: vi.fn().mockResolvedValue(null),
|
|
175
|
-
unlinkAccount: vi.fn(),
|
|
176
|
-
} as any);
|
|
177
|
-
|
|
178
|
-
await expect(
|
|
179
|
-
userRouter.createCaller({ ...mockCtx }).unlinkSSOProvider(mockInput),
|
|
180
|
-
).rejects.toThrow('The account does not exist');
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
136
|
describe('updateSettings', () => {
|
|
185
137
|
it('should update settings with encrypted key vaults', async () => {
|
|
186
138
|
const mockSettings = {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isDesktop } from '@lobechat/const';
|
|
2
2
|
import {
|
|
3
|
-
NextAuthAccountSchame,
|
|
4
3
|
Plans,
|
|
5
4
|
UserGuideSchema,
|
|
6
5
|
type UserInitializationState,
|
|
@@ -16,8 +15,8 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
16
15
|
import { z } from 'zod';
|
|
17
16
|
|
|
18
17
|
import {
|
|
19
|
-
getIsInviteCodeRequired,
|
|
20
18
|
getIsInWaitList,
|
|
19
|
+
getIsInviteCodeRequired,
|
|
21
20
|
getReferralStatus,
|
|
22
21
|
getSubscriptionPlan,
|
|
23
22
|
} from '@/business/server/user';
|
|
@@ -29,7 +28,6 @@ import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
|
|
29
28
|
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
|
30
29
|
import { FileS3 } from '@/server/modules/S3';
|
|
31
30
|
import { FileService } from '@/server/services/file';
|
|
32
|
-
import { NextAuthUserService } from '@/server/services/nextAuthUser';
|
|
33
31
|
|
|
34
32
|
const usernameSchema = z
|
|
35
33
|
.string()
|
|
@@ -42,7 +40,6 @@ const userProcedure = authedProcedure.use(serverDatabase).use(async ({ ctx, next
|
|
|
42
40
|
ctx: {
|
|
43
41
|
fileService: new FileService(ctx.serverDB, ctx.userId),
|
|
44
42
|
messageModel: new MessageModel(ctx.serverDB, ctx.userId),
|
|
45
|
-
nextAuthUserService: new NextAuthUserService(ctx.serverDB),
|
|
46
43
|
sessionModel: new SessionModel(ctx.serverDB, ctx.userId),
|
|
47
44
|
userModel: new UserModel(ctx.serverDB, ctx.userId),
|
|
48
45
|
},
|
|
@@ -138,14 +135,6 @@ export const userRouter = router({
|
|
|
138
135
|
return ctx.userModel.deleteSetting();
|
|
139
136
|
}),
|
|
140
137
|
|
|
141
|
-
unlinkSSOProvider: userProcedure.input(NextAuthAccountSchame).mutation(async ({ ctx, input }) => {
|
|
142
|
-
const { provider, providerAccountId } = input;
|
|
143
|
-
const account = await ctx.nextAuthUserService.getAccount(providerAccountId, provider);
|
|
144
|
-
// The userId can either get from ctx.nextAuth?.id or ctx.userId
|
|
145
|
-
if (!account || account.userId !== ctx.userId) throw new Error('The account does not exist');
|
|
146
|
-
await ctx.nextAuthUserService.unlinkAccount({ provider, providerAccountId });
|
|
147
|
-
}),
|
|
148
|
-
|
|
149
138
|
updateAvatar: userProcedure.input(z.string()).mutation(async ({ ctx, input }) => {
|
|
150
139
|
// If it's Base64 data, need to upload to S3
|
|
151
140
|
if (input.startsWith('data:image')) {
|
|
@@ -49,8 +49,8 @@ export class NodemailerImpl implements EmailServiceImpl {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
async sendMail(payload: EmailPayload): Promise<EmailResponse> {
|
|
52
|
-
// Use
|
|
53
|
-
const from = payload.from ?? emailEnv.SMTP_USER!;
|
|
52
|
+
// Use SMTP_FROM as default sender, fallback to SMTP_USER for backward compatibility
|
|
53
|
+
const from = payload.from ?? emailEnv.SMTP_FROM ?? emailEnv.SMTP_USER!;
|
|
54
54
|
|
|
55
55
|
log('Sending email with payload: %o', {
|
|
56
56
|
from,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type LobeChatDatabase } from '@lobechat/database';
|
|
2
|
+
import { and, eq } from 'drizzle-orm';
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
|
|
5
|
+
import { UserModel } from '@/database/models/user';
|
|
6
|
+
import { type UserItem, account, session } from '@/database/schemas';
|
|
7
|
+
import { pino } from '@/libs/logger';
|
|
8
|
+
|
|
9
|
+
export class WebhookUserService {
|
|
10
|
+
private db: LobeChatDatabase;
|
|
11
|
+
|
|
12
|
+
constructor(db: LobeChatDatabase) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Find user by provider account info
|
|
18
|
+
*/
|
|
19
|
+
private getUserByAccount = async ({
|
|
20
|
+
providerId,
|
|
21
|
+
accountId,
|
|
22
|
+
}: {
|
|
23
|
+
accountId: string;
|
|
24
|
+
providerId: string;
|
|
25
|
+
}) => {
|
|
26
|
+
const result = await this.db.query.account.findFirst({
|
|
27
|
+
where: and(eq(account.providerId, providerId), eq(account.accountId, accountId)),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!result) return null;
|
|
31
|
+
|
|
32
|
+
return this.db.query.users.findFirst({
|
|
33
|
+
where: eq(account.userId, result.userId),
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Safely update user data from webhook
|
|
39
|
+
*/
|
|
40
|
+
safeUpdateUser = async (
|
|
41
|
+
{ accountId, providerId }: { accountId: string; providerId: string },
|
|
42
|
+
data: Partial<UserItem>,
|
|
43
|
+
) => {
|
|
44
|
+
pino.info(`updating user "${JSON.stringify({ accountId, providerId })}" due to webhook`);
|
|
45
|
+
|
|
46
|
+
const user = await this.getUserByAccount({ accountId, providerId });
|
|
47
|
+
|
|
48
|
+
if (user?.id) {
|
|
49
|
+
const userModel = new UserModel(this.db, user.id);
|
|
50
|
+
await userModel.updateUser({
|
|
51
|
+
avatar: data?.avatar,
|
|
52
|
+
email: data?.email,
|
|
53
|
+
fullName: data?.fullName,
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
pino.warn(
|
|
57
|
+
`[${providerId}]: Webhook user "${JSON.stringify({ accountId, providerId })}" update for "${JSON.stringify(data)}", but no user was found.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return NextResponse.json({ message: 'user updated', success: true }, { status: 200 });
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Safely sign out user (delete all sessions)
|
|
66
|
+
*/
|
|
67
|
+
safeSignOutUser = async ({
|
|
68
|
+
accountId,
|
|
69
|
+
providerId,
|
|
70
|
+
}: {
|
|
71
|
+
accountId: string;
|
|
72
|
+
providerId: string;
|
|
73
|
+
}) => {
|
|
74
|
+
pino.info(`Signing out user "${JSON.stringify({ accountId, providerId })}"`);
|
|
75
|
+
|
|
76
|
+
const user = await this.getUserByAccount({ accountId, providerId });
|
|
77
|
+
|
|
78
|
+
if (user?.id) {
|
|
79
|
+
await this.db.delete(session).where(eq(session.userId, user.id));
|
|
80
|
+
} else {
|
|
81
|
+
pino.warn(
|
|
82
|
+
`[${providerId}]: Webhook user "${JSON.stringify({ accountId, providerId })}" signout, but no user was found.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return NextResponse.json({ message: 'user signed out', success: true }, { status: 200 });
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -8,7 +8,6 @@ const mockLambdaClient = vi.hoisted(() => ({
|
|
|
8
8
|
getUserRegistrationDuration: { query: vi.fn() },
|
|
9
9
|
getUserState: { query: vi.fn() },
|
|
10
10
|
getUserSSOProviders: { query: vi.fn() },
|
|
11
|
-
unlinkSSOProvider: { mutate: vi.fn() },
|
|
12
11
|
makeUserOnboarded: { mutate: vi.fn() },
|
|
13
12
|
updateAvatar: { mutate: vi.fn() },
|
|
14
13
|
updateFullName: { mutate: vi.fn() },
|
|
@@ -64,19 +63,6 @@ describe('UserService', () => {
|
|
|
64
63
|
});
|
|
65
64
|
});
|
|
66
65
|
|
|
67
|
-
describe('unlinkSSOProvider', () => {
|
|
68
|
-
it('should call lambdaClient.user.unlinkSSOProvider.mutate with correct params', async () => {
|
|
69
|
-
mockLambdaClient.user.unlinkSSOProvider.mutate.mockResolvedValueOnce({ success: true });
|
|
70
|
-
|
|
71
|
-
await userService.unlinkSSOProvider('github', 'account-123');
|
|
72
|
-
|
|
73
|
-
expect(mockLambdaClient.user.unlinkSSOProvider.mutate).toHaveBeenCalledWith({
|
|
74
|
-
provider: 'github',
|
|
75
|
-
providerAccountId: 'account-123',
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
66
|
describe('makeUserOnboarded', () => {
|
|
81
67
|
it('should call lambdaClient.user.makeUserOnboarded.mutate', async () => {
|
|
82
68
|
mockLambdaClient.user.makeUserOnboarded.mutate.mockResolvedValueOnce({ success: true });
|
|
@@ -27,10 +27,6 @@ export class UserService {
|
|
|
27
27
|
return lambdaClient.user.getUserSSOProviders.query();
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
unlinkSSOProvider = async (provider: string, providerAccountId: string) => {
|
|
31
|
-
return lambdaClient.user.unlinkSSOProvider.mutate({ provider, providerAccountId });
|
|
32
|
-
};
|
|
33
|
-
|
|
34
30
|
makeUserOnboarded = async () => {
|
|
35
31
|
return lambdaClient.user.makeUserOnboarded.mutate();
|
|
36
32
|
};
|
|
@@ -194,6 +194,7 @@ export const createDocumentSlice: StateCreator<
|
|
|
194
194
|
// Check if this response is still for the current active document
|
|
195
195
|
// This prevents race conditions when quickly switching between documents
|
|
196
196
|
const currentActiveId = get().activeDocumentId;
|
|
197
|
+
|
|
197
198
|
if (currentActiveId && currentActiveId !== documentId) {
|
|
198
199
|
// User has already switched to another document, discard this stale response
|
|
199
200
|
return;
|
|
@@ -15,29 +15,6 @@ vi.mock('@/libs/swr', async () => {
|
|
|
15
15
|
};
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
// Use vi.hoisted to ensure variables exist before vi.mock factory executes
|
|
19
|
-
const { enableNextAuth, enableBetterAuth } = vi.hoisted(() => ({
|
|
20
|
-
enableNextAuth: { value: false },
|
|
21
|
-
enableBetterAuth: { value: false },
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
vi.mock('@/envs/auth', () => ({
|
|
25
|
-
get enableNextAuth() {
|
|
26
|
-
return enableNextAuth.value;
|
|
27
|
-
},
|
|
28
|
-
get enableBetterAuth() {
|
|
29
|
-
return enableBetterAuth.value;
|
|
30
|
-
},
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
const mockUserService = vi.hoisted(() => ({
|
|
34
|
-
getUserSSOProviders: vi.fn().mockResolvedValue([]),
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
vi.mock('@/services/user', () => ({
|
|
38
|
-
userService: mockUserService,
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
18
|
const mockBetterAuthClient = vi.hoisted(() => ({
|
|
42
19
|
listAccounts: vi.fn().mockResolvedValue({ data: [] }),
|
|
43
20
|
accountInfo: vi.fn().mockResolvedValue({ data: { user: {} } }),
|
|
@@ -50,9 +27,6 @@ afterEach(() => {
|
|
|
50
27
|
vi.restoreAllMocks();
|
|
51
28
|
vi.clearAllMocks();
|
|
52
29
|
|
|
53
|
-
enableNextAuth.value = false;
|
|
54
|
-
enableBetterAuth.value = false;
|
|
55
|
-
|
|
56
30
|
// Reset store state
|
|
57
31
|
useUserStore.setState({
|
|
58
32
|
isLoadedAuthProviders: false,
|
|
@@ -61,16 +35,6 @@ afterEach(() => {
|
|
|
61
35
|
});
|
|
62
36
|
});
|
|
63
37
|
|
|
64
|
-
/**
|
|
65
|
-
* Mock nextauth 库相关方法
|
|
66
|
-
*/
|
|
67
|
-
vi.mock('next-auth/react', async () => {
|
|
68
|
-
return {
|
|
69
|
-
signIn: vi.fn(),
|
|
70
|
-
signOut: vi.fn(),
|
|
71
|
-
};
|
|
72
|
-
});
|
|
73
|
-
|
|
74
38
|
describe('createAuthSlice', () => {
|
|
75
39
|
describe('refreshUserState', () => {
|
|
76
40
|
it('should refresh user config', async () => {
|
|
@@ -85,64 +49,19 @@ describe('createAuthSlice', () => {
|
|
|
85
49
|
});
|
|
86
50
|
|
|
87
51
|
describe('logout', () => {
|
|
88
|
-
it('should call
|
|
89
|
-
enableNextAuth.value = true;
|
|
90
|
-
|
|
91
|
-
const { result } = renderHook(() => useUserStore());
|
|
92
|
-
|
|
93
|
-
await act(async () => {
|
|
94
|
-
await result.current.logout();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const { signOut } = await import('next-auth/react');
|
|
98
|
-
|
|
99
|
-
expect(signOut).toHaveBeenCalled();
|
|
100
|
-
enableNextAuth.value = false;
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should not call next-auth signOut when NextAuth is disabled', async () => {
|
|
52
|
+
it('should call better-auth signOut', async () => {
|
|
104
53
|
const { result } = renderHook(() => useUserStore());
|
|
105
54
|
|
|
106
55
|
await act(async () => {
|
|
107
56
|
await result.current.logout();
|
|
108
57
|
});
|
|
109
58
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
expect(signOut).not.toHaveBeenCalled();
|
|
59
|
+
expect(mockBetterAuthClient.signOut).toHaveBeenCalled();
|
|
113
60
|
});
|
|
114
61
|
});
|
|
115
62
|
|
|
116
63
|
describe('openLogin', () => {
|
|
117
|
-
it('should
|
|
118
|
-
enableNextAuth.value = true;
|
|
119
|
-
|
|
120
|
-
const { result } = renderHook(() => useUserStore());
|
|
121
|
-
|
|
122
|
-
await act(async () => {
|
|
123
|
-
await result.current.openLogin();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const { signIn } = await import('next-auth/react');
|
|
127
|
-
|
|
128
|
-
expect(signIn).toHaveBeenCalled();
|
|
129
|
-
enableNextAuth.value = false;
|
|
130
|
-
});
|
|
131
|
-
it('should not call next-auth signIn when NextAuth is disabled', async () => {
|
|
132
|
-
const { result } = renderHook(() => useUserStore());
|
|
133
|
-
|
|
134
|
-
await act(async () => {
|
|
135
|
-
await result.current.openLogin();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const { signIn } = await import('next-auth/react');
|
|
139
|
-
|
|
140
|
-
expect(signIn).not.toHaveBeenCalled();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should redirect to signin page when BetterAuth is enabled', async () => {
|
|
144
|
-
enableBetterAuth.value = true;
|
|
145
|
-
|
|
64
|
+
it('should redirect to signin page', async () => {
|
|
146
65
|
const originalLocation = window.location;
|
|
147
66
|
Object.defineProperty(window, 'location', {
|
|
148
67
|
configurable: true,
|
|
@@ -171,18 +90,15 @@ describe('createAuthSlice', () => {
|
|
|
171
90
|
});
|
|
172
91
|
});
|
|
173
92
|
|
|
174
|
-
it('should
|
|
175
|
-
enableNextAuth.value = true;
|
|
176
|
-
useUserStore.setState({ oAuthSSOProviders: ['github'] });
|
|
177
|
-
|
|
93
|
+
it('should not redirect when already on signin page', async () => {
|
|
178
94
|
const originalLocation = window.location;
|
|
179
95
|
Object.defineProperty(window, 'location', {
|
|
180
96
|
configurable: true,
|
|
181
97
|
value: {
|
|
182
98
|
...originalLocation,
|
|
183
99
|
href: '',
|
|
184
|
-
pathname: '/
|
|
185
|
-
toString: () => 'http://localhost/
|
|
100
|
+
pathname: '/signin',
|
|
101
|
+
toString: () => 'http://localhost/signin',
|
|
186
102
|
},
|
|
187
103
|
writable: true,
|
|
188
104
|
});
|
|
@@ -193,9 +109,7 @@ describe('createAuthSlice', () => {
|
|
|
193
109
|
await result.current.openLogin();
|
|
194
110
|
});
|
|
195
111
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
expect(signIn).toHaveBeenCalledWith('github');
|
|
112
|
+
expect(window.location.href).toBe('');
|
|
199
113
|
|
|
200
114
|
Object.defineProperty(window, 'location', {
|
|
201
115
|
configurable: true,
|
|
@@ -215,29 +129,10 @@ describe('createAuthSlice', () => {
|
|
|
215
129
|
await result.current.fetchAuthProviders();
|
|
216
130
|
});
|
|
217
131
|
|
|
218
|
-
expect(
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it('should fetch providers from NextAuth when BetterAuth is disabled', async () => {
|
|
222
|
-
enableBetterAuth.value = false;
|
|
223
|
-
const mockProviders = [
|
|
224
|
-
{ provider: 'github', email: 'test@example.com', providerAccountId: '123' },
|
|
225
|
-
];
|
|
226
|
-
mockUserService.getUserSSOProviders.mockResolvedValueOnce(mockProviders);
|
|
227
|
-
|
|
228
|
-
const { result } = renderHook(() => useUserStore());
|
|
229
|
-
|
|
230
|
-
await act(async () => {
|
|
231
|
-
await result.current.fetchAuthProviders();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
expect(mockUserService.getUserSSOProviders).toHaveBeenCalled();
|
|
235
|
-
expect(result.current.isLoadedAuthProviders).toBe(true);
|
|
236
|
-
expect(result.current.authProviders).toEqual(mockProviders);
|
|
132
|
+
expect(mockBetterAuthClient.listAccounts).not.toHaveBeenCalled();
|
|
237
133
|
});
|
|
238
134
|
|
|
239
|
-
it('should fetch providers from BetterAuth
|
|
240
|
-
enableBetterAuth.value = true;
|
|
135
|
+
it('should fetch providers from BetterAuth', async () => {
|
|
241
136
|
mockBetterAuthClient.listAccounts.mockResolvedValueOnce({
|
|
242
137
|
data: [
|
|
243
138
|
{ providerId: 'github', accountId: 'gh-123' },
|
|
@@ -260,8 +155,7 @@ describe('createAuthSlice', () => {
|
|
|
260
155
|
});
|
|
261
156
|
|
|
262
157
|
it('should handle fetch error gracefully', async () => {
|
|
263
|
-
|
|
264
|
-
mockUserService.getUserSSOProviders.mockRejectedValueOnce(new Error('Network error'));
|
|
158
|
+
mockBetterAuthClient.listAccounts.mockRejectedValueOnce(new Error('Network error'));
|
|
265
159
|
|
|
266
160
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
267
161
|
|
|
@@ -277,12 +171,13 @@ describe('createAuthSlice', () => {
|
|
|
277
171
|
});
|
|
278
172
|
|
|
279
173
|
describe('refreshAuthProviders', () => {
|
|
280
|
-
it('should refresh providers from
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
174
|
+
it('should refresh providers from BetterAuth', async () => {
|
|
175
|
+
mockBetterAuthClient.listAccounts.mockResolvedValueOnce({
|
|
176
|
+
data: [{ providerId: 'google', accountId: 'g-1' }],
|
|
177
|
+
});
|
|
178
|
+
mockBetterAuthClient.accountInfo.mockResolvedValueOnce({
|
|
179
|
+
data: { user: { email: 'user@gmail.com' } },
|
|
180
|
+
});
|
|
286
181
|
|
|
287
182
|
const { result } = renderHook(() => useUserStore());
|
|
288
183
|
|
|
@@ -290,13 +185,14 @@ describe('createAuthSlice', () => {
|
|
|
290
185
|
await result.current.refreshAuthProviders();
|
|
291
186
|
});
|
|
292
187
|
|
|
293
|
-
expect(
|
|
294
|
-
expect(result.current.authProviders).toEqual(
|
|
188
|
+
expect(mockBetterAuthClient.listAccounts).toHaveBeenCalled();
|
|
189
|
+
expect(result.current.authProviders).toEqual([
|
|
190
|
+
{ provider: 'google', email: 'user@gmail.com', providerAccountId: 'g-1' },
|
|
191
|
+
]);
|
|
295
192
|
});
|
|
296
193
|
|
|
297
194
|
it('should handle refresh error gracefully', async () => {
|
|
298
|
-
|
|
299
|
-
mockUserService.getUserSSOProviders.mockRejectedValueOnce(new Error('Refresh failed'));
|
|
195
|
+
mockBetterAuthClient.listAccounts.mockRejectedValueOnce(new Error('Refresh failed'));
|
|
300
196
|
|
|
301
197
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
302
198
|
|