mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: support three-party password-free login
--story=1018017 --user=王孝刚 【登录认证】-X-Pack 支持三方应用(企业微信、钉钉、飞书)免密登录 https://www.tapd.cn/57709429/s/1669142
This commit is contained in:
parent
7bd1dfbdaa
commit
f6e089daee
|
|
@ -25,6 +25,7 @@
|
|||
"axios": "^0.28.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"cropperjs": "^1.6.2",
|
||||
"dingtalk-jsapi": "^2.15.6",
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.9.1",
|
||||
"file-saver": "^2.0.5",
|
||||
|
|
|
|||
|
|
@ -162,6 +162,10 @@ const getQrType: (loading?: Ref<boolean>) => Promise<Result<any>> = (loading) =>
|
|||
return get('qr_type', undefined, loading)
|
||||
}
|
||||
|
||||
const getQrSource: (loading?: Ref<boolean>) => Promise<Result<any>> = (loading) => {
|
||||
return get('qr_type/source', undefined, loading)
|
||||
}
|
||||
|
||||
const getDingCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||
code,
|
||||
loading
|
||||
|
|
@ -169,12 +173,25 @@ const getDingCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<
|
|||
return get('dingtalk', { code }, loading)
|
||||
}
|
||||
|
||||
const getDingOauth2Callback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||
code,
|
||||
loading
|
||||
) => {
|
||||
return get('dingtalk/oauth2', { code }, loading)
|
||||
}
|
||||
|
||||
const getWecomCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||
code,
|
||||
loading
|
||||
) => {
|
||||
return get('wecom', { code }, loading)
|
||||
}
|
||||
const getlarkCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||
code,
|
||||
loading
|
||||
) => {
|
||||
return get('feishu/oauth2', { code }, loading)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置语言
|
||||
|
|
@ -206,5 +223,8 @@ export default {
|
|||
getDingCallback,
|
||||
getQrType,
|
||||
getWecomCallback,
|
||||
postLanguage
|
||||
postLanguage,
|
||||
getDingOauth2Callback,
|
||||
getlarkCallback,
|
||||
getQrSource
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,13 @@ const useUserStore = defineStore({
|
|||
return this.profile()
|
||||
})
|
||||
},
|
||||
async dingOauth2Callback(code: string) {
|
||||
return UserApi.getDingOauth2Callback(code).then((ok) => {
|
||||
this.token = ok.data
|
||||
localStorage.setItem('token', ok.data)
|
||||
return this.profile()
|
||||
})
|
||||
},
|
||||
async wecomCallback(code: string) {
|
||||
return UserApi.getWecomCallback(code).then((ok) => {
|
||||
this.token = ok.data
|
||||
|
|
@ -150,6 +157,13 @@ const useUserStore = defineStore({
|
|||
return this.profile()
|
||||
})
|
||||
},
|
||||
async larkCallback(code: string) {
|
||||
return UserApi.getlarkCallback(code).then((ok) => {
|
||||
this.token = ok.data
|
||||
localStorage.setItem('token', ok.data)
|
||||
return this.profile()
|
||||
})
|
||||
},
|
||||
|
||||
async logout() {
|
||||
return UserApi.logout().then(() => {
|
||||
|
|
@ -167,6 +181,11 @@ const useUserStore = defineStore({
|
|||
return ok.data
|
||||
})
|
||||
},
|
||||
async getQrSource() {
|
||||
return UserApi.getQrSource().then((ok) => {
|
||||
return ok.data
|
||||
})
|
||||
},
|
||||
async postUserLanguage(lang: string, loading?: Ref<boolean>) {
|
||||
return new Promise((resolve, reject) => {
|
||||
UserApi.postLanguage({ language: lang }, loading)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { MsgError } from '@/utils/message'
|
||||
|
||||
export function toThousands(num: any) {
|
||||
return num?.toString().replace(/\d+/, function (n: any) {
|
||||
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
|
||||
|
|
@ -113,3 +115,53 @@ export function getNormalizedUrl(url: string) {
|
|||
}
|
||||
return url
|
||||
}
|
||||
|
||||
interface LoadScriptOptions {
|
||||
jsId?: string // 自定义脚本 ID
|
||||
forceReload?: boolean // 是否强制重新加载(默认 false)
|
||||
}
|
||||
|
||||
export const loadScript = (url: string, options: LoadScriptOptions = {}): Promise<void> => {
|
||||
const { jsId, forceReload = false } = options
|
||||
const scriptId = jsId || `script-${btoa(url).slice(0, 12)}` // 生成唯一 ID
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 检查是否已存在且无需强制加载
|
||||
const existingScript = document.getElementById(scriptId) as HTMLScriptElement | null
|
||||
if (existingScript && !forceReload) {
|
||||
if (existingScript.src === url) {
|
||||
existingScript.onload = () => resolve() // 复用现有脚本
|
||||
return
|
||||
}
|
||||
// URL 不同则移除旧脚本
|
||||
existingScript.parentElement?.removeChild(existingScript)
|
||||
}
|
||||
|
||||
// 创建新脚本
|
||||
const script = document.createElement('script')
|
||||
script.id = scriptId
|
||||
script.src = url
|
||||
script.async = true // 明确启用异步加载
|
||||
|
||||
// 成功回调
|
||||
script.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
|
||||
// 错误处理(兼容性增强)
|
||||
script.onerror = () => {
|
||||
reject(new Error(`Failed to load script: ${url}`))
|
||||
cleanupScript(script)
|
||||
}
|
||||
|
||||
// 插入到 <head> 确保加载顺序
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
// 清理脚本(可选)
|
||||
const cleanupScript = (script: HTMLScriptElement) => {
|
||||
script.onload = null
|
||||
script.onerror = null
|
||||
script.parentElement?.removeChild(script)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ const open = async (platform: Platform) => {
|
|||
app_secret: currentPlatform.config.app_secret,
|
||||
callback_url: defaultCallbackUrl
|
||||
}
|
||||
currentPlatform.config.callback_url = `${defaultCallbackUrl}/api/dingtalk`
|
||||
break
|
||||
case 'lark':
|
||||
currentPlatform.config.callback_url = `${defaultCallbackUrl}/api/feishu`
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import { onMounted, ref, defineAsyncComponent } from 'vue'
|
||||
|
||||
import platformApi from '@/api/platform-source'
|
||||
import useStore from '@/stores'
|
||||
|
||||
interface Tab {
|
||||
key: string
|
||||
|
|
@ -42,11 +43,10 @@ const activeKey = ref('')
|
|||
const allConfigs = ref<PlatformConfig[]>([])
|
||||
const config = ref<Config>({ app_key: '', app_secret: '' })
|
||||
// const logoUrl = ref('')
|
||||
|
||||
const { user } = useStore()
|
||||
async function getPlatformInfo() {
|
||||
try {
|
||||
const res = await platformApi.getPlatformInfo()
|
||||
return res.data
|
||||
return await user.getQrSource()
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ const initActive = async () => {
|
|||
watch(
|
||||
() => props.config,
|
||||
(newConfig) => {
|
||||
if (newConfig.app_secret && newConfig.app_key && newConfig.corp_id) {
|
||||
if (newConfig.app_key && newConfig.corp_id) {
|
||||
isConfigReady.value = true
|
||||
initActive()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@
|
|||
</div>
|
||||
</el-form>
|
||||
|
||||
<el-button size="large" type="primary" class="w-full" @click="login">{{
|
||||
$t('views.login.buttons.login')
|
||||
}}</el-button>
|
||||
<el-button size="large" type="primary" class="w-full" @click="login"
|
||||
>{{ $t('views.login.buttons.login') }}
|
||||
</el-button>
|
||||
<div class="operate-container flex-between mt-12">
|
||||
<!-- <el-button class="register" @click="router.push('/register')" link type="primary">
|
||||
注册
|
||||
|
|
@ -103,15 +103,17 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref, onBeforeMount } from 'vue'
|
||||
import type { LoginRequest } from '@/api/type/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import useStore from '@/stores'
|
||||
import authApi from '@/api/auth-setting'
|
||||
import { MsgConfirm, MsgSuccess } from '@/utils/message'
|
||||
import { MsgConfirm, MsgError, MsgSuccess } from '@/utils/message'
|
||||
|
||||
import { t, getBrowserLang } from '@/locales'
|
||||
import QrCodeTab from '@/views/login/components/QrCodeTab.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import * as dd from 'dingtalk-jsapi'
|
||||
import { loadScript } from '@/utils/utils'
|
||||
const { locale } = useI18n({ useScope: 'global' })
|
||||
const loading = ref<boolean>(false)
|
||||
const { user } = useStore()
|
||||
|
|
@ -143,11 +145,14 @@ const modeList = ref<string[]>([''])
|
|||
const QrList = ref<any[]>([''])
|
||||
const loginMode = ref('')
|
||||
const showQrCodeTab = ref(false)
|
||||
|
||||
interface qrOption {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const orgOptions = ref<qrOption[]>([])
|
||||
|
||||
function redirectAuth(authType: string) {
|
||||
if (authType === 'LDAP' || authType === '') {
|
||||
return
|
||||
|
|
@ -266,6 +271,83 @@ onBeforeMount(() => {
|
|||
}
|
||||
})
|
||||
})
|
||||
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')
|
||||
|
||||
const handleDingTalk = () => {
|
||||
const code = params.get('corpId')
|
||||
if (code) {
|
||||
dd.runtime.permission.requestAuthCode({ corpId: code }).then((res) => {
|
||||
console.log('DingTalk client request success:', res)
|
||||
user.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) => {
|
||||
user.larkCallback(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) => {
|
||||
user.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
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scope>
|
||||
.login-gradient-divider {
|
||||
|
|
|
|||
Loading…
Reference in New Issue