@lobehub/lobehub 2.0.0-next.124 → 2.0.0-next.126
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/.cursor/rules/db-migrations.mdc +16 -1
- package/.cursor/rules/project-introduce.mdc +1 -1
- package/.cursor/rules/project-structure.mdc +20 -2
- package/.env.example +148 -65
- package/.env.example.development +6 -8
- package/AGENTS.md +1 -3
- package/CHANGELOG.md +50 -0
- package/Dockerfile +7 -5
- package/GEMINI.md +63 -0
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +37 -0
- package/docs/self-hosting/advanced/auth.mdx +82 -2
- package/docs/self-hosting/advanced/auth.zh-CN.mdx +82 -2
- package/docs/self-hosting/environment-variables/auth.mdx +187 -1
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +187 -1
- package/locales/en-US/auth.json +93 -0
- package/locales/zh-CN/auth.json +107 -1
- package/package.json +5 -2
- package/packages/const/src/auth.ts +2 -1
- package/packages/database/migrations/0049_better_auth.sql +49 -0
- package/packages/database/migrations/meta/0048_snapshot.json +312 -932
- package/packages/database/migrations/meta/0049_snapshot.json +8151 -0
- package/packages/database/migrations/meta/_journal.json +8 -1
- package/packages/database/src/core/migrations.json +13 -0
- package/packages/database/src/index.ts +1 -0
- package/packages/database/src/models/__tests__/session.test.ts +1 -2
- package/packages/database/src/models/user.ts +9 -8
- package/packages/database/src/repositories/tableViewer/index.test.ts +2 -2
- package/packages/database/src/schemas/betterAuth.ts +63 -0
- package/packages/database/src/schemas/index.ts +1 -0
- package/packages/database/src/schemas/ragEvals.ts +1 -2
- package/packages/database/src/schemas/user.ts +3 -2
- package/packages/database/src/server/models/__tests__/user.test.ts +1 -4
- package/packages/types/src/user/preference.ts +11 -0
- package/packages/utils/src/server/__tests__/auth.test.ts +52 -0
- package/packages/utils/src/server/auth.ts +18 -1
- package/src/app/(backend)/api/auth/[...all]/route.ts +19 -0
- package/src/app/(backend)/api/auth/check-user/route.ts +62 -0
- package/src/app/(backend)/middleware/auth/index.ts +14 -0
- package/src/app/(backend)/middleware/auth/utils.test.ts +16 -0
- package/src/app/(backend)/middleware/auth/utils.ts +13 -10
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +1 -0
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +12 -0
- package/src/app/[variants]/(auth)/reset-password/page.tsx +209 -0
- package/src/app/[variants]/(auth)/signin/layout.tsx +12 -0
- package/src/app/[variants]/(auth)/signin/page.tsx +448 -0
- package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +192 -0
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +31 -6
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +12 -0
- package/src/app/[variants]/(auth)/verify-email/page.tsx +164 -0
- package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +12 -10
- package/src/app/[variants]/(main)/(mobile)/me/(home)/__tests__/useCategory.test.tsx +13 -11
- package/src/app/[variants]/(main)/profile/(home)/Client.tsx +306 -52
- package/src/app/[variants]/(main)/profile/(home)/features/SSOProvidersList/index.tsx +89 -47
- package/src/auth.ts +118 -0
- package/src/components/NextAuth/AuthIcons.tsx +3 -1
- package/src/envs/auth.ts +260 -13
- package/src/envs/email.ts +37 -0
- package/src/features/User/UserPanel/PanelContent.tsx +6 -5
- package/src/features/User/__tests__/PanelContent.test.tsx +15 -6
- package/src/features/User/__tests__/UserAvatar.test.tsx +17 -6
- package/src/features/User/__tests__/useMenu.test.tsx +14 -12
- package/src/layout/AuthProvider/BetterAuth/UserUpdater.tsx +51 -0
- package/src/layout/AuthProvider/BetterAuth/index.tsx +14 -0
- package/src/layout/AuthProvider/index.tsx +3 -0
- package/src/libs/better-auth/auth-client.ts +34 -0
- package/src/libs/better-auth/constants.ts +13 -0
- package/src/libs/better-auth/email-templates/index.ts +3 -0
- package/src/libs/better-auth/email-templates/magic-link.ts +98 -0
- package/src/libs/better-auth/email-templates/reset-password.ts +91 -0
- package/src/libs/better-auth/email-templates/verification.ts +108 -0
- package/src/libs/better-auth/sso/helpers.ts +61 -0
- package/src/libs/better-auth/sso/index.ts +113 -0
- package/src/libs/better-auth/sso/providers/auth0.ts +33 -0
- package/src/libs/better-auth/sso/providers/authelia.ts +35 -0
- package/src/libs/better-auth/sso/providers/authentik.ts +35 -0
- package/src/libs/better-auth/sso/providers/casdoor.ts +48 -0
- package/src/libs/better-auth/sso/providers/cloudflare-zero-trust.ts +41 -0
- package/src/libs/better-auth/sso/providers/cognito.ts +45 -0
- package/src/libs/better-auth/sso/providers/feishu.ts +181 -0
- package/src/libs/better-auth/sso/providers/generic-oidc.ts +44 -0
- package/src/libs/better-auth/sso/providers/github.ts +30 -0
- package/src/libs/better-auth/sso/providers/google.ts +30 -0
- package/src/libs/better-auth/sso/providers/keycloak.ts +35 -0
- package/src/libs/better-auth/sso/providers/logto.ts +38 -0
- package/src/libs/better-auth/sso/providers/microsoft.ts +65 -0
- package/src/libs/better-auth/sso/providers/okta.ts +37 -0
- package/src/libs/better-auth/sso/providers/wechat.ts +140 -0
- package/src/libs/better-auth/sso/providers/zitadel.ts +54 -0
- package/src/libs/better-auth/sso/types.ts +25 -0
- package/src/libs/better-auth/utils/client.ts +1 -0
- package/src/libs/better-auth/utils/common.ts +20 -0
- package/src/libs/better-auth/utils/server.test.ts +61 -0
- package/src/libs/better-auth/utils/server.ts +18 -0
- package/src/libs/trpc/lambda/context.test.ts +116 -0
- package/src/libs/trpc/lambda/context.ts +27 -0
- package/src/libs/trpc/middleware/userAuth.ts +4 -2
- package/src/locales/default/auth.ts +114 -1
- package/src/proxy.ts +71 -7
- package/src/server/globalConfig/index.ts +12 -1
- package/src/server/routers/lambda/user.ts +4 -0
- package/src/server/services/email/README.md +241 -0
- package/src/server/services/email/impls/index.test.ts +39 -0
- package/src/server/services/email/impls/index.ts +32 -0
- package/src/server/services/email/impls/nodemailer/index.ts +108 -0
- package/src/server/services/email/impls/nodemailer/type.ts +31 -0
- package/src/server/services/email/impls/type.ts +61 -0
- package/src/server/services/email/index.test.ts +144 -0
- package/src/server/services/email/index.ts +40 -0
- package/src/services/user/index.test.ts +162 -2
- package/src/services/user/index.ts +6 -3
- package/src/store/user/slices/auth/action.test.ts +213 -16
- package/src/store/user/slices/auth/action.ts +86 -1
- package/src/store/user/slices/auth/initialState.ts +13 -2
- package/src/store/user/slices/auth/selectors.ts +6 -2
- package/src/store/user/slices/common/action.ts +5 -1
- package/src/app/(backend)/api/auth/[...nextauth]/route.ts +0 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { authEnv } from '@/envs/auth';
|
|
2
|
+
|
|
3
|
+
import { buildOidcConfig, pickEnv } from '../helpers';
|
|
4
|
+
import type { GenericProviderDefinition } from '../types';
|
|
5
|
+
|
|
6
|
+
type ZitadelEnv = {
|
|
7
|
+
AUTH_ZITADEL_ID?: string;
|
|
8
|
+
AUTH_ZITADEL_ISSUER?: string;
|
|
9
|
+
AUTH_ZITADEL_SECRET?: string;
|
|
10
|
+
ZITADEL_CLIENT_ID?: string;
|
|
11
|
+
ZITADEL_CLIENT_SECRET?: string;
|
|
12
|
+
ZITADEL_ISSUER?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const getClientId = (env: ZitadelEnv) => {
|
|
16
|
+
return pickEnv(env.ZITADEL_CLIENT_ID, env.AUTH_ZITADEL_ID);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getClientSecret = (env: ZitadelEnv) => {
|
|
20
|
+
return pickEnv(env.ZITADEL_CLIENT_SECRET, env.AUTH_ZITADEL_SECRET);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getIssuer = (env: ZitadelEnv) => {
|
|
24
|
+
return pickEnv(env.ZITADEL_ISSUER, env.AUTH_ZITADEL_ISSUER);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const provider: GenericProviderDefinition<ZitadelEnv> = {
|
|
28
|
+
build: (env) =>
|
|
29
|
+
buildOidcConfig({
|
|
30
|
+
clientId: getClientId(env)!,
|
|
31
|
+
clientSecret: getClientSecret(env)!,
|
|
32
|
+
issuer: getIssuer(env)!,
|
|
33
|
+
providerId: 'zitadel',
|
|
34
|
+
}),
|
|
35
|
+
checkEnvs: () => {
|
|
36
|
+
const clientId = getClientId(authEnv);
|
|
37
|
+
const clientSecret = getClientSecret(authEnv);
|
|
38
|
+
const issuer = getIssuer(authEnv);
|
|
39
|
+
return !!(clientId && clientSecret && issuer)
|
|
40
|
+
? {
|
|
41
|
+
AUTH_ZITADEL_ID: authEnv.AUTH_ZITADEL_ID,
|
|
42
|
+
AUTH_ZITADEL_ISSUER: authEnv.AUTH_ZITADEL_ISSUER,
|
|
43
|
+
AUTH_ZITADEL_SECRET: authEnv.AUTH_ZITADEL_SECRET,
|
|
44
|
+
ZITADEL_CLIENT_ID: authEnv.ZITADEL_CLIENT_ID,
|
|
45
|
+
ZITADEL_CLIENT_SECRET: authEnv.ZITADEL_CLIENT_SECRET,
|
|
46
|
+
ZITADEL_ISSUER: authEnv.ZITADEL_ISSUER,
|
|
47
|
+
}
|
|
48
|
+
: false;
|
|
49
|
+
},
|
|
50
|
+
id: 'zitadel',
|
|
51
|
+
type: 'generic',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default provider;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GenericOAuthConfig } from 'better-auth/plugins';
|
|
2
|
+
import type { SocialProviders } from 'better-auth/social-providers';
|
|
3
|
+
|
|
4
|
+
export type BuiltinProviderDefinition<
|
|
5
|
+
E extends Record<string, string | undefined>,
|
|
6
|
+
Id extends keyof SocialProviders = keyof SocialProviders,
|
|
7
|
+
> = {
|
|
8
|
+
aliases?: string[];
|
|
9
|
+
build: (env: E) => SocialProviders[Id];
|
|
10
|
+
checkEnvs: () => E | false;
|
|
11
|
+
id: Id;
|
|
12
|
+
type: 'builtin';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type GenericProviderDefinition<E extends Record<string, string | undefined>> = {
|
|
16
|
+
aliases?: string[];
|
|
17
|
+
build: (env: E) => GenericOAuthConfig;
|
|
18
|
+
checkEnvs: () => E | false;
|
|
19
|
+
id: string;
|
|
20
|
+
type: 'generic';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type BetterAuthProviderDefinition =
|
|
24
|
+
| BuiltinProviderDefinition<Record<string, string | undefined>>
|
|
25
|
+
| GenericProviderDefinition<Record<string, string | undefined>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isBuiltinProvider, normalizeProviderId } from './common';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BUILTIN_BETTER_AUTH_PROVIDERS, PROVIDER_ALIAS_MAP } from '@/libs/better-auth/constants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize provider id using configured alias map (e.g. microsoft-entra-id -> microsoft).
|
|
5
|
+
*/
|
|
6
|
+
export const normalizeProviderId = (provider: string) => {
|
|
7
|
+
return PROVIDER_ALIAS_MAP[provider] || provider;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check whether a provider is handled by Better-Auth's built-in social providers.
|
|
12
|
+
* Uses alias normalization so callers can pass either canonical ids or aliases.
|
|
13
|
+
*/
|
|
14
|
+
export const isBuiltinProvider = (provider: string) => {
|
|
15
|
+
const normalized = normalizeProviderId(provider);
|
|
16
|
+
|
|
17
|
+
return BUILTIN_BETTER_AUTH_PROVIDERS.includes(
|
|
18
|
+
normalized as (typeof BUILTIN_BETTER_AUTH_PROVIDERS)[number],
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { parseSSOProviders } from './server';
|
|
4
|
+
|
|
5
|
+
describe('parseSSOProviders', () => {
|
|
6
|
+
it('should return empty array when input is undefined', () => {
|
|
7
|
+
expect(parseSSOProviders(undefined)).toEqual([]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should return empty array when input is empty string', () => {
|
|
11
|
+
expect(parseSSOProviders('')).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should return empty array when input contains only whitespace', () => {
|
|
15
|
+
expect(parseSSOProviders(' ')).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should parse single provider', () => {
|
|
19
|
+
expect(parseSSOProviders('google')).toEqual(['google']);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should parse multiple providers separated by English comma', () => {
|
|
23
|
+
expect(parseSSOProviders('google,github,microsoft')).toEqual(['google', 'github', 'microsoft']);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should parse multiple providers separated by Chinese comma', () => {
|
|
27
|
+
expect(parseSSOProviders('google,github,microsoft')).toEqual([
|
|
28
|
+
'google',
|
|
29
|
+
'github',
|
|
30
|
+
'microsoft',
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should parse providers with mixed comma separators', () => {
|
|
35
|
+
expect(parseSSOProviders('google,github,microsoft')).toEqual([
|
|
36
|
+
'google',
|
|
37
|
+
'github',
|
|
38
|
+
'microsoft',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should trim whitespace from providers', () => {
|
|
43
|
+
expect(parseSSOProviders(' google , github , microsoft ')).toEqual([
|
|
44
|
+
'google',
|
|
45
|
+
'github',
|
|
46
|
+
'microsoft',
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should filter out empty entries', () => {
|
|
51
|
+
expect(parseSSOProviders('google,,github,,,microsoft')).toEqual([
|
|
52
|
+
'google',
|
|
53
|
+
'github',
|
|
54
|
+
'microsoft',
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should trim leading and trailing whitespace from input', () => {
|
|
59
|
+
expect(parseSSOProviders(' google,github ')).toEqual(['google', 'github']);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse Better-Auth SSO providers from environment variable
|
|
3
|
+
* Supports comma-separated list (both English and Chinese commas)
|
|
4
|
+
* @param providersEnv - Raw environment variable value (e.g., "google,github")
|
|
5
|
+
* @returns Array of enabled provider names
|
|
6
|
+
*/
|
|
7
|
+
export const parseSSOProviders = (providersEnv?: string): string[] => {
|
|
8
|
+
const providers = providersEnv?.trim();
|
|
9
|
+
|
|
10
|
+
if (!providers) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return providers
|
|
15
|
+
.split(/[,,]/)
|
|
16
|
+
.map((p) => p.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createContextInner } from './context';
|
|
4
|
+
|
|
5
|
+
describe('createContextInner', () => {
|
|
6
|
+
it('should create context with default values when no params provided', async () => {
|
|
7
|
+
const context = await createContextInner();
|
|
8
|
+
|
|
9
|
+
expect(context).toMatchObject({
|
|
10
|
+
authorizationHeader: undefined,
|
|
11
|
+
clerkAuth: undefined,
|
|
12
|
+
marketAccessToken: undefined,
|
|
13
|
+
nextAuth: undefined,
|
|
14
|
+
oidcAuth: undefined,
|
|
15
|
+
userAgent: undefined,
|
|
16
|
+
userId: undefined,
|
|
17
|
+
});
|
|
18
|
+
expect(context.resHeaders).toBeInstanceOf(Headers);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should create context with userId', async () => {
|
|
22
|
+
const context = await createContextInner({ userId: 'user-123' });
|
|
23
|
+
|
|
24
|
+
expect(context.userId).toBe('user-123');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should create context with authorization header', async () => {
|
|
28
|
+
const context = await createContextInner({
|
|
29
|
+
authorizationHeader: 'Bearer token-abc',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(context.authorizationHeader).toBe('Bearer token-abc');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should create context with user agent', async () => {
|
|
36
|
+
const context = await createContextInner({
|
|
37
|
+
userAgent: 'Mozilla/5.0',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(context.userAgent).toBe('Mozilla/5.0');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should create context with market access token', async () => {
|
|
44
|
+
const context = await createContextInner({
|
|
45
|
+
marketAccessToken: 'mp-token-xyz',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(context.marketAccessToken).toBe('mp-token-xyz');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should create context with OIDC auth data', async () => {
|
|
52
|
+
const oidcAuth = {
|
|
53
|
+
sub: 'oidc-user-123',
|
|
54
|
+
payload: { iss: 'https://issuer.com', aud: 'client-id' },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const context = await createContextInner({ oidcAuth });
|
|
58
|
+
|
|
59
|
+
expect(context.oidcAuth).toEqual(oidcAuth);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should create context with Clerk auth data', async () => {
|
|
63
|
+
const clerkAuth = {
|
|
64
|
+
userId: 'clerk-user-id',
|
|
65
|
+
sessionId: 'session-id',
|
|
66
|
+
getToken: async () => 'clerk-token',
|
|
67
|
+
} as any;
|
|
68
|
+
|
|
69
|
+
const context = await createContextInner({ clerkAuth });
|
|
70
|
+
|
|
71
|
+
expect(context.clerkAuth).toBe(clerkAuth);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should create context with NextAuth user data', async () => {
|
|
75
|
+
const nextAuth = {
|
|
76
|
+
id: 'next-auth-user-id',
|
|
77
|
+
name: 'Test User',
|
|
78
|
+
email: 'test@example.com',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const context = await createContextInner({ nextAuth });
|
|
82
|
+
|
|
83
|
+
expect(context.nextAuth).toEqual(nextAuth);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should create context with all parameters combined', async () => {
|
|
87
|
+
const params = {
|
|
88
|
+
authorizationHeader: 'Bearer token',
|
|
89
|
+
userId: 'user-123',
|
|
90
|
+
userAgent: 'Test Agent',
|
|
91
|
+
marketAccessToken: 'mp-token',
|
|
92
|
+
oidcAuth: {
|
|
93
|
+
sub: 'oidc-sub',
|
|
94
|
+
payload: { data: 'test' },
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const context = await createContextInner(params);
|
|
99
|
+
|
|
100
|
+
expect(context).toMatchObject({
|
|
101
|
+
authorizationHeader: 'Bearer token',
|
|
102
|
+
userId: 'user-123',
|
|
103
|
+
userAgent: 'Test Agent',
|
|
104
|
+
marketAccessToken: 'mp-token',
|
|
105
|
+
oidcAuth: { sub: 'oidc-sub', payload: { data: 'test' } },
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should always include response headers', async () => {
|
|
110
|
+
const context1 = await createContextInner();
|
|
111
|
+
const context2 = await createContextInner({ userId: 'test' });
|
|
112
|
+
|
|
113
|
+
expect(context1.resHeaders).toBeInstanceOf(Headers);
|
|
114
|
+
expect(context2.resHeaders).toBeInstanceOf(Headers);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -7,6 +7,7 @@ import { NextRequest } from 'next/server';
|
|
|
7
7
|
import {
|
|
8
8
|
LOBE_CHAT_AUTH_HEADER,
|
|
9
9
|
LOBE_CHAT_OIDC_AUTH_HEADER,
|
|
10
|
+
enableBetterAuth,
|
|
10
11
|
enableClerk,
|
|
11
12
|
enableNextAuth,
|
|
12
13
|
} from '@/const/auth';
|
|
@@ -163,6 +164,32 @@ export const createLambdaContext = async (request: NextRequest): Promise<LambdaC
|
|
|
163
164
|
});
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
if (enableBetterAuth) {
|
|
168
|
+
log('Attempting Better Auth authentication');
|
|
169
|
+
try {
|
|
170
|
+
const { auth: betterAuth } = await import('@/auth');
|
|
171
|
+
|
|
172
|
+
const session = await betterAuth.api.getSession({
|
|
173
|
+
headers: request.headers,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (session && session?.user?.id) {
|
|
177
|
+
userId = session.user.id;
|
|
178
|
+
log('Better Auth authentication successful, userId: %s', userId);
|
|
179
|
+
} else {
|
|
180
|
+
log('Better Auth authentication failed, no valid session');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return createContextInner({
|
|
184
|
+
...commonContext,
|
|
185
|
+
userId,
|
|
186
|
+
});
|
|
187
|
+
} catch (e) {
|
|
188
|
+
log('Better Auth authentication error: %O', e);
|
|
189
|
+
console.error('better auth err', e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
166
193
|
if (enableNextAuth) {
|
|
167
194
|
log('Attempting NextAuth authentication');
|
|
168
195
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TRPCError } from '@trpc/server';
|
|
2
2
|
|
|
3
|
-
import { enableClerk } from '@/const/auth';
|
|
3
|
+
import { enableBetterAuth, enableClerk, enableNextAuth } from '@/const/auth';
|
|
4
4
|
import { DESKTOP_USER_ID } from '@/const/desktop';
|
|
5
5
|
import { isDesktop } from '@/const/version';
|
|
6
6
|
|
|
@@ -19,7 +19,9 @@ export const userAuth = trpc.middleware(async (opts) => {
|
|
|
19
19
|
if (!ctx.userId) {
|
|
20
20
|
if (enableClerk) {
|
|
21
21
|
console.log('clerk auth:', ctx.clerkAuth);
|
|
22
|
-
} else {
|
|
22
|
+
} else if (enableBetterAuth) {
|
|
23
|
+
console.log('better auth: no session found in context');
|
|
24
|
+
} else if (enableNextAuth) {
|
|
23
25
|
console.log('next auth:', ctx.nextAuth);
|
|
24
26
|
}
|
|
25
27
|
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
@@ -52,6 +52,104 @@ export default {
|
|
|
52
52
|
required: '内容不得为空',
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
|
+
betterAuth: {
|
|
56
|
+
errors: {
|
|
57
|
+
emailInvalid: '请输入有效的邮箱地址',
|
|
58
|
+
emailNotRegistered: '该邮箱尚未注册',
|
|
59
|
+
emailNotVerified: '邮箱尚未验证,请先验证邮箱',
|
|
60
|
+
emailRequired: '请输入邮箱地址',
|
|
61
|
+
firstNameRequired: '请输入名字',
|
|
62
|
+
lastNameRequired: '请输入姓氏',
|
|
63
|
+
loginFailed: '登录失败,请检查邮箱和密码',
|
|
64
|
+
passwordFormat: '密码必须同时包含字母和数字',
|
|
65
|
+
passwordMaxLength: '密码最多不超过 64 个字符',
|
|
66
|
+
passwordMinLength: '密码至少需要 8 个字符',
|
|
67
|
+
passwordRequired: '请输入密码',
|
|
68
|
+
usernameRequired: '请输入用户名',
|
|
69
|
+
},
|
|
70
|
+
resetPassword: {
|
|
71
|
+
backToSignIn: '返回登录',
|
|
72
|
+
confirmPasswordPlaceholder: '确认新密码',
|
|
73
|
+
confirmPasswordRequired: '请确认新密码',
|
|
74
|
+
description: '请输入您的新密码',
|
|
75
|
+
error: '重置密码失败,请重试',
|
|
76
|
+
invalidToken: '无效或已过期的重置链接',
|
|
77
|
+
newPasswordPlaceholder: '输入新密码',
|
|
78
|
+
passwordMismatch: '两次输入的密码不一致',
|
|
79
|
+
submit: '重置密码',
|
|
80
|
+
success: '密码重置成功,请使用新密码登录',
|
|
81
|
+
title: '重置密码',
|
|
82
|
+
},
|
|
83
|
+
signin: {
|
|
84
|
+
backToEmail: '返回修改邮箱',
|
|
85
|
+
continueWithAuth0: '使用 Auth0 登录',
|
|
86
|
+
continueWithAuthelia: '使用 Authelia 登录',
|
|
87
|
+
continueWithAuthentik: '使用 Authentik 登录',
|
|
88
|
+
continueWithCasdoor: '使用 Casdoor 登录',
|
|
89
|
+
continueWithCloudflareZeroTrust: '使用 Cloudflare Zero Trust 登录',
|
|
90
|
+
continueWithCognito: '使用 AWS Cognito 登录',
|
|
91
|
+
continueWithFeishu: '使用飞书登录',
|
|
92
|
+
continueWithGithub: '使用 GitHub 登录',
|
|
93
|
+
continueWithGoogle: '使用 Google 登录',
|
|
94
|
+
continueWithKeycloak: '使用 Keycloak 登录',
|
|
95
|
+
continueWithLogto: '使用 Logto 登录',
|
|
96
|
+
continueWithMicrosoft: '使用 Microsoft 登录',
|
|
97
|
+
continueWithOIDC: '使用 OIDC 登录',
|
|
98
|
+
continueWithOkta: '使用 Okta 登录',
|
|
99
|
+
continueWithWechat: '使用微信登录',
|
|
100
|
+
continueWithZitadel: '使用 Zitadel 登录',
|
|
101
|
+
emailPlaceholder: '请输入邮箱地址',
|
|
102
|
+
emailStep: {
|
|
103
|
+
subtitle: '请输入您的邮箱地址以继续',
|
|
104
|
+
title: '登录',
|
|
105
|
+
},
|
|
106
|
+
error: '登录失败,请检查邮箱和密码',
|
|
107
|
+
forgotPassword: '忘记密码?',
|
|
108
|
+
forgotPasswordError: '发送重置密码链接失败',
|
|
109
|
+
forgotPasswordSent: '重置密码链接已发送,请检查邮箱',
|
|
110
|
+
magicLinkButton: '发送登录链接',
|
|
111
|
+
magicLinkError: '发送登录链接失败,请稍后再试',
|
|
112
|
+
magicLinkSent: '登录链接已发送,请检查邮箱',
|
|
113
|
+
nextStep: '下一步',
|
|
114
|
+
noAccount: '还没有账号?',
|
|
115
|
+
orContinueWith: '或',
|
|
116
|
+
passwordPlaceholder: '请输入密码',
|
|
117
|
+
passwordStep: {
|
|
118
|
+
subtitle: '请输入密码以继续',
|
|
119
|
+
},
|
|
120
|
+
signupLink: '立即注册',
|
|
121
|
+
socialError: '社交登录失败,请重试',
|
|
122
|
+
socialOnlyHint: '该邮箱使用社交账号注册,请使用社交账号登录',
|
|
123
|
+
submit: '登录',
|
|
124
|
+
},
|
|
125
|
+
signup: {
|
|
126
|
+
emailPlaceholder: '请输入邮箱地址',
|
|
127
|
+
error: '注册失败,请重试',
|
|
128
|
+
firstNamePlaceholder: '名字',
|
|
129
|
+
hasAccount: '已有账号?',
|
|
130
|
+
lastNamePlaceholder: '姓氏',
|
|
131
|
+
passwordPlaceholder: '请输入密码',
|
|
132
|
+
signinLink: '立即登录',
|
|
133
|
+
submit: '注册',
|
|
134
|
+
subtitle: '加入 LobeChat 社区',
|
|
135
|
+
success: '注册成功!请检查您的邮箱验证邮件',
|
|
136
|
+
title: '创建账号',
|
|
137
|
+
usernamePlaceholder: '请输入用户名',
|
|
138
|
+
},
|
|
139
|
+
verifyEmail: {
|
|
140
|
+
backToSignIn: '返回登录',
|
|
141
|
+
checkSpam: '如果没有收到邮件,请检查垃圾邮件文件夹',
|
|
142
|
+
descriptionPrefix: '我们已向',
|
|
143
|
+
descriptionSuffix: '发送了验证邮件',
|
|
144
|
+
resend: {
|
|
145
|
+
button: '重新发送验证邮件',
|
|
146
|
+
error: '发送失败,请稍后重试',
|
|
147
|
+
noEmail: '邮箱地址缺失',
|
|
148
|
+
success: '验证邮件已重新发送,请检查您的邮箱',
|
|
149
|
+
},
|
|
150
|
+
title: '验证您的邮箱',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
55
153
|
date: {
|
|
56
154
|
prevMonth: '上个月',
|
|
57
155
|
recent30Days: '最近30天',
|
|
@@ -86,17 +184,32 @@ export default {
|
|
|
86
184
|
loginOrSignup: '登录 / 注册',
|
|
87
185
|
profile: {
|
|
88
186
|
avatar: '头像',
|
|
187
|
+
cancel: '取消',
|
|
188
|
+
changePassword: '重置密码',
|
|
89
189
|
email: '电子邮件地址',
|
|
190
|
+
fullName: '全名',
|
|
191
|
+
fullNameInputHint: '请输入新的全名',
|
|
192
|
+
password: '密码',
|
|
193
|
+
resetPasswordError: '发送密码重置链接失败',
|
|
194
|
+
resetPasswordSent: '密码重置链接已发送,请检查邮箱',
|
|
195
|
+
save: '保存',
|
|
90
196
|
sso: {
|
|
197
|
+
link: {
|
|
198
|
+
button: '连接帐户',
|
|
199
|
+
success: '账户关联成功',
|
|
200
|
+
},
|
|
91
201
|
loading: '正在加载已绑定的第三方账户',
|
|
92
202
|
providers: '连接的帐户',
|
|
93
203
|
unlink: {
|
|
94
204
|
description:
|
|
95
|
-
'解绑后,您将无法使用 {{provider}}
|
|
205
|
+
'解绑后,您将无法使用 {{provider}} 账户"{{providerAccountId}}"登录。如果您需要重新绑定 {{provider}} 账户到当前账户,请确保 {{provider}} 账户的邮件地址为 {{email}} ,我们会在登陆时为你自动绑定到当前登录账户。',
|
|
96
206
|
forbidden: '您至少需要保留一个第三方账户绑定。',
|
|
97
207
|
title: '是否解绑该第三方账户 {{provider}} ?',
|
|
98
208
|
},
|
|
99
209
|
},
|
|
210
|
+
title: '个人资料详情',
|
|
211
|
+
updateAvatar: '更新头像',
|
|
212
|
+
updateFullName: '更新全名',
|
|
100
213
|
username: '用户名',
|
|
101
214
|
},
|
|
102
215
|
signout: '退出登录',
|
package/src/proxy.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
5
5
|
import { UAParser } from 'ua-parser-js';
|
|
6
6
|
import urlJoin from 'url-join';
|
|
7
7
|
|
|
8
|
+
import { auth } from '@/auth';
|
|
8
9
|
import { OAUTH_AUTHORIZED } from '@/const/auth';
|
|
9
10
|
import { LOBE_LOCALE_COOKIE } from '@/const/locale';
|
|
10
11
|
import { LOBE_THEME_APPEARANCE } from '@/const/theme';
|
|
@@ -21,6 +22,7 @@ import { RouteVariants } from './utils/server/routeVariants';
|
|
|
21
22
|
const logDefault = debug('middleware:default');
|
|
22
23
|
const logNextAuth = debug('middleware:next-auth');
|
|
23
24
|
const logClerk = debug('middleware:clerk');
|
|
25
|
+
const logBetterAuth = debug('middleware:better-auth');
|
|
24
26
|
|
|
25
27
|
// OIDC session pre-sync constant
|
|
26
28
|
const OIDC_SESSION_HEADER = 'x-oidc-session-sync';
|
|
@@ -47,10 +49,12 @@ export const config = {
|
|
|
47
49
|
|
|
48
50
|
'/login(.*)',
|
|
49
51
|
'/signup(.*)',
|
|
52
|
+
'/signin(.*)',
|
|
53
|
+
'/verify-email(.*)',
|
|
54
|
+
'/reset-password(.*)',
|
|
50
55
|
'/next-auth/(.*)',
|
|
51
56
|
'/oauth(.*)',
|
|
52
57
|
'/oidc(.*)',
|
|
53
|
-
// ↓ cloud ↓
|
|
54
58
|
],
|
|
55
59
|
};
|
|
56
60
|
|
|
@@ -129,8 +133,18 @@ const defaultMiddleware = (request: NextRequest) => {
|
|
|
129
133
|
// / -> /zh-CN__0__dark
|
|
130
134
|
// /discover -> /zh-CN__0__dark/discover
|
|
131
135
|
// All SPA routes that use react-router-dom should be rewritten to just /${route}
|
|
132
|
-
const spaRoutes = [
|
|
133
|
-
|
|
136
|
+
const spaRoutes = [
|
|
137
|
+
'/chat',
|
|
138
|
+
'/discover',
|
|
139
|
+
'/knowledge',
|
|
140
|
+
'/settings',
|
|
141
|
+
'/image',
|
|
142
|
+
'/labs',
|
|
143
|
+
'/changelog',
|
|
144
|
+
'/profile',
|
|
145
|
+
'/me',
|
|
146
|
+
];
|
|
147
|
+
const isSpaRoute = spaRoutes.some((route) => url.pathname.startsWith(route));
|
|
134
148
|
|
|
135
149
|
let nextPathname: string;
|
|
136
150
|
if (isSpaRoute) {
|
|
@@ -142,7 +156,6 @@ const defaultMiddleware = (request: NextRequest) => {
|
|
|
142
156
|
? urlJoin(url.origin, nextPathname)
|
|
143
157
|
: nextPathname;
|
|
144
158
|
|
|
145
|
-
|
|
146
159
|
console.log('nextURL', nextURL);
|
|
147
160
|
|
|
148
161
|
logDefault('URL rewrite: %O', {
|
|
@@ -194,6 +207,10 @@ const isPublicRoute = createRouteMatcher([
|
|
|
194
207
|
// clerk
|
|
195
208
|
'/login',
|
|
196
209
|
'/signup',
|
|
210
|
+
// better auth
|
|
211
|
+
'/signin',
|
|
212
|
+
'/verify-email',
|
|
213
|
+
'/reset-password',
|
|
197
214
|
// oauth
|
|
198
215
|
// Make only the consent view public (GET page), not other oauth paths
|
|
199
216
|
'/oauth/consent/(.*)',
|
|
@@ -304,8 +321,53 @@ const clerkAuthMiddleware = clerkMiddleware(
|
|
|
304
321
|
},
|
|
305
322
|
);
|
|
306
323
|
|
|
324
|
+
const betterAuthMiddleware = async (req: NextRequest) => {
|
|
325
|
+
logBetterAuth('BetterAuth middleware processing request: %s %s', req.method, req.url);
|
|
326
|
+
|
|
327
|
+
const response = defaultMiddleware(req);
|
|
328
|
+
|
|
329
|
+
// when enable auth protection, only public route is not protected, others are all protected
|
|
330
|
+
const isProtected = appEnv.ENABLE_AUTH_PROTECTION ? !isPublicRoute(req) : isProtectedRoute(req);
|
|
331
|
+
|
|
332
|
+
logBetterAuth('Route protection status: %s, %s', req.url, isProtected ? 'protected' : 'public');
|
|
333
|
+
|
|
334
|
+
// Skip session lookup for public routes to reduce latency
|
|
335
|
+
if (!isProtected) return response;
|
|
336
|
+
|
|
337
|
+
// Get full session with user data (Next.js 15.2.0+ feature)
|
|
338
|
+
const session = await auth.api.getSession({
|
|
339
|
+
headers: req.headers,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const isLoggedIn = !!session?.user;
|
|
343
|
+
|
|
344
|
+
logBetterAuth('BetterAuth session status: %O', {
|
|
345
|
+
isLoggedIn,
|
|
346
|
+
userId: session?.user?.id,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (!isLoggedIn) {
|
|
350
|
+
// If request a protected route, redirect to sign-in page
|
|
351
|
+
if (isProtected) {
|
|
352
|
+
logBetterAuth('Request a protected route, redirecting to sign-in page');
|
|
353
|
+
const signInUrl = new URL('/signin', req.nextUrl.origin);
|
|
354
|
+
signInUrl.searchParams.set('callbackUrl', req.nextUrl.href);
|
|
355
|
+
const hl = req.nextUrl.searchParams.get('hl');
|
|
356
|
+
if (hl) {
|
|
357
|
+
signInUrl.searchParams.set('hl', hl);
|
|
358
|
+
logBetterAuth('Preserving locale to sign-in: hl=%s', hl);
|
|
359
|
+
}
|
|
360
|
+
return Response.redirect(signInUrl);
|
|
361
|
+
}
|
|
362
|
+
logBetterAuth('Request a free route but not login, allow visit without auth header');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return response;
|
|
366
|
+
};
|
|
367
|
+
|
|
307
368
|
logDefault('Middleware configuration: %O', {
|
|
308
369
|
enableAuthProtection: appEnv.ENABLE_AUTH_PROTECTION,
|
|
370
|
+
enableBetterAuth: authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH,
|
|
309
371
|
enableClerk: authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH,
|
|
310
372
|
enableNextAuth: authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH,
|
|
311
373
|
enableOIDC: oidcEnv.ENABLE_OIDC,
|
|
@@ -313,6 +375,8 @@ logDefault('Middleware configuration: %O', {
|
|
|
313
375
|
|
|
314
376
|
export default authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH
|
|
315
377
|
? clerkAuthMiddleware
|
|
316
|
-
: authEnv.
|
|
317
|
-
?
|
|
318
|
-
:
|
|
378
|
+
: authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH
|
|
379
|
+
? betterAuthMiddleware
|
|
380
|
+
: authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH
|
|
381
|
+
? nextAuthMiddleware
|
|
382
|
+
: defaultMiddleware;
|
|
@@ -5,6 +5,7 @@ import { fileEnv } from '@/envs/file';
|
|
|
5
5
|
import { imageEnv } from '@/envs/image';
|
|
6
6
|
import { knowledgeEnv } from '@/envs/knowledge';
|
|
7
7
|
import { langfuseEnv } from '@/envs/langfuse';
|
|
8
|
+
import { parseSSOProviders } from '@/libs/better-auth/utils/server';
|
|
8
9
|
import { parseSystemAgent } from '@/server/globalConfig/parseSystemAgent';
|
|
9
10
|
import { GlobalServerConfig } from '@/types/serverConfig';
|
|
10
11
|
import { cleanObject } from '@/utils/object';
|
|
@@ -13,6 +14,14 @@ import { genServerAiProvidersConfig } from './genServerAiProviderConfig';
|
|
|
13
14
|
import { parseAgentConfig } from './parseDefaultAgent';
|
|
14
15
|
import { parseFilesConfig } from './parseFilesConfig';
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Get Better-Auth SSO providers list
|
|
19
|
+
* Parses AUTH_SSO_PROVIDERS and returns enabled providers
|
|
20
|
+
*/
|
|
21
|
+
const getBetterAuthSSOProviders = () => {
|
|
22
|
+
return parseSSOProviders(authEnv.AUTH_SSO_PROVIDERS);
|
|
23
|
+
};
|
|
24
|
+
|
|
16
25
|
export const getServerGlobalConfig = async () => {
|
|
17
26
|
const { ACCESS_CODES, DEFAULT_AGENT_CONFIG } = getAppConfig();
|
|
18
27
|
|
|
@@ -63,7 +72,9 @@ export const getServerGlobalConfig = async () => {
|
|
|
63
72
|
image: cleanObject({
|
|
64
73
|
defaultImageNum: imageEnv.AI_IMAGE_DEFAULT_IMAGE_NUM,
|
|
65
74
|
}),
|
|
66
|
-
oAuthSSOProviders: authEnv.
|
|
75
|
+
oAuthSSOProviders: authEnv.NEXT_PUBLIC_ENABLE_BETTER_AUTH
|
|
76
|
+
? getBetterAuthSSOProviders()
|
|
77
|
+
: authEnv.NEXT_AUTH_SSO_PROVIDERS.trim().split(/[,,]/),
|
|
67
78
|
systemAgent: parseSystemAgent(appEnv.SYSTEM_AGENT),
|
|
68
79
|
telemetry: {
|
|
69
80
|
langfuse: langfuseEnv.ENABLE_LANGFUSE,
|
|
@@ -198,6 +198,10 @@ export const userRouter = router({
|
|
|
198
198
|
return ctx.userModel.updateUser({ avatar: input });
|
|
199
199
|
}),
|
|
200
200
|
|
|
201
|
+
updateFullName: userProcedure.input(z.string()).mutation(async ({ ctx, input }) => {
|
|
202
|
+
return ctx.userModel.updateUser({ fullName: input });
|
|
203
|
+
}),
|
|
204
|
+
|
|
201
205
|
updateGuide: userProcedure.input(UserGuideSchema).mutation(async ({ ctx, input }) => {
|
|
202
206
|
return ctx.userModel.updateGuide(input);
|
|
203
207
|
}),
|