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


![image](https://github.com/user-attachments/assets/190ad3a5-3718-409a-ad0e-01e7aca39069)

```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:
Chaoxi Weng
2025-05-14 12:19:28 +08:00
committed by GitHub
parent d06431f670
commit e7a6a9e47e
7 changed files with 153 additions and 54 deletions

View File

@ -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(() => {

View File

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