+ {props.isShare && (
+
+ {t("Table of contents")}
+
+ )}
+
{links.map((item, idx) => (
component="button"
diff --git a/apps/client/src/features/editor/readonly-page-editor.tsx b/apps/client/src/features/editor/readonly-page-editor.tsx
new file mode 100644
index 00000000..bd6b9f6b
--- /dev/null
+++ b/apps/client/src/features/editor/readonly-page-editor.tsx
@@ -0,0 +1,67 @@
+import "@/features/editor/styles/index.css";
+import React, { useMemo } from "react";
+import { EditorProvider } from "@tiptap/react";
+import { mainExtensions } from "@/features/editor/extensions/extensions";
+import { Document } from "@tiptap/extension-document";
+import { Heading } from "@tiptap/extension-heading";
+import { Text } from "@tiptap/extension-text";
+import { Placeholder } from "@tiptap/extension-placeholder";
+import { useAtom } from "jotai/index";
+import {
+ pageEditorAtom,
+ readOnlyEditorAtom,
+} from "@/features/editor/atoms/editor-atoms.ts";
+import { Editor } from "@tiptap/core";
+
+interface PageEditorProps {
+ title: string;
+ content: any;
+}
+
+export default function ReadonlyPageEditor({
+ title,
+ content,
+}: PageEditorProps) {
+ const [, setReadOnlyEditor] = useAtom(readOnlyEditorAtom);
+
+ const extensions = useMemo(() => {
+ return [...mainExtensions];
+ }, []);
+
+ const titleExtensions = [
+ Document.extend({
+ content: "heading",
+ }),
+ Heading,
+ Text,
+ Placeholder.configure({
+ placeholder: "Untitled",
+ showOnlyWhenEditable: false,
+ }),
+ ];
+
+ return (
+ <>
+
+
+ {
+ if (editor) {
+ // @ts-ignore
+ setReadOnlyEditor(editor);
+ }
+ }}
+ >
+
+ >
+ );
+}
diff --git a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.module.css b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.module.css
index cf3637b2..cebee031 100644
--- a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.module.css
+++ b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.module.css
@@ -2,6 +2,7 @@
display: flex;
align-items: center;
overflow: hidden;
+ flex-wrap: nowrap;
a {
color: var(--mantine-color-default-color);
diff --git a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx
index 367b2682..9d78f38c 100644
--- a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx
+++ b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx
@@ -1,6 +1,6 @@
import { useAtomValue } from "jotai";
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts";
-import React, { useEffect, useState } from "react";
+import React, { useCallback, useEffect, useState } from "react";
import { findBreadcrumbPath } from "@/features/page/tree/utils";
import {
Button,
@@ -9,14 +9,16 @@ import {
Breadcrumbs,
ActionIcon,
Text,
+ Tooltip,
} from "@mantine/core";
-import { IconDots } from "@tabler/icons-react";
+import { IconCornerDownRightDouble, IconDots } from "@tabler/icons-react";
import { Link, useParams } from "react-router-dom";
import classes from "./breadcrumb.module.css";
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
import { buildPageUrl } from "@/features/page/page.utils.ts";
import { usePageQuery } from "@/features/page/queries/page-query.ts";
import { extractPageSlugId } from "@/lib";
+import { useMediaQuery } from "@mantine/hooks";
function getTitle(name: string, icon: string) {
if (icon) {
@@ -34,6 +36,7 @@ export default function Breadcrumb() {
const { data: currentPage } = usePageQuery({
pageId: extractPageSlugId(pageSlug),
});
+ const isMobile = useMediaQuery("(max-width: 48em)");
useEffect(() => {
if (treeData?.length > 0 && currentPage) {
@@ -43,7 +46,7 @@ export default function Breadcrumb() {
}, [currentPage?.id, treeData]);
const HiddenNodesTooltipContent = () =>
- breadcrumbNodes?.slice(1, -2).map((node) => (
+ breadcrumbNodes?.slice(1, -1).map((node) => (
));
- const renderAnchor = (node: SpaceTreeNode) => (
-
- {getTitle(node.name, node.icon)}
-
+ const MobileHiddenNodesTooltipContent = () =>
+ breadcrumbNodes?.map((node) => (
+
+
+
+ {getTitle(node.name, node.icon)}
+
+
+
+ ));
+
+ const renderAnchor = useCallback(
+ (node: SpaceTreeNode) => (
+
+
+ {getTitle(node.name, node.icon)}
+
+
+ ),
+ [spaceSlug],
);
const getBreadcrumbItems = () => {
@@ -77,7 +102,7 @@ export default function Breadcrumb() {
if (breadcrumbNodes.length > 3) {
const firstNode = breadcrumbNodes[0];
- const secondLastNode = breadcrumbNodes[breadcrumbNodes.length - 2];
+ //const secondLastNode = breadcrumbNodes[breadcrumbNodes.length - 2];
const lastNode = breadcrumbNodes[breadcrumbNodes.length - 1];
return [
@@ -98,7 +123,7 @@ export default function Breadcrumb() {
,
- renderAnchor(secondLastNode),
+ //renderAnchor(secondLastNode),
renderAnchor(lastNode),
];
}
@@ -106,11 +131,40 @@ export default function Breadcrumb() {
return breadcrumbNodes.map(renderAnchor);
};
+ const getMobileBreadcrumbItems = () => {
+ if (!breadcrumbNodes) return [];
+
+ if (breadcrumbNodes.length > 0) {
+ return [
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ ];
+ }
+
+ return breadcrumbNodes.map(renderAnchor);
+ };
+
return (
{breadcrumbNodes && (
- {getBreadcrumbItems()}
+ {isMobile ? getMobileBreadcrumbItems() : getBreadcrumbItems()}
)}
diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx
index 93d10520..9267ad9e 100644
--- a/apps/client/src/features/page/components/header/page-header-menu.tsx
+++ b/apps/client/src/features/page/components/header/page-header-menu.tsx
@@ -35,6 +35,7 @@ import {
import { formattedDate, timeAgo } from "@/lib/time.ts";
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
+import ShareModal from '@/features/share/components/share-modal.tsx';
interface PageHeaderMenuProps {
readOnly?: boolean;
@@ -58,6 +59,8 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
)}
+
+
{
],
});
- return `p/${titleSlug}-${pageSlugId}`;
+ return `${titleSlug}-${pageSlugId}`;
};
export const buildPageUrl = (
@@ -17,7 +17,20 @@ export const buildPageUrl = (
pageTitle?: string,
): string => {
if (spaceName === undefined) {
- return `/${buildPageSlug(pageSlugId, pageTitle)}`;
+ return `/p/${buildPageSlug(pageSlugId, pageTitle)}`;
}
- return `/s/${spaceName}/${buildPageSlug(pageSlugId, pageTitle)}`;
+ return `/s/${spaceName}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
+};
+
+export const buildSharedPageUrl = (opts: {
+ shareId: string;
+ pageSlugId: string;
+ pageTitle?: string;
+}): string => {
+ const { shareId, pageSlugId, pageTitle } = opts;
+ if (!shareId) {
+ return `/share/p/${buildPageSlug(pageSlugId, pageTitle)}`;
+ }
+
+ return `/share/${shareId}/p/${buildPageSlug(pageSlugId, pageTitle)}`;
};
diff --git a/apps/client/src/features/page/tree/components/space-tree.tsx b/apps/client/src/features/page/tree/components/space-tree.tsx
index 7b2f2f7d..5a00f258 100644
--- a/apps/client/src/features/page/tree/components/space-tree.tsx
+++ b/apps/client/src/features/page/tree/components/space-tree.tsx
@@ -8,9 +8,9 @@ import {
useUpdatePageMutation,
} from "@/features/page/queries/page-query.ts";
import { useEffect, useRef, useState } from "react";
-import { useNavigate, useParams } from "react-router-dom";
+import { Link, useParams } from "react-router-dom";
import classes from "@/features/page/tree/styles/tree.module.css";
-import { ActionIcon, Menu, rem } from "@mantine/core";
+import { ActionIcon, Box, Menu, rem } from "@mantine/core";
import {
IconArrowRight,
IconChevronDown,
@@ -58,6 +58,8 @@ import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.
import { useTranslation } from "react-i18next";
import ExportModal from "@/components/common/export-modal";
import MovePageModal from "../../components/move-page-modal.tsx";
+import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
+import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
interface SpaceTreeProps {
spaceId: string;
@@ -230,13 +232,14 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
}
function Node({ node, style, dragHandle, tree }: NodeRendererProps) {
- const navigate = useNavigate();
+ const { t } = useTranslation();
const updatePageMutation = useUpdatePageMutation();
const [treeData, setTreeData] = useAtom(treeDataAtom);
const emit = useQueryEmit();
const { spaceSlug } = useParams();
const timerRef = useRef(null);
- const { t } = useTranslation();
+ const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
+ const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
const prefetchPage = () => {
timerRef.current = setTimeout(() => {
@@ -287,11 +290,6 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps) {
}
}
- const handleClick = () => {
- const pageUrl = buildPageUrl(spaceSlug, node.data.slugId, node.data.name);
- navigate(pageUrl);
- };
-
const handleUpdateNodeIcon = (nodeId: string, newIcon: string) => {
const updatedTree = updateTreeNodeIcon(treeData, nodeId, newIcon);
setTreeData(updatedTree);
@@ -345,13 +343,22 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps) {
}, 650);
}
+ const pageUrl = buildPageUrl(spaceSlug, node.data.slugId, node.data.name);
+
return (
<>
- {
+ if (mobileSidebarOpened) {
+ toggleMobileSidebar();
+ }
+ }}
onMouseEnter={prefetchPage}
onMouseLeave={cancelPagePrefetch}
>
@@ -385,7 +392,7 @@ function Node({ node, style, dragHandle, tree }: NodeRendererProps
) {
/>
)}
-
+
>
);
}
diff --git a/apps/client/src/features/page/tree/styles/tree.module.css b/apps/client/src/features/page/tree/styles/tree.module.css
index 0a258fb5..716101e0 100644
--- a/apps/client/src/features/page/tree/styles/tree.module.css
+++ b/apps/client/src/features/page/tree/styles/tree.module.css
@@ -18,7 +18,7 @@
align-items: center;
height: 100%;
width: 93%; /* not to overlap with scroll bar */
-
+ text-decoration: none;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
&:hover {
@@ -70,6 +70,10 @@
background-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
}
+.row:focus .node:global(.isFocused) {
+ background-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
+}
+
.row {
white-space: nowrap;
cursor: pointer;
diff --git a/apps/client/src/features/page/tree/utils/utils.ts b/apps/client/src/features/page/tree/utils/utils.ts
index 0dfe8ed4..7ae84e38 100644
--- a/apps/client/src/features/page/tree/utils/utils.ts
+++ b/apps/client/src/features/page/tree/utils/utils.ts
@@ -1,7 +1,7 @@
import { IPage } from "@/features/page/types/page.types.ts";
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
-function sortPositionKeys(keys: any[]) {
+export function sortPositionKeys(keys: any[]) {
return keys.sort((a, b) => {
if (a.position < b.position) return -1;
if (a.position > b.position) return 1;
diff --git a/apps/client/src/features/share/atoms/sidebar-atom.ts b/apps/client/src/features/share/atoms/sidebar-atom.ts
new file mode 100644
index 00000000..0bc9d681
--- /dev/null
+++ b/apps/client/src/features/share/atoms/sidebar-atom.ts
@@ -0,0 +1,9 @@
+import { atomWithWebStorage } from "@/lib/jotai-helper.ts";
+import { atom } from 'jotai';
+
+export const tableOfContentAsideAtom = atomWithWebStorage
(
+ "showTOC",
+ true,
+);
+
+export const mobileTableOfContentAsideAtom = atom(false);
\ No newline at end of file
diff --git a/apps/client/src/features/share/components/share-action-menu.tsx b/apps/client/src/features/share/components/share-action-menu.tsx
new file mode 100644
index 00000000..398e25e9
--- /dev/null
+++ b/apps/client/src/features/share/components/share-action-menu.tsx
@@ -0,0 +1,106 @@
+import { Menu, ActionIcon, Text } from "@mantine/core";
+import React from "react";
+import {
+ IconCopy,
+ IconDots,
+ IconFileDescription,
+ IconTrash,
+} from "@tabler/icons-react";
+import { modals } from "@mantine/modals";
+import { useTranslation } from "react-i18next";
+import { ISharedItem } from "@/features/share/types/share.types.ts";
+import {
+ buildPageUrl,
+ buildSharedPageUrl,
+} from "@/features/page/page.utils.ts";
+import { useClipboard } from "@mantine/hooks";
+import { notifications } from "@mantine/notifications";
+import { useNavigate } from "react-router-dom";
+import { useDeleteShareMutation } from "@/features/share/queries/share-query.ts";
+
+interface Props {
+ share: ISharedItem;
+}
+export default function ShareActionMenu({ share }: Props) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const clipboard = useClipboard();
+ const deleteShareMutation = useDeleteShareMutation();
+
+ const openPage = () => {
+ const pageLink = buildPageUrl(
+ share.space.slug,
+ share.page.slugId,
+ share.page.title,
+ );
+ navigate(pageLink);
+ };
+
+ const copyLink = () => {
+ const shareLink = buildSharedPageUrl({
+ shareId: share.key,
+ pageTitle: share.page.title,
+ pageSlugId: share.page.slugId,
+ });
+
+ clipboard.copy(shareLink);
+ notifications.show({ message: t("Link copied") });
+ };
+ const onDelete = async () => {
+ deleteShareMutation.mutateAsync(share.key);
+ };
+
+ const openDeleteModal = () =>
+ modals.openConfirmModal({
+ title: t("Delete public share link"),
+ children: (
+
+ {t("Are you sure you want to delete this shared link?")}
+
+ ),
+ centered: true,
+ labels: { confirm: t("Delete"), cancel: t("Don't") },
+ confirmProps: { color: "red" },
+ onConfirm: onDelete,
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ }>
+ {t("Copy link")}
+
+
+ }
+ >
+ {t("Open page")}
+
+ }
+ disabled={share.space?.userRole === "reader"}
+ >
+ {t("Delete share")}
+
+
+
+ >
+ );
+}
diff --git a/apps/client/src/features/share/components/share-layout.tsx b/apps/client/src/features/share/components/share-layout.tsx
new file mode 100644
index 00000000..e3b2eb17
--- /dev/null
+++ b/apps/client/src/features/share/components/share-layout.tsx
@@ -0,0 +1,10 @@
+import { Outlet } from "react-router-dom";
+import ShareShell from "@/features/share/components/share-shell.tsx";
+
+export default function ShareLayout() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/client/src/features/share/components/share-list.tsx b/apps/client/src/features/share/components/share-list.tsx
new file mode 100644
index 00000000..d5acbbd6
--- /dev/null
+++ b/apps/client/src/features/share/components/share-list.tsx
@@ -0,0 +1,97 @@
+import { Table, Group, Text, Anchor } from "@mantine/core";
+import React, { useState } from "react";
+import { Link } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import Paginate from "@/components/common/paginate.tsx";
+import { useGetSharesQuery } from "@/features/share/queries/share-query.ts";
+import { ISharedItem } from "@/features/share/types/share.types.ts";
+import { format } from "date-fns";
+import ShareActionMenu from "@/features/share/components/share-action-menu.tsx";
+import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
+import { getPageIcon } from "@/lib";
+import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
+import classes from "./share.module.css";
+
+export default function ShareList() {
+ const { t } = useTranslation();
+ const [page, setPage] = useState(1);
+ const { data, isLoading } = useGetSharesQuery({ page });
+
+ return (
+ <>
+
+
+
+
+ {t("Page")}
+ {t("Shared by")}
+ {t("Shared at")}
+
+
+
+
+ {data?.items.map((share: ISharedItem, index: number) => (
+
+
+
+
+ {getPageIcon(share.page.icon)}
+
+
+ {share.page.title || t("untitled")}
+
+
+
+
+
+
+
+
+
+ {share.creator.name}
+
+
+
+
+
+ {format(new Date(share.createdAt), "MMM dd, yyyy")}
+
+
+
+
+
+
+ ))}
+
+
+
+
+ {data?.items.length > 0 && (
+
+ )}
+ >
+ );
+}
diff --git a/apps/client/src/features/share/components/share-modal.tsx b/apps/client/src/features/share/components/share-modal.tsx
new file mode 100644
index 00000000..20dcc518
--- /dev/null
+++ b/apps/client/src/features/share/components/share-modal.tsx
@@ -0,0 +1,227 @@
+import {
+ ActionIcon,
+ Anchor,
+ Button,
+ Group,
+ Indicator,
+ Popover,
+ Switch,
+ Text,
+ TextInput,
+ Tooltip,
+} from "@mantine/core";
+import { IconExternalLink, IconWorld } from "@tabler/icons-react";
+import React, { useEffect, useMemo, useState } from "react";
+import {
+ useCreateShareMutation,
+ useDeleteShareMutation,
+ useShareForPageQuery,
+ useUpdateShareMutation,
+} from "@/features/share/queries/share-query.ts";
+import { Link, useParams } from "react-router-dom";
+import { extractPageSlugId, getPageIcon } from "@/lib";
+import { useTranslation } from "react-i18next";
+import CopyTextButton from "@/components/common/copy.tsx";
+import { getAppUrl } from "@/lib/config.ts";
+import { buildPageUrl } from "@/features/page/page.utils.ts";
+import classes from "@/features/share/components/share.module.css";
+
+interface ShareModalProps {
+ readOnly: boolean;
+}
+export default function ShareModal({ readOnly }: ShareModalProps) {
+ const { t } = useTranslation();
+ const { pageSlug } = useParams();
+ const pageId = extractPageSlugId(pageSlug);
+ const { data: share } = useShareForPageQuery(pageId);
+ const { spaceSlug } = useParams();
+ const createShareMutation = useCreateShareMutation();
+ const updateShareMutation = useUpdateShareMutation();
+ const deleteShareMutation = useDeleteShareMutation();
+ // pageIsShared means that the share exists and its level equals zero.
+ const pageIsShared = share && share.level === 0;
+ // if level is greater than zero, then it is a descendant page from a shared page
+ const isDescendantShared = share && share.level > 0;
+
+ const publicLink = `${getAppUrl()}/share/${share?.key}/p/${pageSlug}`;
+
+ const [isPagePublic, setIsPagePublic] = useState(false);
+ useEffect(() => {
+ if (share) {
+ setIsPagePublic(true);
+ } else {
+ setIsPagePublic(false);
+ }
+ }, [share, pageId]);
+
+ const handleChange = async (event: React.ChangeEvent) => {
+ const value = event.currentTarget.checked;
+
+ if (value) {
+ createShareMutation.mutateAsync({
+ pageId: pageId,
+ includeSubPages: true,
+ searchIndexing: true,
+ });
+ setIsPagePublic(value);
+ } else {
+ if (share && share.id) {
+ deleteShareMutation.mutateAsync(share.id);
+ setIsPagePublic(value);
+ }
+ }
+ };
+
+ const handleSubPagesChange = async (
+ event: React.ChangeEvent,
+ ) => {
+ const value = event.currentTarget.checked;
+ updateShareMutation.mutateAsync({
+ shareId: share.id,
+ includeSubPages: value,
+ });
+ };
+
+ const handleIndexSearchChange = async (
+ event: React.ChangeEvent,
+ ) => {
+ const value = event.currentTarget.checked;
+ updateShareMutation.mutateAsync({
+ shareId: share.id,
+ searchIndexing: value,
+ });
+ };
+
+ const shareLink = useMemo(() => (
+
+ }
+ style={{ width: "100%" }}
+ />
+
+
+
+
+ ), [publicLink]);
+
+ return (
+
+
+
+
+
+ }
+ variant="default"
+ >
+ {t("Share")}
+
+
+
+ {isDescendantShared ? (
+ <>
+ {t("Inherits public sharing from")}
+
+
+ {getPageIcon(share.sharedPage.icon)}
+
+
+ {share.sharedPage.title || t("untitled")}
+
+
+
+
+
+ {shareLink}
+ >
+ ) : (
+ <>
+
+
+
+ {isPagePublic ? t("Shared to web") : t("Share to web")}
+
+
+ {isPagePublic
+ ? t("Anyone with the link can view this page")
+ : t("Make this page publicly accessible")}
+
+
+
+
+
+ {pageIsShared && (
+ <>
+ {shareLink}
+
+
+ {t("Include sub-pages")}
+
+ {t("Make sub-pages public too")}
+
+
+
+
+
+
+
+ {t("Search engine indexing")}
+
+ {t("Allow search engines to index page")}
+
+
+
+
+ >
+ )}
+ >
+ )}
+
+
+ );
+}
diff --git a/apps/client/src/features/share/components/share-shell.tsx b/apps/client/src/features/share/components/share-shell.tsx
new file mode 100644
index 00000000..82863b34
--- /dev/null
+++ b/apps/client/src/features/share/components/share-shell.tsx
@@ -0,0 +1,196 @@
+import React, { useState } from "react";
+import {
+ ActionIcon,
+ Affix,
+ AppShell,
+ Button,
+ Group,
+ ScrollArea,
+ Tooltip,
+} from "@mantine/core";
+import { useGetSharedPageTreeQuery } from "@/features/share/queries/share-query.ts";
+import { useParams } from "react-router-dom";
+import SharedTree from "@/features/share/components/shared-tree.tsx";
+import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx";
+import { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
+import { ThemeToggle } from "@/components/theme-toggle.tsx";
+import { useAtomValue } from "jotai";
+import { useAtom } from "jotai";
+import {
+ desktopSidebarAtom,
+ mobileSidebarAtom,
+} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
+import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
+import { useTranslation } from "react-i18next";
+import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
+import {
+ mobileTableOfContentAsideAtom,
+ tableOfContentAsideAtom,
+} from "@/features/share/atoms/sidebar-atom.ts";
+import { IconList } from "@tabler/icons-react";
+import { useToggleToc } from "@/features/share/hooks/use-toggle-toc.ts";
+import classes from "./share.module.css";
+import { useClickOutside } from "@mantine/hooks";
+
+const MemoizedSharedTree = React.memo(SharedTree);
+
+export default function ShareShell({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const { t } = useTranslation();
+ const [mobileOpened] = useAtom(mobileSidebarAtom);
+ const [desktopOpened] = useAtom(desktopSidebarAtom);
+ const toggleMobile = useToggleSidebar(mobileSidebarAtom);
+ const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
+
+ const [tocOpened] = useAtom(tableOfContentAsideAtom);
+ const [mobileTocOpened] = useAtom(mobileTableOfContentAsideAtom);
+ const toggleTocMobile = useToggleToc(mobileTableOfContentAsideAtom);
+ const toggleToc = useToggleToc(tableOfContentAsideAtom);
+
+ const { shareId } = useParams();
+ const { data } = useGetSharedPageTreeQuery(shareId);
+ const readOnlyEditor = useAtomValue(readOnlyEditorAtom);
+
+ const [navbarOutside, setNavbarOutside] = useState(null);
+ const [asideOutside, setAsideOutside] = useState(null);
+
+ useClickOutside(
+ () => {
+ if (mobileOpened) {
+ toggleMobile();
+ }
+ if (mobileTocOpened) {
+ toggleTocMobile();
+ }
+ },
+ null,
+ [navbarOutside, asideOutside],
+ );
+
+ return (
+ 1 && {
+ navbar: {
+ width: 300,
+ breakpoint: "sm",
+ collapsed: {
+ mobile: !mobileOpened,
+ desktop: !desktopOpened,
+ },
+ },
+ })}
+ aside={{
+ width: 300,
+ breakpoint: "sm",
+ collapsed: {
+ mobile: !mobileTocOpened,
+ desktop: !tocOpened,
+ },
+ }}
+ padding="md"
+ >
+
+
+
+ {data?.pageTree?.length > 1 && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+ {data?.pageTree?.length > 1 && (
+
+
+
+ )}
+
+
+ {children}
+
+
+
+ Powered by Docmost
+
+
+
+
+
+
+
+ {readOnlyEditor && (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/client/src/features/share/components/share.module.css b/apps/client/src/features/share/components/share.module.css
new file mode 100644
index 00000000..617768ff
--- /dev/null
+++ b/apps/client/src/features/share/components/share.module.css
@@ -0,0 +1,20 @@
+.shareLinkText {
+ @mixin light {
+ border-bottom: 0.05em solid var(--mantine-color-dark-0);
+ }
+ @mixin dark {
+ border-bottom: 0.05em solid var(--mantine-color-dark-2);
+ }
+}
+
+.treeNode {
+ text-decoration: none;
+ user-select: none;
+}
+
+.navbar,
+.aside {
+ @media (max-width: $mantine-breakpoint-sm) {
+ width: 350px;
+ }
+}
diff --git a/apps/client/src/features/share/components/shared-tree.tsx b/apps/client/src/features/share/components/shared-tree.tsx
new file mode 100644
index 00000000..5e85ab57
--- /dev/null
+++ b/apps/client/src/features/share/components/shared-tree.tsx
@@ -0,0 +1,179 @@
+import { ISharedPageTree } from "@/features/share/types/share.types.ts";
+import { NodeApi, NodeRendererProps, Tree, TreeApi } from "react-arborist";
+import {
+ buildSharedPageTree,
+ SharedPageTreeNode,
+} from "@/features/share/utils.ts";
+import { useEffect, useMemo, useRef, useState } from "react";
+import { useElementSize, useMergedRef } from "@mantine/hooks";
+import { SpaceTreeNode } from "@/features/page/tree/types.ts";
+import { Link, useParams } from "react-router-dom";
+import { atom, useAtom } from "jotai/index";
+import { useTranslation } from "react-i18next";
+import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
+import clsx from "clsx";
+import {
+ IconChevronDown,
+ IconChevronRight,
+ IconPointFilled,
+} from "@tabler/icons-react";
+import { ActionIcon, Box } from "@mantine/core";
+import { extractPageSlugId } from "@/lib";
+import { OpenMap } from "react-arborist/dist/main/state/open-slice";
+import classes from "@/features/page/tree/styles/tree.module.css";
+import styles from "./share.module.css";
+import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
+
+interface SharedTree {
+ sharedPageTree: ISharedPageTree;
+}
+
+const openSharedTreeNodesAtom = atom({});
+
+export default function SharedTree({ sharedPageTree }: SharedTree) {
+ const [tree, setTree] = useState<
+ TreeApi | null | undefined
+ >(null);
+ const rootElement = useRef();
+ const { ref: sizeRef, width, height } = useElementSize();
+ const mergedRef = useMergedRef(rootElement, sizeRef);
+ const { pageSlug } = useParams();
+ const [openTreeNodes, setOpenTreeNodes] = useAtom(
+ openSharedTreeNodesAtom,
+ );
+
+ const currentNodeId = extractPageSlugId(pageSlug);
+
+ const treeData: SharedPageTreeNode[] = useMemo(() => {
+ if (!sharedPageTree?.pageTree) return;
+ return buildSharedPageTree(sharedPageTree.pageTree);
+ }, [sharedPageTree?.pageTree]);
+
+ useEffect(() => {
+ const parentNodeId = treeData?.[0]?.slugId;
+
+ if (parentNodeId && tree) {
+ const parentNode = tree.get(parentNodeId);
+
+ setTimeout(() => {
+ if (parentNode) {
+ tree.openSiblings(parentNode);
+ }
+ });
+
+ // open direct children of parent node
+ parentNode?.children.forEach((node) => {
+ tree.openSiblings(node);
+ });
+ }
+ }, [treeData, tree]);
+
+ useEffect(() => {
+ if (currentNodeId && tree) {
+ setTimeout(() => {
+ // focus on node and open all parents
+ tree?.select(currentNodeId, { align: "auto" });
+ }, 200);
+ } else {
+ tree?.deselectAll();
+ }
+ }, [currentNodeId, tree]);
+
+ if (!sharedPageTree || !sharedPageTree?.pageTree) {
+ return null;
+ }
+
+ return (
+
+ {rootElement.current && (
+ setTree(t)}
+ openByDefault={false}
+ disableMultiSelection={true}
+ className={classes.tree}
+ rowClassName={classes.row}
+ rowHeight={30}
+ overscanCount={10}
+ dndRootElement={rootElement.current}
+ onToggle={() => {
+ setOpenTreeNodes(tree?.openState);
+ }}
+ initialOpenState={openTreeNodes}
+ onClick={(e) => {
+ if (tree && tree.focusedNode) {
+ tree.select(tree.focusedNode);
+ }
+ }}
+ >
+ {Node}
+
+ )}
+
+ );
+}
+
+function Node({ node, style, tree }: NodeRendererProps) {
+ const { shareId } = useParams();
+ const { t } = useTranslation();
+ const [, setMobileSidebarState] = useAtom(mobileSidebarAtom);
+
+ const pageUrl = buildSharedPageUrl({
+ shareId: shareId,
+ pageSlugId: node.data.slugId,
+ pageTitle: node.data.name,
+ });
+
+ return (
+ <>
+ {
+ setMobileSidebarState(false);
+ }}
+ >
+
+ {node.data.name || t("untitled")}
+
+ >
+ );
+}
+
+interface PageArrowProps {
+ node: NodeApi;
+}
+
+function PageArrow({ node }: PageArrowProps) {
+ return (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ node.toggle();
+ }}
+ >
+ {node.isInternal ? (
+ node.children && (node.children.length > 0 || node.data.hasChildren) ? (
+ node.isOpen ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )
+ ) : null}
+
+ );
+}
diff --git a/apps/client/src/features/share/hooks/use-toggle-toc.ts b/apps/client/src/features/share/hooks/use-toggle-toc.ts
new file mode 100644
index 00000000..ec43086a
--- /dev/null
+++ b/apps/client/src/features/share/hooks/use-toggle-toc.ts
@@ -0,0 +1,8 @@
+import { useAtom } from "jotai";
+
+export function useToggleToc(tocAtom: any) {
+ const [tocState, setTocState] = useAtom(tocAtom);
+ return () => {
+ setTocState(!tocState);
+ }
+}
diff --git a/apps/client/src/features/share/queries/share-query.ts b/apps/client/src/features/share/queries/share-query.ts
new file mode 100644
index 00000000..dea047bf
--- /dev/null
+++ b/apps/client/src/features/share/queries/share-query.ts
@@ -0,0 +1,179 @@
+import {
+ keepPreviousData,
+ useMutation,
+ useQuery,
+ useQueryClient,
+ UseQueryResult,
+} from "@tanstack/react-query";
+import { notifications } from "@mantine/notifications";
+import { useTranslation } from "react-i18next";
+import {
+ ICreateShare,
+ IShare,
+ ISharedItem,
+ ISharedPage,
+ ISharedPageTree,
+ IShareForPage,
+ IShareInfoInput,
+ IUpdateShare,
+} from "@/features/share/types/share.types.ts";
+import {
+ createShare,
+ deleteShare,
+ getSharedPageTree,
+ getShareForPage,
+ getShareInfo,
+ getSharePageInfo,
+ getShares,
+ updateShare,
+} from "@/features/share/services/share-service.ts";
+import { IPage } from "@/features/page/types/page.types.ts";
+import { IPagination, QueryParams } from "@/lib/types.ts";
+import { useEffect } from "react";
+
+export function useGetSharesQuery(
+ params?: QueryParams,
+): UseQueryResult, Error> {
+ return useQuery({
+ queryKey: ["share-list"],
+ queryFn: () => getShares(params),
+ placeholderData: keepPreviousData,
+ });
+}
+
+export function useGetShareByIdQuery(
+ shareId: string,
+): UseQueryResult {
+ const query = useQuery({
+ queryKey: ["share-by-id", shareId],
+ queryFn: () => getShareInfo(shareId),
+ enabled: !!shareId,
+ });
+
+ return query;
+}
+
+export function useSharePageQuery(
+ shareInput: Partial,
+): UseQueryResult {
+ const query = useQuery({
+ queryKey: ["shares", shareInput],
+ queryFn: () => getSharePageInfo(shareInput),
+ enabled: !!shareInput.pageId,
+ });
+
+ return query;
+}
+
+export function useShareForPageQuery(
+ pageId: string,
+): UseQueryResult {
+ const query = useQuery({
+ queryKey: ["share-for-page", pageId],
+ queryFn: () => getShareForPage(pageId),
+ enabled: !!pageId,
+ staleTime: 0,
+ retry: false,
+ });
+
+ return query;
+}
+
+export function useCreateShareMutation() {
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data) => createShare(data),
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({
+ predicate: (item) =>
+ ["share-for-page", "share-list"].includes(item.queryKey[0] as string),
+ });
+ },
+ onError: (error) => {
+ notifications.show({ message: t("Failed to share page"), color: "red" });
+ },
+ });
+}
+
+export function useUpdateShareMutation() {
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data) => updateShare(data),
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({
+ predicate: (item) =>
+ ["share-for-page", "share-list"].includes(item.queryKey[0] as string),
+ });
+ },
+ onError: (error, params) => {
+ if (error?.["status"] === 404) {
+ queryClient.removeQueries({
+ predicate: (item) =>
+ ["share-for-page"].includes(item.queryKey[0] as string),
+ });
+
+ notifications.show({
+ message: t("Share not found"),
+ color: "red",
+ });
+ return;
+ }
+
+ notifications.show({
+ message: error?.["response"]?.data?.message || "Share not found",
+ color: "red",
+ });
+ },
+ });
+}
+
+export function useDeleteShareMutation() {
+ const { t } = useTranslation();
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (shareId: string) => deleteShare(shareId),
+ onSuccess: (data) => {
+ queryClient.removeQueries({
+ predicate: (item) =>
+ ["share-for-page"].includes(item.queryKey[0] as string),
+ });
+
+ queryClient.invalidateQueries({
+ predicate: (item) =>
+ ["share-list"].includes(item.queryKey[0] as string),
+ });
+
+ notifications.show({ message: t("Share deleted successfully") });
+ },
+ onError: (error) => {
+ if (error?.["status"] === 404) {
+ queryClient.removeQueries({
+ predicate: (item) =>
+ ["share-for-page"].includes(item.queryKey[0] as string),
+ });
+ }
+
+ notifications.show({
+ message: error?.["response"]?.data?.message || "Failed to delete share",
+ color: "red",
+ });
+ },
+ });
+}
+
+export function useGetSharedPageTreeQuery(
+ shareId: string,
+): UseQueryResult {
+ return useQuery({
+ queryKey: ["shared-page-tree", shareId],
+ queryFn: () => getSharedPageTree(shareId),
+ enabled: !!shareId,
+ placeholderData: keepPreviousData,
+ staleTime: 60 * 60 * 1000,
+ });
+}
diff --git a/apps/client/src/features/share/services/share-service.ts b/apps/client/src/features/share/services/share-service.ts
new file mode 100644
index 00000000..2f43ba20
--- /dev/null
+++ b/apps/client/src/features/share/services/share-service.ts
@@ -0,0 +1,59 @@
+import api from "@/lib/api-client";
+import { IPage } from "@/features/page/types/page.types";
+
+import {
+ ICreateShare,
+ IShare,
+ ISharedItem,
+ ISharedPage,
+ ISharedPageTree,
+ IShareForPage,
+ IShareInfoInput,
+ IUpdateShare,
+} from "@/features/share/types/share.types.ts";
+import { IPagination, QueryParams } from "@/lib/types.ts";
+
+export async function getShares(
+ params?: QueryParams,
+): Promise> {
+ const req = await api.post("/shares", params);
+ return req.data;
+}
+
+export async function createShare(data: ICreateShare): Promise {
+ const req = await api.post("/shares/create", data);
+ return req.data;
+}
+
+export async function getShareInfo(shareId: string): Promise {
+ const req = await api.post("/shares/info", { shareId });
+ return req.data;
+}
+
+export async function updateShare(data: IUpdateShare): Promise {
+ const req = await api.post("/shares/update", data);
+ return req.data;
+}
+
+export async function getShareForPage(pageId: string): Promise {
+ const req = await api.post("/shares/for-page", { pageId });
+ return req.data;
+}
+
+export async function getSharePageInfo(
+ shareInput: Partial,
+): Promise {
+ const req = await api.post("/shares/page-info", shareInput);
+ return req.data;
+}
+
+export async function deleteShare(shareId: string): Promise {
+ await api.post("/shares/delete", { shareId });
+}
+
+export async function getSharedPageTree(
+ shareId: string,
+): Promise {
+ const req = await api.post("/shares/tree", { shareId });
+ return req.data;
+}
diff --git a/apps/client/src/features/share/types/share.types.ts b/apps/client/src/features/share/types/share.types.ts
new file mode 100644
index 00000000..c40801e8
--- /dev/null
+++ b/apps/client/src/features/share/types/share.types.ts
@@ -0,0 +1,73 @@
+import { IPage } from "@/features/page/types/page.types.ts";
+
+export interface IShare {
+ id: string;
+ key: string;
+ pageId: string;
+ includeSubPages: boolean;
+ searchIndexing: boolean;
+ creatorId: string;
+ spaceId: string;
+ workspaceId: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+ sharedPage?: ISharePage;
+}
+
+export interface ISharedItem extends IShare {
+ page: {
+ id: string;
+ title: string;
+ slugId: string;
+ icon: string | null;
+ };
+ space: {
+ id: string;
+ name: string;
+ slug: string;
+ userRole: string;
+ };
+ creator: {
+ id: string;
+ name: string;
+ avatarUrl: string | null;
+ };
+}
+
+export interface ISharedPage extends IShare {
+ page: IPage;
+ share: IShare & {
+ level: number;
+ sharedPage: { id: string; slugId: string; title: string; icon: string };
+ };
+}
+
+export interface IShareForPage extends IShare {
+ level: number;
+ sharedPage: ISharePage;
+}
+
+interface ISharePage {
+ id: string;
+ slugId: string;
+ title: string;
+ icon: string;
+}
+
+export interface ICreateShare {
+ pageId?: string;
+ includeSubPages?: boolean;
+ searchIndexing?: boolean;
+}
+
+export type IUpdateShare = ICreateShare & { shareId: string; pageId?: string };
+
+export interface IShareInfoInput {
+ pageId: string;
+}
+
+export interface ISharedPageTree {
+ share: IShare;
+ pageTree: Partial;
+}
diff --git a/apps/client/src/features/share/utils.ts b/apps/client/src/features/share/utils.ts
new file mode 100644
index 00000000..74ec349f
--- /dev/null
+++ b/apps/client/src/features/share/utils.ts
@@ -0,0 +1,60 @@
+import { IPage } from "@/features/page/types/page.types.ts";
+import { sortPositionKeys } from "@/features/page/tree/utils";
+
+export type SharedPageTreeNode = {
+ id: string;
+ slugId: string;
+ name: string;
+ icon?: string;
+ position: string;
+ spaceId: string;
+ parentPageId: string;
+ hasChildren: boolean;
+ children: SharedPageTreeNode[];
+ label: string,
+ value: string,
+};
+
+export function buildSharedPageTree(pages: Partial): SharedPageTreeNode[] {
+ const pageMap: Record = {};
+
+ // Initialize each page as a tree node and store it in a map.
+ pages.forEach((page) => {
+ pageMap[page.id] = {
+ id: page.slugId,
+ slugId: page.slugId,
+ name: page.title,
+ icon: page.icon,
+ position: page.position,
+ // Initially assume a page has no children.
+ hasChildren: false,
+ spaceId: page.spaceId,
+ parentPageId: page.parentPageId,
+ label: page.title || 'untitled',
+ value: page.id,
+ children: [],
+ };
+ });
+
+ // Build the tree structure.
+ const tree: SharedPageTreeNode[] = [];
+ pages.forEach((page) => {
+ if (page.parentPageId) {
+ // If the page has a parent, add it as a child of the parent node.
+ const parentNode = pageMap[page.parentPageId];
+ if (parentNode) {
+ parentNode.children.push(pageMap[page.id]);
+ parentNode.hasChildren = true;
+ } else {
+ // Parent not found – treat this page as a top-level node.
+ tree.push(pageMap[page.id]);
+ }
+ } else {
+ // No parentPageId indicates a top-level page.
+ tree.push(pageMap[page.id]);
+ }
+ });
+
+ // Return the sorted tree.
+ return sortPositionKeys(tree);
+}
diff --git a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx
index 1df7a2c1..528e8051 100644
--- a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx
+++ b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx
@@ -38,6 +38,8 @@ import PageImportModal from "@/features/page/components/page-import-modal.tsx";
import { useTranslation } from "react-i18next";
import { SwitchSpace } from "./switch-space";
import ExportModal from "@/components/common/export-modal";
+import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
+import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
export function SpaceSidebar() {
const { t } = useTranslation();
@@ -45,6 +47,9 @@ export function SpaceSidebar() {
const location = useLocation();
const [opened, { open: openSettings, close: closeSettings }] =
useDisclosure(false);
+ const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
+ const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
+
const { spaceSlug } = useParams();
const { data: space, isLoading, isError } = useGetSpaceBySlugQuery(spaceSlug);
@@ -123,7 +128,12 @@ export function SpaceSidebar() {
) && (
{
+ handleCreatePage();
+ if (mobileSidebarOpened) {
+ toggleMobileSidebar();
+ }
+ }}
>
+
+
+ {t("Public sharing")} - {getAppName()}
+
+
+
+
+ }>
+ {t(
+ "Publicly shared pages from spaces you are a member of will appear here",
+ )}
+
+
+
+ >
+ );
+}
diff --git a/apps/client/src/pages/share/share-redirect.tsx b/apps/client/src/pages/share/share-redirect.tsx
new file mode 100644
index 00000000..5653e83f
--- /dev/null
+++ b/apps/client/src/pages/share/share-redirect.tsx
@@ -0,0 +1,35 @@
+import { useNavigate, useParams } from "react-router-dom";
+import { useEffect } from "react";
+import { buildSharedPageUrl } from "@/features/page/page.utils.ts";
+import { Error404 } from "@/components/ui/error-404.tsx";
+import { useGetShareByIdQuery } from "@/features/share/queries/share-query.ts";
+
+export default function ShareRedirect() {
+ const { shareId } = useParams();
+ const navigate = useNavigate();
+
+ const { data: share, isLoading, isError } = useGetShareByIdQuery(shareId);
+
+ useEffect(() => {
+ if (share) {
+ navigate(
+ buildSharedPageUrl({
+ shareId: share.key,
+ pageSlugId: share?.sharedPage.slugId,
+ pageTitle: share?.sharedPage.title,
+ }),
+ { replace: true },
+ );
+ }
+ }, [isLoading, share]);
+
+ if (isError) {
+ return ;
+ }
+
+ if (isLoading) {
+ return <>>;
+ }
+
+ return null;
+}
diff --git a/apps/client/src/pages/share/shared-page.tsx b/apps/client/src/pages/share/shared-page.tsx
new file mode 100644
index 00000000..a574a614
--- /dev/null
+++ b/apps/client/src/pages/share/shared-page.tsx
@@ -0,0 +1,58 @@
+import { useNavigate, useParams } from "react-router-dom";
+import { Helmet } from "react-helmet-async";
+import { useTranslation } from "react-i18next";
+import { useSharePageQuery } from "@/features/share/queries/share-query.ts";
+import { Container } from "@mantine/core";
+import React, { useEffect } from "react";
+import ReadonlyPageEditor from "@/features/editor/readonly-page-editor.tsx";
+import { extractPageSlugId } from "@/lib";
+import { Error404 } from "@/components/ui/error-404.tsx";
+
+export default function SingleSharedPage() {
+ const { t } = useTranslation();
+ const { pageSlug } = useParams();
+ const { shareId } = useParams();
+ const navigate = useNavigate();
+
+ const { data, isLoading, isError, error } = useSharePageQuery({
+ pageId: extractPageSlugId(pageSlug),
+ });
+
+ useEffect(() => {
+ if (shareId && data) {
+ if (data.share.key !== shareId) {
+ navigate(`/share/${data.share.key}/p/${pageSlug}`, { replace: true });
+ }
+ }
+ }, [shareId, data]);
+
+ if (isLoading) {
+ return <>>;
+ }
+
+ if (isError || !data) {
+ if ([401, 403, 404].includes(error?.["status"])) {
+ return ;
+ }
+ return {t("Error fetching page data.")}
;
+ }
+
+ return (
+
+
+ {`${data?.page?.title || t("untitled")}`}
+ {!data?.share.searchIndexing && (
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/server/package.json b/apps/server/package.json
index fced5e33..efd8d8ca 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -37,18 +37,18 @@
"@fastify/multipart": "^9.0.3",
"@fastify/static": "^8.1.1",
"@nestjs/bullmq": "^11.0.2",
- "@nestjs/common": "^11.0.10",
- "@nestjs/config": "^4.0.0",
- "@nestjs/core": "^11.0.10",
+ "@nestjs/common": "^11.0.20",
+ "@nestjs/config": "^4.0.2",
+ "@nestjs/core": "^11.0.20",
"@nestjs/event-emitter": "^3.0.0",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^11.0.5",
- "@nestjs/platform-fastify": "^11.0.10",
- "@nestjs/platform-socket.io": "^11.0.10",
+ "@nestjs/platform-fastify": "^11.0.20",
+ "@nestjs/platform-socket.io": "^11.0.20",
"@nestjs/schedule": "^5.0.1",
"@nestjs/terminus": "^11.0.0",
- "@nestjs/websockets": "^11.0.10",
+ "@nestjs/websockets": "^11.0.20",
"@node-saml/passport-saml": "^5.0.1",
"@react-email/components": "0.0.28",
"@react-email/render": "1.0.2",
diff --git a/apps/server/src/common/helpers/prosemirror/utils.ts b/apps/server/src/common/helpers/prosemirror/utils.ts
index 9d9b5ebe..aaadcd56 100644
--- a/apps/server/src/common/helpers/prosemirror/utils.ts
+++ b/apps/server/src/common/helpers/prosemirror/utils.ts
@@ -1,5 +1,6 @@
import { Node } from '@tiptap/pm/model';
import { jsonToNode } from '../../../collaboration/collaboration.util';
+import { validate as isValidUUID } from 'uuid';
export interface MentionNode {
id: string;
@@ -56,3 +57,41 @@ export function extractPageMentions(mentionList: MentionNode[]): MentionNode[] {
}
return pageMentionList as MentionNode[];
}
+
+
+export function getProsemirrorContent(content: any) {
+ return (
+ content ?? {
+ type: 'doc',
+ content: [{ type: 'paragraph', attrs: { textAlign: 'left' } }],
+ }
+ );
+}
+
+export function isAttachmentNode(nodeType: string) {
+ const attachmentNodeTypes = [
+ 'attachment',
+ 'image',
+ 'video',
+ 'excalidraw',
+ 'drawio',
+ ];
+ return attachmentNodeTypes.includes(nodeType);
+}
+
+export function getAttachmentIds(prosemirrorJson: any) {
+ const doc = jsonToNode(prosemirrorJson);
+ const attachmentIds = [];
+
+ doc?.descendants((node: Node) => {
+ if (isAttachmentNode(node.type.name)) {
+ if (node.attrs.attachmentId && isValidUUID(node.attrs.attachmentId)) {
+ if (!attachmentIds.includes(node.attrs.attachmentId)) {
+ attachmentIds.push(node.attrs.attachmentId);
+ }
+ }
+ }
+ });
+
+ return attachmentIds;
+}
\ No newline at end of file
diff --git a/apps/server/src/core/attachment/attachment.controller.ts b/apps/server/src/core/attachment/attachment.controller.ts
index 4804fce6..160d950b 100644
--- a/apps/server/src/core/attachment/attachment.controller.ts
+++ b/apps/server/src/core/attachment/attachment.controller.ts
@@ -1,310 +1,373 @@
import {
- BadRequestException,
- Controller,
- ForbiddenException,
- Get,
- HttpCode,
- HttpStatus,
- Logger,
- NotFoundException,
- Param,
- Post,
- Req,
- Res,
- UseGuards,
- UseInterceptors,
+ BadRequestException,
+ Controller,
+ ForbiddenException,
+ Get,
+ HttpCode,
+ HttpStatus,
+ Logger,
+ NotFoundException,
+ Param,
+ Post,
+ Query,
+ Req,
+ Res,
+ UseGuards,
+ UseInterceptors,
} from '@nestjs/common';
-import {AttachmentService} from './services/attachment.service';
-import {FastifyReply} from 'fastify';
-import {FileInterceptor} from '../../common/interceptors/file.interceptor';
+import { AttachmentService } from './services/attachment.service';
+import { FastifyReply } from 'fastify';
+import { FileInterceptor } from '../../common/interceptors/file.interceptor';
import * as bytes from 'bytes';
-import {AuthUser} from '../../common/decorators/auth-user.decorator';
-import {AuthWorkspace} from '../../common/decorators/auth-workspace.decorator';
-import {JwtAuthGuard} from '../../common/guards/jwt-auth.guard';
-import {User, Workspace} from '@docmost/db/types/entity.types';
-import {StorageService} from '../../integrations/storage/storage.service';
+import { AuthUser } from '../../common/decorators/auth-user.decorator';
+import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
+import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
+import { User, Workspace } from '@docmost/db/types/entity.types';
+import { StorageService } from '../../integrations/storage/storage.service';
import {
- getAttachmentFolderPath,
- validAttachmentTypes,
+ getAttachmentFolderPath,
+ validAttachmentTypes,
} from './attachment.utils';
-import {getMimeType} from '../../common/helpers';
+import { getMimeType } from '../../common/helpers';
import {
- AttachmentType,
- inlineFileExtensions,
- MAX_AVATAR_SIZE,
+ AttachmentType,
+ inlineFileExtensions,
+ MAX_AVATAR_SIZE,
} from './attachment.constants';
import {
- SpaceCaslAction,
- SpaceCaslSubject,
+ SpaceCaslAction,
+ SpaceCaslSubject,
} from '../casl/interfaces/space-ability.type';
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
import {
- WorkspaceCaslAction,
- WorkspaceCaslSubject,
+ WorkspaceCaslAction,
+ WorkspaceCaslSubject,
} from '../casl/interfaces/workspace-ability.type';
import WorkspaceAbilityFactory from '../casl/abilities/workspace-ability.factory';
-import {PageRepo} from '@docmost/db/repos/page/page.repo';
-import {AttachmentRepo} from '@docmost/db/repos/attachment/attachment.repo';
-import {validate as isValidUUID} from 'uuid';
-import {EnvironmentService} from "../../integrations/environment/environment.service";
+import { PageRepo } from '@docmost/db/repos/page/page.repo';
+import { AttachmentRepo } from '@docmost/db/repos/attachment/attachment.repo';
+import { validate as isValidUUID } from 'uuid';
+import { EnvironmentService } from '../../integrations/environment/environment.service';
+import { TokenService } from '../auth/services/token.service';
+import { JwtAttachmentPayload, JwtType } from '../auth/dto/jwt-payload';
@Controller()
export class AttachmentController {
- private readonly logger = new Logger(AttachmentController.name);
+ private readonly logger = new Logger(AttachmentController.name);
- constructor(
- private readonly attachmentService: AttachmentService,
- private readonly storageService: StorageService,
- private readonly workspaceAbility: WorkspaceAbilityFactory,
- private readonly spaceAbility: SpaceAbilityFactory,
- private readonly pageRepo: PageRepo,
- private readonly attachmentRepo: AttachmentRepo,
- private readonly environmentService: EnvironmentService,
- ) {
- }
+ constructor(
+ private readonly attachmentService: AttachmentService,
+ private readonly storageService: StorageService,
+ private readonly workspaceAbility: WorkspaceAbilityFactory,
+ private readonly spaceAbility: SpaceAbilityFactory,
+ private readonly pageRepo: PageRepo,
+ private readonly attachmentRepo: AttachmentRepo,
+ private readonly environmentService: EnvironmentService,
+ private readonly tokenService: TokenService,
+ ) {}
- @UseGuards(JwtAuthGuard)
- @HttpCode(HttpStatus.OK)
- @Post('files/upload')
- @UseInterceptors(FileInterceptor)
- async uploadFile(
- @Req() req: any,
- @Res() res: FastifyReply,
- @AuthUser() user: User,
- @AuthWorkspace() workspace: Workspace,
- ) {
- const maxFileSize = bytes(this.environmentService.getFileUploadSizeLimit());
+ @UseGuards(JwtAuthGuard)
+ @HttpCode(HttpStatus.OK)
+ @Post('files/upload')
+ @UseInterceptors(FileInterceptor)
+ async uploadFile(
+ @Req() req: any,
+ @Res() res: FastifyReply,
+ @AuthUser() user: User,
+ @AuthWorkspace() workspace: Workspace,
+ ) {
+ const maxFileSize = bytes(this.environmentService.getFileUploadSizeLimit());
- let file = null;
- try {
- file = await req.file({
- limits: {fileSize: maxFileSize, fields: 3, files: 1},
- });
- } catch (err: any) {
- this.logger.error(err.message);
- if (err?.statusCode === 413) {
- throw new BadRequestException(
- `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`,
- );
- }
- }
-
- if (!file) {
- throw new BadRequestException('Failed to upload file');
- }
-
- const pageId = file.fields?.pageId?.value;
-
- if (!pageId) {
- throw new BadRequestException('PageId is required');
- }
-
- const page = await this.pageRepo.findById(pageId);
-
- if (!page) {
- throw new NotFoundException('Page not found');
- }
-
- const spaceAbility = await this.spaceAbility.createForUser(
- user,
- page.spaceId,
+ let file = null;
+ try {
+ file = await req.file({
+ limits: { fileSize: maxFileSize, fields: 3, files: 1 },
+ });
+ } catch (err: any) {
+ this.logger.error(err.message);
+ if (err?.statusCode === 413) {
+ throw new BadRequestException(
+ `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`,
);
- if (spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
- throw new ForbiddenException();
- }
-
- const spaceId = page.spaceId;
-
- const attachmentId = file.fields?.attachmentId?.value;
- if (attachmentId && !isValidUUID(attachmentId)) {
- throw new BadRequestException('Invalid attachment id');
- }
-
- try {
- const fileResponse = await this.attachmentService.uploadFile({
- filePromise: file,
- pageId: pageId,
- spaceId: spaceId,
- userId: user.id,
- workspaceId: workspace.id,
- attachmentId: attachmentId,
- });
-
- return res.send(fileResponse);
- } catch (err: any) {
- if (err?.statusCode === 413) {
- const errMessage = `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`;
- this.logger.error(errMessage);
- throw new BadRequestException(errMessage);
- }
- this.logger.error(err);
- throw new BadRequestException('Error processing file upload.');
- }
+ }
}
- @UseGuards(JwtAuthGuard)
- @Get('/files/:fileId/:fileName')
- async getFile(
- @Res() res: FastifyReply,
- @AuthUser() user: User,
- @AuthWorkspace() workspace: Workspace,
- @Param('fileId') fileId: string,
- @Param('fileName') fileName?: string,
+ if (!file) {
+ throw new BadRequestException('Failed to upload file');
+ }
+
+ const pageId = file.fields?.pageId?.value;
+
+ if (!pageId) {
+ throw new BadRequestException('PageId is required');
+ }
+
+ const page = await this.pageRepo.findById(pageId);
+
+ if (!page) {
+ throw new NotFoundException('Page not found');
+ }
+
+ const spaceAbility = await this.spaceAbility.createForUser(
+ user,
+ page.spaceId,
+ );
+ if (spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
+ throw new ForbiddenException();
+ }
+
+ const spaceId = page.spaceId;
+
+ const attachmentId = file.fields?.attachmentId?.value;
+ if (attachmentId && !isValidUUID(attachmentId)) {
+ throw new BadRequestException('Invalid attachment id');
+ }
+
+ try {
+ const fileResponse = await this.attachmentService.uploadFile({
+ filePromise: file,
+ pageId: pageId,
+ spaceId: spaceId,
+ userId: user.id,
+ workspaceId: workspace.id,
+ attachmentId: attachmentId,
+ });
+
+ return res.send(fileResponse);
+ } catch (err: any) {
+ if (err?.statusCode === 413) {
+ const errMessage = `File too large. Exceeds the ${this.environmentService.getFileUploadSizeLimit()} limit`;
+ this.logger.error(errMessage);
+ throw new BadRequestException(errMessage);
+ }
+ this.logger.error(err);
+ throw new BadRequestException('Error processing file upload.');
+ }
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @Get('/files/:fileId/:fileName')
+ async getFile(
+ @Res() res: FastifyReply,
+ @AuthUser() user: User,
+ @AuthWorkspace() workspace: Workspace,
+ @Param('fileId') fileId: string,
+ @Param('fileName') fileName?: string,
+ ) {
+ if (!isValidUUID(fileId)) {
+ throw new NotFoundException('Invalid file id');
+ }
+
+ const attachment = await this.attachmentRepo.findById(fileId);
+ if (
+ !attachment ||
+ attachment.workspaceId !== workspace.id ||
+ !attachment.pageId ||
+ !attachment.spaceId
) {
- if (!isValidUUID(fileId)) {
- throw new NotFoundException('Invalid file id');
- }
+ throw new NotFoundException();
+ }
- const attachment = await this.attachmentRepo.findById(fileId);
- if (
- !attachment ||
- attachment.workspaceId !== workspace.id ||
- !attachment.pageId ||
- !attachment.spaceId
- ) {
- throw new NotFoundException();
- }
+ const spaceAbility = await this.spaceAbility.createForUser(
+ user,
+ attachment.spaceId,
+ );
- const spaceAbility = await this.spaceAbility.createForUser(
- user,
- attachment.spaceId,
+ if (spaceAbility.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
+ throw new ForbiddenException();
+ }
+
+ try {
+ const fileStream = await this.storageService.read(attachment.filePath);
+ res.headers({
+ 'Content-Type': attachment.mimeType,
+ 'Cache-Control': 'private, max-age=3600',
+ });
+
+ if (!inlineFileExtensions.includes(attachment.fileExt)) {
+ res.header(
+ 'Content-Disposition',
+ `attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
);
+ }
- if (spaceAbility.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
- throw new ForbiddenException();
- }
+ return res.send(fileStream);
+ } catch (err) {
+ this.logger.error(err);
+ throw new NotFoundException('File not found');
+ }
+ }
- try {
- const fileStream = await this.storageService.read(attachment.filePath);
- res.headers({
- 'Content-Type': attachment.mimeType,
- 'Cache-Control': 'private, max-age=3600',
- });
-
- if (!inlineFileExtensions.includes(attachment.fileExt)) {
- res.header(
- 'Content-Disposition',
- `attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
- );
- }
-
- return res.send(fileStream);
- } catch (err) {
- this.logger.error(err);
- throw new NotFoundException('File not found');
- }
+ @Get('/files/public/:fileId/:fileName')
+ async getPublicFile(
+ @Res() res: FastifyReply,
+ @AuthWorkspace() workspace: Workspace,
+ @Param('fileId') fileId: string,
+ @Param('fileName') fileName?: string,
+ @Query('jwt') jwtToken?: string,
+ ) {
+ let jwtPayload: JwtAttachmentPayload = null;
+ try {
+ jwtPayload = await this.tokenService.verifyJwt(
+ jwtToken,
+ JwtType.ATTACHMENT,
+ );
+ } catch (err) {
+ throw new BadRequestException(
+ 'Expired or invalid attachment access token',
+ );
}
- @UseGuards(JwtAuthGuard)
- @HttpCode(HttpStatus.OK)
- @Post('attachments/upload-image')
- @UseInterceptors(FileInterceptor)
- async uploadAvatarOrLogo(
- @Req() req: any,
- @Res() res: FastifyReply,
- @AuthUser() user: User,
- @AuthWorkspace() workspace: Workspace,
+ if (
+ !isValidUUID(fileId) ||
+ fileId !== jwtPayload.attachmentId ||
+ jwtPayload.workspaceId !== workspace.id
) {
- const maxFileSize = bytes(MAX_AVATAR_SIZE);
-
- let file = null;
- try {
- file = await req.file({
- limits: {fileSize: maxFileSize, fields: 3, files: 1},
- });
- } catch (err: any) {
- if (err?.statusCode === 413) {
- throw new BadRequestException(
- `File too large. Exceeds the ${MAX_AVATAR_SIZE} limit`,
- );
- }
- }
-
- if (!file) {
- throw new BadRequestException('Invalid file upload');
- }
-
- const attachmentType = file.fields?.type?.value;
- const spaceId = file.fields?.spaceId?.value;
-
- if (!attachmentType) {
- throw new BadRequestException('attachment type is required');
- }
-
- if (
- !validAttachmentTypes.includes(attachmentType) ||
- attachmentType === AttachmentType.File
- ) {
- throw new BadRequestException('Invalid image attachment type');
- }
-
- if (attachmentType === AttachmentType.WorkspaceLogo) {
- const ability = this.workspaceAbility.createForUser(user, workspace);
- if (
- ability.cannot(
- WorkspaceCaslAction.Manage,
- WorkspaceCaslSubject.Settings,
- )
- ) {
- throw new ForbiddenException();
- }
- }
-
- if (attachmentType === AttachmentType.SpaceLogo) {
- if (!spaceId) {
- throw new BadRequestException('spaceId is required');
- }
-
- const spaceAbility = await this.spaceAbility.createForUser(user, spaceId);
- if (
- spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)
- ) {
- throw new ForbiddenException();
- }
- }
-
- try {
- const fileResponse = await this.attachmentService.uploadImage(
- file,
- attachmentType,
- user.id,
- workspace.id,
- spaceId,
- );
-
- return res.send(fileResponse);
- } catch (err: any) {
- this.logger.error(err);
- throw new BadRequestException('Error processing file upload.');
- }
+ throw new NotFoundException('File not found');
}
- @Get('attachments/img/:attachmentType/:fileName')
- async getLogoOrAvatar(
- @Res() res: FastifyReply,
- @AuthWorkspace() workspace: Workspace,
- @Param('attachmentType') attachmentType: AttachmentType,
- @Param('fileName') fileName?: string,
+ const attachment = await this.attachmentRepo.findById(fileId);
+ if (
+ !attachment ||
+ attachment.workspaceId !== workspace.id ||
+ !attachment.pageId ||
+ !attachment.spaceId ||
+ jwtPayload.pageId !== attachment.pageId
) {
- if (
- !validAttachmentTypes.includes(attachmentType) ||
- attachmentType === AttachmentType.File
- ) {
- throw new BadRequestException('Invalid image attachment type');
- }
-
- const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`;
-
- try {
- const fileStream = await this.storageService.read(filePath);
- res.headers({
- 'Content-Type': getMimeType(filePath),
- 'Cache-Control': 'private, max-age=86400',
- });
- return res.send(fileStream);
- } catch (err) {
- this.logger.error(err);
- throw new NotFoundException('File not found');
- }
+ throw new NotFoundException('File not found');
}
+
+ try {
+ const fileStream = await this.storageService.read(attachment.filePath);
+ res.headers({
+ 'Content-Type': attachment.mimeType,
+ 'Cache-Control': 'public, max-age=3600',
+ });
+
+ if (!inlineFileExtensions.includes(attachment.fileExt)) {
+ res.header(
+ 'Content-Disposition',
+ `attachment; filename="${encodeURIComponent(attachment.fileName)}"`,
+ );
+ }
+
+ return res.send(fileStream);
+ } catch (err) {
+ this.logger.error(err);
+ throw new NotFoundException('File not found');
+ }
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @HttpCode(HttpStatus.OK)
+ @Post('attachments/upload-image')
+ @UseInterceptors(FileInterceptor)
+ async uploadAvatarOrLogo(
+ @Req() req: any,
+ @Res() res: FastifyReply,
+ @AuthUser() user: User,
+ @AuthWorkspace() workspace: Workspace,
+ ) {
+ const maxFileSize = bytes(MAX_AVATAR_SIZE);
+
+ let file = null;
+ try {
+ file = await req.file({
+ limits: { fileSize: maxFileSize, fields: 3, files: 1 },
+ });
+ } catch (err: any) {
+ if (err?.statusCode === 413) {
+ throw new BadRequestException(
+ `File too large. Exceeds the ${MAX_AVATAR_SIZE} limit`,
+ );
+ }
+ }
+
+ if (!file) {
+ throw new BadRequestException('Invalid file upload');
+ }
+
+ const attachmentType = file.fields?.type?.value;
+ const spaceId = file.fields?.spaceId?.value;
+
+ if (!attachmentType) {
+ throw new BadRequestException('attachment type is required');
+ }
+
+ if (
+ !validAttachmentTypes.includes(attachmentType) ||
+ attachmentType === AttachmentType.File
+ ) {
+ throw new BadRequestException('Invalid image attachment type');
+ }
+
+ if (attachmentType === AttachmentType.WorkspaceLogo) {
+ const ability = this.workspaceAbility.createForUser(user, workspace);
+ if (
+ ability.cannot(
+ WorkspaceCaslAction.Manage,
+ WorkspaceCaslSubject.Settings,
+ )
+ ) {
+ throw new ForbiddenException();
+ }
+ }
+
+ if (attachmentType === AttachmentType.SpaceLogo) {
+ if (!spaceId) {
+ throw new BadRequestException('spaceId is required');
+ }
+
+ const spaceAbility = await this.spaceAbility.createForUser(user, spaceId);
+ if (
+ spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)
+ ) {
+ throw new ForbiddenException();
+ }
+ }
+
+ try {
+ const fileResponse = await this.attachmentService.uploadImage(
+ file,
+ attachmentType,
+ user.id,
+ workspace.id,
+ spaceId,
+ );
+
+ return res.send(fileResponse);
+ } catch (err: any) {
+ this.logger.error(err);
+ throw new BadRequestException('Error processing file upload.');
+ }
+ }
+
+ @Get('attachments/img/:attachmentType/:fileName')
+ async getLogoOrAvatar(
+ @Res() res: FastifyReply,
+ @AuthWorkspace() workspace: Workspace,
+ @Param('attachmentType') attachmentType: AttachmentType,
+ @Param('fileName') fileName?: string,
+ ) {
+ if (
+ !validAttachmentTypes.includes(attachmentType) ||
+ attachmentType === AttachmentType.File
+ ) {
+ throw new BadRequestException('Invalid image attachment type');
+ }
+
+ const filePath = `${getAttachmentFolderPath(attachmentType, workspace.id)}/${fileName}`;
+
+ try {
+ const fileStream = await this.storageService.read(filePath);
+ res.headers({
+ 'Content-Type': getMimeType(filePath),
+ 'Cache-Control': 'private, max-age=86400',
+ });
+ return res.send(fileStream);
+ } catch (err) {
+ this.logger.error(err);
+ throw new NotFoundException('File not found');
+ }
+ }
}
diff --git a/apps/server/src/core/attachment/attachment.module.ts b/apps/server/src/core/attachment/attachment.module.ts
index 7dc47ed8..f80a2eb7 100644
--- a/apps/server/src/core/attachment/attachment.module.ts
+++ b/apps/server/src/core/attachment/attachment.module.ts
@@ -5,9 +5,10 @@ import { StorageModule } from '../../integrations/storage/storage.module';
import { UserModule } from '../user/user.module';
import { WorkspaceModule } from '../workspace/workspace.module';
import { AttachmentProcessor } from './processors/attachment.processor';
+import { TokenModule } from '../auth/token.module';
@Module({
- imports: [StorageModule, UserModule, WorkspaceModule],
+ imports: [StorageModule, UserModule, WorkspaceModule, TokenModule],
controllers: [AttachmentController],
providers: [AttachmentService, AttachmentProcessor],
})
diff --git a/apps/server/src/core/auth/dto/jwt-payload.ts b/apps/server/src/core/auth/dto/jwt-payload.ts
index ad172b78..b9ce13c4 100644
--- a/apps/server/src/core/auth/dto/jwt-payload.ts
+++ b/apps/server/src/core/auth/dto/jwt-payload.ts
@@ -2,6 +2,7 @@ export enum JwtType {
ACCESS = 'access',
COLLAB = 'collab',
EXCHANGE = 'exchange',
+ ATTACHMENT = 'attachment',
}
export type JwtPayload = {
sub: string;
@@ -21,3 +22,11 @@ export type JwtExchangePayload = {
workspaceId: string;
type: 'exchange';
};
+
+export type JwtAttachmentPayload = {
+ attachmentId: string;
+ pageId: string;
+ workspaceId: string;
+ type: 'attachment';
+};
+
diff --git a/apps/server/src/core/auth/services/token.service.ts b/apps/server/src/core/auth/services/token.service.ts
index ad745290..963e8e65 100644
--- a/apps/server/src/core/auth/services/token.service.ts
+++ b/apps/server/src/core/auth/services/token.service.ts
@@ -6,6 +6,7 @@ import {
import { JwtService } from '@nestjs/jwt';
import { EnvironmentService } from '../../../integrations/environment/environment.service';
import {
+ JwtAttachmentPayload,
JwtCollabPayload,
JwtExchangePayload,
JwtPayload,
@@ -59,6 +60,21 @@ export class TokenService {
return this.jwtService.sign(payload, { expiresIn: '10s' });
}
+ async generateAttachmentToken(opts: {
+ attachmentId: string;
+ pageId: string;
+ workspaceId: string;
+ }): Promise {
+ const { attachmentId, pageId, workspaceId } = opts;
+ const payload: JwtAttachmentPayload = {
+ attachmentId: attachmentId,
+ pageId: pageId,
+ workspaceId: workspaceId,
+ type: JwtType.ATTACHMENT,
+ };
+ return this.jwtService.sign(payload, { expiresIn: '1h' });
+ }
+
async verifyJwt(token: string, tokenType: string) {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.environmentService.getAppSecret(),
diff --git a/apps/server/src/core/casl/abilities/space-ability.factory.ts b/apps/server/src/core/casl/abilities/space-ability.factory.ts
index d2173383..53a57a0c 100644
--- a/apps/server/src/core/casl/abilities/space-ability.factory.ts
+++ b/apps/server/src/core/casl/abilities/space-ability.factory.ts
@@ -45,6 +45,7 @@ function buildSpaceAdminAbility() {
can(SpaceCaslAction.Manage, SpaceCaslSubject.Settings);
can(SpaceCaslAction.Manage, SpaceCaslSubject.Member);
can(SpaceCaslAction.Manage, SpaceCaslSubject.Page);
+ can(SpaceCaslAction.Manage, SpaceCaslSubject.Share);
return build();
}
@@ -55,6 +56,7 @@ function buildSpaceWriterAbility() {
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
can(SpaceCaslAction.Manage, SpaceCaslSubject.Page);
+ can(SpaceCaslAction.Manage, SpaceCaslSubject.Share);
return build();
}
@@ -65,5 +67,6 @@ function buildSpaceReaderAbility() {
can(SpaceCaslAction.Read, SpaceCaslSubject.Settings);
can(SpaceCaslAction.Read, SpaceCaslSubject.Member);
can(SpaceCaslAction.Read, SpaceCaslSubject.Page);
+ can(SpaceCaslAction.Read, SpaceCaslSubject.Share);
return build();
}
diff --git a/apps/server/src/core/casl/interfaces/space-ability.type.ts b/apps/server/src/core/casl/interfaces/space-ability.type.ts
index c927229b..d7801cab 100644
--- a/apps/server/src/core/casl/interfaces/space-ability.type.ts
+++ b/apps/server/src/core/casl/interfaces/space-ability.type.ts
@@ -9,9 +9,11 @@ export enum SpaceCaslSubject {
Settings = 'settings',
Member = 'member',
Page = 'page',
+ Share = 'share',
}
export type ISpaceAbility =
| [SpaceCaslAction, SpaceCaslSubject.Settings]
| [SpaceCaslAction, SpaceCaslSubject.Member]
- | [SpaceCaslAction, SpaceCaslSubject.Page];
+ | [SpaceCaslAction, SpaceCaslSubject.Page]
+ | [SpaceCaslAction, SpaceCaslSubject.Share];
diff --git a/apps/server/src/core/core.module.ts b/apps/server/src/core/core.module.ts
index 182a1420..f7f4f785 100644
--- a/apps/server/src/core/core.module.ts
+++ b/apps/server/src/core/core.module.ts
@@ -15,6 +15,7 @@ import { SpaceModule } from './space/space.module';
import { GroupModule } from './group/group.module';
import { CaslModule } from './casl/casl.module';
import { DomainMiddleware } from '../common/middlewares/domain.middleware';
+import { ShareModule } from './share/share.module';
@Module({
imports: [
@@ -28,6 +29,7 @@ import { DomainMiddleware } from '../common/middlewares/domain.middleware';
SpaceModule,
GroupModule,
CaslModule,
+ ShareModule,
],
})
export class CoreModule implements NestModule {
diff --git a/apps/server/src/core/page/services/page.service.ts b/apps/server/src/core/page/services/page.service.ts
index 43d8f1d2..5e4553c6 100644
--- a/apps/server/src/core/page/services/page.service.ts
+++ b/apps/server/src/core/page/services/page.service.ts
@@ -212,7 +212,7 @@ export class PageService {
trx,
);
const pageIds = await this.pageRepo
- .getPageAndDescendants(rootPage.id)
+ .getPageAndDescendants(rootPage.id, { includeContent: false })
.then((pages) => pages.map((page) => page.id));
// The first id is the root page id
if (pageIds.length > 1) {
@@ -223,6 +223,16 @@ export class PageService {
trx,
);
}
+
+ // update spaceId in shares
+ if (pageIds.length > 0) {
+ await trx
+ .updateTable('shares')
+ .set({ spaceId: spaceId })
+ .where('pageId', 'in', pageIds)
+ .execute();
+ }
+
// Update attachments
await this.attachmentRepo.updateAttachmentsByPageId(
{ spaceId },
diff --git a/apps/server/src/core/share/dto/share.dto.ts b/apps/server/src/core/share/dto/share.dto.ts
new file mode 100644
index 00000000..b6e789ec
--- /dev/null
+++ b/apps/server/src/core/share/dto/share.dto.ts
@@ -0,0 +1,58 @@
+import {
+ IsBoolean,
+ IsNotEmpty,
+ IsOptional,
+ IsString,
+ IsUUID,
+} from 'class-validator';
+
+export class CreateShareDto {
+ @IsString()
+ @IsNotEmpty()
+ pageId: string;
+
+ @IsBoolean()
+ @IsOptional()
+ includeSubPages: boolean;
+
+ @IsOptional()
+ @IsBoolean()
+ searchIndexing: boolean;
+}
+
+export class UpdateShareDto extends CreateShareDto {
+ @IsString()
+ @IsNotEmpty()
+ shareId: string;
+
+ @IsString()
+ @IsOptional()
+ pageId: string;
+}
+
+export class ShareIdDto {
+ @IsString()
+ @IsNotEmpty()
+ shareId: string;
+}
+
+export class SpaceIdDto {
+ @IsUUID()
+ spaceId: string;
+}
+
+export class ShareInfoDto {
+ @IsString()
+ @IsOptional()
+ shareId?: string;
+
+ @IsString()
+ @IsOptional()
+ pageId: string;
+}
+
+export class SharePageIdDto {
+ @IsString()
+ @IsNotEmpty()
+ pageId: string;
+}
diff --git a/apps/server/src/core/share/share-seo.controller.ts b/apps/server/src/core/share/share-seo.controller.ts
new file mode 100644
index 00000000..ecacecf0
--- /dev/null
+++ b/apps/server/src/core/share/share-seo.controller.ts
@@ -0,0 +1,109 @@
+import { Controller, Get, Param, Req, Res } from '@nestjs/common';
+import { ShareService } from './share.service';
+import { FastifyReply, FastifyRequest } from 'fastify';
+import { join } from 'path';
+import * as fs from 'node:fs';
+import { validate as isValidUUID } from 'uuid';
+import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
+import { EnvironmentService } from '../../integrations/environment/environment.service';
+import { Workspace } from '@docmost/db/types/entity.types';
+
+@Controller('share')
+export class ShareSeoController {
+ constructor(
+ private readonly shareService: ShareService,
+ private workspaceRepo: WorkspaceRepo,
+ private environmentService: EnvironmentService,
+ ) {}
+
+ /*
+ * add meta tags to publicly shared pages
+ */
+ @Get([':shareId/p/:pageSlug', 'p/:pageSlug'])
+ async getShare(
+ @Res({ passthrough: false }) res: FastifyReply,
+ @Req() req: FastifyRequest,
+ @Param('shareId') shareId: string,
+ @Param('pageSlug') pageSlug: string,
+ ) {
+ // Nestjs does not to apply middlewares to paths excluded from the global /api prefix
+ // https://github.com/nestjs/nest/issues/9124
+ // https://github.com/nestjs/nest/issues/11572
+ // https://github.com/nestjs/nest/issues/13401
+ // we have to duplicate the DomainMiddleware code here as a workaround
+
+ let workspace: Workspace = null;
+ if (this.environmentService.isSelfHosted()) {
+ workspace = await this.workspaceRepo.findFirst();
+ } else {
+ const header = req.raw.headers.host;
+ const subdomain = header.split('.')[0];
+ workspace = await this.workspaceRepo.findByHostname(subdomain);
+ }
+
+ const clientDistPath = join(
+ __dirname,
+ '..',
+ '..',
+ '..',
+ '..',
+ 'client/dist',
+ );
+
+ if (fs.existsSync(clientDistPath)) {
+ const indexFilePath = join(clientDistPath, 'index.html');
+
+ if (!workspace) {
+ return this.sendIndex(indexFilePath, res);
+ }
+
+ const pageId = this.extractPageSlugId(pageSlug);
+
+ const share = await this.shareService.getShareForPage(
+ pageId,
+ workspace.id,
+ );
+
+ if (!share) {
+ return this.sendIndex(indexFilePath, res);
+ }
+
+ const rawTitle = share.sharedPage.title ?? 'untitled';
+ const metaTitle =
+ rawTitle.length > 80 ? `${rawTitle.slice(0, 77)}…` : rawTitle;
+
+ const metaTagVar = '';
+
+ const metaTags = [
+ ` `,
+ ` `,
+ !share.searchIndexing ? ` ` : '',
+ ]
+ .filter(Boolean)
+ .join('\n ');
+
+ const html = fs.readFileSync(indexFilePath, 'utf8');
+ const transformedHtml = html
+ .replace(/[\s\S]*?<\/title>/i, `${metaTitle} `)
+ .replace(metaTagVar, metaTags);
+
+ res.type('text/html').send(transformedHtml);
+ }
+ }
+
+ sendIndex(indexFilePath: string, res: FastifyReply) {
+ const stream = fs.createReadStream(indexFilePath);
+ res.type('text/html').send(stream);
+ }
+
+ extractPageSlugId(slug: string): string {
+ if (!slug) {
+ return undefined;
+ }
+ if (isValidUUID(slug)) {
+ return slug;
+ }
+ const parts = slug.split('-');
+ return parts.length > 1 ? parts[parts.length - 1] : slug;
+ }
+}
diff --git a/apps/server/src/core/share/share.controller.ts b/apps/server/src/core/share/share.controller.ts
new file mode 100644
index 00000000..5e8debe0
--- /dev/null
+++ b/apps/server/src/core/share/share.controller.ts
@@ -0,0 +1,171 @@
+import {
+ BadRequestException,
+ Body,
+ Controller,
+ ForbiddenException,
+ HttpCode,
+ HttpStatus,
+ NotFoundException,
+ Post,
+ UseGuards,
+} from '@nestjs/common';
+import { AuthUser } from '../../common/decorators/auth-user.decorator';
+import { User, Workspace } from '@docmost/db/types/entity.types';
+import {
+ SpaceCaslAction,
+ SpaceCaslSubject,
+} from '../casl/interfaces/space-ability.type';
+import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
+import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
+import { ShareService } from './share.service';
+import {
+ CreateShareDto,
+ ShareIdDto,
+ ShareInfoDto,
+ SharePageIdDto,
+ UpdateShareDto,
+} from './dto/share.dto';
+import { PageRepo } from '@docmost/db/repos/page/page.repo';
+import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
+import { Public } from '../../common/decorators/public.decorator';
+import { ShareRepo } from '@docmost/db/repos/share/share.repo';
+import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
+
+@UseGuards(JwtAuthGuard)
+@Controller('shares')
+export class ShareController {
+ constructor(
+ private readonly shareService: ShareService,
+ private readonly spaceAbility: SpaceAbilityFactory,
+ private readonly shareRepo: ShareRepo,
+ private readonly pageRepo: PageRepo,
+ ) {}
+
+ @HttpCode(HttpStatus.OK)
+ @Post('/')
+ async getShares(
+ @AuthUser() user: User,
+ @Body() pagination: PaginationOptions,
+ ) {
+ return this.shareRepo.getShares(user.id, pagination);
+ }
+
+ @Public()
+ @HttpCode(HttpStatus.OK)
+ @Post('/page-info')
+ async getSharedPageInfo(
+ @Body() dto: ShareInfoDto,
+ @AuthWorkspace() workspace: Workspace,
+ ) {
+ if (!dto.pageId && !dto.shareId) {
+ throw new BadRequestException();
+ }
+
+ return this.shareService.getSharedPage(dto, workspace.id);
+ }
+
+ @Public()
+ @HttpCode(HttpStatus.OK)
+ @Post('/info')
+ async getShare(@Body() dto: ShareIdDto) {
+ const share = await this.shareRepo.findById(dto.shareId, {
+ includeSharedPage: true,
+ });
+
+ if (!share) {
+ throw new NotFoundException('Share not found');
+ }
+
+ return share;
+ }
+
+ @HttpCode(HttpStatus.OK)
+ @Post('/for-page')
+ async getShareForPage(
+ @Body() dto: SharePageIdDto,
+ @AuthUser() user: User,
+ @AuthWorkspace() workspace: Workspace,
+ ) {
+ const page = await this.pageRepo.findById(dto.pageId);
+ if (!page) {
+ throw new NotFoundException('Shared page not found');
+ }
+
+ const ability = await this.spaceAbility.createForUser(user, page.spaceId);
+ if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Share)) {
+ throw new ForbiddenException();
+ }
+
+ return this.shareService.getShareForPage(page.id, workspace.id);
+ }
+
+ @HttpCode(HttpStatus.OK)
+ @Post('create')
+ async create(
+ @Body() createShareDto: CreateShareDto,
+ @AuthUser() user: User,
+ @AuthWorkspace() workspace: Workspace,
+ ) {
+ const page = await this.pageRepo.findById(createShareDto.pageId);
+
+ if (!page || workspace.id !== page.workspaceId) {
+ throw new NotFoundException('Page not found');
+ }
+
+ const ability = await this.spaceAbility.createForUser(user, page.spaceId);
+ if (ability.cannot(SpaceCaslAction.Create, SpaceCaslSubject.Share)) {
+ throw new ForbiddenException();
+ }
+
+ return this.shareService.createShare({
+ page,
+ authUserId: user.id,
+ workspaceId: workspace.id,
+ createShareDto,
+ });
+ }
+
+ @HttpCode(HttpStatus.OK)
+ @Post('update')
+ async update(@Body() updateShareDto: UpdateShareDto, @AuthUser() user: User) {
+ const share = await this.shareRepo.findById(updateShareDto.shareId);
+
+ if (!share) {
+ throw new NotFoundException('Share not found');
+ }
+
+ const ability = await this.spaceAbility.createForUser(user, share.spaceId);
+ if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Share)) {
+ throw new ForbiddenException();
+ }
+
+ return this.shareService.updateShare(share.id, updateShareDto);
+ }
+
+ @HttpCode(HttpStatus.OK)
+ @Post('delete')
+ async delete(@Body() shareIdDto: ShareIdDto, @AuthUser() user: User) {
+ const share = await this.shareRepo.findById(shareIdDto.shareId);
+
+ if (!share) {
+ throw new NotFoundException('Share not found');
+ }
+
+ const ability = await this.spaceAbility.createForUser(user, share.spaceId);
+ if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Share)) {
+ throw new ForbiddenException();
+ }
+
+ await this.shareRepo.deleteShare(share.id);
+ }
+
+ @Public()
+ @HttpCode(HttpStatus.OK)
+ @Post('/tree')
+ async getSharePageTree(
+ @Body() dto: ShareIdDto,
+ @AuthWorkspace() workspace: Workspace,
+ ) {
+ return this.shareService.getShareTree(dto.shareId, workspace.id);
+ }
+}
diff --git a/apps/server/src/core/share/share.module.ts b/apps/server/src/core/share/share.module.ts
new file mode 100644
index 00000000..2ba9764e
--- /dev/null
+++ b/apps/server/src/core/share/share.module.ts
@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common';
+import { ShareController } from './share.controller';
+import { ShareService } from './share.service';
+import { TokenModule } from '../auth/token.module';
+import { ShareSeoController } from './share-seo.controller';
+
+@Module({
+ imports: [TokenModule],
+ controllers: [ShareController, ShareSeoController],
+ providers: [ShareService],
+ exports: [ShareService],
+})
+export class ShareModule {}
diff --git a/apps/server/src/core/share/share.service.ts b/apps/server/src/core/share/share.service.ts
new file mode 100644
index 00000000..a9140c0b
--- /dev/null
+++ b/apps/server/src/core/share/share.service.ts
@@ -0,0 +1,297 @@
+import {
+ BadRequestException,
+ Injectable,
+ Logger,
+ NotFoundException,
+} from '@nestjs/common';
+import { CreateShareDto, ShareInfoDto, UpdateShareDto } from './dto/share.dto';
+import { InjectKysely } from 'nestjs-kysely';
+import { KyselyDB } from '@docmost/db/types/kysely.types';
+import { nanoIdGen } from '../../common/helpers';
+import { PageRepo } from '@docmost/db/repos/page/page.repo';
+import { TokenService } from '../auth/services/token.service';
+import { jsonToNode } from '../../collaboration/collaboration.util';
+import {
+ getAttachmentIds,
+ getProsemirrorContent,
+ isAttachmentNode,
+} from '../../common/helpers/prosemirror/utils';
+import { Node } from '@tiptap/pm/model';
+import { ShareRepo } from '@docmost/db/repos/share/share.repo';
+import { updateAttachmentAttr } from './share.util';
+import { Page } from '@docmost/db/types/entity.types';
+import { validate as isValidUUID } from 'uuid';
+import { sql } from 'kysely';
+
+@Injectable()
+export class ShareService {
+ private readonly logger = new Logger(ShareService.name);
+
+ constructor(
+ private readonly shareRepo: ShareRepo,
+ private readonly pageRepo: PageRepo,
+ @InjectKysely() private readonly db: KyselyDB,
+ private readonly tokenService: TokenService,
+ ) {}
+
+ async getShareTree(shareId: string, workspaceId: string) {
+ const share = await this.shareRepo.findById(shareId);
+ if (!share || share.workspaceId !== workspaceId) {
+ throw new NotFoundException('Share not found');
+ }
+
+ if (share.includeSubPages) {
+ const pageList = await this.pageRepo.getPageAndDescendants(share.pageId, {
+ includeContent: false,
+ });
+
+ return { share, pageTree: pageList };
+ } else {
+ return { share, pageTree: [] };
+ }
+ }
+
+ async createShare(opts: {
+ authUserId: string;
+ workspaceId: string;
+ page: Page;
+ createShareDto: CreateShareDto;
+ }) {
+ const { authUserId, workspaceId, page, createShareDto } = opts;
+
+ try {
+ const shares = await this.shareRepo.findByPageId(page.id);
+ if (shares) {
+ return shares;
+ }
+
+ return await this.shareRepo.insertShare({
+ key: nanoIdGen().toLowerCase(),
+ pageId: page.id,
+ includeSubPages: createShareDto.includeSubPages || true,
+ searchIndexing: createShareDto.searchIndexing || true,
+ creatorId: authUserId,
+ spaceId: page.spaceId,
+ workspaceId,
+ });
+ } catch (err) {
+ this.logger.error(err);
+ throw new BadRequestException('Failed to share page');
+ }
+ }
+
+ async updateShare(shareId: string, updateShareDto: UpdateShareDto) {
+ try {
+ return this.shareRepo.updateShare(
+ {
+ includeSubPages: updateShareDto.includeSubPages,
+ searchIndexing: updateShareDto.searchIndexing,
+ },
+ shareId,
+ );
+ } catch (err) {
+ this.logger.error(err);
+ throw new BadRequestException('Failed to update share');
+ }
+ }
+
+ async getSharedPage(dto: ShareInfoDto, workspaceId: string) {
+ const share = await this.getShareForPage(dto.pageId, workspaceId);
+
+ if (!share) {
+ throw new NotFoundException('Shared page not found');
+ }
+
+ const page = await this.pageRepo.findById(dto.pageId, {
+ includeContent: true,
+ includeCreator: true,
+ });
+
+ page.content = await this.updatePublicAttachments(page);
+
+ if (!page) {
+ throw new NotFoundException('Shared page not found');
+ }
+
+ return { page, share };
+ }
+
+ async getShareForPage(pageId: string, workspaceId: string) {
+ // here we try to check if a page was shared directly or if it inherits the share from its closest shared ancestor
+ const share = await this.db
+ .withRecursive('page_hierarchy', (cte) =>
+ cte
+ .selectFrom('pages')
+ .select([
+ 'id',
+ 'slugId',
+ 'pages.title',
+ 'pages.icon',
+ 'parentPageId',
+ sql`0`.as('level'),
+ ])
+ .where(isValidUUID(pageId) ? 'id' : 'slugId', '=', pageId)
+ .unionAll((union) =>
+ union
+ .selectFrom('pages as p')
+ .select([
+ 'p.id',
+ 'p.slugId',
+ 'p.title',
+ 'p.icon',
+ 'p.parentPageId',
+ // Increase the level by 1 for each ancestor.
+ sql`ph.level + 1`.as('level'),
+ ])
+ .innerJoin('page_hierarchy as ph', 'ph.parentPageId', 'p.id'),
+ ),
+ )
+ .selectFrom('page_hierarchy')
+ .leftJoin('shares', 'shares.pageId', 'page_hierarchy.id')
+ .select([
+ 'page_hierarchy.id as sharedPageId',
+ 'page_hierarchy.slugId as sharedPageSlugId',
+ 'page_hierarchy.title as sharedPageTitle',
+ 'page_hierarchy.icon as sharedPageIcon',
+ 'page_hierarchy.level as level',
+ 'shares.id',
+ 'shares.key',
+ 'shares.pageId',
+ 'shares.includeSubPages',
+ 'shares.searchIndexing',
+ 'shares.creatorId',
+ 'shares.spaceId',
+ 'shares.workspaceId',
+ 'shares.createdAt',
+ 'shares.updatedAt',
+ ])
+ .where('shares.id', 'is not', null)
+ .orderBy('page_hierarchy.level', 'asc')
+ .executeTakeFirst();
+
+ if (!share || share.workspaceId != workspaceId) {
+ return undefined;
+ }
+
+ if (share.level === 1 && !share.includeSubPages) {
+ // we can only show a page if its shared ancestor permits it
+ return undefined;
+ }
+
+ return {
+ id: share.id,
+ key: share.key,
+ includeSubPages: share.includeSubPages,
+ searchIndexing: share.searchIndexing,
+ pageId: share.pageId,
+ creatorId: share.creatorId,
+ spaceId: share.spaceId,
+ workspaceId: share.workspaceId,
+ createdAt: share.createdAt,
+ level: share.level,
+ sharedPage: {
+ id: share.sharedPageId,
+ slugId: share.sharedPageSlugId,
+ title: share.sharedPageTitle,
+ icon: share.sharedPageIcon,
+ },
+ };
+ }
+
+ async getShareAncestorPage(
+ ancestorPageId: string,
+ childPageId: string,
+ ): Promise {
+ let ancestor = null;
+ try {
+ ancestor = await this.db
+ .withRecursive('page_ancestors', (db) =>
+ db
+ .selectFrom('pages')
+ .select([
+ 'id',
+ 'slugId',
+ 'title',
+ 'parentPageId',
+ 'spaceId',
+ (eb) =>
+ eb
+ .case()
+ .when(eb.ref('id'), '=', ancestorPageId)
+ .then(true)
+ .else(false)
+ .end()
+ .as('found'),
+ ])
+ .where(
+ isValidUUID(childPageId) ? 'id' : 'slugId',
+ '=',
+ childPageId,
+ )
+ .unionAll((exp) =>
+ exp
+ .selectFrom('pages as p')
+ .select([
+ 'p.id',
+ 'p.slugId',
+ 'p.title',
+ 'p.parentPageId',
+ 'p.spaceId',
+ (eb) =>
+ eb
+ .case()
+ .when(eb.ref('p.id'), '=', ancestorPageId)
+ .then(true)
+ .else(false)
+ .end()
+ .as('found'),
+ ])
+ .innerJoin('page_ancestors as pa', 'pa.parentPageId', 'p.id')
+ // Continue recursing only when the target ancestor hasn't been found on that branch.
+ .where('pa.found', '=', false),
+ ),
+ )
+ .selectFrom('page_ancestors')
+ .selectAll()
+ .where('found', '=', true)
+ .limit(1)
+ .executeTakeFirst();
+ } catch (err) {
+ // empty
+ }
+
+ return ancestor;
+ }
+
+ async updatePublicAttachments(page: Page): Promise {
+ const prosemirrorJson = getProsemirrorContent(page.content);
+ const attachmentIds = getAttachmentIds(prosemirrorJson);
+ const attachmentMap = new Map();
+
+ await Promise.all(
+ attachmentIds.map(async (attachmentId: string) => {
+ const token = await this.tokenService.generateAttachmentToken({
+ attachmentId,
+ pageId: page.id,
+ workspaceId: page.workspaceId,
+ });
+ attachmentMap.set(attachmentId, token);
+ }),
+ );
+
+ const doc = jsonToNode(prosemirrorJson);
+
+ doc?.descendants((node: Node) => {
+ if (!isAttachmentNode(node.type.name)) return;
+
+ const attachmentId = node.attrs.attachmentId;
+ const token = attachmentMap.get(attachmentId);
+ if (!token) return;
+
+ updateAttachmentAttr(node, 'src', token);
+ updateAttachmentAttr(node, 'url', token);
+ });
+
+ return doc.toJSON();
+ }
+}
diff --git a/apps/server/src/core/share/share.util.ts b/apps/server/src/core/share/share.util.ts
new file mode 100644
index 00000000..e21f55aa
--- /dev/null
+++ b/apps/server/src/core/share/share.util.ts
@@ -0,0 +1,22 @@
+import { Node } from '@tiptap/pm/model';
+
+export function updateAttachmentAttr(
+ node: Node,
+ attr: 'src' | 'url',
+ token: string,
+) {
+ const attrVal = node.attrs[attr];
+ if (
+ attrVal &&
+ (attrVal.startsWith('/files') || attrVal.startsWith('/api/files'))
+ ) {
+ // @ts-ignore
+ node.attrs[attr] = updateAttachmentUrl(attrVal, token);
+ }
+}
+
+function updateAttachmentUrl(src: string, jwtToken: string) {
+ const updatedSrc = src.replace('/files/', '/files/public/');
+ const separator = updatedSrc.includes('?') ? '&' : '?';
+ return `${updatedSrc}${separator}jwt=${jwtToken}`;
+}
diff --git a/apps/server/src/database/database.module.ts b/apps/server/src/database/database.module.ts
index 930bb59b..68c35dd3 100644
--- a/apps/server/src/database/database.module.ts
+++ b/apps/server/src/database/database.module.ts
@@ -24,6 +24,7 @@ import * as process from 'node:process';
import { MigrationService } from '@docmost/db/services/migration.service';
import { UserTokenRepo } from './repos/user-token/user-token.repo';
import { BacklinkRepo } from '@docmost/db/repos/backlink/backlink.repo';
+import { ShareRepo } from '@docmost/db/repos/share/share.repo';
// https://github.com/brianc/node-postgres/issues/811
types.setTypeParser(types.builtins.INT8, (val) => Number(val));
@@ -74,6 +75,7 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val));
AttachmentRepo,
UserTokenRepo,
BacklinkRepo,
+ ShareRepo
],
exports: [
WorkspaceRepo,
@@ -88,6 +90,7 @@ types.setTypeParser(types.builtins.INT8, (val) => Number(val));
AttachmentRepo,
UserTokenRepo,
BacklinkRepo,
+ ShareRepo
],
})
export class DatabaseModule
diff --git a/apps/server/src/database/migrations/20250408T191830-shares.ts b/apps/server/src/database/migrations/20250408T191830-shares.ts
new file mode 100644
index 00000000..39d91454
--- /dev/null
+++ b/apps/server/src/database/migrations/20250408T191830-shares.ts
@@ -0,0 +1,38 @@
+import { Kysely, sql } from 'kysely';
+
+export async function up(db: Kysely): Promise {
+ await db.schema
+ .createTable('shares')
+ .addColumn('id', 'uuid', (col) =>
+ col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
+ )
+ .addColumn('key', 'varchar', (col) => col.notNull())
+ .addColumn('page_id', 'uuid', (col) =>
+ col.references('pages.id').onDelete('cascade'),
+ )
+ .addColumn('include_sub_pages', 'boolean', (col) => col.defaultTo(false))
+ .addColumn('search_indexing', 'boolean', (col) => col.defaultTo(false))
+ .addColumn('creator_id', 'uuid', (col) => col.references('users.id'))
+ .addColumn('space_id', 'uuid', (col) =>
+ col.references('spaces.id').onDelete('cascade').notNull(),
+ )
+ .addColumn('workspace_id', 'uuid', (col) =>
+ col.references('workspaces.id').onDelete('cascade').notNull(),
+ )
+ .addColumn('created_at', 'timestamptz', (col) =>
+ col.notNull().defaultTo(sql`now()`),
+ )
+ .addColumn('updated_at', 'timestamptz', (col) =>
+ col.notNull().defaultTo(sql`now()`),
+ )
+ .addColumn('deleted_at', 'timestamptz', (col) => col)
+ .addUniqueConstraint('shares_key_workspace_id_unique', [
+ 'key',
+ 'workspace_id',
+ ])
+ .execute();
+}
+
+export async function down(db: Kysely): Promise {
+ await db.schema.dropTable('shares').execute();
+}
diff --git a/apps/server/src/database/repos/page/page.repo.ts b/apps/server/src/database/repos/page/page.repo.ts
index 850fb2d1..8f06c4d4 100644
--- a/apps/server/src/database/repos/page/page.repo.ts
+++ b/apps/server/src/database/repos/page/page.repo.ts
@@ -211,7 +211,10 @@ export class PageRepo {
).as('contributors');
}
- async getPageAndDescendants(parentPageId: string) {
+ async getPageAndDescendants(
+ parentPageId: string,
+ opts: { includeContent: boolean },
+ ) {
return this.db
.withRecursive('page_hierarchy', (db) =>
db
@@ -221,11 +224,12 @@ export class PageRepo {
'slugId',
'title',
'icon',
- 'content',
+ 'position',
'parentPageId',
'spaceId',
'workspaceId',
])
+ .$if(opts?.includeContent, (qb) => qb.select('content'))
.where('id', '=', parentPageId)
.unionAll((exp) =>
exp
@@ -235,11 +239,12 @@ export class PageRepo {
'p.slugId',
'p.title',
'p.icon',
- 'p.content',
+ 'p.position',
'p.parentPageId',
'p.spaceId',
'p.workspaceId',
])
+ .$if(opts?.includeContent, (qb) => qb.select('content'))
.innerJoin('page_hierarchy as ph', 'p.parentPageId', 'ph.id'),
),
)
diff --git a/apps/server/src/database/repos/share/share.repo.ts b/apps/server/src/database/repos/share/share.repo.ts
new file mode 100644
index 00000000..c2943c07
--- /dev/null
+++ b/apps/server/src/database/repos/share/share.repo.ts
@@ -0,0 +1,242 @@
+import { Injectable } from '@nestjs/common';
+import { InjectKysely } from 'nestjs-kysely';
+import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
+import { dbOrTx } from '../../utils';
+import {
+ InsertableShare,
+ Share,
+ UpdatableShare,
+} from '@docmost/db/types/entity.types';
+import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
+import { executeWithPagination } from '@docmost/db/pagination/pagination';
+import { validate as isValidUUID } from 'uuid';
+import { ExpressionBuilder, sql } from 'kysely';
+import { DB } from '@docmost/db/types/db';
+import { jsonObjectFrom } from 'kysely/helpers/postgres';
+import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
+
+@Injectable()
+export class ShareRepo {
+ constructor(
+ @InjectKysely() private readonly db: KyselyDB,
+ private spaceMemberRepo: SpaceMemberRepo,
+ ) {}
+
+ private baseFields: Array = [
+ 'id',
+ 'key',
+ 'pageId',
+ 'includeSubPages',
+ 'searchIndexing',
+ 'creatorId',
+ 'spaceId',
+ 'workspaceId',
+ 'createdAt',
+ 'updatedAt',
+ 'deletedAt',
+ ];
+
+ async findById(
+ shareId: string,
+ opts?: {
+ includeSharedPage?: boolean;
+ includeCreator?: boolean;
+ withLock?: boolean;
+ trx?: KyselyTransaction;
+ },
+ ): Promise {
+ const db = dbOrTx(this.db, opts?.trx);
+
+ let query = db.selectFrom('shares').select(this.baseFields);
+
+ if (opts?.includeSharedPage) {
+ query = query.select((eb) => this.withSharedPage(eb));
+ }
+
+ if (opts?.includeCreator) {
+ query = query.select((eb) => this.withCreator(eb));
+ }
+
+ if (opts?.withLock && opts?.trx) {
+ query = query.forUpdate();
+ }
+
+ if (isValidUUID(shareId)) {
+ query = query.where('id', '=', shareId);
+ } else {
+ query = query.where(sql`LOWER(key)`, '=', shareId.toLowerCase());
+ }
+
+ return query.executeTakeFirst();
+ }
+
+ async findByPageId(
+ pageId: string,
+ opts?: {
+ includeCreator?: boolean;
+ withLock?: boolean;
+ trx?: KyselyTransaction;
+ },
+ ): Promise {
+ const db = dbOrTx(this.db, opts?.trx);
+
+ let query = db
+ .selectFrom('shares')
+ .select(this.baseFields)
+ .where('pageId', '=', pageId);
+
+ if (opts?.includeCreator) {
+ query = query.select((eb) => this.withCreator(eb));
+ }
+
+ if (opts?.withLock && opts?.trx) {
+ query = query.forUpdate();
+ }
+ return query.executeTakeFirst();
+ }
+
+ async updateShare(
+ updatableShare: UpdatableShare,
+ shareId: string,
+ trx?: KyselyTransaction,
+ ) {
+ return dbOrTx(this.db, trx)
+ .updateTable('shares')
+ .set({ ...updatableShare, updatedAt: new Date() })
+ .where(
+ isValidUUID(shareId) ? 'id' : sql`LOWER(key)`,
+ '=',
+ shareId.toLowerCase(),
+ )
+ .returning(this.baseFields)
+ .executeTakeFirst();
+ }
+
+ async insertShare(
+ insertableShare: InsertableShare,
+ trx?: KyselyTransaction,
+ ): Promise {
+ const db = dbOrTx(this.db, trx);
+ return db
+ .insertInto('shares')
+ .values(insertableShare)
+ .returning(this.baseFields)
+ .executeTakeFirst();
+ }
+
+ async deleteShare(shareId: string): Promise {
+ let query = this.db.deleteFrom('shares');
+
+ if (isValidUUID(shareId)) {
+ query = query.where('id', '=', shareId);
+ } else {
+ query = query.where(sql`LOWER(key)`, '=', shareId.toLowerCase());
+ }
+
+ await query.execute();
+ }
+
+ async getShares(userId: string, pagination: PaginationOptions) {
+ const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(userId);
+
+ const query = this.db
+ .selectFrom('shares')
+ .select(this.baseFields)
+ .select((eb) => this.withPage(eb))
+ .select((eb) => this.withSpace(eb, userId))
+ .select((eb) => this.withCreator(eb))
+ .where('spaceId', 'in', userSpaceIds)
+ .orderBy('updatedAt', 'desc');
+
+ const hasEmptyIds = userSpaceIds.length === 0;
+ const result = executeWithPagination(query, {
+ page: pagination.page,
+ perPage: pagination.limit,
+ hasEmptyIds,
+ });
+
+ return result;
+ }
+
+ withPage(eb: ExpressionBuilder) {
+ return jsonObjectFrom(
+ eb
+ .selectFrom('pages')
+ .select(['pages.id', 'pages.title', 'pages.slugId', 'pages.icon'])
+ .whereRef('pages.id', '=', 'shares.pageId'),
+ ).as('page');
+ }
+
+ withSpace(eb: ExpressionBuilder, userId?: string) {
+ return jsonObjectFrom(
+ eb
+ .selectFrom('spaces')
+ .select(['spaces.id', 'spaces.name', 'spaces.slug'])
+ .$if(Boolean(userId), (qb) =>
+ qb.select((eb) => this.withUserSpaceRole(eb, userId)),
+ )
+ .whereRef('spaces.id', '=', 'shares.spaceId'),
+ ).as('space');
+ }
+
+ withUserSpaceRole(eb: ExpressionBuilder, userId: string) {
+ return eb
+ .selectFrom(
+ eb
+ .selectFrom('spaceMembers')
+ .select(['spaceMembers.role'])
+ .whereRef('spaceMembers.spaceId', '=', 'spaces.id')
+ .where('spaceMembers.userId', '=', userId)
+ .unionAll(
+ eb
+ .selectFrom('spaceMembers')
+ .innerJoin(
+ 'groupUsers',
+ 'groupUsers.groupId',
+ 'spaceMembers.groupId',
+ )
+ .select(['spaceMembers.role'])
+ .whereRef('spaceMembers.spaceId', '=', 'spaces.id')
+ .where('groupUsers.userId', '=', userId),
+ )
+ .as('roles_union'),
+ )
+ .select('roles_union.role')
+ .orderBy(
+ sql`CASE roles_union.role
+ WHEN 'admin' THEN 3
+ WHEN 'writer' THEN 2
+ WHEN 'reader' THEN 1
+ ELSE 0
+ END`,
+
+ 'desc',
+ )
+ .limit(1)
+ .as('userRole');
+ }
+
+ withCreator(eb: ExpressionBuilder) {
+ return jsonObjectFrom(
+ eb
+ .selectFrom('users')
+ .select(['users.id', 'users.name', 'users.avatarUrl'])
+ .whereRef('users.id', '=', 'shares.creatorId'),
+ ).as('creator');
+ }
+
+ withSharedPage(eb: ExpressionBuilder) {
+ return jsonObjectFrom(
+ eb
+ .selectFrom('pages')
+ .select([
+ 'pages.id',
+ 'pages.slugId',
+ 'pages.title',
+ 'pages.icon',
+ 'pages.parentPageId',
+ ])
+ .whereRef('pages.id', '=', 'shares.pageId'),
+ ).as('sharedPage');
+ }
+}
diff --git a/apps/server/src/database/types/db.d.ts b/apps/server/src/database/types/db.d.ts
index eae94943..8c4cbd57 100644
--- a/apps/server/src/database/types/db.d.ts
+++ b/apps/server/src/database/types/db.d.ts
@@ -183,6 +183,20 @@ export interface Pages {
ydoc: Buffer | null;
}
+export interface Shares {
+ createdAt: Generated;
+ creatorId: string | null;
+ deletedAt: Timestamp | null;
+ id: Generated;
+ includeSubPages: Generated;
+ key: string;
+ pageId: string | null;
+ searchIndexing: Generated;
+ spaceId: string;
+ updatedAt: Generated;
+ workspaceId: string;
+}
+
export interface SpaceMembers {
addedById: string | null;
createdAt: Generated;
@@ -288,6 +302,7 @@ export interface DB {
groupUsers: GroupUsers;
pageHistory: PageHistory;
pages: Pages;
+ shares: Shares;
spaceMembers: SpaceMembers;
spaces: Spaces;
users: Users;
diff --git a/apps/server/src/database/types/entity.types.ts b/apps/server/src/database/types/entity.types.ts
index 8abd9f98..6cb55a11 100644
--- a/apps/server/src/database/types/entity.types.ts
+++ b/apps/server/src/database/types/entity.types.ts
@@ -16,6 +16,7 @@ import {
Billing as BillingSubscription,
AuthProviders,
AuthAccounts,
+ Shares,
} from './db';
// Workspace
@@ -101,3 +102,8 @@ export type UpdatableAuthProvider = Updateable>;
export type AuthAccount = Selectable;
export type InsertableAuthAccount = Insertable;
export type UpdatableAuthAccount = Updateable>;
+
+// Share
+export type Share = Selectable;
+export type InsertableShare = Insertable;
+export type UpdatableShare = Updateable>;
diff --git a/apps/server/src/integrations/export/export.service.ts b/apps/server/src/integrations/export/export.service.ts
index 09fdb5fd..4bb5146f 100644
--- a/apps/server/src/integrations/export/export.service.ts
+++ b/apps/server/src/integrations/export/export.service.ts
@@ -15,10 +15,8 @@ import { StorageService } from '../storage/storage.service';
import {
buildTree,
computeLocalPath,
- getAttachmentIds,
getExportExtension,
getPageTitle,
- getProsemirrorContent,
PageExportTree,
replaceInternalLinks,
updateAttachmentUrlsToLocalPaths,
@@ -29,6 +27,10 @@ import { EditorState } from '@tiptap/pm/state';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import slugify = require('@sindresorhus/slugify');
import { EnvironmentService } from '../environment/environment.service';
+import {
+ getAttachmentIds,
+ getProsemirrorContent,
+} from '../../common/helpers/prosemirror/utils';
@Injectable()
export class ExportService {
@@ -76,8 +78,11 @@ export class ExportService {
`;
}
- if (format === ExportFormat.Markdown) {
- const newPageHtml = pageHtml.replace(/]*>[\s\S]*?<\/colgroup>/gmi, '');
+ if (format === ExportFormat.Markdown) {
+ const newPageHtml = pageHtml.replace(
+ / ]*>[\s\S]*?<\/colgroup>/gim,
+ '',
+ );
return turndown(newPageHtml);
}
@@ -85,7 +90,9 @@ export class ExportService {
}
async exportPageWithChildren(pageId: string, format: string) {
- const pages = await this.pageRepo.getPageAndDescendants(pageId);
+ const pages = await this.pageRepo.getPageAndDescendants(pageId, {
+ includeContent: true,
+ });
if (!pages || pages.length === 0) {
throw new BadRequestException('No pages to export');
@@ -260,14 +267,7 @@ export class ExportService {
const pages = await this.db
.selectFrom('pages')
- .select([
- 'id',
- 'slugId',
- 'title',
- 'creatorId',
- 'spaceId',
- 'workspaceId',
- ])
+ .select(['id', 'slugId', 'title', 'creatorId', 'spaceId', 'workspaceId'])
.select((eb) => this.pageRepo.withSpace(eb))
.where('id', 'in', pageMentionIds)
.where('workspaceId', '=', workspaceId)
diff --git a/apps/server/src/integrations/export/utils.ts b/apps/server/src/integrations/export/utils.ts
index f99f337a..fe1815b0 100644
--- a/apps/server/src/integrations/export/utils.ts
+++ b/apps/server/src/integrations/export/utils.ts
@@ -4,6 +4,7 @@ import { Node } from '@tiptap/pm/model';
import { validate as isValidUUID } from 'uuid';
import * as path from 'path';
import { Page } from '@docmost/db/types/entity.types';
+import { isAttachmentNode } from '../../common/helpers/prosemirror/utils';
export type PageExportTree = Record;
@@ -25,43 +26,6 @@ export function getPageTitle(title: string) {
return title ? title : 'untitled';
}
-export function getProsemirrorContent(content: any) {
- return (
- content ?? {
- type: 'doc',
- content: [{ type: 'paragraph', attrs: { textAlign: 'left' } }],
- }
- );
-}
-
-export function getAttachmentIds(prosemirrorJson: any) {
- const doc = jsonToNode(prosemirrorJson);
- const attachmentIds = [];
-
- doc?.descendants((node: Node) => {
- if (isAttachmentNode(node.type.name)) {
- if (node.attrs.attachmentId && isValidUUID(node.attrs.attachmentId)) {
- if (!attachmentIds.includes(node.attrs.attachmentId)) {
- attachmentIds.push(node.attrs.attachmentId);
- }
- }
- }
- });
-
- return attachmentIds;
-}
-
-export function isAttachmentNode(nodeType: string) {
- const attachmentNodeTypes = [
- 'attachment',
- 'image',
- 'video',
- 'excalidraw',
- 'drawio',
- ];
- return attachmentNodeTypes.includes(nodeType);
-}
-
export function updateAttachmentUrlsToLocalPaths(prosemirrorJson: any) {
const doc = jsonToNode(prosemirrorJson);
if (!doc) return null;
diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
index 3ceeb789..95df255d 100644
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -4,7 +4,12 @@ import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
-import { Logger, NotFoundException, ValidationPipe } from '@nestjs/common';
+import {
+ Logger,
+ NotFoundException,
+ RequestMethod,
+ ValidationPipe,
+} from '@nestjs/common';
import { TransformHttpResponseInterceptor } from './common/interceptors/http-response.interceptor';
import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter';
import { InternalLogFilter } from './common/logger/internal-log-filter';
@@ -26,7 +31,9 @@ async function bootstrap() {
},
);
- app.setGlobalPrefix('api', { exclude: ['robots.txt'] });
+ app.setGlobalPrefix('api', {
+ exclude: ['robots.txt', 'share/:shareId/p/:pageSlug'],
+ });
const reflector = app.get(Reflector);
const redisIoAdapter = new WsRedisIoAdapter(app);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8772d574..2fd73d8b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -419,43 +419,43 @@ importers:
version: 8.1.1
'@nestjs/bullmq':
specifier: ^11.0.2
- version: 11.0.2(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(bullmq@5.41.3)
+ version: 11.0.2(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(bullmq@5.41.3)
'@nestjs/common':
- specifier: ^11.0.10
- version: 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ specifier: ^11.0.20
+ version: 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/config':
- specifier: ^4.0.0
- version: 4.0.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)
+ specifier: ^4.0.2
+ version: 4.0.2(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)
'@nestjs/core':
- specifier: ^11.0.10
- version: 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ specifier: ^11.0.20
+ version: 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/event-emitter':
specifier: ^3.0.0
- version: 3.0.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)
+ version: 3.0.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)
'@nestjs/jwt':
specifier: ^11.0.0
- version: 11.0.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))
+ version: 11.0.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))
'@nestjs/mapped-types':
specifier: ^2.1.0
- version: 2.1.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)
+ version: 2.1.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)
'@nestjs/passport':
specifier: ^11.0.5
- version: 11.0.5(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)
+ version: 11.0.5(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)
'@nestjs/platform-fastify':
- specifier: ^11.0.10
- version: 11.0.10(@fastify/static@8.1.1)(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)
+ specifier: ^11.0.20
+ version: 11.0.20(@fastify/static@8.1.1)(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)
'@nestjs/platform-socket.io':
- specifier: ^11.0.10
- version: 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(rxjs@7.8.1)
+ specifier: ^11.0.20
+ version: 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(rxjs@7.8.1)
'@nestjs/schedule':
specifier: ^5.0.1
- version: 5.0.1(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)
+ version: 5.0.1(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)
'@nestjs/terminus':
specifier: ^11.0.0
- version: 11.0.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ version: 11.0.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/websockets':
- specifier: ^11.0.10
- version: 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(@nestjs/platform-socket.io@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ specifier: ^11.0.20
+ version: 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(@nestjs/platform-socket.io@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@node-saml/passport-saml':
specifier: ^5.0.1
version: 5.0.1
@@ -509,7 +509,7 @@ importers:
version: 3.3.11
nestjs-kysely:
specifier: ^1.1.0
- version: 1.1.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(kysely@0.27.5)(reflect-metadata@0.2.2)
+ version: 1.1.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(kysely@0.27.5)(reflect-metadata@0.2.2)
nodemailer:
specifier: ^6.10.0
version: 6.10.0
@@ -564,7 +564,7 @@ importers:
version: 11.0.1(chokidar@4.0.3)(typescript@5.7.3)
'@nestjs/testing':
specifier: ^11.0.10
- version: 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)
+ version: 11.0.10(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)
'@types/bcrypt':
specifier: ^5.0.2
version: 5.0.2
@@ -2083,8 +2083,8 @@ packages:
'@fastify/cookie@11.0.2':
resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
- '@fastify/cors@10.0.2':
- resolution: {integrity: sha512-DGdxOG36sS/tZv1NFiCJGi7wGuXOSPL2CmNX5PbOVKx0C6LuIALRMrqLByHTCcX1Rbl8NJ9IWlJex32bzydvlw==}
+ '@fastify/cors@11.0.1':
+ resolution: {integrity: sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==}
'@fastify/deepmerge@2.0.2':
resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==}
@@ -2566,8 +2566,8 @@ packages:
'@swc/core':
optional: true
- '@nestjs/common@11.0.10':
- resolution: {integrity: sha512-pzGXp14KF2Q4CDZGQgPK4l8zEg7i6cNkb+10yc8ZA5K41cLe3ZbWW1YxtY2e/glHauOJwTLSVjH4tiRVtOTizg==}
+ '@nestjs/common@11.0.20':
+ resolution: {integrity: sha512-/GH8NDCczjn6+6RNEtSNAts/nq/wQE8L1qZ9TRjqjNqEsZNE1vpFuRIhmcO2isQZ0xY5rySnpaRdrOAul3gQ3A==}
peerDependencies:
class-transformer: '*'
class-validator: '*'
@@ -2579,14 +2579,14 @@ packages:
class-validator:
optional: true
- '@nestjs/config@4.0.0':
- resolution: {integrity: sha512-hyhUMtVwlT+tavtPNyekl8iP0QTU1U6awKrgdOSxhMhp3TQMltx7hz2yqGTcARp+19zWPfgJudyxthuD3lPp/Q==}
+ '@nestjs/config@4.0.2':
+ resolution: {integrity: sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
rxjs: ^7.1.0
- '@nestjs/core@11.0.10':
- resolution: {integrity: sha512-f0qB8ztNWZeAD4E4fUdHConmNYCa/A78U7WJu5mX9OLYfOAs3ESYCDfsH9MRUvkA4Ft4Y1uMmyJo5L4fg4+beg==}
+ '@nestjs/core@11.0.20':
+ resolution: {integrity: sha512-yUkEzBGiRNSEThVl6vMCXgoA9sDGWoRbJsTLdYdCC7lg7PE1iXBnna1FiBfQjT995pm0fjyM1e3WsXmyWeJXbw==}
engines: {node: '>= 20'}
peerDependencies:
'@nestjs/common': ^11.0.0
@@ -2633,11 +2633,11 @@ packages:
'@nestjs/common': ^10.0.0 || ^11.0.0
passport: ^0.5.0 || ^0.6.0 || ^0.7.0
- '@nestjs/platform-fastify@11.0.10':
- resolution: {integrity: sha512-aOvuFsSUsfGziy6OmJwVDNx6aXougCMeUpEAlphuCLehSwfZQxhpy4SpThxTAtHU7RdmgGO7VfUGH+uUY8vHdQ==}
+ '@nestjs/platform-fastify@11.0.20':
+ resolution: {integrity: sha512-MZnjO77N/XesVzXhn8qnSEcnjXVIHxkh5zTz8SEIr6K2yWgGJZbTlNm7ul6l7QBeaCeNZtZJlvY/F+4Dbx8yCQ==}
peerDependencies:
'@fastify/static': ^8.0.0
- '@fastify/view': ^10.0.0
+ '@fastify/view': ^10.0.0 || ^11.0.0
'@nestjs/common': ^11.0.0
'@nestjs/core': ^11.0.0
peerDependenciesMeta:
@@ -2646,8 +2646,8 @@ packages:
'@fastify/view':
optional: true
- '@nestjs/platform-socket.io@11.0.10':
- resolution: {integrity: sha512-39lAjq0+kZRiMuscDcugoG+onPDciM4jhuf8ZDjVcuSwtib1OGwrFtErSzp/KJsmHPSStgapbNev7eFi32uWQA==}
+ '@nestjs/platform-socket.io@11.0.20':
+ resolution: {integrity: sha512-fUyDjLt0wJ4WK+rXrd5/oSWw5xWpfDOknpP7YNgaFfvYW726KuS5gWysV7JPD2mgH85S6i+qiO3qZvHIs5DvxQ==}
peerDependencies:
'@nestjs/common': ^11.0.0
'@nestjs/websockets': ^11.0.0
@@ -2725,8 +2725,8 @@ packages:
'@nestjs/platform-express':
optional: true
- '@nestjs/websockets@11.0.10':
- resolution: {integrity: sha512-GPIEfqJyAkTHrHGK9w2OU8LJaZAZKW8WpWcTplThLxMelRq7mBkYOaGvc6dpr7fE1wWzWkwY0ZjQEnwnVmmxSg==}
+ '@nestjs/websockets@11.0.20':
+ resolution: {integrity: sha512-qcybahXdrPJFMILhAwJML9D/bExBEBFsfwFiePCeI4f//tiP0rXiLspLVOHClSeUPBaCNrx+Ae/HVe9UP+wtOg==}
peerDependencies:
'@nestjs/common': ^11.0.0
'@nestjs/core': ^11.0.0
@@ -3747,6 +3747,13 @@ packages:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
+ '@tokenizer/inflate@0.2.7':
+ resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
+ engines: {node: '>=18'}
+
+ '@tokenizer/token@0.3.0':
+ resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
+
'@tsconfig/node10@1.0.9':
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
@@ -5134,6 +5141,15 @@ packages:
supports-color:
optional: true
+ debug@4.4.0:
+ resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
@@ -5564,8 +5580,8 @@ packages:
fastify-plugin@5.0.1:
resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==}
- fastify@5.2.1:
- resolution: {integrity: sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA==}
+ fastify@5.3.0:
+ resolution: {integrity: sha512-vDpCJa4KRkHrdDMpDNtyPaIDi/ptCwoJ0M8RiefuIMvyXTgG63xYGe9DYYiCpydjh0ETIaLoSyKBNKkh7ew1eA==}
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
@@ -5581,6 +5597,9 @@ packages:
picomatch:
optional: true
+ fflate@0.8.2:
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
@@ -5592,6 +5611,10 @@ packages:
file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
+ file-type@20.4.1:
+ resolution: {integrity: sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==}
+ engines: {node: '>=18'}
+
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
@@ -6560,6 +6583,10 @@ packages:
linkifyjs@4.2.0:
resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==}
+ load-esm@1.0.2:
+ resolution: {integrity: sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==}
+ engines: {node: '>=13.2.0'}
+
loader-runner@4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'}
@@ -6813,9 +6840,6 @@ packages:
mlly@1.7.3:
resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==}
- mnemonist@0.39.8:
- resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==}
-
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
@@ -7002,9 +7026,6 @@ packages:
resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==}
engines: {node: '>= 0.4'}
- obliterator@2.0.4:
- resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==}
-
obuf@1.1.2:
resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==}
@@ -7165,6 +7186,10 @@ packages:
peberminta@0.9.0:
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
+ peek-readable@7.0.0:
+ resolution: {integrity: sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==}
+ engines: {node: '>=18'}
+
pg-cloudflare@1.1.1:
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
@@ -7388,6 +7413,9 @@ packages:
process-warning@4.0.0:
resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==}
+ process-warning@5.0.0:
+ resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
+
process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
@@ -7874,6 +7902,9 @@ packages:
secure-json-parse@3.0.2:
resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==}
+ secure-json-parse@4.0.0:
+ resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==}
+
selderee@0.11.0:
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
@@ -8091,6 +8122,10 @@ packages:
strnum@1.0.5:
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
+ strtok3@10.2.2:
+ resolution: {integrity: sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==}
+ engines: {node: '>=18'}
+
styled-jsx@5.1.1:
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
@@ -8234,6 +8269,10 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
+ token-types@6.0.0:
+ resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==}
+ engines: {node: '>=14.16'}
+
tough-cookie@5.1.0:
resolution: {integrity: sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==}
engines: {node: '>=16'}
@@ -8432,6 +8471,10 @@ packages:
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
engines: {node: '>=8'}
+ uint8array-extras@1.4.0:
+ resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==}
+ engines: {node: '>=18'}
+
unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
@@ -10906,8 +10949,8 @@ snapshots:
'@fastify/ajv-compiler@4.0.2':
dependencies:
- ajv: 8.12.0
- ajv-formats: 3.0.1(ajv@8.12.0)
+ ajv: 8.17.1
+ ajv-formats: 3.0.1(ajv@8.17.1)
fast-uri: 3.0.6
'@fastify/busboy@3.1.1': {}
@@ -10917,10 +10960,10 @@ snapshots:
cookie: 1.0.2
fastify-plugin: 5.0.1
- '@fastify/cors@10.0.2':
+ '@fastify/cors@11.0.1':
dependencies:
fastify-plugin: 5.0.1
- mnemonist: 0.39.8
+ toad-cache: 3.7.0
'@fastify/deepmerge@2.0.2': {}
@@ -11545,17 +11588,17 @@ snapshots:
'@emnapi/runtime': 1.2.0
'@tybys/wasm-util': 0.9.0
- '@nestjs/bull-shared@11.0.2(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)':
+ '@nestjs/bull-shared@11.0.2(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
tslib: 2.8.1
- '@nestjs/bullmq@11.0.2(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(bullmq@5.41.3)':
+ '@nestjs/bullmq@11.0.2(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(bullmq@5.41.3)':
dependencies:
- '@nestjs/bull-shared': 11.0.2(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/bull-shared': 11.0.2(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
bullmq: 5.41.3
tslib: 2.8.1
@@ -11588,9 +11631,11 @@ snapshots:
- uglify-js
- webpack-cli
- '@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+ '@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
dependencies:
+ file-type: 20.4.1
iterare: 1.2.1
+ load-esm: 1.0.2
reflect-metadata: 0.2.2
rxjs: 7.8.1
tslib: 2.8.1
@@ -11598,18 +11643,20 @@ snapshots:
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.14.1
+ transitivePeerDependencies:
+ - supports-color
- '@nestjs/config@4.0.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)':
+ '@nestjs/config@4.0.2(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
dotenv: 16.4.7
dotenv-expand: 12.0.1
lodash: 4.17.21
rxjs: 7.8.1
- '@nestjs/core@11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+ '@nestjs/core@11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nuxt/opencollective': 0.4.1
fast-safe-stringify: 2.1.1
iterare: 1.2.1
@@ -11619,51 +11666,52 @@ snapshots:
tslib: 2.8.1
uid: 2.0.2
optionalDependencies:
- '@nestjs/websockets': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(@nestjs/platform-socket.io@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/websockets': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(@nestjs/platform-socket.io@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/event-emitter@3.0.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)':
+ '@nestjs/event-emitter@3.0.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
eventemitter2: 6.4.9
- '@nestjs/jwt@11.0.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))':
+ '@nestjs/jwt@11.0.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@types/jsonwebtoken': 9.0.7
jsonwebtoken: 9.0.2
- '@nestjs/mapped-types@2.1.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)':
+ '@nestjs/mapped-types@2.1.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
reflect-metadata: 0.2.2
optionalDependencies:
class-transformer: 0.5.1
class-validator: 0.14.1
- '@nestjs/passport@11.0.5(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)':
+ '@nestjs/passport@11.0.5(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
passport: 0.7.0
- '@nestjs/platform-fastify@11.0.10(@fastify/static@8.1.1)(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)':
+ '@nestjs/platform-fastify@11.0.20(@fastify/static@8.1.1)(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)':
dependencies:
- '@fastify/cors': 10.0.2
+ '@fastify/cors': 11.0.1
'@fastify/formbody': 8.0.2
'@fastify/middie': 9.0.3
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- fastify: 5.2.1
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ fast-querystring: 1.1.2
+ fastify: 5.3.0
light-my-request: 6.6.0
path-to-regexp: 8.2.0
tslib: 2.8.1
optionalDependencies:
'@fastify/static': 8.1.1
- '@nestjs/platform-socket.io@11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(rxjs@7.8.1)':
+ '@nestjs/platform-socket.io@11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(rxjs@7.8.1)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/websockets': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(@nestjs/platform-socket.io@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/websockets': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(@nestjs/platform-socket.io@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
rxjs: 7.8.1
socket.io: 4.8.1
tslib: 2.8.1
@@ -11672,10 +11720,10 @@ snapshots:
- supports-color
- utf-8-validate
- '@nestjs/schedule@5.0.1(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)':
+ '@nestjs/schedule@5.0.1(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
cron: 3.5.0
'@nestjs/schematics@11.0.1(chokidar@4.0.3)(typescript@5.7.3)':
@@ -11689,32 +11737,32 @@ snapshots:
transitivePeerDependencies:
- chokidar
- '@nestjs/terminus@11.0.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+ '@nestjs/terminus@11.0.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
boxen: 5.1.2
check-disk-space: 3.4.0
reflect-metadata: 0.2.2
rxjs: 7.8.1
- '@nestjs/testing@11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)':
+ '@nestjs/testing@11.0.10(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
tslib: 2.8.1
- '@nestjs/websockets@11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(@nestjs/platform-socket.io@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+ '@nestjs/websockets@11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(@nestjs/platform-socket.io@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
iterare: 1.2.1
object-hash: 3.0.0
reflect-metadata: 0.2.2
rxjs: 7.8.1
tslib: 2.8.1
optionalDependencies:
- '@nestjs/platform-socket.io': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(rxjs@7.8.1)
+ '@nestjs/platform-socket.io': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(rxjs@7.8.1)
'@next/env@14.2.10': {}
@@ -11798,7 +11846,7 @@ snapshots:
nx: 20.4.5(@swc/core@1.5.25(@swc/helpers@0.5.5))
semver: 7.6.3
tmp: 0.2.1
- tslib: 2.8.0
+ tslib: 2.8.1
yargs-parser: 21.1.1
'@nx/js@20.4.5(@babel/traverse@7.27.0)(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@22.13.4)(nx@20.4.5(@swc/core@1.5.25(@swc/helpers@0.5.5)))(typescript@5.7.3)':
@@ -11880,7 +11928,7 @@ snapshots:
chalk: 4.1.2
enquirer: 2.3.6
nx: 20.4.5(@swc/core@1.5.25(@swc/helpers@0.5.5))
- tslib: 2.8.0
+ tslib: 2.8.1
yargs-parser: 21.1.1
transitivePeerDependencies:
- '@swc-node/register'
@@ -12797,6 +12845,16 @@ snapshots:
'@tiptap/core': 2.10.3(@tiptap/pm@2.10.3)
'@tiptap/pm': 2.10.3
+ '@tokenizer/inflate@0.2.7':
+ dependencies:
+ debug: 4.4.0
+ fflate: 0.8.2
+ token-types: 6.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@tokenizer/token@0.3.0': {}
+
'@tsconfig/node10@1.0.9': {}
'@tsconfig/node12@1.0.11': {}
@@ -13521,10 +13579,6 @@ snapshots:
optionalDependencies:
ajv: 8.12.0
- ajv-formats@3.0.1(ajv@8.12.0):
- optionalDependencies:
- ajv: 8.12.0
-
ajv-formats@3.0.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
@@ -14459,6 +14513,10 @@ snapshots:
dependencies:
ms: 2.1.3
+ debug@4.4.0:
+ dependencies:
+ ms: 2.1.3
+
decimal.js@10.4.3: {}
dedent@1.5.1(babel-plugin-macros@3.1.0):
@@ -14574,7 +14632,7 @@ snapshots:
dotenv-expand@11.0.6:
dependencies:
- dotenv: 16.4.5
+ dotenv: 16.4.7
dotenv-expand@12.0.1:
dependencies:
@@ -15034,8 +15092,8 @@ snapshots:
fast-json-stringify@6.0.1:
dependencies:
'@fastify/merge-json-schemas': 0.2.1
- ajv: 8.12.0
- ajv-formats: 3.0.1(ajv@8.12.0)
+ ajv: 8.17.1
+ ajv-formats: 3.0.1(ajv@8.17.1)
fast-uri: 3.0.6
json-schema-ref-resolver: 2.0.1
rfdc: 1.3.1
@@ -15058,7 +15116,7 @@ snapshots:
fastify-plugin@5.0.1: {}
- fastify@5.2.1:
+ fastify@5.3.0:
dependencies:
'@fastify/ajv-compiler': 4.0.2
'@fastify/error': 4.0.0
@@ -15070,10 +15128,10 @@ snapshots:
find-my-way: 9.2.0
light-my-request: 6.6.0
pino: 9.1.0
- process-warning: 4.0.0
+ process-warning: 5.0.0
rfdc: 1.3.1
- secure-json-parse: 3.0.2
- semver: 7.6.3
+ secure-json-parse: 4.0.0
+ semver: 7.7.1
toad-cache: 3.7.0
fastq@1.17.1:
@@ -15088,6 +15146,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
+ fflate@0.8.2: {}
+
figures@3.2.0:
dependencies:
escape-string-regexp: 1.0.5
@@ -15098,6 +15158,15 @@ snapshots:
file-saver@2.0.5: {}
+ file-type@20.4.1:
+ dependencies:
+ '@tokenizer/inflate': 0.2.7
+ strtok3: 10.2.2
+ token-types: 6.0.0
+ uint8array-extras: 1.4.0
+ transitivePeerDependencies:
+ - supports-color
+
filelist@1.0.4:
dependencies:
minimatch: 5.1.6
@@ -16293,6 +16362,8 @@ snapshots:
linkifyjs@4.2.0: {}
+ load-esm@1.0.2: {}
+
loader-runner@4.3.0: {}
local-pkg@0.5.1:
@@ -16529,10 +16600,6 @@ snapshots:
pkg-types: 1.2.1
ufo: 1.5.4
- mnemonist@0.39.8:
- dependencies:
- obliterator: 2.0.4
-
ms@2.1.2: {}
ms@2.1.3: {}
@@ -16571,10 +16638,10 @@ snapshots:
neo-async@2.6.2: {}
- nestjs-kysely@1.1.0(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.10)(kysely@0.27.5)(reflect-metadata@0.2.2):
+ nestjs-kysely@1.1.0(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@11.0.20)(kysely@0.27.5)(reflect-metadata@0.2.2):
dependencies:
- '@nestjs/common': 11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 11.0.10(@nestjs/common@11.0.10(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.10)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 11.0.20(@nestjs/common@11.0.20(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@11.0.20)(reflect-metadata@0.2.2)(rxjs@7.8.1)
kysely: 0.27.5
reflect-metadata: 0.2.2
@@ -16748,8 +16815,6 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.0.0
- obliterator@2.0.4: {}
-
obuf@1.1.2: {}
oidc-token-hash@5.0.3: {}
@@ -16919,6 +16984,8 @@ snapshots:
peberminta@0.9.0: {}
+ peek-readable@7.0.0: {}
+
pg-cloudflare@1.1.1:
optional: true
@@ -17128,6 +17195,8 @@ snapshots:
process-warning@4.0.0: {}
+ process-warning@5.0.0: {}
+
process@0.11.10: {}
prompts@2.4.2:
@@ -17703,6 +17772,8 @@ snapshots:
secure-json-parse@3.0.2: {}
+ secure-json-parse@4.0.0: {}
+
selderee@0.11.0:
dependencies:
parseley: 0.12.1
@@ -17958,6 +18029,11 @@ snapshots:
strnum@1.0.5: {}
+ strtok3@10.2.2:
+ dependencies:
+ '@tokenizer/token': 0.3.0
+ peek-readable: 7.0.0
+
styled-jsx@5.1.1(@babel/core@7.24.5)(babel-plugin-macros@3.1.0)(react@18.3.1):
dependencies:
client-only: 0.0.1
@@ -18099,6 +18175,11 @@ snapshots:
toidentifier@1.0.1: {}
+ token-types@6.0.0:
+ dependencies:
+ '@tokenizer/token': 0.3.0
+ ieee754: 1.2.1
+
tough-cookie@5.1.0:
dependencies:
tldts: 6.1.72
@@ -18304,6 +18385,8 @@ snapshots:
dependencies:
'@lukeed/csprng': 1.1.0
+ uint8array-extras@1.4.0: {}
+
unbox-primitive@1.0.2:
dependencies:
call-bind: 1.0.7