@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
package/locales/zh-TW/oauth.json
CHANGED
@@ -28,10 +28,14 @@
|
|
28
28
|
},
|
29
29
|
"title": "授權 {{clientName}}"
|
30
30
|
},
|
31
|
-
"
|
31
|
+
"error": {
|
32
32
|
"backToHome": "返回首頁",
|
33
|
-
"
|
34
|
-
"
|
33
|
+
"desc": "OAuth 授權失敗,失敗原因:{{reason}}",
|
34
|
+
"reason": {
|
35
|
+
"internal_error": "服務端錯誤",
|
36
|
+
"invalid_request": "無效的請求參數"
|
37
|
+
},
|
38
|
+
"title": "授權失敗"
|
35
39
|
},
|
36
40
|
"handoff": {
|
37
41
|
"desc": {
|
@@ -50,7 +54,7 @@
|
|
50
54
|
"userWelcome": "歡迎回來,"
|
51
55
|
},
|
52
56
|
"success": {
|
53
|
-
"subTitle": "
|
57
|
+
"subTitle": "您已成功授權應用訪問您的帳戶,可以關閉該頁面了",
|
54
58
|
"title": "授權成功"
|
55
59
|
}
|
56
60
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.100.0",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -218,7 +218,7 @@
|
|
218
218
|
"numeral": "^2.0.6",
|
219
219
|
"nuqs": "^2.4.3",
|
220
220
|
"officeparser": "5.1.1",
|
221
|
-
"oidc-provider": "^
|
221
|
+
"oidc-provider": "^9.2.0",
|
222
222
|
"ollama": "^0.5.16",
|
223
223
|
"openai": "^4.104.0",
|
224
224
|
"openapi-fetch": "^0.9.8",
|
@@ -306,7 +306,7 @@
|
|
306
306
|
"@types/lodash-es": "^4.17.12",
|
307
307
|
"@types/node": "^22.15.29",
|
308
308
|
"@types/numeral": "^2.0.5",
|
309
|
-
"@types/oidc-provider": "^
|
309
|
+
"@types/oidc-provider": "^9.1.1",
|
310
310
|
"@types/pg": "^8.15.4",
|
311
311
|
"@types/react": "^19.1.6",
|
312
312
|
"@types/react-dom": "^19.1.5",
|
@@ -1,7 +1,19 @@
|
|
1
|
-
import { DispatchInvoke } from './types';
|
1
|
+
import { DispatchInvoke, type ProxyTRPCRequestParams } from './types';
|
2
|
+
|
3
|
+
interface StreamerCallbacks {
|
4
|
+
onData: (chunk: Uint8Array) => void;
|
5
|
+
onEnd: () => void;
|
6
|
+
onError: (error: Error) => void;
|
7
|
+
onResponse: (response: {
|
8
|
+
headers: Record<string, string>;
|
9
|
+
status: number;
|
10
|
+
statusText: string;
|
11
|
+
}) => void;
|
12
|
+
}
|
2
13
|
|
3
14
|
interface IElectronAPI {
|
4
15
|
invoke: DispatchInvoke;
|
16
|
+
onStreamInvoke: (params: ProxyTRPCRequestParams, callbacks: StreamerCallbacks) => () => void;
|
5
17
|
}
|
6
18
|
|
7
19
|
declare global {
|
@@ -11,7 +23,7 @@ declare global {
|
|
11
23
|
}
|
12
24
|
|
13
25
|
/**
|
14
|
-
* client 端请求
|
26
|
+
* client 端请求 main 端 event 数据的方法
|
15
27
|
*/
|
16
28
|
export const dispatch: DispatchInvoke = async (event, ...data) => {
|
17
29
|
if (!window.electronAPI || !window.electronAPI.invoke)
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import { ProxyTRPCRequestParams } from './types';
|
2
|
+
import { headersToRecord } from './utils/headers';
|
3
|
+
import { getRequestBody } from './utils/request';
|
4
|
+
|
5
|
+
// eslint-disable-next-line no-undef
|
6
|
+
export const streamInvoke = async (input: RequestInfo | URL, init?: RequestInit) => {
|
7
|
+
const url = input.toString();
|
8
|
+
const parsedUrl = new URL(url, window.location.origin);
|
9
|
+
const urlPath = parsedUrl.pathname + parsedUrl.search;
|
10
|
+
const method = init?.method?.toUpperCase() || 'GET';
|
11
|
+
const headers = headersToRecord(init?.headers);
|
12
|
+
const body = await getRequestBody(init?.body);
|
13
|
+
|
14
|
+
const params: ProxyTRPCRequestParams = {
|
15
|
+
body,
|
16
|
+
headers,
|
17
|
+
method,
|
18
|
+
urlPath,
|
19
|
+
};
|
20
|
+
|
21
|
+
return new Promise<Response>((resolve, reject) => {
|
22
|
+
let streamController: ReadableStreamDefaultController<any>;
|
23
|
+
let responseResolved = false;
|
24
|
+
|
25
|
+
const stream = new ReadableStream({
|
26
|
+
cancel() {
|
27
|
+
// This will be called if the consumer of the stream calls .cancel()
|
28
|
+
// We should clean up the IPC listeners
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
30
|
+
cleanup?.();
|
31
|
+
},
|
32
|
+
start(controller) {
|
33
|
+
streamController = controller;
|
34
|
+
},
|
35
|
+
});
|
36
|
+
|
37
|
+
const cleanup = window.electronAPI.onStreamInvoke(params, {
|
38
|
+
onData: (chunk) => {
|
39
|
+
if (streamController) streamController.enqueue(chunk);
|
40
|
+
},
|
41
|
+
onEnd: () => {
|
42
|
+
if (streamController) streamController.close();
|
43
|
+
},
|
44
|
+
onError: (error) => {
|
45
|
+
console.error('[streamInvoke] Error during IPC stream proxy call:', error);
|
46
|
+
if (!responseResolved) {
|
47
|
+
responseResolved = true;
|
48
|
+
reject(error); // Reject the main promise if response not yet sent
|
49
|
+
} else if (streamController) {
|
50
|
+
streamController.error(error); // Otherwise, propagate error through the stream
|
51
|
+
}
|
52
|
+
},
|
53
|
+
onResponse: (meta) => {
|
54
|
+
if (responseResolved) return;
|
55
|
+
responseResolved = true;
|
56
|
+
|
57
|
+
const response = new Response(stream, meta);
|
58
|
+
resolve(response);
|
59
|
+
},
|
60
|
+
});
|
61
|
+
});
|
62
|
+
};
|
@@ -9,6 +9,11 @@ export type ProxyTRPCRequestParams = {
|
|
9
9
|
urlPath: string;
|
10
10
|
};
|
11
11
|
|
12
|
+
export interface ProxyTRPCStreamRequestParams extends Omit<ProxyTRPCRequestParams, 'body'> {
|
13
|
+
body?: ArrayBuffer;
|
14
|
+
requestId: string;
|
15
|
+
}
|
16
|
+
|
12
17
|
export interface ProxyTRPCRequestResult {
|
13
18
|
/** Response body (likely as ArrayBuffer or string) */
|
14
19
|
body: ArrayBuffer | string;
|
@@ -0,0 +1,27 @@
|
|
1
|
+
/**
|
2
|
+
* 将 HeadersInit 转换为 Record<string, string>
|
3
|
+
* @param headersInit - Headers 初始化对象
|
4
|
+
* @returns 转换后的记录对象
|
5
|
+
*/
|
6
|
+
// eslint-disable-next-line no-undef
|
7
|
+
export const headersToRecord = (headersInit?: HeadersInit): Record<string, string> => {
|
8
|
+
const record: Record<string, string> = {};
|
9
|
+
if (!headersInit) {
|
10
|
+
return record;
|
11
|
+
}
|
12
|
+
if (headersInit instanceof Headers) {
|
13
|
+
headersInit.forEach((value, key) => {
|
14
|
+
record[key] = value;
|
15
|
+
});
|
16
|
+
} else if (Array.isArray(headersInit)) {
|
17
|
+
headersInit.forEach(([key, value]) => {
|
18
|
+
record[key] = value;
|
19
|
+
});
|
20
|
+
} else {
|
21
|
+
Object.assign(record, headersInit);
|
22
|
+
}
|
23
|
+
delete record['host'];
|
24
|
+
delete record['connection'];
|
25
|
+
delete record['content-length'];
|
26
|
+
return record;
|
27
|
+
};
|
@@ -0,0 +1,28 @@
|
|
1
|
+
/**
|
2
|
+
* 从请求体中获取数据
|
3
|
+
* @param body - 请求体
|
4
|
+
* @returns 转换后的请求体数据
|
5
|
+
*/
|
6
|
+
export const getRequestBody = async (
|
7
|
+
// eslint-disable-next-line no-undef
|
8
|
+
body?: BodyInit | null,
|
9
|
+
): Promise<string | ArrayBuffer | undefined> => {
|
10
|
+
if (!body) {
|
11
|
+
return undefined;
|
12
|
+
}
|
13
|
+
if (typeof body === 'string') {
|
14
|
+
return body;
|
15
|
+
}
|
16
|
+
if (body instanceof ArrayBuffer) {
|
17
|
+
return body;
|
18
|
+
}
|
19
|
+
if (ArrayBuffer.isView(body)) {
|
20
|
+
return body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) as ArrayBuffer;
|
21
|
+
}
|
22
|
+
if (body instanceof Blob) {
|
23
|
+
return await body.arrayBuffer();
|
24
|
+
}
|
25
|
+
|
26
|
+
console.warn('不支持的 IPC 代理请求体类型:', typeof body);
|
27
|
+
throw new Error('不支持的 IPC 代理请求体类型');
|
28
|
+
};
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { NextRequest, NextResponse, after } from 'next/server';
|
3
|
+
|
4
|
+
import { OAuthHandoffModel } from '@/database/models/oauthHandoff';
|
5
|
+
import { serverDB } from '@/database/server';
|
6
|
+
|
7
|
+
const log = debug('lobe-oidc:callback:desktop');
|
8
|
+
|
9
|
+
export const GET = async (req: NextRequest) => {
|
10
|
+
try {
|
11
|
+
const searchParams = req.nextUrl.searchParams;
|
12
|
+
const code = searchParams.get('code');
|
13
|
+
const state = searchParams.get('state'); // This `state` is the handoff ID
|
14
|
+
|
15
|
+
if (!code || !state || typeof code !== 'string' || typeof state !== 'string') {
|
16
|
+
log('Missing code or state in form data');
|
17
|
+
const errorUrl = req.nextUrl.clone();
|
18
|
+
errorUrl.pathname = '/oauth/callback/error';
|
19
|
+
errorUrl.searchParams.set('reason', 'invalid_request');
|
20
|
+
return NextResponse.redirect(errorUrl);
|
21
|
+
}
|
22
|
+
|
23
|
+
log('Received OIDC callback. state(handoffId): %s', state);
|
24
|
+
|
25
|
+
// The 'client' is 'desktop' because this redirect_uri is for the desktop client.
|
26
|
+
const client = 'desktop';
|
27
|
+
const payload = { code, state };
|
28
|
+
const id = state;
|
29
|
+
|
30
|
+
const authHandoffModel = new OAuthHandoffModel(serverDB);
|
31
|
+
await authHandoffModel.create({ client, id, payload });
|
32
|
+
log('Handoff record created successfully for id: %s', id);
|
33
|
+
|
34
|
+
// Redirect to a generic success page. The desktop app will poll for the result.
|
35
|
+
const successUrl = req.nextUrl.clone();
|
36
|
+
successUrl.pathname = '/oauth/callback/success';
|
37
|
+
|
38
|
+
// cleanup expired
|
39
|
+
after(async () => {
|
40
|
+
const cleanedCount = await authHandoffModel.cleanupExpired();
|
41
|
+
|
42
|
+
log('Cleaned up %d expired handoff records', cleanedCount);
|
43
|
+
});
|
44
|
+
|
45
|
+
return NextResponse.redirect(successUrl);
|
46
|
+
} catch (error) {
|
47
|
+
log('Error in OIDC callback: %O', error);
|
48
|
+
const errorUrl = req.nextUrl.clone();
|
49
|
+
errorUrl.pathname = '/oauth/callback/error';
|
50
|
+
errorUrl.searchParams.set('reason', 'internal_error');
|
51
|
+
|
52
|
+
if (error instanceof Error) {
|
53
|
+
errorUrl.searchParams.set('errorMessage', error.message);
|
54
|
+
}
|
55
|
+
|
56
|
+
return NextResponse.redirect(errorUrl);
|
57
|
+
}
|
58
|
+
};
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
3
|
+
|
4
|
+
import { OAuthHandoffModel } from '@/database/models/oauthHandoff';
|
5
|
+
import { serverDB } from '@/database/server';
|
6
|
+
|
7
|
+
const log = debug('lobe-oidc:handoff');
|
8
|
+
|
9
|
+
/**
|
10
|
+
* GET /oidc/handoff?id=xxx&client=xxx
|
11
|
+
* 轮询获取并消费认证凭证
|
12
|
+
*/
|
13
|
+
export async function GET(request: NextRequest) {
|
14
|
+
log('Received GET request for /oidc/handoff');
|
15
|
+
|
16
|
+
try {
|
17
|
+
const { searchParams } = new URL(request.url);
|
18
|
+
const id = searchParams.get('id');
|
19
|
+
const client = searchParams.get('client');
|
20
|
+
|
21
|
+
if (!id || !client) {
|
22
|
+
return NextResponse.json(
|
23
|
+
{ error: 'Missing required parameters: id and client' },
|
24
|
+
{ status: 400 },
|
25
|
+
);
|
26
|
+
}
|
27
|
+
|
28
|
+
log('Fetching handoff record - id=%s, client=%s', id, client);
|
29
|
+
|
30
|
+
const authHandoffModel = new OAuthHandoffModel(serverDB);
|
31
|
+
const result = await authHandoffModel.fetchAndConsume(id, client);
|
32
|
+
|
33
|
+
if (!result) {
|
34
|
+
log('Handoff record not found or expired - id=%s', id);
|
35
|
+
return NextResponse.json({ error: 'Handoff record not found or expired' }, { status: 404 });
|
36
|
+
}
|
37
|
+
|
38
|
+
log('Handoff record found and consumed - id=%s', id);
|
39
|
+
|
40
|
+
return NextResponse.json({ data: result, success: true });
|
41
|
+
} catch (error) {
|
42
|
+
log('Error fetching handoff record: %O', error);
|
43
|
+
|
44
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
45
|
+
}
|
46
|
+
}
|
@@ -0,0 +1,55 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { Button, Highlighter, Icon } from '@lobehub/ui';
|
4
|
+
import { Card, Result } from 'antd';
|
5
|
+
import { ShieldX } from 'lucide-react';
|
6
|
+
import Link from 'next/link';
|
7
|
+
import { parseAsString, useQueryState } from 'nuqs';
|
8
|
+
import React, { memo } from 'react';
|
9
|
+
import { useTranslation } from 'react-i18next';
|
10
|
+
import { Center, Flexbox } from 'react-layout-kit';
|
11
|
+
|
12
|
+
const FailedPage = memo(() => {
|
13
|
+
const { t } = useTranslation('oauth');
|
14
|
+
const [reason] = useQueryState('reason');
|
15
|
+
const [errorMessage] = useQueryState<string>('errorMessage', parseAsString);
|
16
|
+
|
17
|
+
return (
|
18
|
+
<Center height="100vh">
|
19
|
+
<Card
|
20
|
+
style={{
|
21
|
+
alignItems: 'center',
|
22
|
+
display: 'flex',
|
23
|
+
justifyContent: 'center',
|
24
|
+
minHeight: 280,
|
25
|
+
minWidth: 500,
|
26
|
+
width: '100%',
|
27
|
+
}}
|
28
|
+
>
|
29
|
+
<Result
|
30
|
+
extra={
|
31
|
+
<Link href="/">
|
32
|
+
<Button type="primary">{t('error.backToHome')}</Button>
|
33
|
+
</Link>
|
34
|
+
}
|
35
|
+
icon={<Icon icon={ShieldX} />}
|
36
|
+
status="error"
|
37
|
+
subTitle={
|
38
|
+
<Flexbox gap={8}>
|
39
|
+
{t('error.desc', {
|
40
|
+
reason: t(`error.reason.${reason}` as any, { defaultValue: reason }),
|
41
|
+
})}
|
42
|
+
|
43
|
+
{!!errorMessage && <Highlighter language={'log'}>{errorMessage}</Highlighter>}
|
44
|
+
</Flexbox>
|
45
|
+
}
|
46
|
+
title={t('error.title')}
|
47
|
+
/>
|
48
|
+
</Card>
|
49
|
+
</Center>
|
50
|
+
);
|
51
|
+
});
|
52
|
+
|
53
|
+
FailedPage.displayName = 'FailedPage';
|
54
|
+
|
55
|
+
export default FailedPage;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { notFound } from 'next/navigation';
|
2
|
+
import { PropsWithChildren } from 'react';
|
3
|
+
|
4
|
+
import { oidcEnv } from '@/envs/oidc';
|
5
|
+
|
6
|
+
const Layout = ({ children }: PropsWithChildren) => {
|
7
|
+
if (!oidcEnv.ENABLE_OIDC) return notFound();
|
8
|
+
|
9
|
+
return children;
|
10
|
+
};
|
11
|
+
|
12
|
+
export default Layout;
|
@@ -12,7 +12,16 @@ const SuccessPage = memo(() => {
|
|
12
12
|
|
13
13
|
return (
|
14
14
|
<Center height="100vh">
|
15
|
-
<Card
|
15
|
+
<Card
|
16
|
+
style={{
|
17
|
+
alignItems: 'center',
|
18
|
+
display: 'flex',
|
19
|
+
justifyContent: 'center',
|
20
|
+
minHeight: 280,
|
21
|
+
minWidth: 500,
|
22
|
+
width: '100%',
|
23
|
+
}}
|
24
|
+
>
|
16
25
|
<Result
|
17
26
|
icon={<Icon icon={CheckCircle} />}
|
18
27
|
status="success"
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import { Button, Text } from '@lobehub/ui';
|
4
4
|
import { Card, Divider } from 'antd';
|
5
5
|
import { createStyles } from 'antd-style';
|
6
|
-
import { memo } from 'react';
|
6
|
+
import { memo, useState } from 'react';
|
7
7
|
import { useTranslation } from 'react-i18next';
|
8
8
|
import { Center, Flexbox } from 'react-layout-kit';
|
9
9
|
|
@@ -122,6 +122,8 @@ const ConsentClient = memo<ClientProps>(
|
|
122
122
|
const { styles, theme } = useStyles();
|
123
123
|
const { t } = useTranslation('oauth');
|
124
124
|
|
125
|
+
const [isLoading, setIsLoading] = useState(false);
|
126
|
+
|
125
127
|
const clientDisplayName = clientMetadata?.clientName || clientId;
|
126
128
|
return (
|
127
129
|
<Center className={styles.container} gap={16}>
|
@@ -165,7 +167,11 @@ const ConsentClient = memo<ClientProps>(
|
|
165
167
|
<Button
|
166
168
|
className={styles.authButton}
|
167
169
|
htmlType="submit"
|
170
|
+
loading={isLoading}
|
168
171
|
name="consent"
|
172
|
+
onClick={() => {
|
173
|
+
setIsLoading(true);
|
174
|
+
}}
|
169
175
|
type="primary"
|
170
176
|
value="accept"
|
171
177
|
>
|
@@ -541,5 +541,13 @@
|
|
541
541
|
"bps": true,
|
542
542
|
"folderMillis": 1752413805765,
|
543
543
|
"hash": "abed92b1356df6d7eb35c03f47fbbdcdaf25aefda750dc3e4963c1c2a0d38b54"
|
544
|
+
},
|
545
|
+
{
|
546
|
+
"sql": [
|
547
|
+
"CREATE TABLE \"oauth_handoffs\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"client\" varchar(50) NOT NULL,\n\t\"payload\" jsonb NOT NULL,\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n"
|
548
|
+
],
|
549
|
+
"bps": true,
|
550
|
+
"folderMillis": 1752567402506,
|
551
|
+
"hash": "8ba3ae52ed72e8aad1623dbcf47ca26a8406ebffc6d5284abff94ea994b59c04"
|
544
552
|
}
|
545
553
|
]
|
@@ -0,0 +1,8 @@
|
|
1
|
+
CREATE TABLE "oauth_handoffs" (
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
3
|
+
"client" varchar(50) NOT NULL,
|
4
|
+
"payload" jsonb NOT NULL,
|
5
|
+
"accessed_at" timestamp with time zone DEFAULT now() NOT NULL,
|
6
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
7
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
8
|
+
);
|