feat: git login

This commit is contained in:
archer
2023-08-07 17:19:04 +08:00
parent 206eb81bb4
commit ce729dff1f
12 changed files with 310 additions and 32 deletions

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View 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
};
}

View File

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

View 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;

View File

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

View File

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

View File

@@ -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;
};
/**
* 格式化时间成聊天格式
*/