@lobehub/lobehub 2.0.0-next.354 → 2.0.0-next.356
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/locales/en-US/plugin.json +3 -0
- package/locales/zh-CN/plugin.json +3 -0
- package/package.json +4 -6
- package/packages/builtin-tool-memory/src/client/Render/SearchUserMemory/index.tsx +3 -11
- package/packages/context-engine/src/engine/messages/MessagesEngine.ts +0 -13
- package/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts +0 -25
- package/packages/database/src/models/__tests__/topics/topic.create.test.ts +3 -3
- package/packages/database/src/schemas/nextauth.ts +7 -2
- 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/_layout/Sidebar/Topic/List/Item/index.tsx +1 -0
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +34 -21
- package/src/app/[variants]/(main)/agent/features/Conversation/ConversationArea.tsx +4 -0
- package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/List/Item/index.tsx +1 -0
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/InboxItem.tsx +19 -29
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/List.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx +1 -1
- 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/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/locales/default/plugin.ts +3 -0
- package/src/server/globalConfig/index.ts +1 -3
- package/src/server/modules/Mecha/ContextEngineering/__tests__/serverMessagesEngine.test.ts +0 -25
- 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/chat/chat.test.ts +19 -19
- package/src/services/chat/index.ts +8 -3
- package/src/services/chat/mecha/agentConfigResolver.test.ts +72 -55
- package/src/services/chat/mecha/agentConfigResolver.ts +28 -4
- package/src/services/chat/mecha/contextEngineering.test.ts +21 -14
- package/src/services/chat/mecha/contextEngineering.ts +12 -0
- package/src/services/chat/types.ts +7 -1
- package/src/services/user/index.test.ts +0 -14
- package/src/services/user/index.ts +0 -4
- package/src/store/chat/agents/createAgentExecutors.ts +15 -4
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +1 -0
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -2
- 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
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/* eslint-disable unicorn/prefer-top-level-await */
|
|
2
|
+
import { getMigrationMode } from './_internal/config';
|
|
3
|
+
import { db, pool, schema } from './_internal/db';
|
|
4
|
+
|
|
5
|
+
type ExpectedAccount = {
|
|
6
|
+
accountId: string;
|
|
7
|
+
providerId: string;
|
|
8
|
+
scope?: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ActualAccount = {
|
|
13
|
+
accountId: string;
|
|
14
|
+
providerId: string;
|
|
15
|
+
scope: string | null;
|
|
16
|
+
userId: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const MAX_SAMPLES = 5;
|
|
20
|
+
|
|
21
|
+
const formatDuration = (ms: number) => `${(ms / 1000).toFixed(1)}s`;
|
|
22
|
+
|
|
23
|
+
function buildAccountKey(account: { accountId: string; providerId: string; userId: string }) {
|
|
24
|
+
return `${account.userId}__${account.providerId}__${account.accountId}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function loadNextAuthAccounts() {
|
|
28
|
+
const rows = await db.select().from(schema.nextauthAccounts);
|
|
29
|
+
return rows;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadActualAccounts() {
|
|
33
|
+
const rows = await db
|
|
34
|
+
.select({
|
|
35
|
+
accountId: schema.account.accountId,
|
|
36
|
+
providerId: schema.account.providerId,
|
|
37
|
+
scope: schema.account.scope,
|
|
38
|
+
userId: schema.account.userId,
|
|
39
|
+
})
|
|
40
|
+
.from(schema.account);
|
|
41
|
+
|
|
42
|
+
return rows as ActualAccount[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildExpectedAccounts(nextauthAccounts: Awaited<ReturnType<typeof loadNextAuthAccounts>>) {
|
|
46
|
+
const expectedAccounts: ExpectedAccount[] = [];
|
|
47
|
+
|
|
48
|
+
for (const nextauthAccount of nextauthAccounts) {
|
|
49
|
+
expectedAccounts.push({
|
|
50
|
+
accountId: nextauthAccount.providerAccountId,
|
|
51
|
+
providerId: nextauthAccount.provider,
|
|
52
|
+
scope: nextauthAccount.scope ?? undefined,
|
|
53
|
+
userId: nextauthAccount.userId,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { expectedAccounts };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const startedAt = Date.now();
|
|
62
|
+
const mode = getMigrationMode();
|
|
63
|
+
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
66
|
+
console.log('║ NextAuth to Better Auth Verification Script ║');
|
|
67
|
+
console.log('╠════════════════════════════════════════════════════════════╣');
|
|
68
|
+
console.log(`║ Mode: ${mode.padEnd(48)}║`);
|
|
69
|
+
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
70
|
+
console.log('');
|
|
71
|
+
|
|
72
|
+
const [nextauthAccounts, actualAccounts] = await Promise.all([
|
|
73
|
+
loadNextAuthAccounts(),
|
|
74
|
+
loadActualAccounts(),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
console.log(`📦 [verify] Loaded nextauth_accounts=${nextauthAccounts.length}`);
|
|
78
|
+
|
|
79
|
+
const { expectedAccounts } = buildExpectedAccounts(nextauthAccounts);
|
|
80
|
+
|
|
81
|
+
console.log(`🧮 [verify] Expected accounts=${expectedAccounts.length}`);
|
|
82
|
+
console.log(`🗄️ [verify] DB snapshot: accounts=${actualAccounts.length}`);
|
|
83
|
+
|
|
84
|
+
const expectedAccountSet = new Set(expectedAccounts.map(buildAccountKey));
|
|
85
|
+
const actualAccountSet = new Set(actualAccounts.map(buildAccountKey));
|
|
86
|
+
|
|
87
|
+
let missingAccounts = 0;
|
|
88
|
+
const missingAccountSamples: string[] = [];
|
|
89
|
+
for (const account of expectedAccounts) {
|
|
90
|
+
const key = buildAccountKey(account);
|
|
91
|
+
if (!actualAccountSet.has(key)) {
|
|
92
|
+
missingAccounts += 1;
|
|
93
|
+
if (missingAccountSamples.length < MAX_SAMPLES) {
|
|
94
|
+
missingAccountSamples.push(`${account.providerId}/${account.accountId}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let unexpectedAccounts = 0;
|
|
100
|
+
const unexpectedAccountSamples: string[] = [];
|
|
101
|
+
for (const account of actualAccounts) {
|
|
102
|
+
const key = buildAccountKey(account);
|
|
103
|
+
if (!expectedAccountSet.has(key)) {
|
|
104
|
+
unexpectedAccounts += 1;
|
|
105
|
+
if (unexpectedAccountSamples.length < MAX_SAMPLES) {
|
|
106
|
+
unexpectedAccountSamples.push(`${account.providerId}/${account.accountId}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Provider counts
|
|
112
|
+
const expectedProviderCounts: Record<string, number> = {};
|
|
113
|
+
const actualProviderCounts: Record<string, number> = {};
|
|
114
|
+
|
|
115
|
+
for (const account of expectedAccounts) {
|
|
116
|
+
expectedProviderCounts[account.providerId] =
|
|
117
|
+
(expectedProviderCounts[account.providerId] ?? 0) + 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const account of actualAccounts) {
|
|
121
|
+
actualProviderCounts[account.providerId] = (actualProviderCounts[account.providerId] ?? 0) + 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const formatCounts = (counts: Record<string, number>) =>
|
|
125
|
+
Object.entries(counts)
|
|
126
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
127
|
+
.map(([providerId, count]) => `${providerId}=${count}`)
|
|
128
|
+
.join(', ');
|
|
129
|
+
|
|
130
|
+
console.log(
|
|
131
|
+
`📊 [verify] Expected provider counts: ${formatCounts(expectedProviderCounts) || 'n/a'}`,
|
|
132
|
+
);
|
|
133
|
+
console.log(
|
|
134
|
+
`📊 [verify] Actual provider counts: ${formatCounts(actualProviderCounts) || 'n/a'}`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Check for missing scope in actual accounts
|
|
138
|
+
let missingScopeNonCredential = 0;
|
|
139
|
+
const sampleMissingScope: string[] = [];
|
|
140
|
+
|
|
141
|
+
for (const account of actualAccounts) {
|
|
142
|
+
if (account.providerId !== 'credential' && !account.scope) {
|
|
143
|
+
// Find corresponding nextauth account to check if it had scope
|
|
144
|
+
const nextauthAccount = nextauthAccounts.find(
|
|
145
|
+
(na) => na.provider === account.providerId && na.providerAccountId === account.accountId,
|
|
146
|
+
);
|
|
147
|
+
if (nextauthAccount?.scope) {
|
|
148
|
+
missingScopeNonCredential += 1;
|
|
149
|
+
if (sampleMissingScope.length < MAX_SAMPLES) {
|
|
150
|
+
sampleMissingScope.push(`${account.providerId}/${account.accountId}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log('📋 [verify] Summary:');
|
|
158
|
+
console.log(
|
|
159
|
+
` - Missing accounts: ${missingAccounts} ${missingAccountSamples.length > 0 ? `(samples: ${missingAccountSamples.join(', ')})` : ''}`,
|
|
160
|
+
);
|
|
161
|
+
console.log(
|
|
162
|
+
` - Unexpected accounts: ${unexpectedAccounts} ${unexpectedAccountSamples.length > 0 ? `(samples: ${unexpectedAccountSamples.join(', ')})` : '(accounts not from nextauth)'}`,
|
|
163
|
+
);
|
|
164
|
+
console.log(
|
|
165
|
+
` - Missing scope (had in nextauth): ${missingScopeNonCredential} ${sampleMissingScope.length > 0 ? `(samples: ${sampleMissingScope.join(', ')})` : ''}`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
console.log('');
|
|
169
|
+
if (missingAccounts === 0) {
|
|
170
|
+
console.log(
|
|
171
|
+
`✅ Verification success! All nextauth accounts migrated. (${formatDuration(Date.now() - startedAt)})`,
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(
|
|
175
|
+
`⚠️ Verification completed with ${missingAccounts} missing accounts. (${formatDuration(Date.now() - startedAt)})`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
void main()
|
|
181
|
+
.catch((error) => {
|
|
182
|
+
console.log('');
|
|
183
|
+
console.error('❌ Verification failed:', error);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
})
|
|
186
|
+
.finally(async () => {
|
|
187
|
+
await pool.end();
|
|
188
|
+
});
|
package/scripts/prebuild.mts
CHANGED
|
@@ -9,10 +9,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
|
|
10
10
|
// Use createRequire for CommonJS module compatibility
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
|
-
const {
|
|
12
|
+
const { checkDeprecatedAuth } = require('./_shared/checkDeprecatedAuth.js');
|
|
13
13
|
|
|
14
14
|
const isDesktop = process.env.NEXT_PUBLIC_IS_DESKTOP_APP === '1';
|
|
15
15
|
const isBundleAnalyzer = process.env.ANALYZE === 'true' && process.env.CI === 'true';
|
|
16
|
+
const isServerDB = !!process.env.DATABASE_URL;
|
|
16
17
|
|
|
17
18
|
if (isDesktop) {
|
|
18
19
|
dotenvExpand.expand(dotenv.config({ path: '.env.desktop' }));
|
|
@@ -21,10 +22,40 @@ if (isDesktop) {
|
|
|
21
22
|
dotenvExpand.expand(dotenv.config());
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
const AUTH_SECRET_DOC_URL = 'https://lobehub.com/docs/self-hosting/environment-variables/auth#auth-secret';
|
|
26
|
+
const KEY_VAULTS_SECRET_DOC_URL = 'https://lobehub.com/docs/self-hosting/environment-variables/basic#key-vaults-secret';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check for required environment variables in server database mode
|
|
30
|
+
*/
|
|
31
|
+
const checkRequiredEnvVars = () => {
|
|
32
|
+
if (isDesktop || !isServerDB) return;
|
|
33
|
+
|
|
34
|
+
const missingVars: { docUrl: string; name: string }[] = [];
|
|
35
|
+
|
|
36
|
+
if (!process.env.AUTH_SECRET) {
|
|
37
|
+
missingVars.push({ docUrl: AUTH_SECRET_DOC_URL, name: 'AUTH_SECRET' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!process.env.KEY_VAULTS_SECRET) {
|
|
41
|
+
missingVars.push({ docUrl: KEY_VAULTS_SECRET_DOC_URL, name: 'KEY_VAULTS_SECRET' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (missingVars.length > 0) {
|
|
45
|
+
console.error('\n' + '═'.repeat(70));
|
|
46
|
+
console.error('❌ ERROR: Missing required environment variables!');
|
|
47
|
+
console.error('═'.repeat(70));
|
|
48
|
+
console.error('\nThe following environment variables are required for server database mode:\n');
|
|
49
|
+
for (const { name, docUrl } of missingVars) {
|
|
50
|
+
console.error(` • ${name}`);
|
|
51
|
+
console.error(` 📖 Documentation: ${docUrl}\n`);
|
|
52
|
+
}
|
|
53
|
+
console.error('Please configure these environment variables and redeploy.');
|
|
54
|
+
console.error('═'.repeat(70) + '\n');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
28
59
|
|
|
29
60
|
const getCommandVersion = (command: string): string | null => {
|
|
30
61
|
try {
|
|
@@ -58,13 +89,32 @@ const printEnvInfo = () => {
|
|
|
58
89
|
console.log(` VERCEL_PROJECT_PRODUCTION_URL: ${process.env.VERCEL_PROJECT_PRODUCTION_URL ?? '(not set)'}`);
|
|
59
90
|
console.log(` AUTH_EMAIL_VERIFICATION: ${process.env.AUTH_EMAIL_VERIFICATION ?? '(not set)'}`);
|
|
60
91
|
console.log(` ENABLE_MAGIC_LINK: ${process.env.ENABLE_MAGIC_LINK ?? '(not set)'}`);
|
|
61
|
-
console.log(` AUTH_SECRET: ${process.env.AUTH_SECRET ? '✓ set' : '✗ not set'}`);
|
|
62
|
-
console.log(` KEY_VAULTS_SECRET: ${process.env.KEY_VAULTS_SECRET ? '✓ set' : '✗ not set'}`);
|
|
63
92
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
console.log(`
|
|
67
|
-
|
|
93
|
+
// Check SSO providers configuration
|
|
94
|
+
const ssoProviders = process.env.AUTH_SSO_PROVIDERS;
|
|
95
|
+
console.log(` AUTH_SSO_PROVIDERS: ${ssoProviders ?? '(not set)'}`);
|
|
96
|
+
|
|
97
|
+
if (ssoProviders) {
|
|
98
|
+
const getEnvPrefix = (provider: string) => `AUTH_${provider.toUpperCase().replaceAll('-', '_')}`;
|
|
99
|
+
|
|
100
|
+
const providers = ssoProviders.split(/[,,]/).map(p => p.trim()).filter(Boolean);
|
|
101
|
+
const missingProviders: string[] = [];
|
|
102
|
+
|
|
103
|
+
for (const provider of providers) {
|
|
104
|
+
const envPrefix = getEnvPrefix(provider);
|
|
105
|
+
const hasEnvVar = Object.keys(process.env).some(key => key.startsWith(envPrefix));
|
|
106
|
+
if (!hasEnvVar) {
|
|
107
|
+
missingProviders.push(provider);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (missingProviders.length > 0) {
|
|
112
|
+
console.log('\n ⚠️ SSO Provider Configuration Warning:');
|
|
113
|
+
for (const provider of missingProviders) {
|
|
114
|
+
console.log(` - "${provider}" is configured but no ${getEnvPrefix(provider)}_* env vars found`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
68
118
|
|
|
69
119
|
console.log('─'.repeat(50));
|
|
70
120
|
};
|
|
@@ -160,8 +210,11 @@ export const runPrebuild = async (targetDir: string = 'src') => {
|
|
|
160
210
|
const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
|
|
161
211
|
|
|
162
212
|
if (isMainModule) {
|
|
163
|
-
// Check for deprecated
|
|
164
|
-
|
|
213
|
+
// Check for deprecated auth env vars first - fail fast if found
|
|
214
|
+
checkDeprecatedAuth();
|
|
215
|
+
|
|
216
|
+
// Check for required env vars in server database mode
|
|
217
|
+
checkRequiredEnvVars();
|
|
165
218
|
|
|
166
219
|
printEnvInfo();
|
|
167
220
|
// 执行删除操作
|
|
@@ -7,11 +7,11 @@ const { existsSync } = require('node:fs');
|
|
|
7
7
|
// Resolve shared module path for both local dev and Docker environments
|
|
8
8
|
// Local: scripts/serverLauncher/startServer.js -> scripts/_shared/...
|
|
9
9
|
// Docker: /app/startServer.js -> /app/scripts/_shared/...
|
|
10
|
-
const localPath = path.join(__dirname, '..', '_shared', '
|
|
11
|
-
const dockerPath = '/app/scripts/_shared/
|
|
10
|
+
const localPath = path.join(__dirname, '..', '_shared', 'checkDeprecatedAuth.js');
|
|
11
|
+
const dockerPath = '/app/scripts/_shared/checkDeprecatedAuth.js';
|
|
12
12
|
const sharedModulePath = existsSync(localPath) ? localPath : dockerPath;
|
|
13
13
|
|
|
14
|
-
const {
|
|
14
|
+
const { checkDeprecatedAuth } = require(sharedModulePath);
|
|
15
15
|
|
|
16
16
|
// Set file paths
|
|
17
17
|
const DB_MIGRATION_SCRIPT_PATH = '/app/docker.cjs';
|
|
@@ -139,8 +139,8 @@ const runServer = async () => {
|
|
|
139
139
|
|
|
140
140
|
// Main execution block
|
|
141
141
|
(async () => {
|
|
142
|
-
// Check for deprecated
|
|
143
|
-
|
|
142
|
+
// Check for deprecated auth env vars first - fail fast if found
|
|
143
|
+
checkDeprecatedAuth({ action: 'restart' });
|
|
144
144
|
|
|
145
145
|
console.log('🌐 DNS Server:', dns.getServers());
|
|
146
146
|
console.log('-------------------------------------');
|
|
@@ -1,32 +1,14 @@
|
|
|
1
|
+
import { toNextJsHandler } from 'better-auth/next-js';
|
|
1
2
|
import type { NextRequest } from 'next/server';
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import { auth } from '@/auth';
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
if (enableBetterAuth) {
|
|
7
|
-
const [{ toNextJsHandler }, { auth }] = await Promise.all([
|
|
8
|
-
import('better-auth/next-js'),
|
|
9
|
-
import('@/auth'),
|
|
10
|
-
]);
|
|
11
|
-
return toNextJsHandler(auth);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (enableNextAuth) {
|
|
15
|
-
const NextAuthNode = await import('@/libs/next-auth');
|
|
16
|
-
return NextAuthNode.default.handlers;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return { GET: undefined, POST: undefined };
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const handler = createHandler();
|
|
6
|
+
const handler = toNextJsHandler(auth);
|
|
23
7
|
|
|
24
8
|
export const GET = async (req: NextRequest) => {
|
|
25
|
-
|
|
26
|
-
return GET?.(req);
|
|
9
|
+
return handler.GET(req);
|
|
27
10
|
};
|
|
28
11
|
|
|
29
12
|
export const POST = async (req: NextRequest) => {
|
|
30
|
-
|
|
31
|
-
return POST?.(req);
|
|
13
|
+
return handler.POST(req);
|
|
32
14
|
};
|
|
@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server';
|
|
|
3
3
|
import { serverDB } from '@/database/server';
|
|
4
4
|
import { authEnv } from '@/envs/auth';
|
|
5
5
|
import { pino } from '@/libs/logger';
|
|
6
|
-
import {
|
|
6
|
+
import { WebhookUserService } from '@/server/services/webhookUser';
|
|
7
7
|
|
|
8
8
|
import { validateRequest } from './validateRequest';
|
|
9
9
|
|
|
@@ -19,13 +19,13 @@ export const POST = async (req: Request): Promise<NextResponse> => {
|
|
|
19
19
|
|
|
20
20
|
const { action, object } = payload;
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const webhookUserService = new WebhookUserService(serverDB);
|
|
23
23
|
switch (action) {
|
|
24
24
|
case 'update-user': {
|
|
25
|
-
return
|
|
25
|
+
return webhookUserService.safeUpdateUser(
|
|
26
26
|
{
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
accountId: object.id,
|
|
28
|
+
providerId: 'casdoor',
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
avatar: object?.avatar,
|
|
@@ -3,7 +3,7 @@ import { NextResponse } from 'next/server';
|
|
|
3
3
|
import { serverDB } from '@/database/server';
|
|
4
4
|
import { authEnv } from '@/envs/auth';
|
|
5
5
|
import { pino } from '@/libs/logger';
|
|
6
|
-
import {
|
|
6
|
+
import { WebhookUserService } from '@/server/services/webhookUser';
|
|
7
7
|
|
|
8
8
|
import { validateRequest } from './validateRequest';
|
|
9
9
|
|
|
@@ -21,13 +21,13 @@ export const POST = async (req: Request): Promise<NextResponse> => {
|
|
|
21
21
|
|
|
22
22
|
pino.trace(`logto webhook payload: ${{ data, event }}`);
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const webhookUserService = new WebhookUserService(serverDB);
|
|
25
25
|
switch (event) {
|
|
26
26
|
case 'User.Data.Updated': {
|
|
27
|
-
return
|
|
27
|
+
return webhookUserService.safeUpdateUser(
|
|
28
28
|
{
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
accountId: data.id,
|
|
30
|
+
providerId: 'logto',
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
avatar: data?.avatar,
|
|
@@ -38,9 +38,9 @@ export const POST = async (req: Request): Promise<NextResponse> => {
|
|
|
38
38
|
}
|
|
39
39
|
case 'User.SuspensionStatus.Updated': {
|
|
40
40
|
if (data.isSuspended) {
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
return webhookUserService.safeSignOutUser({
|
|
42
|
+
accountId: data.id,
|
|
43
|
+
providerId: 'logto',
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
return NextResponse.json({ message: 'user reactivated', success: true }, { status: 200 });
|
|
@@ -24,10 +24,17 @@ vi.mock('@/envs/auth', async (importOriginal) => {
|
|
|
24
24
|
const actual = await importOriginal<typeof import('@/envs/auth')>();
|
|
25
25
|
return {
|
|
26
26
|
...actual,
|
|
27
|
-
enableBetterAuth: false,
|
|
28
27
|
};
|
|
29
28
|
});
|
|
30
29
|
|
|
30
|
+
vi.mock('@/auth', () => ({
|
|
31
|
+
auth: {
|
|
32
|
+
api: {
|
|
33
|
+
getSession: vi.fn().mockResolvedValue(null),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
31
38
|
describe('checkAuth', () => {
|
|
32
39
|
const mockHandler: RequestHandler = vi.fn();
|
|
33
40
|
const mockRequest = new Request('https://example.com');
|
|
@@ -6,14 +6,10 @@ import {
|
|
|
6
6
|
import { ChatErrorType, type ClientSecretPayload } from '@lobechat/types';
|
|
7
7
|
import { getXorPayload } from '@lobechat/utils/server';
|
|
8
8
|
|
|
9
|
+
import { auth } from '@/auth';
|
|
9
10
|
import { getServerDB } from '@/database/core/db-adaptor';
|
|
10
11
|
import { type LobeChatDatabase } from '@/database/type';
|
|
11
|
-
import {
|
|
12
|
-
LOBE_CHAT_AUTH_HEADER,
|
|
13
|
-
LOBE_CHAT_OIDC_AUTH_HEADER,
|
|
14
|
-
OAUTH_AUTHORIZED,
|
|
15
|
-
enableBetterAuth,
|
|
16
|
-
} from '@/envs/auth';
|
|
12
|
+
import { LOBE_CHAT_AUTH_HEADER, LOBE_CHAT_OIDC_AUTH_HEADER, OAUTH_AUTHORIZED } from '@/envs/auth';
|
|
17
13
|
import { validateOIDCJWT } from '@/libs/oidc-provider/jwt';
|
|
18
14
|
import { createErrorResponse } from '@/utils/errorResponse';
|
|
19
15
|
|
|
@@ -58,18 +54,13 @@ export const checkAuth =
|
|
|
58
54
|
// get Authorization from header
|
|
59
55
|
const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER);
|
|
60
56
|
const oauthAuthorized = !!req.headers.get(OAUTH_AUTHORIZED);
|
|
61
|
-
let betterAuthAuthorized = false;
|
|
62
57
|
|
|
63
58
|
// better auth handler
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const session = await betterAuth.api.getSession({
|
|
68
|
-
headers: req.headers,
|
|
69
|
-
});
|
|
59
|
+
const session = await auth.api.getSession({
|
|
60
|
+
headers: req.headers,
|
|
61
|
+
});
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
}
|
|
63
|
+
const betterAuthAuthorized = !!session?.user?.id;
|
|
73
64
|
|
|
74
65
|
if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
|
75
66
|
|
|
@@ -2,49 +2,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
import { checkAuthMethod } from './utils';
|
|
4
4
|
|
|
5
|
-
let enableNextAuthMock = false;
|
|
6
|
-
let enableBetterAuthMock = false;
|
|
7
|
-
|
|
8
|
-
vi.mock('@/envs/auth', async (importOriginal) => {
|
|
9
|
-
const data = await importOriginal();
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
...(data as any),
|
|
13
|
-
get enableBetterAuth() {
|
|
14
|
-
return enableBetterAuthMock;
|
|
15
|
-
},
|
|
16
|
-
get enableNextAuth() {
|
|
17
|
-
return enableNextAuthMock;
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
});
|
|
21
|
-
|
|
22
5
|
describe('checkAuthMethod', () => {
|
|
23
6
|
beforeEach(() => {
|
|
24
7
|
vi.clearAllMocks();
|
|
25
8
|
});
|
|
26
9
|
|
|
27
|
-
it('should pass with valid Next auth', () => {
|
|
28
|
-
enableNextAuthMock = true;
|
|
29
|
-
expect(() =>
|
|
30
|
-
checkAuthMethod({
|
|
31
|
-
nextAuthAuthorized: true,
|
|
32
|
-
}),
|
|
33
|
-
).not.toThrow();
|
|
34
|
-
|
|
35
|
-
enableNextAuthMock = false;
|
|
36
|
-
});
|
|
37
|
-
|
|
38
10
|
it('should pass with valid Better Auth session', () => {
|
|
39
|
-
enableBetterAuthMock = true;
|
|
40
|
-
|
|
41
11
|
expect(() =>
|
|
42
12
|
checkAuthMethod({
|
|
43
13
|
betterAuthAuthorized: true,
|
|
44
14
|
}),
|
|
45
15
|
).not.toThrow();
|
|
46
|
-
|
|
47
|
-
enableBetterAuthMock = false;
|
|
48
16
|
});
|
|
49
17
|
|
|
50
18
|
it('should pass with valid API key', () => {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { enableBetterAuth, enableNextAuth } from '@/envs/auth';
|
|
2
|
-
|
|
3
1
|
interface CheckAuthParams {
|
|
4
2
|
apiKey?: string;
|
|
5
3
|
betterAuthAuthorized?: boolean;
|
|
@@ -11,16 +9,13 @@ interface CheckAuthParams {
|
|
|
11
9
|
* @param {CheckAuthParams} params - Authentication parameters extracted from headers.
|
|
12
10
|
* @param {string} [params.apiKey] - The user API key.
|
|
13
11
|
* @param {boolean} [params.betterAuthAuthorized] - Whether the Better Auth session exists.
|
|
14
|
-
* @param {boolean} [params.nextAuthAuthorized] - Whether the OAuth 2 header is provided.
|
|
12
|
+
* @param {boolean} [params.nextAuthAuthorized] - Whether the OAuth 2 header is provided (legacy, kept for compatibility).
|
|
15
13
|
*/
|
|
16
14
|
export const checkAuthMethod = (params: CheckAuthParams) => {
|
|
17
|
-
const { apiKey, betterAuthAuthorized
|
|
15
|
+
const { apiKey, betterAuthAuthorized } = params;
|
|
18
16
|
|
|
19
17
|
// if better auth session exists
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
// if next auth handler is provided
|
|
23
|
-
if (enableNextAuth && nextAuthAuthorized) return;
|
|
18
|
+
if (betterAuthAuthorized) return;
|
|
24
19
|
|
|
25
20
|
// if apiKey exist
|
|
26
21
|
if (apiKey) return;
|
|
@@ -27,10 +27,17 @@ vi.mock('@/envs/auth', async (importOriginal) => {
|
|
|
27
27
|
const actual = await importOriginal<typeof import('@/envs/auth')>();
|
|
28
28
|
return {
|
|
29
29
|
...actual,
|
|
30
|
-
enableBetterAuth: false,
|
|
31
30
|
};
|
|
32
31
|
});
|
|
33
32
|
|
|
33
|
+
vi.mock('@/auth', () => ({
|
|
34
|
+
auth: {
|
|
35
|
+
api: {
|
|
36
|
+
getSession: vi.fn().mockResolvedValue(null),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
34
41
|
// 模拟请求和响应
|
|
35
42
|
let request: Request;
|
|
36
43
|
beforeEach(() => {
|
|
@@ -21,7 +21,6 @@ const handler = async (req: Request, { jwtPayload }: { jwtPayload?: any }) => {
|
|
|
21
21
|
|
|
22
22
|
const caller = createCaller({
|
|
23
23
|
jwtPayload,
|
|
24
|
-
nextAuth: undefined, // WebAPI routes don't have nextAuth session
|
|
25
24
|
userId: jwtPayload?.userId, // Required for userAuth middleware
|
|
26
25
|
});
|
|
27
26
|
|
|
@@ -21,10 +21,17 @@ vi.mock('@/envs/auth', async (importOriginal) => {
|
|
|
21
21
|
const actual = await importOriginal<typeof import('@/envs/auth')>();
|
|
22
22
|
return {
|
|
23
23
|
...actual,
|
|
24
|
-
enableBetterAuth: false,
|
|
25
24
|
};
|
|
26
25
|
});
|
|
27
26
|
|
|
27
|
+
vi.mock('@/auth', () => ({
|
|
28
|
+
auth: {
|
|
29
|
+
api: {
|
|
30
|
+
getSession: vi.fn().mockResolvedValue(null),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
28
35
|
vi.mock('@/server/modules/ModelRuntime', () => ({
|
|
29
36
|
initModelRuntimeFromDB: vi.fn(),
|
|
30
37
|
}));
|
|
@@ -7,7 +7,7 @@ import { ChevronRight, Mail } from 'lucide-react';
|
|
|
7
7
|
import { useEffect, useRef } from 'react';
|
|
8
8
|
import { Trans, useTranslation } from 'react-i18next';
|
|
9
9
|
|
|
10
|
-
import AuthIcons from '@/components/
|
|
10
|
+
import AuthIcons from '@/components/AuthIcons';
|
|
11
11
|
import { PRIVACY_URL, TERMS_URL } from '@/const/url';
|
|
12
12
|
|
|
13
13
|
import AuthCard from '../../../../features/AuthCard';
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { enableBetterAuth } from '@/envs/auth';
|
|
2
|
-
import { notFound } from '@/libs/next/navigation';
|
|
3
1
|
import { metadataModule } from '@/server/metadata';
|
|
4
2
|
import { translation } from '@/server/translation';
|
|
5
3
|
import { type DynamicLayoutProps } from '@/types/next';
|
|
@@ -9,28 +7,17 @@ import BetterAuthSignUpForm from './BetterAuthSignUpForm';
|
|
|
9
7
|
|
|
10
8
|
export const generateMetadata = async (props: DynamicLayoutProps) => {
|
|
11
9
|
const locale = await RouteVariants.getLocale(props);
|
|
12
|
-
|
|
13
|
-
if (enableBetterAuth) {
|
|
14
|
-
const { t } = await translation('auth', locale);
|
|
15
|
-
return metadataModule.generate({
|
|
16
|
-
description: t('betterAuth.signup.subtitle'),
|
|
17
|
-
title: t('betterAuth.signup.title'),
|
|
18
|
-
url: '/signup',
|
|
19
|
-
});
|
|
20
|
-
}
|
|
10
|
+
const { t } = await translation('auth', locale);
|
|
21
11
|
|
|
22
12
|
return metadataModule.generate({
|
|
23
|
-
|
|
13
|
+
description: t('betterAuth.signup.subtitle'),
|
|
14
|
+
title: t('betterAuth.signup.title'),
|
|
24
15
|
url: '/signup',
|
|
25
16
|
});
|
|
26
17
|
};
|
|
27
18
|
|
|
28
19
|
const Page = () => {
|
|
29
|
-
|
|
30
|
-
return <BetterAuthSignUpForm />;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return notFound();
|
|
20
|
+
return <BetterAuthSignUpForm />;
|
|
34
21
|
};
|
|
35
22
|
|
|
36
23
|
export default Page;
|
|
@@ -110,6 +110,7 @@ const TopicItem = memo<TopicItemProps>(({ id, title, fav, active, threadId }) =>
|
|
|
110
110
|
fill={fav ? cssVar.colorWarning : 'transparent'}
|
|
111
111
|
icon={Star}
|
|
112
112
|
onClick={(e) => {
|
|
113
|
+
e.preventDefault();
|
|
113
114
|
e.stopPropagation();
|
|
114
115
|
favoriteTopic(id, !fav);
|
|
115
116
|
}}
|