feat: supports email & code login

This commit is contained in:
NFish 2024-08-29 15:26:23 +08:00
parent cea867cd06
commit a2214e3249
4 changed files with 79 additions and 33 deletions

View File

@ -6,7 +6,11 @@ import { useTranslation } from 'react-i18next'
const COUNT_DOWN_TIME_MS = 59000
const COUNT_DOWN_KEY = 'leftTime'
export default function Countdown() {
type CountdownProps = {
onResend?: () => void
}
export default function Countdown({ onResend }: CountdownProps) {
const { t } = useTranslation()
const [leftTime, setLeftTime] = useState(Number(localStorage.getItem(COUNT_DOWN_KEY) || COUNT_DOWN_TIME_MS))
const [time] = useCountDown({
@ -17,6 +21,7 @@ export default function Countdown() {
const resend = async function () {
setLeftTime(COUNT_DOWN_TIME_MS)
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
onResend?.()
}
useEffect(() => {

View File

@ -3,33 +3,55 @@ import Link from 'next/link'
import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { useRouter, useSearchParams } from 'next/navigation'
import Countdown from './countdown'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { emailLoginWithCode, getEMailLoginCode } from '@/service/common'
export default function CheckCode() {
const router = useRouter()
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
const email = searchParams.get('email') as string
const token = searchParams.get('token') as string
const [code, setVerifyCode] = useState('')
const [loading, setIsLoading] = useState(false)
const verify = async () => {
if (!code.trim()) {
Toast.notify({
type: 'error',
message: t('login.checkCode.emptyCode'),
})
return
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 emailLoginWithCode({ email, code, token })
localStorage.setItem('console_token', ret.data)
router.replace('/apps')
}
if (!/\d{6}/.test(code)) {
Toast.notify({
type: 'error',
message: t('login.checkCode.invalidCode'),
})
return
catch (error) { console.error(error) }
finally {
setIsLoading(false)
}
router.replace('/signin?console_token=123')
}
const resendCode = async () => {
try {
const ret = await getEMailLoginCode(email)
router.replace(`/signin/check-code?token=${ret.token}&email=${encodeURIComponent(email)}`)
}
catch (error) { console.error(error) }
}
return <div className='flex flex-col gap-3'>
@ -39,7 +61,7 @@ export default function CheckCode() {
<div className='pt-3 pb-4'>
<h2 className='text-4xl font-semibold'>{t('login.checkCode.checkYourEmail')}</h2>
<p className='text-text-secondary text-sm mt-2 leading-5'>
<span dangerouslySetInnerHTML={{ __html: t('login.checkCode.tips', { email: 'evan@dify.ai' }) as string }}></span>
<span dangerouslySetInnerHTML={{ __html: t('login.checkCode.tips', { email }) as string }}></span>
<br />
{t('login.checkCode.validTime')}
</p>
@ -47,9 +69,9 @@ export default function CheckCode() {
<form action="">
<label htmlFor="code" className='text-text-secondary text-sm font-semibold mb-1'>{t('login.checkCode.verificationCode')}</label>
<Input value={code} onChange={setVerifyCode} max-length={6} className='px-3 mt-1 leading-5 h-9 appearance-none' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Button className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown />
<Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='px-3 mt-1 leading-5 h-9 appearance-none' placeholder={t('login.checkCode.verificationCodePlaceholder') as string} />
<Button loading={loading} disabled={loading} className='my-3 w-full' variant='primary' onClick={verify}>{t('login.checkCode.verify')}</Button>
<Countdown onResend={resendCode} />
</form>
<div className='py-2'>
<div className='bg-gradient-to-r from-white/[0.01] via-[#101828]/8 to-white/[0.01] h-px'></div>

View File

@ -1,10 +1,11 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSearchParams } from 'next/navigation'
import { useRouter, useSearchParams } from 'next/navigation'
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 { getEMailLoginCode } from '@/service/common'
type MailAndCodeAuthProps = {
isInvite: boolean
@ -12,24 +13,36 @@ type MailAndCodeAuthProps = {
export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
const emailFromLink = searchParams.get('email') as string
const [email, setEmail] = useState(isInvite ? emailFromLink : '')
const [loading, setIsLoading] = useState(false)
const handleGetEMailVerificationCode = async () => {
if (!email) {
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
return
}
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
if (!emailRegex.test(email)) {
Toast.notify({
type: 'error',
message: t('login.error.emailInValid'),
})
return
}
setIsLoading(true)
const ret = await getEMailLoginCode(email)
router.push(`/signin/check-code?token=${ret.token}&email=${encodeURIComponent(email)}`)
}
catch (error) {
console.error(error)
}
finally {
setIsLoading(false)
}
window.location.href = '/signin/check-code'
}
return (<form onSubmit={() => { }}>
@ -39,7 +52,7 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
<Input id='email' type="email" disabled={isInvite} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} className="px-3 h-9" />
</div>
<div className='mt-3'>
<Button variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.continueWithCode')}</Button>
<Button loading={loading} disabled={loading} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.continueWithCode')}</Button>
</div>
</div>
</form>

View File

@ -308,3 +308,9 @@ export const verifyForgotPasswordToken: Fetcher<CommonResponse & { is_valid: boo
export const changePasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) =>
post<CommonResponse>(url, { body })
export const getEMailLoginCode = (email: string) =>
post<CommonResponse & { token: string }>('/email-code-login', { body: { email } })
export const emailLoginWithCode = (data: { email: string;code: string;token: string }) =>
post<CommonResponse & { data: string }>('/email-code-login/validity', { body: data })