fix: 钉钉登录

This commit is contained in:
npc0-hue
2026-01-22 17:47:02 +08:00
parent 7ba4db8888
commit ad3429c410
7 changed files with 154 additions and 31 deletions
@@ -106,7 +106,7 @@ const NormalForm = () => {
<div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('pageTitleForE', { ns: 'login' }) : t('pageTitle', { ns: 'login' })}</h2>
{/*extend: login.welcome*/}
{!systemFeatures.branding.enabled && <p className="body-md-regular mt-2 text-text-tertiary">{t('welcome', { ns: 'login' })}</p>
{!systemFeatures.branding.enabled && <p className="body-md-regular mt-2 text-text-tertiary">{t('welcome', { ns: 'login' })}</p>}
</div>
<div className="relative">
<div className="mt-6 flex flex-col gap-3">
@@ -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<ISelectProps> = ({
onSelect(value)
}
}}>
<div className={classNames('relative')}>
<div className={cn('relative')}>
<div className='group text-gray-800'>
{allowSearch
? <Combobox.Input
@@ -99,7 +99,7 @@ const Select: FC<ISelectProps> = ({
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`)}>
<div className='w-0 grow text-left truncate' title={selectedItem?.name}>{selectedItem?.name}</div>
</Combobox.Button>}
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none group-hover:bg-gray-200" onClick={
@@ -119,7 +119,7 @@ const Select: FC<ISelectProps> = ({
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<ISelectProps> = ({
>
{({ /* active, */ selected }) => (
<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
<span className={cn('block', selected && 'font-normal')}>{item.name}</span>
{selected && (
<span
className={classNames(
className={cn(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
@@ -159,7 +159,7 @@ const SimpleSelect: FC<ISelectProps> = ({
placeholder,
}) => {
const { t } = useTranslation()
const localPlaceholder = placeholder || t('common.placeholder.select')
const localPlaceholder = placeholder || t('placeholder.select', { ns: 'common' })
const [selectedItem, setSelectedItem] = useState<Item | null>(null)
useEffect(() => {
@@ -183,7 +183,7 @@ const SimpleSelect: FC<ISelectProps> = ({
>
<div className={`relative h-9 ${wrapperClassName}`}>
<Listbox.Button className={`w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'} ${className}`}>
<span className={classNames('block truncate text-left', !selectedItem?.name && 'text-gray-400')}>{selectedItem?.name ?? localPlaceholder}</span>
<span className={cn('block truncate text-left', !selectedItem?.name && 'text-gray-400')}>{selectedItem?.name ?? localPlaceholder}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
{selectedItem
? (
@@ -226,10 +226,10 @@ const SimpleSelect: FC<ISelectProps> = ({
>
{({ /* active, */ selected }) => (
<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
<span className={cn('block', selected && 'font-normal')}>{item.name}</span>
{selected && (
<span
className={classNames(
className={cn(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
@@ -264,7 +264,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
}) => {
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<PortalSelectProps> = ({
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)} className='w-full'>
<div
className={`
flex items-center justify-between px-2.5 h-9 rounded-lg border-0 bg-gray-100 text-sm cursor-pointer
flex items-center justify-between px-2.5 h-9 rounded-lg border-0 bg-gray-100 text-sm cursor-pointer
`}
title={selectedItem?.name}
>
+3 -3
View File
@@ -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"
>
<span className={
classNames(
cn(
style.dingIcon,
'mr-2 h-5 w-5',
)
}
/>
<span className="truncate">{t('extend.sidebar.withDingTalk')}</span>
<span className="truncate">{t('sidebar.withDingTalk', { ns: 'extend' })}</span>
</Button>
</a>
</div>
@@ -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 ----------------
}
}
+8 -4
View File
@@ -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"
>
<span className={
classNames(
cn(
style.oauth2Icon,
'mr-2 h-5 w-5',
)
}
/>
<span className="truncate">{t('appOverview.overview.appInfo.settings.sso.label')}</span>
<span className="truncate">{props.title === '' ? t('withSSO', { ns: 'login' }) : props.title}</span>
</Button>
</a>
</div>
+97 -3
View File
@@ -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 = () => {
<SSOAuth protocol={systemFeatures.sso_enforced_for_signin_protocol} />
</div>
)}
{/* Extend: start ding_talk login */}
{systemFeatures.ding_talk && (<DingTalkAuth clientId={systemFeatures.ding_talk_client_id}></DingTalkAuth>)}
{systemFeatures.is_custom_auth2 && (<OAuth2 title={systemFeatures.is_custom_auth2_button}></OAuth2>)}
{/* Extend: end oauth2 login */}
</div>
{showORLine && (
+16
View File
@@ -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 {