mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-08-14 00:36:02 +08:00
Feat: Add frontend support for third-party login integration (#7553)
### What problem does this PR solve? Add frontend support for third-party login integration: - Used `getLoginChannels` API to fetch available login channels from the server - Used `loginWithChannel` function to initiate login based on the selected channel - Refactored `useLoginWithGithub` hook to `useOAuthCallback` for generalized OAuth callback handling - Updated the login page to dynamically render third-party login buttons based on the fetched channel list - Styled third-party login buttons to improve user experience - Removed unused code snippets > This PR removes the previously hardcoded GitHub login button. Since the functionality only worked when `location.host` was equal to `demo.ragflow.io`, and the authentication logic is now based on `login.ragflow.io`, this change does not affect the existing logic and is considered a non-breaking change --- #### Frontend Screenshot && Backend Configuration  ```yaml # docker/service_conf.yaml.template # ... oauth: github: icon: github display_name: "Github" # ... custom_channel: display_name: "OIDC" # ... custom_channel_2: display_name: "OAuth2" # ... ``` --- - Related pull requests: - #7379 - #7521 - Related issues: - #3495 ### Type of change - [x] New Feature (non-breaking change which adds functionality) - [x] Refactoring - [x] Performance Improvement
This commit is contained in:
parent
d06431f670
commit
e7a6a9e47e
6
web/src/assets/svg/sso.svg
Normal file
6
web/src/assets/svg/sso.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1746329730618" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3652" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M434.286835 669.519808L284.185447 511.738503a31.249664 31.249664 0 0 1 1.023989-44.231525 31.249664 31.249664 0 0 1 44.231525 1.023989l106.791853 112.319793a31.341663 31.341663 0 0 0 44.231525 1.125988L696.095022 376.585955a31.249664 31.249664 0 0 1 44.231525 1.125988 31.249664 31.249664 0 0 1-1.023989 44.231524L478.52036 670.543797c-12.593865 11.876872-32.355652 11.466877-44.232525-1.023989" fill="#74BEFF" p-id="3653"></path>
|
||||
<path d="M510.567015 1023.989l-6.859926-1.535983a645.171069 645.171069 0 0 1-356.416171-219.930638 314.506622 314.506622 0 0 1-68.292267-199.657855V164.538232c0-15.051838 10.647886-27.9527 25.391728-30.819668A1615.736643 1615.736643 0 0 0 492.751207 6.142934l6.44993-3.070967a31.495662 31.495662 0 0 1 27.133709 0 1531.566548 1531.566548 0 0 0 391.123798 129.930604l2.149977 0.306997a31.228665 31.228665 0 0 1 26.723713 31.024667v433.103347a316.246603 316.246603 0 0 1-65.938291 196.48389 645.509066 645.509066 0 0 1-363.172099 228.327547l-6.654929 1.739981zM166.438712 184.812015c-14.334846 3.173966-24.573736 15.767831-24.675735 30.409673v387.540837a252.664286 252.664286 0 0 0 54.265417 160.033281c78.839153 96.039968 186.347998 164.436234 306.756705 194.947906a31.227665 31.227665 0 0 0 15.665832 0 582.458743 582.458743 0 0 0 312.899638-202.524825c34.606628-45.153515 53.139429-100.648919 52.422437-157.576307V215.732683A31.320664 31.320664 0 0 0 858.996273 185.323009 1596.402851 1596.402851 0 0 1 525.514855 72.286223a31.410663 31.410663 0 0 0-26.10872 0c-106.790853 49.146472-218.394654 86.826067-332.967423 112.525792" fill="#333333" p-id="3654"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
@ -3,7 +3,7 @@ import { message } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'umi';
|
||||
|
||||
export const useLoginWithGithub = () => {
|
||||
export const useOAuthCallback = () => {
|
||||
const [currentQueryParameters, setSearchParams] = useSearchParams();
|
||||
const error = currentQueryParameters.get('error');
|
||||
const newQueryParameters: URLSearchParams = useMemo(
|
||||
@ -12,26 +12,38 @@ export const useLoginWithGithub = () => {
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (error) {
|
||||
message.error(error);
|
||||
navigate('/login');
|
||||
newQueryParameters.delete('error');
|
||||
setSearchParams(newQueryParameters);
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
message.error(error);
|
||||
setTimeout(() => {
|
||||
navigate('/login');
|
||||
newQueryParameters.delete('error');
|
||||
setSearchParams(newQueryParameters);
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = currentQueryParameters.get('auth');
|
||||
const auth = currentQueryParameters.get('auth');
|
||||
if (auth) {
|
||||
authorizationUtil.setAuthorization(auth);
|
||||
newQueryParameters.delete('auth');
|
||||
setSearchParams(newQueryParameters);
|
||||
navigate('/knowledge');
|
||||
}
|
||||
}, [
|
||||
error,
|
||||
currentQueryParameters,
|
||||
newQueryParameters,
|
||||
navigate,
|
||||
setSearchParams,
|
||||
]);
|
||||
|
||||
if (auth) {
|
||||
authorizationUtil.setAuthorization(auth);
|
||||
newQueryParameters.delete('auth');
|
||||
setSearchParams(newQueryParameters);
|
||||
}
|
||||
return auth;
|
||||
console.debug(currentQueryParameters.get('auth'));
|
||||
return currentQueryParameters.get('auth');
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const auth = useLoginWithGithub();
|
||||
const auth = useOAuthCallback();
|
||||
const [isLogin, setIsLogin] = useState<Nullable<boolean>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { Authorization } from '@/constants/authorization';
|
||||
import userService from '@/services/user-service';
|
||||
import userService, {
|
||||
getLoginChannels,
|
||||
loginWithChannel,
|
||||
} from '@/services/user-service';
|
||||
import authorizationUtil, { redirectToLogin } from '@/utils/authorization-util';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { Form, message } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import { useEffect, useState } from 'react';
|
||||
@ -16,6 +19,36 @@ export interface IRegisterRequestBody extends ILoginRequestBody {
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
export interface ILoginChannel {
|
||||
channel: string;
|
||||
display_name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const useLoginChannels = () => {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['loginChannels'],
|
||||
queryFn: async () => {
|
||||
const { data: res = {} } = await getLoginChannels();
|
||||
return res.data || [];
|
||||
},
|
||||
});
|
||||
|
||||
return { channels: data as ILoginChannel[], loading: isLoading };
|
||||
};
|
||||
|
||||
export const useLoginWithChannel = () => {
|
||||
const { isPending: loading, mutateAsync } = useMutation({
|
||||
mutationKey: ['loginWithChannel'],
|
||||
mutationFn: async (channel: string) => {
|
||||
loginWithChannel(channel);
|
||||
return Promise.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
return { loading, login: mutateAsync };
|
||||
};
|
||||
|
||||
export const useLogin = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -67,8 +100,13 @@ export const useRegister = () => {
|
||||
const { data = {} } = await userService.register(params);
|
||||
if (data.code === 0) {
|
||||
message.success(t('message.registered'));
|
||||
} else if (data.message && data.message.includes('registration is disabled')) {
|
||||
message.error(t('message.registerDisabled') || 'User registration is disabled');
|
||||
} else if (
|
||||
data.message &&
|
||||
data.message.includes('registration is disabled')
|
||||
) {
|
||||
message.error(
|
||||
t('message.registerDisabled') || 'User registration is disabled',
|
||||
);
|
||||
}
|
||||
return data.code;
|
||||
},
|
||||
|
@ -19,6 +19,35 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.thirdPartyLoginButton {
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
// padding-top: 0px;
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d9d9d9;
|
||||
background: #fff;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loginRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { useLogin, useRegister } from '@/hooks/login-hooks';
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import { useAuth } from '@/hooks/auth-hooks';
|
||||
import {
|
||||
useLogin,
|
||||
useLoginChannels,
|
||||
useLoginWithChannel,
|
||||
useRegister,
|
||||
} from '@/hooks/login-hooks';
|
||||
import { useSystemConfig } from '@/hooks/system-hooks';
|
||||
import { rsaPsw } from '@/utils';
|
||||
import { Button, Checkbox, Form, Input } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, useNavigate } from 'umi';
|
||||
import { useNavigate } from 'umi';
|
||||
import RightPanel from './right-panel';
|
||||
|
||||
import { Domain } from '@/constants/common';
|
||||
import styles from './index.less';
|
||||
|
||||
const Login = () => {
|
||||
@ -15,11 +21,29 @@ const Login = () => {
|
||||
const navigate = useNavigate();
|
||||
const { login, loading: signLoading } = useLogin();
|
||||
const { register, loading: registerLoading } = useRegister();
|
||||
const { channels, loading: channelsLoading } = useLoginChannels();
|
||||
const { login: loginWithChannel, loading: loginWithChannelLoading } =
|
||||
useLoginWithChannel();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
const loading = signLoading || registerLoading;
|
||||
const loading =
|
||||
signLoading ||
|
||||
registerLoading ||
|
||||
channelsLoading ||
|
||||
loginWithChannelLoading;
|
||||
const { config } = useSystemConfig();
|
||||
const registerEnabled = config?.registerEnabled !== 0;
|
||||
|
||||
const { isLogin } = useAuth();
|
||||
useEffect(() => {
|
||||
if (isLogin) {
|
||||
navigate('/knowledge');
|
||||
}
|
||||
}, [isLogin, navigate]);
|
||||
|
||||
const handleLoginWithChannel = async (channel: string) => {
|
||||
await loginWithChannel(channel);
|
||||
};
|
||||
|
||||
const changeTitle = () => {
|
||||
if (title === 'login' && !registerEnabled) {
|
||||
return;
|
||||
@ -65,11 +89,6 @@ const Login = () => {
|
||||
// wrapperCol: { span: 8 },
|
||||
};
|
||||
|
||||
const toGoogle = () => {
|
||||
window.location.href =
|
||||
'https://github.com/login/oauth/authorize?scope=user:email&client_id=302129228f0d96055bee';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.loginPage}>
|
||||
<div className={styles.loginLeft}>
|
||||
@ -151,39 +170,28 @@ const Login = () => {
|
||||
>
|
||||
{title === 'login' ? t('login') : t('continue')}
|
||||
</Button>
|
||||
{title === 'login' && (
|
||||
<>
|
||||
{/* <Button
|
||||
block
|
||||
size="large"
|
||||
onClick={toGoogle}
|
||||
style={{ marginTop: 15 }}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
icon="local:google"
|
||||
style={{ verticalAlign: 'middle', marginRight: 5 }}
|
||||
/>
|
||||
Sign in with Google
|
||||
</div>
|
||||
</Button> */}
|
||||
{location.host === Domain && (
|
||||
{title === 'login' && channels && channels.length > 0 && (
|
||||
<div className={styles.thirdPartyLoginButton}>
|
||||
{channels.map((item) => (
|
||||
<Button
|
||||
key={item.channel}
|
||||
block
|
||||
size="large"
|
||||
onClick={toGoogle}
|
||||
style={{ marginTop: 15 }}
|
||||
onClick={() => handleLoginWithChannel(item.channel)}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon
|
||||
icon="local:github"
|
||||
style={{ verticalAlign: 'middle', marginRight: 5 }}
|
||||
<SvgIcon
|
||||
name={item.icon || 'sso'}
|
||||
width={20}
|
||||
height={20}
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
Sign in with Github
|
||||
Sign in with {item.display_name}
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
|
@ -123,6 +123,10 @@ const methods = {
|
||||
|
||||
const userService = registerServer<keyof typeof methods>(methods, request);
|
||||
|
||||
export const getLoginChannels = () => request.get(api.login_channels);
|
||||
export const loginWithChannel = (channel: string) =>
|
||||
(window.location.href = api.login_channel(channel));
|
||||
|
||||
export const listTenantUser = (tenantId: string) =>
|
||||
request.get(api.listTenantUser(tenantId));
|
||||
|
||||
|
@ -11,6 +11,8 @@ export default {
|
||||
user_info: `${api_host}/user/info`,
|
||||
tenant_info: `${api_host}/user/tenant_info`,
|
||||
set_tenant_info: `${api_host}/user/set_tenant_info`,
|
||||
login_channels: `${api_host}/user/login/channels`,
|
||||
login_channel: (channel: string) => `${api_host}/user/login/${channel}`,
|
||||
|
||||
// team
|
||||
addTenantUser: (tenantId: string) => `${api_host}/tenant/${tenantId}/user`,
|
||||
|
Loading…
x
Reference in New Issue
Block a user