Skip to content

Commit f6e089d

Browse files
committed
feat: support three-party password-free login
--story=1018017 --user=王孝刚 【登录认证】-X-Pack 支持三方应用(企业微信、钉钉、飞书)免密登录 https://www.tapd.cn/57709429/s/1669142
1 parent 7bd1dfb commit f6e089d

File tree

8 files changed

+185
-10
lines changed

8 files changed

+185
-10
lines changed

ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"axios": "^0.28.0",
2626
"codemirror": "^6.0.1",
2727
"cropperjs": "^1.6.2",
28+
"dingtalk-jsapi": "^2.15.6",
2829
"echarts": "^5.5.0",
2930
"element-plus": "^2.9.1",
3031
"file-saver": "^2.0.5",

ui/src/api/user.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,19 +162,36 @@ const getQrType: (loading?: Ref<boolean>) => Promise<Result<any>> = (loading) =>
162162
return get('qr_type', undefined, loading)
163163
}
164164

165+
const getQrSource: (loading?: Ref<boolean>) => Promise<Result<any>> = (loading) => {
166+
return get('qr_type/source', undefined, loading)
167+
}
168+
165169
const getDingCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
166170
code,
167171
loading
168172
) => {
169173
return get('dingtalk', { code }, loading)
170174
}
171175

176+
const getDingOauth2Callback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
177+
code,
178+
loading
179+
) => {
180+
return get('dingtalk/oauth2', { code }, loading)
181+
}
182+
172183
const getWecomCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
173184
code,
174185
loading
175186
) => {
176187
return get('wecom', { code }, loading)
177188
}
189+
const getlarkCallback: (code: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
190+
code,
191+
loading
192+
) => {
193+
return get('feishu/oauth2', { code }, loading)
194+
}
178195

