diff --git a/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx
new file mode 100644
index 0000000000..da754794b1
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-reset-password/check-code/page.tsx
@@ -0,0 +1,96 @@
+'use client'
+import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { useState } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useContext } from 'use-context-selector'
+import Countdown from '@/app/components/signin/countdown'
+import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
+import Toast from '@/app/components/base/toast'
+import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
+import I18NContext from '@/context/i18n'
+
+export default function CheckCode() {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const email = decodeURIComponent(searchParams.get('email') as string)
+ const token = decodeURIComponent(searchParams.get('token') as string)
+ const [code, setVerifyCode] = useState('')
+ const [loading, setIsLoading] = useState(false)
+ const { locale } = useContext(I18NContext)
+
+ const verify = async () => {
+ try {
+ if (!code.trim()) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.checkCode.emptyCode'),
+ })
+ return
+ }
+ if (!/\d{6}/.test(code)) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.checkCode.invalidCode'),
+ })
+ return
+ }
+ setIsLoading(true)
+ const ret = await verifyWebAppResetPasswordCode({ email, code, token })
+ if (ret.is_valid) {
+ const params = new URLSearchParams(searchParams)
+ params.set('token', encodeURIComponent(ret.token))
+ router.push(`/webapp-reset-password/set-password?${params.toString()}`)
+ }
+ }
+ catch (error) { console.error(error) }
+ finally {
+ setIsLoading(false)
+ }
+ }
+
+ const resendCode = async () => {
+ try {
+ const res = await sendWebAppResetPasswordCode(email, locale)
+ if (res.result === 'success') {
+ const params = new URLSearchParams(searchParams)
+ params.set('token', encodeURIComponent(res.data))
+ router.replace(`/webapp-reset-password/check-code?${params.toString()}`)
+ }
+ }
+ catch (error) { console.error(error) }
+ }
+
+ return
+
+
+
+
+
{t('login.checkCode.checkYourEmail')}
+
+
+
+ {t('login.checkCode.validTime')}
+
+
+
+
+
+
router.back()} className='flex h-9 cursor-pointer items-center justify-center text-text-tertiary'>
+
+
+
+
{t('login.back')}
+
+
+}
diff --git a/web/app/(shareLayout)/webapp-reset-password/layout.tsx b/web/app/(shareLayout)/webapp-reset-password/layout.tsx
new file mode 100644
index 0000000000..c7df9ecd27
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-reset-password/layout.tsx
@@ -0,0 +1,30 @@
+'use client'
+import Header from '@/app/signin/_header'
+
+import cn from '@/utils/classnames'
+import { useGlobalPublicStore } from '@/context/global-public-context'
+
+export default function SignInLayout({ children }: any) {
+ const { systemFeatures } = useGlobalPublicStore()
+ return <>
+
+
+
+
+ {!systemFeatures.branding.enabled &&
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
+
}
+
+
+ >
+}
diff --git a/web/app/(shareLayout)/webapp-reset-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/page.tsx
new file mode 100644
index 0000000000..96cd4c5805
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-reset-password/page.tsx
@@ -0,0 +1,104 @@
+'use client'
+import Link from 'next/link'
+import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { useState } from 'react'
+import { useRouter, useSearchParams } from 'next/navigation'
+import { useContext } from 'use-context-selector'
+import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
+import { emailRegex } from '@/config'
+import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
+import Toast from '@/app/components/base/toast'
+import { sendResetPasswordCode } from '@/service/common'
+import I18NContext from '@/context/i18n'
+import { noop } from 'lodash-es'
+import useDocumentTitle from '@/hooks/use-document-title'
+
+export default function CheckCode() {
+ const { t } = useTranslation()
+ useDocumentTitle('')
+ const searchParams = useSearchParams()
+ const router = useRouter()
+ const [email, setEmail] = useState('')
+ const [loading, setIsLoading] = useState(false)
+ const { locale } = useContext(I18NContext)
+
+ const handleGetEMailVerificationCode = async () => {
+ try {
+ if (!email) {
+ Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
+ return
+ }
+
+ if (!emailRegex.test(email)) {
+ Toast.notify({
+ type: 'error',
+ message: t('login.error.emailInValid'),
+ })
+ return
+ }
+ setIsLoading(true)
+ const res = await sendResetPasswordCode(email, locale)
+ if (res.result === 'success') {
+ localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
+ const params = new URLSearchParams(searchParams)
+ params.set('token', encodeURIComponent(res.data))
+ params.set('email', encodeURIComponent(email))
+ router.push(`/webapp-reset-password/check-code?${params.toString()}`)
+ }
+ else if (res.code === 'account_not_found') {
+ Toast.notify({
+ type: 'error',
+ message: t('login.error.registrationNotAllowed'),
+ })
+ }
+ else {
+ Toast.notify({
+ type: 'error',
+ message: res.data,
+ })
+ }
+ }
+ catch (error) {
+ console.error(error)
+ }
+ finally {
+ setIsLoading(false)
+ }
+ }
+
+ return
+
+
+
+
+
{t('login.resetPassword')}
+
+ {t('login.resetPasswordDesc')}
+
+
+
+
+
+
+
+
+
+
{t('login.backToLogin')}
+
+
+}
diff --git a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx
new file mode 100644
index 0000000000..9f9a8ad4e3
--- /dev/null
+++ b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx
@@ -0,0 +1,188 @@
+'use client'
+import { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useRouter, useSearchParams } from 'next/navigation'
+import cn from 'classnames'
+import { RiCheckboxCircleFill } from '@remixicon/react'
+import { useCountDown } from 'ahooks'
+import Button from '@/app/components/base/button'
+import { changeWebAppPasswordWithToken } from '@/service/common'
+import Toast from '@/app/components/base/toast'
+import Input from '@/app/components/base/input'
+
+const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
+
+const ChangePasswordForm = () => {
+ const { t } = useTranslation()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const token = decodeURIComponent(searchParams.get('token') || '')
+
+ const [password, setPassword] = useState('')
+ const [confirmPassword, setConfirmPassword] = useState('')
+ const [showSuccess, setShowSuccess] = useState(false)
+ const [showPassword, setShowPassword] = useState(false)
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false)
+
+ const showErrorMessage = useCallback((message: string) => {
+ Toast.notify({
+ type: 'error',
+ message,
+ })
+ }, [])
+
+ const getSignInUrl = () => {
+ return `/webapp-signin?redirect_url=${searchParams.get('redirect_url') || ''}`
+ }
+
+ const AUTO_REDIRECT_TIME = 5000
+ const [leftTime, setLeftTime] = useState(undefined)
+ const [countdown] = useCountDown({
+ leftTime,
+ onEnd: () => {
+ router.replace(getSignInUrl())
+ },
+ })
+
+ const valid = useCallback(() => {
+ if (!password.trim()) {
+ showErrorMessage(t('login.error.passwordEmpty'))
+ return false
+ }
+ if (!validPassword.test(password)) {
+ showErrorMessage(t('login.error.passwordInvalid'))
+ return false
+ }
+ if (password !== confirmPassword) {
+ showErrorMessage(t('common.account.notEqual'))
+ return false
+ }
+ return true
+ }, [password, confirmPassword, showErrorMessage, t])
+
+ const handleChangePassword = useCallback(async () => {
+ if (!valid())
+ return
+ try {
+ await changeWebAppPasswordWithToken({
+ url: '/forgot-password/resets',
+ body: {
+ token,
+ new_password: password,
+ password_confirm: confirmPassword,
+ },
+ })
+ setShowSuccess(true)
+ setLeftTime(AUTO_REDIRECT_TIME)
+ }
+ catch (error) {
+ console.error(error)
+ }
+ }, [password, token, valid, confirmPassword])
+
+ return (
+
+ {!showSuccess && (
+
+
+
+ {t('login.changePassword')}
+
+
+ {t('login.changePasswordTip')}
+
+
+
+
+
+ {/* Password */}
+
+
+
+
setPassword(e.target.value)}
+ placeholder={t('login.passwordPlaceholder') || ''}
+ />
+
+
+
+
+
+
{t('login.error.passwordInvalid')}
+
+ {/* Confirm Password */}
+
+
+
+
setConfirmPassword(e.target.value)}
+ placeholder={t('login.confirmPasswordPlaceholder') || ''}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ {showSuccess && (
+
+
+
+
+
+
+ {t('login.passwordChangedTip')}
+
+
+
+
+
+
+ )}
+
+ )
+}
+
+export default ChangePasswordForm
diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx
index bf9876a5c4..3231cb4fbb 100644
--- a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx
+++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx
@@ -8,7 +8,7 @@ import Countdown from '@/app/components/signin/countdown'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
-import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
+import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
import I18NContext from '@/context/i18n'
import { checkOrSetAccessToken } from '@/app/components/share/utils'
@@ -56,7 +56,7 @@ export default function CheckCode() {
return
}
setIsLoading(true)
- const ret = await emailLoginWithCode({ email, code, token })
+ const ret = await webAppEmailLoginWithCode({ email, code, token })
if (ret.result === 'success') {
localStorage.setItem('webAppAccessToken', ret.data.access_token)
await checkOrSetAccessToken()
@@ -71,7 +71,7 @@ export default function CheckCode() {
const resendCode = async () => {
try {
- const ret = await sendEMailLoginCode(email, locale)
+ const ret = await sendWebAppEMailLoginCode(email, locale)
if (ret.result === 'success') {
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(ret.data))
diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx
index eaaea4e2b3..29af3e3a57 100644
--- a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx
+++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx
@@ -6,7 +6,7 @@ import Input from '@/app/components/base/input'
import Button from '@/app/components/base/button'
import { emailRegex } from '@/config'
import Toast from '@/app/components/base/toast'
-import { sendEMailLoginCode } from '@/service/common'
+import { sendWebAppEMailLoginCode } from '@/service/common'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
@@ -35,7 +35,7 @@ export default function MailAndCodeAuth() {
return
}
setIsLoading(true)
- const ret = await sendEMailLoginCode(email, locale)
+ const ret = await sendWebAppEMailLoginCode(email, locale)
if (ret.result === 'success') {
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
const params = new URLSearchParams(searchParams)
diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx
index e86c0b2022..1e5044b6ef 100644
--- a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx
+++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx
@@ -99,11 +99,6 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
setIsLoading(false)
}
}
- const getResetPasswordParams = () => {
- const params = new URLSearchParams(searchParams)
- params.set('from', 'webapp-signin')
- return params.toString()
- }
return