This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
'use client'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useSearchParams } from 'next/navigation'
import { basePath } from '@/utils/var'
import cn from 'classnames'
import { CheckCircleIcon } from '@heroicons/react/24/solid'
import Input from '../components/base/input'
import Button from '@/app/components/base/button'
import { changePasswordWithToken, verifyForgotPasswordToken } from '@/service/common'
import Toast from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import { validPassword } from '@/config'
const ChangePasswordForm = () => {
const { t } = useTranslation()
const searchParams = useSearchParams()
const token = searchParams.get('token')
const verifyTokenParams = {
url: '/forgot-password/validity',
body: { token },
}
const { data: verifyTokenRes, mutate: revalidateToken } = useSWR(verifyTokenParams, verifyForgotPasswordToken, {
revalidateOnFocus: false,
})
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [showSuccess, setShowSuccess] = useState(false)
const showErrorMessage = useCallback((message: string) => {
Toast.notify({
type: 'error',
message,
})
}, [])
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 () => {
const token = searchParams.get('token') || ''
if (!valid())
return
try {
await changePasswordWithToken({
url: '/forgot-password/resets',
body: {
token,
new_password: password,
password_confirm: confirmPassword,
},
})
setShowSuccess(true)
}
catch {
await revalidateToken()
}
}, [confirmPassword, password, revalidateToken, searchParams, valid])
return (
<div className={
cn(
'flex w-full grow flex-col items-center justify-center',
'px-6',
'md:px-[108px]',
)
}>
{!verifyTokenRes && <Loading />}
{verifyTokenRes && !verifyTokenRes.is_valid && (
<div className="flex flex-col md:w-[400px]">
<div className="mx-auto w-full">
<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>
<h2 className="text-[32px] font-bold text-text-primary">{t('login.invalid')}</h2>
</div>
<div className="mx-auto mt-6 w-full">
<Button variant='primary' className='w-full !text-sm'>
<a href="https://dify.ai">{t('login.explore')}</a>
</Button>
</div>
</div>
)}
{verifyTokenRes && verifyTokenRes.is_valid && !showSuccess && (
<div className='flex flex-col md:w-[400px]'>
<div className="mx-auto w-full">
<h2 className="text-[32px] font-bold text-text-primary">
{t('login.changePassword')}
</h2>
<p className='mt-1 text-sm text-text-secondary'>
{t('login.changePasswordTip')}
</p>
</div>
<div className="mx-auto mt-6 w-full">
<div className="relative">
{/* Password */}
<div className='mb-5'>
<label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-text-primary">
{t('common.account.newPassword')}
</label>
<Input
id="password"
type='password'
value={password}
onChange={e => setPassword(e.target.value)}
placeholder={t('login.passwordPlaceholder') || ''}
className='mt-1'
/>
<div className='mt-1 text-xs text-text-secondary'>{t('login.error.passwordInvalid')}</div>
</div>
{/* Confirm Password */}
<div className='mb-5'>
<label htmlFor="confirmPassword" className="my-2 flex items-center justify-between text-sm font-medium text-text-primary">
{t('common.account.confirmPassword')}
</label>
<Input
id="confirmPassword"
type='password'
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
placeholder={t('login.confirmPasswordPlaceholder') || ''}
className='mt-1'
/>
</div>
<div>
<Button
variant='primary'
className='w-full !text-sm'
onClick={handleChangePassword}
>
{t('common.operation.reset')}
</Button>
</div>
</div>
</div>
</div>
)}
{verifyTokenRes && verifyTokenRes.is_valid && showSuccess && (
<div className="flex flex-col md:w-[400px]">
<div className="mx-auto w-full">
<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">
<CheckCircleIcon className='h-10 w-10 text-[#039855]' />
</div>
<h2 className="text-[32px] font-bold text-text-primary">
{t('login.passwordChangedTip')}
</h2>
</div>
<div className="mx-auto mt-6 w-full">
<Button variant='primary' className='w-full'>
<a href={`${basePath}/signin`}>{t('login.passwordChanged')}</a>
</Button>
</div>
</div>
)}
</div>
)
}
export default ChangePasswordForm

