mirror of
https://github.com/docmost/docmost.git
synced 2026-08-24 03:52:12 +10:00
merge main
This commit is contained in:
@@ -74,8 +74,12 @@ function redirectToLogin() {
|
||||
];
|
||||
if (!exemptPaths.some((path) => window.location.pathname.startsWith(path))) {
|
||||
const redirectTo = window.location.pathname;
|
||||
const params = new URLSearchParams({ redirect: redirectTo });
|
||||
window.location.href = `${APP_ROUTE.AUTH.LOGIN}?${params.toString()}`;
|
||||
if (redirectTo === APP_ROUTE.HOME) {
|
||||
window.location.href = APP_ROUTE.AUTH.LOGIN;
|
||||
} else {
|
||||
const params = new URLSearchParams({ redirect: redirectTo });
|
||||
window.location.href = `${APP_ROUTE.AUTH.LOGIN}?${params.toString()}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const APP_ROUTE = {
|
||||
HOME: "/home",
|
||||
SPACES: "/spaces",
|
||||
FAVORITES: "/favorites",
|
||||
SEARCH: "/search",
|
||||
AUTH: {
|
||||
LOGIN: "/login",
|
||||
@@ -12,6 +13,7 @@ const APP_ROUTE = {
|
||||
SELECT_WORKSPACE: "/select",
|
||||
MFA_CHALLENGE: "/login/mfa",
|
||||
MFA_SETUP_REQUIRED: "/login/mfa/setup",
|
||||
VERIFY_EMAIL: "/verify-email",
|
||||
},
|
||||
SETTINGS: {
|
||||
ACCOUNT: {
|
||||
@@ -30,20 +32,38 @@ const APP_ROUTE = {
|
||||
},
|
||||
};
|
||||
|
||||
export function safeRedirectPath(input: unknown): string | null {
|
||||
if (typeof input !== "string") return null;
|
||||
if (input.length === 0 || input.length > 2048) return null;
|
||||
// Reject whitespace, backslash, and any Unicode "Other" category char
|
||||
// (ASCII controls, zero-width space, BOM, bidi marks, etc).
|
||||
if (/[\s\\]|\p{C}/u.test(input)) return null;
|
||||
if (!input.startsWith("/") || input.startsWith("//")) return null;
|
||||
if (input.toLowerCase().includes("://")) return null;
|
||||
if (/^\/[a-z][a-z0-9+\-.]*:/i.test(input)) return null;
|
||||
try {
|
||||
const resolved = new URL(input, window.location.origin);
|
||||
if (resolved.origin !== window.location.origin) return null;
|
||||
return resolved.pathname + resolved.search + resolved.hash;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPostLoginRedirect(): string {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const redirect = params.get("redirect");
|
||||
if (redirect) {
|
||||
try {
|
||||
const resolved = new URL(redirect, window.location.origin);
|
||||
if (resolved.origin === window.location.origin) {
|
||||
return resolved.pathname + resolved.search + resolved.hash;
|
||||
}
|
||||
} catch {
|
||||
// malformed URL, fall through to default
|
||||
}
|
||||
}
|
||||
return APP_ROUTE.HOME;
|
||||
return safeRedirectPath(params.get("redirect")) ?? APP_ROUTE.HOME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `?redirect=` value from the current URL only when it is a safe
|
||||
* same-origin path. Unlike {@link getPostLoginRedirect} this returns `null`
|
||||
* (not `/home`) when no redirect is present, so callers can distinguish
|
||||
* "user came here directly" from "user was bounced from a deep link".
|
||||
*/
|
||||
export function getRedirectParam(): string | null {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return safeRedirectPath(params.get("redirect"));
|
||||
}
|
||||
|
||||
export default APP_ROUTE;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import bytes from "bytes";
|
||||
import { castToBoolean } from "@/lib/utils.tsx";
|
||||
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
|
||||
import { sanitizeUrl } from "@docmost/editor-ext";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -66,7 +67,7 @@ export function getFileUrl(src: string) {
|
||||
if (src.startsWith("/files/")) {
|
||||
return getBackendUrl() + src;
|
||||
}
|
||||
return src;
|
||||
return sanitizeUrl(src);
|
||||
}
|
||||
|
||||
export function getFileUploadSizeLimit() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { validate as isValidUUID } from "uuid";
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
import { IconFileDescription } from "@tabler/icons-react";
|
||||
import { ReactNode } from "react";
|
||||
import { TFunction } from "i18next";
|
||||
@@ -87,13 +86,30 @@ export function capitalizeFirstChar(string: string) {
|
||||
export function getPageIcon(icon: string, size = 18): string | ReactNode {
|
||||
return (
|
||||
icon || (
|
||||
<ActionIcon variant="transparent" color="gray" size={size}>
|
||||
<IconFileDescription size={size} />
|
||||
</ActionIcon>
|
||||
<IconFileDescription
|
||||
size={size}
|
||||
color="var(--mantine-color-gray-6)"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export const normalizeUrl = (url: string): string => {
|
||||
if (!url) return url;
|
||||
if (url.startsWith("/") || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) return url;
|
||||
return `https://${url}`;
|
||||
};
|
||||
|
||||
const _isApple = /mac|iphone|ipad|ipod/i.test(navigator.platform ?? "");
|
||||
|
||||
/// Cmd key on Apple devices, Ctrl key everywhere else
|
||||
export function platformModifierKey(event: KeyboardEvent): boolean {
|
||||
return _isApple ? event.metaKey : event.ctrlKey;
|
||||
}
|
||||
|
||||
export const platformModifierLabel = _isApple ? "⌘" : "Ctrl";
|
||||
|
||||
export function castToBoolean(value: unknown): boolean {
|
||||
if (value == null) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user