mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 03:32:23 +08:00
New Auth Methods (#8119)
This commit is contained in:
parent
853b0e84cc
commit
3898fe3311
|
@ -15,6 +15,7 @@ import { ToastContext } from '@/app/components/base/toast'
|
|||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
||||
const titleClassName = `
|
||||
text-sm font-medium text-gray-900
|
||||
|
@ -31,6 +32,7 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
|
|||
|
||||
export default function AccountPage() {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useAppContext()
|
||||
const { mutateUserProfile, userProfile, apps } = useAppContext()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
|
||||
|
@ -41,6 +43,9 @@ export default function AccountPage() {
|
|||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false)
|
||||
const [showCurrentPassword, setShowCurrentPassword] = useState(false)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
|
||||
const handleEditName = () => {
|
||||
setEditNameModalVisible(true)
|
||||
|
@ -158,8 +163,8 @@ export default function AccountPage() {
|
|||
</div>
|
||||
</div>
|
||||
{
|
||||
IS_CE_EDITION && (
|
||||
<div className='mb-8 flex justify-between'>
|
||||
systemFeatures.enable_email_password_login && (
|
||||
<div className='mb-8 flex justify-between gap-2'>
|
||||
<div>
|
||||
<div className='mb-1 text-sm font-medium text-gray-900'>{t('common.account.password')}</div>
|
||||
<div className='mb-2 text-xs text-gray-500'>{t('common.account.passwordTip')}</div>
|
||||
|
@ -191,8 +196,7 @@ export default function AccountPage() {
|
|||
>
|
||||
<div className='mb-6 text-lg font-medium text-gray-900'>{t('common.account.editName')}</div>
|
||||
<div className={titleClassName}>{t('common.account.name')}</div>
|
||||
<input
|
||||
className={inputClassName}
|
||||
<Input className='mt-2'
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
/>
|
||||
|
@ -223,30 +227,61 @@ export default function AccountPage() {
|
|||
{userProfile.is_password_set && (
|
||||
<>
|
||||
<div className={titleClassName}>{t('common.account.currentPassword')}</div>
|
||||
<input
|
||||
type="password"
|
||||
className={inputClassName}
|
||||
value={currentPassword}
|
||||
onChange={e => setCurrentPassword(e.target.value)}
|
||||
/>
|
||||
<div className='relative mt-2'>
|
||||
<Input
|
||||
type={showCurrentPassword ? 'text' : 'password'}
|
||||
value={currentPassword}
|
||||
onChange={e => setCurrentPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant='ghost'
|
||||
onClick={() => setShowCurrentPassword(!showCurrentPassword)}
|
||||
>
|
||||
{showCurrentPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className='mt-8 text-sm font-medium text-gray-900'>
|
||||
{userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
className={inputClassName}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<div className='relative mt-2'>
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<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-8 text-sm font-medium text-gray-900'>{t('common.account.confirmPassword')}</div>
|
||||
<input
|
||||
type="password"
|
||||
className={inputClassName}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
<div className='relative mt-2'>
|
||||
<Input
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant='ghost'
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? '👀' : '😝'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-end mt-10'>
|
||||
<Button className='mr-2' onClick={() => {
|
||||
setEditPasswordModalVisible(false)
|
||||
|
|
|
@ -58,7 +58,7 @@ export default function AppSelector() {
|
|||
>
|
||||
<Menu.Items
|
||||
className="
|
||||
absolute -right-3 -top-3 w-60 max-w-80
|
||||
absolute -right-2 -top-1 w-60 max-w-80
|
||||
divide-y divide-gray-100 origin-top-right rounded-lg bg-white
|
||||
shadow-lg
|
||||
"
|
||||
|
|
|
@ -1,27 +1,16 @@
|
|||
'use client'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
||||
import style from './style.module.css'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { timezones } from '@/utils/timezone'
|
||||
import { LanguagesSupported, languages } from '@/i18n/language'
|
||||
import { activateMember, invitationCheck } from '@/service/common'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { invitationCheck } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import I18n from '@/context/i18n'
|
||||
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
|
||||
|
||||
const ActivateForm = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const { locale, setLocaleOnClient } = useContext(I18n)
|
||||
const searchParams = useSearchParams()
|
||||
const workspaceID = searchParams.get('workspace_id')
|
||||
const email = searchParams.get('email')
|
||||
|
@ -35,64 +24,20 @@ const ActivateForm = () => {
|
|||
token,
|
||||
},
|
||||
}
|
||||
const { data: checkRes, mutate: recheck } = useSWR(checkParams, invitationCheck, {
|
||||
const { data: checkRes } = useSWR(checkParams, invitationCheck, {
|
||||
revalidateOnFocus: false,
|
||||
onSuccess(data) {
|
||||
if (data.is_valid) {
|
||||
const params = new URLSearchParams(searchParams)
|
||||
const { email, workspace_id } = data.data
|
||||
params.set('email', encodeURIComponent(email))
|
||||
params.set('workspace_id', encodeURIComponent(workspace_id))
|
||||
params.set('invite_token', encodeURIComponent(token as string))
|
||||
router.replace(`/signin?${params.toString()}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone)
|
||||
const [language, setLanguage] = useState(locale)
|
||||
const [showSuccess, setShowSuccess] = useState(false)
|
||||
|
||||
const showErrorMessage = useCallback((message: string) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message,
|
||||
})
|
||||
}, [])
|
||||
|
||||
const valid = useCallback(() => {
|
||||
if (!name.trim()) {
|
||||
showErrorMessage(t('login.error.nameEmpty'))
|
||||
return false
|
||||
}
|
||||
if (!password.trim()) {
|
||||
showErrorMessage(t('login.error.passwordEmpty'))
|
||||
return false
|
||||
}
|
||||
if (!validPassword.test(password)) {
|
||||
showErrorMessage(t('login.error.passwordInvalid'))
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, [name, password, showErrorMessage, t])
|
||||
|
||||
const handleActivate = useCallback(async () => {
|
||||
if (!valid())
|
||||
return
|
||||
try {
|
||||
await activateMember({
|
||||
url: '/activate',
|
||||
body: {
|
||||
workspace_id: workspaceID,
|
||||
email,
|
||||
token,
|
||||
name,
|
||||
password,
|
||||
interface_language: language,
|
||||
timezone,
|
||||
},
|
||||
})
|
||||
setLocaleOnClient(language, false)
|
||||
setShowSuccess(true)
|
||||
}
|
||||
catch {
|
||||
recheck()
|
||||
}
|
||||
}, [email, language, name, password, recheck, setLocaleOnClient, timezone, token, valid, workspaceID])
|
||||
|
||||
return (
|
||||
<div className={
|
||||
cn(
|
||||
|
@ -115,125 +60,6 @@ const ActivateForm = () => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{checkRes && checkRes.is_valid && !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-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold ${style.logo}`}>
|
||||
</div>
|
||||
<h2 className="text-[32px] font-bold text-gray-900">
|
||||
{`${t('login.join')} ${checkRes.workspace_name}`}
|
||||
</h2>
|
||||
<p className='mt-1 text-sm text-gray-600 '>
|
||||
{`${t('login.joinTipStart')} ${checkRes.workspace_name} ${t('login.joinTipEnd')}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full mx-auto mt-6">
|
||||
<div className="bg-white">
|
||||
{/* username */}
|
||||
<div className='mb-5'>
|
||||
<label htmlFor="name" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
|
||||
{t('login.name')}
|
||||
</label>
|
||||
<div className="mt-1 relative rounded-md shadow-sm">
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder={t('login.namePlaceholder') || ''}
|
||||
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'}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* password */}
|
||||
<div className='mb-5'>
|
||||
<label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
|
||||
{t('login.password')}
|
||||
</label>
|
||||
<div className="mt-1 relative rounded-md shadow-sm">
|
||||
<input
|
||||
id="password"
|
||||
type='password'
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder={t('login.passwordPlaceholder') || ''}
|
||||
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'}
|
||||
tabIndex={2}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-1 text-xs text-gray-500'>{t('login.error.passwordInvalid')}</div>
|
||||
</div>
|
||||
{/* language */}
|
||||
<div className='mb-5'>
|
||||
<label htmlFor="name" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
|
||||
{t('login.interfaceLanguage')}
|
||||
</label>
|
||||
<div className="relative mt-1 rounded-md shadow-sm">
|
||||
<SimpleSelect
|
||||
defaultValue={LanguagesSupported[0]}
|
||||
items={languages.filter(item => item.supported)}
|
||||
onSelect={(item) => {
|
||||
setLanguage(item.value as string)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* timezone */}
|
||||
<div className='mb-4'>
|
||||
<label htmlFor="timezone" className="block text-sm font-medium text-gray-700">
|
||||
{t('login.timezone')}
|
||||
</label>
|
||||
<div className="relative mt-1 rounded-md shadow-sm">
|
||||
<SimpleSelect
|
||||
defaultValue={timezone}
|
||||
items={timezones}
|
||||
onSelect={(item) => {
|
||||
setTimezone(item.value as string)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full !text-sm'
|
||||
onClick={handleActivate}
|
||||
>
|
||||
{`${t('login.join')} ${checkRes.workspace_name}`}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="block w-hull mt-2 text-xs text-gray-600">
|
||||
{t('login.license.tip')}
|
||||
|
||||
<Link
|
||||
className='text-primary-600'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href={`https://docs.dify.ai/${language !== LanguagesSupported[1] ? 'user-agreement' : `v/${locale.toLowerCase()}/policies`}/open-source`}
|
||||
>{t('login.license.link')}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{checkRes && checkRes.is_valid && 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-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold">
|
||||
<CheckCircleIcon className='w-10 h-10 text-[#039855]' />
|
||||
</div>
|
||||
<h2 className="text-[32px] font-bold text-gray-900">
|
||||
{`${t('login.activatedTipStart')} ${checkRes.workspace_name} ${t('login.activatedTipEnd')}`}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="w-full mx-auto mt-6">
|
||||
<Button variant='primary' className='w-full !text-sm'>
|
||||
<a href="/signin">{t('login.activated')}</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -2,7 +2,7 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import Collapse from '../collapse'
|
||||
import type { IItem } from '../collapse'
|
||||
import s from './index.module.css'
|
||||
|
@ -11,7 +11,7 @@ import Modal from '@/app/components/base/modal'
|
|||
import Confirm from '@/app/components/base/confirm'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import AppContext, { useAppContext } from '@/context/app-context'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
|
@ -42,6 +42,7 @@ export default function AccountPage() {
|
|||
const [password, setPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false)
|
||||
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
||||
|
||||
const handleEditName = () => {
|
||||
setEditNameModalVisible(true)
|
||||
|
@ -144,7 +145,7 @@ export default function AccountPage() {
|
|||
<div className={titleClassName}>{t('common.account.email')}</div>
|
||||
<div className={classNames(inputClassName, 'cursor-pointer')}>{userProfile.email}</div>
|
||||
</div>
|
||||
{IS_CE_EDITION && (
|
||||
{systemFeatures.enable_email_password_login && (
|
||||
<div className='mb-8'>
|
||||
<div className='mb-1 text-sm font-medium text-gray-900'>{t('common.account.password')}</div>
|
||||
<div className='mb-2 text-xs text-gray-500'>{t('common.account.passwordTip')}</div>
|
||||
|
|
41
web/app/components/signin/countdown.tsx
Normal file
41
web/app/components/signin/countdown.tsx
Normal file
|
@ -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>
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
'use client'
|
||||
|
||||
import { SWRConfig } from 'swr'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import useRefreshToken from '@/hooks/use-refresh-token'
|
||||
import { fetchSetupStatus } from '@/service/common'
|
||||
|
||||
type SwrInitorProps = {
|
||||
children: ReactNode
|
||||
|
@ -21,27 +22,60 @@ const SwrInitor = ({
|
|||
const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token')
|
||||
const [init, setInit] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!(consoleToken || refreshToken || consoleTokenFromLocalStorage || refreshTokenFromLocalStorage)) {
|
||||
router.replace('/signin')
|
||||
return
|
||||
const isSetupFinished = useCallback(async () => {
|
||||
try {
|
||||
if (localStorage.getItem('setup_status') === 'finished')
|
||||
return true
|
||||
const setUpStatus = await fetchSetupStatus()
|
||||
if (setUpStatus.step !== 'finished') {
|
||||
localStorage.removeItem('setup_status')
|
||||
return false
|
||||
}
|
||||
localStorage.setItem('setup_status', 'finished')
|
||||
return true
|
||||
}
|
||||
if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage)
|
||||
getNewAccessToken()
|
||||
|
||||
if (consoleToken && refreshToken) {
|
||||
localStorage.setItem('console_token', consoleToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
getNewAccessToken().then(() => {
|
||||
router.replace('/apps', { forceOptimisticNavigation: false } as any)
|
||||
}).catch(() => {
|
||||
router.replace('/signin')
|
||||
})
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
|
||||
setInit(true)
|
||||
}, [])
|
||||
|
||||
const setRefreshToken = useCallback(async () => {
|
||||
try {
|
||||
if (!(consoleToken || refreshToken || consoleTokenFromLocalStorage || refreshTokenFromLocalStorage))
|
||||
return Promise.reject(new Error('No token found'))
|
||||
|
||||
if (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage)
|
||||
await getNewAccessToken()
|
||||
|
||||
if (consoleToken && refreshToken) {
|
||||
localStorage.setItem('console_token', consoleToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
await getNewAccessToken()
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}, [consoleToken, refreshToken, consoleTokenFromLocalStorage, refreshTokenFromLocalStorage, getNewAccessToken])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const isFinished = await isSetupFinished()
|
||||
if (!isFinished) {
|
||||
router.replace('/install')
|
||||
return
|
||||
}
|
||||
await setRefreshToken()
|
||||
setInit(true)
|
||||
}
|
||||
catch (error) {
|
||||
router.replace('/signin')
|
||||
}
|
||||
})()
|
||||
}, [isSetupFinished, setRefreshToken, router])
|
||||
|
||||
return init
|
||||
? (
|
||||
<SWRConfig value={{
|
||||
|
|
|
@ -5,6 +5,7 @@ import useSWR from 'swr'
|
|||
import { useSearchParams } from 'next/navigation'
|
||||
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'
|
||||
|
@ -113,33 +114,29 @@ const ChangePasswordForm = () => {
|
|||
<label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
|
||||
{t('common.account.newPassword')}
|
||||
</label>
|
||||
<div className="mt-1 relative rounded-md shadow-sm">
|
||||
<input
|
||||
id="password"
|
||||
type='password'
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
placeholder={t('login.passwordPlaceholder') || ''}
|
||||
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'}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-1 text-xs text-gray-500'>{t('login.error.passwordInvalid')}</div>
|
||||
<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-gray-900">
|
||||
{t('common.account.confirmPassword')}
|
||||
</label>
|
||||
<div className="mt-1 relative rounded-md shadow-sm">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type='password'
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
placeholder={t('login.confirmPasswordPlaceholder') || ''}
|
||||
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'}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type='password'
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
placeholder={t('login.confirmPasswordPlaceholder') || ''}
|
||||
className='mt-1'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
|
@ -165,7 +162,7 @@ const ChangePasswordForm = () => {
|
|||
</h2>
|
||||
</div>
|
||||
<div className="w-full mx-auto mt-6">
|
||||
<Button variant='primary' className='w-full !text-sm'>
|
||||
<Button variant='primary' className='w-full'>
|
||||
<a href="/signin">{t('login.passwordChanged')}</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ 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 {
|
||||
|
@ -78,7 +79,7 @@ const ForgotPasswordForm = () => {
|
|||
|
||||
return (
|
||||
loading
|
||||
? <Loading/>
|
||||
? <Loading />
|
||||
: <>
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<h2 className="text-[32px] font-bold text-gray-900">
|
||||
|
@ -98,10 +99,9 @@ const ForgotPasswordForm = () => {
|
|||
{t('login.email')}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
<Input
|
||||
{...register('email')}
|
||||
placeholder={t('login.emailPlaceholder') || ''}
|
||||
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'}
|
||||
/>
|
||||
{errors.email && <span className='text-red-400 text-sm'>{t(`${errors.email?.message}`)}</span>}
|
||||
</div>
|
||||
|
|
|
@ -65,6 +65,7 @@ const InstallForm = () => {
|
|||
useEffect(() => {
|
||||
fetchSetupStatus().then((res: SetupStatusResponse) => {
|
||||
if (res.step === 'finished') {
|
||||
localStorage.setItem('setup_status', 'finished')
|
||||
window.location.href = '/signin'
|
||||
}
|
||||
else {
|
||||
|
@ -153,7 +154,7 @@ const InstallForm = () => {
|
|||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="block w-hull mt-2 text-xs text-gray-600">
|
||||
<div className="block w-full mt-2 text-xs text-gray-600">
|
||||
{t('login.license.tip')}
|
||||
|
||||
<Link
|
||||
|
|
92
web/app/reset-password/check-code/page.tsx
Normal file
92
web/app/reset-password/check-code/page.tsx
Normal file
|
@ -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>
|
||||
}
|
39
web/app/reset-password/layout.tsx
Normal file
39
web/app/reset-password/layout.tsx
Normal file
|
@ -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>
|
||||
</>
|
||||
}
|
101
web/app/reset-password/page.tsx
Normal file
101
web/app/reset-password/page.tsx
Normal file
|
@ -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>
|
||||
}
|
193
web/app/reset-password/set-password/page.tsx
Normal file
193
web/app/reset-password/set-password/page.tsx
Normal file
|
@ -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
|
96
web/app/signin/check-code/page.tsx
Normal file
96
web/app/signin/check-code/page.tsx
Normal file
|
@ -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>
|
||||
}
|
71
web/app/signin/components/mail-and-code-auth.tsx
Normal file
71
web/app/signin/components/mail-and-code-auth.tsx
Normal file
|
@ -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>
|
||||
)
|
||||
}
|
167
web/app/signin/components/mail-and-password-auth.tsx
Normal file
167
web/app/signin/components/mail-and-password-auth.tsx
Normal file
|
@ -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>
|
||||
}
|
62
web/app/signin/components/social-auth.tsx
Normal file
62
web/app/signin/components/social-auth.tsx
Normal file
|
@ -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>
|
||||
</>
|
||||
}
|
73
web/app/signin/components/sso-auth.tsx
Normal file
73
web/app/signin/components/sso-auth.tsx
Normal file
|
@ -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
|
154
web/app/signin/invite-settings/page.tsx
Normal file
154
web/app/signin/invite-settings/page.tsx
Normal file
|
@ -0,0 +1,154 @@
|
|||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import useSWR from 'swr'
|
||||
import { RiAccountCircleLine } from '@remixicon/react'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { timezones } from '@/utils/timezone'
|
||||
import { LanguagesSupported, languages } from '@/i18n/language'
|
||||
import I18n from '@/context/i18n'
|
||||
import { activateMember, invitationCheck } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
export default function InviteSettingsPage() {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const token = decodeURIComponent(searchParams.get('invite_token') as string)
|
||||
const { locale, setLocaleOnClient } = useContext(I18n)
|
||||
const [name, setName] = useState('')
|
||||
const [language, setLanguage] = useState(LanguagesSupported[0])
|
||||
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/Los_Angeles')
|
||||
|
||||
const checkParams = {
|
||||
url: '/activate/check',
|
||||
params: {
|
||||
token,
|
||||
},
|
||||
}
|
||||
const { data: checkRes, mutate: recheck } = useSWR(checkParams, invitationCheck, {
|
||||
revalidateOnFocus: false,
|
||||
})
|
||||
|
||||
const handleActivate = useCallback(async () => {
|
||||
try {
|
||||
if (!name) {
|
||||
Toast.notify({ type: 'error', message: t('login.enterYourName') })
|
||||
return
|
||||
}
|
||||
const res = await activateMember({
|
||||
url: '/activate',
|
||||
body: {
|
||||
token,
|
||||
name,
|
||||
interface_language: language,
|
||||
timezone,
|
||||
},
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
localStorage.setItem('console_token', res.data.access_token)
|
||||
localStorage.setItem('refresh_token', res.data.refresh_token)
|
||||
setLocaleOnClient(language, false)
|
||||
router.replace('/apps')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
recheck()
|
||||
}
|
||||
}, [language, name, recheck, setLocaleOnClient, timezone, token, router, t])
|
||||
|
||||
if (!checkRes)
|
||||
return <Loading />
|
||||
if (!checkRes.is_valid) {
|
||||
return <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 text-2xl font-bold">🤷♂️</div>
|
||||
<h2 className="title-4xl-semi-bold">{t('login.invalid')}</h2>
|
||||
</div>
|
||||
<div className="w-full mx-auto mt-6">
|
||||
<Button variant='primary' className='w-full !text-sm'>
|
||||
<a href="https://dify.ai">{t('login.explore')}</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
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'>
|
||||
<RiAccountCircleLine 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'>{t('login.setYourAccount')}</h2>
|
||||
</div>
|
||||
<form action=''>
|
||||
|
||||
<div className='mb-5'>
|
||||
<label htmlFor="name" className="my-2 system-md-semibold">
|
||||
{t('login.name')}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder={t('login.namePlaceholder') || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-5'>
|
||||
<label htmlFor="name" className="my-2 system-md-semibold">
|
||||
{t('login.interfaceLanguage')}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<SimpleSelect
|
||||
defaultValue={LanguagesSupported[0]}
|
||||
items={languages.filter(item => item.supported)}
|
||||
onSelect={(item) => {
|
||||
setLanguage(item.value as string)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* timezone */}
|
||||
<div className='mb-5'>
|
||||
<label htmlFor="timezone" className="system-md-semibold">
|
||||
{t('login.timezone')}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<SimpleSelect
|
||||
defaultValue={timezone}
|
||||
items={timezones}
|
||||
onSelect={(item) => {
|
||||
setTimezone(item.value as string)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
onClick={handleActivate}
|
||||
>
|
||||
{`${t('login.join')} ${checkRes?.data?.workspace_name}`}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="block w-full mt-2 system-xs-regular">
|
||||
{t('login.license.tip')}
|
||||
|
||||
<Link
|
||||
className='system-xs-medium text-text-accent-secondary'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href={`https://docs.dify.ai/${language !== LanguagesSupported[1] ? 'user-agreement' : `v/${locale.toLowerCase()}/policies`}/open-source`}
|
||||
>{t('login.license.link')}</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
54
web/app/signin/layout.tsx
Normal file
54
web/app/signin/layout.tsx
Normal file
|
@ -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,299 +1,170 @@
|
|||
'use client'
|
||||
import React, { useEffect, useReducer, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import useSWR from 'swr'
|
||||
import Link from 'next/link'
|
||||
import Toast from '../components/base/toast'
|
||||
import style from './page.module.css'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { IS_CE_EDITION, SUPPORT_MAIL_LOGIN, apiPrefix, emailRegex } from '@/config'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { login, oauth } from '@/service/common'
|
||||
import { getPurifyHref } from '@/utils'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { RiDoorLockLine } from '@remixicon/react'
|
||||
import Loading from '../components/base/loading'
|
||||
import MailAndCodeAuth from './components/mail-and-code-auth'
|
||||
import MailAndPasswordAuth from './components/mail-and-password-auth'
|
||||
import SocialAuth from './components/social-auth'
|
||||
import SSOAuth from './components/sso-auth'
|
||||
import cn from '@/utils/classnames'
|
||||
import { getSystemFeatures, invitationCheck } from '@/service/common'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import useRefreshToken from '@/hooks/use-refresh-token'
|
||||
|
||||
type IState = {
|
||||
formValid: boolean
|
||||
github: boolean
|
||||
google: boolean
|
||||
}
|
||||
|
||||
type IAction = {
|
||||
type: 'login' | 'login_failed' | 'github_login' | 'github_login_failed' | 'google_login' | 'google_login_failed'
|
||||
}
|
||||
|
||||
function reducer(state: IState, action: IAction) {
|
||||
switch (action.type) {
|
||||
case 'login':
|
||||
return {
|
||||
...state,
|
||||
formValid: true,
|
||||
}
|
||||
case 'login_failed':
|
||||
return {
|
||||
...state,
|
||||
formValid: true,
|
||||
}
|
||||
case 'github_login':
|
||||
return {
|
||||
...state,
|
||||
github: true,
|
||||
}
|
||||
case 'github_login_failed':
|
||||
return {
|
||||
...state,
|
||||
github: false,
|
||||
}
|
||||
case 'google_login':
|
||||
return {
|
||||
...state,
|
||||
google: true,
|
||||
}
|
||||
case 'google_login_failed':
|
||||
return {
|
||||
...state,
|
||||
google: false,
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown action.')
|
||||
}
|
||||
}
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
const NormalForm = () => {
|
||||
const { t } = useTranslation()
|
||||
const { getNewAccessToken } = useRefreshToken()
|
||||
const useEmailLogin = IS_CE_EDITION || SUPPORT_MAIL_LOGIN
|
||||
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const consoleToken = decodeURIComponent(searchParams.get('access_token') || '')
|
||||
const refreshToken = decodeURIComponent(searchParams.get('refresh_token') || '')
|
||||
const message = decodeURIComponent(searchParams.get('message') || '')
|
||||
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [systemFeatures, setSystemFeatures] = useState(defaultSystemFeatures)
|
||||
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
|
||||
const [showORLine, setShowORLine] = useState(false)
|
||||
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
|
||||
const [workspaceName, setWorkSpaceName] = useState('')
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
formValid: false,
|
||||
github: false,
|
||||
google: false,
|
||||
})
|
||||
const isInviteLink = Boolean(invite_token && invite_token !== 'null')
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const handleEmailPasswordLogin = async () => {
|
||||
if (!emailRegex.test(email)) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('login.error.emailInValid'),
|
||||
})
|
||||
return
|
||||
}
|
||||
const init = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const res = await login({
|
||||
url: '/login',
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
remember_me: true,
|
||||
},
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
localStorage.setItem('console_token', res.data.access_token)
|
||||
localStorage.setItem('refresh_token', res.data.refresh_token)
|
||||
if (consoleToken && refreshToken) {
|
||||
localStorage.setItem('console_token', consoleToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
getNewAccessToken()
|
||||
router.replace('/apps')
|
||||
return
|
||||
}
|
||||
else {
|
||||
|
||||
if (message) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: res.data,
|
||||
message,
|
||||
})
|
||||
}
|
||||
const features = await getSystemFeatures()
|
||||
const allFeatures = { ...defaultSystemFeatures, ...features }
|
||||
setSystemFeatures(allFeatures)
|
||||
setAllMethodsAreDisabled(!allFeatures.enable_social_oauth_login && !allFeatures.enable_email_code_login && !allFeatures.enable_email_password_login && !allFeatures.sso_enforced_for_signin)
|
||||
setShowORLine((allFeatures.enable_social_oauth_login || allFeatures.sso_enforced_for_signin) && (allFeatures.enable_email_code_login || allFeatures.enable_email_password_login))
|
||||
updateAuthType(allFeatures.enable_email_password_login ? 'password' : 'code')
|
||||
if (isInviteLink) {
|
||||
const checkRes = await invitationCheck({
|
||||
url: '/activate/check',
|
||||
params: {
|
||||
token: invite_token,
|
||||
},
|
||||
})
|
||||
setWorkSpaceName(checkRes?.data?.workspace_name || '')
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
setAllMethodsAreDisabled(true)
|
||||
setSystemFeatures(defaultSystemFeatures)
|
||||
}
|
||||
finally { setIsLoading(false) }
|
||||
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, getNewAccessToken])
|
||||
useEffect(() => {
|
||||
init()
|
||||
}, [init])
|
||||
if (isLoading || consoleToken) {
|
||||
return <div className={
|
||||
cn(
|
||||
'flex flex-col items-center w-full grow justify-center',
|
||||
'px-6',
|
||||
'md:px-[108px]',
|
||||
)
|
||||
}>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
}
|
||||
|
||||
const { data: github, error: github_error } = useSWR(state.github
|
||||
? ({
|
||||
url: '/oauth/login/github',
|
||||
// params: {
|
||||
// provider: 'github',
|
||||
// },
|
||||
})
|
||||
: null, oauth)
|
||||
|
||||
const { data: google, error: google_error } = useSWR(state.google
|
||||
? ({
|
||||
url: '/oauth/login/google',
|
||||
// params: {
|
||||
// provider: 'google',
|
||||
// },
|
||||
})
|
||||
: null, oauth)
|
||||
|
||||
useEffect(() => {
|
||||
if (github_error !== undefined)
|
||||
dispatch({ type: 'github_login_failed' })
|
||||
if (github)
|
||||
window.location.href = github.redirect_url
|
||||
}, [github, github_error])
|
||||
|
||||
useEffect(() => {
|
||||
if (google_error !== undefined)
|
||||
dispatch({ type: 'google_login_failed' })
|
||||
if (google)
|
||||
window.location.href = google.redirect_url
|
||||
}, [google, google_error])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full mx-auto">
|
||||
<h2 className="text-[32px] font-bold text-gray-900">{t('login.pageTitle')}</h2>
|
||||
<p className='mt-1 text-sm text-gray-600'>{t('login.welcome')}</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full mx-auto mt-8">
|
||||
<div className="bg-white ">
|
||||
{!useEmailLogin && (
|
||||
<div className="flex flex-col gap-3 mt-6">
|
||||
<div className='w-full'>
|
||||
<a href={getPurifyHref(`${apiPrefix}/oauth/login/github`)}>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
className='w-full hover:!bg-gray-50'
|
||||
>
|
||||
<>
|
||||
<span className={
|
||||
classNames(
|
||||
style.githubIcon,
|
||||
'w-5 h-5 mr-2',
|
||||
)
|
||||
} />
|
||||
<span className="truncate text-gray-800">{t('login.withGitHub')}</span>
|
||||
</>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<a href={getPurifyHref(`${apiPrefix}/oauth/login/google`)}>
|
||||
<Button
|
||||
disabled={isLoading}
|
||||
className='w-full hover:!bg-gray-50'
|
||||
>
|
||||
<>
|
||||
<span className={
|
||||
classNames(
|
||||
style.googleIcon,
|
||||
'w-5 h-5 mr-2',
|
||||
)
|
||||
} />
|
||||
<span className="truncate text-gray-800">{t('login.withGoogle')}</span>
|
||||
</>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
{isInviteLink
|
||||
? <div className="w-full mx-auto">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.join')}{workspaceName}</h2>
|
||||
<p className='mt-2 body-md-regular text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p>
|
||||
</div>
|
||||
: <div className="w-full mx-auto">
|
||||
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2>
|
||||
<p className='mt-2 body-md-regular text-text-tertiary'>{t('login.welcome')}</p>
|
||||
</div>}
|
||||
<div className="bg-white">
|
||||
<div className="flex flex-col gap-3 mt-6">
|
||||
{systemFeatures.enable_social_oauth_login && <SocialAuth />}
|
||||
{systemFeatures.sso_enforced_for_signin && <div className='w-full'>
|
||||
<SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} />
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{showORLine && <div className="relative mt-6">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className='bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent h-px w-full'></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative flex justify-center">
|
||||
<span className="px-2 text-text-tertiary system-xs-medium-uppercase bg-white">{t('login.or')}</span>
|
||||
</div>
|
||||
</div>}
|
||||
{
|
||||
useEmailLogin && <>
|
||||
{/* <div className="relative mt-6">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 text-gray-300 bg-white">OR</span>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<form onSubmit={() => { }}>
|
||||
<div className='mb-5'>
|
||||
<label htmlFor="email" className="my-2 block text-sm font-medium text-gray-900">
|
||||
{t('login.email')}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder={t('login.emailPlaceholder') || ''}
|
||||
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'}
|
||||
tabIndex={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-4'>
|
||||
<label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
|
||||
<span>{t('login.password')}</span>
|
||||
<Link href='/forgot-password' className='text-primary-600'>
|
||||
{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') || ''}
|
||||
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'}
|
||||
tabIndex={2}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="text-gray-400 hover:text-gray-500 focus:outline-none focus:text-gray-500"
|
||||
>
|
||||
{showPassword ? '👀' : '😝'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-2'>
|
||||
<Button
|
||||
tabIndex={0}
|
||||
variant='primary'
|
||||
onClick={handleEmailPasswordLogin}
|
||||
disabled={isLoading}
|
||||
className="w-full"
|
||||
>{t('login.signBtn')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
(systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login) && <>
|
||||
{systemFeatures.enable_email_code_login && authType === 'code' && <>
|
||||
<MailAndCodeAuth isInvite={isInviteLink} />
|
||||
{systemFeatures.enable_email_password_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('password') }}>
|
||||
<span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.usePassword')}</span>
|
||||
</div>}
|
||||
</>}
|
||||
{systemFeatures.enable_email_password_login && authType === 'password' && <>
|
||||
<MailAndPasswordAuth isInvite={isInviteLink} allowRegistration={systemFeatures.is_allow_register} />
|
||||
{systemFeatures.enable_email_code_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('code') }}>
|
||||
<span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.useVerificationCode')}</span>
|
||||
</div>}
|
||||
</>}
|
||||
</>
|
||||
}
|
||||
{/* agree to our Terms and Privacy Policy. */}
|
||||
<div className="w-hull text-center block mt-2 text-xs text-gray-600">
|
||||
{allMethodsAreDisabled && <>
|
||||
<div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2">
|
||||
<div className='flex items-center justify-center w-10 h-10 rounded-xl bg-components-card-bg shadow shadows-shadow-lg mb-2'>
|
||||
<RiDoorLockLine className='w-5 h-5' />
|
||||
</div>
|
||||
<p className='system-sm-medium text-text-primary'>{t('login.noLoginMethod')}</p>
|
||||
<p className='system-xs-regular text-text-tertiary mt-1'>{t('login.noLoginMethodTip')}</p>
|
||||
</div>
|
||||
<div className="relative my-2 py-2">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className='bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent h-px w-full'></div>
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
<div className="w-full block mt-2 system-xs-regular text-text-tertiary">
|
||||
{t('login.tosDesc')}
|
||||
|
||||
<Link
|
||||
className='text-primary-600'
|
||||
className='system-xs-medium text-text-secondary hover:underline'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://dify.ai/terms'
|
||||
>{t('login.tos')}</Link>
|
||||
&
|
||||
<Link
|
||||
className='text-primary-600'
|
||||
className='system-xs-medium text-text-secondary hover:underline'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://dify.ai/privacy'
|
||||
>{t('login.pp')}</Link>
|
||||
</div>
|
||||
|
||||
{IS_CE_EDITION && <div className="w-hull text-center block mt-2 text-xs text-gray-600">
|
||||
{IS_CE_EDITION && <div className="w-hull block mt-2 system-xs-regular text-text-tertiary">
|
||||
{t('login.goToInit')}
|
||||
|
||||
<Link
|
||||
className='text-primary-600'
|
||||
className='system-xs-medium text-text-secondary hover:underline'
|
||||
href='/install'
|
||||
>{t('login.setAdminAccount')}</Link>
|
||||
</div>}
|
||||
|
|
|
@ -3,8 +3,8 @@ import React, { useEffect, useReducer } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import useSWR from 'swr'
|
||||
import { useRouter } from 'next/navigation'
|
||||
// import { useContext } from 'use-context-selector'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import Input from '../components/base/input'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
|
@ -12,7 +12,6 @@ import { timezones } from '@/utils/timezone'
|
|||
import { LanguagesSupported, languages } from '@/i18n/language'
|
||||
import { oneMoreStep } from '@/service/common'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
// import I18n from '@/context/i18n'
|
||||
|
||||
type IState = {
|
||||
formState: 'processing' | 'error' | 'success' | 'initial'
|
||||
|
@ -46,11 +45,11 @@ const reducer = (state: IState, action: any) => {
|
|||
const OneMoreStep = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
// const { locale } = useContext(I18n)
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
formState: 'initial',
|
||||
invitation_code: '',
|
||||
invitation_code: searchParams.get('invitation_code') || '',
|
||||
interface_language: 'en-US',
|
||||
timezone: 'Asia/Shanghai',
|
||||
})
|
||||
|
@ -77,36 +76,35 @@ const OneMoreStep = () => {
|
|||
return (
|
||||
<>
|
||||
<div className="w-full mx-auto">
|
||||
<h2 className="text-[32px] font-bold text-gray-900">{t('login.oneMoreStep')}</h2>
|
||||
<p className='mt-1 text-sm text-gray-600 '>{t('login.createSample')}</p>
|
||||
<h2 className="title-4xl-semi-bold text-text-secondary">{t('login.oneMoreStep')}</h2>
|
||||
<p className='mt-1 body-md-regular text-text-tertiary'>{t('login.createSample')}</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full mx-auto mt-6">
|
||||
<div className="bg-white">
|
||||
<div className="mb-5">
|
||||
<label className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
|
||||
<label className="my-2 flex items-center justify-between system-md-semibold text-text-secondary">
|
||||
{t('login.invitationCode')}
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[256px] text-xs font-medium'>
|
||||
<div className='font-medium'>{t('login.sendUsMail')}</div>
|
||||
<div className='text-xs font-medium cursor-pointer text-primary-600'>
|
||||
<div className='text-xs font-medium cursor-pointer text-text-accent-secondary'>
|
||||
<a href="mailto:request-invitation@langgenius.ai">request-invitation@langgenius.ai</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
needsDelay
|
||||
>
|
||||
<span className='cursor-pointer text-primary-600'>{t('login.dontHave')}</span>
|
||||
<span className='cursor-pointer text-text-accent-secondary'>{t('login.dontHave')}</span>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
<Input
|
||||
id="invitation_code"
|
||||
value={state.invitation_code}
|
||||
type="text"
|
||||
placeholder={t('login.invitationCodePlaceholder') || ''}
|
||||
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'}
|
||||
onChange={(e) => {
|
||||
dispatch({ type: 'invitation_code', value: e.target.value.trim() })
|
||||
}}
|
||||
|
@ -114,10 +112,10 @@ const OneMoreStep = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className='mb-5'>
|
||||
<label htmlFor="name" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
|
||||
<label htmlFor="name" className="my-2 system-md-semibold text-text-secondary">
|
||||
{t('login.interfaceLanguage')}
|
||||
</label>
|
||||
<div className="relative mt-1 rounded-md shadow-sm">
|
||||
<div className="mt-1">
|
||||
<SimpleSelect
|
||||
defaultValue={LanguagesSupported[0]}
|
||||
items={languages.filter(item => item.supported)}
|
||||
|
@ -128,10 +126,10 @@ const OneMoreStep = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className='mb-4'>
|
||||
<label htmlFor="timezone" className="block text-sm font-medium text-gray-700">
|
||||
<label htmlFor="timezone" className="system-md-semibold text-text-tertiary">
|
||||
{t('login.timezone')}
|
||||
</label>
|
||||
<div className="relative mt-1 rounded-md shadow-sm">
|
||||
<div className="mt-1">
|
||||
<SimpleSelect
|
||||
defaultValue={state.timezone}
|
||||
items={timezones}
|
||||
|
@ -153,11 +151,11 @@ const OneMoreStep = () => {
|
|||
{t('login.go')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="block w-hull mt-2 text-xs text-gray-600">
|
||||
<div className="block w-full mt-2 system-xs-regular text-text-tertiary">
|
||||
{t('login.license.tip')}
|
||||
|
||||
<Link
|
||||
className='text-primary-600'
|
||||
className='system-xs-medium text-text-accent-secondary'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href={'https://docs.dify.ai/user-agreement/open-source'}
|
||||
>{t('login.license.link')}</Link>
|
||||
|
|
|
@ -1,94 +1,15 @@
|
|||
'use client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Script from 'next/script'
|
||||
import Loading from '../components/base/loading'
|
||||
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'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import OneMoreStep from './oneMoreStep'
|
||||
import NormalForm from './normalForm'
|
||||
|
||||
const SignIn = () => {
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [systemFeatures, setSystemFeatures] = useState<SystemFeatures>(defaultSystemFeatures)
|
||||
const searchParams = useSearchParams()
|
||||
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
|
||||
|
|
|
@ -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,6 +1,6 @@
|
|||
const translation = {
|
||||
pageTitle: 'Hey, let\'s get started!👋',
|
||||
welcome: 'Welcome to Dify, please log in to continue.',
|
||||
pageTitle: 'Hey, let\'s get started!',
|
||||
welcome: '👋 Welcome to Dify, please log in to continue.',
|
||||
email: 'Email address',
|
||||
emailPlaceholder: 'Your email',
|
||||
password: 'Password',
|
||||
|
@ -9,7 +9,11 @@ const translation = {
|
|||
namePlaceholder: 'Your username',
|
||||
forget: 'Forgot your password?',
|
||||
signBtn: 'Sign in',
|
||||
sso: 'Continue with SSO',
|
||||
continueWithCode: 'Continue With Code',
|
||||
sendVerificationCode: 'Send Verification Code',
|
||||
usePassword: 'Use Password',
|
||||
useVerificationCode: 'Use Verification Code',
|
||||
or: 'OR',
|
||||
installBtn: 'Set up',
|
||||
setAdminAccount: 'Setting up an admin account',
|
||||
setAdminAccountDesc: 'Maximum privileges for admin account, which can be used to create applications and manage LLM providers, etc.',
|
||||
|
@ -26,6 +30,7 @@ const translation = {
|
|||
reset: 'Please run following command to reset your password',
|
||||
withGitHub: 'Continue with GitHub',
|
||||
withGoogle: 'Continue with Google',
|
||||
withSSO: 'Continue with SSO',
|
||||
rightTitle: 'Unlock the full potential of LLM',
|
||||
rightDesc: 'Effortlessly build visually captivating, operable, and improvable AI applications.',
|
||||
tos: 'Terms of Service',
|
||||
|
@ -42,8 +47,9 @@ const translation = {
|
|||
forgotPasswordDesc: 'Please enter your email address to reset your password. We will send you an email with instructions on how to reset your password.',
|
||||
checkEmailForResetLink: 'Please check your email for a link to reset your password. If it doesn\'t appear within a few minutes, make sure to check your spam folder.',
|
||||
passwordChanged: 'Sign in now',
|
||||
changePassword: 'Change Password',
|
||||
changePassword: 'Set a password',
|
||||
changePasswordTip: 'Please enter a new password for your account',
|
||||
changePasswordBtn: 'Set a password',
|
||||
invalidToken: 'Invalid or expired token',
|
||||
confirmPassword: 'Confirm Password',
|
||||
confirmPasswordPlaceholder: 'Confirm your new password',
|
||||
|
@ -55,14 +61,15 @@ const translation = {
|
|||
passwordEmpty: 'Password is required',
|
||||
passwordLengthInValid: 'Password must be at least 8 characters',
|
||||
passwordInvalid: 'Password must contain letters and numbers, and the length must be greater than 8',
|
||||
registrationNotAllowed: 'Account not found. Please contact the system admin to register.',
|
||||
},
|
||||
license: {
|
||||
tip: 'Before starting Dify Community Edition, read the GitHub',
|
||||
link: 'Open-source License',
|
||||
},
|
||||
join: 'Join',
|
||||
joinTipStart: 'Invite you join',
|
||||
joinTipEnd: 'team on Dify',
|
||||
join: 'Join ',
|
||||
joinTipStart: 'Invite you join ',
|
||||
joinTipEnd: ' team on Dify',
|
||||
invalid: 'The link has expired',
|
||||
explore: 'Explore Dify',
|
||||
activatedTipStart: 'You have joined the',
|
||||
|
@ -70,6 +77,27 @@ const translation = {
|
|||
activated: 'Sign in now',
|
||||
adminInitPassword: 'Admin initialization password',
|
||||
validate: 'Validate',
|
||||
checkCode: {
|
||||
checkYourEmail: 'Check your email',
|
||||
tips: 'We send a verification code to <strong>{{email}}</strong>',
|
||||
validTime: 'Bear in mind that the code is valid for 5 minutes',
|
||||
verificationCode: 'Verification code',
|
||||
verificationCodePlaceholder: 'Enter 6-digit code',
|
||||
verify: 'Verify',
|
||||
didNotReceiveCode: 'Didn\'t receive the code? ',
|
||||
resend: 'Resend',
|
||||
useAnotherMethod: 'Use another method',
|
||||
emptyCode: 'Code is required',
|
||||
invalidCode: 'Invalid code',
|
||||
},
|
||||
resetPassword: 'Reset Password',
|
||||
resetPasswordDesc: 'Type the email you used to sign up on Dify and we will send you a password reset email.',
|
||||
backToLogin: 'Back to login',
|
||||
setYourAccount: 'Set Your Account',
|
||||
enterYourName: 'Please enter your username',
|
||||
back: 'Back',
|
||||
noLoginMethod: 'Authentication method not configured',
|
||||
noLoginMethodTip: 'Please contact the system admin to add an authentication method.',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const translation = {
|
||||
pageTitle: '嗨,近来可好 👋',
|
||||
welcome: '欢迎来到 Dify, 登录以继续',
|
||||
pageTitle: '嗨,近来可好',
|
||||
welcome: '👋 欢迎来到 Dify, 登录以继续',
|
||||
email: '邮箱',
|
||||
emailPlaceholder: '输入邮箱地址',
|
||||
password: '密码',
|
||||
|
@ -9,6 +9,11 @@ const translation = {
|
|||
namePlaceholder: '输入用户名',
|
||||
forget: '忘记密码?',
|
||||
signBtn: '登录',
|
||||
continueWithCode: '发送验证码',
|
||||
sendVerificationCode: '发送验证码',
|
||||
usePassword: '使用密码登录',
|
||||
useVerificationCode: '使用验证码登录',
|
||||
or: '或',
|
||||
installBtn: '设置',
|
||||
setAdminAccount: '设置管理员账户',
|
||||
setAdminAccountDesc: '管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。',
|
||||
|
@ -25,11 +30,12 @@ const translation = {
|
|||
reset: '请运行以下命令重置密码',
|
||||
withGitHub: '使用 GitHub 登录',
|
||||
withGoogle: '使用 Google 登录',
|
||||
withSSO: '使用 SSO 登录',
|
||||
rightTitle: '释放大型语言模型的全部潜能',
|
||||
rightDesc: '简单构建可视化、可运营、可改进的 AI 应用',
|
||||
tos: '使用协议',
|
||||
pp: '隐私政策',
|
||||
tosDesc: '使用即代表你并同意我们的',
|
||||
tosDesc: '使用即代表您同意我们的',
|
||||
goToInit: '如果您还没有初始化账户,请前往初始化页面',
|
||||
dontHave: '还没有邀请码?',
|
||||
invalidInvitationCode: '无效的邀请码',
|
||||
|
@ -41,8 +47,9 @@ const translation = {
|
|||
forgotPasswordDesc: '请输入您的电子邮件地址以重置密码。我们将向您发送一封电子邮件,包含如何重置密码的说明。',
|
||||
checkEmailForResetLink: '请检查您的电子邮件以获取重置密码的链接。如果几分钟内没有收到,请检查您的垃圾邮件文件夹。',
|
||||
passwordChanged: '立即登录',
|
||||
changePassword: '更改密码',
|
||||
changePassword: '设置密码',
|
||||
changePasswordTip: '请输入您的新密码',
|
||||
changePasswordBtn: '设置密码',
|
||||
invalidToken: '无效或已过期的令牌',
|
||||
confirmPassword: '确认密码',
|
||||
confirmPasswordPlaceholder: '确认您的新密码',
|
||||
|
@ -54,14 +61,15 @@ const translation = {
|
|||
passwordEmpty: '密码不能为空',
|
||||
passwordInvalid: '密码必须包含字母和数字,且长度不小于8位',
|
||||
passwordLengthInValid: '密码必须至少为 8 个字符',
|
||||
registrationNotAllowed: '账户不存在,请联系系统管理员注册账户',
|
||||
},
|
||||
license: {
|
||||
tip: '启动 Dify 社区版之前, 请阅读 GitHub 上的',
|
||||
link: '开源协议',
|
||||
},
|
||||
join: '加入',
|
||||
joinTipStart: '邀请你加入',
|
||||
joinTipEnd: '团队',
|
||||
join: '加入 ',
|
||||
joinTipStart: '邀请你加入 ',
|
||||
joinTipEnd: ' 团队',
|
||||
invalid: '链接已失效',
|
||||
explore: '探索 Dify',
|
||||
activatedTipStart: '您已加入',
|
||||
|
@ -70,6 +78,27 @@ const translation = {
|
|||
adminInitPassword: '管理员初始化密码',
|
||||
validate: '验证',
|
||||
sso: '使用 SSO 继续',
|
||||
checkCode: {
|
||||
checkYourEmail: '验证您的电子邮件',
|
||||
tips: '验证码已经发送到您的邮箱 <strong>{{email}}</strong>',
|
||||
validTime: '请注意验证码 5 分钟内有效',
|
||||
verificationCode: '验证码',
|
||||
verificationCodePlaceholder: '输入 6 位验证码',
|
||||
verify: '验证',
|
||||
didNotReceiveCode: '没有收到验证码?',
|
||||
resend: '重新发送',
|
||||
useAnotherMethod: '使用其他方式登录',
|
||||
emptyCode: '验证码不能为空',
|
||||
invalidCode: '验证码无效',
|
||||
},
|
||||
resetPassword: '重置密码',
|
||||
resetPasswordDesc: '请输入您的电子邮件地址以重置密码。我们将向您发送一封电子邮件。',
|
||||
backToLogin: '返回登录',
|
||||
setYourAccount: '设置您的账户',
|
||||
enterYourName: '请输入用户名',
|
||||
back: '返回',
|
||||
noLoginMethod: '未配置身份认证方式',
|
||||
noLoginMethodTip: '请联系系统管理员添加身份认证方式',
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const translation = {
|
||||
pageTitle: '嗨,近來可好 👋',
|
||||
welcome: '歡迎來到 Dify, 登入以繼續',
|
||||
pageTitle: '嗨,近來可好',
|
||||
welcome: '👋 歡迎來到 Dify, 登入以繼續',
|
||||
email: '郵箱',
|
||||
emailPlaceholder: '輸入郵箱地址',
|
||||
password: '密碼',
|
||||
|
|
|
@ -45,6 +45,8 @@ type LoginSuccess = {
|
|||
type LoginFail = {
|
||||
result: 'fail'
|
||||
data: string
|
||||
code: string
|
||||
message: string
|
||||
}
|
||||
type LoginResponse = LoginSuccess | LoginFail
|
||||
export const login: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
|
||||
|
@ -169,12 +171,12 @@ export const updatePluginProviderAIKey: Fetcher<UpdateOpenAIKeyResponse, { url:
|
|||
return post<UpdateOpenAIKeyResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const invitationCheck: Fetcher<CommonResponse & { is_valid: boolean; workspace_name: string }, { url: string; params: { workspace_id: string; email: string; token: string } }> = ({ url, params }) => {
|
||||
return get<CommonResponse & { is_valid: boolean; workspace_name: string }>(url, { params })
|
||||
export const invitationCheck: Fetcher<CommonResponse & { is_valid: boolean; data: { workspace_name: string; email: string; workspace_id: string } }, { url: string; params: { workspace_id?: string; email?: string; token: string } }> = ({ url, params }) => {
|
||||
return get<CommonResponse & { is_valid: boolean; data: { workspace_name: string; email: string; workspace_id: string } }>(url, { params })
|
||||
}
|
||||
|
||||
export const activateMember: Fetcher<CommonResponse, { url: string; body: any }> = ({ url, body }) => {
|
||||
return post<CommonResponse>(url, { body })
|
||||
export const activateMember: Fetcher<LoginResponse, { url: string; body: any }> = ({ url, body }) => {
|
||||
return post<LoginResponse>(url, { body })
|
||||
}
|
||||
|
||||
export const fetchModelProviders: Fetcher<{ data: ModelProvider[] }, string> = (url) => {
|
||||
|
@ -312,8 +314,8 @@ export const enableModel = (url: string, body: { model: string; model_type: Mode
|
|||
export const disableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) =>
|
||||
patch<CommonResponse>(url, { body })
|
||||
|
||||
export const sendForgotPasswordEmail: Fetcher<CommonResponse, { url: string; body: { email: string } }> = ({ url, body }) =>
|
||||
post<CommonResponse>(url, { body })
|
||||
export const sendForgotPasswordEmail: Fetcher<CommonResponse & { data: string }, { url: string; body: { email: string } }> = ({ url, body }) =>
|
||||
post<CommonResponse & { data: string }>(url, { body })
|
||||
|
||||
export const verifyForgotPasswordToken: Fetcher<CommonResponse & { is_valid: boolean; email: string }, { url: string; body: { token: string } }> = ({ url, body }) => {
|
||||
return post(url, { body }) as Promise<CommonResponse & { is_valid: boolean; email: string }>
|
||||
|
@ -321,3 +323,15 @@ 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 sendEMailLoginCode = (email: string, language = 'en-US') =>
|
||||
post<CommonResponse & { data: string }>('/email-code-login', { body: { email, language } })
|
||||
|
||||
export const emailLoginWithCode = (data: { email: string;code: string;token: string }) =>
|
||||
post<LoginResponse>('/email-code-login/validity', { body: data })
|
||||
|
||||
export const sendResetPasswordCode = (email: string, language = 'en-US') =>
|
||||
post<CommonResponse & { data: string;message?: string ;code?: string }>('/forgot-password', { body: { email, language } })
|
||||
|
||||
export const verifyResetPasswordCode = (body: { email: string;code: string;token: string }) =>
|
||||
post<CommonResponse & { is_valid: boolean }>('/forgot-password/validity', { body })
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { get } from './base'
|
||||
|
||||
export const getUserSAMLSSOUrl = () => {
|
||||
return get<{ url: string }>('/enterprise/sso/saml/login')
|
||||
export const getUserSAMLSSOUrl = (invite_token?: string) => {
|
||||
const url = invite_token ? `/enterprise/sso/saml/login?invite_token=${invite_token}` : '/enterprise/sso/saml/login'
|
||||
return get<{ url: string }>(url)
|
||||
}
|
||||
|
||||
export const getUserOIDCSSOUrl = () => {
|
||||
return get<{ url: string; state: string }>('/enterprise/sso/oidc/login')
|
||||
export const getUserOIDCSSOUrl = (invite_token?: string) => {
|
||||
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 = () => {
|
||||
return get<{ url: string; state: string }>('/enterprise/sso/oauth2/login')
|
||||
export const getUserOAuth2SSOUrl = (invite_token?: string) => {
|
||||
const url = invite_token ? `/enterprise/sso/oauth2/login?invite_token=${invite_token}` : '/enterprise/sso/oauth2/login'
|
||||
return get<{ url: string; state: string }>(url)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
export enum SSOProtocol {
|
||||
SAML = 'saml',
|
||||
OIDC = 'oidc',
|
||||
OAuth2 = 'oauth2',
|
||||
}
|
||||
|
||||
export type SystemFeatures = {
|
||||
sso_enforced_for_signin: boolean
|
||||
sso_enforced_for_signin_protocol: string
|
||||
sso_enforced_for_signin_protocol: SSOProtocol | ''
|
||||
sso_enforced_for_web: boolean
|
||||
sso_enforced_for_web_protocol: string
|
||||
sso_enforced_for_web_protocol: SSOProtocol | ''
|
||||
enable_web_sso_switch_component: boolean
|
||||
enable_email_code_login: boolean
|
||||
enable_email_password_login: boolean
|
||||
enable_social_oauth_login: boolean
|
||||
is_allow_create_workspace: boolean
|
||||
is_allow_register: boolean
|
||||
}
|
||||
|
||||
export const defaultSystemFeatures: SystemFeatures = {
|
||||
|
@ -12,4 +23,9 @@ export const defaultSystemFeatures: SystemFeatures = {
|
|||
sso_enforced_for_web: false,
|
||||
sso_enforced_for_web_protocol: '',
|
||||
enable_web_sso_switch_component: false,
|
||||
enable_email_code_login: false,
|
||||
enable_email_password_login: false,
|
||||
enable_social_oauth_login: false,
|
||||
is_allow_create_workspace: false,
|
||||
is_allow_register: false,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user