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:
Magicbook1108
2026-02-26 19:19:40 +08:00
committed by GitHub
parent 22c4d72891
commit c03c537bf8
4 changed files with 176 additions and 18 deletions

View File

@ -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

View File

@ -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

View File

@ -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 });