diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx index 91fb1d50a..d3845f5e1 100644 --- a/web/app/(shareLayout)/webapp-signin/normalForm.tsx +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -106,7 +106,7 @@ const NormalForm = () => {

{systemFeatures.branding.enabled ? t('pageTitleForE', { ns: 'login' }) : t('pageTitle', { ns: 'login' })}

{/*extend: login.welcome*/} - {!systemFeatures.branding.enabled &&

{t('welcome', { ns: 'login' })}

+ {!systemFeatures.branding.enabled &&

{t('welcome', { ns: 'login' })}

}
diff --git a/web/app/components/base/auto-select-extend/index.tsx b/web/app/components/base/auto-select-extend/index.tsx index 60f03dde0..1c014cd21 100644 --- a/web/app/components/base/auto-select-extend/index.tsx +++ b/web/app/components/base/auto-select-extend/index.tsx @@ -4,7 +4,7 @@ import React, { Fragment, useEffect, useState } from 'react' import { Combobox, Listbox, Transition } from '@headlessui/react' import { CheckIcon, ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid' import { useTranslation } from 'react-i18next' -import classNames from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { PortalToFollowElem, PortalToFollowElemContent, @@ -83,7 +83,7 @@ const Select: FC = ({ onSelect(value) } }}> -
+
{allowSearch ? = ({ if (!disabled) setOpen(!open) } - } className={classNames(optionClassName, `flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200`)}> + } className={cn(optionClassName, `flex items-center h-9 w-full rounded-lg border-0 ${bgClassName} py-1.5 pl-3 pr-10 shadow-sm sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200`)}>
{selectedItem?.name}
} = ({ key={item.value} value={item} className={({ active }: { active: boolean }) => - classNames( + cn( optionClassName, 'relative cursor-default select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700', active ? 'bg-gray-100' : '', @@ -128,10 +128,10 @@ const Select: FC = ({ > {({ /* active, */ selected }) => ( <> - {item.name} + {item.name} {selected && ( @@ -159,7 +159,7 @@ const SimpleSelect: FC = ({ placeholder, }) => { const { t } = useTranslation() - const localPlaceholder = placeholder || t('common.placeholder.select') + const localPlaceholder = placeholder || t('placeholder.select', { ns: 'common' }) const [selectedItem, setSelectedItem] = useState(null) useEffect(() => { @@ -183,7 +183,7 @@ const SimpleSelect: FC = ({ >
- {selectedItem?.name ?? localPlaceholder} + {selectedItem?.name ?? localPlaceholder} {selectedItem ? ( @@ -226,10 +226,10 @@ const SimpleSelect: FC = ({ > {({ /* active, */ selected }) => ( <> - {item.name} + {item.name} {selected && ( @@ -264,7 +264,7 @@ const PortalSelect: FC = ({ }) => { const { t } = useTranslation() const [open, setOpen] = useState(false) - const localPlaceholder = placeholder || t('common.placeholder.select') + const localPlaceholder = placeholder || t('placeholder.select', { ns: 'common' }) const selectedItem = items.find(item => item.value === value) return ( @@ -277,7 +277,7 @@ const PortalSelect: FC = ({ setOpen(v => !v)} className='w-full'>
diff --git a/web/app/signin/components/dingtalk-auth.tsx b/web/app/signin/components/dingtalk-auth.tsx index cd9b0e28d..c56ce92c5 100644 --- a/web/app/signin/components/dingtalk-auth.tsx +++ b/web/app/signin/components/dingtalk-auth.tsx @@ -3,7 +3,7 @@ import { useRouter } from 'next/navigation' import { useTranslation } from 'react-i18next' import style from '../page.module.css' import Button from '@/app/components/base/button' -import classNames from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { API_PREFIX } from '@/config' type SocialAuthProps = { @@ -32,13 +32,13 @@ export default function DingTalkAuth(props: SocialAuthProps) { className="w-full" > - {t('extend.sidebar.withDingTalk')} + {t('sidebar.withDingTalk', { ns: 'extend' })}
diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx index 7fa14eb02..55222615d 100644 --- a/web/app/signin/components/mail-and-password-auth.tsx +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -8,7 +8,7 @@ import { trackEvent } from '@/app/components/base/amplitude' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Toast from '@/app/components/base/toast' -import { emailRegex } from '@/config' +import { emailRegex, CSRF_COOKIE_NAME } from '@/config' import { useLocale } from '@/context/i18n' import { login } from '@/service/common' import { setWebAppAccessToken } from '@/service/webapp-auth' @@ -66,7 +66,9 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis }) if (res.result === 'success') { // Track login success event - setWebAppAccessToken(res.data.access_token) + if (res.data?.access_token) { + setWebAppAccessToken(res.data.access_token) + } trackEvent('user_login_success', { method: 'email_password', is_invite: isInvite, @@ -76,17 +78,24 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis router.replace(`/signin/invite-settings?${searchParams.toString()}`) } else { - const redirectUrl = resolvePostLoginRedirect(searchParams) - router.replace(redirectUrl || '/apps') // Extend Begin ---------------- + const tokenKey = CSRF_COOKIE_NAME() + const loginData = res.data as { access_token?: string; refresh_token?: string } | undefined + if (loginData?.access_token) { + localStorage.setItem(tokenKey, loginData.access_token) + if (loginData.refresh_token) { + localStorage.setItem('refresh_token', loginData.refresh_token) + } + } // 如果本地浏览器缓存数据存在重定向url,则跳转到重定向url - if (localStorage.getItem('redirect_url')) { - const redirectUrl = localStorage.getItem('redirect_url') + const redirectUrl = localStorage.getItem('redirect_url') + if (redirectUrl) { localStorage.removeItem('redirect_url') - router.replace(redirectUrl as string) + window.location.href = redirectUrl return } - router.replace('/explore/apps-center-extend') + // 强制跳转到应用中心 + window.location.href = '/explore/apps-center-extend' // Extend End ---------------- } } diff --git a/web/app/signin/components/oauth2.tsx b/web/app/signin/components/oauth2.tsx index 23bf53ed0..b22d9a144 100644 --- a/web/app/signin/components/oauth2.tsx +++ b/web/app/signin/components/oauth2.tsx @@ -3,10 +3,14 @@ import { useRouter } from 'next/navigation' import { useTranslation } from 'react-i18next' import style from '../page.module.css' import Button from '@/app/components/base/button' -import classNames from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { API_PREFIX } from '@/config' -export default function OAuth2() { +type OAuth2Props = { + title: string +} + +export default function OAuth2(props: OAuth2Props) { const { t } = useTranslation() const router = useRouter() @@ -22,13 +26,13 @@ export default function OAuth2() { className="w-full" > - {t('appOverview.overview.appInfo.settings.sso.label')} + {props.title === '' ? t('withSSO', { ns: 'login' }) : props.title}
diff --git a/web/app/signin/normal-form.tsx b/web/app/signin/normal-form.tsx index be0feea6c..6879d11b4 100644 --- a/web/app/signin/normal-form.tsx +++ b/web/app/signin/normal-form.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Toast from '@/app/components/base/toast' -import { IS_CE_EDITION } from '@/config' +import { IS_CE_EDITION, CSRF_COOKIE_NAME } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' import { invitationCheck } from '@/service/common' import { useIsLogin } from '@/service/use-common' @@ -18,6 +18,23 @@ import SocialAuth from './components/social-auth' import SSOAuth from './components/sso-auth' import Split from './split' import { resolvePostLoginRedirect } from './utils/post-login-redirect' +// Extend: start support ding_talk login +import DingTalkAuth from '@/app/signin/components/dingtalk-auth' +import OAuth2 from '@/app/signin/components/oauth2' // extend: add oauth2 +// Extend: end + +// Extend: start 声明一个变量来存储钉钉SDK +// 客户端环境中初始化钉钉SDK +let dd: any = null +if (typeof window !== 'undefined') { + try { + dd = require('dingtalk-jsapi') + } + catch (e) { + console.error('Failed to load dingtalk-jsapi:', e) + } +} +// Extend: end const NormalForm = () => { const { t } = useTranslation() @@ -38,6 +55,73 @@ const NormalForm = () => { const isInviteLink = Boolean(invite_token && invite_token !== 'null') + // Extend: start Ding Talk Auto Login Logic + const dingTalkLogin = async (allFeatures: typeof systemFeatures) => { + // 确保只在客户端环境执行 + if (typeof window === 'undefined' || !dd) + return + + const tokenKey = CSRF_COOKIE_NAME() + let consoleToken: string | null | undefined = decodeURIComponent(searchParams.get('console_token') || '') + const consoleTokenFromLocalStorage = localStorage?.getItem(tokenKey) + const jumpsNumber = Number(localStorage?.getItem('jumps_number')) + if (consoleToken || consoleTokenFromLocalStorage) { + if (!consoleToken) + consoleToken = consoleTokenFromLocalStorage + if (consoleToken) { + if (jumpsNumber) { + // token无效 + localStorage.removeItem(tokenKey) + window.location.href = '/explore/apps-center-extend' + return + } + localStorage.setItem(tokenKey, consoleToken) + localStorage?.setItem('jumps_number', (jumpsNumber + 1).toString()) + window.location.href = `/explore/apps-center-extend?console_token=${consoleToken}` + return + } + else { + window.location.href = '/explore/apps-center-extend' + return + } + } + const userAgent = navigator.userAgent.toLowerCase() + const host = process.env.NEXT_PUBLIC_API_PREFIX + const corpId = allFeatures.ding_talk_corp_id + if (userAgent.includes('dingtalk') && corpId && host) { + // Extend Start DingTalk login compatible + localStorage?.removeItem('redirect_url') + // Extend Stop DingTalk login compatible + + try { + await dd.getAuthCode({ + corpId, + // 获取临时授权ID + success: (res: { code: any }) => { + // 在这里可以将免登授权码发送给后台服务器进行验证和获取用户信息等操作 + window.location.href = `${host}/ding-talk/login?code=${res.code}` + }, + fail() { + if (dd.runtime && dd.runtime.permission) { + dd.runtime.permission.requestAuthCode({ + corpId, + // 在这里我们移除了agentId参数,因为类型检查显示它不是有效的参数 + onSuccess(result: { code: any }) { + // 在这里可以将免登授权码发送给后台服务器进行验证和获取用户信息等操作 + window.location.href = `${host}/ding-talk/login?code=${result.code}` + }, + }) + } + }, + }) + } + catch (error) { + console.error('DingTalk auth error:', error) + } + } + } + // Extend: end Ding Talk Auto Login Logic + const init = useCallback(async () => { try { if (isLoggedIn) { @@ -53,9 +137,15 @@ const NormalForm = () => { message, }) } - setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin) - setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login)) + setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin && !systemFeatures.ding_talk && !systemFeatures.is_custom_auth2) + setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin || !!systemFeatures.ding_talk || !!systemFeatures.is_custom_auth2) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login)) updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code') + + // Extend: start 只在客户端执行钉钉登录 + if (typeof window !== 'undefined') + await dingTalkLogin(systemFeatures) + // Extend: end + if (isInviteLink) { const checkRes = await invitationCheck({ url: '/activate/check', @@ -171,6 +261,10 @@ const NormalForm = () => {
)} + {/* Extend: start ding_talk login */} + {systemFeatures.ding_talk && ()} + {systemFeatures.is_custom_auth2 && ()} + {/* Extend: end oauth2 login */}
{showORLine && ( diff --git a/web/types/feature.ts b/web/types/feature.ts index bd331d450..3e6eb9ede 100644 --- a/web/types/feature.ts +++ b/web/types/feature.ts @@ -59,6 +59,14 @@ export type SystemFeatures = { allow_email_code_login: boolean allow_email_password_login: boolean } + // Extend: start 钉钉和OAuth2登录字段 + is_custom_auth2: string // extend: Customizing AUTH2 + is_custom_auth2_button: string // extend: Customizing AUTH2 button text + is_custom_auth2_logout: string // extend: AUTH2 logout url + ding_talk_client_id: string // Extend: DingTalk third-party login + ding_talk_corp_id: string // Extend: DingTalk sidebar login + ding_talk: boolean // Extend: switch DingTalk sidebar login + // Extend: end } export const defaultSystemFeatures: SystemFeatures = { @@ -98,6 +106,14 @@ export const defaultSystemFeatures: SystemFeatures = { allow_email_code_login: false, allow_email_password_login: false, }, + // Extend: start 钉钉和OAuth2登录字段默认值 + is_custom_auth2: '', // extend: Customizing AUTH2 + is_custom_auth2_button: '', // extend: Customizing AUTH2 button text + is_custom_auth2_logout: '', // extend: Customizing AUTH2 logout url + ding_talk_client_id: '', // Extend: DingTalk third-party login + ding_talk_corp_id: '', // Extend: DingTalk sidebar login + ding_talk: false, // Extend: switch DingTalk sidebar login + // Extend: end } export enum DatasetAttr {