TwoFAVerification.jsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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 { API, showError, showSuccess } from '../../helpers';
  16. import {
  17. Button,
  18. Card,
  19. Divider,
  20. Form,
  21. Input,
  22. Typography,
  23. } from '@douyinfe/semi-ui';
  24. import React, { useState } from 'react';
  25. const { Title, Text, Paragraph } = Typography;
  26. const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => {
  27. const [loading, setLoading] = useState(false);
  28. const [useBackupCode, setUseBackupCode] = useState(false);
  29. const [verificationCode, setVerificationCode] = useState('');
  30. const handleSubmit = async () => {
  31. if (!verificationCode) {
  32. showError('请输入验证码');
  33. return;
  34. }
  35. // Validate code format
  36. if (useBackupCode && verificationCode.length !== 8) {
  37. showError('备用码必须是8位');
  38. return;
  39. } else if (!useBackupCode && !/^\d{6}$/.test(verificationCode)) {
  40. showError('验证码必须是6位数字');
  41. return;
  42. }
  43. setLoading(true);
  44. try {
  45. const res = await API.post('/api/user/login/2fa', {
  46. code: verificationCode,
  47. });
  48. if (res.data.success) {
  49. showSuccess('登录成功');
  50. // 保存用户信息到本地存储
  51. localStorage.setItem('user', JSON.stringify(res.data.data));
  52. if (onSuccess) {
  53. onSuccess(res.data.data);
  54. }
  55. } else {
  56. showError(res.data.message);
  57. }
  58. } catch (error) {
  59. showError('验证失败,请重试');
  60. } finally {
  61. setLoading(false);
  62. }
  63. };
  64. const handleKeyPress = (e) => {
  65. if (e.key === 'Enter') {
  66. handleSubmit();
  67. }
  68. };
  69. if (isModal) {
  70. return (
  71. <div className='space-y-4'>
  72. <Paragraph className='text-gray-600 dark:text-gray-300'>
  73. 请输入认证器应用显示的验证码完成登录
  74. </Paragraph>
  75. <Form onSubmit={handleSubmit}>
  76. <Form.Input
  77. field='code'
  78. label={useBackupCode ? '备用码' : '验证码'}
  79. placeholder={useBackupCode ? '请输入8位备用码' : '请输入6位验证码'}
  80. value={verificationCode}
  81. onChange={setVerificationCode}
  82. onKeyPress={handleKeyPress}
  83. size='large'
  84. style={{ marginBottom: 16 }}
  85. autoFocus
  86. />
  87. <Button
  88. htmlType='submit'
  89. type='primary'
  90. loading={loading}
  91. block
  92. size='large'
  93. style={{ marginBottom: 16 }}
  94. >
  95. 验证并登录
  96. </Button>
  97. </Form>
  98. <Divider />
  99. <div style={{ textAlign: 'center' }}>
  100. <Button
  101. theme='borderless'
  102. type='tertiary'
  103. onClick={() => {
  104. setUseBackupCode(!useBackupCode);
  105. setVerificationCode('');
  106. }}
  107. style={{ marginRight: 16, color: '#1890ff', padding: 0 }}
  108. >
  109. {useBackupCode ? '使用认证器验证码' : '使用备用码'}
  110. </Button>
  111. {onBack && (
  112. <Button
  113. theme='borderless'
  114. type='tertiary'
  115. onClick={onBack}
  116. style={{ color: '#1890ff', padding: 0 }}
  117. >
  118. 返回登录
  119. </Button>
  120. )}
  121. </div>
  122. <div className='bg-gray-50 dark:bg-gray-800 rounded-lg p-3'>
  123. <Text size='small' type='secondary'>
  124. <strong>提示:</strong>
  125. <br />
  126. • 验证码每30秒更新一次
  127. <br />
  128. • 如果无法获取验证码,请使用备用码
  129. <br />• 每个备用码只能使用一次
  130. </Text>
  131. </div>
  132. </div>
  133. );
  134. }
  135. return (
  136. <div
  137. style={{
  138. display: 'flex',
  139. justifyContent: 'center',
  140. alignItems: 'center',
  141. minHeight: '60vh',
  142. }}
  143. >
  144. <Card style={{ width: 400, padding: 24 }}>
  145. <div style={{ textAlign: 'center', marginBottom: 24 }}>
  146. <Title heading={3}>两步验证</Title>
  147. <Paragraph type='secondary'>
  148. 请输入认证器应用显示的验证码完成登录
  149. </Paragraph>
  150. </div>
  151. <Form onSubmit={handleSubmit}>
  152. <Form.Input
  153. field='code'
  154. label={useBackupCode ? '备用码' : '验证码'}
  155. placeholder={useBackupCode ? '请输入8位备用码' : '请输入6位验证码'}
  156. value={verificationCode}
  157. onChange={setVerificationCode}
  158. onKeyPress={handleKeyPress}
  159. size='large'
  160. style={{ marginBottom: 16 }}
  161. autoFocus
  162. />
  163. <Button
  164. htmlType='submit'
  165. type='primary'
  166. loading={loading}
  167. block
  168. size='large'
  169. style={{ marginBottom: 16 }}
  170. >
  171. 验证并登录
  172. </Button>
  173. </Form>
  174. <Divider />
  175. <div style={{ textAlign: 'center' }}>
  176. <Button
  177. theme='borderless'
  178. type='tertiary'
  179. onClick={() => {
  180. setUseBackupCode(!useBackupCode);
  181. setVerificationCode('');
  182. }}
  183. style={{ marginRight: 16, color: '#1890ff', padding: 0 }}
  184. >
  185. {useBackupCode ? '使用认证器验证码' : '使用备用码'}
  186. </Button>
  187. {onBack && (
  188. <Button
  189. theme='borderless'
  190. type='tertiary'
  191. onClick={onBack}
  192. style={{ color: '#1890ff', padding: 0 }}
  193. >
  194. 返回登录
  195. </Button>
  196. )}
  197. </div>
  198. <div
  199. style={{
  200. marginTop: 24,
  201. padding: 16,
  202. background: '#f6f8fa',
  203. borderRadius: 6,
  204. }}
  205. >
  206. <Text size='small' type='secondary'>
  207. <strong>提示:</strong>
  208. <br />
  209. • 验证码每30秒更新一次
  210. <br />
  211. • 如果无法获取验证码,请使用备用码
  212. <br />• 每个备用码只能使用一次
  213. </Text>
  214. </div>
  215. </Card>
  216. </div>
  217. );
  218. };
  219. export default TwoFAVerification;