Merge remote-tracking branch 'origin/main'

# Conflicts:
#	web/src/components/PersonalSetting.js
This commit is contained in:
CaIon 2023-09-15 18:01:06 +08:00
commit 377da2dfcb
8 changed files with 426 additions and 371 deletions

View File

@ -86,10 +86,10 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
+ [x] [360 智脑](https://ai.360.cn) + [x] [360 智脑](https://ai.360.cn)
2. 支持配置镜像以及众多第三方代理服务: 2. 支持配置镜像以及众多第三方代理服务:
+ [x] [OpenAI-SB](https://openai-sb.com) + [x] [OpenAI-SB](https://openai-sb.com)
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
+ [x] [API2D](https://api2d.com/r/197971) + [x] [API2D](https://api2d.com/r/197971)
+ [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI` + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
+ [x] 自定义渠道:例如各种未收录的第三方代理服务 + [x] 自定义渠道:例如各种未收录的第三方代理服务
3. 支持通过**负载均衡**的方式访问多个渠道。 3. 支持通过**负载均衡**的方式访问多个渠道。
4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 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` 注意修改端口号、`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> <details>
<summary><strong>部署到 Sealos </strong></summary> <summary><strong>部署到 Sealos </strong></summary>

View File

@ -79,6 +79,14 @@ func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
func GitHubOAuth(c *gin.Context) { func GitHubOAuth(c *gin.Context) {
session := sessions.Default(c) 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") username := session.Get("username")
if username != nil { if username != nil {
GitHubBind(c) GitHubBind(c)
@ -205,3 +213,22 @@ func GitHubBind(c *gin.Context) {
}) })
return 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,
})
}

View File

@ -357,6 +357,15 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream")
if resp.StatusCode != http.StatusOK { 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) return relayErrorHandler(resp)
} }
} }

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } f
import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { API, getLogo, showError, showSuccess } from '../helpers'; import { API, getLogo, showError, showSuccess } from '../helpers';
import { getOAuthState, onGitHubOAuthClicked } from './utils';
const LoginForm = () => { const LoginForm = () => {
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@ -31,12 +32,6 @@ const LoginForm = () => {
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); 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 = () => { const onWeChatLoginClicked = () => {
setShowWeChatLoginModal(true); setShowWeChatLoginModal(true);
}; };
@ -131,7 +126,7 @@ const LoginForm = () => {
circular circular
color='black' color='black'
icon='github' icon='github'
onClick={onGitHubOAuthClicked} onClick={()=>onGitHubOAuthClicked(status.github_client_id)}
/> />
) : ( ) : (
<></> <></>

View File

@ -4,378 +4,373 @@ import { Link, useNavigate } from 'react-router-dom';
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
import Turnstile from 'react-turnstile'; import Turnstile from 'react-turnstile';
import { UserContext } from '../context/User'; import { UserContext } from '../context/User';
import { onGitHubOAuthClicked } from './utils';
const PersonalSetting = () => { const PersonalSetting = () => {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
let navigate = useNavigate(); let navigate = useNavigate();
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
wechat_verification_code: '', wechat_verification_code: '',
email_verification_code: '', email_verification_code: '',
email: '', email: '',
self_account_deletion_confirmation: '' self_account_deletion_confirmation: ''
}); });
const [status, setStatus] = useState({}); const [status, setStatus] = useState({});
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
const [showEmailBindModal, setShowEmailBindModal] = useState(false); const [showEmailBindModal, setShowEmailBindModal] = useState(false);
const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false); const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState(''); const [turnstileToken, setTurnstileToken] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [disableButton, setDisableButton] = useState(false); const [disableButton, setDisableButton] = useState(false);
const [countdown, setCountdown] = useState(30); const [countdown, setCountdown] = useState(30);
const [affLink, setAffLink] = useState(""); const [affLink, setAffLink] = useState("");
const [systemToken, setSystemToken] = useState(""); const [systemToken, setSystemToken] = useState("");
useEffect(() => { useEffect(() => {
let status = localStorage.getItem('status'); let status = localStorage.getItem('status');
if (status) { if (status) {
status = JSON.parse(status); status = JSON.parse(status);
setStatus(status); setStatus(status);
if (status.turnstile_check) { if (status.turnstile_check) {
setTurnstileEnabled(true); setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key); setTurnstileSiteKey(status.turnstile_site_key);
} }
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
let countdownInterval = null; let countdownInterval = null;
if (disableButton && countdown > 0) { if (disableButton && countdown > 0) {
countdownInterval = setInterval(() => { countdownInterval = setInterval(() => {
setCountdown(countdown - 1); setCountdown(countdown - 1);
}, 1000); }, 1000);
} else if (countdown === 0) { } else if (countdown === 0) {
setDisableButton(false); setDisableButton(false);
setCountdown(30); setCountdown(30);
} }
return () => clearInterval(countdownInterval); // Clean up on unmount return () => clearInterval(countdownInterval); // Clean up on unmount
}, [disableButton, countdown]); }, [disableButton, countdown]);
const handleInputChange = (e, { name, value }) => { const handleInputChange = (e, { name, value }) => {
setInputs((inputs) => ({ ...inputs, [name]: value })); setInputs((inputs) => ({ ...inputs, [name]: value }));
}; };
const generateAccessToken = async () => { const generateAccessToken = async () => {
const res = await API.get('/api/user/token'); const res = await API.get('/api/user/token');
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setSystemToken(data); setSystemToken(data);
setAffLink(""); setAffLink("");
await copy(data); await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板`); showSuccess(`令牌已重置并已复制到剪贴板`);
} else { } else {
showError(message); showError(message);
} }
}; };
const getAffLink = async () => { const getAffLink = async () => {
const res = await API.get('/api/user/aff'); const res = await API.get('/api/user/aff');
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
let link = `${window.location.origin}/register?aff=${data}`; let link = `${window.location.origin}/register?aff=${data}`;
setAffLink(link); setAffLink(link);
setSystemToken(""); setSystemToken("");
await copy(link); await copy(link);
showSuccess(`邀请链接已复制到剪切板`); showSuccess(`邀请链接已复制到剪切板`);
} else { } else {
showError(message); showError(message);
} }
}; };
const handleAffLinkClick = async (e) => { const handleAffLinkClick = async (e) => {
e.target.select(); e.target.select();
await copy(e.target.value); await copy(e.target.value);
showSuccess(`邀请链接已复制到剪切板`); showSuccess(`邀请链接已复制到剪切板`);
}; };
const handleSystemTokenClick = async (e) => { const handleSystemTokenClick = async (e) => {
e.target.select(); e.target.select();
await copy(e.target.value); await copy(e.target.value);
showSuccess(`系统令牌已复制到剪切板`); showSuccess(`系统令牌已复制到剪切板`);
}; };
const deleteAccount = async () => { const deleteAccount = async () => {
if (inputs.self_account_deletion_confirmation !== userState.user.username) { if (inputs.self_account_deletion_confirmation !== userState.user.username) {
showError('请输入你的账户名以确认删除!'); showError('请输入你的账户名以确认删除!');
return; return;
} }
const res = await API.delete('/api/user/self'); const res = await API.delete('/api/user/self');
const { success, message } = res.data; const { success, message } = res.data;
if (success) { if (success) {
showSuccess('账户已删除!'); showSuccess('账户已删除!');
await API.get('/api/user/logout'); await API.get('/api/user/logout');
userDispatch({ type: 'logout' }); userDispatch({ type: 'logout' });
localStorage.removeItem('user'); localStorage.removeItem('user');
navigate('/login'); navigate('/login');
} else { } else {
showError(message); showError(message);
} }
}; };
const bindWeChat = async () => { const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return; if (inputs.wechat_verification_code === '') return;
const res = await API.get( const res = await API.get(
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}` `/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 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);
};
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={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>
); );
const { success, message } = res.data;
if (success) {
showSuccess('微信账户绑定成功!');
setShowWeChatBindModal(false);
} else {
showError(message);
}
};
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 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);
};
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);
}}
/>
) : (
<></>
)}
<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>
);
}; };
export default PersonalSetting; export default PersonalSetting;

View File

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