View File

@@ -0,0 +1,123 @@
'use client'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import Loading from '../components/base/loading'
import Input from '../components/base/input'
import Button from '@/app/components/base/button'
import { basePath } from '@/utils/var'
import {
fetchInitValidateStatus,
fetchSetupStatus,
sendForgotPasswordEmail,
} from '@/service/common'
import type { InitValidateStatusResponse } from '@/models/common'
const accountFormSchema = z.object({
email: z
.string()
.min(1, { message: 'login.error.emailInValid' })
.email('login.error.emailInValid'),
})
type AccountFormValues = z.infer<typeof accountFormSchema>
const ForgotPasswordForm = () => {
const { t } = useTranslation()
const router = useRouter()
const [loading, setLoading] = useState(true)
const [isEmailSent, setIsEmailSent] = useState(false)
const { register, trigger, getValues, formState: { errors } } = useForm<AccountFormValues>({
resolver: zodResolver(accountFormSchema),
defaultValues: { email: '' },
})
const handleSendResetPasswordEmail = async (email: string) => {
try {
const res = await sendForgotPasswordEmail({
url: '/forgot-password',
body: { email },
})
if (res.result === 'success')
setIsEmailSent(true)
else console.error('Email verification failed')
}
catch (error) {
console.error('Request failed:', error)
}
}
const handleSendResetPasswordClick = async () => {
if (isEmailSent) {
router.push('/signin')
}
else {
const isValid = await trigger('email')
if (isValid) {
const email = getValues('email')
await handleSendResetPasswordEmail(email)
}
}
}
useEffect(() => {
fetchSetupStatus().then(() => {
fetchInitValidateStatus().then((res: InitValidateStatusResponse) => {
if (res.status === 'not_started')
window.location.href = `${basePath}/init`
})
setLoading(false)
})
}, [])
return (
loading
? <Loading />
: <>
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h2 className="text-[32px] font-bold text-text-primary">
{isEmailSent ? t('login.resetLinkSent') : t('login.forgotPassword')}
</h2>
<p className='mt-1 text-sm text-text-secondary'>
{isEmailSent ? t('login.checkEmailForResetLink') : t('login.forgotPasswordDesc')}
</p>
</div>
<div className="mt-8 grow sm:mx-auto sm:w-full sm:max-w-md">
<div className="relative">
<form>
{!isEmailSent && (
<div className='mb-5'>
<label htmlFor="email"
className="my-2 flex items-center justify-between text-sm font-medium text-text-primary">
{t('login.email')}
</label>
<div className="mt-1">
<Input
{...register('email')}
placeholder={t('login.emailPlaceholder') || ''}
/>
{errors.email && <span className='text-sm text-red-400'>{t(`${errors.email?.message}`)}</span>}
</div>
</div>
)}
<div>
<Button variant='primary' className='w-full' onClick={handleSendResetPasswordClick}>
{isEmailSent ? t('login.backToSignIn') : t('login.sendResetLink')}
</Button>
</div>
</form>
</div>
</div>
</>
)
}
export default ForgotPasswordForm

View File

@@ -0,0 +1,30 @@
'use client'
import React from 'react'
import cn from 'classnames'
import { useSearchParams } from 'next/navigation'
import Header from '../signin/_header'
import ForgotPasswordForm from './ForgotPasswordForm'
import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ForgotPassword = () => {
useDocumentTitle('')
const searchParams = useSearchParams()
const token = searchParams.get('token')
const { systemFeatures } = useGlobalPublicStore()
return (
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
{token ? <ChangePasswordForm /> : <ForgotPasswordForm />}
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>}
</div>
</div>
)
}
export default ForgotPassword