158 lines
4.9 KiB
JavaScript
158 lines
4.9 KiB
JavaScript
const BLOCKED_REDIRECT_PATHS = ["/login", "/login-verification", "/setup"];
|
|
|
|
const ASSET_RECOVERY_KEY = 'smail:route-asset-recovery';
|
|
const READ_ONLY_ROUTES = new Set(['home', 'list', 'detail']);
|
|
const RECOVERABLE_ROUTES = new Set([...READ_ONLY_ROUTES, 'editer']);
|
|
|
|
function isRouteAssetLoadError(error) {
|
|
const message = typeof error?.message === 'string' ? error.message : '';
|
|
return /^Unable to preload CSS for /.test(message)
|
|
|| /^Failed to fetch dynamically imported module:/.test(message)
|
|
|| /^error loading dynamically imported module/i.test(message)
|
|
|| /^Importing a module script failed/.test(message)
|
|
|| (error?.name === 'ChunkLoadError' && /^Loading chunk /.test(message));
|
|
}
|
|
|
|
function isReadOnlyRoute(route) {
|
|
return READ_ONLY_ROUTES.has(route?.name);
|
|
}
|
|
|
|
function createRouteAssetRecovery({buildID, getStorage, reload}) {
|
|
let recovering = false;
|
|
|
|
return function recoverRouteAsset(error, to, from, {editing = false, online = true} = {}) {
|
|
if (!isRouteAssetLoadError(error) || !isReadOnlyRoute(from) || editing || !online) {
|
|
return false;
|
|
}
|
|
let recoveryPath = to?.fullPath;
|
|
if (to?.name === 'login' && to.path === '/login') {
|
|
// 旧邮件页登录失效时也可能缺失登录资源,只重建安全的登录目标。
|
|
const query = new URLSearchParams({redirect: getSafeLoginRedirect(to.query?.redirect)});
|
|
recoveryPath = `/login?${query}`;
|
|
} else if (!RECOVERABLE_ROUTES.has(to?.name) || !recoveryPath
|
|
|| getSafeLoginRedirect(recoveryPath, '') !== recoveryPath) {
|
|
return false;
|
|
}
|
|
if (recovering) {
|
|
return true;
|
|
}
|
|
try {
|
|
const storage = getStorage();
|
|
if (!buildID || storage.getItem(ASSET_RECOVERY_KEY) === buildID) {
|
|
return false;
|
|
}
|
|
// 先持久化重试标记;同一标签页的同一构建最多自动恢复一次。
|
|
storage.setItem(ASSET_RECOVERY_KEY, buildID);
|
|
recovering = true;
|
|
reload(recoveryPath);
|
|
return true;
|
|
} catch {
|
|
// 浏览器禁用存储或导航失败时保留手动恢复入口,不能无限刷新。
|
|
recovering = false;
|
|
return false;
|
|
}
|
|
};
|
|
}
|
|
|
|
function decodeRedirect(value) {
|
|
let decoded = value;
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
try {
|
|
const next = decodeURIComponent(decoded);
|
|
if (next === decoded) {
|
|
break;
|
|
}
|
|
decoded = next;
|
|
} catch {
|
|
break;
|
|
}
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
function getSafeLoginRedirect(value, fallback = "/") {
|
|
if (typeof value !== "string") {
|
|
return fallback;
|
|
}
|
|
|
|
const redirect = value.trim();
|
|
if (!redirect.startsWith("/") || redirect.startsWith("//")) {
|
|
return fallback;
|
|
}
|
|
|
|
const decodedRedirect = decodeRedirect(redirect);
|
|
if (!decodedRedirect.startsWith("/") || decodedRedirect.startsWith("//") || decodedRedirect.includes("\\")) {
|
|
return fallback;
|
|
}
|
|
|
|
const decodedPath = decodedRedirect
|
|
.split(/[?#]/, 1)[0]
|
|
.replace(/\/+$/, "")
|
|
.toLowerCase() || "/";
|
|
|
|
if (BLOCKED_REDIRECT_PATHS.some((path) => decodedPath === path || decodedPath.startsWith(`${path}/`))) {
|
|
return fallback;
|
|
}
|
|
|
|
return redirect;
|
|
}
|
|
|
|
function createLoginLocation(redirect) {
|
|
return {
|
|
name: "login",
|
|
query: {
|
|
redirect: getSafeLoginRedirect(redirect)
|
|
}
|
|
};
|
|
}
|
|
|
|
function createSetupLocation(redirect) {
|
|
return {
|
|
name: "setup",
|
|
query: {
|
|
redirect: getSafeLoginRedirect(redirect)
|
|
}
|
|
};
|
|
}
|
|
|
|
function getCurrentLoginRedirect(route) {
|
|
if (route?.name === "login" || route?.name === "login-verification") {
|
|
return getSafeLoginRedirect(route?.query?.redirect);
|
|
}
|
|
return getSafeLoginRedirect(route?.fullPath);
|
|
}
|
|
|
|
function createLoginRedirectHandler(router) {
|
|
let redirectPromise = null;
|
|
|
|
return function redirectToLogin() {
|
|
const currentRoute = router.currentRoute.value;
|
|
if (currentRoute?.name === "login" || currentRoute?.name === "login-verification") {
|
|
return Promise.resolve(false);
|
|
}
|
|
if (redirectPromise) {
|
|
return redirectPromise;
|
|
}
|
|
|
|
const location = createLoginLocation(getCurrentLoginRedirect(currentRoute));
|
|
redirectPromise = Promise.resolve(router.replace(location))
|
|
.then(() => true)
|
|
.finally(() => {
|
|
redirectPromise = null;
|
|
});
|
|
|
|
return redirectPromise;
|
|
};
|
|
}
|
|
|
|
export {
|
|
createRouteAssetRecovery,
|
|
isReadOnlyRoute,
|
|
isRouteAssetLoadError,
|
|
createLoginLocation,
|
|
createLoginRedirectHandler,
|
|
createSetupLocation,
|
|
getCurrentLoginRedirect,
|
|
getSafeLoginRedirect
|
|
};
|