LoginForm.js 18 KB

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