修复通用 OAuth 弹窗登录与账号关联并隔离迟到结果
This commit is contained in:
1 parent
d330ad01cc
commit
e27f08e36e
5 files changed
+690
-78
No files matched your search
+152
-40
@@ -1,45 +1,157 @@
|
||||
/** OAuth 授权发起器:校验提供商和返回 URL,再整页跳转;不在前端构造第三方授权参数。 */
|
||||
import { onBeforeUnmount } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { useApi } from "~/lib/auth.js";
|
||||
import { r as oauth_r } from "~/lib/oauth.js";
|
||||
function i() {
|
||||
let i = useApi(),
|
||||
a = ref(!1),
|
||||
o = ref(``),
|
||||
s = !0;
|
||||
/** 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(() => {
|
||||
s = !1;
|
||||
mounted = false;
|
||||
cancel?.();
|
||||
});
|
||||
// busy 防止重复发起,卸载标记防止用户离开页面后被迟到的授权响应再次跳走。
|
||||
async function c(e) {
|
||||
if (s && !a.value) {
|
||||
if (((o.value = ``), !/^[a-z][a-z0-9_]*$/i.test(e))) {
|
||||
o.value = `授权方式无效`;
|
||||
return;
|
||||
}
|
||||
a.value = !0;
|
||||
try {
|
||||
let t = await i.get(`/oauth/${encodeURIComponent(e)}`);
|
||||
if (!s) return;
|
||||
let n = oauth_r(t?.url, window.location.origin);
|
||||
if (!n) throw Error(`授权地址无效,请联系支持`);
|
||||
try {
|
||||
// 仅记录当前标签页曾发起授权,不存储 OAuth 凭证或 JWT。
|
||||
sessionStorage.setItem(`console-oauth-pending`, `1`);
|
||||
} catch {}
|
||||
window.location.assign(n);
|
||||
} catch (e) {
|
||||
s && (o.value = e instanceof Error ? e.message : `授权失败`);
|
||||
} finally {
|
||||
s && (a.value = !1);
|
||||
}
|
||||
|
||||
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: a,
|
||||
error: o,
|
||||
start: c,
|
||||
};
|
||||
|
||||
return { busy, error, start };
|
||||
}
|
||||
export { i as t };
|
||||
|
||||
export { useOAuth as t };
|
||||
Reference in new issue
Block a user