@lobehub/chat 1.99.6 → 1.100.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/desktop-pr-build.yml +3 -3
- package/.github/workflows/release-desktop-beta.yml +3 -3
- package/CHANGELOG.md +25 -0
- package/apps/desktop/package.json +5 -2
- package/apps/desktop/src/main/controllers/AuthCtr.ts +310 -111
- package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +1 -1
- package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +50 -3
- package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +188 -23
- package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +37 -18
- package/apps/desktop/src/main/types/store.ts +1 -0
- package/apps/desktop/src/preload/electronApi.ts +2 -1
- package/apps/desktop/src/preload/streamer.ts +58 -0
- package/changelog/v1.json +9 -0
- package/docs/development/database-schema.dbml +9 -0
- package/locales/ar/electron.json +3 -0
- package/locales/ar/oauth.json +8 -4
- package/locales/bg-BG/electron.json +3 -0
- package/locales/bg-BG/oauth.json +8 -4
- package/locales/de-DE/electron.json +3 -0
- package/locales/de-DE/oauth.json +9 -5
- package/locales/en-US/electron.json +3 -0
- package/locales/en-US/oauth.json +8 -4
- package/locales/es-ES/electron.json +3 -0
- package/locales/es-ES/oauth.json +9 -5
- package/locales/fa-IR/electron.json +3 -0
- package/locales/fa-IR/oauth.json +8 -4
- package/locales/fr-FR/electron.json +3 -0
- package/locales/fr-FR/oauth.json +8 -4
- package/locales/it-IT/electron.json +3 -0
- package/locales/it-IT/oauth.json +9 -5
- package/locales/ja-JP/electron.json +3 -0
- package/locales/ja-JP/oauth.json +8 -4
- package/locales/ko-KR/electron.json +3 -0
- package/locales/ko-KR/oauth.json +8 -4
- package/locales/nl-NL/electron.json +3 -0
- package/locales/nl-NL/oauth.json +9 -5
- package/locales/pl-PL/electron.json +3 -0
- package/locales/pl-PL/oauth.json +8 -4
- package/locales/pt-BR/electron.json +3 -0
- package/locales/pt-BR/oauth.json +8 -4
- package/locales/ru-RU/electron.json +3 -0
- package/locales/ru-RU/oauth.json +8 -4
- package/locales/tr-TR/electron.json +3 -0
- package/locales/tr-TR/oauth.json +8 -4
- package/locales/vi-VN/electron.json +3 -0
- package/locales/vi-VN/oauth.json +9 -5
- package/locales/zh-CN/electron.json +3 -0
- package/locales/zh-CN/oauth.json +8 -4
- package/locales/zh-TW/electron.json +3 -0
- package/locales/zh-TW/oauth.json +8 -4
- package/package.json +3 -3
- package/packages/electron-client-ipc/src/dispatch.ts +14 -2
- package/packages/electron-client-ipc/src/index.ts +1 -0
- package/packages/electron-client-ipc/src/streamInvoke.ts +62 -0
- package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +5 -0
- package/packages/electron-client-ipc/src/utils/headers.ts +27 -0
- package/packages/electron-client-ipc/src/utils/request.ts +28 -0
- package/src/app/(backend)/oidc/callback/desktop/route.ts +58 -0
- package/src/app/(backend)/oidc/handoff/route.ts +46 -0
- package/src/app/[variants]/oauth/callback/error/page.tsx +55 -0
- package/src/app/[variants]/oauth/callback/layout.tsx +12 -0
- package/src/app/[variants]/oauth/callback/loading.tsx +3 -0
- package/src/app/[variants]/oauth/{consent/[uid] → callback}/success/page.tsx +10 -1
- package/src/app/[variants]/oauth/consent/[uid]/Consent.tsx +7 -1
- package/src/database/client/migrations.json +8 -0
- package/src/database/migrations/0028_oauth_handoffs.sql +8 -0
- package/src/database/migrations/meta/0028_snapshot.json +6055 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/models/oauthHandoff.ts +94 -0
- package/src/database/repositories/tableViewer/index.test.ts +1 -1
- package/src/database/schemas/oidc.ts +46 -0
- package/src/features/ElectronTitlebar/Connection/Waiting.tsx +59 -115
- package/src/features/ElectronTitlebar/Connection/WaitingAnim.tsx +114 -0
- package/src/libs/oidc-provider/config.ts +16 -17
- package/src/libs/oidc-provider/jwt.ts +135 -0
- package/src/libs/oidc-provider/provider.ts +22 -38
- package/src/libs/trpc/client/async.ts +1 -2
- package/src/libs/trpc/client/edge.ts +1 -2
- package/src/libs/trpc/client/lambda.ts +1 -1
- package/src/libs/trpc/client/tools.ts +1 -2
- package/src/libs/trpc/lambda/context.ts +9 -16
- package/src/locales/default/electron.ts +3 -0
- package/src/locales/default/oauth.ts +8 -4
- package/src/middleware.ts +10 -4
- package/src/server/services/oidc/index.ts +0 -71
- package/src/services/chat.ts +5 -1
- package/src/services/electron/remoteServer.ts +0 -7
- package/src/{libs/trpc/client/helpers → utils/electron}/desktopRemoteRPCFetch.ts +22 -7
- package/src/utils/server/auth.ts +22 -0
- package/src/app/[variants]/oauth/consent/[uid]/failed/page.tsx +0 -36
- package/src/app/[variants]/oauth/handoff/Client.tsx +0 -98
- package/src/app/[variants]/oauth/handoff/page.tsx +0 -13
@@ -196,6 +196,13 @@
|
|
196
196
|
"when": 1752413805765,
|
197
197
|
"tag": "0027_ai_image",
|
198
198
|
"breakpoints": true
|
199
|
+
},
|
200
|
+
{
|
201
|
+
"idx": 28,
|
202
|
+
"version": "7",
|
203
|
+
"when": 1752567402506,
|
204
|
+
"tag": "0028_oauth_handoffs",
|
205
|
+
"breakpoints": true
|
199
206
|
}
|
200
207
|
],
|
201
208
|
"version": "6"
|
@@ -0,0 +1,94 @@
|
|
1
|
+
import { and, eq, lt, sql } from 'drizzle-orm';
|
2
|
+
|
3
|
+
import { LobeChatDatabase } from '@/database/type';
|
4
|
+
|
5
|
+
import { NewOAuthHandoff, OAuthHandoffItem, oauthHandoffs } from '../schemas';
|
6
|
+
|
7
|
+
export class OAuthHandoffModel {
|
8
|
+
private db: LobeChatDatabase;
|
9
|
+
|
10
|
+
constructor(db: LobeChatDatabase) {
|
11
|
+
this.db = db;
|
12
|
+
}
|
13
|
+
|
14
|
+
/**
|
15
|
+
* 创建新的认证凭证传递记录
|
16
|
+
* @param params 凭证数据
|
17
|
+
* @returns 创建的记录
|
18
|
+
*/
|
19
|
+
create = async (params: NewOAuthHandoff): Promise<OAuthHandoffItem> => {
|
20
|
+
const [result] = await this.db
|
21
|
+
.insert(oauthHandoffs)
|
22
|
+
.values(params)
|
23
|
+
.onConflictDoNothing()
|
24
|
+
.returning();
|
25
|
+
|
26
|
+
return result;
|
27
|
+
};
|
28
|
+
|
29
|
+
/**
|
30
|
+
* 获取并消费认证凭证
|
31
|
+
* 该方法会先查询记录,如果找到则立即删除,确保凭证只能被使用一次
|
32
|
+
* @param id 凭证ID
|
33
|
+
* @param client 客户端类型
|
34
|
+
* @returns 凭证数据,如果不存在或已过期则返回null
|
35
|
+
*/
|
36
|
+
fetchAndConsume = async (id: string, client: string): Promise<OAuthHandoffItem | null> => {
|
37
|
+
// 先查找记录,同时检查是否过期 (5分钟TTL)
|
38
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
39
|
+
|
40
|
+
const handoff = await this.db.query.oauthHandoffs.findFirst({
|
41
|
+
where: and(
|
42
|
+
eq(oauthHandoffs.id, id),
|
43
|
+
eq(oauthHandoffs.client, client),
|
44
|
+
// 检查记录是否在5分钟内创建
|
45
|
+
sql`${oauthHandoffs.createdAt} > ${fiveMinutesAgo}`,
|
46
|
+
),
|
47
|
+
});
|
48
|
+
|
49
|
+
if (!handoff) {
|
50
|
+
return null;
|
51
|
+
}
|
52
|
+
|
53
|
+
// 立即删除记录以确保一次性使用
|
54
|
+
await this.db.delete(oauthHandoffs).where(eq(oauthHandoffs.id, id));
|
55
|
+
|
56
|
+
return handoff;
|
57
|
+
};
|
58
|
+
|
59
|
+
/**
|
60
|
+
* 清理过期的认证凭证记录
|
61
|
+
* 这个方法应该被定期调用(比如通过 cron job)来清理过期的记录
|
62
|
+
* @returns 清理的记录数量
|
63
|
+
*/
|
64
|
+
cleanupExpired = async (): Promise<number> => {
|
65
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
66
|
+
|
67
|
+
const result = await this.db
|
68
|
+
.delete(oauthHandoffs)
|
69
|
+
.where(lt(oauthHandoffs.createdAt, fiveMinutesAgo));
|
70
|
+
|
71
|
+
return result.rowCount || 0;
|
72
|
+
};
|
73
|
+
|
74
|
+
/**
|
75
|
+
* 检查凭证是否存在(不消费)
|
76
|
+
* 主要用于测试和调试
|
77
|
+
* @param id 凭证ID
|
78
|
+
* @param client 客户端类型
|
79
|
+
* @returns 是否存在且未过期
|
80
|
+
*/
|
81
|
+
exists = async (id: string, client: string): Promise<boolean> => {
|
82
|
+
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
83
|
+
|
84
|
+
const handoff = await this.db.query.oauthHandoffs.findFirst({
|
85
|
+
where: and(
|
86
|
+
eq(oauthHandoffs.id, id),
|
87
|
+
eq(oauthHandoffs.client, client),
|
88
|
+
sql`${oauthHandoffs.createdAt} > ${fiveMinutesAgo}`,
|
89
|
+
),
|
90
|
+
});
|
91
|
+
|
92
|
+
return !!handoff;
|
93
|
+
};
|
94
|
+
}
|
@@ -23,7 +23,7 @@ describe('TableViewerRepo', () => {
|
|
23
23
|
it('should return all tables with counts', async () => {
|
24
24
|
const result = await repo.getAllTables();
|
25
25
|
|
26
|
-
expect(result.length).toEqual(
|
26
|
+
expect(result.length).toEqual(59);
|
27
27
|
expect(result[0]).toEqual({ name: 'agents', count: 0, type: 'BASE TABLE' });
|
28
28
|
});
|
29
29
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
2
2
|
import { boolean, jsonb, pgTable, primaryKey, text, varchar } from 'drizzle-orm/pg-core';
|
3
|
+
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
|
3
4
|
|
4
5
|
import { timestamps, timestamptz } from './_helpers';
|
5
6
|
import { users } from './user';
|
@@ -156,3 +157,48 @@ export const oidcConsents = pgTable(
|
|
156
157
|
pk: primaryKey({ columns: [table.userId, table.clientId] }),
|
157
158
|
}),
|
158
159
|
);
|
160
|
+
|
161
|
+
/**
|
162
|
+
* 通用认证凭证传递表
|
163
|
+
* 用于在不同客户端(桌面端、浏览器插件、移动端等)之间安全传递认证凭证
|
164
|
+
*
|
165
|
+
* 工作流程:
|
166
|
+
* 1. 客户端生成唯一的 handoff ID
|
167
|
+
* 2. 将 handoff ID 作为参数附加到 OAuth redirect_uri
|
168
|
+
* 3. 认证成功后,中间页将凭证存储到此表
|
169
|
+
* 4. 客户端轮询此表获取凭证
|
170
|
+
* 5. 成功获取后立即删除记录
|
171
|
+
*/
|
172
|
+
export const oauthHandoffs = pgTable('oauth_handoffs', {
|
173
|
+
/**
|
174
|
+
* 由客户端生成的一次性唯一标识符
|
175
|
+
* 用于客户端轮询时认领自己的凭证
|
176
|
+
*/
|
177
|
+
id: text('id').primaryKey(),
|
178
|
+
|
179
|
+
/**
|
180
|
+
* 客户端类型标识
|
181
|
+
* 如: 'desktop', 'browser-extension', 'mobile-app' 等
|
182
|
+
*/
|
183
|
+
client: varchar('client', { length: 50 }).notNull(),
|
184
|
+
|
185
|
+
/**
|
186
|
+
* 凭证数据的 JSON 载荷
|
187
|
+
* 灵活存储不同认证流程所需的各种数据
|
188
|
+
* 当前主要包含: { code: string; state: string }
|
189
|
+
*/
|
190
|
+
payload: jsonb('payload').$type<Record<string, unknown>>().notNull(),
|
191
|
+
|
192
|
+
/**
|
193
|
+
* 时间戳字段,用于 TTL 控制
|
194
|
+
* 凭证应在创建后 5 分钟内被消费,否则视为过期
|
195
|
+
*/
|
196
|
+
...timestamps,
|
197
|
+
});
|
198
|
+
|
199
|
+
// Zod schemas for validation
|
200
|
+
export const insertAuthHandoffSchema = createInsertSchema(oauthHandoffs);
|
201
|
+
export const selectAuthHandoffSchema = createSelectSchema(oauthHandoffs);
|
202
|
+
|
203
|
+
export type OAuthHandoffItem = typeof oauthHandoffs.$inferSelect;
|
204
|
+
export type NewOAuthHandoff = typeof oauthHandoffs.$inferInsert;
|
@@ -1,24 +1,16 @@
|
|
1
1
|
'use client';
|
2
2
|
|
3
3
|
import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
|
4
|
-
import { Button, Icon, Text } from '@lobehub/ui';
|
5
|
-
import { createStyles
|
6
|
-
import {
|
7
|
-
import { memo } from 'react';
|
4
|
+
import { Button, Highlighter, Icon, Text } from '@lobehub/ui';
|
5
|
+
import { createStyles } from 'antd-style';
|
6
|
+
import { ShieldX } from 'lucide-react';
|
7
|
+
import { memo, useState } from 'react';
|
8
8
|
import { useTranslation } from 'react-i18next';
|
9
|
+
import { Flexbox } from 'react-layout-kit';
|
9
10
|
|
10
11
|
import { useElectronStore } from '@/store/electron';
|
11
12
|
|
12
|
-
|
13
|
-
0% {
|
14
|
-
transform: translate(-50%, -50%) scale(0.8);
|
15
|
-
opacity: 0.5;
|
16
|
-
}
|
17
|
-
100% {
|
18
|
-
transform: translate(-50%, -50%) scale(2.5);
|
19
|
-
opacity: 0;
|
20
|
-
}
|
21
|
-
`;
|
13
|
+
import WaitingAnim from './WaitingAnim';
|
22
14
|
|
23
15
|
const useStyles = createStyles(({ css, token }) => ({
|
24
16
|
container: css`
|
@@ -47,15 +39,15 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
47
39
|
color: ${token.colorTextSecondary} !important;
|
48
40
|
`,
|
49
41
|
|
50
|
-
|
51
|
-
margin-
|
52
|
-
color: ${token.
|
53
|
-
|
54
|
-
text-underline-offset: 2px;
|
42
|
+
errorIcon: css`
|
43
|
+
margin-block-end: ${token.marginXL}px;
|
44
|
+
color: ${token.colorError};
|
45
|
+
`,
|
55
46
|
|
56
|
-
|
57
|
-
|
58
|
-
}
|
47
|
+
errorMessage: css`
|
48
|
+
margin-block-end: ${token.marginXL}px !important;
|
49
|
+
color: ${token.colorError} !important;
|
50
|
+
text-align: center;
|
59
51
|
`,
|
60
52
|
|
61
53
|
helpText: css`
|
@@ -63,84 +55,7 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
63
55
|
font-size: ${token.fontSizeSM}px;
|
64
56
|
color: ${token.colorTextTertiary};
|
65
57
|
`,
|
66
|
-
// 新增:图标和脉冲动画的容器
|
67
|
-
iconContainer: css`
|
68
|
-
position: relative;
|
69
|
-
|
70
|
-
display: flex;
|
71
|
-
align-items: center;
|
72
|
-
justify-content: center;
|
73
|
-
|
74
|
-
width: 160px;
|
75
|
-
height: 160px;
|
76
|
-
margin-block-end: ${token.marginXL}px;
|
77
|
-
`,
|
78
|
-
|
79
|
-
// 新增:不同延迟的脉冲动画
|
80
|
-
pulse1: css`
|
81
|
-
animation: ${airdropPulse} 3s ease-out infinite;
|
82
|
-
`,
|
83
|
-
|
84
|
-
pulse2: css`
|
85
|
-
animation: ${airdropPulse} 3s ease-out 1.2s infinite;
|
86
|
-
`,
|
87
58
|
|
88
|
-
pulse3: css`
|
89
|
-
animation: ${airdropPulse} 3s ease-out 1.8s infinite;
|
90
|
-
`,
|
91
|
-
// 新增:基础脉冲样式
|
92
|
-
pulseBase: css`
|
93
|
-
pointer-events: none;
|
94
|
-
content: '';
|
95
|
-
|
96
|
-
position: absolute;
|
97
|
-
inset-block-start: 50%;
|
98
|
-
inset-inline-start: 50%;
|
99
|
-
transform: translate(-50%, -50%);
|
100
|
-
|
101
|
-
width: 100px;
|
102
|
-
height: 100px;
|
103
|
-
border-radius: 50%;
|
104
|
-
|
105
|
-
opacity: 0;
|
106
|
-
background-color: ${token.colorPrimaryBgHover};
|
107
|
-
`,
|
108
|
-
|
109
|
-
// 新增:Radar 图标样式
|
110
|
-
radarIcon: css`
|
111
|
-
z-index: 1;
|
112
|
-
color: ${token.colorPrimary};
|
113
|
-
`,
|
114
|
-
|
115
|
-
ring1: css`
|
116
|
-
width: 80px;
|
117
|
-
height: 80px;
|
118
|
-
border: 1px solid ${token.colorText};
|
119
|
-
`,
|
120
|
-
|
121
|
-
ring2: css`
|
122
|
-
width: 120px;
|
123
|
-
height: 120px;
|
124
|
-
border: 1px solid ${token.colorTextQuaternary};
|
125
|
-
`,
|
126
|
-
|
127
|
-
ring3: css`
|
128
|
-
width: 160px;
|
129
|
-
height: 160px;
|
130
|
-
border: 1px solid ${token.colorFillSecondary};
|
131
|
-
`,
|
132
|
-
|
133
|
-
// 新增:星环基础样式
|
134
|
-
ringBase: css`
|
135
|
-
pointer-events: none;
|
136
|
-
|
137
|
-
position: absolute;
|
138
|
-
inset-block-start: 50%;
|
139
|
-
inset-inline-start: 50%;
|
140
|
-
transform: translate(-50%, -50%);
|
141
|
-
|
142
|
-
border-radius: 50%;
|
143
|
-
`,
|
144
59
|
title: css`
|
145
60
|
margin-block-end: ${token.marginSM}px !important;
|
146
61
|
color: ${token.colorText} !important;
|
@@ -151,46 +66,75 @@ interface WaitingOAuthProps {
|
|
151
66
|
setIsOpen: (open: boolean) => void;
|
152
67
|
setWaiting: (waiting: boolean) => void;
|
153
68
|
}
|
69
|
+
|
154
70
|
const WaitingOAuth = memo<WaitingOAuthProps>(({ setWaiting, setIsOpen }) => {
|
155
71
|
const { styles } = useStyles();
|
156
|
-
const { t } = useTranslation('electron');
|
157
|
-
const [
|
72
|
+
const { t } = useTranslation('electron');
|
73
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
74
|
+
const [disconnect, refreshServerConfig, connectRemoteServer] = useElectronStore((s) => [
|
158
75
|
s.disconnectRemoteServer,
|
159
76
|
s.refreshServerConfig,
|
77
|
+
s.connectRemoteServer,
|
160
78
|
]);
|
161
79
|
|
162
80
|
const handleCancel = async () => {
|
163
81
|
await disconnect();
|
164
82
|
setWaiting(false);
|
83
|
+
setErrorMessage(null);
|
84
|
+
};
|
85
|
+
|
86
|
+
const handleRetry = async () => {
|
87
|
+
setErrorMessage(null);
|
88
|
+
const { dataSyncConfig } = useElectronStore.getState();
|
89
|
+
await connectRemoteServer(dataSyncConfig);
|
165
90
|
};
|
166
91
|
|
167
92
|
useWatchBroadcast('authorizationSuccessful', async () => {
|
168
93
|
setIsOpen(false);
|
169
94
|
setWaiting(false);
|
95
|
+
setErrorMessage(null);
|
170
96
|
await refreshServerConfig();
|
171
97
|
});
|
172
98
|
|
99
|
+
useWatchBroadcast('authorizationFailed', ({ error }) => {
|
100
|
+
setErrorMessage(error);
|
101
|
+
});
|
102
|
+
|
103
|
+
// 错误状态
|
104
|
+
if (errorMessage) {
|
105
|
+
return (
|
106
|
+
<div className={styles.container}>
|
107
|
+
<Flexbox className={styles.content} gap={12}>
|
108
|
+
<Flexbox align={'center'}>
|
109
|
+
<Icon className={styles.errorIcon} icon={ShieldX} size={64} />
|
110
|
+
<Text as={'h4'} className={styles.title}>
|
111
|
+
{t('waitingOAuth.errorTitle')}
|
112
|
+
</Text>
|
113
|
+
</Flexbox>
|
114
|
+
<Highlighter language={'log'} style={{ maxHeight: 500, maxWidth: 800, overflow: 'auto' }}>
|
115
|
+
{errorMessage}
|
116
|
+
</Highlighter>
|
117
|
+
<Flexbox gap={12} horizontal>
|
118
|
+
<Button onClick={handleCancel}>{t('waitingOAuth.cancel')}</Button>
|
119
|
+
<Button onClick={handleRetry} type="primary">
|
120
|
+
{t('waitingOAuth.retry')}
|
121
|
+
</Button>
|
122
|
+
</Flexbox>
|
123
|
+
</Flexbox>
|
124
|
+
</div>
|
125
|
+
);
|
126
|
+
}
|
127
|
+
|
128
|
+
// 正常等待状态
|
173
129
|
return (
|
174
130
|
<div className={styles.container}>
|
175
131
|
<div className={styles.content}>
|
176
|
-
|
177
|
-
<div className={styles.iconContainer}>
|
178
|
-
{/* 新增:星环 */}
|
179
|
-
<div className={cx(styles.ringBase, styles.ring1)} />
|
180
|
-
<div className={cx(styles.ringBase, styles.ring2)} />
|
181
|
-
<div className={cx(styles.ringBase, styles.ring3)} />
|
182
|
-
{/* 脉冲 */}
|
183
|
-
<div className={cx(styles.pulseBase, styles.pulse1)} />
|
184
|
-
<div className={cx(styles.pulseBase, styles.pulse2)} />
|
185
|
-
<div className={cx(styles.pulseBase, styles.pulse3)} />
|
186
|
-
|
187
|
-
<Icon className={styles.radarIcon} icon={WifiIcon} size={40} />
|
188
|
-
</div>
|
132
|
+
<WaitingAnim />
|
189
133
|
<Text as={'h4'} className={styles.title}>
|
190
134
|
{t('waitingOAuth.title')}
|
191
135
|
</Text>
|
192
136
|
<Text className={styles.description}>{t('waitingOAuth.description')}</Text>
|
193
|
-
<Button onClick={handleCancel}>{t('waitingOAuth.cancel')}</Button>
|
137
|
+
<Button onClick={handleCancel}>{t('waitingOAuth.cancel')}</Button>
|
194
138
|
<Text className={styles.helpText}>{t('waitingOAuth.helpText')}</Text>
|
195
139
|
</div>
|
196
140
|
</div>
|
@@ -0,0 +1,114 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { Icon } from '@lobehub/ui';
|
4
|
+
import { createStyles, cx, keyframes } from 'antd-style';
|
5
|
+
import { WifiIcon } from 'lucide-react';
|
6
|
+
import { memo } from 'react';
|
7
|
+
|
8
|
+
const airdropPulse = keyframes`
|
9
|
+
0% {
|
10
|
+
transform: translate(-50%, -50%) scale(0.8);
|
11
|
+
opacity: 0.5;
|
12
|
+
}
|
13
|
+
100% {
|
14
|
+
transform: translate(-50%, -50%) scale(2.5);
|
15
|
+
opacity: 0;
|
16
|
+
}
|
17
|
+
`;
|
18
|
+
|
19
|
+
const useStyles = createStyles(({ css, token }) => ({
|
20
|
+
container: css`
|
21
|
+
position: relative;
|
22
|
+
|
23
|
+
display: flex;
|
24
|
+
align-items: center;
|
25
|
+
justify-content: center;
|
26
|
+
|
27
|
+
width: 160px;
|
28
|
+
height: 160px;
|
29
|
+
margin-block-end: ${token.marginXL}px;
|
30
|
+
`,
|
31
|
+
|
32
|
+
pulse1: css`
|
33
|
+
animation: ${airdropPulse} 3s ease-out infinite;
|
34
|
+
`,
|
35
|
+
|
36
|
+
pulse2: css`
|
37
|
+
animation: ${airdropPulse} 3s ease-out 1.2s infinite;
|
38
|
+
`,
|
39
|
+
|
40
|
+
pulse3: css`
|
41
|
+
animation: ${airdropPulse} 3s ease-out 1.8s infinite;
|
42
|
+
`,
|
43
|
+
pulseBase: css`
|
44
|
+
pointer-events: none;
|
45
|
+
content: '';
|
46
|
+
|
47
|
+
position: absolute;
|
48
|
+
inset-block-start: 50%;
|
49
|
+
inset-inline-start: 50%;
|
50
|
+
transform: translate(-50%, -50%);
|
51
|
+
|
52
|
+
width: 100px;
|
53
|
+
height: 100px;
|
54
|
+
border-radius: 50%;
|
55
|
+
|
56
|
+
opacity: 0;
|
57
|
+
background-color: ${token.colorPrimaryBgHover};
|
58
|
+
`,
|
59
|
+
|
60
|
+
radarIcon: css`
|
61
|
+
z-index: 1;
|
62
|
+
color: ${token.colorPrimary};
|
63
|
+
`,
|
64
|
+
|
65
|
+
ring1: css`
|
66
|
+
width: 80px;
|
67
|
+
height: 80px;
|
68
|
+
border: 1px solid ${token.colorText};
|
69
|
+
`,
|
70
|
+
|
71
|
+
ring2: css`
|
72
|
+
width: 120px;
|
73
|
+
height: 120px;
|
74
|
+
border: 1px solid ${token.colorTextQuaternary};
|
75
|
+
`,
|
76
|
+
|
77
|
+
ring3: css`
|
78
|
+
width: 160px;
|
79
|
+
height: 160px;
|
80
|
+
border: 1px solid ${token.colorFillSecondary};
|
81
|
+
`,
|
82
|
+
|
83
|
+
ringBase: css`
|
84
|
+
pointer-events: none;
|
85
|
+
|
86
|
+
position: absolute;
|
87
|
+
inset-block-start: 50%;
|
88
|
+
inset-inline-start: 50%;
|
89
|
+
transform: translate(-50%, -50%);
|
90
|
+
|
91
|
+
border-radius: 50%;
|
92
|
+
`,
|
93
|
+
}));
|
94
|
+
|
95
|
+
const WaitingAnim = memo(() => {
|
96
|
+
const { styles } = useStyles();
|
97
|
+
|
98
|
+
return (
|
99
|
+
<div className={styles.container}>
|
100
|
+
{/* 新增:星环 */}
|
101
|
+
<div className={cx(styles.ringBase, styles.ring1)} />
|
102
|
+
<div className={cx(styles.ringBase, styles.ring2)} />
|
103
|
+
<div className={cx(styles.ringBase, styles.ring3)} />
|
104
|
+
{/* 脉冲 */}
|
105
|
+
<div className={cx(styles.pulseBase, styles.pulse1)} />
|
106
|
+
<div className={cx(styles.pulseBase, styles.pulse2)} />
|
107
|
+
<div className={cx(styles.pulseBase, styles.pulse3)} />
|
108
|
+
|
109
|
+
<Icon className={styles.radarIcon} icon={WifiIcon} size={40} />
|
110
|
+
</div>
|
111
|
+
);
|
112
|
+
});
|
113
|
+
|
114
|
+
export default WaitingAnim;
|
@@ -1,11 +1,14 @@
|
|
1
1
|
import { ClientMetadata } from 'oidc-provider';
|
2
|
+
import urlJoin from 'url-join';
|
3
|
+
|
4
|
+
import { appEnv } from '@/envs/app';
|
2
5
|
|
3
6
|
/**
|
4
7
|
* 默认 OIDC 客户端配置
|
5
8
|
*/
|
6
9
|
export const defaultClients: ClientMetadata[] = [
|
7
10
|
{
|
8
|
-
application_type: '
|
11
|
+
application_type: 'web',
|
9
12
|
client_id: 'lobehub-desktop',
|
10
13
|
client_name: 'LobeHub Desktop',
|
11
14
|
// 仅支持授权码流程
|
@@ -13,19 +16,17 @@ export const defaultClients: ClientMetadata[] = [
|
|
13
16
|
|
14
17
|
logo_uri: 'https://hub-apac-1.lobeobjects.space/lobehub-desktop-icon.png',
|
15
18
|
|
16
|
-
// 桌面端注册的自定义协议回调(使用反向域名格式)
|
17
19
|
post_logout_redirect_uris: [
|
18
|
-
|
19
|
-
'
|
20
|
-
'
|
21
|
-
'com.lobehub.lobehub-desktop://auth/logout/callback',
|
20
|
+
// 动态构建 Web 页面回调 URL
|
21
|
+
urlJoin(appEnv.APP_URL!, '/oauth/logout'),
|
22
|
+
'http://localhost:3210/oauth/logout',
|
22
23
|
],
|
23
24
|
|
25
|
+
// 桌面端授权回调 - 改为 Web 页面路径
|
24
26
|
redirect_uris: [
|
25
|
-
|
26
|
-
'
|
27
|
-
'
|
28
|
-
'com.lobehub.lobehub-desktop://auth/callback',
|
27
|
+
// 动态构建 Web 页面回调 URL
|
28
|
+
urlJoin(appEnv.APP_URL!, '/oidc/callback/desktop'),
|
29
|
+
'http://localhost:3210/oidc/callback/desktop',
|
29
30
|
],
|
30
31
|
|
31
32
|
// 支持授权码获取令牌和刷新令牌
|
@@ -40,16 +41,14 @@ export const defaultClients: ClientMetadata[] = [
|
|
40
41
|
* OIDC Scopes 定义
|
41
42
|
*/
|
42
43
|
export const defaultScopes = [
|
43
|
-
'openid',
|
44
|
-
'profile',
|
45
|
-
'email',
|
46
|
-
'offline_access', //
|
47
|
-
'sync:read', // 自定义 Scope:读取同步数据权限
|
48
|
-
'sync:write', // 自定义 Scope:写入同步数据权限
|
44
|
+
'openid',
|
45
|
+
'profile',
|
46
|
+
'email',
|
47
|
+
'offline_access', // 允许获取 refresh_token
|
49
48
|
];
|
50
49
|
|
51
50
|
/**
|
52
|
-
* OIDC Claims 定义
|
51
|
+
* OIDC Claims 定义
|
53
52
|
*/
|
54
53
|
export const defaultClaims = {
|
55
54
|
email: ['email', 'email_verified'],
|