/** OAuth 授权由发起页持有弹窗;窗口关闭只表示授权结束,身份结果仍须向后端确认。 */ import { navigateTo, useRoute, useRuntimeConfig } from "#app"; import { onBeforeUnmount, ref } from "vue"; import { useApi, useAuth } from "~/lib/auth.js"; import { r as safeRedirect } from "~/lib/auth-redirect.js"; import { n as parseOAuthResult, r as authorizeUrl, t as providers, } from "~/lib/oauth.js"; function useOAuth() { const api = useApi(); const auth = useAuth(); const config = useRuntimeConfig(); const route = useRoute(); const busy = ref(false); const error = ref(""); let mounted = true; let cancel; onBeforeUnmount(() => { mounted = false; cancel?.(); }); function start(provider) { if (!mounted || busy.value) return Promise.resolve(false); error.value = ""; if (typeof provider !== "string" || !/^[a-z][a-z0-9_]*$/i.test(provider)) { error.value = "授权方式无效"; return Promise.resolve(false); } // 必须在点击的同步调用栈创建窗口,等待接口后再打开会触发浏览器弹窗拦截。 const popup = window.open( "about:blank", "_blank", "popup,width=800,height=800", ); if (!popup) { error.value = "授权窗口被浏览器拦截,请允许本站弹窗后重试"; return Promise.resolve(false); } // 保留父页检查 closed 的句柄,同时防止外部授权页反向控制发起页。 popup.opener = null; const attempt = auth.prepareLogin(); const linking = !!auth.token; const redirect = safeRedirect(route.query.redirect); const deadline = Date.now() + 10 * 60 * 1000; busy.value = true; return new Promise((resolve) => { let finished = false; let launched = false; let consuming = false; const isCurrent = () => mounted && !finished && attempt.isCurrent(); function finish(message = "", success = false) { if (finished) return; finished = true; clearInterval(timer); // 外站加载后 opener 已隔离,浏览器不允许父页强关;后端回调或用户自行关闭窗口。 if (!launched && !popup.closed) popup.close(); if (cancel === finish) cancel = undefined; if (mounted) { busy.value = false; if (attempt.isCurrent()) error.value = message; } resolve(success); } function ensureCurrent() { if (!isCurrent()) { finish(); return false; } if (Date.now() >= deadline) { finish("授权已超时,请关闭授权窗口后重新授权"); return false; } return true; } async function consumeResult() { try { const data = await api.get(linking ? "/account" : "/oauth/token"); if (!ensureCurrent()) return; if (linking) { // 已登录关联会清除后端 OAuth 会话,因此通过账户资料确认,不能再次消费登录令牌。 if ( !providers(data.account?.oauth).some( (item) => item.name === provider && item.linked, ) ) throw Error("授权未完成,请重新关联账号"); finish("", true); return; } const result = parseOAuthResult( data, window.location.origin, new URL( String(config.public.apiBase || window.location.origin), window.location.origin, ).origin, ); if (result.kind === "login") { attempt.commit(result.jwt); finish("", true); await navigateTo(redirect); } else if (result.kind === "bind") { const target = new URL(result.url); if (redirect !== "/") target.searchParams.set("redirect", redirect); finish("", true); await navigateTo(target.pathname + target.search + target.hash); } else { throw Error("授权未完成或已过期,请重新授权"); } } catch (cause) { if (ensureCurrent()) finish(cause instanceof Error ? cause.message : "授权失败"); } } // 后端授权状态有效期为十分钟;离页、身份切换、取消与超时统一结束这一次授权。 const timer = setInterval(() => { if (!ensureCurrent()) return; if (popup.closed && !consuming) { if (!launched) return finish(); consuming = true; void consumeResult(); } }, 300); cancel = finish; void (async () => { try { const data = await api.get(`/oauth/${encodeURIComponent(provider)}`); if (!ensureCurrent()) return; if (popup.closed) return finish(); const url = authorizeUrl(data?.url, window.location.origin); if (!url) throw Error("授权地址无效,请联系支持"); popup.location.href = url; launched = true; } catch (cause) { if (ensureCurrent()) finish(cause instanceof Error ? cause.message : "授权失败"); } })(); }); } return { busy, error, start }; } export { useOAuth as t };