| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import Avatar from '@/app/components/base/avatar' | import Avatar from '@/app/components/base/avatar' | ||||
| import { IS_CE_EDITION } from '@/config' | import { IS_CE_EDITION } from '@/config' | ||||
| import Input from '@/app/components/base/input' | |||||
| const titleClassName = ` | const titleClassName = ` | ||||
| text-sm font-medium text-gray-900 | text-sm font-medium text-gray-900 | ||||
| export default function AccountPage() { | export default function AccountPage() { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { systemFeatures } = useAppContext() | |||||
| const { mutateUserProfile, userProfile, apps } = useAppContext() | const { mutateUserProfile, userProfile, apps } = useAppContext() | ||||
| const { notify } = useContext(ToastContext) | const { notify } = useContext(ToastContext) | ||||
| const [editNameModalVisible, setEditNameModalVisible] = useState(false) | const [editNameModalVisible, setEditNameModalVisible] = useState(false) | ||||
| const [password, setPassword] = useState('') | const [password, setPassword] = useState('') | ||||
| const [confirmPassword, setConfirmPassword] = useState('') | const [confirmPassword, setConfirmPassword] = useState('') | ||||
| const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) | const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) | ||||
| const [showCurrentPassword, setShowCurrentPassword] = useState(false) | |||||
| const [showPassword, setShowPassword] = useState(false) | |||||
| const [showConfirmPassword, setShowConfirmPassword] = useState(false) | |||||
| const handleEditName = () => { | const handleEditName = () => { | ||||
| setEditNameModalVisible(true) | setEditNameModalVisible(true) | ||||
| </div> | </div> | ||||
| </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> | ||||
| <div className='mb-1 text-sm font-medium text-gray-900'>{t('common.account.password')}</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> | <div className='mb-2 text-xs text-gray-500'>{t('common.account.passwordTip')}</div> | ||||
| > | > | ||||
| <div className='mb-6 text-lg font-medium text-gray-900'>{t('common.account.editName')}</div> | <div className='mb-6 text-lg font-medium text-gray-900'>{t('common.account.editName')}</div> | ||||
| <div className={titleClassName}>{t('common.account.name')}</div> | <div className={titleClassName}>{t('common.account.name')}</div> | ||||
| <input | |||||
| className={inputClassName} | |||||
| <Input className='mt-2' | |||||
| value={editName} | value={editName} | ||||
| onChange={e => setEditName(e.target.value)} | onChange={e => setEditName(e.target.value)} | ||||
| /> | /> | ||||
| {userProfile.is_password_set && ( | {userProfile.is_password_set && ( | ||||
| <> | <> | ||||
| <div className={titleClassName}>{t('common.account.currentPassword')}</div> | <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'> | <div className='mt-8 text-sm font-medium text-gray-900'> | ||||
| {userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')} | {userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')} | ||||
| </div> | </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> | <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'> | <div className='flex justify-end mt-10'> | ||||
| <Button className='mr-2' onClick={() => { | <Button className='mr-2' onClick={() => { | ||||
| setEditPasswordModalVisible(false) | setEditPasswordModalVisible(false) |
| > | > | ||||
| <Menu.Items | <Menu.Items | ||||
| className=" | 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 | divide-y divide-gray-100 origin-top-right rounded-lg bg-white | ||||
| shadow-lg | shadow-lg | ||||
| " | " |
| 'use client' | 'use client' | ||||
| import { useCallback, useState } from 'react' | |||||
| import { useContext } from 'use-context-selector' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import useSWR from 'swr' | 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 cn from '@/utils/classnames' | ||||
| import Button from '@/app/components/base/button' | 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 Loading from '@/app/components/base/loading' | ||||
| import I18n from '@/context/i18n' | |||||
| const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ | |||||
| const ActivateForm = () => { | const ActivateForm = () => { | ||||
| const router = useRouter() | |||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { locale, setLocaleOnClient } = useContext(I18n) | |||||
| const searchParams = useSearchParams() | const searchParams = useSearchParams() | ||||
| const workspaceID = searchParams.get('workspace_id') | const workspaceID = searchParams.get('workspace_id') | ||||
| const email = searchParams.get('email') | const email = searchParams.get('email') | ||||
| token, | token, | ||||
| }, | }, | ||||
| } | } | ||||
| const { data: checkRes, mutate: recheck } = useSWR(checkParams, invitationCheck, { | |||||
| const { data: checkRes } = useSWR(checkParams, invitationCheck, { | |||||
| revalidateOnFocus: false, | 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 ( | return ( | ||||
| <div className={ | <div className={ | ||||
| cn( | cn( | ||||
| </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 ${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> | </div> | ||||
| ) | ) | ||||
| } | } |
| <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> |
| import { useState } from 'react' | import { useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useContext } from 'use-context-selector' | |||||
| import { useContext, useContextSelector } from 'use-context-selector' | |||||
| import Collapse from '../collapse' | import Collapse from '../collapse' | ||||
| import type { IItem } from '../collapse' | import type { IItem } from '../collapse' | ||||
| import s from './index.module.css' | import s from './index.module.css' | ||||
| import Confirm from '@/app/components/base/confirm' | import Confirm from '@/app/components/base/confirm' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { updateUserProfile } from '@/service/common' | 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 { ToastContext } from '@/app/components/base/toast' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import Avatar from '@/app/components/base/avatar' | import Avatar from '@/app/components/base/avatar' | ||||
| const [password, setPassword] = useState('') | const [password, setPassword] = useState('') | ||||
| const [confirmPassword, setConfirmPassword] = useState('') | const [confirmPassword, setConfirmPassword] = useState('') | ||||
| const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) | const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) | ||||
| const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) | |||||
| const handleEditName = () => { | const handleEditName = () => { | ||||
| setEditNameModalVisible(true) | setEditNameModalVisible(true) | ||||
| <div className={titleClassName}>{t('common.account.email')}</div> | <div className={titleClassName}>{t('common.account.email')}</div> | ||||
| <div className={classNames(inputClassName, 'cursor-pointer')}>{userProfile.email}</div> | <div className={classNames(inputClassName, 'cursor-pointer')}>{userProfile.email}</div> | ||||
| </div> | </div> | ||||
| {IS_CE_EDITION && ( | |||||
| {systemFeatures.enable_email_password_login && ( | |||||
| <div className='mb-8'> | <div className='mb-8'> | ||||
| <div className='mb-1 text-sm font-medium text-gray-900'>{t('common.account.password')}</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> | <div className='mb-2 text-xs text-gray-500'>{t('common.account.passwordTip')}</div> |
| '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> | |||||
| } |
| 'use client' | 'use client' | ||||
| import { SWRConfig } from 'swr' | import { SWRConfig } from 'swr' | ||||
| import { useEffect, useState } from 'react' | |||||
| import { useCallback, useEffect, useState } from 'react' | |||||
| import type { ReactNode } from 'react' | import type { ReactNode } from 'react' | ||||
| import { useRouter, useSearchParams } from 'next/navigation' | import { useRouter, useSearchParams } from 'next/navigation' | ||||
| import useRefreshToken from '@/hooks/use-refresh-token' | import useRefreshToken from '@/hooks/use-refresh-token' | ||||
| import { fetchSetupStatus } from '@/service/common' | |||||
| type SwrInitorProps = { | type SwrInitorProps = { | ||||
| children: ReactNode | children: ReactNode | ||||
| const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token') | const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token') | ||||
| const [init, setInit] = useState(false) | 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 | return init | ||||
| ? ( | ? ( | ||||
| <SWRConfig value={{ | <SWRConfig value={{ |
| import { useSearchParams } from 'next/navigation' | import { useSearchParams } from 'next/navigation' | ||||
| import cn from 'classnames' | import cn from 'classnames' | ||||
| import { CheckCircleIcon } from '@heroicons/react/24/solid' | import { CheckCircleIcon } from '@heroicons/react/24/solid' | ||||
| import Input from '../components/base/input' | |||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { changePasswordWithToken, verifyForgotPasswordToken } from '@/service/common' | import { changePasswordWithToken, verifyForgotPasswordToken } from '@/service/common' | ||||
| import Toast from '@/app/components/base/toast' | import Toast from '@/app/components/base/toast' | ||||
| <label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900"> | <label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900"> | ||||
| {t('common.account.newPassword')} | {t('common.account.newPassword')} | ||||
| </label> | </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> | </div> | ||||
| {/* Confirm Password */} | {/* Confirm Password */} | ||||
| <div className='mb-5'> | <div className='mb-5'> | ||||
| <label htmlFor="confirmPassword" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900"> | <label htmlFor="confirmPassword" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900"> | ||||
| {t('common.account.confirmPassword')} | {t('common.account.confirmPassword')} | ||||
| </label> | </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> | ||||
| <div> | <div> | ||||
| <Button | <Button | ||||
| </h2> | </h2> | ||||
| </div> | </div> | ||||
| <div className="w-full mx-auto mt-6"> | <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> | <a href="/signin">{t('login.passwordChanged')}</a> | ||||
| </Button> | </Button> | ||||
| </div> | </div> |
| import { z } from 'zod' | import { z } from 'zod' | ||||
| import { zodResolver } from '@hookform/resolvers/zod' | import { zodResolver } from '@hookform/resolvers/zod' | ||||
| import Loading from '../components/base/loading' | import Loading from '../components/base/loading' | ||||
| import Input from '../components/base/input' | |||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { | import { | ||||
| return ( | return ( | ||||
| loading | loading | ||||
| ? <Loading/> | |||||
| ? <Loading /> | |||||
| : <> | : <> | ||||
| <div className="sm:mx-auto sm:w-full sm:max-w-md"> | <div className="sm:mx-auto sm:w-full sm:max-w-md"> | ||||
| <h2 className="text-[32px] font-bold text-gray-900"> | <h2 className="text-[32px] font-bold text-gray-900"> | ||||
| {t('login.email')} | {t('login.email')} | ||||
| </label> | </label> | ||||
| <div className="mt-1"> | <div className="mt-1"> | ||||
| <input | |||||
| <Input | |||||
| {...register('email')} | {...register('email')} | ||||
| placeholder={t('login.emailPlaceholder') || ''} | 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>} | {errors.email && <span className='text-red-400 text-sm'>{t(`${errors.email?.message}`)}</span>} | ||||
| </div> | </div> |
| useEffect(() => { | useEffect(() => { | ||||
| fetchSetupStatus().then((res: SetupStatusResponse) => { | fetchSetupStatus().then((res: SetupStatusResponse) => { | ||||
| if (res.step === 'finished') { | if (res.step === 'finished') { | ||||
| localStorage.setItem('setup_status', 'finished') | |||||
| window.location.href = '/signin' | window.location.href = '/signin' | ||||
| } | } | ||||
| else { | else { | ||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| </form> | </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')} | {t('login.license.tip')} | ||||
| | | ||||
| <Link | <Link |
| '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> | |||||
| } |
| 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> | |||||
| </> | |||||
| } |
| '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> | |||||
| } |
| '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 |
| '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> | |||||
| } |
| 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> | |||||
| ) | |||||
| } |
| 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> | |||||
| } |
| 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> | |||||
| </> | |||||
| } |
| '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 |
| '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 |
| '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> | |||||
| } |
| 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> | |||||
| </> | |||||
| } |
| 'use client' | |||||
| import React, { useEffect, useReducer, useState } from 'react' | |||||
| import React, { useCallback, useEffect, useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useRouter } from 'next/navigation' | |||||
| import useSWR from 'swr' | |||||
| import Link from 'next/link' | 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' | 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 NormalForm = () => { | ||||
| const { t } = useTranslation() | |||||
| const { getNewAccessToken } = useRefreshToken() | const { getNewAccessToken } = useRefreshToken() | ||||
| const useEmailLogin = IS_CE_EDITION || SUPPORT_MAIL_LOGIN | |||||
| const { t } = useTranslation() | |||||
| const router = useRouter() | const router = useRouter() | ||||
| const [state, dispatch] = useReducer(reducer, { | |||||
| formValid: false, | |||||
| github: false, | |||||
| google: false, | |||||
| }) | |||||
| 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 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 isInviteLink = Boolean(invite_token && invite_token !== 'null') | |||||
| const init = useCallback(async () => { | |||||
| try { | 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() | getNewAccessToken() | ||||
| router.replace('/apps') | router.replace('/apps') | ||||
| return | |||||
| } | } | ||||
| else { | |||||
| if (message) { | |||||
| Toast.notify({ | Toast.notify({ | ||||
| type: 'error', | 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) | |||||
| } | } | ||||
| } | |||||
| 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]) | |||||
| finally { setIsLoading(false) } | |||||
| }, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, getNewAccessToken]) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (google_error !== undefined) | |||||
| dispatch({ type: 'google_login_failed' }) | |||||
| if (google) | |||||
| window.location.href = google.redirect_url | |||||
| }, [google, google_error]) | |||||
| 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> | |||||
| } | |||||
| return ( | 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="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> | |||||
| </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')} | {t('login.tosDesc')} | ||||
| | | ||||
| <Link | <Link | ||||
| className='text-primary-600' | |||||
| className='system-xs-medium text-text-secondary hover:underline' | |||||
| target='_blank' rel='noopener noreferrer' | target='_blank' rel='noopener noreferrer' | ||||
| href='https://dify.ai/terms' | href='https://dify.ai/terms' | ||||
| >{t('login.tos')}</Link> | >{t('login.tos')}</Link> | ||||
| & | & | ||||
| <Link | <Link | ||||
| className='text-primary-600' | |||||
| className='system-xs-medium text-text-secondary hover:underline' | |||||
| target='_blank' rel='noopener noreferrer' | target='_blank' rel='noopener noreferrer' | ||||
| href='https://dify.ai/privacy' | href='https://dify.ai/privacy' | ||||
| >{t('login.pp')}</Link> | >{t('login.pp')}</Link> | ||||
| </div> | </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')} | {t('login.goToInit')} | ||||
| | | ||||
| <Link | <Link | ||||
| className='text-primary-600' | |||||
| className='system-xs-medium text-text-secondary hover:underline' | |||||
| href='/install' | href='/install' | ||||
| >{t('login.setAdminAccount')}</Link> | >{t('login.setAdminAccount')}</Link> | ||||
| </div>} | </div>} |
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import Link from 'next/link' | import Link from 'next/link' | ||||
| import useSWR from 'swr' | 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 Button from '@/app/components/base/button' | ||||
| import Tooltip from '@/app/components/base/tooltip' | import Tooltip from '@/app/components/base/tooltip' | ||||
| import { SimpleSelect } from '@/app/components/base/select' | import { SimpleSelect } from '@/app/components/base/select' | ||||
| import { LanguagesSupported, languages } from '@/i18n/language' | import { LanguagesSupported, languages } from '@/i18n/language' | ||||
| import { oneMoreStep } from '@/service/common' | import { oneMoreStep } from '@/service/common' | ||||
| import Toast from '@/app/components/base/toast' | import Toast from '@/app/components/base/toast' | ||||
| // import I18n from '@/context/i18n' | |||||
| type IState = { | type IState = { | ||||
| formState: 'processing' | 'error' | 'success' | 'initial' | formState: 'processing' | 'error' | 'success' | 'initial' | ||||
| const OneMoreStep = () => { | const OneMoreStep = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const router = useRouter() | const router = useRouter() | ||||
| // const { locale } = useContext(I18n) | |||||
| const searchParams = useSearchParams() | |||||
| const [state, dispatch] = useReducer(reducer, { | const [state, dispatch] = useReducer(reducer, { | ||||
| formState: 'initial', | formState: 'initial', | ||||
| invitation_code: '', | |||||
| invitation_code: searchParams.get('invitation_code') || '', | |||||
| interface_language: 'en-US', | interface_language: 'en-US', | ||||
| timezone: 'Asia/Shanghai', | timezone: 'Asia/Shanghai', | ||||
| }) | }) | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <div className="w-full mx-auto"> | <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> | ||||
| <div className="w-full mx-auto mt-6"> | <div className="w-full mx-auto mt-6"> | ||||
| <div className="bg-white"> | <div className="bg-white"> | ||||
| <div className="mb-5"> | <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')} | {t('login.invitationCode')} | ||||
| <Tooltip | <Tooltip | ||||
| popupContent={ | popupContent={ | ||||
| <div className='w-[256px] text-xs font-medium'> | <div className='w-[256px] text-xs font-medium'> | ||||
| <div className='font-medium'>{t('login.sendUsMail')}</div> | <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> | <a href="mailto:request-invitation@langgenius.ai">request-invitation@langgenius.ai</a> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| } | } | ||||
| needsDelay | 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> | </Tooltip> | ||||
| </label> | </label> | ||||
| <div className="mt-1"> | <div className="mt-1"> | ||||
| <input | |||||
| <Input | |||||
| id="invitation_code" | id="invitation_code" | ||||
| value={state.invitation_code} | value={state.invitation_code} | ||||
| type="text" | type="text" | ||||
| placeholder={t('login.invitationCodePlaceholder') || ''} | 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) => { | onChange={(e) => { | ||||
| dispatch({ type: 'invitation_code', value: e.target.value.trim() }) | dispatch({ type: 'invitation_code', value: e.target.value.trim() }) | ||||
| }} | }} | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div className='mb-5'> | <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')} | {t('login.interfaceLanguage')} | ||||
| </label> | </label> | ||||
| <div className="relative mt-1 rounded-md shadow-sm"> | |||||
| <div className="mt-1"> | |||||
| <SimpleSelect | <SimpleSelect | ||||
| defaultValue={LanguagesSupported[0]} | defaultValue={LanguagesSupported[0]} | ||||
| items={languages.filter(item => item.supported)} | items={languages.filter(item => item.supported)} | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div className='mb-4'> | <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')} | {t('login.timezone')} | ||||
| </label> | </label> | ||||
| <div className="relative mt-1 rounded-md shadow-sm"> | |||||
| <div className="mt-1"> | |||||
| <SimpleSelect | <SimpleSelect | ||||
| defaultValue={state.timezone} | defaultValue={state.timezone} | ||||
| items={timezones} | items={timezones} | ||||
| {t('login.go')} | {t('login.go')} | ||||
| </Button> | </Button> | ||||
| </div> | </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')} | {t('login.license.tip')} | ||||
| | | ||||
| <Link | <Link | ||||
| className='text-primary-600' | |||||
| className='system-xs-medium text-text-accent-secondary' | |||||
| target='_blank' rel='noopener noreferrer' | target='_blank' rel='noopener noreferrer' | ||||
| href={'https://docs.dify.ai/user-agreement/open-source'} | href={'https://docs.dify.ai/user-agreement/open-source'} | ||||
| >{t('login.license.link')}</Link> | >{t('login.license.link')}</Link> |
| 'use client' | '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 SignIn = () => { | ||||
| const [loading, setLoading] = useState<boolean>(true) | |||||
| const [systemFeatures, setSystemFeatures] = useState<SystemFeatures>(defaultSystemFeatures) | |||||
| 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> | |||||
| const searchParams = useSearchParams() | |||||
| const step = searchParams.get('step') | |||||
| </> | |||||
| ) | |||||
| if (step === 'next') | |||||
| return <OneMoreStep /> | |||||
| return <NormalForm /> | |||||
| } | } | ||||
| export default SignIn | export default SignIn |
| '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 |
| const translation = { | 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', | email: 'Email address', | ||||
| emailPlaceholder: 'Your email', | emailPlaceholder: 'Your email', | ||||
| password: 'Password', | password: 'Password', | ||||
| namePlaceholder: 'Your username', | namePlaceholder: 'Your username', | ||||
| forget: 'Forgot your password?', | forget: 'Forgot your password?', | ||||
| signBtn: 'Sign in', | 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', | installBtn: 'Set up', | ||||
| setAdminAccount: 'Setting up an admin account', | setAdminAccount: 'Setting up an admin account', | ||||
| setAdminAccountDesc: 'Maximum privileges for admin account, which can be used to create applications and manage LLM providers, etc.', | setAdminAccountDesc: 'Maximum privileges for admin account, which can be used to create applications and manage LLM providers, etc.', | ||||
| reset: 'Please run following command to reset your password', | reset: 'Please run following command to reset your password', | ||||
| withGitHub: 'Continue with GitHub', | withGitHub: 'Continue with GitHub', | ||||
| withGoogle: 'Continue with Google', | withGoogle: 'Continue with Google', | ||||
| withSSO: 'Continue with SSO', | |||||
| rightTitle: 'Unlock the full potential of LLM', | rightTitle: 'Unlock the full potential of LLM', | ||||
| rightDesc: 'Effortlessly build visually captivating, operable, and improvable AI applications.', | rightDesc: 'Effortlessly build visually captivating, operable, and improvable AI applications.', | ||||
| tos: 'Terms of Service', | tos: 'Terms of Service', | ||||
| forgotPasswordDesc: 'Please enter your email address to reset your password. We will send you an email with instructions on how to reset your password.', | 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.', | 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', | passwordChanged: 'Sign in now', | ||||
| changePassword: 'Change Password', | |||||
| changePassword: 'Set a password', | |||||
| changePasswordTip: 'Please enter a new password for your account', | changePasswordTip: 'Please enter a new password for your account', | ||||
| changePasswordBtn: 'Set a password', | |||||
| invalidToken: 'Invalid or expired token', | invalidToken: 'Invalid or expired token', | ||||
| confirmPassword: 'Confirm Password', | confirmPassword: 'Confirm Password', | ||||
| confirmPasswordPlaceholder: 'Confirm your new password', | confirmPasswordPlaceholder: 'Confirm your new password', | ||||
| passwordEmpty: 'Password is required', | passwordEmpty: 'Password is required', | ||||
| passwordLengthInValid: 'Password must be at least 8 characters', | passwordLengthInValid: 'Password must be at least 8 characters', | ||||
| passwordInvalid: 'Password must contain letters and numbers, and the length must be greater than 8', | 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: { | license: { | ||||
| tip: 'Before starting Dify Community Edition, read the GitHub', | tip: 'Before starting Dify Community Edition, read the GitHub', | ||||
| link: 'Open-source License', | 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', | invalid: 'The link has expired', | ||||
| explore: 'Explore Dify', | explore: 'Explore Dify', | ||||
| activatedTipStart: 'You have joined the', | activatedTipStart: 'You have joined the', | ||||
| activated: 'Sign in now', | activated: 'Sign in now', | ||||
| adminInitPassword: 'Admin initialization password', | adminInitPassword: 'Admin initialization password', | ||||
| validate: 'Validate', | 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 | export default translation |
| const translation = { | const translation = { | ||||
| pageTitle: '嗨,近来可好 👋', | |||||
| welcome: '欢迎来到 Dify, 登录以继续', | |||||
| pageTitle: '嗨,近来可好', | |||||
| welcome: '👋 欢迎来到 Dify, 登录以继续', | |||||
| email: '邮箱', | email: '邮箱', | ||||
| emailPlaceholder: '输入邮箱地址', | emailPlaceholder: '输入邮箱地址', | ||||
| password: '密码', | password: '密码', | ||||
| namePlaceholder: '输入用户名', | namePlaceholder: '输入用户名', | ||||
| forget: '忘记密码?', | forget: '忘记密码?', | ||||
| signBtn: '登录', | signBtn: '登录', | ||||
| continueWithCode: '发送验证码', | |||||
| sendVerificationCode: '发送验证码', | |||||
| usePassword: '使用密码登录', | |||||
| useVerificationCode: '使用验证码登录', | |||||
| or: '或', | |||||
| installBtn: '设置', | installBtn: '设置', | ||||
| setAdminAccount: '设置管理员账户', | setAdminAccount: '设置管理员账户', | ||||
| setAdminAccountDesc: '管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。', | setAdminAccountDesc: '管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。', | ||||
| reset: '请运行以下命令重置密码', | reset: '请运行以下命令重置密码', | ||||
| withGitHub: '使用 GitHub 登录', | withGitHub: '使用 GitHub 登录', | ||||
| withGoogle: '使用 Google 登录', | withGoogle: '使用 Google 登录', | ||||
| withSSO: '使用 SSO 登录', | |||||
| rightTitle: '释放大型语言模型的全部潜能', | rightTitle: '释放大型语言模型的全部潜能', | ||||
| rightDesc: '简单构建可视化、可运营、可改进的 AI 应用', | rightDesc: '简单构建可视化、可运营、可改进的 AI 应用', | ||||
| tos: '使用协议', | tos: '使用协议', | ||||
| pp: '隐私政策', | pp: '隐私政策', | ||||
| tosDesc: '使用即代表你并同意我们的', | |||||
| tosDesc: '使用即代表您同意我们的', | |||||
| goToInit: '如果您还没有初始化账户,请前往初始化页面', | goToInit: '如果您还没有初始化账户,请前往初始化页面', | ||||
| dontHave: '还没有邀请码?', | dontHave: '还没有邀请码?', | ||||
| invalidInvitationCode: '无效的邀请码', | invalidInvitationCode: '无效的邀请码', | ||||
| forgotPasswordDesc: '请输入您的电子邮件地址以重置密码。我们将向您发送一封电子邮件,包含如何重置密码的说明。', | forgotPasswordDesc: '请输入您的电子邮件地址以重置密码。我们将向您发送一封电子邮件,包含如何重置密码的说明。', | ||||
| checkEmailForResetLink: '请检查您的电子邮件以获取重置密码的链接。如果几分钟内没有收到,请检查您的垃圾邮件文件夹。', | checkEmailForResetLink: '请检查您的电子邮件以获取重置密码的链接。如果几分钟内没有收到,请检查您的垃圾邮件文件夹。', | ||||
| passwordChanged: '立即登录', | passwordChanged: '立即登录', | ||||
| changePassword: '更改密码', | |||||
| changePassword: '设置密码', | |||||
| changePasswordTip: '请输入您的新密码', | changePasswordTip: '请输入您的新密码', | ||||
| changePasswordBtn: '设置密码', | |||||
| invalidToken: '无效或已过期的令牌', | invalidToken: '无效或已过期的令牌', | ||||
| confirmPassword: '确认密码', | confirmPassword: '确认密码', | ||||
| confirmPasswordPlaceholder: '确认您的新密码', | confirmPasswordPlaceholder: '确认您的新密码', | ||||
| passwordEmpty: '密码不能为空', | passwordEmpty: '密码不能为空', | ||||
| passwordInvalid: '密码必须包含字母和数字,且长度不小于8位', | passwordInvalid: '密码必须包含字母和数字,且长度不小于8位', | ||||
| passwordLengthInValid: '密码必须至少为 8 个字符', | passwordLengthInValid: '密码必须至少为 8 个字符', | ||||
| registrationNotAllowed: '账户不存在,请联系系统管理员注册账户', | |||||
| }, | }, | ||||
| license: { | license: { | ||||
| tip: '启动 Dify 社区版之前, 请阅读 GitHub 上的', | tip: '启动 Dify 社区版之前, 请阅读 GitHub 上的', | ||||
| link: '开源协议', | link: '开源协议', | ||||
| }, | }, | ||||
| join: '加入', | |||||
| joinTipStart: '邀请你加入', | |||||
| joinTipEnd: '团队', | |||||
| join: '加入 ', | |||||
| joinTipStart: '邀请你加入 ', | |||||
| joinTipEnd: ' 团队', | |||||
| invalid: '链接已失效', | invalid: '链接已失效', | ||||
| explore: '探索 Dify', | explore: '探索 Dify', | ||||
| activatedTipStart: '您已加入', | activatedTipStart: '您已加入', | ||||
| adminInitPassword: '管理员初始化密码', | adminInitPassword: '管理员初始化密码', | ||||
| validate: '验证', | validate: '验证', | ||||
| sso: '使用 SSO 继续', | 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 | export default translation |
| const translation = { | const translation = { | ||||
| pageTitle: '嗨,近來可好 👋', | |||||
| welcome: '歡迎來到 Dify, 登入以繼續', | |||||
| pageTitle: '嗨,近來可好', | |||||
| welcome: '👋 歡迎來到 Dify, 登入以繼續', | |||||
| email: '郵箱', | email: '郵箱', | ||||
| emailPlaceholder: '輸入郵箱地址', | emailPlaceholder: '輸入郵箱地址', | ||||
| password: '密碼', | password: '密碼', |
| type LoginFail = { | type LoginFail = { | ||||
| result: 'fail' | result: 'fail' | ||||
| data: string | data: string | ||||
| code: string | |||||
| message: string | |||||
| } | } | ||||
| type LoginResponse = LoginSuccess | LoginFail | type LoginResponse = LoginSuccess | LoginFail | ||||
| export const login: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => { | export const login: Fetcher<LoginResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => { | ||||
| return post<UpdateOpenAIKeyResponse>(url, { body }) | 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) => { | export const fetchModelProviders: Fetcher<{ data: ModelProvider[] }, string> = (url) => { | ||||
| export const disableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) => | export const disableModel = (url: string, body: { model: string; model_type: ModelTypeEnum }) => | ||||
| patch<CommonResponse>(url, { body }) | 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 }) => { | 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 }> | return post(url, { body }) as Promise<CommonResponse & { is_valid: boolean; email: string }> | ||||
| export const changePasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) => | export const changePasswordWithToken: Fetcher<CommonResponse, { url: string; body: { token: string; new_password: string; password_confirm: string } }> = ({ url, body }) => | ||||
| post<CommonResponse>(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 }) |
| import { get } from './base' | 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) | |||||
| } | } |
| export enum SSOProtocol { | |||||
| SAML = 'saml', | |||||
| OIDC = 'oidc', | |||||
| OAuth2 = 'oauth2', | |||||
| } | |||||
| export type SystemFeatures = { | export type SystemFeatures = { | ||||
| sso_enforced_for_signin: boolean | 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: boolean | ||||
| sso_enforced_for_web_protocol: string | |||||
| sso_enforced_for_web_protocol: SSOProtocol | '' | |||||
| enable_web_sso_switch_component: boolean | 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 = { | export const defaultSystemFeatures: SystemFeatures = { | ||||
| sso_enforced_for_web: false, | sso_enforced_for_web: false, | ||||
| sso_enforced_for_web_protocol: '', | sso_enforced_for_web_protocol: '', | ||||
| enable_web_sso_switch_component: false, | 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, | |||||
| } | } |