LoginForm.jsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useContext, useEffect, useState } from 'react';
  16. import { Link, useNavigate, useSearchParams } from 'react-router-dom';
  17. import { UserContext } from '../../context/User';
  18. import {
  19. API,
  20. getLogo,
  21. showError,
  22. showInfo,
  23. showSuccess,
  24. updateAPI,
  25. getSystemName,
  26. setUserData,
  27. onGitHubOAuthClicked,
  28. onOIDCClicked,
  29. onLinuxDOOAuthClicked,
  30. prepareCredentialRequestOptions,
  31. buildAssertionResult,
  32. isPasskeySupported,
  33. } from '../../helpers';
  34. import Turnstile from 'react-turnstile';
  35. import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
  36. import Title from '@douyinfe/semi-ui/lib/es/typography/title';
  37. import Text from '@douyinfe/semi-ui/lib/es/typography/text';
  38. import TelegramLoginButton from 'react-telegram-login';
  39. import { IconGithubLogo, IconMail, IconLock, IconKey } from '@douyinfe/semi-icons';
  40. import OIDCIcon from '../common/logo/OIDCIcon';
  41. import WeChatIcon from '../common/logo/WeChatIcon';
  42. import LinuxDoIcon from '../common/logo/LinuxDoIcon';
  43. import TwoFAVerification from './TwoFAVerification';
  44. import { useTranslation } from 'react-i18next';
  45. const LoginForm = () => {
  46. let navigate = useNavigate();
  47. const { t } = useTranslation();
  48. const [inputs, setInputs] = useState({
  49. username: '',
  50. password: '',
  51. wechat_verification_code: '',
  52. });
  53. const { username, password } = inputs;
  54. const [searchParams, setSearchParams] = useSearchParams();
  55. const [submitted, setSubmitted] = useState(false);
  56. const [userState, userDispatch] = useContext(UserContext);
  57. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  58. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  59. const [turnstileToken, setTurnstileToken] = useState('');
  60. const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
  61. const [showEmailLogin, setShowEmailLogin] = useState(false);
  62. const [wechatLoading, setWechatLoading] = useState(false);
  63. const [githubLoading, setGithubLoading] = useState(false);
  64. const [oidcLoading, setOidcLoading] = useState(false);
  65. const [linuxdoLoading, setLinuxdoLoading] = useState(false);
  66. const [emailLoginLoading, setEmailLoginLoading] = useState(false);
  67. const [loginLoading, setLoginLoading] = useState(false);
  68. const [resetPasswordLoading, setResetPasswordLoading] = useState(false);
  69. const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] =
  70. useState(false);
  71. const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
  72. const [showTwoFA, setShowTwoFA] = useState(false);
  73. const [passkeySupported, setPasskeySupported] = useState(false);
  74. const [passkeyLoading, setPasskeyLoading] = useState(false);
  75. const logo = getLogo();
  76. const systemName = getSystemName();
  77. let affCode = new URLSearchParams(window.location.search).get('aff');
  78. if (affCode) {
  79. localStorage.setItem('aff', affCode);
  80. }
  81. const [status] = useState(() => {
  82. const savedStatus = localStorage.getItem('status');
  83. return savedStatus ? JSON.parse(savedStatus) : {};
  84. });
  85. useEffect(() => {
  86. if (status.turnstile_check) {
  87. setTurnstileEnabled(true);
  88. setTurnstileSiteKey(status.turnstile_site_key);
  89. }
  90. }, [status]);
  91. useEffect(() => {
  92. isPasskeySupported()
  93. .then(setPasskeySupported)
  94. .catch(() => setPasskeySupported(false));
  95. }, []);
  96. useEffect(() => {
  97. if (searchParams.get('expired')) {
  98. showError(t('未登录或登录已过期,请重新登录'));
  99. }
  100. }, []);
  101. const onWeChatLoginClicked = () => {
  102. setWechatLoading(true);
  103. setShowWeChatLoginModal(true);
  104. setWechatLoading(false);
  105. };
  106. const onSubmitWeChatVerificationCode = async () => {
  107. if (turnstileEnabled && turnstileToken === '') {
  108. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  109. return;
  110. }
  111. setWechatCodeSubmitLoading(true);
  112. try {
  113. const res = await API.get(
  114. `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
  115. );
  116. const { success, message, data } = res.data;
  117. if (success) {
  118. userDispatch({ type: 'login', payload: data });
  119. localStorage.setItem('user', JSON.stringify(data));
  120. setUserData(data);
  121. updateAPI();
  122. navigate('/');
  123. showSuccess('登录成功!');
  124. setShowWeChatLoginModal(false);
  125. } else {
  126. showError(message);
  127. }
  128. } catch (error) {
  129. showError('登录失败,请重试');
  130. } finally {
  131. setWechatCodeSubmitLoading(false);
  132. }
  133. };
  134. function handleChange(name, value) {
  135. setInputs((inputs) => ({ ...inputs, [name]: value }));
  136. }
  137. async function handleSubmit(e) {
  138. if (turnstileEnabled && turnstileToken === '') {
  139. showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
  140. return;
  141. }
  142. setSubmitted(true);
  143. setLoginLoading(true);
  144. try {
  145. if (username && password) {
  146. const res = await API.post(
  147. `/api/user/login?turnstile=${turnstileToken}`,
  148. {
  149. username,
  150. password,
  151. },
  152. );
  153. const { success, message, data } = res.data;
  154. if (success) {
  155. // 检查是否需要2FA验证
  156. if (data && data.require_2fa) {
  157. setShowTwoFA(true);
  158. setLoginLoading(false);
  159. return;
  160. }
  161. userDispatch({ type: 'login', payload: data });
  162. setUserData(data);
  163. updateAPI();
  164. showSuccess('登录成功!');
  165. if (username === 'root' && password === '123456') {
  166. Modal.error({
  167. title: '您正在使用默认密码!',
  168. content: '请立刻修改默认密码!',
  169. centered: true,
  170. });
  171. }
  172. navigate('/console');
  173. } else {
  174. showError(message);
  175. }
  176. } else {
  177. showError('请输入用户名和密码!');
  178. }
  179. } catch (error) {
  180. showError('登录失败,请重试');
  181. } finally {
  182. setLoginLoading(false);
  183. }
  184. }
  185. // 添加Telegram登录处理函数
  186. const onTelegramLoginClicked = async (response) => {
  187. const fields = [
  188. 'id',
  189. 'first_name',
  190. 'last_name',
  191. 'username',
  192. 'photo_url',
  193. 'auth_date',
  194. 'hash',
  195. 'lang',
  196. ];
  197. const params = {};
  198. fields.forEach((field) => {
  199. if (response[field]) {
  200. params[field] = response[field];
  201. }
  202. });
  203. try {
  204. const res = await API.get(`/api/oauth/telegram/login`, { params });
  205. const { success, message, data } = res.data;
  206. if (success) {
  207. userDispatch({ type: 'login', payload: data });
  208. localStorage.setItem('user', JSON.stringify(data));
  209. showSuccess('登录成功!');
  210. setUserData(data);
  211. updateAPI();
  212. navigate('/');
  213. } else {
  214. showError(message);
  215. }
  216. } catch (error) {
  217. showError('登录失败,请重试');
  218. }
  219. };
  220. // 包装的GitHub登录点击处理
  221. const handleGitHubClick = () => {
  222. setGithubLoading(true);
  223. try {
  224. onGitHubOAuthClicked(status.github_client_id);
  225. } finally {
  226. // 由于重定向,这里不会执行到,但为了完整性添加
  227. setTimeout(() => setGithubLoading(false), 3000);
  228. }
  229. };
  230. // 包装的OIDC登录点击处理
  231. const handleOIDCClick = () => {
  232. setOidcLoading(true);
  233. try {
  234. onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
  235. } finally {
  236. // 由于重定向,这里不会执行到,但为了完整性添加
  237. setTimeout(() => setOidcLoading(false), 3000);
  238. }
  239. };
  240. // 包装的LinuxDO登录点击处理
  241. const handleLinuxDOClick = () => {
  242. setLinuxdoLoading(true);
  243. try {
  244. onLinuxDOOAuthClicked(status.linuxdo_client_id);
  245. } finally {
  246. // 由于重定向,这里不会执行到,但为了完整性添加
  247. setTimeout(() => setLinuxdoLoading(false), 3000);
  248. }
  249. };
  250. // 包装的邮箱登录选项点击处理
  251. const handleEmailLoginClick = () => {
  252. setEmailLoginLoading(true);
  253. setShowEmailLogin(true);
  254. setEmailLoginLoading(false);
  255. };
  256. const handlePasskeyLogin = async () => {
  257. if (!passkeySupported) {
  258. showInfo('当前环境无法使用 Passkey 登录');
  259. return;
  260. }
  261. if (!window.PublicKeyCredential) {
  262. showInfo('当前浏览器不支持 Passkey');
  263. return;
  264. }
  265. setPasskeyLoading(true);
  266. try {
  267. const beginRes = await API.post('/api/user/passkey/login/begin');
  268. const { success, message, data } = beginRes.data;
  269. if (!success) {
  270. showError(message || '无法发起 Passkey 登录');
  271. return;
  272. }
  273. const publicKeyOptions = prepareCredentialRequestOptions(data?.options || data?.publicKey || data);
  274. const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions });
  275. const payload = buildAssertionResult(assertion);
  276. if (!payload) {
  277. showError('Passkey 验证失败,请重试');
  278. return;
  279. }
  280. const finishRes = await API.post('/api/user/passkey/login/finish', payload);
  281. const finish = finishRes.data;
  282. if (finish.success) {
  283. userDispatch({ type: 'login', payload: finish.data });
  284. setUserData(finish.data);
  285. updateAPI();
  286. showSuccess('登录成功!');
  287. navigate('/console');
  288. } else {
  289. showError(finish.message || 'Passkey 登录失败,请重试');
  290. }
  291. } catch (error) {
  292. if (error?.name === 'AbortError') {
  293. showInfo('已取消 Passkey 登录');
  294. } else {
  295. showError('Passkey 登录失败,请重试');
  296. }
  297. } finally {
  298. setPasskeyLoading(false);
  299. }
  300. };
  301. // 包装的重置密码点击处理
  302. const handleResetPasswordClick = () => {
  303. setResetPasswordLoading(true);
  304. navigate('/reset');
  305. setResetPasswordLoading(false);
  306. };
  307. // 包装的其他登录选项点击处理
  308. const handleOtherLoginOptionsClick = () => {
  309. setOtherLoginOptionsLoading(true);
  310. setShowEmailLogin(false);
  311. setOtherLoginOptionsLoading(false);
  312. };
  313. // 2FA验证成功处理
  314. const handle2FASuccess = (data) => {
  315. userDispatch({ type: 'login', payload: data });
  316. setUserData(data);
  317. updateAPI();
  318. showSuccess('登录成功!');
  319. navigate('/console');
  320. };
  321. // 返回登录页面
  322. const handleBackToLogin = () => {
  323. setShowTwoFA(false);
  324. setInputs({ username: '', password: '', wechat_verification_code: '' });
  325. };
  326. const renderOAuthOptions = () => {
  327. return (
  328. <div className='flex flex-col items-center'>
  329. <div className='w-full max-w-md'>
  330. <div className='flex items-center justify-center mb-6 gap-2'>
  331. <img src={logo} alt='Logo' className='h-10 rounded-full' />
  332. <Title heading={3} className='!text-gray-800'>
  333. {systemName}
  334. </Title>
  335. </div>
  336. <Card className='border-0 !rounded-2xl overflow-hidden'>
  337. <div className='flex justify-center pt-6 pb-2'>
  338. <Title heading={3} className='text-gray-800 dark:text-gray-200'>
  339. {t('登 录')}
  340. </Title>
  341. </div>
  342. <div className='px-2 py-8'>
  343. <div className='space-y-3'>
  344. {status.wechat_login && (
  345. <Button
  346. theme='outline'
  347. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  348. type='tertiary'
  349. icon={
  350. <Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />
  351. }
  352. onClick={onWeChatLoginClicked}
  353. loading={wechatLoading}
  354. >
  355. <span className='ml-3'>{t('使用 微信 继续')}</span>
  356. </Button>
  357. )}
  358. {status.github_oauth && (
  359. <Button
  360. theme='outline'
  361. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  362. type='tertiary'
  363. icon={<IconGithubLogo size='large' />}
  364. onClick={handleGitHubClick}
  365. loading={githubLoading}
  366. >
  367. <span className='ml-3'>{t('使用 GitHub 继续')}</span>
  368. </Button>
  369. )}
  370. {status.oidc_enabled && (
  371. <Button
  372. theme='outline'
  373. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  374. type='tertiary'
  375. icon={<OIDCIcon style={{ color: '#1877F2' }} />}
  376. onClick={handleOIDCClick}
  377. loading={oidcLoading}
  378. >
  379. <span className='ml-3'>{t('使用 OIDC 继续')}</span>
  380. </Button>
  381. )}
  382. {status.linuxdo_oauth && (
  383. <Button
  384. theme='outline'
  385. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  386. type='tertiary'
  387. icon={
  388. <LinuxDoIcon
  389. style={{
  390. color: '#E95420',
  391. width: '20px',
  392. height: '20px',
  393. }}
  394. />
  395. }
  396. onClick={handleLinuxDOClick}
  397. loading={linuxdoLoading}
  398. >
  399. <span className='ml-3'>{t('使用 LinuxDO 继续')}</span>
  400. </Button>
  401. )}
  402. {status.telegram_oauth && (
  403. <div className='flex justify-center my-2'>
  404. <TelegramLoginButton
  405. dataOnauth={onTelegramLoginClicked}
  406. botName={status.telegram_bot_name}
  407. />
  408. </div>
  409. )}
  410. {status.passkey_login && passkeySupported && (
  411. <Button
  412. theme='outline'
  413. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
  414. type='tertiary'
  415. icon={<IconKey size='large' />}
  416. onClick={handlePasskeyLogin}
  417. loading={passkeyLoading}
  418. >
  419. <span className='ml-3'>{t('使用 Passkey 登录')}</span>
  420. </Button>
  421. )}
  422. <Divider margin='12px' align='center'>
  423. {t('或')}
  424. </Divider>
  425. <Button
  426. theme='solid'
  427. type='primary'
  428. className='w-full h-12 flex items-center justify-center bg-black text-white !rounded-full hover:bg-gray-800 transition-colors'
  429. icon={<IconMail size='large' />}
  430. onClick={handleEmailLoginClick}
  431. loading={emailLoginLoading}
  432. >
  433. <span className='ml-3'>{t('使用 邮箱或用户名 登录')}</span>
  434. </Button>
  435. </div>
  436. {!status.self_use_mode_enabled && (
  437. <div className='mt-6 text-center text-sm'>
  438. <Text>
  439. {t('没有账户?')}{' '}
  440. <Link
  441. to='/register'
  442. className='text-blue-600 hover:text-blue-800 font-medium'
  443. >
  444. {t('注册')}
  445. </Link>
  446. </Text>
  447. </div>
  448. )}
  449. </div>
  450. </Card>
  451. </div>
  452. </div>
  453. );
  454. };
  455. const renderEmailLoginForm = () => {
  456. return (
  457. <div className='flex flex-col items-center'>
  458. <div className='w-full max-w-md'>
  459. <div className='flex items-center justify-center mb-6 gap-2'>
  460. <img src={logo} alt='Logo' className='h-10 rounded-full' />
  461. <Title heading={3}>{systemName}</Title>
  462. </div>
  463. <Card className='border-0 !rounded-2xl overflow-hidden'>
  464. <div className='flex justify-center pt-6 pb-2'>
  465. <Title heading={3} className='text-gray-800 dark:text-gray-200'>
  466. {t('登 录')}
  467. </Title>
  468. </div>
  469. <div className='px-2 py-8'>
  470. {status.passkey_login && passkeySupported && (
  471. <Button
  472. theme='outline'
  473. type='tertiary'
  474. className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors mb-4'
  475. icon={<IconKey size='large' />}
  476. onClick={handlePasskeyLogin}
  477. loading={passkeyLoading}
  478. >
  479. <span className='ml-3'>{t('使用 Passkey 登录')}</span>
  480. </Button>
  481. )}
  482. <Form className='space-y-3'>
  483. <Form.Input
  484. field='username'
  485. label={t('用户名或邮箱')}
  486. placeholder={t('请输入您的用户名或邮箱地址')}
  487. name='username'
  488. onChange={(value) => handleChange('username', value)}
  489. prefix={<IconMail />}
  490. />
  491. <Form.Input
  492. field='password'
  493. label={t('密码')}
  494. placeholder={t('请输入您的密码')}
  495. name='password'
  496. mode='password'
  497. onChange={(value) => handleChange('password', value)}
  498. prefix={<IconLock />}
  499. />
  500. <div className='space-y-2 pt-2'>
  501. <Button
  502. theme='solid'
  503. className='w-full !rounded-full'
  504. type='primary'
  505. htmlType='submit'
  506. onClick={handleSubmit}
  507. loading={loginLoading}
  508. >
  509. {t('继续')}
  510. </Button>
  511. <Button
  512. theme='borderless'
  513. type='tertiary'
  514. className='w-full !rounded-full'
  515. onClick={handleResetPasswordClick}
  516. loading={resetPasswordLoading}
  517. >
  518. {t('忘记密码?')}
  519. </Button>
  520. </div>
  521. </Form>
  522. {(status.github_oauth ||
  523. status.oidc_enabled ||
  524. status.wechat_login ||
  525. status.linuxdo_oauth ||
  526. status.telegram_oauth) && (
  527. <>
  528. <Divider margin='12px' align='center'>
  529. {t('或')}
  530. </Divider>
  531. <div className='mt-4 text-center'>
  532. <Button
  533. theme='outline'
  534. type='tertiary'
  535. className='w-full !rounded-full'
  536. onClick={handleOtherLoginOptionsClick}
  537. loading={otherLoginOptionsLoading}
  538. >
  539. {t('其他登录选项')}
  540. </Button>
  541. </div>
  542. </>
  543. )}
  544. {!status.self_use_mode_enabled && (
  545. <div className='mt-6 text-center text-sm'>
  546. <Text>
  547. {t('没有账户?')}{' '}
  548. <Link
  549. to='/register'
  550. className='text-blue-600 hover:text-blue-800 font-medium'
  551. >
  552. {t('注册')}
  553. </Link>
  554. </Text>
  555. </div>
  556. )}
  557. </div>
  558. </Card>
  559. </div>
  560. </div>
  561. );
  562. };
  563. // 微信登录模态框
  564. const renderWeChatLoginModal = () => {
  565. return (
  566. <Modal
  567. title={t('微信扫码登录')}
  568. visible={showWeChatLoginModal}
  569. maskClosable={true}
  570. onOk={onSubmitWeChatVerificationCode}
  571. onCancel={() => setShowWeChatLoginModal(false)}
  572. okText={t('登录')}
  573. centered={true}
  574. okButtonProps={{
  575. loading: wechatCodeSubmitLoading,
  576. }}
  577. >
  578. <div className='flex flex-col items-center'>
  579. <img src={status.wechat_qrcode} alt='微信二维码' className='mb-4' />
  580. </div>
  581. <div className='text-center mb-4'>
  582. <p>
  583. {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
  584. </p>
  585. </div>
  586. <Form>
  587. <Form.Input
  588. field='wechat_verification_code'
  589. placeholder={t('验证码')}
  590. label={t('验证码')}
  591. value={inputs.wechat_verification_code}
  592. onChange={(value) =>
  593. handleChange('wechat_verification_code', value)
  594. }
  595. />
  596. </Form>
  597. </Modal>
  598. );
  599. };
  600. // 2FA验证弹窗
  601. const render2FAModal = () => {
  602. return (
  603. <Modal
  604. title={
  605. <div className='flex items-center'>
  606. <div className='w-8 h-8 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center mr-3'>
  607. <svg
  608. className='w-4 h-4 text-green-600 dark:text-green-400'
  609. fill='currentColor'
  610. viewBox='0 0 20 20'
  611. >
  612. <path
  613. fillRule='evenodd'
  614. d='M6 8a2 2 0 11-4 0 2 2 0 014 0zM8 7a1 1 0 100 2h8a1 1 0 100-2H8zM6 14a2 2 0 11-4 0 2 2 0 014 0zM8 13a1 1 0 100 2h8a1 1 0 100-2H8z'
  615. clipRule='evenodd'
  616. />
  617. </svg>
  618. </div>
  619. 两步验证
  620. </div>
  621. }
  622. visible={showTwoFA}
  623. onCancel={handleBackToLogin}
  624. footer={null}
  625. width={450}
  626. centered
  627. >
  628. <TwoFAVerification
  629. onSuccess={handle2FASuccess}
  630. onBack={handleBackToLogin}
  631. isModal={true}
  632. />
  633. </Modal>
  634. );
  635. };
  636. return (
  637. <div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
  638. {/* 背景模糊晕染球 */}
  639. <div
  640. className='blur-ball blur-ball-indigo'
  641. style={{ top: '-80px', right: '-80px', transform: 'none' }}
  642. />
  643. <div
  644. className='blur-ball blur-ball-teal'
  645. style={{ top: '50%', left: '-120px' }}
  646. />
  647. <div className='w-full max-w-sm mt-[60px]'>
  648. {showEmailLogin ||
  649. !(
  650. status.github_oauth ||
  651. status.oidc_enabled ||
  652. status.wechat_login ||
  653. status.linuxdo_oauth ||
  654. status.telegram_oauth
  655. )
  656. ? renderEmailLoginForm()
  657. : renderOAuthOptions()}
  658. {renderWeChatLoginModal()}
  659. {render2FAModal()}
  660. {turnstileEnabled && (
  661. <div className='flex justify-center mt-6'>
  662. <Turnstile
  663. sitekey={turnstileSiteKey}
  664. onVerify={(token) => {
  665. setTurnstileToken(token);
  666. }}
  667. />
  668. </div>
  669. )}
  670. </div>
  671. </div>
  672. );
  673. };
  674. export default LoginForm;