Browse Source

Merge remote-tracking branch 'origin/main'

# Conflicts:
#	web/src/components/PersonalSetting.js
CaIon 2 years ago
parent
commit
377da2dfcb

+ 9 - 2
README.md

@@ -86,10 +86,10 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 
    + [x] [360 智脑](https://ai.360.cn)
 2. 支持配置镜像以及众多第三方代理服务:
    + [x] [OpenAI-SB](https://openai-sb.com)
+   + [x] [CloseAI](https://console.closeai-asia.com/r/2412)
    + [x] [API2D](https://api2d.com/r/197971)
    + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
    + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`)
-   + [x] [CloseAI](https://console.closeai-asia.com/r/2412)
    + [x] 自定义渠道:例如各种未收录的第三方代理服务
 3. 支持通过**负载均衡**的方式访问多个渠道。
 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
@@ -226,6 +226,13 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
 
 注意修改端口号、`OPENAI_API_BASE_URL` 和 `OPENAI_API_KEY`。
 
+#### QChatGPT - QQ机器人
+项目主页:https://github.com/RockChinQ/QChatGPT
+
+根据文档完成部署后,在`config.py`设置配置项`openai_config`的`reverse_proxy`为 One API 后端地址,设置`api_key`为 One API 生成的key,并在配置项`completion_api_params`的`model`参数设置为 One API 支持的模型名称。
+
+可安装 [Switcher 插件](https://github.com/RockChinQ/Switcher)在运行时切换所使用的模型。
+
 ### 部署到第三方平台
 <details>
 <summary><strong>部署到 Sealos </strong></summary>
@@ -379,4 +386,4 @@ https://openai.justsong.cn
 
 同样适用于基于本项目的二开项目。
 
-依据 MIT 协议,使用者需自行承担使用本项目的风险与责任,本开源项目开发者与此无关。
+依据 MIT 协议,使用者需自行承担使用本项目的风险与责任,本开源项目开发者与此无关。

+ 27 - 0
controller/github.go

@@ -79,6 +79,14 @@ func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
 
 func GitHubOAuth(c *gin.Context) {
 	session := sessions.Default(c)
+	state := c.Query("state")
+	if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
+		c.JSON(http.StatusForbidden, gin.H{
+			"success": false,
+			"message": "state is empty or not same",
+		})
+		return
+	}
 	username := session.Get("username")
 	if username != nil {
 		GitHubBind(c)
@@ -205,3 +213,22 @@ func GitHubBind(c *gin.Context) {
 	})
 	return
 }
+
+func GenerateOAuthCode(c *gin.Context) {
+	session := sessions.Default(c)
+	state := common.GetRandomString(12)
+	session.Set("oauth_state", state)
+	err := session.Save()
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    state,
+	})
+}

+ 9 - 0
controller/relay-text.go

@@ -357,6 +357,15 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 		isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
 
 		if resp.StatusCode != http.StatusOK {
+			if preConsumedQuota != 0 {
+				go func() {
+					// return pre-consumed quota
+					err := model.PostConsumeTokenQuota(tokenId, -preConsumedQuota)
+					if err != nil {
+						common.SysError("error return pre-consumed quota: " + err.Error())
+					}
+				}()
+			}
 			return relayErrorHandler(resp)
 		}
 	}

+ 1 - 0
router/api-router.go

@@ -22,6 +22,7 @@ func SetApiRouter(router *gin.Engine) {
 		apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
 		apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
 		apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
+		apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
 		apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
 		apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
 		apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)

+ 5 - 4
web/src/components/GitHubOAuth.js

@@ -13,8 +13,8 @@ const GitHubOAuth = () => {
 
   let navigate = useNavigate();
 
-  const sendCode = async (code, count) => {
-    const res = await API.get(`/api/oauth/github?code=${code}`);
+  const sendCode = async (code, state, count) => {
+    const res = await API.get(`/api/oauth/github?code=${code}&state=${state}`);
     const { success, message, data } = res.data;
     if (success) {
       if (message === 'bind') {
@@ -36,13 +36,14 @@ const GitHubOAuth = () => {
       count++;
       setPrompt(`出现错误,第 ${count} 次重试中...`);
       await new Promise((resolve) => setTimeout(resolve, count * 2000));
-      await sendCode(code, count);
+      await sendCode(code, state, count);
     }
   };
 
   useEffect(() => {
     let code = searchParams.get('code');
-    sendCode(code, 0).then();
+    let state = searchParams.get('state');
+    sendCode(code, state, 0).then();
   }, []);
 
   return (

+ 2 - 7
web/src/components/LoginForm.js

@@ -3,6 +3,7 @@ import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } f
 import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 import { UserContext } from '../context/User';
 import { API, getLogo, showError, showSuccess } from '../helpers';
+import { getOAuthState, onGitHubOAuthClicked } from './utils';
 
 const LoginForm = () => {
   const [inputs, setInputs] = useState({
@@ -31,12 +32,6 @@ const LoginForm = () => {
 
   const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
 
-  const onGitHubOAuthClicked = () => {
-    window.open(
-      `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
-    );
-  };
-
   const onWeChatLoginClicked = () => {
     setShowWeChatLoginModal(true);
   };
@@ -131,7 +126,7 @@ const LoginForm = () => {
                 circular
                 color='black'
                 icon='github'
-                onClick={onGitHubOAuthClicked}
+                onClick={()=>onGitHubOAuthClicked(status.github_client_id)}
               />
             ) : (
               <></>

+ 349 - 354
web/src/components/PersonalSetting.js

@@ -4,378 +4,373 @@ import { Link, useNavigate } from 'react-router-dom';
 import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
 import Turnstile from 'react-turnstile';
 import { UserContext } from '../context/User';
+import { onGitHubOAuthClicked } from './utils';
 
 const PersonalSetting = () => {
-    const [userState, userDispatch] = useContext(UserContext);
-    let navigate = useNavigate();
+  const [userState, userDispatch] = useContext(UserContext);
+  let navigate = useNavigate();
 
-    const [inputs, setInputs] = useState({
-        wechat_verification_code: '',
-        email_verification_code: '',
-        email: '',
-        self_account_deletion_confirmation: ''
-    });
-    const [status, setStatus] = useState({});
-    const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
-    const [showEmailBindModal, setShowEmailBindModal] = useState(false);
-    const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
-    const [turnstileEnabled, setTurnstileEnabled] = useState(false);
-    const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
-    const [turnstileToken, setTurnstileToken] = useState('');
-    const [loading, setLoading] = useState(false);
-    const [disableButton, setDisableButton] = useState(false);
-    const [countdown, setCountdown] = useState(30);
-    const [affLink, setAffLink] = useState("");
-    const [systemToken, setSystemToken] = useState("");
+  const [inputs, setInputs] = useState({
+    wechat_verification_code: '',
+    email_verification_code: '',
+    email: '',
+    self_account_deletion_confirmation: ''
+  });
+  const [status, setStatus] = useState({});
+  const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
+  const [showEmailBindModal, setShowEmailBindModal] = useState(false);
+  const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
+  const [turnstileEnabled, setTurnstileEnabled] = useState(false);
+  const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
+  const [turnstileToken, setTurnstileToken] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [disableButton, setDisableButton] = useState(false);
+  const [countdown, setCountdown] = useState(30);
+  const [affLink, setAffLink] = useState("");
+  const [systemToken, setSystemToken] = useState("");
 
-    useEffect(() => {
-        let status = localStorage.getItem('status');
-        if (status) {
-            status = JSON.parse(status);
-            setStatus(status);
-            if (status.turnstile_check) {
-                setTurnstileEnabled(true);
-                setTurnstileSiteKey(status.turnstile_site_key);
-            }
-        }
-    }, []);
+  useEffect(() => {
+    let status = localStorage.getItem('status');
+    if (status) {
+      status = JSON.parse(status);
+      setStatus(status);
+      if (status.turnstile_check) {
+        setTurnstileEnabled(true);
+        setTurnstileSiteKey(status.turnstile_site_key);
+      }
+    }
+  }, []);
 
-    useEffect(() => {
-        let countdownInterval = null;
-        if (disableButton && countdown > 0) {
-            countdownInterval = setInterval(() => {
-                setCountdown(countdown - 1);
-            }, 1000);
-        } else if (countdown === 0) {
-            setDisableButton(false);
-            setCountdown(30);
-        }
-        return () => clearInterval(countdownInterval); // Clean up on unmount
-    }, [disableButton, countdown]);
+  useEffect(() => {
+    let countdownInterval = null;
+    if (disableButton && countdown > 0) {
+      countdownInterval = setInterval(() => {
+        setCountdown(countdown - 1);
+      }, 1000);
+    } else if (countdown === 0) {
+      setDisableButton(false);
+      setCountdown(30);
+    }
+    return () => clearInterval(countdownInterval); // Clean up on unmount
+  }, [disableButton, countdown]);
 
-    const handleInputChange = (e, { name, value }) => {
-        setInputs((inputs) => ({ ...inputs, [name]: value }));
-    };
+  const handleInputChange = (e, { name, value }) => {
+    setInputs((inputs) => ({ ...inputs, [name]: value }));
+  };
 
-    const generateAccessToken = async () => {
-        const res = await API.get('/api/user/token');
-        const { success, message, data } = res.data;
-        if (success) {
-            setSystemToken(data);
-            setAffLink("");
-            await copy(data);
-            showSuccess(`令牌已重置并已复制到剪贴板`);
-        } else {
-            showError(message);
-        }
-    };
+  const generateAccessToken = async () => {
+    const res = await API.get('/api/user/token');
+    const { success, message, data } = res.data;
+    if (success) {
+      setSystemToken(data);
+      setAffLink(""); 
+      await copy(data);
+      showSuccess(`令牌已重置并已复制到剪贴板`);
+    } else {
+      showError(message);
+    }
+  };
 
-    const getAffLink = async () => {
-        const res = await API.get('/api/user/aff');
-        const { success, message, data } = res.data;
-        if (success) {
-            let link = `${window.location.origin}/register?aff=${data}`;
-            setAffLink(link);
-            setSystemToken("");
-            await copy(link);
-            showSuccess(`邀请链接已复制到剪切板`);
-        } else {
-            showError(message);
-        }
-    };
+  const getAffLink = async () => {
+    const res = await API.get('/api/user/aff');
+    const { success, message, data } = res.data;
+    if (success) {
+      let link = `${window.location.origin}/register?aff=${data}`;
+      setAffLink(link);
+      setSystemToken("");
+      await copy(link);
+      showSuccess(`邀请链接已复制到剪切板`);
+    } else {
+      showError(message);
+    }
+  };
 
-    const handleAffLinkClick = async (e) => {
-        e.target.select();
-        await copy(e.target.value);
-        showSuccess(`邀请链接已复制到剪切板`);
-    };
+  const handleAffLinkClick = async (e) => {
+    e.target.select();
+    await copy(e.target.value);
+    showSuccess(`邀请链接已复制到剪切板`);
+  };
 
-    const handleSystemTokenClick = async (e) => {
-        e.target.select();
-        await copy(e.target.value);
-        showSuccess(`系统令牌已复制到剪切板`);
-    };
+  const handleSystemTokenClick = async (e) => {
+    e.target.select();
+    await copy(e.target.value);
+    showSuccess(`系统令牌已复制到剪切板`);
+  };
 
-    const deleteAccount = async () => {
-        if (inputs.self_account_deletion_confirmation !== userState.user.username) {
-            showError('请输入你的账户名以确认删除!');
-            return;
-        }
+  const deleteAccount = async () => {
+    if (inputs.self_account_deletion_confirmation !== userState.user.username) {
+      showError('请输入你的账户名以确认删除!');
+      return;
+    }
 
-        const res = await API.delete('/api/user/self');
-        const { success, message } = res.data;
+    const res = await API.delete('/api/user/self');
+    const { success, message } = res.data;
 
-        if (success) {
-            showSuccess('账户已删除!');
-            await API.get('/api/user/logout');
-            userDispatch({ type: 'logout' });
-            localStorage.removeItem('user');
-            navigate('/login');
-        } else {
-            showError(message);
-        }
-    };
+    if (success) {
+      showSuccess('账户已删除!');
+      await API.get('/api/user/logout');
+      userDispatch({ type: 'logout' });
+      localStorage.removeItem('user');
+      navigate('/login');
+    } else {
+      showError(message);
+    }
+  };
 
-    const bindWeChat = async () => {
-        if (inputs.wechat_verification_code === '') return;
-        const res = await API.get(
-            `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
-        );
-        const { success, message } = res.data;
-        if (success) {
-            showSuccess('微信账户绑定成功!');
-            setShowWeChatBindModal(false);
-        } else {
-            showError(message);
-        }
-    };
-
-    const openGitHubOAuth = () => {
-        window.open(
-            `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
-        );
-    };
-
-    const sendVerificationCode = async () => {
-        setDisableButton(true);
-        if (inputs.email === '') return;
-        if (turnstileEnabled && turnstileToken === '') {
-            showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
-            return;
-        }
-        setLoading(true);
-        const res = await API.get(
-            `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
-        );
-        const { success, message } = res.data;
-        if (success) {
-            showSuccess('验证码发送成功,请检查邮箱!');
-        } else {
-            showError(message);
-        }
-        setLoading(false);
-    };
+  const bindWeChat = async () => {
+    if (inputs.wechat_verification_code === '') return;
+    const res = await API.get(
+      `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
+    );
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess('微信账户绑定成功!');
+      setShowWeChatBindModal(false);
+    } else {
+      showError(message);
+    }
+  };
 
-    const bindEmail = async () => {
-        if (inputs.email_verification_code === '') return;
-        setLoading(true);
-        const res = await API.get(
-            `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
-        );
-        const { success, message } = res.data;
-        if (success) {
-            showSuccess('邮箱账户绑定成功!');
-            setShowEmailBindModal(false);
-        } else {
-            showError(message);
-        }
-        setLoading(false);
-    };
+  const sendVerificationCode = async () => {
+    setDisableButton(true);
+    if (inputs.email === '') return;
+    if (turnstileEnabled && turnstileToken === '') {
+      showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
+      return;
+    }
+    setLoading(true);
+    const res = await API.get(
+      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
+    );
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess('验证码发送成功,请检查邮箱!');
+    } else {
+      showError(message);
+    }
+    setLoading(false);
+  };
 
-    return (
-        <div style={{ lineHeight: '40px' }}>
-            <Header as='h3'>通用设置</Header>
-            <Message>
-                注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。
-            </Message>
-            <Button as={Link} to={`/user/edit/`}>
-                更新个人信息
-            </Button>
-            <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
-            <Button onClick={getAffLink}>复制邀请链接</Button>
-            <Button onClick={() => {
-                setShowAccountDeleteModal(true);
-            }}>删除个人账户</Button>
+  const bindEmail = async () => {
+    if (inputs.email_verification_code === '') return;
+    setLoading(true);
+    const res = await API.get(
+      `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
+    );
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess('邮箱账户绑定成功!');
+      setShowEmailBindModal(false);
+    } else {
+      showError(message);
+    }
+    setLoading(false);
+  };
 
-            {systemToken && (
-                <Form.Input
-                    fluid
-                    readOnly
-                    value={systemToken}
-                    onClick={handleSystemTokenClick}
-                    style={{ marginTop: '10px' }}
+  return (
+    <div style={{ lineHeight: '40px' }}>
+      <Header as='h3'>通用设置</Header>
+      <Message>
+        注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。
+      </Message>
+      <Button as={Link} to={`/user/edit/`}>
+        更新个人信息
+      </Button>
+      <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
+      <Button onClick={getAffLink}>复制邀请链接</Button>
+      <Button onClick={() => {
+        setShowAccountDeleteModal(true);
+      }}>删除个人账户</Button>
+      
+      {systemToken && (
+        <Form.Input 
+          fluid 
+          readOnly 
+          value={systemToken} 
+          onClick={handleSystemTokenClick}
+          style={{ marginTop: '10px' }}
+        />
+      )}
+      {affLink && (
+        <Form.Input 
+          fluid 
+          readOnly 
+          value={affLink} 
+          onClick={handleAffLinkClick}
+          style={{ marginTop: '10px' }}
+        />
+      )}
+      <Divider />
+      <Header as='h3'>账号绑定</Header>
+      {
+        status.wechat_login && (
+          <Button
+            onClick={() => {
+              setShowWeChatBindModal(true);
+            }}
+          >
+            绑定微信账号
+          </Button>
+        )
+      }
+      <Modal
+        onClose={() => setShowWeChatBindModal(false)}
+        onOpen={() => setShowWeChatBindModal(true)}
+        open={showWeChatBindModal}
+        size={'mini'}
+      >
+        <Modal.Content>
+          <Modal.Description>
+            <Image src={status.wechat_qrcode} fluid />
+            <div style={{ textAlign: 'center' }}>
+              <p>
+                微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
+              </p>
+            </div>
+            <Form size='large'>
+              <Form.Input
+                fluid
+                placeholder='验证码'
+                name='wechat_verification_code'
+                value={inputs.wechat_verification_code}
+                onChange={handleInputChange}
+              />
+              <Button color='' fluid size='large' onClick={bindWeChat}>
+                绑定
+              </Button>
+            </Form>
+          </Modal.Description>
+        </Modal.Content>
+      </Modal>
+      {
+        status.github_oauth && (
+          <Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button>
+        )
+      }
+      <Button
+        onClick={() => {
+          setShowEmailBindModal(true);
+        }}
+      >
+        绑定邮箱地址
+      </Button>
+      <Modal
+        onClose={() => setShowEmailBindModal(false)}
+        onOpen={() => setShowEmailBindModal(true)}
+        open={showEmailBindModal}
+        size={'tiny'}
+        style={{ maxWidth: '450px' }}
+      >
+        <Modal.Header>绑定邮箱地址</Modal.Header>
+        <Modal.Content>
+          <Modal.Description>
+            <Form size='large'>
+              <Form.Input
+                fluid
+                placeholder='输入邮箱地址'
+                onChange={handleInputChange}
+                name='email'
+                type='email'
+                action={
+                  <Button onClick={sendVerificationCode} disabled={disableButton || loading}>
+                    {disableButton ? `重新发送(${countdown})` : '获取验证码'}
+                  </Button>
+                }
+              />
+              <Form.Input
+                fluid
+                placeholder='验证码'
+                name='email_verification_code'
+                value={inputs.email_verification_code}
+                onChange={handleInputChange}
+              />
+              {turnstileEnabled ? (
+                <Turnstile
+                  sitekey={turnstileSiteKey}
+                  onVerify={(token) => {
+                    setTurnstileToken(token);
+                  }}
                 />
-            )}
-            {affLink && (
-                <Form.Input
-                    fluid
-                    readOnly
-                    value={affLink}
-                    onClick={handleAffLinkClick}
-                    style={{ marginTop: '10px' }}
+              ) : (
+                <></>
+              )}
+              <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
+              <Button
+                color=''
+                fluid
+                size='large'
+                onClick={bindEmail}
+                loading={loading}
+              >
+                确认绑定
+              </Button>
+              <div style={{ width: '1rem' }}></div> 
+              <Button
+                fluid
+                size='large'
+                onClick={() => setShowEmailBindModal(false)}
+              >
+                取消
+              </Button>
+              </div>
+            </Form>
+          </Modal.Description>
+        </Modal.Content>
+      </Modal>
+      <Modal
+        onClose={() => setShowAccountDeleteModal(false)}
+        onOpen={() => setShowAccountDeleteModal(true)}
+        open={showAccountDeleteModal}
+        size={'tiny'}
+        style={{ maxWidth: '450px' }}
+      >
+        <Modal.Header>危险操作</Modal.Header>
+        <Modal.Content>
+        <Message>您正在删除自己的帐户,将清空所有数据且不可恢复</Message>
+          <Modal.Description>
+            <Form size='large'>
+              <Form.Input
+                fluid
+                placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
+                name='self_account_deletion_confirmation'
+                value={inputs.self_account_deletion_confirmation}
+                onChange={handleInputChange}
+              />
+              {turnstileEnabled ? (
+                <Turnstile
+                  sitekey={turnstileSiteKey}
+                  onVerify={(token) => {
+                    setTurnstileToken(token);
+                  }}
                 />
-            )}
-            <Divider />
-            <Header as='h3'>账号绑定</Header>
-            {
-                status.wechat_login && (
-                    <Button
-                        onClick={() => {
-                            setShowWeChatBindModal(true);
-                        }}
-                    >
-                        绑定微信账号
-                    </Button>
-                )
-            }
-            <Modal
-                onClose={() => setShowWeChatBindModal(false)}
-                onOpen={() => setShowWeChatBindModal(true)}
-                open={showWeChatBindModal}
-                size={'mini'}
-            >
-                <Modal.Content>
-                    <Modal.Description>
-                        <Image src={status.wechat_qrcode} fluid />
-                        <div style={{ textAlign: 'center' }}>
-                            <p>
-                                微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
-                            </p>
-                        </div>
-                        <Form size='large'>
-                            <Form.Input
-                                fluid
-                                placeholder='验证码'
-                                name='wechat_verification_code'
-                                value={inputs.wechat_verification_code}
-                                onChange={handleInputChange}
-                            />
-                            <Button color='' fluid size='large' onClick={bindWeChat}>
-                                绑定
-                            </Button>
-                        </Form>
-                    </Modal.Description>
-                </Modal.Content>
-            </Modal>
-            {
-                status.github_oauth && (
-                    <Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
-                )
-            }
-            <Button
-                onClick={() => {
-                    setShowEmailBindModal(true);
-                }}
-            >
-                绑定邮箱地址
-            </Button>
-            <Modal
-                onClose={() => setShowEmailBindModal(false)}
-                onOpen={() => setShowEmailBindModal(true)}
-                open={showEmailBindModal}
-                size={'tiny'}
-                style={{ maxWidth: '450px' }}
-            >
-                <Modal.Header>绑定邮箱地址</Modal.Header>
-                <Modal.Content>
-                    <Modal.Description>
-                        <Form size='large'>
-                            <Form.Input
-                                fluid
-                                placeholder='输入邮箱地址'
-                                onChange={handleInputChange}
-                                name='email'
-                                type='email'
-                                action={
-                                    <Button onClick={sendVerificationCode} disabled={disableButton || loading}>
-                                        {disableButton ? `重新发送(${countdown})` : '获取验证码'}
-                                    </Button>
-                                }
-                            />
-                            <Form.Input
-                                fluid
-                                placeholder='验证码'
-                                name='email_verification_code'
-                                value={inputs.email_verification_code}
-                                onChange={handleInputChange}
-                            />
-                            {turnstileEnabled ? (
-                                <Turnstile
-                                    sitekey={turnstileSiteKey}
-                                    onVerify={(token) => {
-                                        setTurnstileToken(token);
-                                    }}
-                                />
-                            ) : (
-                                <></>
-                            )}
-                            <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
-                                <Button
-                                    color=''
-                                    fluid
-                                    size='large'
-                                    onClick={bindEmail}
-                                    loading={loading}
-                                >
-                                    确认绑定
-                                </Button>
-                                <div style={{ width: '1rem' }}></div>
-                                <Button
-                                    fluid
-                                    size='large'
-                                    onClick={() => setShowEmailBindModal(false)}
-                                >
-                                    取消
-                                </Button>
-                            </div>
-                        </Form>
-                    </Modal.Description>
-                </Modal.Content>
-            </Modal>
-            <Modal
-                onClose={() => setShowAccountDeleteModal(false)}
-                onOpen={() => setShowAccountDeleteModal(true)}
-                open={showAccountDeleteModal}
-                size={'tiny'}
-                style={{ maxWidth: '450px' }}
-            >
-                <Modal.Header>危险操作</Modal.Header>
-                <Modal.Content>
-                    <Message>您正在删除自己的帐户,将清空所有数据且不可恢复</Message>
-                    <Modal.Description>
-                        <Form size='large'>
-                            <Form.Input
-                                fluid
-                                placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
-                                name='self_account_deletion_confirmation'
-                                value={inputs.self_account_deletion_confirmation}
-                                onChange={handleInputChange}
-                            />
-                            {turnstileEnabled ? (
-                                <Turnstile
-                                    sitekey={turnstileSiteKey}
-                                    onVerify={(token) => {
-                                        setTurnstileToken(token);
-                                    }}
-                                />
-                            ) : (
-                                <></>
-                            )}
-                            <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
-                                <Button
-                                    color='red'
-                                    fluid
-                                    size='large'
-                                    onClick={deleteAccount}
-                                    loading={loading}
-                                >
-                                    确认删除
-                                </Button>
-                                <div style={{ width: '1rem' }}></div>
-                                <Button
-                                    fluid
-                                    size='large'
-                                    onClick={() => setShowAccountDeleteModal(false)}
-                                >
-                                    取消
-                                </Button>
-                            </div>
-                        </Form>
-                    </Modal.Description>
-                </Modal.Content>
-            </Modal>
-        </div>
-    );
+              ) : (
+                <></>
+              )}
+              <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
+                <Button
+                  color='red'
+                  fluid
+                  size='large'
+                  onClick={deleteAccount}
+                  loading={loading}
+                >
+                  确认删除
+                </Button>
+                <div style={{ width: '1rem' }}></div>
+                <Button
+                  fluid
+                  size='large'
+                  onClick={() => setShowAccountDeleteModal(false)}
+                >
+                  取消
+                </Button>
+              </div>
+            </Form>
+          </Modal.Description>
+        </Modal.Content>
+      </Modal>
+    </div>
+  );
 };
 
-export default PersonalSetting;
+export default PersonalSetting;

+ 20 - 0
web/src/components/utils.js

@@ -0,0 +1,20 @@
+import { API, showError } from '../helpers';
+
+export async function getOAuthState() {
+  const res = await API.get('/api/oauth/state');
+  const { success, message, data } = res.data;
+  if (success) {
+    return data;
+  } else {
+    showError(message);
+    return '';
+  }
+}
+
+export async function onGitHubOAuthClicked(github_client_id) {
+  const state = await getOAuthState();
+  if (!state) return;
+  window.open(
+    `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`
+  );
+}