LoginForm.js 18 KB

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