mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 05:12:39 +00:00
feat: git login
This commit is contained in:
@@ -7,9 +7,11 @@
|
||||
"show_git": true,
|
||||
"beianText": "",
|
||||
"systemTitle": "FastAI",
|
||||
"authorText": "Made by FastAI Team."
|
||||
"authorText": "Made by FastAI Team.",
|
||||
"gitLoginKey": ""
|
||||
},
|
||||
"SystemParams": {
|
||||
"gitLoginSecret": "",
|
||||
"vectorMaxProcess": 15,
|
||||
"qaMaxProcess": 15,
|
||||
"pgIvfflatProbe": 20
|
||||
|
@@ -13,6 +13,7 @@ export const sendAuthCode = (data: {
|
||||
}) => POST('/user/sendAuthCode', data);
|
||||
|
||||
export const getTokenLogin = () => GET<UserType>('/user/account/tokenLogin');
|
||||
export const gitLogin = (code: string) => GET<ResLogin>('/user/account/gitLogin', { code });
|
||||
|
||||
export const postRegister = ({
|
||||
username,
|
||||
|
1
client/src/components/Icon/icons/fill/git.svg
Normal file
1
client/src/components/Icon/icons/fill/git.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?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="1691391466028" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4715" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M511.957333 21.333333C241.024 21.333333 21.333333 240.981333 21.333333 512c0 216.832 140.544 400.725333 335.573334 465.664 24.490667 4.394667 32.256-10.069333 32.256-23.082667 0-11.690667 0.256-44.245333 0-85.205333-136.448 29.610667-164.736-64.64-164.736-64.64-22.314667-56.704-54.4-71.765333-54.4-71.765333-44.586667-30.464 3.285333-29.824 3.285333-29.824 49.194667 3.413333 75.178667 50.517333 75.178667 50.517333 43.776 75.008 114.816 53.333333 142.762666 40.789333 4.522667-31.658667 17.152-53.376 31.189334-65.536-108.970667-12.458667-223.488-54.485333-223.488-242.602666 0-53.546667 19.114667-97.322667 50.517333-131.669334-5.034667-12.330667-21.930667-62.293333 4.778667-129.834666 0 0 41.258667-13.184 134.912 50.346666a469.802667 469.802667 0 0 1 122.88-16.554666c41.642667 0.213333 83.626667 5.632 122.88 16.554666 93.653333-63.488 134.784-50.346667 134.784-50.346666 26.752 67.541333 9.898667 117.504 4.864 129.834666 31.402667 34.346667 50.474667 78.122667 50.474666 131.669334 0 188.586667-114.730667 230.016-224.042666 242.090666 17.578667 15.232 33.578667 44.672 33.578666 90.453334v135.850666c0 13.141333 7.936 27.605333 32.853334 22.869334C862.250667 912.597333 1002.666667 728.746667 1002.666667 512 1002.666667 240.981333 783.018667 21.333333 511.957333 21.333333z" p-id="4716"></path></svg>
|
After Width: | Height: | Size: 1.6 KiB |
@@ -23,6 +23,7 @@ const map = {
|
||||
wx: require('./icons/wx.svg').default,
|
||||
out: require('./icons/out.svg').default,
|
||||
git: require('./icons/git.svg').default,
|
||||
gitFill: require('./icons/fill/git.svg').default,
|
||||
menu: require('./icons/menu.svg').default,
|
||||
edit: require('./icons/edit.svg').default,
|
||||
inform: require('./icons/inform.svg').default,
|
||||
|
@@ -7,6 +7,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
const unAuthPage: { [key: string]: boolean } = {
|
||||
'/': true,
|
||||
'/login': true,
|
||||
'/login/provider': true,
|
||||
'/appStore': true,
|
||||
'/chat/share': true
|
||||
};
|
||||
|
@@ -14,6 +14,7 @@ import { getUnreadCount } from '@/api/user';
|
||||
const pcUnShowLayoutRoute: Record<string, boolean> = {
|
||||
'/': true,
|
||||
'/login': true,
|
||||
'/login/provider': true,
|
||||
'/chat/share': true,
|
||||
'/app/edit': true,
|
||||
'/chat': true
|
||||
@@ -21,6 +22,7 @@ const pcUnShowLayoutRoute: Record<string, boolean> = {
|
||||
const phoneUnShowLayoutRoute: Record<string, boolean> = {
|
||||
'/': true,
|
||||
'/login': true,
|
||||
'/login/provider': true,
|
||||
'/chat/share': true
|
||||
};
|
||||
|
||||
|
116
client/src/pages/api/user/account/gitLogin.ts
Normal file
116
client/src/pages/api/user/account/gitLogin.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { User } from '@/service/models/user';
|
||||
import { generateToken, setCookie } from '@/service/utils/tools';
|
||||
import axios from 'axios';
|
||||
import { parseQueryString } from '@/utils/tools';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 8);
|
||||
|
||||
type GithubAccessTokenType = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
refresh_token_expires_in: number;
|
||||
token_type: 'bearer';
|
||||
scope: string;
|
||||
};
|
||||
type GithubUserType = {
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
const { code } = req.query as { code: string };
|
||||
|
||||
const { data: gitAccessToken } = await axios.post<string>(
|
||||
`https://github.com/login/oauth/access_token?client_id=${global.feConfigs.gitLoginKey}&client_secret=${global.systemEnv.gitLoginSecret}&code=${code}`
|
||||
);
|
||||
const jsonGitAccessToken = parseQueryString(gitAccessToken) as GithubAccessTokenType;
|
||||
|
||||
const access_token = jsonGitAccessToken?.access_token;
|
||||
if (!access_token) {
|
||||
throw new Error('access_token is null');
|
||||
}
|
||||
|
||||
const {
|
||||
data: { email, avatar_url }
|
||||
} = await axios.get<GithubUserType>('https://api.github.com/user', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
jsonRes(res, {
|
||||
data: await loginByUsername({ username: email, res })
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err?.code === 500) {
|
||||
jsonRes(res, {
|
||||
data: await registerUser({ username: email, avatar: avatar_url, res })
|
||||
});
|
||||
}
|
||||
throw new Error(err);
|
||||
}
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginByUsername({
|
||||
username,
|
||||
res
|
||||
}: {
|
||||
username: string;
|
||||
res: NextApiResponse;
|
||||
}) {
|
||||
const user = await User.findOne({ username });
|
||||
console.log(user, username);
|
||||
|
||||
if (!user) {
|
||||
return Promise.reject({
|
||||
code: 500
|
||||
});
|
||||
}
|
||||
|
||||
const token = generateToken(user._id);
|
||||
setCookie(res, token);
|
||||
return { user, token };
|
||||
}
|
||||
|
||||
export async function registerUser({
|
||||
username,
|
||||
avatar,
|
||||
res
|
||||
}: {
|
||||
username: string;
|
||||
avatar?: string;
|
||||
res: NextApiResponse;
|
||||
}) {
|
||||
const response = await User.create({
|
||||
username,
|
||||
avatar,
|
||||
password: nanoid()
|
||||
});
|
||||
|
||||
// 根据 id 获取用户信息
|
||||
const user = await User.findById(response._id);
|
||||
|
||||
if (!user) {
|
||||
throw new Error('获取用户信息异常');
|
||||
}
|
||||
|
||||
const token = generateToken(user._id);
|
||||
setCookie(res, token);
|
||||
|
||||
return {
|
||||
user,
|
||||
token
|
||||
};
|
||||
}
|
@@ -1,11 +1,14 @@
|
||||
import React, { useState, Dispatch, useCallback } from 'react';
|
||||
import { FormControl, Flex, Input, Button, FormErrorMessage, Box } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PageTypeEnum } from '@/constants/user';
|
||||
import { postLogin } from '@/api/user';
|
||||
import type { ResLogin } from '@/api/response/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { feConfigs } from '@/store/static';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
interface Props {
|
||||
setPageType: Dispatch<`${PageTypeEnum}`>;
|
||||
@@ -18,7 +21,10 @@ interface LoginFormType {
|
||||
}
|
||||
|
||||
const LoginForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
const router = useRouter();
|
||||
const { lastRoute = '/app/list' } = router.query as { lastRoute: string };
|
||||
const { toast } = useToast();
|
||||
const { setLoginStore } = useGlobalStore();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -52,6 +58,19 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
[loginSuccess, toast]
|
||||
);
|
||||
|
||||
const onclickGit = useCallback(() => {
|
||||
setLoginStore({
|
||||
provider: 'git',
|
||||
lastRoute
|
||||
});
|
||||
router.replace(
|
||||
`https://github.com/login/oauth/authorize?client_id=${
|
||||
feConfigs?.gitLoginKey
|
||||
}&redirect_uri=${`${location.origin}/login/provider`}&scope=user:email%20read:user`,
|
||||
'_self'
|
||||
);
|
||||
}, [lastRoute, setLoginStore]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
|
||||
@@ -117,6 +136,17 @@ const LoginForm = ({ setPageType, loginSuccess }: Props) => {
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
{feConfigs?.show_register && (
|
||||
<Flex mt={10} justifyContent={'center'} alignItems={'center'}>
|
||||
<MyIcon
|
||||
name="gitFill"
|
||||
w={'34px'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.800'}
|
||||
onClick={onclickGit}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
74
client/src/pages/login/provider.tsx
Normal file
74
client/src/pages/login/provider.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { ResLogin } from '@/api/response/user';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { setToken } from '@/utils/user';
|
||||
import { gitLogin } from '@/api/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import Loading from '@/components/Loading';
|
||||
|
||||
const provider = () => {
|
||||
const { loginStore } = useGlobalStore();
|
||||
const { setLastChatId, setLastChatAppId } = useChatStore();
|
||||
const { setUserInfo } = useUserStore();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { code } = router.query as { code?: string };
|
||||
|
||||
const loginSuccess = useCallback(
|
||||
(res: ResLogin) => {
|
||||
// init store
|
||||
setLastChatId('');
|
||||
setLastChatAppId('');
|
||||
|
||||
setUserInfo(res.user);
|
||||
setToken(res.token);
|
||||
setTimeout(() => {
|
||||
router.push(
|
||||
loginStore?.lastRoute ? decodeURIComponent(loginStore?.lastRoute) : '/app/list'
|
||||
);
|
||||
}, 100);
|
||||
},
|
||||
[setLastChatId, setLastChatAppId, setUserInfo, router, loginStore?.lastRoute]
|
||||
);
|
||||
|
||||
const authCode = useCallback(async () => {
|
||||
if (!code) return;
|
||||
if (!loginStore) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await (async () => {
|
||||
if (loginStore.provider === 'git') {
|
||||
return gitLogin(code);
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
if (!res) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: '登录异常'
|
||||
});
|
||||
return router.replace('/login');
|
||||
}
|
||||
loginSuccess(res);
|
||||
} catch (error) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: '登录异常'
|
||||
});
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [code, loginStore, loginSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
authCode();
|
||||
}, [authCode]);
|
||||
|
||||
return <Loading />;
|
||||
};
|
||||
|
||||
export default provider;
|
@@ -1,9 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import axios from 'axios';
|
||||
|
||||
type LoginStoreType = { provider: 'git'; lastRoute: string };
|
||||
|
||||
type State = {
|
||||
loginStore?: LoginStoreType;
|
||||
setLoginStore: (e: LoginStoreType) => void;
|
||||
loading: boolean;
|
||||
setLoading: (val: boolean) => null;
|
||||
screenWidth: number;
|
||||
@@ -16,39 +20,53 @@ type State = {
|
||||
|
||||
export const useGlobalStore = create<State>()(
|
||||
devtools(
|
||||
immer((set, get) => ({
|
||||
loading: false,
|
||||
setLoading: (val: boolean) => {
|
||||
set((state) => {
|
||||
state.loading = val;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
screenWidth: 600,
|
||||
setScreenWidth(val: number) {
|
||||
set((state) => {
|
||||
state.screenWidth = val;
|
||||
state.isPc = val < 900 ? false : true;
|
||||
});
|
||||
},
|
||||
isPc: undefined,
|
||||
initIsPc(val: boolean) {
|
||||
if (get().isPc !== undefined) return;
|
||||
|
||||
set((state) => {
|
||||
state.isPc = val;
|
||||
});
|
||||
},
|
||||
gitStar: 2700,
|
||||
async loadGitStar() {
|
||||
try {
|
||||
const { data: git } = await axios.get('https://api.github.com/repos/labring/FastGPT');
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
loginStore: undefined,
|
||||
setLoginStore(e) {
|
||||
set((state) => {
|
||||
state.loginStore = e;
|
||||
});
|
||||
},
|
||||
loading: false,
|
||||
setLoading: (val: boolean) => {
|
||||
set((state) => {
|
||||
state.loading = val;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
screenWidth: 600,
|
||||
setScreenWidth(val: number) {
|
||||
set((state) => {
|
||||
state.screenWidth = val;
|
||||
state.isPc = val < 900 ? false : true;
|
||||
});
|
||||
},
|
||||
isPc: undefined,
|
||||
initIsPc(val: boolean) {
|
||||
if (get().isPc !== undefined) return;
|
||||
|
||||
set((state) => {
|
||||
state.gitStar = git.stargazers_count;
|
||||
state.isPc = val;
|
||||
});
|
||||
} catch (error) {}
|
||||
},
|
||||
gitStar: 2700,
|
||||
async loadGitStar() {
|
||||
try {
|
||||
const { data: git } = await axios.get('https://api.github.com/repos/labring/FastGPT');
|
||||
|
||||
set((state) => {
|
||||
state.gitStar = git.stargazers_count;
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
})),
|
||||
{
|
||||
name: 'globalStore',
|
||||
partialize: (state) => ({
|
||||
loginStore: state.loginStore
|
||||
})
|
||||
}
|
||||
}))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
2
client/src/types/index.d.ts
vendored
2
client/src/types/index.d.ts
vendored
@@ -24,9 +24,11 @@ export type FeConfigsType = {
|
||||
beianText?: string;
|
||||
googleClientVerKey?: string;
|
||||
baiduTongjiUrl?: string;
|
||||
gitLoginKey?: string;
|
||||
};
|
||||
export type SystemEnvType = {
|
||||
googleServiceVerKey?: string;
|
||||
gitLoginSecret?: string;
|
||||
vectorMaxProcess: number;
|
||||
qaMaxProcess: number;
|
||||
pgIvfflatProbe: number;
|
||||
|
@@ -53,6 +53,36 @@ export const Obj2Query = (obj: Record<string, string | number>) => {
|
||||
return queryParams.toString();
|
||||
};
|
||||
|
||||
export const parseQueryString = (str: string) => {
|
||||
const queryObject: Record<string, any> = {};
|
||||
|
||||
const splitStr = str.split('?');
|
||||
|
||||
str = splitStr[1] || splitStr[0];
|
||||
|
||||
// 将字符串按照 '&' 分割成键值对数组
|
||||
const keyValuePairs = str.split('&');
|
||||
|
||||
// 遍历键值对数组,将每个键值对解析为对象的属性和值
|
||||
keyValuePairs.forEach(function (keyValuePair) {
|
||||
const pair = keyValuePair.split('=');
|
||||
const key = decodeURIComponent(pair[0]);
|
||||
const value = decodeURIComponent(pair[1] || '');
|
||||
|
||||
// 如果对象中已经存在该属性,则将值转换为数组
|
||||
if (queryObject.hasOwnProperty(key)) {
|
||||
if (!Array.isArray(queryObject[key])) {
|
||||
queryObject[key] = [queryObject[key]];
|
||||
}
|
||||
queryObject[key].push(value);
|
||||
} else {
|
||||
queryObject[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return queryObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化时间成聊天格式
|
||||
*/
|
||||
|
Reference in New Issue
Block a user