@lobehub/chat 0.162.24 → 0.163.0
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/.github/workflows/release.yml +21 -2
- package/.github/workflows/sync.yml +1 -1
- package/.github/workflows/test.yml +35 -4
- package/CHANGELOG.md +50 -0
- package/LICENSE +38 -21
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/codecov.yml +11 -0
- package/docs/self-hosting/platform/zeabur.mdx +1 -1
- package/docs/self-hosting/platform/zeabur.zh-CN.mdx +1 -1
- package/drizzle.config.ts +29 -0
- package/next.config.mjs +3 -0
- package/package.json +25 -5
- package/scripts/migrateServerDB/index.ts +30 -0
- package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +2 -1
- package/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +95 -88
- package/src/app/(main)/chat/settings/features/HeaderContent.tsx +37 -31
- package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +11 -1
- package/src/app/api/middleware/auth/index.ts +1 -1
- package/src/app/api/webhooks/clerk/__tests__/fixtures/createUser.json +73 -0
- package/src/app/api/webhooks/clerk/route.ts +159 -0
- package/src/app/api/webhooks/clerk/validateRequest.ts +22 -0
- package/src/app/trpc/edge/[trpc]/route.ts +1 -1
- package/src/app/trpc/lambda/[trpc]/route.ts +26 -0
- package/src/config/auth.ts +2 -0
- package/src/config/db.ts +13 -1
- package/src/database/server/core/db.ts +44 -0
- package/src/database/server/core/dbForTest.ts +45 -0
- package/src/database/server/index.ts +1 -0
- package/src/database/server/migrations/0000_init.sql +439 -0
- package/src/database/server/migrations/0001_add_client_id.sql +9 -0
- package/src/database/server/migrations/0002_amusing_puma.sql +9 -0
- package/src/database/server/migrations/meta/0000_snapshot.json +1583 -0
- package/src/database/server/migrations/meta/0001_snapshot.json +1636 -0
- package/src/database/server/migrations/meta/0002_snapshot.json +1630 -0
- package/src/database/server/migrations/meta/_journal.json +27 -0
- package/src/database/server/models/__tests__/file.test.ts +140 -0
- package/src/database/server/models/__tests__/message.test.ts +847 -0
- package/src/database/server/models/__tests__/plugin.test.ts +172 -0
- package/src/database/server/models/__tests__/session.test.ts +595 -0
- package/src/database/server/models/__tests__/topic.test.ts +623 -0
- package/src/database/server/models/__tests__/user.test.ts +173 -0
- package/src/database/server/models/_template.ts +44 -0
- package/src/database/server/models/file.ts +51 -0
- package/src/database/server/models/message.ts +378 -0
- package/src/database/server/models/plugin.ts +63 -0
- package/src/database/server/models/session.ts +290 -0
- package/src/database/server/models/sessionGroup.ts +69 -0
- package/src/database/server/models/topic.ts +265 -0
- package/src/database/server/models/user.ts +138 -0
- package/src/database/server/modules/DataImporter/__tests__/fixtures/messages.json +1101 -0
- package/src/database/server/modules/DataImporter/__tests__/index.test.ts +954 -0
- package/src/database/server/modules/DataImporter/index.ts +333 -0
- package/src/database/server/schemas/_id.ts +15 -0
- package/src/database/server/schemas/lobechat.ts +601 -0
- package/src/database/server/utils/idGenerator.test.ts +39 -0
- package/src/database/server/utils/idGenerator.ts +26 -0
- package/src/features/AgentSetting/AgentModal/index.tsx +6 -7
- package/src/features/User/UserPanel/useMenu.tsx +43 -37
- package/src/libs/trpc/client.ts +52 -3
- package/src/server/files/s3.ts +21 -1
- package/src/server/keyVaultsEncrypt/index.test.ts +62 -0
- package/src/server/keyVaultsEncrypt/index.ts +93 -0
- package/src/server/mock.ts +1 -1
- package/src/server/routers/{index.ts → edge/index.ts} +3 -3
- package/src/server/routers/lambda/file.ts +49 -0
- package/src/server/routers/lambda/importer.ts +54 -0
- package/src/server/routers/lambda/index.ts +28 -0
- package/src/server/routers/lambda/message.ts +165 -0
- package/src/server/routers/lambda/plugin.ts +100 -0
- package/src/server/routers/lambda/session.ts +194 -0
- package/src/server/routers/lambda/sessionGroup.ts +77 -0
- package/src/server/routers/lambda/topic.ts +134 -0
- package/src/server/routers/lambda/user.ts +57 -0
- package/src/services/file/index.ts +4 -7
- package/src/services/file/server.ts +45 -0
- package/src/services/import/index.ts +4 -1
- package/src/services/import/server.ts +115 -0
- package/src/services/message/index.ts +4 -8
- package/src/services/message/server.ts +93 -0
- package/src/services/plugin/index.ts +4 -9
- package/src/services/plugin/server.ts +46 -0
- package/src/services/session/index.ts +4 -8
- package/src/services/session/server.ts +148 -0
- package/src/services/topic/index.ts +4 -9
- package/src/services/topic/server.ts +68 -0
- package/src/services/user/index.ts +4 -9
- package/src/services/user/server.ts +28 -0
- package/src/store/user/slices/modelList/selectors/keyVaults.test.ts +201 -0
- package/src/store/user/slices/modelList/selectors/keyVaults.ts +15 -3
- package/src/store/user/slices/modelList/selectors/modelConfig.test.ts +29 -1
- package/src/store/user/slices/modelList/selectors/modelConfig.ts +21 -1
- package/src/types/user/settings/keyVaults.ts +1 -1
- package/tests/setup-db.ts +7 -0
- package/vitest.config.ts +2 -1
- package/vitest.server.config.ts +23 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ActionIcon, Icon } from '@lobehub/ui';
|
|
2
|
-
import { App, Dropdown
|
|
2
|
+
import { App, Dropdown } from 'antd';
|
|
3
3
|
import { createStyles } from 'antd-style';
|
|
4
|
+
import { ItemType } from 'antd/es/menu/interface';
|
|
4
5
|
import isEqual from 'fast-deep-equal';
|
|
5
6
|
import {
|
|
6
7
|
Check,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
import { memo, useMemo } from 'react';
|
|
17
18
|
import { useTranslation } from 'react-i18next';
|
|
18
19
|
|
|
20
|
+
import { isServerMode } from '@/const/version';
|
|
19
21
|
import { configService } from '@/services/config';
|
|
20
22
|
import { useSessionStore } from '@/store/session';
|
|
21
23
|
import { sessionHelpers } from '@/store/session/helpers';
|
|
@@ -58,108 +60,113 @@ const Actions = memo<ActionProps>(({ group, id, openCreateGroupModal, setOpen })
|
|
|
58
60
|
const isDefault = group === SessionDefaultGroup.Default;
|
|
59
61
|
// const hasDivider = !isDefault || Object.keys(sessionByGroup).length > 0;
|
|
60
62
|
|
|
61
|
-
const items
|
|
62
|
-
() =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
icon: <Icon icon={LucideCopy} />,
|
|
73
|
-
key: 'duplicate',
|
|
74
|
-
label: t('duplicate', { ns: 'common' }),
|
|
75
|
-
onClick: ({ domEvent }) => {
|
|
76
|
-
domEvent.stopPropagation();
|
|
77
|
-
|
|
78
|
-
duplicateSession(id);
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
type: 'divider',
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
children: [
|
|
86
|
-
...sessionCustomGroups.map(({ id: groupId, name }) => ({
|
|
87
|
-
icon: group === groupId ? <Icon icon={Check} /> : <div />,
|
|
88
|
-
key: groupId,
|
|
89
|
-
label: name,
|
|
63
|
+
const items = useMemo(
|
|
64
|
+
() =>
|
|
65
|
+
(
|
|
66
|
+
[
|
|
67
|
+
{
|
|
68
|
+
icon: <Icon icon={pin ? PinOff : Pin} />,
|
|
69
|
+
key: 'pin',
|
|
70
|
+
label: t(pin ? 'pinOff' : 'pin'),
|
|
90
71
|
onClick: () => {
|
|
91
|
-
|
|
72
|
+
pinSession(id, !pin);
|
|
92
73
|
},
|
|
93
|
-
}
|
|
74
|
+
},
|
|
94
75
|
{
|
|
95
|
-
icon:
|
|
96
|
-
key: '
|
|
97
|
-
label: t('
|
|
98
|
-
onClick: () => {
|
|
99
|
-
|
|
76
|
+
icon: <Icon icon={LucideCopy} />,
|
|
77
|
+
key: 'duplicate',
|
|
78
|
+
label: t('duplicate', { ns: 'common' }),
|
|
79
|
+
onClick: ({ domEvent }) => {
|
|
80
|
+
domEvent.stopPropagation();
|
|
81
|
+
|
|
82
|
+
duplicateSession(id);
|
|
100
83
|
},
|
|
101
84
|
},
|
|
102
85
|
{
|
|
103
86
|
type: 'divider',
|
|
104
87
|
},
|
|
105
88
|
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
89
|
+
children: [
|
|
90
|
+
...sessionCustomGroups.map(({ id: groupId, name }) => ({
|
|
91
|
+
icon: group === groupId ? <Icon icon={Check} /> : <div />,
|
|
92
|
+
key: groupId,
|
|
93
|
+
label: name,
|
|
94
|
+
onClick: () => {
|
|
95
|
+
updateSessionGroup(id, groupId);
|
|
96
|
+
},
|
|
97
|
+
})),
|
|
98
|
+
{
|
|
99
|
+
icon: isDefault ? <Icon icon={Check} /> : <div />,
|
|
100
|
+
key: 'defaultList',
|
|
101
|
+
label: t('defaultList'),
|
|
102
|
+
onClick: () => {
|
|
103
|
+
updateSessionGroup(id, SessionDefaultGroup.Default);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'divider',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
icon: <Icon icon={LucidePlus} />,
|
|
111
|
+
key: 'createGroup',
|
|
112
|
+
label: <div>{t('sessionGroup.createGroup')}</div>,
|
|
113
|
+
onClick: ({ domEvent }) => {
|
|
114
|
+
domEvent.stopPropagation();
|
|
115
|
+
openCreateGroupModal();
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
icon: <Icon icon={ListTree} />,
|
|
120
|
+
key: 'moveGroup',
|
|
121
|
+
label: t('sessionGroup.moveGroup'),
|
|
113
122
|
},
|
|
114
|
-
],
|
|
115
|
-
icon: <Icon icon={ListTree} />,
|
|
116
|
-
key: 'moveGroup',
|
|
117
|
-
label: t('sessionGroup.moveGroup'),
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
type: 'divider',
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
children: [
|
|
124
123
|
{
|
|
125
|
-
|
|
126
|
-
label: t('exportType.agent', { ns: 'common' }),
|
|
127
|
-
onClick: () => {
|
|
128
|
-
configService.exportSingleAgent(id);
|
|
129
|
-
},
|
|
124
|
+
type: 'divider',
|
|
130
125
|
},
|
|
126
|
+
isServerMode
|
|
127
|
+
? undefined
|
|
128
|
+
: {
|
|
129
|
+
children: [
|
|
130
|
+
{
|
|
131
|
+
key: 'agent',
|
|
132
|
+
label: t('exportType.agent', { ns: 'common' }),
|
|
133
|
+
onClick: () => {
|
|
134
|
+
configService.exportSingleAgent(id);
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
key: 'agentWithMessage',
|
|
139
|
+
label: t('exportType.agentWithMessage', { ns: 'common' }),
|
|
140
|
+
onClick: () => {
|
|
141
|
+
configService.exportSingleSession(id);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
icon: <Icon icon={HardDriveDownload} />,
|
|
146
|
+
key: 'export',
|
|
147
|
+
label: t('export', { ns: 'common' }),
|
|
148
|
+
},
|
|
131
149
|
{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
danger: true,
|
|
151
|
+
icon: <Icon icon={Trash} />,
|
|
152
|
+
key: 'delete',
|
|
153
|
+
label: t('delete', { ns: 'common' }),
|
|
154
|
+
onClick: ({ domEvent }) => {
|
|
155
|
+
domEvent.stopPropagation();
|
|
156
|
+
modal.confirm({
|
|
157
|
+
centered: true,
|
|
158
|
+
okButtonProps: { danger: true },
|
|
159
|
+
onOk: async () => {
|
|
160
|
+
await removeSession(id);
|
|
161
|
+
message.success(t('confirmRemoveSessionSuccess'));
|
|
162
|
+
},
|
|
163
|
+
rootClassName: styles.modalRoot,
|
|
164
|
+
title: t('confirmRemoveSessionItemAlert'),
|
|
165
|
+
});
|
|
136
166
|
},
|
|
137
167
|
},
|
|
138
|
-
]
|
|
139
|
-
|
|
140
|
-
key: 'export',
|
|
141
|
-
label: t('export', { ns: 'common' }),
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
danger: true,
|
|
145
|
-
icon: <Icon icon={Trash} />,
|
|
146
|
-
key: 'delete',
|
|
147
|
-
label: t('delete', { ns: 'common' }),
|
|
148
|
-
onClick: ({ domEvent }) => {
|
|
149
|
-
domEvent.stopPropagation();
|
|
150
|
-
modal.confirm({
|
|
151
|
-
centered: true,
|
|
152
|
-
okButtonProps: { danger: true },
|
|
153
|
-
onOk: async () => {
|
|
154
|
-
await removeSession(id);
|
|
155
|
-
message.success(t('confirmRemoveSessionSuccess'));
|
|
156
|
-
},
|
|
157
|
-
rootClassName: styles.modalRoot,
|
|
158
|
-
title: t('confirmRemoveSessionItemAlert'),
|
|
159
|
-
});
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
],
|
|
168
|
+
] as ItemType[]
|
|
169
|
+
).filter(Boolean),
|
|
163
170
|
[id, pin],
|
|
164
171
|
);
|
|
165
172
|
|
|
@@ -5,6 +5,7 @@ import { memo, useMemo } from 'react';
|
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
|
|
7
7
|
import { HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
|
8
|
+
import { isServerMode } from '@/const/version';
|
|
8
9
|
import { configService } from '@/services/config';
|
|
9
10
|
import { useServerConfigStore } from '@/store/serverConfig';
|
|
10
11
|
import { useSessionStore } from '@/store/session';
|
|
@@ -18,45 +19,50 @@ export const HeaderContent = memo<{ mobile?: boolean; modal?: boolean }>(({ moda
|
|
|
18
19
|
const mobile = useServerConfigStore((s) => s.isMobile);
|
|
19
20
|
|
|
20
21
|
const items = useMemo<MenuProps['items']>(
|
|
21
|
-
() =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
() =>
|
|
23
|
+
isServerMode
|
|
24
|
+
? []
|
|
25
|
+
: [
|
|
26
|
+
{
|
|
27
|
+
key: 'agent',
|
|
28
|
+
label: <div>{t('exportType.agent', { ns: 'common' })}</div>,
|
|
29
|
+
onClick: () => {
|
|
30
|
+
if (!id) return;
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
configService.exportSingleAgent(id);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'agentWithMessage',
|
|
37
|
+
label: <div>{t('exportType.agentWithMessage', { ns: 'common' })}</div>,
|
|
38
|
+
onClick: () => {
|
|
39
|
+
if (!id) return;
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
configService.exportSingleSession(id);
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
41
45
|
[],
|
|
42
46
|
);
|
|
43
47
|
|
|
44
48
|
return (
|
|
45
49
|
<>
|
|
46
50
|
<SubmitAgentButton modal={modal} />
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
{!isServerMode && (
|
|
52
|
+
<Dropdown arrow={false} menu={{ items }} trigger={['click']}>
|
|
53
|
+
{modal ? (
|
|
54
|
+
<Button block icon={<Icon icon={HardDriveDownload} />}>
|
|
55
|
+
{t('export', { ns: 'common' })}
|
|
56
|
+
</Button>
|
|
57
|
+
) : (
|
|
58
|
+
<ActionIcon
|
|
59
|
+
icon={HardDriveDownload}
|
|
60
|
+
size={HEADER_ICON_SIZE(mobile)}
|
|
61
|
+
title={t('export', { ns: 'common' })}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
</Dropdown>
|
|
65
|
+
)}
|
|
60
66
|
</>
|
|
61
67
|
);
|
|
62
68
|
});
|
|
@@ -87,12 +87,14 @@ const ProviderConfig = memo<ProviderConfigProps>(
|
|
|
87
87
|
enabled,
|
|
88
88
|
isFetchOnClient,
|
|
89
89
|
isProviderEndpointNotEmpty,
|
|
90
|
+
isProviderApiKeyNotEmpty,
|
|
90
91
|
] = useUserStore((s) => [
|
|
91
92
|
s.toggleProviderEnabled,
|
|
92
93
|
s.setSettings,
|
|
93
94
|
modelConfigSelectors.isProviderEnabled(id)(s),
|
|
94
95
|
modelConfigSelectors.isProviderFetchOnClient(id)(s),
|
|
95
96
|
keyVaultsConfigSelectors.isProviderEndpointNotEmpty(id)(s),
|
|
97
|
+
keyVaultsConfigSelectors.isProviderApiKeyNotEmpty(id)(s),
|
|
96
98
|
]);
|
|
97
99
|
|
|
98
100
|
useSyncSettings(form);
|
|
@@ -122,7 +124,15 @@ const ProviderConfig = memo<ProviderConfigProps>(
|
|
|
122
124
|
label: proxyUrl?.title || t('llm.proxyUrl.title'),
|
|
123
125
|
name: [KeyVaultsConfigKey, id, LLMProviderBaseUrlKey],
|
|
124
126
|
},
|
|
125
|
-
|
|
127
|
+
/*
|
|
128
|
+
* Conditions to show Client Fetch Switch
|
|
129
|
+
* 1. Component props
|
|
130
|
+
* 2. Provider allow to edit endpoint and the value of endpoint is not empty
|
|
131
|
+
* 3. There is an apikey provided by user
|
|
132
|
+
*/
|
|
133
|
+
(showBrowserRequest ||
|
|
134
|
+
(showEndpoint && isProviderEndpointNotEmpty) ||
|
|
135
|
+
(showApiKey && isProviderApiKeyNotEmpty)) && {
|
|
126
136
|
children: (
|
|
127
137
|
<Switch
|
|
128
138
|
onChange={(enabled) => {
|
|
@@ -10,7 +10,7 @@ import { ChatErrorType } from '@/types/fetch';
|
|
|
10
10
|
import { checkAuthMethod, getJWTPayload } from './utils';
|
|
11
11
|
|
|
12
12
|
type CreateRuntime = (jwtPayload: JWTPayload) => AgentRuntime;
|
|
13
|
-
type RequestOptions = { createRuntime?: CreateRuntime
|
|
13
|
+
type RequestOptions = { createRuntime?: CreateRuntime; params: { provider: string } };
|
|
14
14
|
|
|
15
15
|
export type RequestHandler = (
|
|
16
16
|
req: Request,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"backup_code_enabled": false,
|
|
3
|
+
"banned": false,
|
|
4
|
+
"create_organization_enabled": true,
|
|
5
|
+
"created_at": 1713709987911,
|
|
6
|
+
"delete_self_enabled": true,
|
|
7
|
+
"email_addresses": [
|
|
8
|
+
{
|
|
9
|
+
"created_at": 1713709977919,
|
|
10
|
+
"email_address": "arvinx@foxmail.com",
|
|
11
|
+
"id": "idn_2fPkD9X1lfzSn5lJVDGyochYq8k",
|
|
12
|
+
"linked_to": [],
|
|
13
|
+
"object": "email_address",
|
|
14
|
+
"reserved": false,
|
|
15
|
+
"updated_at": 1713709987951,
|
|
16
|
+
"verification": []
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"external_accounts": [
|
|
20
|
+
{
|
|
21
|
+
"approved_scopes": "read:user user:email",
|
|
22
|
+
"avatar_url": "https://avatars.githubusercontent.com/u/28616219?v=4",
|
|
23
|
+
"created_at": 1713709542104,
|
|
24
|
+
"email_address": "arvinx@foxmail.com",
|
|
25
|
+
"first_name": "Arvin",
|
|
26
|
+
"id": "eac_2fPjKROeJ1bBs8Uxa6RFMxKogTB",
|
|
27
|
+
"identification_id": "idn_2fPjyV3sqtQJZUbEzdK2y23a1bq",
|
|
28
|
+
"image_url": "https://img.clerk.com/eyJ0eXBlIjoicHJveHkiLCJzcmMiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMjg2MTYyMTk/dj00IiwicyI6IkhCeHE5NmdlRk85ekRxMjJlR05EalUrbVFBbmVDZjRVQkpwNGYxcW5JajQifQ",
|
|
29
|
+
"label": null,
|
|
30
|
+
"last_name": "Xu",
|
|
31
|
+
"object": "external_account",
|
|
32
|
+
"provider": "oauth_github",
|
|
33
|
+
"provider_user_id": "28616219",
|
|
34
|
+
"public_metadata": {},
|
|
35
|
+
"updated_at": 1713709542104,
|
|
36
|
+
"username": "arvinxx",
|
|
37
|
+
"verification": {
|
|
38
|
+
"attempts": null,
|
|
39
|
+
"expire_at": 1713710140131,
|
|
40
|
+
"status": "verified",
|
|
41
|
+
"strategy": "oauth_github"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"external_id": null,
|
|
46
|
+
"first_name": "Arvin",
|
|
47
|
+
"has_image": true,
|
|
48
|
+
"id": "user_2fPkELglwI48WpZVwwdAxBKBPK6",
|
|
49
|
+
"image_url": "https://img.clerk.com/eyJ0eXBlIjoicHJveHkiLCJzcmMiOiJodHRwczovL2ltYWdlcy5jbGVyay5kZXYvb2F1dGhfZ2l0aHViL2ltZ18yZlBrRU1adVpwdlpvZFBHcVREdHJnTzJJM3cifQ",
|
|
50
|
+
"last_active_at": 1713709987902,
|
|
51
|
+
"last_name": "Xu",
|
|
52
|
+
"last_sign_in_at": null,
|
|
53
|
+
"locked": false,
|
|
54
|
+
"lockout_expires_in_seconds": null,
|
|
55
|
+
"object": "user",
|
|
56
|
+
"passkeys": [],
|
|
57
|
+
"password_enabled": false,
|
|
58
|
+
"phone_numbers": [],
|
|
59
|
+
"primary_email_address_id": "idn_2fPkD9X1lfzSn5lJVDGyochYq8k",
|
|
60
|
+
"primary_phone_number_id": null,
|
|
61
|
+
"primary_web3_wallet_id": null,
|
|
62
|
+
"private_metadata": {},
|
|
63
|
+
"profile_image_url": "https://images.clerk.dev/oauth_github/img_2fPkEMZuZpvZodPGqTDtrgO2I3w",
|
|
64
|
+
"public_metadata": {},
|
|
65
|
+
"saml_accounts": [],
|
|
66
|
+
"totp_enabled": false,
|
|
67
|
+
"two_factor_enabled": false,
|
|
68
|
+
"unsafe_metadata": {},
|
|
69
|
+
"updated_at": 1713709987972,
|
|
70
|
+
"username": "arvinxx",
|
|
71
|
+
"verification_attempts_remaining": 100,
|
|
72
|
+
"web3_wallets": []
|
|
73
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { UserJSON } from '@clerk/backend';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
|
|
4
|
+
import { authEnv } from '@/config/auth';
|
|
5
|
+
import { isServerMode } from '@/const/version';
|
|
6
|
+
import { UserModel } from '@/database/server/models/user';
|
|
7
|
+
import { pino } from '@/libs/logger';
|
|
8
|
+
|
|
9
|
+
import { validateRequest } from './validateRequest';
|
|
10
|
+
|
|
11
|
+
if (authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH && isServerMode && !authEnv.CLERK_WEBHOOK_SECRET) {
|
|
12
|
+
throw new Error('`CLERK_WEBHOOK_SECRET` environment variable is missing');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const createUser = async (id: string, params: UserJSON) => {
|
|
16
|
+
pino.info('creating user due to clerk webhook');
|
|
17
|
+
|
|
18
|
+
const userModel = new UserModel();
|
|
19
|
+
|
|
20
|
+
// Check if user already exists
|
|
21
|
+
const res = await userModel.findById(id);
|
|
22
|
+
|
|
23
|
+
// If user already exists, skip creating a new user
|
|
24
|
+
if (res)
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ message: 'user not created due to user already existing in the database', success: false },
|
|
27
|
+
{ status: 200 },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const email = params.email_addresses.find((e) => e.id === params.primary_email_address_id);
|
|
31
|
+
const phone = params.phone_numbers.find((e) => e.id === params.primary_phone_number_id);
|
|
32
|
+
|
|
33
|
+
await userModel.createUser({
|
|
34
|
+
avatar: params.image_url,
|
|
35
|
+
clerkCreatedAt: new Date(params.created_at),
|
|
36
|
+
email: email?.email_address,
|
|
37
|
+
firstName: params.first_name,
|
|
38
|
+
id,
|
|
39
|
+
lastName: params.last_name,
|
|
40
|
+
phone: phone?.phone_number,
|
|
41
|
+
username: params.username,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return NextResponse.json({ message: 'user created', success: true }, { status: 200 });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const deleteUser = async (id?: string) => {
|
|
48
|
+
if (id) {
|
|
49
|
+
pino.info('delete user due to clerk webhook');
|
|
50
|
+
const userModel = new UserModel();
|
|
51
|
+
|
|
52
|
+
await userModel.deleteUser(id);
|
|
53
|
+
|
|
54
|
+
return NextResponse.json({ message: 'user deleted' }, { status: 200 });
|
|
55
|
+
} else {
|
|
56
|
+
pino.warn('clerk sent a delete user request, but no user ID was included in the payload');
|
|
57
|
+
return NextResponse.json({ message: 'ok' }, { status: 200 });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const updateUser = async (id: string, params: UserJSON) => {
|
|
62
|
+
pino.info('updating user due to clerk webhook');
|
|
63
|
+
|
|
64
|
+
const userModel = new UserModel();
|
|
65
|
+
|
|
66
|
+
// Check if user already exists
|
|
67
|
+
const res = await userModel.findById(id);
|
|
68
|
+
|
|
69
|
+
// If user not exists, skip update the user
|
|
70
|
+
if (!res)
|
|
71
|
+
return NextResponse.json(
|
|
72
|
+
{
|
|
73
|
+
message: "user not updated due to the user don't existing in the database",
|
|
74
|
+
success: false,
|
|
75
|
+
},
|
|
76
|
+
{ status: 200 },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const email = params.email_addresses.find((e) => e.id === params.primary_email_address_id);
|
|
80
|
+
const phone = params.phone_numbers.find((e) => e.id === params.primary_phone_number_id);
|
|
81
|
+
|
|
82
|
+
await userModel.updateUser(id, {
|
|
83
|
+
avatar: params.image_url,
|
|
84
|
+
email: email?.email_address,
|
|
85
|
+
firstName: params.first_name,
|
|
86
|
+
id,
|
|
87
|
+
lastName: params.last_name,
|
|
88
|
+
phone: phone?.phone_number,
|
|
89
|
+
username: params.username,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return NextResponse.json({ message: 'user updated', success: true }, { status: 200 });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const POST = async (req: Request): Promise<NextResponse> => {
|
|
96
|
+
const payload = await validateRequest(req, authEnv.CLERK_WEBHOOK_SECRET!);
|
|
97
|
+
|
|
98
|
+
if (!payload) {
|
|
99
|
+
return NextResponse.json(
|
|
100
|
+
{ error: 'webhook verification failed or payload was malformed' },
|
|
101
|
+
{ status: 400 },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { type, data } = payload;
|
|
106
|
+
|
|
107
|
+
pino.trace(`clerk webhook payload: ${{ data, type }}`);
|
|
108
|
+
|
|
109
|
+
switch (type) {
|
|
110
|
+
case 'user.created': {
|
|
111
|
+
return createUser(data.id, data);
|
|
112
|
+
}
|
|
113
|
+
case 'user.deleted': {
|
|
114
|
+
return deleteUser(data.id);
|
|
115
|
+
}
|
|
116
|
+
case 'user.updated': {
|
|
117
|
+
return updateUser(data.id, data);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
default: {
|
|
121
|
+
pino.warn(
|
|
122
|
+
`${req.url} received event type "${type}", but no handler is defined for this type`,
|
|
123
|
+
);
|
|
124
|
+
return NextResponse.json({ error: `uncreognised payload type: ${type}` }, { status: 400 });
|
|
125
|
+
}
|
|
126
|
+
// case 'user.updated':
|
|
127
|
+
// break;
|
|
128
|
+
// case 'session.created':
|
|
129
|
+
// break;
|
|
130
|
+
// case 'session.ended':
|
|
131
|
+
// break;
|
|
132
|
+
// case 'session.removed':
|
|
133
|
+
// break;
|
|
134
|
+
// case 'session.revoked':
|
|
135
|
+
// break;
|
|
136
|
+
// case 'email.created':
|
|
137
|
+
// break;
|
|
138
|
+
// case 'sms.created':
|
|
139
|
+
// break;
|
|
140
|
+
// case 'organization.created':
|
|
141
|
+
// break;
|
|
142
|
+
// case 'organization.updated':
|
|
143
|
+
// break;
|
|
144
|
+
// case 'organization.deleted':
|
|
145
|
+
// break;
|
|
146
|
+
// case 'organizationMembership.created':
|
|
147
|
+
// break;
|
|
148
|
+
// case 'organizationMembership.deleted':
|
|
149
|
+
// break;
|
|
150
|
+
// case 'organizationMembership.updated':
|
|
151
|
+
// break;
|
|
152
|
+
// case 'organizationInvitation.accepted':
|
|
153
|
+
// break;
|
|
154
|
+
// case 'organizationInvitation.created':
|
|
155
|
+
// break;
|
|
156
|
+
// case 'organizationInvitation.revoked':
|
|
157
|
+
// break;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { WebhookEvent } from '@clerk/nextjs/server';
|
|
2
|
+
import { headers } from 'next/headers';
|
|
3
|
+
import { Webhook } from 'svix';
|
|
4
|
+
|
|
5
|
+
export const validateRequest = async (request: Request, secret: string) => {
|
|
6
|
+
const payloadString = await request.text();
|
|
7
|
+
const headerPayload = headers();
|
|
8
|
+
|
|
9
|
+
const svixHeaders = {
|
|
10
|
+
'svix-id': headerPayload.get('svix-id')!,
|
|
11
|
+
'svix-signature': headerPayload.get('svix-signature')!,
|
|
12
|
+
'svix-timestamp': headerPayload.get('svix-timestamp')!,
|
|
13
|
+
};
|
|
14
|
+
const wh = new Webhook(secret);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return wh.verify(payloadString, svixHeaders) as WebhookEvent;
|
|
18
|
+
} catch {
|
|
19
|
+
console.error('incoming webhook failed verification');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -3,7 +3,7 @@ import type { NextRequest } from 'next/server';
|
|
|
3
3
|
|
|
4
4
|
import { pino } from '@/libs/logger';
|
|
5
5
|
import { createContext } from '@/server/context';
|
|
6
|
-
import { edgeRouter } from '@/server/routers';
|
|
6
|
+
import { edgeRouter } from '@/server/routers/edge';
|
|
7
7
|
|
|
8
8
|
export const runtime = 'edge';
|
|
9
9
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
2
|
+
import type { NextRequest } from 'next/server';
|
|
3
|
+
|
|
4
|
+
import { pino } from '@/libs/logger';
|
|
5
|
+
import { createContext } from '@/server/context';
|
|
6
|
+
import { lambdaRouter } from '@/server/routers/lambda';
|
|
7
|
+
|
|
8
|
+
const handler = (req: NextRequest) =>
|
|
9
|
+
fetchRequestHandler({
|
|
10
|
+
/**
|
|
11
|
+
* @link https://trpc.io/docs/v11/context
|
|
12
|
+
*/
|
|
13
|
+
createContext: () => createContext(req),
|
|
14
|
+
|
|
15
|
+
endpoint: '/trpc/lambda',
|
|
16
|
+
|
|
17
|
+
onError: ({ error, path, type }) => {
|
|
18
|
+
pino.info(`Error in tRPC handler (lambda) on path: ${path}, type: ${type}`);
|
|
19
|
+
console.error(error);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
req,
|
|
23
|
+
router: lambdaRouter,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export { handler as GET, handler as POST };
|
package/src/config/auth.ts
CHANGED
|
@@ -75,6 +75,7 @@ export const getAuthConfig = () => {
|
|
|
75
75
|
server: {
|
|
76
76
|
// Clerk
|
|
77
77
|
CLERK_SECRET_KEY: z.string().optional(),
|
|
78
|
+
CLERK_WEBHOOK_SECRET: z.string().optional(),
|
|
78
79
|
|
|
79
80
|
// NEXT-AUTH
|
|
80
81
|
NEXT_AUTH_SECRET: z.string().optional(),
|
|
@@ -110,6 +111,7 @@ export const getAuthConfig = () => {
|
|
|
110
111
|
NEXT_PUBLIC_ENABLE_CLERK_AUTH: !!process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
|
111
112
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
|
112
113
|
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
|
114
|
+
CLERK_WEBHOOK_SECRET: process.env.CLERK_WEBHOOK_SECRET,
|
|
113
115
|
|
|
114
116
|
// Next Auth
|
|
115
117
|
NEXT_PUBLIC_ENABLE_NEXT_AUTH:
|