RegisterForm.js 19 KB

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