修复钉钉用户自动免登与信息同步

This commit is contained in:
kx_guo 2026-09-13 16:28:00 +08:00
parent 73f574add4
commit 5582f168c2
4 changed files with 66 additions and 12 deletions

View File

@ -2,6 +2,7 @@
import { onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted } from "vue";
import { RouterView } from "vue-router"; import { RouterView } from "vue-router";
import { isDingTalkContainer } from "@/api/dingtalk";
import router from "@/router"; import router from "@/router";
import { useAuthStore } from "@/stores/auth.store"; import { useAuthStore } from "@/stores/auth.store";
@ -17,9 +18,28 @@ function handleAuthExpired() {
} }
} }
onMounted(() => async function syncDingTalkUser() {
window.addEventListener("risk-auth-expired", handleAuthExpired), if (
); !isDingTalkContainer() ||
!auth.user?.dingtalkId ||
auth.user.dingtalkId.startsWith("local-")
) {
return;
}
try {
await auth.dingTalkLogin(true);
} catch {
// A stale token must remain usable if DingTalk is temporarily unavailable.
}
}
onMounted(async () => {
window.addEventListener("risk-auth-expired", handleAuthExpired);
// Restore /auth/me first: on a hard refresh auth.user starts as null, so a
// synchronous check would skip the DingTalk profile resync entirely.
if (auth.token && !auth.user) await auth.restore();
await syncDingTalkUser();
});
onUnmounted(() => onUnmounted(() =>
window.removeEventListener("risk-auth-expired", handleAuthExpired), window.removeEventListener("risk-auth-expired", handleAuthExpired),
); );

View File

@ -3,14 +3,43 @@ import dd from "dingtalk-jsapi";
import { getDingTalkConfig, loginWithDingTalk } from "@/api/auth"; import { getDingTalkConfig, loginWithDingTalk } from "@/api/auth";
import type { AuthTokenData } from "@/types/auth"; import type { AuthTokenData } from "@/types/auth";
declare global {
interface Window {
dingtalk?: { platform?: { invokeAPI?: unknown } };
DingTalkJSBridge?: unknown;
}
}
export function isDingTalkContainer() { export function isDingTalkContainer() {
return ( if (typeof window === "undefined" || typeof navigator === "undefined")
typeof navigator !== "undefined" && /DingTalk/i.test(navigator.userAgent) return false;
const userAgent =
navigator.userAgent ||
(navigator as Navigator & { swuserAgent?: string }).swuserAgent ||
"";
return Boolean(
/DingTalk|dingtalk-win|dd-web/i.test(userAgent) ||
window.DingTalkJSBridge ||
window.dingtalk?.platform?.invokeAPI ||
// DingTalk desktop can expose its container identity through window.name
// even when the embedded browser omits DingTalk from userAgent.
/dingtalk/i.test(window.name || ""),
); );
} }
export async function waitForDingTalkContainer(timeoutMs = 5000) {
if (isDingTalkContainer()) return true;
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
await new Promise((resolve) => window.setTimeout(resolve, 100));
if (isDingTalkContainer()) return true;
}
return false;
}
export async function authenticateWithDingTalk(): Promise<AuthTokenData> { export async function authenticateWithDingTalk(): Promise<AuthTokenData> {
if (!isDingTalkContainer()) throw new Error("请从钉钉工作台打开此应用"); if (!(await waitForDingTalkContainer()))
throw new Error("请从钉钉工作台打开此应用");
const configResponse = await getDingTalkConfig(); const configResponse = await getDingTalkConfig();
const config = configResponse.data.data; const config = configResponse.data.data;
if (!config) throw new Error("钉钉免登配置不完整"); if (!config) throw new Error("钉钉免登配置不完整");

View File

@ -9,7 +9,7 @@ import {
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { isDingTalkContainer } from "@/api/dingtalk"; import { waitForDingTalkContainer } from "@/api/dingtalk";
import { useAuthStore } from "@/stores/auth.store"; import { useAuthStore } from "@/stores/auth.store";
const auth = useAuthStore(); const auth = useAuthStore();
@ -19,6 +19,7 @@ const useDevelopmentLogin = ref(true);
const username = ref("local-admin"); const username = ref("local-admin");
const password = ref("risk-admin-2026"); const password = ref("risk-admin-2026");
const autoDingTalkAttempted = ref(false); const autoDingTalkAttempted = ref(false);
const autoDingTalk = ref(false);
// 登录后的统一落点是智能对话页,避免从其他受保护页面跳转到登录后 // 登录后的统一落点是智能对话页,避免从其他受保护页面跳转到登录后
// 又回到原页面,保证用户进入系统后先看到对话工作台。 // 又回到原页面,保证用户进入系统后先看到对话工作台。
@ -49,11 +50,14 @@ async function submitDingTalk() {
} }
} }
onMounted(() => { onMounted(async () => {
if (!isDingTalkContainer() || autoDingTalkAttempted.value) return; if (autoDingTalkAttempted.value) return;
const inDingTalk = await waitForDingTalkContainer();
if (!inDingTalk || autoDingTalkAttempted.value) return;
autoDingTalkAttempted.value = true; autoDingTalkAttempted.value = true;
autoDingTalk.value = true;
mode.value = "dingtalk"; mode.value = "dingtalk";
void submitDingTalk(); await submitDingTalk();
}); });
</script> </script>
@ -144,6 +148,7 @@ onMounted(() => {
}} }}
</p> </p>
<button <button
v-if="!autoDingTalk || auth.error"
class="login-submit" class="login-submit"
type="button" type="button"
:disabled="auth.loading" :disabled="auth.loading"

View File

@ -61,7 +61,7 @@ export const useAuthStore = defineStore("auth", () => {
} }
} }
async function dingTalkLogin() { async function dingTalkLogin(preserveSessionOnFailure = false) {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
try { try {
@ -69,7 +69,7 @@ export const useAuthStore = defineStore("auth", () => {
saveToken(response.accessToken); saveToken(response.accessToken);
user.value = response.user; user.value = response.user;
} catch (reason) { } catch (reason) {
clear(); if (!preserveSessionOnFailure) clear();
error.value = reason instanceof Error ? reason.message : "钉钉免登失败"; error.value = reason instanceof Error ? reason.message : "钉钉免登失败";
throw reason; throw reason;
} finally { } finally {