Introduce origin migration handling with outdated route guards, redirect logic, and migration-related error views.

- Add `originMigration.js` for handling allowed origins, standalone contexts, and redirect routes.
- Create `OutdatedGateway.vue` and `OutdatedInstallation.vue` error views with timer-based redirection and user instructions.
- Update router to include `/outdated-installation` and `/outdated-gateway` paths.
- Implement unit tests for migration utility methods and route guards.
- Remove debug `console.log` statements across multiple components.
This commit is contained in:
Jeppe Bundgaard
2026-03-19 10:44:10 +01:00
parent 7e9489573a
commit efed5ac557
24 changed files with 566 additions and 27 deletions
+113
View File
@@ -0,0 +1,113 @@
export const OUTDATED_INSTALLATION_ROUTE_NAME = 'outdated-installation';
export const OUTDATED_GATEWAY_ROUTE_NAME = 'outdated-gateway';
const EXEMPT_ROUTE_NAMES = new Set([
OUTDATED_INSTALLATION_ROUTE_NAME,
OUTDATED_GATEWAY_ROUTE_NAME,
]);
const normalizeAllowedOrigins = (allowedOrigins = []) => {
return new Set(
allowedOrigins
.map((origin) => {
try {
return new URL(origin).origin;
} catch (_error) {
return origin;
}
})
.filter(Boolean),
);
};
export const isOriginAllowed = (currentOrigin, allowedOrigins = []) => {
if (!currentOrigin) return true;
const allowedOriginSet = normalizeAllowedOrigins(allowedOrigins);
return allowedOriginSet.has(currentOrigin);
};
export const isStandaloneContext = (win) => {
const resolvedWindow = win || (typeof window !== 'undefined' ? window : undefined);
if (!resolvedWindow) return false;
const iosStandalone = resolvedWindow.navigator?.standalone === true;
const mediaStandalone = typeof resolvedWindow.matchMedia === 'function'
&& resolvedWindow.matchMedia('(display-mode: standalone)').matches;
return iosStandalone || mediaStandalone;
};
export const shouldBypassOriginGate = (route) => {
return EXEMPT_ROUTE_NAMES.has(route?.name);
};
export const resolveOriginMigrationRoute = ({
to,
currentOrigin,
allowedOrigins,
standalone,
}) => {
if (shouldBypassOriginGate(to)) {
return null;
}
if (isOriginAllowed(currentOrigin, allowedOrigins)) {
return null;
}
if (standalone) {
return { name: OUTDATED_INSTALLATION_ROUTE_NAME };
}
return {
name: OUTDATED_GATEWAY_ROUTE_NAME,
query: {
redirect: to?.fullPath || '/',
},
};
};
export const sanitizeRedirectPath = (redirectPath) => {
if (typeof redirectPath !== 'string') return '/';
if (!redirectPath.startsWith('/')) return '/';
return redirectPath;
};
export const buildMigrationUrl = (migrationOrigin, redirectPath) => {
const safePath = sanitizeRedirectPath(redirectPath);
try {
return new URL(safePath, migrationOrigin).toString();
} catch (_error) {
return migrationOrigin;
}
};
export const startRedirectCountdown = ({
seconds = 5,
onTick,
onRedirect,
setIntervalFn = setInterval,
clearIntervalFn = clearInterval,
setTimeoutFn = setTimeout,
clearTimeoutFn = clearTimeout,
}) => {
let remainingSeconds = seconds;
const intervalId = setIntervalFn(() => {
remainingSeconds = Math.max(remainingSeconds - 1, 0);
if (typeof onTick === 'function') {
onTick(remainingSeconds);
}
if (remainingSeconds === 0) {
clearIntervalFn(intervalId);
}
}, 1000);
const timeoutId = setTimeoutFn(() => {
if (typeof onRedirect === 'function') {
onRedirect();
}
}, seconds * 1000);
return () => {
clearIntervalFn(intervalId);
clearTimeoutFn(timeoutId);
};
};