RegisterForm.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. import React, { useContext, useEffect, useState } from 'react';
  2. import { Link, useNavigate } from 'react-router-dom';
  3. import {
  4. API,
  5. getLogo,
  6. showError,
  7. showInfo,
  8. showSuccess,
  9. updateAPI,
  10. getSystemName,
  11. setUserData
  12. } from '../../helpers/index.js';
  13. import Turnstile from 'react-turnstile';
  14. import {
  15. Button,
  16. Card,
  17. Divider,
  18. Form,
  19. Icon,
  20. Modal,
  21. } from '@douyinfe/semi-ui';
  22. import Title from '@douyinfe/semi-ui/lib/es/typography/title';
  23. import Text from '@douyinfe/semi-ui/lib/es/typography/text';
  24. import { IconGithubLogo, IconMail, IconUser, IconLock, IconKey } from '@douyinfe/semi-icons';
  25. import {
  26. onGitHubOAuthClicked,
  27. onLinuxDOOAuthClicked,
  28. onOIDCClicked,
  29. } from '../../helpers/index.js';
  30. import OIDCIcon from '../common/logo/OIDCIcon.js';
  31. import LinuxDoIcon from '../common/logo/LinuxDoIcon.js';
  32. import WeChatIcon from '../common/logo/WeChatIcon.js';
  33. import TelegramLoginButton from 'react-telegram-login/src';
  34. import { UserContext } from '../../context/User/index.js';
  35. import { useTranslation } from 'react-i18next';
  36. const RegisterForm = () => {
  37. const { t } = useTranslation();
  38. const [inputs, setInputs] = useState({
  39. username: '',
  40. password: '',
  41. password2: '',
  42. email: '',
  43. verification_code: '',
  44. wechat_verification_code: '',
  45. });
  46. const { username, password, password2 } = inputs;
  47. const [showEmailVerification, setShowEmailVerification] = useState(false);
  48. const [userState, userDispatch] = useContext(UserContext);
  49. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  50. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  51. const [turnstileToken, setTurnstileToken] = useState('');
  52. const [loading, setLoading] = useState(false);
  53. const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
  54. const [showEmailRegister, setShowEmailRegister] = useState(false);
  55. const [status, setStatus] = useState({});
  56. const [wechatLoading, setWechatLoading] = useState(false);
  57. const [githubLoading, setGithubLoading] = useState(false);
  58. const [oidcLoading, setOidcLoading] = useState(false);
  59. const [linuxdoLoading, setLinuxdoLoading] = useState(false);
  60. const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
  61. const [registerLoading, setRegisterLoading] = useState(false);
  62. const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
  63. const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false);
  64. const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
  65. let navigate = useNavigate();
  66. const logo = getLogo();
  67. const systemName = getSystemName();
  68. let affCode = new URLSearchParams(window.location.search).get('aff');
  69. if (affCode) {
  70. localStorage.setItem('aff', affCode);
  71. }
  72. useEffect(() => {
  73. let status = localStorage.getItem('status');
  74. if (status) {
  75. status = JSON.parse(status);
  76. setStatus(status);
  77. setShowEmailVerification(status.email_verification);
  78. if (status.turnstile_check) {
  79. setTurnstileEnabled(true);
  80. setTurnstileSiteKey(status.turnstile_site_key);
  81. }
  82. }
  83. }, []);
  84. const onWeChatLoginClicked = () => {
  85. setWechatLoading(true);
  86. setShowWeChatLoginModal(true);
  87. setWechatLoading(false);
  88. };
  89. const onSubmitWeChatVerificationCode = async () => {
  90. if (turnstileEnabled && turnstileToken === '') {
  91. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  92. return;
  93. }
  94. setWechatCodeSubmitLoading(true);
  95. try {
  96. const res = await API.get(
  97. `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
  98. );
  99. const { success, message, data } = res.data;
  100. if (success) {
  101. userDispatch({ type: 'login', payload: data });
  102. localStorage.setItem('user', JSON.stringify(data));
  103. setUserData(data);
  104. updateAPI();
  105. navigate('/');
  106. showSuccess('登录成功!');
  107. setShowWeChatLoginModal(false);
  108. } else {
  109. showError(message);
  110. }
  111. } catch (error) {
  112. showError('登录失败,请重试');
  113. } finally {
  114. setWechatCodeSubmitLoading(false);
  115. }
  116. };
  117. function handleChange(name, value) {
  118. setInputs((inputs) => ({ ...inputs, [name]: value }));
  119. }
  120. async function handleSubmit(e) {
  121. if (password.length < 8) {
  122. showInfo('密码长度不得小于 8 位!');
  123. return;
  124. }
  125. if (password !== password2) {
  126. showInfo('两次输入的密码不一致');
  127. return;
  128. }
  129. if (username && password) {
  130. if (turnstileEnabled && turnstileToken === '') {
  131. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  132. return;
  133. }
  134. setRegisterLoading(true);
  135. try {
  136. if (!affCode) {
  137. affCode = localStorage.getItem('aff');
  138. }
  139. inputs.aff_code = affCode;
  140. const res = await API.post(
  141. `/api/user/register?turnstile=${turnstileToken}`,
  142. inputs,
  143. );
  144. const { success, message } = res.data;
  145. if (success) {
  146. navigate('/login');
  147. showSuccess('注册成功!');
  148. } else {
  149. showError(message);
  150. }
  151. } catch (error) {
  152. showError('注册失败,请重试');
  153. } finally {
  154. setRegisterLoading(false);
  155. }
  156. }
  157. }
  158. const sendVerificationCode = async () => {
  159. if (inputs.email === '') return;
  160. if (turnstileEnabled && turnstileToken === '') {
  161. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  162. return;
  163. }
  164. setVerificationCodeLoading(true);
  165. try {
  166. const res = await API.get(
  167. `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
  168. );
  169. const { success, message } = res.data;
  170. if (success) {
  171. showSuccess('验证码发送成功,请检查你的邮箱!');
  172. } else {
  173. showError(message);
  174. }
  175. } catch (error) {
  176. showError('发送验证码失败,请重试');
  177. } finally {
  178. setVerificationCodeLoading(false);
  179. }
  180. };
  181. const handleGitHubClick = () => {
  182. setGithubLoading(true);
  183. try {
  184. onGitHubOAuthClicked(status.github_client_id);
  185. } finally {
  186. setTimeout(() => setGithubLoading(false), 3000);
  187. }
  188. };
  189. const handleOIDCClick = () => {
  190. setOidcLoading(true);
  191. try {
  192. onOIDCClicked(
  193. status.oidc_authorization_endpoint,
  194. status.oidc_client_id
  195. );
  196. } finally {
  197. setTimeout(() => setOidcLoading(false), 3000);
  198. }
  199. };
  200. const handleLinuxDOClick = () => {
  201. setLinuxdoLoading(true);
  202. try {
  203. onLinuxDOOAuthClicked(status.linuxdo_client_id);
  204. } finally {
  205. setTimeout(() => setLinuxdoLoading(false), 3000);
  206. }
  207. };
  208. const handleEmailRegisterClick = () => {
  209. setEmailRegisterLoading(true);
  210. setShowEmailRegister(true);
  211. setEmailRegisterLoading(false);
  212. };
  213. const handleOtherRegisterOptionsClick = () => {
  214. setOtherRegisterOptionsLoading(true);
  215. setShowEmailRegister(false);
  216. setOtherRegisterOptionsLoading(false);
  217. };
  218. const onTelegramLoginClicked = async (response) => {
  219. const fields = [
  220. 'id',
  221. 'first_name',
  222. 'last_name',
  223. 'username',
  224. 'photo_url',
  225. 'auth_date',
  226. 'hash',
  227. 'lang',
  228. ];
  229. const params = {};
  230. fields.forEach((field) => {
  231. if (response[field]) {
  232. params[field] = response[field];
  233. }
  234. });
  235. try {
  236. const res = await API.get(`/api/oauth/telegram/login`, { params });
  237. const { success, message, data } = res.data;
  238. if (success) {
  239. userDispatch({ type: 'login', payload: data });
  240. localStorage.setItem('user', JSON.stringify(data));
  241. showSuccess('登录成功!');
  242. setUserData(data);
  243. updateAPI();
  244. navigate('/');
  245. } else {
  246. showError(message);
  247. }
  248. } catch (error) {
  249. showError('登录失败,请重试');
  250. }
  251. };
  252. const renderOAuthOptions = () => {
  253. return (
  254. <div className="flex flex-col items-center">
  255. <div className="w-full max-w-md">
  256. <div className="flex items-center justify-center mb-6 gap-2">
  257. <img src={logo} alt="Logo" className="h-10 rounded-full" />
  258. <Title heading={3} className='!text-gray-800'>{systemName}</Title>
  259. </div>
  260. <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
  261. <div className="flex justify-center pt-6 pb-2">
  262. <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('注 册')}</Title>
  263. </div>
  264. <div className="px-2 py-8">
  265. <div className="space-y-3">
  266. {status.wechat_login && (
  267. <Button
  268. theme='outline'
  269. className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
  270. type="tertiary"
  271. icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
  272. size="large"
  273. onClick={onWeChatLoginClicked}
  274. loading={wechatLoading}
  275. >
  276. <span className="ml-3">{t('使用 微信 继续')}</span>
  277. </Button>
  278. )}
  279. {status.github_oauth && (
  280. <Button
  281. theme='outline'
  282. className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
  283. type="tertiary"
  284. icon={<IconGithubLogo size="large" />}
  285. size="large"
  286. onClick={handleGitHubClick}
  287. loading={githubLoading}
  288. >
  289. <span className="ml-3">{t('使用 GitHub 继续')}</span>
  290. </Button>
  291. )}
  292. {status.oidc_enabled && (
  293. <Button
  294. theme='outline'
  295. className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
  296. type="tertiary"
  297. icon={<OIDCIcon style={{ color: '#1877F2' }} />}
  298. size="large"
  299. onClick={handleOIDCClick}
  300. loading={oidcLoading}
  301. >
  302. <span className="ml-3">{t('使用 OIDC 继续')}</span>
  303. </Button>
  304. )}
  305. {status.linuxdo_oauth && (
  306. <Button
  307. theme='outline'
  308. className="w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors"
  309. type="tertiary"
  310. icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
  311. size="large"
  312. onClick={handleLinuxDOClick}
  313. loading={linuxdoLoading}
  314. >
  315. <span className="ml-3">{t('使用 LinuxDO 继续')}</span>
  316. </Button>
  317. )}
  318. {status.telegram_oauth && (
  319. <div className="flex justify-center my-2">
  320. <TelegramLoginButton
  321. dataOnauth={onTelegramLoginClicked}
  322. botName={status.telegram_bot_name}
  323. />
  324. </div>
  325. )}
  326. <Divider margin='12px' align='center'>
  327. {t('或')}
  328. </Divider>
  329. <Button
  330. theme="solid"
  331. type="primary"
  332. className="w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors"
  333. icon={<IconMail size="large" />}
  334. size="large"
  335. onClick={handleEmailRegisterClick}
  336. loading={emailRegisterLoading}
  337. >
  338. <span className="ml-3">{t('使用 用户名 注册')}</span>
  339. </Button>
  340. </div>
  341. <div className="mt-6 text-center text-sm">
  342. <Text>{t('已有账户?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
  343. </div>
  344. </div>
  345. </Card>
  346. </div>
  347. </div>
  348. );
  349. };
  350. const renderEmailRegisterForm = () => {
  351. return (
  352. <div className="flex flex-col items-center">
  353. <div className="w-full max-w-md">
  354. <div className="flex items-center justify-center mb-6 gap-2">
  355. <img src={logo} alt="Logo" className="h-10 rounded-full" />
  356. <Title heading={3} className='!text-gray-800'>{systemName}</Title>
  357. </div>
  358. <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
  359. <div className="flex justify-center pt-6 pb-2">
  360. <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('注 册')}</Title>
  361. </div>
  362. <div className="px-2 py-8">
  363. <Form className="space-y-3">
  364. <Form.Input
  365. field="username"
  366. label={t('用户名')}
  367. placeholder={t('请输入用户名')}
  368. name="username"
  369. size="large"
  370. className="!rounded-md"
  371. onChange={(value) => handleChange('username', value)}
  372. prefix={<IconUser />}
  373. />
  374. <Form.Input
  375. field="password"
  376. label={t('密码')}
  377. placeholder={t('输入密码,最短 8 位,最长 20 位')}
  378. name="password"
  379. mode="password"
  380. size="large"
  381. className="!rounded-md"
  382. onChange={(value) => handleChange('password', value)}
  383. prefix={<IconLock />}
  384. />
  385. <Form.Input
  386. field="password2"
  387. label={t('确认密码')}
  388. placeholder={t('确认密码')}
  389. name="password2"
  390. mode="password"
  391. size="large"
  392. className="!rounded-md"
  393. onChange={(value) => handleChange('password2', value)}
  394. prefix={<IconLock />}
  395. />
  396. {showEmailVerification && (
  397. <>
  398. <Form.Input
  399. field="email"
  400. label={t('邮箱')}
  401. placeholder={t('输入邮箱地址')}
  402. name="email"
  403. type="email"
  404. size="large"
  405. className="!rounded-md"
  406. onChange={(value) => handleChange('email', value)}
  407. prefix={<IconMail />}
  408. suffix={
  409. <Button
  410. onClick={sendVerificationCode}
  411. loading={verificationCodeLoading}
  412. size="small"
  413. className="!rounded-md mr-2"
  414. >
  415. {t('获取验证码')}
  416. </Button>
  417. }
  418. />
  419. <Form.Input
  420. field="verification_code"
  421. label={t('验证码')}
  422. placeholder={t('输入验证码')}
  423. name="verification_code"
  424. size="large"
  425. className="!rounded-md"
  426. onChange={(value) => handleChange('verification_code', value)}
  427. prefix={<IconKey />}
  428. />
  429. </>
  430. )}
  431. <div className="space-y-2 pt-2">
  432. <Button
  433. theme="solid"
  434. className="w-full !rounded-full"
  435. type="primary"
  436. htmlType="submit"
  437. size="large"
  438. onClick={handleSubmit}
  439. loading={registerLoading}
  440. >
  441. {t('注册')}
  442. </Button>
  443. </div>
  444. </Form>
  445. {(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && (
  446. <>
  447. <Divider margin='12px' align='center'>
  448. {t('或')}
  449. </Divider>
  450. <div className="mt-4 text-center">
  451. <Button
  452. theme="outline"
  453. type="tertiary"
  454. className="w-full !rounded-full"
  455. size="large"
  456. onClick={handleOtherRegisterOptionsClick}
  457. loading={otherRegisterOptionsLoading}
  458. >
  459. {t('其他注册选项')}
  460. </Button>
  461. </div>
  462. </>
  463. )}
  464. <div className="mt-6 text-center text-sm">
  465. <Text>{t('已有账户?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
  466. </div>
  467. </div>
  468. </Card>
  469. </div>
  470. </div>
  471. );
  472. };
  473. const renderWeChatLoginModal = () => {
  474. return (
  475. <Modal
  476. title={t('微信扫码登录')}
  477. visible={showWeChatLoginModal}
  478. maskClosable={true}
  479. onOk={onSubmitWeChatVerificationCode}
  480. onCancel={() => setShowWeChatLoginModal(false)}
  481. okText={t('登录')}
  482. size="small"
  483. centered={true}
  484. okButtonProps={{
  485. loading: wechatCodeSubmitLoading,
  486. }}
  487. >
  488. <div className="flex flex-col items-center">
  489. <img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />
  490. </div>
  491. <div className="text-center mb-4">
  492. <p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
  493. </div>
  494. <Form size="large">
  495. <Form.Input
  496. field="wechat_verification_code"
  497. placeholder={t('验证码')}
  498. label={t('验证码')}
  499. value={inputs.wechat_verification_code}
  500. onChange={(value) => handleChange('wechat_verification_code', value)}
  501. />
  502. </Form>
  503. </Modal>
  504. );
  505. };
  506. return (
  507. <div className="bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 mt-[64px]">
  508. <div className="w-full max-w-sm">
  509. {showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
  510. ? renderEmailRegisterForm()
  511. : renderOAuthOptions()}
  512. {renderWeChatLoginModal()}
  513. {turnstileEnabled && (
  514. <div className="flex justify-center mt-6">
  515. <Turnstile
  516. sitekey={turnstileSiteKey}
  517. onVerify={(token) => {
  518. setTurnstileToken(token);
  519. }}
  520. />
  521. </div>
  522. )}
  523. </div>
  524. </div>
  525. );
  526. };
  527. export default RegisterForm;