|
|
import React, { useContext, useEffect, useState } from 'react'; |
|
|
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react'; |
|
|
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'; |
|
|
|
|
|
const PersonalSetting = () => { |
|
|
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(""); |
|
|
|
|
|
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); |
|
|
}, [disableButton, countdown]); |
|
|
|
|
|
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 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 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 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); |
|
|
} |
|
|
}; |
|
|
|
|
|
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 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> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default PersonalSetting; |
|
|
|