finish box connector
This commit is contained in:
parent
d263989317
commit
34fc06c283
7 changed files with 350 additions and 56 deletions
|
|
@ -380,7 +380,6 @@ async def poll_google_web_result():
|
||||||
|
|
||||||
@manager.route("/box/oauth/web/start", methods=["POST"]) # noqa: F821
|
@manager.route("/box/oauth/web/start", methods=["POST"]) # noqa: F821
|
||||||
@login_required
|
@login_required
|
||||||
@validate_request("")
|
|
||||||
async def start_box_web_oauth():
|
async def start_box_web_oauth():
|
||||||
req = await get_request_json()
|
req = await get_request_json()
|
||||||
|
|
||||||
|
|
@ -394,7 +393,7 @@ async def start_box_web_oauth():
|
||||||
flow_id = str(uuid.uuid4())
|
flow_id = str(uuid.uuid4())
|
||||||
|
|
||||||
box_auth = BoxOAuth(
|
box_auth = BoxOAuth(
|
||||||
oauth_config=OAuthConfig(
|
OAuthConfig(
|
||||||
client_id=client_id,
|
client_id=client_id,
|
||||||
client_secret=client_secret,
|
client_secret=client_secret,
|
||||||
)
|
)
|
||||||
|
|
@ -411,7 +410,7 @@ async def start_box_web_oauth():
|
||||||
"user_id": current_user.id,
|
"user_id": current_user.id,
|
||||||
"auth_url": auth_url,
|
"auth_url": auth_url,
|
||||||
"client_id": client_id,
|
"client_id": client_id,
|
||||||
"clientsecret": client_secret,
|
"client_secret": client_secret,
|
||||||
"created_at": int(time.time()),
|
"created_at": int(time.time()),
|
||||||
}
|
}
|
||||||
REDIS_CONN.set_obj(_web_state_cache_key(flow_id, "box"), cache_payload, WEB_FLOW_TTL_SECS)
|
REDIS_CONN.set_obj(_web_state_cache_key(flow_id, "box"), cache_payload, WEB_FLOW_TTL_SECS)
|
||||||
|
|
@ -425,11 +424,14 @@ async def start_box_web_oauth():
|
||||||
@manager.route("/box/oauth/web/callback", methods=["GET"]) # noqa: F821
|
@manager.route("/box/oauth/web/callback", methods=["GET"]) # noqa: F821
|
||||||
async def box_web_oauth_callback():
|
async def box_web_oauth_callback():
|
||||||
flow_id = request.args.get("state")
|
flow_id = request.args.get("state")
|
||||||
|
if not flow_id:
|
||||||
if not code or not flow_id:
|
|
||||||
return await _render_web_oauth_popup("", False, "Missing OAuth parameters.", "box")
|
return await _render_web_oauth_popup("", False, "Missing OAuth parameters.", "box")
|
||||||
|
|
||||||
cache_payload = REDIS_CONN.get_obj(_web_state_cache_key(flow_id, "box"))
|
code = request.args.get("code")
|
||||||
|
if not code:
|
||||||
|
return await _render_web_oauth_popup(flow_id, False, "Missing authorization code from Box.", "box")
|
||||||
|
|
||||||
|
cache_payload = json.loads(REDIS_CONN.get(_web_state_cache_key(flow_id, "box")))
|
||||||
if not cache_payload:
|
if not cache_payload:
|
||||||
return get_json_result(code=RetCode.ARGUMENT_ERROR, message="Box OAuth session expired or invalid.")
|
return get_json_result(code=RetCode.ARGUMENT_ERROR, message="Box OAuth session expired or invalid.")
|
||||||
|
|
||||||
|
|
@ -439,15 +441,21 @@ async def box_web_oauth_callback():
|
||||||
REDIS_CONN.delete(_web_state_cache_key(flow_id, "box"))
|
REDIS_CONN.delete(_web_state_cache_key(flow_id, "box"))
|
||||||
return await _render_web_oauth_popup(flow_id, False, error_description or "Authorization failed.", "box")
|
return await _render_web_oauth_popup(flow_id, False, error_description or "Authorization failed.", "box")
|
||||||
|
|
||||||
code = request.args.get("code")
|
auth = BoxOAuth(
|
||||||
if not code:
|
OAuthConfig(
|
||||||
return await _render_web_oauth_popup(flow_id, False, "Missing authorization code from Box.", "box")
|
client_id=cache_payload.get("client_id"),
|
||||||
|
client_secret=cache_payload.get("client_secret"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
auth.get_tokens_authorization_code_grant(code)
|
||||||
|
token = auth.retrieve_token()
|
||||||
result_payload = {
|
result_payload = {
|
||||||
"user_id": cache_payload.get("user_id"),
|
"user_id": cache_payload.get("user_id"),
|
||||||
"client_id": cache_payload.get("client_id"),
|
"client_id": cache_payload.get("client_id"),
|
||||||
"client_secret": cache_payload.get("clientsecret"),
|
"client_secret": cache_payload.get("client_secret"),
|
||||||
"code": code,
|
"access_token": token.access_token,
|
||||||
|
"refresh_token": token.refresh_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
REDIS_CONN.set_obj(_web_result_cache_key(flow_id, "box"), result_payload, WEB_FLOW_TTL_SECS)
|
REDIS_CONN.set_obj(_web_result_cache_key(flow_id, "box"), result_payload, WEB_FLOW_TTL_SECS)
|
||||||
|
|
@ -462,12 +470,14 @@ async def poll_box_web_result():
|
||||||
req = await get_request_json()
|
req = await get_request_json()
|
||||||
flow_id = req.get("flow_id")
|
flow_id = req.get("flow_id")
|
||||||
|
|
||||||
cache_raw = REDIS_CONN.get_obj(_web_result_cache_key(flow_id, "box"))
|
cache_blob = REDIS_CONN.get(_web_result_cache_key(flow_id, "box"))
|
||||||
if not cache_raw:
|
if not cache_blob:
|
||||||
return get_json_result(code=RetCode.RUNNING, message="Authorization is still pending.")
|
return get_json_result(code=RetCode.RUNNING, message="Authorization is still pending.")
|
||||||
|
|
||||||
|
cache_raw = json.loads(cache_blob)
|
||||||
if cache_raw.get("user_id") != current_user.id:
|
if cache_raw.get("user_id") != current_user.id:
|
||||||
return get_json_result(code=RetCode.PERMISSION_ERROR, message="You are not allowed to access this authorization result.")
|
return get_json_result(code=RetCode.PERMISSION_ERROR, message="You are not allowed to access this authorization result.")
|
||||||
|
|
||||||
REDIS_CONN.delete(_web_result_cache_key(flow_id, "box"))
|
REDIS_CONN.delete(_web_result_cache_key(flow_id, "box"))
|
||||||
|
|
||||||
return get_json_result(data={"credentials": cache_raw})
|
return get_json_result(data={"credentials": cache_raw})
|
||||||
|
|
@ -15,9 +15,9 @@ from common.data_source.models import Document, GenerateDocumentsOutput
|
||||||
from common.data_source.utils import get_file_ext
|
from common.data_source.utils import get_file_ext
|
||||||
|
|
||||||
class BoxConnector(LoadConnector, PollConnector):
|
class BoxConnector(LoadConnector, PollConnector):
|
||||||
def __init__(self, folder_id: str = "0", batch_size: int = INDEX_BATCH_SIZE, use_marker: bool = False) -> None:
|
def __init__(self, folder_id: str, batch_size: int = INDEX_BATCH_SIZE, use_marker: bool = False) -> None:
|
||||||
self.batch_size = batch_size
|
self.batch_size = batch_size
|
||||||
self.folder_id = folder_id
|
self.folder_id = "0" if not folder_id else folder_id
|
||||||
self.use_marker = use_marker
|
self.use_marker = use_marker
|
||||||
|
|
||||||
def load_credentials(self, auth: Any):
|
def load_credentials(self, auth: Any):
|
||||||
|
|
@ -49,7 +49,7 @@ class BoxConnector(LoadConnector, PollConnector):
|
||||||
result = self.box_client.folders.get_folder_items(
|
result = self.box_client.folders.get_folder_items(
|
||||||
folder_id=folder_id,
|
folder_id=folder_id,
|
||||||
limit=self.batch_size,
|
limit=self.batch_size,
|
||||||
usemarker=self.usemarker
|
usemarker=self.use_marker
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import traceback
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from flask import json
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from api.db.services.connector_service import ConnectorService, SyncLogsService
|
from api.db.services.connector_service import ConnectorService, SyncLogsService
|
||||||
|
|
@ -48,7 +49,7 @@ from common.data_source.utils import load_all_docs_from_checkpoint_connector
|
||||||
from common.log_utils import init_root_logger
|
from common.log_utils import init_root_logger
|
||||||
from common.signal_utils import start_tracemalloc_and_snapshot, stop_tracemalloc
|
from common.signal_utils import start_tracemalloc_and_snapshot, stop_tracemalloc
|
||||||
from common.versions import get_ragflow_version
|
from common.versions import get_ragflow_version
|
||||||
from box_sdk_gen import BoxClient, BoxOAuth, OAuthConfig, GetAuthorizeUrlOptions
|
from box_sdk_gen import BoxOAuth, OAuthConfig, AccessToken
|
||||||
|
|
||||||
MAX_CONCURRENT_TASKS = int(os.environ.get("MAX_CONCURRENT_TASKS", "5"))
|
MAX_CONCURRENT_TASKS = int(os.environ.get("MAX_CONCURRENT_TASKS", "5"))
|
||||||
task_limiter = trio.Semaphore(MAX_CONCURRENT_TASKS)
|
task_limiter = trio.Semaphore(MAX_CONCURRENT_TASKS)
|
||||||
|
|
@ -611,14 +612,20 @@ class BOX(SyncBase):
|
||||||
use_marker=self.conf.get("use_marker", False)
|
use_marker=self.conf.get("use_marker", False)
|
||||||
)
|
)
|
||||||
|
|
||||||
credential = self.conf['credentials']
|
credential = json.loads(self.conf['credentials']['box_tokens'])
|
||||||
|
|
||||||
auth = BoxOAuth(
|
auth = BoxOAuth(
|
||||||
OAuthConfig(
|
OAuthConfig(
|
||||||
client_id=credential['client_id'],
|
client_id=credential['client_id'],
|
||||||
client_secret=credential['client_secret'],
|
client_secret=credential['client_secret'],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
auth.get_tokens_authorization_code_grant(credential['code'])
|
|
||||||
|
token = AccessToken(
|
||||||
|
access_token=credential['access_token'],
|
||||||
|
refresh_token=credential['refresh_token'],
|
||||||
|
)
|
||||||
|
auth.token_storage.store(token)
|
||||||
|
|
||||||
self.connector.load_credentials(auth)
|
self.connector.load_credentials(auth)
|
||||||
if task["reindex"] == "1" or not task["poll_range_start"]:
|
if task["reindex"] == "1" or not task["poll_range_start"]:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
|
|
@ -11,11 +11,14 @@ import {
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import message from '@/components/ui/message';
|
import message from '@/components/ui/message';
|
||||||
|
import {
|
||||||
|
pollBoxWebAuthResult,
|
||||||
|
startBoxWebAuth,
|
||||||
|
} from '@/services/data-source-service';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
export type BoxTokenFieldProps = {
|
export type BoxTokenFieldProps = {
|
||||||
/** 存储在表单里的值,约定为 JSON 字符串 */
|
|
||||||
value?: string;
|
value?: string;
|
||||||
/** 表单回写,用 JSON 字符串承载 client_id / client_secret / redirect_uri */
|
|
||||||
onChange: (value: any) => void;
|
onChange: (value: any) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
};
|
};
|
||||||
|
|
@ -24,8 +27,13 @@ type BoxCredentials = {
|
||||||
client_id?: string;
|
client_id?: string;
|
||||||
client_secret?: string;
|
client_secret?: string;
|
||||||
redirect_uri?: string;
|
redirect_uri?: string;
|
||||||
|
authorization_code?: string;
|
||||||
|
access_token?: string;
|
||||||
|
refresh_token?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BoxAuthStatus = 'idle' | 'waiting' | 'success' | 'error';
|
||||||
|
|
||||||
const parseBoxCredentials = (content?: string): BoxCredentials | null => {
|
const parseBoxCredentials = (content?: string): BoxCredentials | null => {
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -34,25 +42,30 @@ const parseBoxCredentials = (content?: string): BoxCredentials | null => {
|
||||||
client_id: parsed.client_id,
|
client_id: parsed.client_id,
|
||||||
client_secret: parsed.client_secret,
|
client_secret: parsed.client_secret,
|
||||||
redirect_uri: parsed.redirect_uri,
|
redirect_uri: parsed.redirect_uri,
|
||||||
|
authorization_code: parsed.authorization_code ?? parsed.code,
|
||||||
|
access_token: parsed.access_token,
|
||||||
|
refresh_token: parsed.refresh_token,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoxTokenField = ({
|
const BoxTokenField = ({ value, onChange }: BoxTokenFieldProps) => {
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
placeholder,
|
|
||||||
}: BoxTokenFieldProps) => {
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [clientId, setClientId] = useState('');
|
const [clientId, setClientId] = useState('');
|
||||||
const [clientSecret, setClientSecret] = useState('');
|
const [clientSecret, setClientSecret] = useState('');
|
||||||
const [redirectUri, setRedirectUri] = useState('');
|
const [redirectUri, setRedirectUri] = useState('');
|
||||||
|
const [submitLoading, setSubmitLoading] = useState(false);
|
||||||
|
const [webFlowId, setWebFlowId] = useState<string | null>(null);
|
||||||
|
const webFlowIdRef = useRef<string | null>(null);
|
||||||
|
const webPollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const [webStatus, setWebStatus] = useState<BoxAuthStatus>('idle');
|
||||||
|
const [webStatusMessage, setWebStatusMessage] = useState('');
|
||||||
|
|
||||||
const parsed = useMemo(() => parseBoxCredentials(value), [value]);
|
const parsed = useMemo(() => parseBoxCredentials(value), [value]);
|
||||||
|
const parsedRedirectUri = useMemo(() => parsed?.redirect_uri ?? '', [parsed]);
|
||||||
|
|
||||||
// 当外部 value 变化且弹窗关闭时,同步初始值
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dialogOpen) {
|
if (!dialogOpen) {
|
||||||
setClientId(parsed?.client_id ?? '');
|
setClientId(parsed?.client_id ?? '');
|
||||||
|
|
@ -61,6 +74,18 @@ const BoxTokenField = ({
|
||||||
}
|
}
|
||||||
}, [parsed, dialogOpen]);
|
}, [parsed, dialogOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
webFlowIdRef.current = webFlowId;
|
||||||
|
}, [webFlowId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (webPollTimerRef.current) {
|
||||||
|
clearTimeout(webPollTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const hasConfigured = useMemo(
|
const hasConfigured = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Boolean(
|
Boolean(
|
||||||
|
|
@ -69,15 +94,161 @@ const BoxTokenField = ({
|
||||||
[parsed],
|
[parsed],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenDialog = useCallback(() => {
|
const hasAuthorized = useMemo(
|
||||||
setDialogOpen(true);
|
() =>
|
||||||
|
Boolean(
|
||||||
|
parsed?.access_token ||
|
||||||
|
parsed?.refresh_token ||
|
||||||
|
parsed?.authorization_code,
|
||||||
|
),
|
||||||
|
[parsed],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetWebStatus = useCallback(() => {
|
||||||
|
setWebStatus('idle');
|
||||||
|
setWebStatusMessage('');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const clearWebState = useCallback(() => {
|
||||||
|
if (webPollTimerRef.current) {
|
||||||
|
clearTimeout(webPollTimerRef.current);
|
||||||
|
webPollTimerRef.current = null;
|
||||||
|
}
|
||||||
|
webFlowIdRef.current = null;
|
||||||
|
setWebFlowId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchWebResult = useCallback(
|
||||||
|
async (flowId: string) => {
|
||||||
|
try {
|
||||||
|
const { data } = await pollBoxWebAuthResult({ flow_id: flowId });
|
||||||
|
if (data.code === 0 && data.data?.credentials) {
|
||||||
|
const credentials = (data.data.credentials || {}) as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
const { user_id: _userId, code, ...rest } = credentials;
|
||||||
|
|
||||||
|
const finalValue: Record<string, any> = {
|
||||||
|
...rest,
|
||||||
|
// 确保客户端配置字段有值(优先后端返回,其次当前输入)
|
||||||
|
client_id: rest.client_id ?? clientId.trim(),
|
||||||
|
client_secret: rest.client_secret ?? clientSecret.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirect =
|
||||||
|
redirectUri.trim() || parsedRedirectUri || rest.redirect_uri;
|
||||||
|
if (redirect) {
|
||||||
|
finalValue.redirect_uri = redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
finalValue.authorization_code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// access_token / refresh_token 由后端返回,已在 ...rest 中带上,无需额外 state
|
||||||
|
|
||||||
|
onChange(JSON.stringify(finalValue));
|
||||||
|
message.success('Box authorization completed.');
|
||||||
|
clearWebState();
|
||||||
|
resetWebStatus();
|
||||||
|
setDialogOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.code === 106) {
|
||||||
|
setWebStatus('waiting');
|
||||||
|
setWebStatusMessage(
|
||||||
|
'Authorization confirmed. Finalizing credentials...',
|
||||||
|
);
|
||||||
|
if (webPollTimerRef.current) {
|
||||||
|
clearTimeout(webPollTimerRef.current);
|
||||||
|
}
|
||||||
|
webPollTimerRef.current = setTimeout(
|
||||||
|
() => fetchWebResult(flowId),
|
||||||
|
1500,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = data.message || 'Authorization failed.';
|
||||||
|
message.error(errorMessage);
|
||||||
|
setWebStatus('error');
|
||||||
|
setWebStatusMessage(errorMessage);
|
||||||
|
clearWebState();
|
||||||
|
} catch (_error) {
|
||||||
|
message.error('Unable to retrieve authorization result.');
|
||||||
|
setWebStatus('error');
|
||||||
|
setWebStatusMessage('Unable to retrieve authorization result.');
|
||||||
|
clearWebState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
clearWebState,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
parsedRedirectUri,
|
||||||
|
redirectUri,
|
||||||
|
resetWebStatus,
|
||||||
|
onChange,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (event: MessageEvent) => {
|
||||||
|
const payload = event.data;
|
||||||
|
if (!payload || payload.type !== 'ragflow-box-oauth') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetFlowId = payload.flowId || webFlowIdRef.current;
|
||||||
|
if (!targetFlowId) return;
|
||||||
|
if (webFlowIdRef.current && webFlowIdRef.current !== targetFlowId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.status === 'success') {
|
||||||
|
setWebStatus('waiting');
|
||||||
|
setWebStatusMessage(
|
||||||
|
'Authorization confirmed. Finalizing credentials...',
|
||||||
|
);
|
||||||
|
fetchWebResult(targetFlowId);
|
||||||
|
} else {
|
||||||
|
const errorMessage = payload.message || 'Authorization failed.';
|
||||||
|
message.error(errorMessage);
|
||||||
|
setWebStatus('error');
|
||||||
|
setWebStatusMessage(errorMessage);
|
||||||
|
clearWebState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handler);
|
||||||
|
return () => window.removeEventListener('message', handler);
|
||||||
|
}, [clearWebState, fetchWebResult]);
|
||||||
|
|
||||||
|
const handleOpenDialog = useCallback(() => {
|
||||||
|
resetWebStatus();
|
||||||
|
clearWebState();
|
||||||
|
setDialogOpen(true);
|
||||||
|
}, [clearWebState, resetWebStatus]);
|
||||||
|
|
||||||
const handleCloseDialog = useCallback(() => {
|
const handleCloseDialog = useCallback(() => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
}, []);
|
clearWebState();
|
||||||
|
resetWebStatus();
|
||||||
|
}, [clearWebState, resetWebStatus]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleManualWebCheck = useCallback(() => {
|
||||||
|
if (!webFlowId) {
|
||||||
|
message.info('Start browser authorization first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setWebStatus('waiting');
|
||||||
|
setWebStatusMessage('Checking authorization status...');
|
||||||
|
fetchWebResult(webFlowId);
|
||||||
|
}, [fetchWebResult, webFlowId]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(async () => {
|
||||||
if (!clientId.trim() || !clientSecret.trim() || !redirectUri.trim()) {
|
if (!clientId.trim() || !clientSecret.trim() || !redirectUri.trim()) {
|
||||||
message.error(
|
message.error(
|
||||||
'Please fill in Client ID, Client Secret, and Redirect URI.',
|
'Please fill in Client ID, Client Secret, and Redirect URI.',
|
||||||
|
|
@ -85,31 +256,91 @@ const BoxTokenField = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: BoxCredentials = {
|
const trimmedClientId = clientId.trim();
|
||||||
client_id: clientId.trim(),
|
const trimmedClientSecret = clientSecret.trim();
|
||||||
client_secret: clientSecret.trim(),
|
const trimmedRedirectUri = redirectUri.trim();
|
||||||
redirect_uri: redirectUri.trim(),
|
|
||||||
|
const payloadForStorage: BoxCredentials = {
|
||||||
|
client_id: trimmedClientId,
|
||||||
|
client_secret: trimmedClientSecret,
|
||||||
|
redirect_uri: trimmedRedirectUri,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setSubmitLoading(true);
|
||||||
|
resetWebStatus();
|
||||||
|
clearWebState();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
onChange(JSON.stringify(payload));
|
const { data } = await startBoxWebAuth({
|
||||||
message.success('Box credentials saved locally.');
|
client_id: trimmedClientId,
|
||||||
setDialogOpen(false);
|
client_secret: trimmedClientSecret,
|
||||||
} catch {
|
redirect_uri: trimmedRedirectUri,
|
||||||
message.error('Failed to save Box credentials.');
|
});
|
||||||
|
|
||||||
|
if (data.code === 0 && data.data?.authorization_url) {
|
||||||
|
onChange(JSON.stringify(payloadForStorage));
|
||||||
|
|
||||||
|
const popup = window.open(
|
||||||
|
data.data.authorization_url,
|
||||||
|
'ragflow-box-oauth',
|
||||||
|
'width=600,height=720',
|
||||||
|
);
|
||||||
|
if (!popup) {
|
||||||
|
message.error(
|
||||||
|
'Popup was blocked. Please allow popups for this site.',
|
||||||
|
);
|
||||||
|
clearWebState();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [clientId, clientSecret, redirectUri, onChange]);
|
popup.focus();
|
||||||
|
|
||||||
|
const flowId = data.data.flow_id;
|
||||||
|
setWebFlowId(flowId);
|
||||||
|
webFlowIdRef.current = flowId;
|
||||||
|
setWebStatus('waiting');
|
||||||
|
setWebStatusMessage(
|
||||||
|
'Complete the Box consent in the opened window and return here.',
|
||||||
|
);
|
||||||
|
message.info(
|
||||||
|
'Authorization window opened. Complete the Box consent to continue.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
message.error(data.message || 'Failed to start Box authorization.');
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
message.error('Failed to start Box authorization.');
|
||||||
|
} finally {
|
||||||
|
setSubmitLoading(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
clearWebState,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
redirectUri,
|
||||||
|
resetWebStatus,
|
||||||
|
onChange,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{hasConfigured && (
|
{(hasConfigured || hasAuthorized) && (
|
||||||
<div className="flex flex-wrap items-center gap-2 rounded-md border border-dashed border-muted-foreground/40 bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-3 rounded-md border border-dashed border-muted-foreground/40 bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
{hasAuthorized ? (
|
||||||
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-700">
|
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-emerald-700">
|
||||||
|
Authorized
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{hasConfigured ? (
|
||||||
|
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-blue-700">
|
||||||
Configured
|
Configured
|
||||||
</span>
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<p className="m-0">
|
<p className="m-0">
|
||||||
Box OAuth credentials have been configured. You can update them at
|
{hasAuthorized
|
||||||
any time.
|
? 'Box OAuth credentials are authorized and ready to use.'
|
||||||
|
: 'Box OAuth client information has been stored. Run the browser authorization to finalize the setup.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -143,7 +374,7 @@ const BoxTokenField = ({
|
||||||
<label className="text-sm font-medium">Client ID</label>
|
<label className="text-sm font-medium">Client ID</label>
|
||||||
<Input
|
<Input
|
||||||
value={clientId}
|
value={clientId}
|
||||||
placeholder={placeholder || 'Enter Box Client ID'}
|
placeholder="Enter Box Client ID"
|
||||||
onChange={(e) => setClientId(e.target.value)}
|
onChange={(e) => setClientId(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -164,13 +395,49 @@ const BoxTokenField = ({
|
||||||
onChange={(e) => setRedirectUri(e.target.value)}
|
onChange={(e) => setRedirectUri(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{webStatus !== 'idle' && (
|
||||||
|
<div className="rounded-md border border-dashed border-muted-foreground/40 bg-muted/10 px-4 py-4 text-sm text-muted-foreground">
|
||||||
|
<div className="text-sm font-semibold text-foreground">
|
||||||
|
Browser authorization
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className={`mt-2 text-xs ${
|
||||||
|
webStatus === 'error'
|
||||||
|
? 'text-destructive'
|
||||||
|
: 'text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{webStatusMessage}
|
||||||
|
</p>
|
||||||
|
{webStatus === 'waiting' && webFlowId ? (
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleManualWebCheck}
|
||||||
|
>
|
||||||
|
Refresh status
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="pt-3">
|
<DialogFooter className="pt-3">
|
||||||
<Button variant="ghost" onClick={handleCloseDialog}>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleCloseDialog}
|
||||||
|
disabled={submitLoading}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSubmit}>Submit</Button>
|
<Button onClick={handleSubmit} disabled={submitLoading}>
|
||||||
|
{submitLoading && (
|
||||||
|
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
Submit & Authorize
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -721,8 +721,7 @@ export const DataSourceFormDefaultValues = {
|
||||||
source: DataSourceKey.BOX,
|
source: DataSourceKey.BOX,
|
||||||
config: {
|
config: {
|
||||||
name: '',
|
name: '',
|
||||||
folder_id: '',
|
folder_id: '0',
|
||||||
index_recursively: false,
|
|
||||||
credentials: {
|
credentials: {
|
||||||
box_tokens: '',
|
box_tokens: '',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -47,4 +47,13 @@ export const startGmailWebAuth = (payload: { credentials: string }) =>
|
||||||
export const pollGmailWebAuthResult = (payload: { flow_id: string }) =>
|
export const pollGmailWebAuthResult = (payload: { flow_id: string }) =>
|
||||||
request.post(api.googleWebAuthResult('gmail'), { data: payload });
|
request.post(api.googleWebAuthResult('gmail'), { data: payload });
|
||||||
|
|
||||||
|
export const startBoxWebAuth = (payload: {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
redirect_uri?: string;
|
||||||
|
}) => request.post(api.boxWebAuthStart(), { data: payload });
|
||||||
|
|
||||||
|
export const pollBoxWebAuthResult = (payload: { flow_id: string }) =>
|
||||||
|
request.post(api.boxWebAuthResult(), { data: payload });
|
||||||
|
|
||||||
export default dataSourceService;
|
export default dataSourceService;
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ export default {
|
||||||
`${api_host}/connector/google/oauth/web/start?type=${type}`,
|
`${api_host}/connector/google/oauth/web/start?type=${type}`,
|
||||||
googleWebAuthResult: (type: 'google-drive' | 'gmail') =>
|
googleWebAuthResult: (type: 'google-drive' | 'gmail') =>
|
||||||
`${api_host}/connector/google/oauth/web/result?type=${type}`,
|
`${api_host}/connector/google/oauth/web/result?type=${type}`,
|
||||||
|
boxWebAuthStart: () => `${api_host}/connector/box/oauth/web/start`,
|
||||||
|
boxWebAuthResult: () => `${api_host}/connector/box/oauth/web/result`,
|
||||||
|
|
||||||
// plugin
|
// plugin
|
||||||
llm_tools: `${api_host}/plugin/llm_tools`,
|
llm_tools: `${api_host}/plugin/llm_tools`,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue