New Auth Methods (#8119)
parent
853b0e84cc
commit
3898fe3311
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="lock">
|
||||||
|
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8 1.75C6.27411 1.75 4.875 3.14911 4.875 4.875V6.125C3.83947 6.125 3 6.96444 3 8V12.375C3 13.4106 3.83947 14.25 4.875 14.25H11.125C12.1606 14.25 13 13.4106 13 12.375V8C13 6.96444 12.1606 6.125 11.125 6.125V4.875C11.125 3.14911 9.72587 1.75 8 1.75ZM9.875 6.125V4.875C9.875 3.83947 9.03556 3 8 3C6.96444 3 6.125 3.83947 6.125 4.875V6.125H9.875ZM8 8.625C8.34519 8.625 8.625 8.90481 8.625 9.25V11.125C8.625 11.4702 8.34519 11.75 8 11.75C7.65481 11.75 7.375 11.4702 7.375 11.125V9.25C7.375 8.90481 7.65481 8.625 8 8.625Z" fill="#155AEF"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 717 B |
@ -0,0 +1,41 @@
|
|||||||
|
'use client'
|
||||||
|
import { useCountDown } from 'ahooks'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export const COUNT_DOWN_TIME_MS = 59000
|
||||||
|
export const COUNT_DOWN_KEY = 'leftTime'
|
||||||
|
|
||||||
|
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({
|
||||||
|
leftTime,
|
||||||
|
onEnd: () => {
|
||||||
|
setLeftTime(0)
|
||||||
|
localStorage.removeItem(COUNT_DOWN_KEY)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const resend = async function () {
|
||||||
|
setLeftTime(COUNT_DOWN_TIME_MS)
|
||||||
|
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
|
||||||
|
onResend?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(COUNT_DOWN_KEY, `${time}`)
|
||||||
|
}, [time])
|
||||||
|
|
||||||
|
return <p className='system-xs-regular text-text-tertiary'>
|
||||||
|
<span>{t('login.checkCode.didNotReceiveCode')}</span>
|
||||||
|
{time > 0 && <span>{Math.round(time / 1000)}s</span>}
|
||||||
|
{
|
||||||
|
time <= 0 && <span className='system-xs-medium text-text-accent-secondary cursor-pointer' onClick={resend}>{t('login.checkCode.resend')}</span>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
'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 { sendResetPasswordCode, verifyResetPasswordCode } 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 verifyResetPasswordCode({ email, code, token })
|
||||||
|
ret.is_valid && router.push(`/reset-password/set-password?${searchParams.toString()}`)
|
||||||
|
}
|
||||||
|
catch (error) { console.error(error) }
|
||||||
|
finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resendCode = async () => {
|
||||||
|
try {
|
||||||
|
const res = await sendResetPasswordCode(email, locale)
|
||||||
|
if (res.result === 'success') {
|
||||||
|
const params = new URLSearchParams(searchParams)
|
||||||
|
params.set('token', encodeURIComponent(res.data))
|
||||||
|
router.replace(`/reset-password/check-code?${params.toString()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) { console.error(error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className='flex flex-col gap-3'>
|
||||||
|
<div className='bg-background-default-dodge text-text-accent-light-mode-only border border-components-panel-border-subtle shadow-lg inline-flex w-14 h-14 justify-center items-center rounded-2xl'>
|
||||||
|
<RiMailSendFill className='w-6 h-6 text-2xl' />
|
||||||
|
</div>
|
||||||
|
<div className='pt-2 pb-4'>
|
||||||
|
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2>
|
||||||
|
<p className='mt-2 body-md-regular text-text-secondary'>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: t('login.checkCode.tips', { email }) as string }}></span>
|
||||||
|
<br />
|
||||||
|
{t('login.checkCode.validTime')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="">
|
||||||
|
<input type='text' className='hidden' />
|
||||||
|
<label htmlFor="code" className='system-md-semibold text-text-secondary mb-1'>{t('login.checkCode.verificationCode')}</label>
|
||||||
|
<Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' 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-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent h-px'></div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => router.back()} className='flex items-center justify-center h-9 text-text-tertiary cursor-pointer'>
|
||||||
|
<div className='inline-block p-1 rounded-full bg-background-default-dimm'>
|
||||||
|
<RiArrowLeftLine size={12} />
|
||||||
|
</div>
|
||||||
|
<span className='ml-2 system-xs-regular'>{t('login.back')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import Header from '../signin/_header'
|
||||||
|
import style from '../signin/page.module.css'
|
||||||
|
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
export default async function SignInLayout({ children }: any) {
|
||||||
|
return <>
|
||||||
|
<div className={cn(
|
||||||
|
style.background,
|
||||||
|
'flex w-full min-h-screen',
|
||||||
|
'sm:p-4 lg:p-8',
|
||||||
|
'gap-x-20',
|
||||||
|
'justify-center lg:justify-start',
|
||||||
|
)}>
|
||||||
|
<div className={
|
||||||
|
cn(
|
||||||
|
'flex w-full flex-col bg-white shadow rounded-2xl shrink-0',
|
||||||
|
'space-between',
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
<Header />
|
||||||
|
<div className={
|
||||||
|
cn(
|
||||||
|
'flex flex-col items-center w-full grow justify-center',
|
||||||
|
'px-6',
|
||||||
|
'md:px-[108px]',
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
<div className='flex flex-col md:w-[400px]'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='px-8 py-6 system-xs-regular text-text-tertiary'>
|
||||||
|
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
'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 '../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'
|
||||||
|
|
||||||
|
export default function CheckCode() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
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(`/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 <div className='flex flex-col gap-3'>
|
||||||
|
<div className='bg-background-default-dodge border border-components-panel-border-subtle shadow-lg inline-flex w-14 h-14 justify-center items-center rounded-2xl'>
|
||||||
|
<RiLockPasswordLine className='w-6 h-6 text-2xl text-text-accent-light-mode-only' />
|
||||||
|
</div>
|
||||||
|
<div className='pt-2 pb-4'>
|
||||||
|
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.resetPassword')}</h2>
|
||||||
|
<p className='body-md-regular mt-2 text-text-secondary'>
|
||||||
|
{t('login.resetPasswordDesc')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={() => { }}>
|
||||||
|
<input type='text' className='hidden' />
|
||||||
|
<div className='mb-2'>
|
||||||
|
<label htmlFor="email" className='my-2 system-md-semibold text-text-secondary'>{t('login.email')}</label>
|
||||||
|
<div className='mt-1'>
|
||||||
|
<Input id='email' type="email" disabled={loading} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className='mt-3'>
|
||||||
|
<Button loading={loading} disabled={loading} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.sendVerificationCode')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className='py-2'>
|
||||||
|
<div className='bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent h-px'></div>
|
||||||
|
</div>
|
||||||
|
<Link href={`/signin?${searchParams.toString()}`} className='flex items-center justify-center h-9 text-text-tertiary'>
|
||||||
|
<div className='inline-block p-1 rounded-full bg-background-default-dimm'>
|
||||||
|
<RiArrowLeftLine size={12} />
|
||||||
|
</div>
|
||||||
|
<span className='ml-2 system-xs-regular'>{t('login.backToLogin')}</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@ -0,0 +1,193 @@
|
|||||||
|
'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 { changePasswordWithToken } 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 = () => {
|
||||||
|
if (searchParams.has('invite_token')) {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('token', searchParams.get('invite_token') as string)
|
||||||
|
return `/activate?${params.toString()}`
|
||||||
|
}
|
||||||
|
return '/signin'
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTO_REDIRECT_TIME = 5000
|
||||||
|
const [leftTime, setLeftTime] = useState<number | undefined>(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 changePasswordWithToken({
|
||||||
|
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 (
|
||||||
|
<div className={
|
||||||
|
cn(
|
||||||
|
'flex flex-col items-center w-full grow justify-center',
|
||||||
|
'px-6',
|
||||||
|
'md:px-[108px]',
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
{!showSuccess && (
|
||||||
|
<div className='flex flex-col md:w-[400px]'>
|
||||||
|
<div className="w-full mx-auto">
|
||||||
|
<h2 className="title-4xl-semi-bold text-text-primary">
|
||||||
|
{t('login.changePassword')}
|
||||||
|
</h2>
|
||||||
|
<p className='mt-2 body-md-regular text-text-secondary'>
|
||||||
|
{t('login.changePasswordTip')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full mx-auto mt-6">
|
||||||
|
<div className="bg-white">
|
||||||
|
{/* Password */}
|
||||||
|
<div className='mb-5'>
|
||||||
|
<label htmlFor="password" className="my-2 system-md-semibold text-text-secondary">
|
||||||
|
{t('common.account.newPassword')}
|
||||||
|
</label>
|
||||||
|
<div className='relative mt-1'>
|
||||||
|
<Input
|
||||||
|
id="password" type={showPassword ? 'text' : 'password'}
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
placeholder={t('login.passwordPlaceholder') || ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant='ghost'
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
{showPassword ? '👀' : '😝'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mt-1 body-xs-regular text-text-secondary'>{t('login.error.passwordInvalid')}</div>
|
||||||
|
</div>
|
||||||
|
{/* Confirm Password */}
|
||||||
|
<div className='mb-5'>
|
||||||
|
<label htmlFor="confirmPassword" className="my-2 system-md-semibold text-text-secondary">
|
||||||
|
{t('common.account.confirmPassword')}
|
||||||
|
</label>
|
||||||
|
<div className='relative mt-1'>
|
||||||
|
<Input
|
||||||
|
id="confirmPassword"
|
||||||
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={e => setConfirmPassword(e.target.value)}
|
||||||
|
placeholder={t('login.confirmPasswordPlaceholder') || ''}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant='ghost'
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? '👀' : '😝'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
className='w-full'
|
||||||
|
onClick={handleChangePassword}
|
||||||
|
>
|
||||||
|
{t('login.changePasswordBtn')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showSuccess && (
|
||||||
|
<div className="flex flex-col md:w-[400px]">
|
||||||
|
<div className="w-full mx-auto">
|
||||||
|
<div className="mb-3 flex justify-center items-center w-14 h-14 rounded-2xl border border-components-panel-border-subtle shadow-lg font-bold">
|
||||||
|
<RiCheckboxCircleFill className='w-6 h-6 text-text-success' />
|
||||||
|
</div>
|
||||||
|
<h2 className="title-4xl-semi-bold text-text-primary">
|
||||||
|
{t('login.passwordChangedTip')}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="w-full mx-auto mt-6">
|
||||||
|
<Button variant='primary' className='w-full' onClick={() => {
|
||||||
|
setLeftTime(undefined)
|
||||||
|
router.replace(getSignInUrl())
|
||||||
|
}}>{t('login.passwordChanged')} ({Math.round(countdown / 1000)}) </Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChangePasswordForm
|
||||||
@ -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 { emailLoginWithCode, sendEMailLoginCode } 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 invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
|
||||||
|
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 emailLoginWithCode({ email, code, token })
|
||||||
|
if (ret.result === 'success') {
|
||||||
|
localStorage.setItem('console_token', ret.data.access_token)
|
||||||
|
localStorage.setItem('refresh_token', ret.data.refresh_token)
|
||||||
|
router.replace(invite_token ? `/signin/invite-settings?${searchParams.toString()}` : '/apps')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) { console.error(error) }
|
||||||
|
finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resendCode = async () => {
|
||||||
|
try {
|
||||||
|
const ret = await sendEMailLoginCode(email, locale)
|
||||||
|
if (ret.result === 'success') {
|
||||||
|
const params = new URLSearchParams(searchParams)
|
||||||
|
params.set('token', encodeURIComponent(ret.data))
|
||||||
|
router.replace(`/signin/check-code?${params.toString()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) { console.error(error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className='flex flex-col gap-3'>
|
||||||
|
<div className='bg-background-default-dodge border border-components-panel-border-subtle shadow-lg inline-flex w-14 h-14 justify-center items-center rounded-2xl'>
|
||||||
|
<RiMailSendFill className='w-6 h-6 text-2xl text-text-accent-light-mode-only' />
|
||||||
|
</div>
|
||||||
|
<div className='pt-2 pb-4'>
|
||||||
|
<h2 className='title-4xl-semi-bold text-text-primary'>{t('login.checkCode.checkYourEmail')}</h2>
|
||||||
|
<p className='body-md-regular mt-2 text-text-secondary'>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: t('login.checkCode.tips', { email }) as string }}></span>
|
||||||
|
<br />
|
||||||
|
{t('login.checkCode.validTime')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="">
|
||||||
|
<label htmlFor="code" className='system-md-semibold mb-1 text-text-secondary'>{t('login.checkCode.verificationCode')}</label>
|
||||||
|
<Input value={code} onChange={e => setVerifyCode(e.target.value)} max-length={6} className='mt-1' 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-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent h-px'></div>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => router.back()} className='flex items-center justify-center h-9 text-text-tertiary cursor-pointer'>
|
||||||
|
<div className='inline-block p-1 rounded-full bg-background-default-dimm'>
|
||||||
|
<RiArrowLeftLine size={12} />
|
||||||
|
</div>
|
||||||
|
<span className='ml-2 system-xs-regular'>{t('login.back')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
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 { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||||
|
import I18NContext from '@/context/i18n'
|
||||||
|
|
||||||
|
type MailAndCodeAuthProps = {
|
||||||
|
isInvite: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
|
||||||
|
const [email, setEmail] = useState(emailFromLink)
|
||||||
|
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 ret = await sendEMailLoginCode(email, locale)
|
||||||
|
if (ret.result === 'success') {
|
||||||
|
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`)
|
||||||
|
const params = new URLSearchParams(searchParams)
|
||||||
|
params.set('email', encodeURIComponent(email))
|
||||||
|
params.set('token', encodeURIComponent(ret.data))
|
||||||
|
router.push(`/signin/check-code?${params.toString()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<form onSubmit={() => { }}>
|
||||||
|
<input type='text' className='hidden' />
|
||||||
|
<div className='mb-2'>
|
||||||
|
<label htmlFor="email" className='my-2 system-md-semibold text-text-secondary'>{t('login.email')}</label>
|
||||||
|
<div className='mt-1'>
|
||||||
|
<Input id='email' type="email" disabled={isInvite} value={email} placeholder={t('login.emailPlaceholder') as string} onChange={e => setEmail(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className='mt-3'>
|
||||||
|
<Button loading={loading} disabled={loading || !email} variant='primary' className='w-full' onClick={handleGetEMailVerificationCode}>{t('login.continueWithCode')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,167 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { useContext } from 'use-context-selector'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { emailRegex } from '@/config'
|
||||||
|
import { login } from '@/service/common'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import I18NContext from '@/context/i18n'
|
||||||
|
|
||||||
|
type MailAndPasswordAuthProps = {
|
||||||
|
isInvite: boolean
|
||||||
|
allowRegistration: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
|
||||||
|
|
||||||
|
export default function MailAndPasswordAuth({ isInvite, allowRegistration }: MailAndPasswordAuthProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { locale } = useContext(I18NContext)
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
|
||||||
|
const [email, setEmail] = useState(emailFromLink)
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const handleEmailPasswordLogin = async () => {
|
||||||
|
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 (!password?.trim()) {
|
||||||
|
Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!passwordRegex.test(password)) {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: t('login.error.passwordInvalid'),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const loginData: Record<string, any> = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
language: locale,
|
||||||
|
remember_me: true,
|
||||||
|
}
|
||||||
|
if (isInvite)
|
||||||
|
loginData.invite_token = decodeURIComponent(searchParams.get('invite_token') as string)
|
||||||
|
const res = await login({
|
||||||
|
url: '/login',
|
||||||
|
body: loginData,
|
||||||
|
})
|
||||||
|
if (res.result === 'success') {
|
||||||
|
if (isInvite) {
|
||||||
|
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
localStorage.setItem('console_token', res.data.access_token)
|
||||||
|
localStorage.setItem('refresh_token', res.data.refresh_token)
|
||||||
|
router.replace('/apps')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (res.code === 'account_not_found') {
|
||||||
|
if (allowRegistration) {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('email', encodeURIComponent(email))
|
||||||
|
params.append('token', encodeURIComponent(res.data))
|
||||||
|
router.replace(`/reset-password/check-code?${params.toString()}`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: t('login.error.registrationNotAllowed'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: res.data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <form onSubmit={() => { }}>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label htmlFor="email" className="my-2 system-md-semibold text-text-secondary">
|
||||||
|
{t('login.email')}
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<Input
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
disabled={isInvite}
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
placeholder={t('login.emailPlaceholder') || ''}
|
||||||
|
tabIndex={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-3'>
|
||||||
|
<label htmlFor="password" className="my-2 flex items-center justify-between">
|
||||||
|
<span className='system-md-semibold text-text-secondary'>{t('login.password')}</span>
|
||||||
|
<Link href={`/reset-password?${searchParams.toString()}`} className='system-xs-regular text-components-button-secondary-accent-text'>
|
||||||
|
{t('login.forget')}
|
||||||
|
</Link>
|
||||||
|
</label>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter')
|
||||||
|
handleEmailPasswordLogin()
|
||||||
|
}}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
autoComplete="current-password"
|
||||||
|
placeholder={t('login.passwordPlaceholder') || ''}
|
||||||
|
tabIndex={2}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant='ghost'
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
{showPassword ? '👀' : '😝'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-2'>
|
||||||
|
<Button
|
||||||
|
tabIndex={2}
|
||||||
|
variant='primary'
|
||||||
|
onClick={handleEmailPasswordLogin}
|
||||||
|
disabled={isLoading || !email || !password}
|
||||||
|
className="w-full"
|
||||||
|
>{t('login.signBtn')}</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
|
import style from '../page.module.css'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { apiPrefix } from '@/config'
|
||||||
|
import classNames from '@/utils/classnames'
|
||||||
|
import { getPurifyHref } from '@/utils'
|
||||||
|
|
||||||
|
type SocialAuthProps = {
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SocialAuth(props: SocialAuthProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
const getOAuthLink = (href: string) => {
|
||||||
|
const url = getPurifyHref(`${apiPrefix}${href}`)
|
||||||
|
if (searchParams.has('invite_token'))
|
||||||
|
return `${url}?${searchParams.toString()}`
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
<div className='w-full'>
|
||||||
|
<a href={getOAuthLink('/oauth/login/github')}>
|
||||||
|
<Button
|
||||||
|
disabled={props.disabled}
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<span className={
|
||||||
|
classNames(
|
||||||
|
style.githubIcon,
|
||||||
|
'w-5 h-5 mr-2',
|
||||||
|
)
|
||||||
|
} />
|
||||||
|
<span className="truncate">{t('login.withGitHub')}</span>
|
||||||
|
</>
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className='w-full'>
|
||||||
|
<a href={getOAuthLink('/oauth/login/google')}>
|
||||||
|
<Button
|
||||||
|
disabled={props.disabled}
|
||||||
|
className='w-full'
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<span className={
|
||||||
|
classNames(
|
||||||
|
style.googleIcon,
|
||||||
|
'w-5 h-5 mr-2',
|
||||||
|
)
|
||||||
|
} />
|
||||||
|
<span className="truncate">{t('login.withGoogle')}</span>
|
||||||
|
</>
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
'use client'
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { getUserOAuth2SSOUrl, getUserOIDCSSOUrl, getUserSAMLSSOUrl } from '@/service/sso'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import { SSOProtocol } from '@/types/feature'
|
||||||
|
|
||||||
|
type SSOAuthProps = {
|
||||||
|
protocol: SSOProtocol | ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const SSOAuth: FC<SSOAuthProps> = ({
|
||||||
|
protocol,
|
||||||
|
}) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleSSOLogin = () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
if (protocol === SSOProtocol.SAML) {
|
||||||
|
getUserSAMLSSOUrl(invite_token).then((res) => {
|
||||||
|
router.push(res.url)
|
||||||
|
}).finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (protocol === SSOProtocol.OIDC) {
|
||||||
|
getUserOIDCSSOUrl(invite_token).then((res) => {
|
||||||
|
document.cookie = `user-oidc-state=${res.state}`
|
||||||
|
router.push(res.url)
|
||||||
|
}).finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (protocol === SSOProtocol.OAuth2) {
|
||||||
|
getUserOAuth2SSOUrl(invite_token).then((res) => {
|
||||||
|
document.cookie = `user-oauth2-state=${res.state}`
|
||||||
|
router.push(res.url)
|
||||||
|
}).finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Toast.notify({
|
||||||
|
type: 'error',
|
||||||
|
message: 'invalid SSO protocol',
|
||||||
|
})
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => { handleSSOLogin() }}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Lock01 className='mr-2 w-5 h-5 text-text-accent-light-mode-only' />
|
||||||
|
<span className="truncate">{t('login.withSSO')}</span>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SSOAuth
|
||||||
@ -1,34 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import React from 'react'
|
|
||||||
import { useSearchParams } from 'next/navigation'
|
|
||||||
|
|
||||||
import NormalForm from './normalForm'
|
|
||||||
import OneMoreStep from './oneMoreStep'
|
|
||||||
import cn from '@/utils/classnames'
|
|
||||||
|
|
||||||
const Forms = () => {
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const step = searchParams.get('step')
|
|
||||||
|
|
||||||
const getForm = () => {
|
|
||||||
switch (step) {
|
|
||||||
case 'next':
|
|
||||||
return <OneMoreStep />
|
|
||||||
default:
|
|
||||||
return <NormalForm />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <div className={
|
|
||||||
cn(
|
|
||||||
'flex flex-col items-center w-full grow justify-center',
|
|
||||||
'px-6',
|
|
||||||
'md:px-[108px]',
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
<div className='flex flex-col md:w-[400px]'>
|
|
||||||
{getForm()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Forms
|
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import Script from 'next/script'
|
||||||
|
import Header from './_header'
|
||||||
|
import style from './page.module.css'
|
||||||
|
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { IS_CE_EDITION } from '@/config'
|
||||||
|
|
||||||
|
export default async function SignInLayout({ children }: any) {
|
||||||
|
return <>
|
||||||
|
{!IS_CE_EDITION && (
|
||||||
|
<>
|
||||||
|
<Script strategy="beforeInteractive" async src={'https://www.googletagmanager.com/gtag/js?id=AW-11217955271'}></Script>
|
||||||
|
<Script
|
||||||
|
id="ga-monitor-register"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: 'window.dataLayer2 = window.dataLayer2 || [];function gtag(){dataLayer2.push(arguments);}gtag(\'js\', new Date());gtag(\'config\', \'AW-11217955271"\');',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</Script>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={cn(
|
||||||
|
style.background,
|
||||||
|
'flex w-full min-h-screen',
|
||||||
|
'sm:p-4 lg:p-8',
|
||||||
|
'gap-x-20',
|
||||||
|
'justify-center lg:justify-start',
|
||||||
|
)}>
|
||||||
|
<div className={
|
||||||
|
cn(
|
||||||
|
'flex w-full flex-col bg-white shadow rounded-2xl shrink-0',
|
||||||
|
'space-between',
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
<Header />
|
||||||
|
<div className={
|
||||||
|
cn(
|
||||||
|
'flex flex-col items-center w-full grow justify-center',
|
||||||
|
'px-6',
|
||||||
|
'md:px-[108px]',
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
<div className='flex flex-col md:w-[400px]'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='px-8 py-6 system-xs-regular text-text-tertiary'>
|
||||||
|
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@ -1,94 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useEffect, useState } from 'react'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import Script from 'next/script'
|
import OneMoreStep from './oneMoreStep'
|
||||||
import Loading from '../components/base/loading'
|
import NormalForm from './normalForm'
|
||||||
import Forms from './forms'
|
|
||||||
import Header from './_header'
|
|
||||||
import style from './page.module.css'
|
|
||||||
import UserSSOForm from './userSSOForm'
|
|
||||||
import cn from '@/utils/classnames'
|
|
||||||
import { IS_CE_EDITION } from '@/config'
|
|
||||||
|
|
||||||
import type { SystemFeatures } from '@/types/feature'
|
|
||||||
import { defaultSystemFeatures } from '@/types/feature'
|
|
||||||
import { getSystemFeatures } from '@/service/common'
|
|
||||||
|
|
||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
const searchParams = useSearchParams()
|
||||||
const [systemFeatures, setSystemFeatures] = useState<SystemFeatures>(defaultSystemFeatures)
|
const step = searchParams.get('step')
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getSystemFeatures().then((res) => {
|
|
||||||
setSystemFeatures(res)
|
|
||||||
}).finally(() => {
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!IS_CE_EDITION && (
|
|
||||||
<>
|
|
||||||
<Script strategy="beforeInteractive" async src={'https://www.googletagmanager.com/gtag/js?id=AW-11217955271'}></Script>
|
|
||||||
<Script
|
|
||||||
id="ga-monitor-register"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `
|
|
||||||
window.dataLayer2 = window.dataLayer2 || [];
|
|
||||||
function gtag(){dataLayer2.push(arguments);}
|
|
||||||
gtag('js', new Date());
|
|
||||||
gtag('config', 'AW-11217955271"');
|
|
||||||
`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</Script>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className={cn(
|
|
||||||
style.background,
|
|
||||||
'flex w-full min-h-screen',
|
|
||||||
'sm:p-4 lg:p-8',
|
|
||||||
'gap-x-20',
|
|
||||||
'justify-center lg:justify-start',
|
|
||||||
)}>
|
|
||||||
<div className={
|
|
||||||
cn(
|
|
||||||
'flex w-full flex-col bg-white shadow rounded-2xl shrink-0',
|
|
||||||
'space-between',
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
{loading && (
|
|
||||||
<div className={
|
|
||||||
cn(
|
|
||||||
'flex flex-col items-center w-full grow justify-center',
|
|
||||||
'px-6',
|
|
||||||
'md:px-[108px]',
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
<Loading type='area' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && !systemFeatures.sso_enforced_for_signin && (
|
|
||||||
<>
|
|
||||||
<Forms />
|
|
||||||
<div className='px-8 py-6 text-sm font-normal text-gray-500'>
|
|
||||||
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && systemFeatures.sso_enforced_for_signin && (
|
|
||||||
<UserSSOForm protocol={systemFeatures.sso_enforced_for_signin_protocol} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</>
|
if (step === 'next')
|
||||||
)
|
return <OneMoreStep />
|
||||||
|
return <NormalForm />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SignIn
|
export default SignIn
|
||||||
|
|||||||
@ -1,107 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
|
||||||
import type { FC } from 'react'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import cn from '@/utils/classnames'
|
|
||||||
import Toast from '@/app/components/base/toast'
|
|
||||||
import { getUserOAuth2SSOUrl, getUserOIDCSSOUrl, getUserSAMLSSOUrl } from '@/service/sso'
|
|
||||||
import Button from '@/app/components/base/button'
|
|
||||||
import useRefreshToken from '@/hooks/use-refresh-token'
|
|
||||||
|
|
||||||
type UserSSOFormProps = {
|
|
||||||
protocol: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserSSOForm: FC<UserSSOFormProps> = ({
|
|
||||||
protocol,
|
|
||||||
}) => {
|
|
||||||
const { getNewAccessToken } = useRefreshToken()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const consoleToken = searchParams.get('access_token')
|
|
||||||
const refreshToken = searchParams.get('refresh_token')
|
|
||||||
const message = searchParams.get('message')
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (refreshToken && consoleToken) {
|
|
||||||
localStorage.setItem('console_token', consoleToken)
|
|
||||||
localStorage.setItem('refresh_token', refreshToken)
|
|
||||||
getNewAccessToken()
|
|
||||||
router.replace('/apps')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
Toast.notify({
|
|
||||||
type: 'error',
|
|
||||||
message,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [consoleToken, refreshToken, message, router])
|
|
||||||
|
|
||||||
const handleSSOLogin = () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
if (protocol === 'saml') {
|
|
||||||
getUserSAMLSSOUrl().then((res) => {
|
|
||||||
router.push(res.url)
|
|
||||||
}).finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (protocol === 'oidc') {
|
|
||||||
getUserOIDCSSOUrl().then((res) => {
|
|
||||||
document.cookie = `user-oidc-state=${res.state}`
|
|
||||||
router.push(res.url)
|
|
||||||
}).finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else if (protocol === 'oauth2') {
|
|
||||||
getUserOAuth2SSOUrl().then((res) => {
|
|
||||||
document.cookie = `user-oauth2-state=${res.state}`
|
|
||||||
router.push(res.url)
|
|
||||||
}).finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Toast.notify({
|
|
||||||
type: 'error',
|
|
||||||
message: 'invalid SSO protocol',
|
|
||||||
})
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={
|
|
||||||
cn(
|
|
||||||
'flex flex-col items-center w-full grow justify-center',
|
|
||||||
'px-6',
|
|
||||||
'md:px-[108px]',
|
|
||||||
)
|
|
||||||
}>
|
|
||||||
<div className='flex flex-col md:w-[400px]'>
|
|
||||||
<div className="w-full mx-auto">
|
|
||||||
<h2 className="text-[32px] font-bold text-gray-900">{t('login.pageTitle')}</h2>
|
|
||||||
</div>
|
|
||||||
<div className="w-full mx-auto mt-10">
|
|
||||||
<Button
|
|
||||||
tabIndex={0}
|
|
||||||
variant='primary'
|
|
||||||
onClick={() => { handleSSOLogin() }}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full"
|
|
||||||
>{t('login.sso')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserSSOForm
|
|
||||||
@ -1,13 +1,16 @@
|
|||||||
import { get } from './base'
|
import { get } from './base'
|
||||||
|
|
||||||
export const getUserSAMLSSOUrl = () => {
|
export const getUserSAMLSSOUrl = (invite_token?: string) => {
|
||||||
return get<{ url: string }>('/enterprise/sso/saml/login')
|
const url = invite_token ? `/enterprise/sso/saml/login?invite_token=${invite_token}` : '/enterprise/sso/saml/login'
|
||||||
|
return get<{ url: string }>(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUserOIDCSSOUrl = () => {
|
export const getUserOIDCSSOUrl = (invite_token?: string) => {
|
||||||
return get<{ url: string; state: string }>('/enterprise/sso/oidc/login')
|
const url = invite_token ? `/enterprise/sso/oidc/login?invite_token=${invite_token}` : '/enterprise/sso/oidc/login'
|
||||||
|
return get<{ url: string; state: string }>(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUserOAuth2SSOUrl = () => {
|
export const getUserOAuth2SSOUrl = (invite_token?: string) => {
|
||||||
return get<{ url: string; state: string }>('/enterprise/sso/oauth2/login')
|
const url = invite_token ? `/enterprise/sso/oauth2/login?invite_token=${invite_token}` : '/enterprise/sso/oauth2/login'
|
||||||
|
return get<{ url: string; state: string }>(url)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue