mirror of
https://github.com/YFGaia/dify-plus.git
synced 2026-06-04 10:14:00 +08:00
fix: 钉钉登录
This commit is contained in:
@@ -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,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 ----------------
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user