diff --git a/web/app/signin/check-code/page.tsx b/web/app/signin/check-code/page.tsx new file mode 100644 index 0000000000..efec379bab --- /dev/null +++ b/web/app/signin/check-code/page.tsx @@ -0,0 +1,3 @@ +export default function CheckCode() { + return
CheckCode
+} diff --git a/web/app/signin/components/github-auth-button.tsx b/web/app/signin/components/github-auth-button.tsx deleted file mode 100644 index d6804f09d5..0000000000 --- a/web/app/signin/components/github-auth-button.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useTranslation } from 'react-i18next' -import style from '../page.module.css' -import Button from '@/app/components/base/button' -import { apiPrefix } from '@/config' -import { getPurifyHref } from '@/utils' -import classNames from '@/utils/classnames' - -type GithubAuthButtonProps = { - disabled: boolean -} - -export default function GithubAuthButton(props: GithubAuthButtonProps) { - const { t } = useTranslation() - return - - -} diff --git a/web/app/signin/components/google-auth-button.tsx b/web/app/signin/components/google-auth-button.tsx deleted file mode 100644 index db67f95dc0..0000000000 --- a/web/app/signin/components/google-auth-button.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useTranslation } from 'react-i18next' -import style from '../page.module.css' -import Button from '@/app/components/base/button' -import { apiPrefix } from '@/config' -import { getPurifyHref } from '@/utils' -import classNames from '@/utils/classnames' - -type GoogleAuthButtonProps = { - disabled: boolean -} - -export default function GoogleAuthButton(props: GoogleAuthButtonProps) { - const { t } = useTranslation() - return - - -} diff --git a/web/app/signin/components/mail-and-code-auth.tsx b/web/app/signin/components/mail-and-code-auth.tsx new file mode 100644 index 0000000000..5d67c059de --- /dev/null +++ b/web/app/signin/components/mail-and-code-auth.tsx @@ -0,0 +1,34 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +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' + +export default function MailAndCodeAuth() { + const { t } = useTranslation() + const [email, setEmail] = useState('') + + const handleGetEMailVerificationCode = async () => { + if (!emailRegex.test(email)) { + Toast.notify({ + type: 'error', + message: t('login.error.emailInValid'), + }) + } + window.location.href = '/signin/check-code' + } + + return (
{ }}> +
+ +
+ +
+
+ +
+
+
+ ) +} diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx new file mode 100644 index 0000000000..3efd0c28d9 --- /dev/null +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -0,0 +1,112 @@ +import Link from 'next/link' +import router from 'next/router' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import Toast from '@/app/components/base/toast' +import { emailRegex } from '@/config' +import { login } from '@/service/common' + +export default function MailAndPasswordAuth() { + const { t } = useTranslation() + 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 + } + 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) + router.replace('/apps') + } + else { + Toast.notify({ + type: 'error', + message: res.data, + }) + } + } + finally { + setIsLoading(false) + } + } + + return
{ }}> +
+ +
+ 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'} + /> +
+
+ +
+ +
+ 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'} + /> +
+ +
+
+
+ +
+ +
+
+} diff --git a/web/app/signin/components/social-auth.tsx b/web/app/signin/components/social-auth.tsx new file mode 100644 index 0000000000..54e1117070 --- /dev/null +++ b/web/app/signin/components/social-auth.tsx @@ -0,0 +1,52 @@ +import { useTranslation } from 'react-i18next' +import style from '../page.module.css' +import Button from '@/app/components/base/button' +import { apiPrefix } from '@/config' +import { getPurifyHref } from '@/utils' +import classNames from '@/utils/classnames' + +type SocialAuthProps = { + disabled?: boolean +} + +export default function SocialAuth(props: SocialAuthProps) { + const { t } = useTranslation() + return <> +
+ + + +
+
+ + + +
+ +} diff --git a/web/app/signin/components/sso-auth-button.tsx b/web/app/signin/components/sso-auth.tsx similarity index 100% rename from web/app/signin/components/sso-auth-button.tsx rename to web/app/signin/components/sso-auth.tsx diff --git a/web/app/signin/forms.tsx b/web/app/signin/forms.tsx index 70a34c26fa..f1e8f3dc9a 100644 --- a/web/app/signin/forms.tsx +++ b/web/app/signin/forms.tsx @@ -1,34 +1,17 @@ -'use client' -import React from 'react' -import { useSearchParams } from 'next/navigation' - +import SSOAuthButton from './components/sso-auth' import NormalForm from './normalForm' -import OneMoreStep from './oneMoreStep' -import cn from '@/utils/classnames' +import { API_PREFIX } from '@/config' -const Forms = () => { - const searchParams = useSearchParams() - const step = searchParams.get('step') - - const getForm = () => { - switch (step) { - case 'next': - return - default: - return - } +export default function RenderFormBySystemFeatures() { + async function getFeatures() { + 'use server' + const ret = await fetch(`${API_PREFIX}/system-features`).then((ret) => { + return ret.json() + }) + return ret } - return
-
- {getForm()} -
-
+ const systemFeatures = await getFeatures() + if (systemFeatures.isSsoEnabled) + return + return } - -export default Forms diff --git a/web/app/signin/layout.tsx b/web/app/signin/layout.tsx new file mode 100644 index 0000000000..9002424a20 --- /dev/null +++ b/web/app/signin/layout.tsx @@ -0,0 +1,54 @@ +import Script from 'next/script' +import Header from './_header' +import style from './page.module.css' + +import cn from '@/utils/classnames' +import { IS_CE_EDITION } from '@/config' + +export default async function SignInLayout({ children }: any) { + return <> + {!IS_CE_EDITION && ( + <> + + + + )} + +
+
+
+
+
+ {children} +
+
+
+ © {new Date().getFullYear()} LangGenius, Inc. All rights reserved. +
+
+
+ +} diff --git a/web/app/signin/normalForm.tsx b/web/app/signin/normalForm.tsx index fdea0d567b..6eaf65fe7c 100644 --- a/web/app/signin/normalForm.tsx +++ b/web/app/signin/normalForm.tsx @@ -1,146 +1,47 @@ -'use client' -import React, { useReducer, useState } from 'react' +import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useRouter } from 'next/navigation' import Link from 'next/link' -import Toast from '../components/base/toast' -import SSOAuthButton from './components/sso-auth-button' -import GoogleAuthButton from './components/google-auth-button' -import GithubAuthButton from './components/github-auth-button' -import { IS_CE_EDITION, SUPPORT_MAIL_LOGIN, emailRegex } from '@/config' -import Button from '@/app/components/base/button' -import { login } from '@/service/common' +import Loading from '../components/base/loading' +import SSOAuthButton from './components/sso-auth' +import MailAndCodeAuth from './components/mail-and-code-auth' +import MailAndPasswordAuth from './components/mail-and-password-auth' +import SocialAuth from './components/social-auth' +import { IS_CE_EDITION } from '@/config' +import { defaultSystemFeatures } from '@/types/feature' +import { getSystemFeatures } from '@/service/common' +import cn from '@/utils/classnames' -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.') - } -} +type AuthType = 'password' | 'code' | 'sso' | 'social' const NormalForm = () => { const { t } = useTranslation() - const useEmailLogin = IS_CE_EDITION || SUPPORT_MAIL_LOGIN + const [authType, updateAuthType] = useState('code') + const [enabledAuthType, updateEnabledAuthType] = useState(['password', 'code', 'sso', 'social']) - const router = useRouter() + const [isLoading, setIsLoading] = useState(true) + const [systemFeatures, setSystemFeatures] = useState(defaultSystemFeatures) - 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 - } - 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) - router.replace('/apps') - } - else { - Toast.notify({ - type: 'error', - message: res.data, - }) - } - } - finally { + useEffect(() => { + getSystemFeatures().then((res) => { + console.log('🚀 ~ getSystemFeatures ~ res:', res) + setSystemFeatures(res) + }).finally(() => { setIsLoading(false) - } + }) + }, []) + + if (isLoading) { + return
+ +
} - // const { data: github, error: github_error } = useSWR(state.github - // ? ({ - // url: '/oauth/login/github', - // // params: { - // // provider: 'github', - // // }, - // }) - // : null, oauth) - - // const { data: google, error: google_error } = useSWR(state.google - // ? ({ - // url: '/oauth/login/google', - // // params: { - // // provider: 'google', - // // }, - // }) - // : null, oauth) - - // useEffect(() => { - // if (github_error !== undefined) - // dispatch({ type: 'github_login_failed' }) - // if (github) - // window.location.href = github.redirect_url - // }, [github, github_error]) - - // useEffect(() => { - // if (google_error !== undefined) - // dispatch({ type: 'google_login_failed' }) - // if (google) - // window.location.href = google.redirect_url - // }, [google, google_error]) - return ( <>
@@ -152,91 +53,32 @@ const NormalForm = () => {
-
- -
-
- -
-
- -
+ {enabledAuthType.includes('social') && } + {enabledAuthType.includes('sso') &&
}
- <> -
+ {(enabledAuthType.includes('code') || enabledAuthType.includes('password')) + &&
+
} + {enabledAuthType.includes('code') && authType === 'code' && <> + + {enabledAuthType.includes('password') &&
{ updateAuthType('password') }}> + {t('login.usePassword')} +
} + } + {enabledAuthType.includes('password') && authType === 'password' && <> + + {enabledAuthType.includes('code') &&
{ updateAuthType('code') }}> + {t('login.useVerificationCode')} +
} + } -
{ }}> -
- -
- 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'} - /> -
-
- -
- -
- 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'} - /> -
- -
-
-
- -
- -
-
- - - {/* agree to our Terms and Privacy Policy. */}
{t('login.tosDesc')}   diff --git a/web/app/signin/page.tsx b/web/app/signin/page.tsx index 5865f40e71..ea142d71d0 100644 --- a/web/app/signin/page.tsx +++ b/web/app/signin/page.tsx @@ -1,94 +1,16 @@ '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 OneMoreStep from './oneMoreStep' +import NormalForm from './normalForm' -import type { SystemFeatures } from '@/types/feature' -import { defaultSystemFeatures } from '@/types/feature' -import { getSystemFeatures } from '@/service/common' +type Props = { + searchParams: { step?: 'next' } +} -const SignIn = () => { - const [loading, setLoading] = useState(true) - const [systemFeatures, setSystemFeatures] = useState(defaultSystemFeatures) +const SignIn = ({ searchParams }: Props) => { + if (searchParams?.step === 'next') + return - useEffect(() => { - getSystemFeatures().then((res) => { - setSystemFeatures(res) - }).finally(() => { - setLoading(false) - }) - }, []) - - return ( - <> - {!IS_CE_EDITION && ( - <> - - - - )} -
-
-
- - {loading && ( -
- -
- )} - - {!loading && !systemFeatures.sso_enforced_for_signin && ( - <> - -
- © {new Date().getFullYear()} LangGenius, Inc. All rights reserved. -
- - )} - - {!loading && systemFeatures.sso_enforced_for_signin && ( - - )} -
- -
- - - ) + return } export default SignIn diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index bc19fecadb..d28b482c89 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -9,6 +9,10 @@ const translation = { namePlaceholder: '输入用户名', forget: '忘记密码?', signBtn: '登录', + continueWithCode: '发送验证码', + usePassword: '使用密码登录', + useVerificationCode: '使用验证码登录', + or: '或', installBtn: '设置', setAdminAccount: '设置管理员账户', setAdminAccountDesc: '管理员拥有的最大权限,可用于创建应用和管理 LLM 供应商等。',