mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-05-03 08:47:48 +08:00
Feat: optimize gmail/google-drive (#13230)
### What problem does this PR solve? Feat: optimize gmail/google-drive Now: <img width="700" alt="image" src="https://github.com/user-attachments/assets/0c4b6044-7209-4c4f-ac0c-32070b79daf7" /> <img width="700" alt="image" src="https://github.com/user-attachments/assets/406f93d8-9b0f-4f5a-b8bb-3936990f558c" /> ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -10,6 +10,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import message from '@/components/ui/message';
|
||||
import { FileMimeType } from '@/constants/common';
|
||||
import {
|
||||
@ -49,6 +50,51 @@ const describeCredentials = (content?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseJsonObject = (content?: string): Record<string, any> | null => {
|
||||
if (!content) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return typeof parsed === 'object' && parsed !== null ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const extractRedirectUri = (content?: string): string => {
|
||||
const parsed = parseJsonObject(content);
|
||||
if (!parsed) return '';
|
||||
|
||||
if (typeof parsed.redirect_uri === 'string' && parsed.redirect_uri.trim()) {
|
||||
return parsed.redirect_uri.trim();
|
||||
}
|
||||
|
||||
const redirectUris =
|
||||
parsed.web?.redirect_uris ?? parsed.installed?.redirect_uris;
|
||||
if (Array.isArray(redirectUris)) {
|
||||
const firstValidRedirect = redirectUris.find(
|
||||
(item) => typeof item === 'string' && item.trim(),
|
||||
);
|
||||
if (firstValidRedirect) {
|
||||
return firstValidRedirect.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const withRedirectUri = (credentials: string, redirectUri: string): string => {
|
||||
const trimmedRedirectUri = redirectUri.trim();
|
||||
if (!trimmedRedirectUri) return credentials;
|
||||
|
||||
const parsed = parseJsonObject(credentials);
|
||||
if (!parsed) return credentials;
|
||||
|
||||
return JSON.stringify({
|
||||
...parsed,
|
||||
redirect_uri: trimmedRedirectUri,
|
||||
});
|
||||
};
|
||||
|
||||
const GmailTokenField = ({
|
||||
value,
|
||||
onChange,
|
||||
@ -56,6 +102,7 @@ const GmailTokenField = ({
|
||||
}: GmailTokenFieldProps) => {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [pendingCredentials, setPendingCredentials] = useState<string>('');
|
||||
const [redirectUri, setRedirectUri] = useState('');
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [webAuthLoading, setWebAuthLoading] = useState(false);
|
||||
const [webFlowId, setWebFlowId] = useState<string | null>(null);
|
||||
@ -89,6 +136,12 @@ const GmailTokenField = ({
|
||||
webFlowIdRef.current = webFlowId;
|
||||
}, [webFlowId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialogOpen) {
|
||||
setRedirectUri(extractRedirectUri(value));
|
||||
}
|
||||
}, [dialogOpen, value]);
|
||||
|
||||
const credentialSummary = useMemo(() => describeCredentials(value), [value]);
|
||||
const hasVerifiedTokens = useMemo(
|
||||
() => Boolean(value && credentialHasRefreshToken(value)),
|
||||
@ -118,7 +171,11 @@ const GmailTokenField = ({
|
||||
flow_id: flowId,
|
||||
});
|
||||
if (data.code === 0 && data.data?.credentials) {
|
||||
onChange(data.data.credentials);
|
||||
const rawCredentials =
|
||||
typeof data.data.credentials === 'string'
|
||||
? data.data.credentials
|
||||
: JSON.stringify(data.data.credentials);
|
||||
onChange(withRedirectUri(rawCredentials, redirectUri));
|
||||
setPendingCredentials('');
|
||||
message.success('Gmail credentials verified.');
|
||||
resetDialog(false);
|
||||
@ -143,7 +200,7 @@ const GmailTokenField = ({
|
||||
clearWebState();
|
||||
}
|
||||
},
|
||||
[clearWebState, onChange, resetDialog],
|
||||
[clearWebState, onChange, redirectUri, resetDialog],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -198,8 +255,10 @@ const GmailTokenField = ({
|
||||
}
|
||||
setFiles([file]);
|
||||
clearWebState();
|
||||
const extractedRedirectUri = extractRedirectUri(text);
|
||||
setRedirectUri(extractedRedirectUri);
|
||||
if (credentialHasRefreshToken(text)) {
|
||||
onChange(text);
|
||||
onChange(withRedirectUri(text, extractedRedirectUri));
|
||||
setPendingCredentials('');
|
||||
message.success('Gmail OAuth credentials uploaded.');
|
||||
return;
|
||||
@ -223,11 +282,17 @@ const GmailTokenField = ({
|
||||
message.error('No Google credential file detected.');
|
||||
return;
|
||||
}
|
||||
if (!redirectUri.trim()) {
|
||||
message.error('Please fill in Redirect URI.');
|
||||
return;
|
||||
}
|
||||
const trimmedRedirectUri = redirectUri.trim();
|
||||
setWebAuthLoading(true);
|
||||
clearWebState();
|
||||
try {
|
||||
const { data } = await startGmailWebAuth({
|
||||
credentials: pendingCredentials,
|
||||
redirect_uri: trimmedRedirectUri,
|
||||
});
|
||||
if (data.code === 0 && data.data?.authorization_url) {
|
||||
const flowId = data.data.flow_id;
|
||||
@ -255,7 +320,7 @@ const GmailTokenField = ({
|
||||
} finally {
|
||||
setWebAuthLoading(false);
|
||||
}
|
||||
}, [clearWebState, pendingCredentials]);
|
||||
}, [clearWebState, pendingCredentials, redirectUri]);
|
||||
|
||||
const handleManualWebCheck = useCallback(() => {
|
||||
if (!webFlowId) {
|
||||
@ -334,6 +399,14 @@ const GmailTokenField = ({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">Redirect URI</label>
|
||||
<Input
|
||||
value={redirectUri}
|
||||
placeholder="https://example.com/gmail/oauth/callback"
|
||||
onChange={(e) => setRedirectUri(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
Authorize in browser
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import message from '@/components/ui/message';
|
||||
import { FileMimeType } from '@/constants/common';
|
||||
import {
|
||||
@ -47,12 +48,58 @@ const describeCredentials = (content?: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseJsonObject = (content?: string): Record<string, any> | null => {
|
||||
if (!content) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return typeof parsed === 'object' && parsed !== null ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const extractRedirectUri = (content?: string): string => {
|
||||
const parsed = parseJsonObject(content);
|
||||
if (!parsed) return '';
|
||||
|
||||
if (typeof parsed.redirect_uri === 'string' && parsed.redirect_uri.trim()) {
|
||||
return parsed.redirect_uri.trim();
|
||||
}
|
||||
|
||||
const redirectUris =
|
||||
parsed.web?.redirect_uris ?? parsed.installed?.redirect_uris;
|
||||
if (Array.isArray(redirectUris)) {
|
||||
const firstValidRedirect = redirectUris.find(
|
||||
(item) => typeof item === 'string' && item.trim(),
|
||||
);
|
||||
if (firstValidRedirect) {
|
||||
return firstValidRedirect.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const withRedirectUri = (credentials: string, redirectUri: string): string => {
|
||||
const trimmedRedirectUri = redirectUri.trim();
|
||||
if (!trimmedRedirectUri) return credentials;
|
||||
|
||||
const parsed = parseJsonObject(credentials);
|
||||
if (!parsed) return credentials;
|
||||
|
||||
return JSON.stringify({
|
||||
...parsed,
|
||||
redirect_uri: trimmedRedirectUri,
|
||||
});
|
||||
};
|
||||
|
||||
const GoogleDriveTokenField = ({
|
||||
value,
|
||||
onChange,
|
||||
}: GoogleDriveTokenFieldProps) => {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [pendingCredentials, setPendingCredentials] = useState<string>('');
|
||||
const [redirectUri, setRedirectUri] = useState('');
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [webAuthLoading, setWebAuthLoading] = useState(false);
|
||||
const [webFlowId, setWebFlowId] = useState<string | null>(null);
|
||||
@ -86,6 +133,12 @@ const GoogleDriveTokenField = ({
|
||||
webFlowIdRef.current = webFlowId;
|
||||
}, [webFlowId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialogOpen) {
|
||||
setRedirectUri(extractRedirectUri(value));
|
||||
}
|
||||
}, [dialogOpen, value]);
|
||||
|
||||
const credentialSummary = useMemo(() => describeCredentials(value), [value]);
|
||||
const hasVerifiedTokens = useMemo(
|
||||
() => Boolean(value && credentialHasRefreshToken(value)),
|
||||
@ -115,7 +168,11 @@ const GoogleDriveTokenField = ({
|
||||
flow_id: flowId,
|
||||
});
|
||||
if (data.code === 0 && data.data?.credentials) {
|
||||
onChange(data.data.credentials);
|
||||
const rawCredentials =
|
||||
typeof data.data.credentials === 'string'
|
||||
? data.data.credentials
|
||||
: JSON.stringify(data.data.credentials);
|
||||
onChange(withRedirectUri(rawCredentials, redirectUri));
|
||||
setPendingCredentials('');
|
||||
message.success('Google Drive credentials verified.');
|
||||
resetDialog(false);
|
||||
@ -140,7 +197,7 @@ const GoogleDriveTokenField = ({
|
||||
clearWebState();
|
||||
}
|
||||
},
|
||||
[clearWebState, onChange, resetDialog],
|
||||
[clearWebState, onChange, redirectUri, resetDialog],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -195,8 +252,10 @@ const GoogleDriveTokenField = ({
|
||||
}
|
||||
setFiles([file]);
|
||||
clearWebState();
|
||||
const extractedRedirectUri = extractRedirectUri(text);
|
||||
setRedirectUri(extractedRedirectUri);
|
||||
if (credentialHasRefreshToken(text)) {
|
||||
onChange(text);
|
||||
onChange(withRedirectUri(text, extractedRedirectUri));
|
||||
setPendingCredentials('');
|
||||
message.success('OAuth credentials uploaded.');
|
||||
return;
|
||||
@ -220,11 +279,17 @@ const GoogleDriveTokenField = ({
|
||||
message.error('No Google credential file detected.');
|
||||
return;
|
||||
}
|
||||
if (!redirectUri.trim()) {
|
||||
message.error('Please fill in Redirect URI.');
|
||||
return;
|
||||
}
|
||||
const trimmedRedirectUri = redirectUri.trim();
|
||||
setWebAuthLoading(true);
|
||||
clearWebState();
|
||||
try {
|
||||
const { data } = await startGoogleDriveWebAuth({
|
||||
credentials: pendingCredentials,
|
||||
redirect_uri: trimmedRedirectUri,
|
||||
});
|
||||
if (data.code === 0 && data.data?.authorization_url) {
|
||||
const flowId = data.data.flow_id;
|
||||
@ -252,7 +317,7 @@ const GoogleDriveTokenField = ({
|
||||
} finally {
|
||||
setWebAuthLoading(false);
|
||||
}
|
||||
}, [clearWebState, pendingCredentials]);
|
||||
}, [clearWebState, pendingCredentials, redirectUri]);
|
||||
|
||||
const handleManualWebCheck = useCallback(() => {
|
||||
if (!webFlowId) {
|
||||
@ -330,6 +395,14 @@ const GoogleDriveTokenField = ({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">Redirect URI</label>
|
||||
<Input
|
||||
value={redirectUri}
|
||||
placeholder="https://example.com/google-drive/oauth/callback"
|
||||
onChange={(e) => setRedirectUri(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
Authorize in browser
|
||||
|
||||
@ -33,16 +33,20 @@ export const getDataSourceLogs = (id: string, params?: any) =>
|
||||
export const featchDataSourceDetail = (id: string) =>
|
||||
request.get(api.dataSourceDetail(id));
|
||||
|
||||
export const startGoogleDriveWebAuth = (payload: { credentials: string }) =>
|
||||
request.post(api.googleWebAuthStart('google-drive'), { data: payload });
|
||||
export const startGoogleDriveWebAuth = (payload: {
|
||||
credentials: string;
|
||||
redirect_uri?: string;
|
||||
}) => request.post(api.googleWebAuthStart('google-drive'), { data: payload });
|
||||
|
||||
export const pollGoogleDriveWebAuthResult = (payload: { flow_id: string }) =>
|
||||
request.post(api.googleWebAuthResult('google-drive'), { data: payload });
|
||||
|
||||
// Gmail web auth follows the same pattern as Google Drive, but uses
|
||||
// Gmail-specific endpoints and is consumed by the GmailTokenField UI.
|
||||
export const startGmailWebAuth = (payload: { credentials: string }) =>
|
||||
request.post(api.googleWebAuthStart('gmail'), { data: payload });
|
||||
export const startGmailWebAuth = (payload: {
|
||||
credentials: string;
|
||||
redirect_uri?: string;
|
||||
}) => request.post(api.googleWebAuthStart('gmail'), { data: payload });
|
||||
|
||||
export const pollGmailWebAuthResult = (payload: { flow_id: string }) =>
|
||||
request.post(api.googleWebAuthResult('gmail'), { data: payload });
|
||||
|
||||
Reference in New Issue
Block a user