ChangePasswordForm.tsx 6.7 KB

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