Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. 'use client'
  2. import { useCallback, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import useSWR from 'swr'
  5. import { useSearchParams } from 'next/navigation'
  6. import { basePath } from '@/utils/var'
  7. import cn from 'classnames'
  8. import { CheckCircleIcon } from '@heroicons/react/24/solid'
  9. import Input from '../components/base/input'
  10. import Button from '@/app/components/base/button'
  11. import { changePasswordWithToken, verifyForgotPasswordToken } from '@/service/common'
  12. import Toast from '@/app/components/base/toast'
  13. import Loading from '@/app/components/base/loading'
  14. import { validPassword } from '@/config'
  15. const ChangePasswordForm = () => {
  16. const { t } = useTranslation()
  17. const searchParams = useSearchParams()
  18. const token = searchParams.get('token')
  19. const verifyTokenParams = {
  20. url: '/forgot-password/validity',
  21. body: { token },
  22. }
  23. const { data: verifyTokenRes, mutate: revalidateToken } = useSWR(verifyTokenParams, verifyForgotPasswordToken, {
  24. revalidateOnFocus: false,
  25. })
  26. const [password, setPassword] = useState('')
  27. const [confirmPassword, setConfirmPassword] = useState('')
  28. const [showSuccess, setShowSuccess] = useState(false)
  29. const showErrorMessage = useCallback((message: string) => {
  30. Toast.notify({
  31. type: 'error',
  32. message,
  33. })
  34. }, [])
  35. const valid = useCallback(() => {
  36. if (!password.trim()) {
  37. showErrorMessage(t('login.error.passwordEmpty'))
  38. return false
  39. }
  40. if (!validPassword.test(password)) {
  41. showErrorMessage(t('login.error.passwordInvalid'))
  42. return false
  43. }
  44. if (password !== confirmPassword) {
  45. showErrorMessage(t('common.account.notEqual'))
  46. return false
  47. }
  48. return true
  49. }, [password, confirmPassword, showErrorMessage, t])
  50. const handleChangePassword = useCallback(async () => {
  51. const token = searchParams.get('token') || ''
  52. if (!valid())
  53. return
  54. try {
  55. await changePasswordWithToken({
  56. url: '/forgot-password/resets',
  57. body: {
  58. token,
  59. new_password: password,
  60. password_confirm: confirmPassword,
  61. },
  62. })
  63. setShowSuccess(true)
  64. }
  65. catch {
  66. await revalidateToken()
  67. }
  68. }, [confirmPassword, password, revalidateToken, searchParams, valid])
  69. return (
  70. <div className={
  71. cn(
  72. 'flex w-full grow flex-col items-center justify-center',
  73. 'px-6',
  74. 'md:px-[108px]',
  75. )
  76. }>
  77. {!verifyTokenRes && <Loading />}
  78. {verifyTokenRes && !verifyTokenRes.is_valid && (
  79. <div className="flex flex-col md:w-[400px]">
  80. <div className="mx-auto w-full">
  81. <div className="mb-3 flex h-20 w-20 items-center justify-center rounded-[20px] border border-divider-regular bg-components-option-card-option-bg p-5 text-[40px] font-bold shadow-lg">🤷‍♂️</div>
  82. <h2 className="text-[32px] font-bold text-text-primary">{t('login.invalid')}</h2>
  83. </div>
  84. <div className="mx-auto mt-6 w-full">
  85. <Button variant='primary' className='w-full !text-sm'>
  86. <a href="https://dify.ai">{t('login.explore')}</a>
  87. </Button>
  88. </div>
  89. </div>
  90. )}
  91. {verifyTokenRes && verifyTokenRes.is_valid && !showSuccess && (
  92. <div className='flex flex-col md:w-[400px]'>
  93. <div className="mx-auto w-full">
  94. <h2 className="text-[32px] font-bold text-text-primary">
  95. {t('login.changePassword')}
  96. </h2>
  97. <p className='mt-1 text-sm text-text-secondary'>
  98. {t('login.changePasswordTip')}
  99. </p>
  100. </div>
  101. <div className="mx-auto mt-6 w-full">
  102. <div className="relative">
  103. {/* Password */}
  104. <div className='mb-5'>
  105. <label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-text-primary">
  106. {t('common.account.newPassword')}
  107. </label>
  108. <Input
  109. id="password"
  110. type='password'
  111. value={password}
  112. onChange={e => setPassword(e.target.value)}
  113. placeholder={t('login.passwordPlaceholder') || ''}
  114. className='mt-1'
  115. />
  116. <div className='mt-1 text-xs text-text-secondary'>{t('login.error.passwordInvalid')}</div>
  117. </div>
  118. {/* Confirm Password */}
  119. <div className='mb-5'>
  120. <label htmlFor="confirmPassword" className="my-2 flex items-center justify-between text-sm font-medium text-text-primary">
  121. {t('common.account.confirmPassword')}
  122. </label>
  123. <Input
  124. id="confirmPassword"
  125. type='password'
  126. value={confirmPassword}
  127. onChange={e => setConfirmPassword(e.target.value)}
  128. placeholder={t('login.confirmPasswordPlaceholder') || ''}
  129. className='mt-1'
  130. />
  131. </div>
  132. <div>
  133. <Button
  134. variant='primary'
  135. className='w-full !text-sm'
  136. onClick={handleChangePassword}
  137. >
  138. {t('common.operation.reset')}
  139. </Button>
  140. </div>
  141. </div>
  142. </div>
  143. </div>
  144. )}
  145. {verifyTokenRes && verifyTokenRes.is_valid && showSuccess && (
  146. <div className="flex flex-col md:w-[400px]">
  147. <div className="mx-auto w-full">
  148. <div className="mb-3 flex h-20 w-20 items-center justify-center rounded-[20px] border border-divider-regular bg-components-option-card-option-bg p-5 text-[40px] font-bold shadow-lg">
  149. <CheckCircleIcon className='h-10 w-10 text-[#039855]' />
  150. </div>
  151. <h2 className="text-[32px] font-bold text-text-primary">
  152. {t('login.passwordChangedTip')}
  153. </h2>
  154. </div>
  155. <div className="mx-auto mt-6 w-full">
  156. <Button variant='primary' className='w-full'>
  157. <a href={`${basePath}/signin`}>{t('login.passwordChanged')}</a>
  158. </Button>
  159. </div>
  160. </div>
  161. )}
  162. </div>
  163. )
  164. }
  165. export default ChangePasswordForm