### 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 Improvementtags/v0.19.0
| @@ -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> | |||
| @@ -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`, | |||