LoginForm.js 18 KB

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