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. {turnstileEnabled && (
  348. <div className="flex justify-center mt-6">
  349. <Turnstile
  350. sitekey={turnstileSiteKey}
  351. onVerify={(token) => {
  352. setTurnstileToken(token);
  353. }}
  354. />
  355. </div>
  356. )}
  357. </div>
  358. </div>
  359. );
  360. };
  361. const renderEmailRegisterForm = () => {
  362. return (
  363. <div className="flex flex-col items-center">
  364. <div className="w-full max-w-md">
  365. <div className="flex items-center justify-center mb-6 gap-2">
  366. <img src={logo} alt="Logo" className="h-10 rounded-full" />
  367. <Title heading={3} className='!text-white'>{systemName}</Title>
  368. </div>
  369. <Card className="shadow-xl border-0 !rounded-2xl overflow-hidden">
  370. <div className="flex justify-center pt-6 pb-2">
  371. <Title heading={3} className="text-gray-800 dark:text-gray-200">{t('注 册')}</Title>
  372. </div>
  373. <div className="px-2 py-8">
  374. <Form className="space-y-3">
  375. <Form.Input
  376. field="username"
  377. label={t('用户名')}
  378. placeholder={t('请输入用户名')}
  379. name="username"
  380. size="large"
  381. className="!rounded-md"
  382. onChange={(value) => handleChange('username', value)}
  383. prefix={<IconUser />}
  384. />
  385. <Form.Input
  386. field="password"
  387. label={t('密码')}
  388. placeholder={t('输入密码,最短 8 位,最长 20 位')}
  389. name="password"
  390. mode="password"
  391. size="large"
  392. className="!rounded-md"
  393. onChange={(value) => handleChange('password', value)}
  394. prefix={<IconLock />}
  395. />
  396. <Form.Input
  397. field="password2"
  398. label={t('确认密码')}
  399. placeholder={t('确认密码')}
  400. name="password2"
  401. mode="password"
  402. size="large"
  403. className="!rounded-md"
  404. onChange={(value) => handleChange('password2', value)}
  405. prefix={<IconLock />}
  406. />
  407. {showEmailVerification && (
  408. <>
  409. <Form.Input
  410. field="email"
  411. label={t('邮箱')}
  412. placeholder={t('输入邮箱地址')}
  413. name="email"
  414. type="email"
  415. size="large"
  416. className="!rounded-md"
  417. onChange={(value) => handleChange('email', value)}
  418. prefix={<IconMail />}
  419. suffix={
  420. <Button
  421. onClick={sendVerificationCode}
  422. loading={verificationCodeLoading}
  423. size="small"
  424. className="!rounded-md mr-2"
  425. >
  426. {t('获取验证码')}
  427. </Button>
  428. }
  429. />
  430. <Form.Input
  431. field="verification_code"
  432. label={t('验证码')}
  433. placeholder={t('输入验证码')}
  434. name="verification_code"
  435. size="large"
  436. className="!rounded-md"
  437. onChange={(value) => handleChange('verification_code', value)}
  438. prefix={<IconKey />}
  439. />
  440. </>
  441. )}
  442. <div className="space-y-2 pt-2">
  443. <Button
  444. theme="solid"
  445. className="w-full !rounded-full"
  446. type="primary"
  447. htmlType="submit"
  448. size="large"
  449. onClick={handleSubmit}
  450. loading={registerLoading}
  451. >
  452. {t('注册')}
  453. </Button>
  454. </div>
  455. </Form>
  456. {(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && (
  457. <>
  458. <Divider margin='12px' align='center'>
  459. {t('或')}
  460. </Divider>
  461. <div className="mt-4 text-center">
  462. <Button
  463. theme="outline"
  464. type="tertiary"
  465. className="w-full !rounded-full"
  466. size="large"
  467. onClick={handleOtherRegisterOptionsClick}
  468. loading={otherRegisterOptionsLoading}
  469. >
  470. {t('其他注册选项')}
  471. </Button>
  472. </div>
  473. </>
  474. )}
  475. <div className="mt-6 text-center text-sm">
  476. <Text>{t('已有账户?')} <Link to="/login" className="text-blue-600 hover:text-blue-800 font-medium">{t('登录')}</Link></Text>
  477. </div>
  478. </div>
  479. </Card>
  480. </div>
  481. </div>
  482. );
  483. };
  484. const renderWeChatLoginModal = () => {
  485. return (
  486. <Modal
  487. title={t('微信扫码登录')}
  488. visible={showWeChatLoginModal}
  489. maskClosable={true}
  490. onOk={onSubmitWeChatVerificationCode}
  491. onCancel={() => setShowWeChatLoginModal(false)}
  492. okText={t('登录')}
  493. size="small"
  494. centered={true}
  495. okButtonProps={{
  496. loading: wechatCodeSubmitLoading,
  497. }}
  498. >
  499. <div className="flex flex-col items-center">
  500. <img src={status.wechat_qrcode} alt="微信二维码" className="mb-4" />
  501. </div>
  502. <div className="text-center mb-4">
  503. <p>{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}</p>
  504. </div>
  505. <Form size="large">
  506. <Form.Input
  507. field="wechat_verification_code"
  508. placeholder={t('验证码')}
  509. label={t('验证码')}
  510. value={inputs.wechat_verification_code}
  511. onChange={(value) => handleChange('wechat_verification_code', value)}
  512. />
  513. </Form>
  514. </Modal>
  515. );
  516. };
  517. return (
  518. <div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
  519. <div
  520. className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
  521. style={{
  522. backgroundImage: `url(${Background})`
  523. }}
  524. ></div>
  525. <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>
  526. <div className="w-full max-w-sm relative z-10">
  527. {showEmailRegister || !(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth)
  528. ? renderEmailRegisterForm()
  529. : renderOAuthOptions()}
  530. {renderWeChatLoginModal()}
  531. </div>
  532. </div>
  533. );
  534. };
  535. export default RegisterForm;