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",
|
"axios": "^0.28.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"cropperjs": "^1.6.2",
|
"cropperjs": "^1.6.2",
|
||||||
|
"dingtalk-jsapi": "^2.15.6",
|
||||||
"echarts": "^5.5.0",
|
"echarts": "^5.5.0",
|
||||||
"element-plus": "^2.9.1",
|
"element-plus": "^2.9.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
|||||||
@ -162,6 +162,10 @@ const getQrType: (loading?: Ref<boolean>) => Promise<Result<any>> = (loading) =>
|
|||||||
return get('qr_type', undefined, 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>> = (
|
const getDingCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||||
code,
|
code,
|
||||||
loading
|
loading
|
||||||
@ -169,12 +173,25 @@ const getDingCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<
|
|||||||
return get('dingtalk', { code }, loading)
|
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>> = (
|
const getWecomCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
|
||||||
code,
|
code,
|
||||||
loading
|
loading
|
||||||
) => {
|
) => {
|
||||||
return get('wecom', { 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,
|
getDingCallback,
|
||||||
getQrType,
|
getQrType,
|
||||||
getWecomCallback,
|
getWecomCallback,
|
||||||
postLanguage
|
postLanguage,
|
||||||
|
getDingOauth2Callback,
|
||||||
|
getlarkCallback,
|
||||||
|
getQrSource
|
||||||
}
|
}
|
||||||
|
|||||||
@ -143,6 +143,13 @@ const useUserStore = defineStore({
|
|||||||
return this.profile()
|
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) {
|
async wecomCallback(code: string) {
|
||||||
return UserApi.getWecomCallback(code).then((ok) => {
|
return UserApi.getWecomCallback(code).then((ok) => {
|
||||||
this.token = ok.data
|
this.token = ok.data
|
||||||
@ -150,6 +157,13 @@ const useUserStore = defineStore({
|
|||||||
return this.profile()
|
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() {
|
async logout() {
|
||||||
return UserApi.logout().then(() => {
|
return UserApi.logout().then(() => {
|
||||||
@ -167,6 +181,11 @@ const useUserStore = defineStore({
|
|||||||
return ok.data
|
return ok.data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async getQrSource() {
|
||||||
|
return UserApi.getQrSource().then((ok) => {
|
||||||
|
return ok.data
|
||||||
|
})
|
||||||
|
},
|
||||||
async postUserLanguage(lang: string, loading?: Ref<boolean>) {
|
async postUserLanguage(lang: string, loading?: Ref<boolean>) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
UserApi.postLanguage({ language: lang }, loading)
|
UserApi.postLanguage({ language: lang }, loading)
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { MsgError } from '@/utils/message'
|
||||||
|
|
||||||
export function toThousands(num: any) {
|
export function toThousands(num: any) {
|
||||||
return num?.toString().replace(/\d+/, function (n: any) {
|
return num?.toString().replace(/\d+/, function (n: any) {
|
||||||
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
|
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
|
||||||
@ -113,3 +115,53 @@ export function getNormalizedUrl(url: string) {
|
|||||||
}
|
}
|
||||||
return url
|
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,
|
app_secret: currentPlatform.config.app_secret,
|
||||||
callback_url: defaultCallbackUrl
|
callback_url: defaultCallbackUrl
|
||||||
}
|
}
|
||||||
|
currentPlatform.config.callback_url = `${defaultCallbackUrl}/api/dingtalk`
|
||||||
break
|
break
|
||||||
case 'lark':
|
case 'lark':
|
||||||
currentPlatform.config.callback_url = `${defaultCallbackUrl}/api/feishu`
|
currentPlatform.config.callback_url = `${defaultCallbackUrl}/api/feishu`
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
import { onMounted, ref, defineAsyncComponent } from 'vue'
|
import { onMounted, ref, defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
import platformApi from '@/api/platform-source'
|
import platformApi from '@/api/platform-source'
|
||||||
|
import useStore from '@/stores'
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
key: string
|
key: string
|
||||||
@ -42,11 +43,10 @@ const activeKey = ref('')
|
|||||||
const allConfigs = ref<PlatformConfig[]>([])
|
const allConfigs = ref<PlatformConfig[]>([])
|
||||||
const config = ref<Config>({ app_key: '', app_secret: '' })
|
const config = ref<Config>({ app_key: '', app_secret: '' })
|
||||||
// const logoUrl = ref('')
|
// const logoUrl = ref('')
|
||||||
|
const { user } = useStore()
|
||||||
async function getPlatformInfo() {
|
async function getPlatformInfo() {
|
||||||
try {
|
try {
|
||||||
const res = await platformApi.getPlatformInfo()
|
return await user.getQrSource()
|
||||||
return res.data
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -123,7 +123,7 @@ const initActive = async () => {
|
|||||||
watch(
|
watch(
|
||||||
() => props.config,
|
() => props.config,
|
||||||
(newConfig) => {
|
(newConfig) => {
|
||||||
if (newConfig.app_secret && newConfig.app_key && newConfig.corp_id) {
|
if (newConfig.app_key && newConfig.corp_id) {
|
||||||
isConfigReady.value = true
|
isConfigReady.value = true
|
||||||
initActive()
|
initActive()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,9 +36,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-button size="large" type="primary" class="w-full" @click="login">{{
|
<el-button size="large" type="primary" class="w-full" @click="login"
|
||||||
$t('views.login.buttons.login')
|
>{{ $t('views.login.buttons.login') }}
|
||||||
}}</el-button>
|
</el-button>
|
||||||
<div class="operate-container flex-between mt-12">
|
<div class="operate-container flex-between mt-12">
|
||||||
<!-- <el-button class="register" @click="router.push('/register')" link type="primary">
|
<!-- <el-button class="register" @click="router.push('/register')" link type="primary">
|
||||||
注册
|
注册
|
||||||
@ -103,15 +103,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, onBeforeMount } from 'vue'
|
import { onMounted, ref, onBeforeMount } from 'vue'
|
||||||
import type { LoginRequest } from '@/api/type/user'
|
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 type { FormInstance, FormRules } from 'element-plus'
|
||||||
import useStore from '@/stores'
|
import useStore from '@/stores'
|
||||||
import authApi from '@/api/auth-setting'
|
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 { t, getBrowserLang } from '@/locales'
|
||||||
import QrCodeTab from '@/views/login/components/QrCodeTab.vue'
|
import QrCodeTab from '@/views/login/components/QrCodeTab.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import * as dd from 'dingtalk-jsapi'
|
||||||
|
import { loadScript } from '@/utils/utils'
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { locale } = useI18n({ useScope: 'global' })
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false)
|
||||||
const { user } = useStore()
|
const { user } = useStore()
|
||||||
@ -143,11 +145,14 @@ const modeList = ref<string[]>([''])
|
|||||||
const QrList = ref<any[]>([''])
|
const QrList = ref<any[]>([''])
|
||||||
const loginMode = ref('')
|
const loginMode = ref('')
|
||||||
const showQrCodeTab = ref(false)
|
const showQrCodeTab = ref(false)
|
||||||
|
|
||||||
interface qrOption {
|
interface qrOption {
|
||||||
key: string
|
key: string
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgOptions = ref<qrOption[]>([])
|
const orgOptions = ref<qrOption[]>([])
|
||||||
|
|
||||||
function redirectAuth(authType: string) {
|
function redirectAuth(authType: string) {
|
||||||
if (authType === 'LDAP' || authType === '') {
|
if (authType === 'LDAP' || authType === '') {
|
||||||
return
|
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>
|
</script>
|
||||||
<style lang="scss" scope>
|
<style lang="scss" scope>
|
||||||
.login-gradient-divider {
|
.login-gradient-divider {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user