diff --git a/ui/src/api/system-settings/auth-setting.ts b/ui/src/api/system-settings/auth-setting.ts index 5f71b1b37..b8be07daf 100644 --- a/ui/src/api/system-settings/auth-setting.ts +++ b/ui/src/api/system-settings/auth-setting.ts @@ -50,11 +50,19 @@ const getLoginAuthSetting: (loading?: Ref) => Promise> = (l return get(`login/auth/setting`, undefined, loading) } +/** + * 获取认证设置 + */ +const getLoginViewAuthSetting: (auth_type: string, loading?: Ref) => Promise> = (auth_type, loading) => { + return get(`login/${prefix}/${auth_type}/detail`, undefined, loading) +} + export default { getAuthSetting, postAuthSetting, putAuthSetting, putLoginSetting, getLoginSetting, - getLoginAuthSetting + getLoginAuthSetting, + getLoginViewAuthSetting } diff --git a/ui/src/views/chat/user-login/index.vue b/ui/src/views/chat/user-login/index.vue index 0b30602f7..fac09fedf 100644 --- a/ui/src/views/chat/user-login/index.vue +++ b/ui/src/views/chat/user-login/index.vue @@ -66,6 +66,7 @@ size="large" class="input-item" v-model="loginForm.username" + @blur="handleUsernameBlur(loginForm.username)" :placeholder="$t('views.login.loginForm.username.placeholder')" > @@ -84,7 +85,7 @@ -
+
@@ -212,6 +213,7 @@ const loginForm = ref({ captcha: '', }) +const max_attempts = ref(1) // 声明为 ref const rules = ref>({ username: [ { @@ -253,20 +255,30 @@ const loginHandle = () => { params: {accessToken: chatUser.accessToken}, query: route.query, }) + localStorage.removeItem('chat_' + loginForm.value.username) + }).catch(() => { + const username = loginForm.value.username + localStorage.setItem('chat_' + username, String(Number(localStorage.getItem('chat_' + username) || '0') + 1)) + loading.value = false + loginForm.value.username = '' + loginForm.value.password = '' + const timestampKey = `${username}_chat_first_fail_timestamp` + if (!localStorage.getItem(timestampKey)) { + localStorage.setItem(timestampKey, Date.now().toString()) + } }) } }) } -function makeCode(userrname?: string) { - loginApi.getCaptcha(userrname).then((res: any) => { +function makeCode(username?: string) { + loginApi.getCaptcha(username).then((res: any) => { identifyCode.value = res.data.captcha }) } onBeforeMount(() => { locale.value = chatUser.getLanguage() - makeCode() }) const modeList = ref([]) @@ -365,7 +377,59 @@ function changeMode(val: string) { loginFormRef.value?.clearValidate() } +const showCaptcha = computed(() => { + // -1 表示一直不显示 + if (max_attempts.value === -1) { + return false + } + + // 0 表示一直显示 + if (max_attempts.value === 0) { + return true + } + + // 大于 0,根据登录失败次数决定 + const username = loginForm.value.username?.trim() + if (!username) { + return false // 没有输入用户名时不显示 + } + + const timestampKey = `${username}_chat_first_fail_timestamp` + const firstFailTimestamp = localStorage.getItem(timestampKey) + + if (firstFailTimestamp) { + const expirationTime = 60 * 60 * 1000 // 10分钟毫秒数 + if (Date.now() - parseInt(firstFailTimestamp) > expirationTime) { + // 过期则清除记录 + localStorage.removeItem('chat_' + username) + localStorage.removeItem(timestampKey) + return false + } + } else { + // 如果没有时间戳但有失败次数,可能是旧数据,清除失败次数 + const failCount = Number(localStorage.getItem('chat_' + username) || '0') + if (failCount > 0) { + localStorage.removeItem('chat_' + username) + return false + } + } + + const failCount = Number(localStorage.getItem('chat_' + username) || '0') + console.log('failCount', failCount) + + return failCount >= max_attempts.value +}) + +function handleUsernameBlur(username: string) { + if (showCaptcha.value) { + makeCode(username) + } +} + onBeforeMount(() => { + if (chatUser.chat_profile?.max_attempts) { + max_attempts.value = chatUser.chat_profile.max_attempts + } if (chatUser.chat_profile?.login_value) { modeList.value = chatUser.chat_profile.login_value if (modeList.value.includes('LOCAL')) { diff --git a/ui/src/views/login/index.vue b/ui/src/views/login/index.vue index b8b4b1f57..04d8293fb 100644 --- a/ui/src/views/login/index.vue +++ b/ui/src/views/login/index.vue @@ -287,245 +287,254 @@ function handleUsernameBlur(username: string) { } onBeforeMount(() => { - authApi.getLoginAuthSetting().then((res) => { - if (Object.keys(res.data).length > 0) { - authSetting.value = res.data; + user.asyncGetProfile().then((res) => { + // 企业版和专业版:第三方登录 + if (user.isPE() || user.isEE()) { + authApi.getLoginAuthSetting().then((res) => { + if (Object.keys(res.data).length > 0) { + authSetting.value = res.data; + } else { + authSetting.value = { + max_attempts: 1, + default_value: 'password', + } + } + const params = route.query + if (params.login_mode !== 'manual') { + const defaultMode = authSetting.value.default_value + if (['lark', 'wecom', 'dingtalk'].includes(defaultMode)) { + changeMode('QR_CODE', false) + defaultQrTab.value = defaultMode + } else { + changeMode(defaultMode, false) + } + } + }) } else { authSetting.value = { max_attempts: 1, default_value: 'password', } } - const params = route.query - if (params.login_mode !== 'manual') { - const defaultMode = authSetting.value.default_value - if (['lark', 'wecom', 'dingtalk'].includes(defaultMode)) { - changeMode('QR_CODE', false) - defaultQrTab.value = defaultMode + }) + + const modeList = ref(['']) + const QrList = ref(['']) + const loginMode = ref('') + const showQrCodeTab = ref(false) + + interface qrOption { + key: string + value: string + } + + const orgOptions = ref([]) + + function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0 + const v = c === 'x' ? r : (r & 0x3) | 0x8 + return v.toString(16) + }) + } + + const newDefaultSlogan = computed(() => { + const default_login = '强大易用的企业级智能体平台' + if (!theme.themeInfo?.slogan || default_login == theme.themeInfo?.slogan) { + return t('theme.defaultSlogan') + } else { + return theme.themeInfo?.slogan + } + }) + + function redirectAuth(authType: string, needMessage: boolean = true) { + if (authType === 'LDAP' || authType === '') { + return + } + authApi.getLoginViewAuthSetting(authType, loading).then((res: any) => { + if (!res.data || !res.data.config) { + return + } + + const config = res.data.config + // 构造带查询参数的redirectUrl + const redirectUrl = `${config.redirectUrl}` + let url + if (authType === 'CAS') { + url = config.ldpUri + url += + url.indexOf('?') !== -1 + ? `&service=${encodeURIComponent(redirectUrl)}` + : `?service=${encodeURIComponent(redirectUrl)}` + } else if (authType === 'OIDC') { + const scope = config.scope || 'openid+profile+email' + url = `${config.authEndpoint}?client_id=${config.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}` + if (config.state) { + url += `&state=${config.state}` + } + } else if (authType === 'OAuth2') { + url = `${config.authEndpoint}?client_id=${config.clientId}&response_type=code&redirect_uri=${redirectUrl}&state=${uuidv4()}` + if (config.scope) { + url += `&scope=${config.scope}` + } + } + if (!url) { + return + } + if (needMessage) { + MsgConfirm(t('views.login.jump_tip'), '', { + confirmButtonText: t('views.login.jump'), + cancelButtonText: t('common.cancel'), + confirmButtonClass: '', + }) + .then(() => { + window.location.href = url + }) + .catch(() => { + }) } else { - changeMode(defaultMode, false) + console.log('url', url) + window.location.href = url } - } - }) -}) - -const modeList = ref(['']) -const QrList = ref(['']) -const loginMode = ref('') -const showQrCodeTab = ref(false) - -interface qrOption { - key: string - value: string -} - -const orgOptions = ref([]) - -function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = (Math.random() * 16) | 0 - const v = c === 'x' ? r : (r & 0x3) | 0x8 - return v.toString(16) - }) -} - -const newDefaultSlogan = computed(() => { - const default_login = '强大易用的企业级智能体平台' - if (!theme.themeInfo?.slogan || default_login == theme.themeInfo?.slogan) { - return t('theme.defaultSlogan') - } else { - return theme.themeInfo?.slogan + }) } -}) -function redirectAuth(authType: string, needMessage: boolean = true) { - if (authType === 'LDAP' || authType === '') { - return - } - authApi.getAuthSetting(authType, loading).then((res: any) => { - if (!res.data || !res.data.config) { + function changeMode(val: string, needMessage: boolean = true) { + loginMode.value = val === 'LDAP' ? val : '' + if (val === 'QR_CODE') { + loginMode.value = val + showQrCodeTab.value = true return } - - const config = res.data.config - // 构造带查询参数的redirectUrl - const redirectUrl = `${config.redirectUrl}` - let url - if (authType === 'CAS') { - url = config.ldpUri - url += - url.indexOf('?') !== -1 - ? `&service=${encodeURIComponent(redirectUrl)}` - : `?service=${encodeURIComponent(redirectUrl)}` - } else if (authType === 'OIDC') { - const scope = config.scope || 'openid+profile+email' - url = `${config.authEndpoint}?client_id=${config.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}` - if (config.state) { - url += `&state=${config.state}` - } - } else if (authType === 'OAuth2') { - url = `${config.authEndpoint}?client_id=${config.clientId}&response_type=code&redirect_uri=${redirectUrl}&state=${uuidv4()}` - if (config.scope) { - url += `&scope=${config.scope}` - } + showQrCodeTab.value = false + loginForm.value = { + username: '', + password: '', + captcha: '', } - if (!url) { - return - } - if (needMessage) { - MsgConfirm(t('views.login.jump_tip'), '', { - confirmButtonText: t('views.login.jump'), - cancelButtonText: t('common.cancel'), - confirmButtonClass: '', - }) - .then(() => { - window.location.href = url - }) - .catch(() => { - }) - } else { - console.log('url', url) - window.location.href = url - } - }) -} - -function changeMode(val: string, needMessage: boolean = true) { - loginMode.value = val === 'LDAP' ? val : '' - if (val === 'QR_CODE') { - loginMode.value = val - showQrCodeTab.value = true - return + redirectAuth(val, needMessage) + loginFormRef.value?.clearValidate() } - showQrCodeTab.value = false - loginForm.value = { - username: '', - password: '', - captcha: '', - } - redirectAuth(val, needMessage) - loginFormRef.value?.clearValidate() -} -onBeforeMount(() => { - loading.value = true - user.asyncGetProfile().then((res) => { - // 企业版和专业版:第三方登录 - if (user.isPE() || user.isEE()) { - login - .getAuthType() - .then((res) => { - //如果结果包含LDAP,把LDAP放在第一个 - const ldapIndex = res.indexOf('LDAP') - if (ldapIndex !== -1) { - const [ldap] = res.splice(ldapIndex, 1) - res.unshift(ldap) - } - modeList.value = [...modeList.value, ...res] - }) - .finally(() => (loading.value = false)) - login - .getQrType() - .then((res) => { - if (res.length > 0) { - modeList.value = ['QR_CODE', ...modeList.value] - QrList.value = res - QrList.value.forEach((item) => { - orgOptions.value.push({ - key: item, - value: - item === 'wecom' - ? t('views.system.authentication.scanTheQRCode.wecom') - : item === 'dingtalk' - ? t('views.system.authentication.scanTheQRCode.dingtalk') - : t('views.system.authentication.scanTheQRCode.lark'), + onBeforeMount(() => { + loading.value = true + user.asyncGetProfile().then((res) => { + // 企业版和专业版:第三方登录 + if (user.isPE() || user.isEE()) { + login + .getAuthType() + .then((res) => { + //如果结果包含LDAP,把LDAP放在第一个 + const ldapIndex = res.indexOf('LDAP') + if (ldapIndex !== -1) { + const [ldap] = res.splice(ldapIndex, 1) + res.unshift(ldap) + } + modeList.value = [...modeList.value, ...res] + }) + .finally(() => (loading.value = false)) + login + .getQrType() + .then((res) => { + if (res.length > 0) { + modeList.value = ['QR_CODE', ...modeList.value] + QrList.value = res + QrList.value.forEach((item) => { + orgOptions.value.push({ + key: item, + value: + item === 'wecom' + ? t('views.system.authentication.scanTheQRCode.wecom') + : item === 'dingtalk' + ? t('views.system.authentication.scanTheQRCode.dingtalk') + : t('views.system.authentication.scanTheQRCode.lark'), + }) }) - }) - } - }) - .finally(() => (loading.value = false)) - } else { - loading.value = false - } + } + }) + .finally(() => (loading.value = false)) + } else { + loading.value = false + } + }) }) -}) -declare const window: any + declare const window: any -onMounted(() => { - const route = useRoute() - const currentUrl = ref(route.fullPath) - const params = new URLSearchParams(currentUrl.value.split('?')[1]) - const client = params.get('client') + onMounted(() => { + const route = useRoute() + const currentUrl = ref(route.fullPath) + const params = new URLSearchParams(currentUrl.value.split('?')[1]) + const client = params.get('client') - const handleDingTalk = () => { - const code = params.get('corpId') - if (code) { - dd.runtime.permission.requestAuthCode({corpId: code}).then((res) => { - console.log('DingTalk client request success:', res) - login.dingOauth2Callback(res.code).then(() => { - router.push({name: 'home'}) - }) - }) - } - } - - const handleLark = () => { - const appId = params.get('appId') - const callRequestAuthCode = () => { - window.tt?.requestAuthCode({ - appId: appId, - success: (res: any) => { - login.larkCallback(res.code).then(() => { + const handleDingTalk = () => { + const code = params.get('corpId') + if (code) { + dd.runtime.permission.requestAuthCode({corpId: code}).then((res) => { + console.log('DingTalk client request success:', res) + login.dingOauth2Callback(res.code).then(() => { router.push({name: 'home'}) }) - }, - fail: (error: any) => { - MsgError(error) - }, - }) + }) + } } - loadScript('https://lf-scm-cn.feishucdn.com/lark/op/h5-js-sdk-1.5.35.js', { - jsId: 'lark-sdk', - forceReload: true, - }) - .then(() => { - if (window.tt) { - window.tt.requestAccess({ - appID: appId, - scopeList: [], - success: (res: any) => { - login.larkCallback(res.code).then(() => { - router.push({name: 'home'}) - }) - }, - fail: (error: any) => { - const {errno} = error - if (errno === 103) { - callRequestAuthCode() - } - }, - }) - } else { - callRequestAuthCode() - } - }) - .catch((error) => { - console.error('SDK 加载失败:', error) - }) - } + const handleLark = () => { + const appId = params.get('appId') + const callRequestAuthCode = () => { + window.tt?.requestAuthCode({ + appId: appId, + success: (res: any) => { + login.larkCallback(res.code).then(() => { + router.push({name: 'home'}) + }) + }, + fail: (error: any) => { + MsgError(error) + }, + }) + } - switch (client) { - case 'dingtalk': - handleDingTalk() - break - case 'lark': - handleLark() - break - default: - break - } -}) + loadScript('https://lf-scm-cn.feishucdn.com/lark/op/h5-js-sdk-1.5.35.js', { + jsId: 'lark-sdk', + forceReload: true, + }) + .then(() => { + if (window.tt) { + window.tt.requestAccess({ + appID: appId, + scopeList: [], + success: (res: any) => { + login.larkCallback(res.code).then(() => { + router.push({name: 'home'}) + }) + }, + fail: (error: any) => { + const {errno} = error + if (errno === 103) { + callRequestAuthCode() + } + }, + }) + } else { + callRequestAuthCode() + } + }) + .catch((error) => { + console.error('SDK 加载失败:', error) + }) + } + + switch (client) { + case 'dingtalk': + handleDingTalk() + break + case 'lark': + handleLark() + break + default: + break + } + })