179196
/**
180197
* 设置语言
@@ -206,5 +223,8 @@ export default {
206223
getDingCallback,
207224
getQrType,
208225
getWecomCallback,
209-
postLanguage
226+
postLanguage,
227+
getDingOauth2Callback,
228+
getlarkCallback,
229+
getQrSource
210230
}

ui/src/stores/modules/user.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,27 @@ const useUserStore = defineStore({
143143
return this.profile()
144144
})
145145
},
146+
async dingOauth2Callback(code: string) {
147+
return UserApi.getDingOauth2Callback(code).then((ok) => {
148+
this.token = ok.data
149+
localStorage.setItem('token', ok.data)
150+
return this.profile()
151+
})
152+
},
146153
async wecomCallback(code: string) {
147154
return UserApi.getWecomCallback(code).then((ok) => {
148155
this.token = ok.data
149156
localStorage.setItem('token', ok.data)
150157
return this.profile()
151158
})
152159
},
160+
async larkCallback(code: string) {
161+
return UserApi.getlarkCallback(code).then((ok) => {
162+
this.token = ok.data
163+
localStorage.setItem('token', ok.data)
164+
return this.profile()
165+
})
166+
},
153167

154168
async logout() {
155169
return UserApi.logout().then(() => {
@@ -167,6 +181,11 @@ const useUserStore = defineStore({
167181
return ok.data
168182
})
169183
},
184+
async getQrSource() {
185+
return UserApi.getQrSource().then((ok) => {
186+
return ok.data
187+
})
188+
},
170189
async postUserLanguage(lang: string, loading?: Ref<boolean>) {
171190
return new Promise((resolve, reject) => {
172191
UserApi.postLanguage({ language: lang }, loading)

ui/src/utils/utils.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { MsgError } from '@/utils/message'
2+
13
export function toThousands(num: any) {
24
return num?.toString().replace(/\d+/, function (n: any) {
35
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
@@ -113,3 +115,53 @@ export function getNormalizedUrl(url: string) {
113115
}
114116
return url
115117
}
118+
119+
interface LoadScriptOptions {
120+
jsId?: string // 自定义脚本 ID
121+
forceReload?: boolean // 是否强制重新加载(默认 false)
122+
}
123+
124+
export const loadScript = (url: string, options: LoadScriptOptions = {}): Promise<void> => {
125+
const { jsId, forceReload = false } = options
126+
const scriptId = jsId || `script-${btoa(url).slice(0, 12)}` // 生成唯一 ID
127+
128+
return new Promise((resolve, reject) => {
129+
// 检查是否已存在且无需强制加载
130+
const existingScript = document.getElementById(scriptId) as HTMLScriptElement | null
131+
if (existingScript && !forceReload) {
132+
if (existingScript.src === url) {
133+
existingScript.onload = () => resolve() // 复用现有脚本
134+
return
135+
}
136+
// URL 不同则移除旧脚本
137+
existingScript.parentElement?.removeChild(existingScript)
138+
}
139+
140+
// 创建新脚本
141+
const script = document.createElement('script')
142+
script.id = scriptId
143+
script.src = url
144+
script.async = true // 明确启用异步加载
145+
146+
// 成功回调
147+
script.onload = () => {
148+
resolve()
149+
}
150+
151+
// 错误处理(兼容性增强)
152+
script.onerror = () => {
153+
reject(new Error(`Failed to load script: ${url}`))
154+
cleanupScript(script)
155+
}
156+
157+
// 插入到 <head> 确保加载顺序
158+
document.head.appendChild(script)
159+
})
160+
}
161+
162+
// 清理脚本(可选)
163+
const cleanupScript = (script: HTMLScriptElement) => {
164+
script.onload = null
165+
script.onerror = null
166+
script.parentElement?.removeChild(script)
167+
}

ui/src/views/authentication/component/EditModal.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ const open = async (platform: Platform) => {
174174
app_secret: currentPlatform.config.app_secret,
175175
callback_url: defaultCallbackUrl
176176
}
177+
currentPlatform.config.callback_url = `${defaultCallbackUrl}/api/dingtalk`
177178
break
178179
case 'lark':
179180
currentPlatform.config.callback_url = `${defaultCallbackUrl}/api/feishu`

ui/src/views/login/components/QrCodeTab.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { onMounted, ref, defineAsyncComponent } from 'vue'
1818
1919
import platformApi from '@/api/platform-source'
20+
import useStore from '@/stores'
2021
2122
interface Tab {
2223
key: string
@@ -42,11 +43,10 @@ const activeKey = ref('')
4243
const allConfigs = ref<PlatformConfig[]>([])
4344
const config = ref<Config>({ app_key: '', app_secret: '' })
4445
// const logoUrl = ref('')
45-
46+
const { user } = useStore()
4647
async function getPlatformInfo() {
4748
try {
48-
const res = await platformApi.getPlatformInfo()
49-
return res.data
49+
return await user.getQrSource()
5050
} catch (error) {
5151
return []
5252
}

ui/src/views/login/components/dingtalkQrCode.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ const initActive = async () => {
123123
watch(
124124
() => props.config,
125125
(newConfig) => {
126-
if (newConfig.app_secret && newConfig.app_key && newConfig.corp_id) {
126+
if (newConfig.app_key && newConfig.corp_id) {
127127
isConfigReady.value = true
128128
initActive()
129129
}

ui/src/views/login/index.vue

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@
3636
</div>
3737
</el-form>
3838

39-
<el-button size="large" type="primary" class="w-full" @click="login">{{
40-
$t('views.login.buttons.login')
41-
}}</el-button>
39+
<el-button size="large" type="primary" class="w-full" @click="login"
40+
>{{ $t('views.login.buttons.login') }}
41+
</el-button>
4242
<div class="operate-container flex-between mt-12">
4343
<!-- <el-button class="register" @click="router.push('/register')" link type="primary">
4444
注册
@@ -103,15 +103,17 @@
103103
<script setup lang="ts">
104104
import { onMounted, ref, onBeforeMount } from 'vue'
105105
import type { LoginRequest } from '@/api/type/user'
106-
import { useRouter } from 'vue-router'
106+
import { useRoute, useRouter } from 'vue-router'
107107
import type { FormInstance, FormRules } from 'element-plus'
108108
import useStore from '@/stores'
109109
import authApi from '@/api/auth-setting'
110-
import { MsgConfirm, MsgSuccess } from '@/utils/message'
110+
import { MsgConfirm, MsgError, MsgSuccess } from '@/utils/message'
111111
112112
import { t, getBrowserLang } from '@/locales'
113113
import QrCodeTab from '@/views/login/components/QrCodeTab.vue'
114114
import { useI18n } from 'vue-i18n'
115+
import * as dd from 'dingtalk-jsapi'
116+
import { loadScript } from '@/utils/utils'
115117
const { locale } = useI18n({ useScope: 'global' })
116118
const loading = ref<boolean>(false)
117119
const { user } = useStore()
@@ -143,11 +145,14 @@ const modeList = ref<string[]>([''])
143145
const QrList = ref<any[]>([''])
144146
const loginMode = ref('')
145147
const showQrCodeTab = ref(false)
148+
146149
interface qrOption {
147150
key: string
148151
value: string
149152
}
153+
150154
const orgOptions = ref<qrOption[]>([])
155+
151156
function redirectAuth(authType: string) {
152157
if (authType === 'LDAP' || authType === '') {
153158
return
@@ -266,6 +271,83 @@ onBeforeMount(() => {
266271
}
267272
})
268273
})
274+
declare const window: any
275+
276+
onMounted(() => {
277+
const route = useRoute()
278+
const currentUrl = ref(route.fullPath)
279+
const params = new URLSearchParams(currentUrl.value.split('?')[1])
280+
const client = params.get('client')
281+
282+
const handleDingTalk = () => {
283+
const code = params.get('corpId')
284+
if (code) {
285+
dd.runtime.permission.requestAuthCode({ corpId: code }).then((res) => {
286+
console.log('DingTalk client request success:', res)
287+
user.dingOauth2Callback(res.code).then(() => {
288+
router.push({ name: 'home' })
289+
})
290+
})
291+
}
292+
}
293+
294+
const handleLark = () => {
295+
const appId = params.get('appId')
296+
const callRequestAuthCode = () => {
297+
window.tt?.requestAuthCode({
298+
appId: appId,
299+
success: (res: any) => {
300+
user.larkCallback(res.code).then(() => {
301+
router.push({ name: 'home' })
302+
})
303+
},
304+
fail: (error: any) => {
305+
MsgError(error)
306+
}
307+
})
308+
}
309+
310+
loadScript('https://lf-scm-cn.feishucdn.com/lark/op/h5-js-sdk-1.5.35.js', {
311+
jsId: 'lark-sdk',
312+
forceReload: true
313+
})
314+
.then(() => {
315+
if (window.tt) {
316+
window.tt.requestAccess({
317+
appID: appId,
318+
scopeList: [],
319+
success: (res: any) => {
320+
user.larkCallback(res.code).then(() => {
321+
router.push({ name: 'home' })
322+
})
323+
},
324+
fail: (error: any) => {
325+
const { errno } = error
326+
if (errno === 103) {
327+
callRequestAuthCode()
328+
}
329+
}
330+
})
331+
} else {
332+
callRequestAuthCode()
333+
}
334+
})
335+
.catch((error) => {
336+
console.error('SDK 加载失败:', error)
337+
})
338+
}
339+
340+
switch (client) {
341+
case 'dingtalk':
342+
handleDingTalk()
343+
break
344+
case 'lark':
345+
handleLark()
346+
break
347+
default:
348+
break
349+
}
350+
})
269351
</script>
270352
<style lang="scss" scope>
271353
.login-gradient-divider {

0 commit comments

Comments
 (0)