(null);
- const [menuPlacement, setMenuPlacement] = useState<{
- top: number;
- left: number;
- width: number;
- }>({
- top: 0,
- left: 0,
- width: 0,
- });
- const currentItems = useMemo(() => {
- return commandItems[activeCommandSet].filter((item) => {
- return item.name.toLowerCase().includes(prompt.toLowerCase());
- });
- }, [prompt, output, activeCommandSet]);
- const updateMenuPlacement = useCallback(() => {
- if (!editor || !showAiMenu) return;
-
- const { view } = editor;
- const { from, to } = editor.state.selection;
- const editorRect = view.dom.getBoundingClientRect();
- const fromCoords = view.coordsAtPos(from);
- const toCoords = view.coordsAtPos(to);
- const topOffset = 8;
- const editorPadding = isSmBreakpoint ? 16 : 48;
-
- const anchorBottom =
- toCoords.bottom > 0 && toCoords.bottom < window.innerHeight
- ? toCoords.bottom
- : fromCoords.bottom;
-
- const menuMaxWidth = 600;
- const editorLeft = editorRect.left + editorPadding;
- const editorRight = editorRect.right - editorPadding;
- const availableWidth = editorRight - editorLeft;
- const menuWidth = Math.min(menuMaxWidth, availableWidth);
-
- let menuLeft = Math.max(editorLeft, fromCoords.left);
- if (menuLeft + menuWidth > editorRight) {
- menuLeft = editorRight - menuWidth;
- }
- menuLeft = Math.max(editorLeft, menuLeft);
-
- setMenuPlacement({
- top: anchorBottom + topOffset + window.scrollY,
- left: menuLeft + window.scrollX,
- width: menuWidth,
- });
- }, [editor, showAiMenu, isSmBreakpoint]);
- const resetMenu = useCallback(() => {
- setPrompt("");
- setOutput("");
- setActiveCommandSet("main");
- setLastAction(null);
- aiGenerateStreamMutation.reset();
- }, [aiGenerateStreamMutation.reset]);
- const debouncedUpdateMenuPlacement = useDebouncedCallback(
- updateMenuPlacement,
- 60,
- );
- const handleGenerate = useCallback(
- (item?: CommandItem) => {
- if (!editor || isLoading) return;
-
- let command: CommandItem | null = item || null;
-
- if (!command) {
- if (!prompt) return;
-
- command = {
- id: "custom",
- name: "Custom",
- action: AiAction.CUSTOM,
- prompt,
- };
- }
-
- const { from, to } = editor.state.selection;
- const slice = editor.state.doc.slice(from, to);
- const serializer = DOMSerializer.fromSchema(editor.schema);
- const fragment = serializer.serializeFragment(slice.content);
- const wrapper = document.createElement("div");
- wrapper.appendChild(fragment);
- const content = htmlToMarkdown(wrapper.innerHTML);
-
- setOutput("");
- setIsLoading(true);
- aiGenerateStreamMutation.mutate({
- action: command.action,
- prompt: command.prompt,
- content,
- onChunk: (chunk) => {
- setOutput((output) => output + chunk.content);
- },
- onComplete: () => {
- setPrompt("");
- setIsLoading(false);
- setActiveCommandSet("result");
- },
- onError: () => {
- setIsLoading(false);
- resetMenu();
- },
- });
- setLastAction(command);
- },
- [
- editor,
- prompt,
- isLoading,
- aiGenerateStreamMutation.mutateAsync,
- resetMenu,
- ],
- );
- const handleCommand = useCallback(
- (item?: CommandItem) => {
- setPrompt("");
-
- if (!item) {
- return handleGenerate();
- }
- if (item.id === "back") {
- return setActiveCommandSet("main");
- }
- if (item.id === "result-replace") {
- const chain = editor.chain().focus();
-
- if (lastAction.action === AiAction.CONTINUE_WRITING) {
- chain.setTextSelection(editor.state.selection.to);
- }
-
- const html = (marked.parse(output) as string).trim();
- const isSingleParagraph =
- html.startsWith("") &&
- html.endsWith("
") &&
- html.lastIndexOf("") === 0;
-
- // Strip
wrapper for single-paragraph output to preserve inline context,
- // then decode HTML entities via DOMParser since TipTap would otherwise
- // treat the tagless string as plain text and insert entities literally.
- const content = isSingleParagraph
- ? new DOMParser().parseFromString(html.slice(3, -4), "text/html")
- .body.innerHTML
- : html;
-
- chain.insertContent(content).run();
-
- return setShowAiMenu(false);
- }
- if (item.id === "result-insert-below") {
- editor
- .chain()
- .focus()
- .setTextSelection(editor.state.selection.to)
- .insertContent(marked.parse(output))
- .run();
-
- return setShowAiMenu(false);
- }
- if (item.id === "result-copy") {
- copyToClipboard(output);
-
- return setShowAiMenu(false);
- }
- if (item.id === "result-discard") {
- setOutput("");
-
- return resetMenu();
- }
- if (item.id === "result-try-again" && lastAction) {
- return handleGenerate(lastAction);
- }
- if (item.subCommandSet) {
- return setActiveCommandSet(item.subCommandSet);
- }
-
- return handleGenerate(item);
- },
- [editor, output, lastAction, handleGenerate, resetMenu],
- );
- const handleKeyDown = useCallback(
- (event: React.KeyboardEvent) => {
- const totalItems = currentItems.length;
- const cycleSize = totalItems + 1;
-
- if (event.key === "Escape") {
- return setShowAiMenu(false);
- }
-
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
- event.preventDefault();
-
- return setSelectedIndex((selectedIndex) => {
- const direction = event.key === "ArrowDown" ? 1 : -1;
- const newIndex = selectedIndex + direction;
-
- if (newIndex < -1) return cycleSize - 1;
- if (newIndex >= cycleSize) return 0;
-
- return newIndex;
- });
- }
-
- if (event.key === "Enter") {
- event.preventDefault();
-
- return handleCommand(currentItems[selectedIndex]);
- }
- },
- [currentItems, selectedIndex],
- );
-
- useEffect(() => {
- if (!editor) return;
-
- const handleClose = () => setShowAiMenu(false);
- const observer = new ResizeObserver(() => {
- debouncedUpdateMenuPlacement();
- });
-
- updateMenuPlacement();
- editor.on("focus", handleClose);
- editor.on("blur", handleClose);
- window.addEventListener("resize", debouncedUpdateMenuPlacement);
- window.addEventListener("scroll", debouncedUpdateMenuPlacement, true);
- observer.observe(editor.view.dom);
-
- return () => {
- editor.off("focus", handleClose);
- editor.off("blur", handleClose);
- window.removeEventListener("resize", debouncedUpdateMenuPlacement);
- window.removeEventListener("scroll", debouncedUpdateMenuPlacement, true);
- observer.disconnect();
- };
- }, [editor, updateMenuPlacement, debouncedUpdateMenuPlacement]);
-
- useEffect(() => {
- setShowAiMenu(false);
- }, [location]);
- useEffect(() => {
- if (showAiMenu) {
- resetMenu();
- }
- }, [showAiMenu, resetMenu]);
- useEffect(() => {
- // Focus input when menu opens or command set changes
- requestAnimationFrame(() => {
- inputRef.current?.focus({ preventScroll: true });
- });
- }, [showAiMenu, isLoading, currentItems]);
- useEffect(() => {
- if (!currentItems.length) {
- setSelectedIndex(-1);
- }
- setSelectedIndex(prompt || activeCommandSet !== "main" ? 0 : -1);
- }, [prompt, activeCommandSet, currentItems]);
-
- if (!showAiMenu) return null;
-
- return createPortal(
-
-
-
-
- setPrompt(e.currentTarget.value)}
- rightSection={
- handleGenerate()}
- >
-
-
- }
- onKeyDown={handleKeyDown}
- />
-
-
-
,
- document.body,
- );
-};
-
-export { EditorAiMenu };
diff --git a/apps/client/src/ee/ai/components/editor/ai-menu/command-items.ts b/apps/client/src/ee/ai/components/editor/ai-menu/command-items.ts
deleted file mode 100644
index 71eaa9cb..00000000
--- a/apps/client/src/ee/ai/components/editor/ai-menu/command-items.ts
+++ /dev/null
@@ -1,219 +0,0 @@
-import { AiAction } from "@/ee/ai/types/ai.types.ts";
-import {
- IconSparkles,
- IconArrowsMaximize,
- IconArrowsMinimize,
- IconWriting,
- IconHelp,
- IconList,
- IconMoodSmile,
- IconLanguage,
- IconTrash,
- IconRefresh,
- IconChevronLeft,
- IconCheck,
- IconArrowDownLeft,
- IconCopy,
- IconTextPlus,
- IconAlignJustified,
-} from "@tabler/icons-react";
-
-interface CommandItem {
- name: string;
- id: string;
- icon?: typeof IconSparkles;
- action?: AiAction;
- prompt?: string;
- subCommandSet?: CommandSet;
-}
-
-type CommandSet = "main" | "tone" | "translate" | "result";
-
-const mainItems: CommandItem[] = [
- {
- id: "improve-writing",
- name: "Improve writing",
- icon: IconSparkles,
- action: AiAction.IMPROVE_WRITING,
- },
- {
- id: "fix-spelling-grammar",
- name: "Fix spelling & grammar",
- icon: IconCheck,
- action: AiAction.FIX_SPELLING_GRAMMAR,
- },
- {
- id: "make-longer",
- name: "Make longer",
- icon: IconTextPlus,
- action: AiAction.MAKE_LONGER,
- },
- {
- id: "make-shorter",
- name: "Make shorter",
- icon: IconAlignJustified,
- action: AiAction.MAKE_SHORTER,
- },
- {
- id: "continue-writing",
- name: "Continue writing",
- icon: IconWriting,
- action: AiAction.CONTINUE_WRITING,
- },
- {
- id: "explain",
- name: "Explain",
- icon: IconHelp,
- action: AiAction.EXPLAIN,
- },
- {
- id: "summarize",
- name: "Summarize",
- icon: IconList,
- action: AiAction.SUMMARIZE,
- },
- {
- id: "change-tone",
- name: "Change tone",
- icon: IconMoodSmile,
- subCommandSet: "tone",
- },
- {
- id: "translate",
- name: "Translate",
- icon: IconLanguage,
- subCommandSet: "translate",
- },
-];
-const toneItems: CommandItem[] = [
- {
- id: "back",
- name: "Back",
- icon: IconChevronLeft,
- },
- {
- id: "tone-professional",
- name: "Professional",
- icon: IconMoodSmile,
- action: AiAction.CHANGE_TONE,
- prompt: "Professional",
- },
- {
- id: "tone-casual",
- name: "Casual",
- icon: IconMoodSmile,
- action: AiAction.CHANGE_TONE,
- prompt: "Casual",
- },
- {
- id: "tone-friendly",
- name: "Friendly",
- icon: IconMoodSmile,
- action: AiAction.CHANGE_TONE,
- prompt: "Friendly",
- },
-];
-const translateItems: CommandItem[] = [
- {
- id: "back",
- name: "Back",
- icon: IconChevronLeft,
- },
- {
- id: "translate-english",
- name: "English",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "English",
- },
- {
- id: "translate-spanish",
- name: "Spanish",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "Spanish",
- },
- {
- id: "translate-german",
- name: "German",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "German",
- },
- {
- id: "translate-french",
- name: "French",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "French",
- },
- {
- id: "translate-dutch",
- name: "Dutch",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "Dutch",
- },
- {
- id: "translate-portuguese",
- name: "Portuguese",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "Portuguese",
- },
- {
- id: "translate-italian",
- name: "Italian",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "Italian",
- },
- {
- id: "translate-japanese",
- name: "Japanese",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "Japanese",
- },
- {
- id: "translate-korean",
- name: "Korean",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "Korean",
- },
- {
- id: "translate-swedish",
- name: "Swedish",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "Swedish",
- },
- {
- id: "translate-chinese",
- name: "Chinese (Simplified)",
- icon: IconLanguage,
- action: AiAction.TRANSLATE,
- prompt: "Simplified Chinese",
- },
-];
-const resultItems: CommandItem[] = [
- { id: "result-replace", name: "Replace", icon: IconCheck },
- { id: "result-insert-below", name: "Insert below", icon: IconArrowDownLeft },
- { id: "result-copy", name: "Copy", icon: IconCopy },
- { id: "result-discard", name: "Discard", icon: IconTrash },
- {
- id: "result-try-again",
- name: "Try again",
- icon: IconRefresh,
- },
-];
-const commandItems: Record = {
- main: mainItems,
- tone: toneItems,
- translate: translateItems,
- result: resultItems,
-};
-
-export type { CommandItem, CommandSet };
-export { commandItems };
diff --git a/apps/client/src/ee/ai/components/editor/ai-menu/command-selector.tsx b/apps/client/src/ee/ai/components/editor/ai-menu/command-selector.tsx
deleted file mode 100644
index 8e66bee0..00000000
--- a/apps/client/src/ee/ai/components/editor/ai-menu/command-selector.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { Loader, Menu, ScrollArea } from "@mantine/core";
-import { IconChevronRight } from "@tabler/icons-react";
-import { ReactNode } from "react";
-import { CommandItem } from "./command-items.ts";
-import classes from "./ai-menu.module.css";
-
-interface CommandSelectorProps {
- selectedIndex: number;
-
- isLoading: boolean;
- output: string;
- currentItems: CommandItem[];
- children: ReactNode;
- handleCommand(item: CommandItem): void;
-}
-
-const CommandSelector = ({
- selectedIndex,
- children,
- isLoading,
- output,
- currentItems,
- handleCommand,
-}: CommandSelectorProps) => {
- return (
- 0}
- middlewares={{ flip: false }}
- position="bottom-start"
- offset={4}
- width={250}
- trapFocus={false}
- shadow="lg"
- >
- {children}
-
-
- {currentItems.map((item, index) => {
- const isSelected = selectedIndex === index;
- const showLoader =
- isLoading && output === "" && !item.subCommandSet;
-
- return (
-
- ) : item.icon ? (
-
- ) : undefined
- }
- rightSection={
- item.subCommandSet ? (
-
- ) : undefined
- }
- onClick={() => handleCommand(item)}
- disabled={isLoading}
- >
- {item.name}
-
- );
- })}
-
-
-
- );
-};
-
-export { CommandSelector };
diff --git a/apps/client/src/ee/ai/components/editor/ai-menu/result-preview.tsx b/apps/client/src/ee/ai/components/editor/ai-menu/result-preview.tsx
deleted file mode 100644
index d34682e3..00000000
--- a/apps/client/src/ee/ai/components/editor/ai-menu/result-preview.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Loader, Paper, ScrollArea } from "@mantine/core";
-import DOMPurify from "dompurify";
-import { marked } from "marked";
-import { memo } from "react";
-import classes from "./ai-menu.module.css";
-
-interface ResultPreviewProps {
- output: string;
- isLoading: boolean;
-}
-const ResultPreview = memo(({ output, isLoading }: ResultPreviewProps) => {
- if (!output && !isLoading) return;
-
- const parsedOutput = `${marked.parse(output)}`;
-
- return (
-
-
-
- {parsedOutput && (
-
- )}
- {isLoading &&
}
-
-
-
- );
-});
-
-export { ResultPreview };
diff --git a/apps/client/src/ee/ai/components/enable-ai-search.tsx b/apps/client/src/ee/ai/components/enable-ai-search.tsx
deleted file mode 100644
index 3a2abd26..00000000
--- a/apps/client/src/ee/ai/components/enable-ai-search.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Group, Text, Switch, MantineSize, Tooltip } from "@mantine/core";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import React, { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
-import { notifications } from "@mantine/notifications";
-import { useHasFeature } from "@/ee/hooks/use-feature";
-import { Feature } from "@/ee/features";
-import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
-
-export default function EnableAiSearch() {
- const { t } = useTranslation();
-
- return (
- <>
-
-
- {t("AI-powered search (AI Answers)")}
-
- {t(
- "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.",
- )}
-
-
-
-
-
- >
- );
-}
-
-interface AiSearchToggleProps {
- size?: MantineSize;
- label?: string;
-}
-export function AiSearchToggle({ size, label }: AiSearchToggleProps) {
- const { t } = useTranslation();
- const [workspace, setWorkspace] = useAtom(workspaceAtom);
- const [checked, setChecked] = useState(workspace?.settings?.ai?.search);
- const hasAccess = useHasFeature(Feature.AI);
- const upgradeLabel = useUpgradeLabel();
-
- const handleChange = async (event: React.ChangeEvent) => {
- const value = event.currentTarget.checked;
- try {
- const updatedWorkspace = await updateWorkspace({ aiSearch: value });
- setChecked(value);
- setWorkspace(updatedWorkspace);
- } catch (err) {
- notifications.show({
- message: err?.response?.data?.message,
- color: "red",
- });
- }
- };
-
- return (
-
-
-
- );
-}
diff --git a/apps/client/src/ee/ai/components/enable-generative-ai.tsx b/apps/client/src/ee/ai/components/enable-generative-ai.tsx
deleted file mode 100644
index 1db611ce..00000000
--- a/apps/client/src/ee/ai/components/enable-generative-ai.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Group, Text, Switch, Tooltip } from "@mantine/core";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import React, { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
-import { notifications } from "@mantine/notifications";
-import { useHasFeature } from "@/ee/hooks/use-feature";
-import { Feature } from "@/ee/features";
-import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
-
-export default function EnableGenerativeAi() {
- const { t } = useTranslation();
- const [workspace, setWorkspace] = useAtom(workspaceAtom);
- const [checked, setChecked] = useState(workspace?.settings?.ai?.generative);
- const hasAccess = useHasFeature(Feature.AI);
- const upgradeLabel = useUpgradeLabel();
-
- const handleChange = async (event: React.ChangeEvent) => {
- const value = event.currentTarget.checked;
- try {
- const updatedWorkspace = await updateWorkspace({ generativeAi: value });
- setChecked(value);
- setWorkspace(updatedWorkspace);
- } catch (err) {
- notifications.show({
- message: err?.response?.data?.message,
- color: "red",
- });
- }
- };
-
- return (
-
-
- {t("Generative AI (Ask AI)")}
-
- {t(
- "Enable AI-powered content generation in the editor. Allows users to generate, improve, translate and transform text.",
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/ai/components/mcp-settings.tsx b/apps/client/src/ee/ai/components/mcp-settings.tsx
deleted file mode 100644
index e7cc2234..00000000
--- a/apps/client/src/ee/ai/components/mcp-settings.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import {
- Anchor,
- Group,
- List,
- Text,
- Switch,
- TextInput,
- ActionIcon,
- Tooltip,
- Stack,
- Alert,
-} from "@mantine/core";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import React, { useState } from "react";
-import { Trans, useTranslation } from "react-i18next";
-import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
-import { notifications } from "@mantine/notifications";
-import { useHasFeature } from "@/ee/hooks/use-feature";
-import { Feature } from "@/ee/features";
-import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
-import { getAppUrl } from "@/lib/config.ts";
-import { IconCheck, IconCopy, IconInfoCircle } from "@tabler/icons-react";
-import { CopyButton } from "@/components/common/copy-button.tsx";
-
-export default function McpSettings() {
- const { t } = useTranslation();
- const [workspace, setWorkspace] = useAtom(workspaceAtom);
- const [checked, setChecked] = useState(workspace?.settings?.ai?.mcp);
- const hasAccess = useHasFeature(Feature.MCP);
- const upgradeLabel = useUpgradeLabel();
-
- const mcpUrl = `${getAppUrl()}/mcp`;
-
- const handleChange = async (event: React.ChangeEvent) => {
- const value = event.currentTarget.checked;
- try {
- const updatedWorkspace = await updateWorkspace({ mcpEnabled: value });
- setChecked(value);
- setWorkspace(updatedWorkspace);
- } catch (err) {
- notifications.show({
- message: err?.response?.data?.message,
- color: "red",
- });
- }
- };
-
- return (
-
- {!hasAccess && (
- } title={upgradeLabel} color="blue">
- {t(
- "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
- )}
-
- )}
-
-
-
- {t("Model Context Protocol (MCP)")}
-
- {t(
- "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.",
- )}{" "}
- ,
- }}
- />
-
-
-
-
-
-
-
-
- {checked && (
-
-
- {t("MCP Server URL")}
-
-
-
-
- {({ copied, copy }) => (
-
-
- {copied ? : }
-
-
- )}
-
-
-
- {t(
- "Use your API key for authentication. You can manage API keys in your account settings.",
- )}
-
-
-
-
- {t("Supported tools")}
-
-
-
-
- search_pages, get_page, create_page, update_page
-
-
-
-
- list_pages, list_child_pages, duplicate_page
-
-
-
-
- copy_page_to_space, move_page, move_page_to_space
-
-
-
-
- get_space, list_spaces, create_space, update_space
-
-
-
-
- get_comments, create_comment, update_comment
-
-
-
-
- search_attachments, list_workspace_members, get_current_user
-
-
-
-
-
- )}
-
- );
-}
diff --git a/apps/client/src/ee/ai/hooks/use-ai-search.ts b/apps/client/src/ee/ai/hooks/use-ai-search.ts
deleted file mode 100644
index 03b24424..00000000
--- a/apps/client/src/ee/ai/hooks/use-ai-search.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useMutation, UseMutationResult } from "@tanstack/react-query";
-import { useState, useCallback } from "react";
-import { aiAnswers, IAiSearchResponse } from "@/ee/ai/services/ai-search-service.ts";
-import { IPageSearchParams } from "@/features/search/types/search.types.ts";
-
-// @ts-ignore
-interface UseAiSearchResult extends UseMutationResult {
- streamingAnswer: string;
- streamingSources: any[];
- clearStreaming: () => void;
-}
-
-export function useAiSearch(): UseAiSearchResult {
- const [streamingAnswer, setStreamingAnswer] = useState("");
- const [streamingSources, setStreamingSources] = useState([]);
-
- const clearStreaming = useCallback(() => {
- setStreamingAnswer("");
- setStreamingSources([]);
- }, []);
-
- const mutation = useMutation({
- mutationFn: async (params: IPageSearchParams & { contentType?: string }) => {
- setStreamingAnswer("");
- setStreamingSources([]);
-
- const { contentType, ...apiParams } = params;
-
- return await aiAnswers(apiParams, (chunk) => {
- if (chunk.content) {
- setStreamingAnswer((prev) => prev + chunk.content);
- }
- if (chunk.sources) {
- setStreamingSources(chunk.sources);
- }
- });
- },
- });
-
- return {
- ...mutation,
- streamingAnswer,
- streamingSources,
- clearStreaming,
- };
-}
diff --git a/apps/client/src/ee/ai/hooks/use-ai.ts b/apps/client/src/ee/ai/hooks/use-ai.ts
deleted file mode 100644
index 40c1ca12..00000000
--- a/apps/client/src/ee/ai/hooks/use-ai.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { useState, useCallback, useRef } from "react";
-import { useAiGenerateStreamMutation } from "@/ee/ai/queries/ai-query.ts";
-import { AiGenerateDto } from "@/ee/ai/types/ai.types.ts";
-
-export function useAiStream() {
- const [content, setContent] = useState("");
- const [isStreaming, setIsStreaming] = useState(false);
- const abortControllerRef = useRef(null);
- const mutation = useAiGenerateStreamMutation();
-
- const startStream = useCallback(
- async (data: AiGenerateDto) => {
- setContent("");
- setIsStreaming(true);
-
- try {
- const controller = await mutation.mutateAsync({
- ...data,
- onChunk: (chunk) => {
- setContent((prev) => prev + chunk.content);
- },
- onError: (error) => {
- console.error("AI stream error:", error);
- setIsStreaming(false);
- },
- onComplete: () => {
- setIsStreaming(false);
- },
- });
-
- abortControllerRef.current = controller;
- } catch (error) {
- console.error("Failed to start stream:", error);
- setIsStreaming(false);
- }
- },
- [mutation]
- );
-
- const stopStream = useCallback(() => {
- if (abortControllerRef.current) {
- abortControllerRef.current.abort();
- abortControllerRef.current = null;
- setIsStreaming(false);
- }
- }, []);
-
- const resetContent = useCallback(() => {
- setContent("");
- }, []);
-
- return {
- content,
- isStreaming,
- startStream,
- stopStream,
- resetContent,
- isLoading: mutation.isPending,
- error: mutation.error,
- };
-}
\ No newline at end of file
diff --git a/apps/client/src/ee/ai/pages/ai-settings.tsx b/apps/client/src/ee/ai/pages/ai-settings.tsx
deleted file mode 100644
index 3a5a281e..00000000
--- a/apps/client/src/ee/ai/pages/ai-settings.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { Helmet } from "react-helmet-async";
-import { getAppName } from "@/lib/config.ts";
-import SettingsTitle from "@/components/settings/settings-title.tsx";
-import React from "react";
-import useUserRole from "@/hooks/use-user-role.tsx";
-import { useTranslation } from "react-i18next";
-import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx";
-import EnableGenerativeAi from "@/ee/ai/components/enable-generative-ai.tsx";
-import EnableAiChat from "@/ee/ai-chat/components/enable-ai-chat.tsx";
-import McpSettings from "@/ee/ai/components/mcp-settings.tsx";
-import { Alert, Stack, Tabs } from "@mantine/core";
-import { IconInfoCircle } from "@tabler/icons-react";
-import { useHasFeature } from "@/ee/hooks/use-feature";
-import { Feature } from "@/ee/features";
-import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
-import { isCloud } from "@/lib/config.ts";
-import { useLocation, useNavigate } from "react-router-dom";
-
-export default function AiSettings() {
- const { t } = useTranslation();
- const { isAdmin } = useUserRole();
- const hasAccess = useHasFeature(Feature.AI);
- const upgradeLabel = useUpgradeLabel();
- const location = useLocation();
- const navigate = useNavigate();
-
- const activeTab = location.pathname.endsWith("/mcp") ? "mcp" : "ai";
-
- if (!isAdmin) {
- return null;
- }
-
- const handleTabChange = (value: string | null) => {
- if (value === "mcp") {
- navigate("/settings/ai/mcp");
- } else {
- navigate("/settings/ai");
- }
- };
-
- return (
- <>
-
- AI settings - {getAppName()}
-
-
-
-
-
-
- {t("AI")}
-
-
- {t("MCP")}
-
-
-
-
- {!hasAccess && (
- }
- title={upgradeLabel}
- color="blue"
- mb="lg"
- >
- {t(
- "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
- )}
-
- )}
-
-
- {!isCloud() && }
-
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/apps/client/src/ee/ai/queries/ai-query.ts b/apps/client/src/ee/ai/queries/ai-query.ts
deleted file mode 100644
index 076de9c7..00000000
--- a/apps/client/src/ee/ai/queries/ai-query.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import {
- useMutation,
- UseMutationResult,
- useQuery,
- UseQueryResult,
-} from "@tanstack/react-query";
-import {
- generateAiContent,
- generateAiContentStream,
-} from "@/ee/ai/services/ai-service.ts";
-import {
- AiConfigResponse,
- AiContentResponse,
- AiGenerateDto,
- AiStreamChunk,
- AiStreamError,
-} from "@/ee/ai/types/ai.types.ts";
-
-export function useAiGenerateMutation(): UseMutationResult<
- AiContentResponse,
- Error,
- AiGenerateDto
-> {
- return useMutation({
- mutationFn: (data: AiGenerateDto) => generateAiContent(data),
- });
-}
-
-interface StreamCallbacks {
- onChunk: (chunk: AiStreamChunk) => void;
- onError?: (error: AiStreamError) => void;
- onComplete?: () => void;
-}
-
-export function useAiGenerateStreamMutation(): UseMutationResult<
- AbortController,
- Error,
- AiGenerateDto & StreamCallbacks
-> {
- return useMutation({
- mutationFn: ({ onChunk, onError, onComplete, ...data }) =>
- generateAiContentStream(data, onChunk, onError, onComplete),
- });
-}
diff --git a/apps/client/src/ee/ai/services/ai-search-service.ts b/apps/client/src/ee/ai/services/ai-search-service.ts
deleted file mode 100644
index 8c2af64a..00000000
--- a/apps/client/src/ee/ai/services/ai-search-service.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import api from "@/lib/api-client.ts";
-import { IPageSearchParams } from "@/features/search/types/search.types.ts";
-
-export interface IAiSearchResponse {
- answer: string;
- sources?: Array<{
- pageId: string;
- title: string;
- slugId: string;
- spaceSlug: string;
- similarity: number;
- distance: number;
- chunkIndex: number;
- excerpt: string;
- }>;
-}
-
-export async function aiAnswers(
- params: IPageSearchParams,
- onChunk?: (chunk: { content?: string; sources?: any[] }) => void,
-): Promise {
- const response = await fetch("/api/ai/answers", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- credentials: "include",
- body: JSON.stringify(params),
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const reader = response.body?.getReader();
- const decoder = new TextDecoder();
-
- let answer = "";
- let sources: any[] = [];
- let buffer = "";
-
- if (reader) {
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split("\n");
-
- // Keep the last incomplete line in the buffer
- buffer = lines.pop() || "";
-
- for (const line of lines) {
- if (line.startsWith("data: ")) {
- const data = line.slice(6);
- if (data === "[DONE]") break;
-
- try {
- const parsed = JSON.parse(data);
- if (parsed.error) {
- throw new Error(parsed.error);
- }
- if (parsed.content) {
- answer += parsed.content;
- onChunk?.({ content: parsed.content });
- }
- if (parsed.sources) {
- sources = parsed.sources;
- onChunk?.({ sources: parsed.sources });
- }
- } catch (e) {
- if (e instanceof Error) {
- throw e;
- }
- // Skip invalid JSON
- }
- }
- }
- }
- }
-
- return { answer, sources };
-}
diff --git a/apps/client/src/ee/ai/services/ai-service.ts b/apps/client/src/ee/ai/services/ai-service.ts
deleted file mode 100644
index 88557ff1..00000000
--- a/apps/client/src/ee/ai/services/ai-service.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import api from "@/lib/api-client.ts";
-import {
- AiGenerateDto,
- AiContentResponse,
- AiStreamChunk,
- AiStreamError,
-} from "@/ee/ai/types/ai.types.ts";
-
-export async function generateAiContent(
- data: AiGenerateDto,
-): Promise {
- const req = await api.post("/ai/generate", data);
- return req.data;
-}
-
-export async function generateAiContentStream(
- data: AiGenerateDto,
- onChunk: (chunk: AiStreamChunk) => void,
- onError?: (error: AiStreamError) => void,
- onComplete?: () => void,
-): Promise {
- const abortController = new AbortController();
- try {
- const response = await fetch("/api/ai/generate/stream", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(data),
- signal: abortController.signal,
- credentials: "include", // This ensures cookies are sent, matching axios withCredentials
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const reader = response.body?.getReader();
- const decoder = new TextDecoder();
-
- if (!reader) {
- throw new Error("Response body is not readable");
- }
-
- const processStream = async () => {
- let buffer = "";
- try {
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split("\n");
-
- buffer = lines.pop() || "";
-
- for (const line of lines) {
- if (line.startsWith("data: ")) {
- const data = line.slice(6);
- if (data === "[DONE]") {
- onComplete?.();
- return;
- }
- try {
- const parsed = JSON.parse(data);
- if (parsed.error) {
- onError?.(parsed);
- } else {
- onChunk(parsed);
- }
- } catch (e) {
- // Skip invalid JSON
- }
- }
- }
- }
- } catch (error) {
- if (error.name !== "AbortError") {
- onError?.({ error: error.message });
- }
- } finally {
- reader.releaseLock();
- }
- };
-
- processStream();
- } catch (error) {
- onError?.({ error: error.message });
- }
-
- return abortController;
-}
diff --git a/apps/client/src/ee/ai/types/ai.types.ts b/apps/client/src/ee/ai/types/ai.types.ts
deleted file mode 100644
index 54778563..00000000
--- a/apps/client/src/ee/ai/types/ai.types.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-export enum AiAction {
- IMPROVE_WRITING = "improve_writing",
- FIX_SPELLING_GRAMMAR = "fix_spelling_grammar",
- MAKE_SHORTER = "make_shorter",
- MAKE_LONGER = "make_longer",
- SIMPLIFY = "simplify",
- CHANGE_TONE = "change_tone",
- SUMMARIZE = "summarize",
- EXPLAIN = "explain",
- CONTINUE_WRITING = "continue_writing",
- TRANSLATE = "translate",
- CUSTOM = "custom",
-}
-
-export interface AiGenerateDto {
- action?: AiAction;
- content: string;
- prompt?: string;
-}
-
-export interface AiContentResponse {
- content: string;
- usage?: {
- promptTokens: number;
- completionTokens: number;
- totalTokens: number;
- };
-}
-
-export interface AiConfigResponse {
- configured: boolean;
- availableActions: AiAction[];
-}
-
-export interface AiStreamChunk {
- content: string;
-}
-
-export interface AiStreamError {
- error: string;
-}
diff --git a/apps/client/src/ee/api-key/components/api-key-created-modal.tsx b/apps/client/src/ee/api-key/components/api-key-created-modal.tsx
deleted file mode 100644
index e787c0d4..00000000
--- a/apps/client/src/ee/api-key/components/api-key-created-modal.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import {
- Modal,
- Text,
- Stack,
- Alert,
- Group,
- Button,
- TextInput,
-} from "@mantine/core";
-import { IconAlertTriangle } from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import { IApiKey } from "@/ee/api-key";
-import CopyTextButton from "@/components/common/copy.tsx";
-
-interface ApiKeyCreatedModalProps {
- opened: boolean;
- onClose: () => void;
- apiKey: IApiKey;
-}
-
-export function ApiKeyCreatedModal({
- opened,
- onClose,
- apiKey,
-}: ApiKeyCreatedModalProps) {
- const { t } = useTranslation();
-
- if (!apiKey) return null;
-
- return (
-
-
- }
- title={t("Important")}
- color="red"
- >
- {t(
- "Make sure to copy your {{credential}} now. You won't be able to see it again!",
- { credential: t("API key") },
- )}
-
-
-
-
- {t("API key")}
-
-
-
-
-
-
-
-
-
- {t("I've saved my {{credential}}", { credential: t("API key") })}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/api-key/components/api-key-table.tsx b/apps/client/src/ee/api-key/components/api-key-table.tsx
deleted file mode 100644
index efb77448..00000000
--- a/apps/client/src/ee/api-key/components/api-key-table.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core";
-import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import { IApiKey } from "@/ee/api-key";
-import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
-import React from "react";
-import NoTableResults from "@/components/common/no-table-results";
-import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
-
-interface ApiKeyTableProps {
- apiKeys: IApiKey[];
- isLoading?: boolean;
- showUserColumn?: boolean;
- onUpdate?: (apiKey: IApiKey) => void;
- onRevoke?: (apiKey: IApiKey) => void;
-}
-
-export function ApiKeyTable({
- apiKeys,
- isLoading,
- showUserColumn = false,
- onUpdate,
- onRevoke,
-}: ApiKeyTableProps) {
- const { t } = useTranslation();
- const locale = useDateFnsLocale();
-
- const formatDate = (date: Date | string | null) => {
- if (!date) return t("Never");
- return formatLocalized(date, "MMM dd, yyyy", "PP", locale);
- };
-
- const isExpired = (expiresAt: string | null) => {
- if (!expiresAt) return false;
- return new Date(expiresAt) < new Date();
- };
-
- return (
-
-
-
-
- {t("Name")}
- {showUserColumn && {t("User")} }
- {t("Last used")}
- {t("Expires")}
- {t("Created")}
-
-
-
-
-
- {apiKeys && apiKeys.length > 0 ? (
- apiKeys.map((apiKey: IApiKey, index: number) => (
-
-
-
- {apiKey.name}
-
-
-
- {showUserColumn && apiKey.creator && (
-
-
-
-
- {apiKey.creator.name}
-
-
-
- )}
-
-
-
- {formatDate(apiKey.lastUsedAt)}
-
-
-
-
- {apiKey.expiresAt ? (
- isExpired(apiKey.expiresAt) ? (
-
- {t("Expired")}
-
- ) : (
-
- {formatDate(apiKey.expiresAt)}
-
- )
- ) : (
-
- {t("Never")}
-
- )}
-
-
-
-
- {formatDate(apiKey.createdAt)}
-
-
-
-
-
-
-
-
-
-
-
- {onUpdate && (
- }
- onClick={() => onUpdate(apiKey)}
- >
- {t("Rename")}
-
- )}
- {onRevoke && (
- }
- color="red"
- onClick={() => onRevoke(apiKey)}
- >
- {t("Revoke")}
-
- )}
-
-
-
-
- ))
- ) : (
-
- )}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/api-key/components/create-api-key-modal.tsx b/apps/client/src/ee/api-key/components/create-api-key-modal.tsx
deleted file mode 100644
index 53341a61..00000000
--- a/apps/client/src/ee/api-key/components/create-api-key-modal.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import { lazy, Suspense, useState } from "react";
-import { Modal, TextInput, Button, Group, Stack, Select } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { z } from "zod/v4";
-import { useTranslation } from "react-i18next";
-import { useCreateApiKeyMutation } from "@/ee/api-key/queries/api-key-query";
-import { IconCalendar } from "@tabler/icons-react";
-import { IApiKey } from "@/ee/api-key";
-
-const DateInput = lazy(() =>
- import("@mantine/dates").then((module) => ({
- default: module.DateInput,
- })),
-);
-
-interface CreateApiKeyModalProps {
- opened: boolean;
- onClose: () => void;
- onSuccess: (response: IApiKey) => void;
-}
-
-const formSchema = z.object({
- name: z.string().min(1, "Name is required"),
- expiresAt: z.string().optional(),
-});
-type FormValues = z.infer;
-
-export function CreateApiKeyModal({
- opened,
- onClose,
- onSuccess,
-}: CreateApiKeyModalProps) {
- const { t, i18n } = useTranslation();
- const [expirationOption, setExpirationOption] = useState("30");
- const createApiKeyMutation = useCreateApiKeyMutation();
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- name: "",
- expiresAt: "",
- },
- });
-
- const getExpirationDate = (): string | undefined => {
- if (expirationOption === "never") {
- return undefined;
- }
- if (expirationOption === "custom") {
- return form.values.expiresAt;
- }
- const days = parseInt(expirationOption);
- const date = new Date();
- date.setDate(date.getDate() + days);
- return date.toISOString();
- };
-
- const getExpirationLabel = (days: number) => {
- const date = new Date();
- date.setDate(date.getDate() + days);
- const formatted = date.toLocaleDateString(i18n.language, {
- month: "short",
- day: "2-digit",
- year: "numeric",
- });
- return `${days} days (${formatted})`;
- };
-
- const expirationOptions = [
- { value: "30", label: getExpirationLabel(30) },
- { value: "60", label: getExpirationLabel(60) },
- { value: "90", label: getExpirationLabel(90) },
- { value: "365", label: getExpirationLabel(365) },
- { value: "custom", label: t("Custom") },
- { value: "never", label: t("No expiration") },
- ];
-
- const handleSubmit = async (data: {
- name?: string;
- expiresAt?: string | Date;
- }) => {
- const groupData = {
- name: data.name,
- expiresAt: getExpirationDate(),
- };
-
- try {
- const createdKey = await createApiKeyMutation.mutateAsync(groupData);
- onSuccess(createdKey);
- form.reset();
- onClose();
- } catch (err) {
- //
- }
- };
-
- const handleClose = () => {
- form.reset();
- setExpirationOption("30");
- onClose();
- };
-
- return (
-
-
-
- );
-}
diff --git a/apps/client/src/ee/api-key/components/restrict-api-to-admins.tsx b/apps/client/src/ee/api-key/components/restrict-api-to-admins.tsx
deleted file mode 100644
index 356d3dcb..00000000
--- a/apps/client/src/ee/api-key/components/restrict-api-to-admins.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Text, Switch, Tooltip } from "@mantine/core";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import React, { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
-import { notifications } from "@mantine/notifications";
-import { useHasFeature } from "@/ee/hooks/use-feature";
-import { Feature } from "@/ee/features";
-import {
- ResponsiveSettingsRow,
- ResponsiveSettingsContent,
- ResponsiveSettingsControl,
-} from "@/components/ui/responsive-settings-row";
-import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
-
-export default function RestrictApiToAdmins() {
- const { t } = useTranslation();
- const [workspace, setWorkspace] = useAtom(workspaceAtom);
- const [checked, setChecked] = useState(
- workspace?.settings?.api?.restrictToAdmins === true,
- );
- const hasAccess = useHasFeature(Feature.API_KEYS);
- const upgradeLabel = useUpgradeLabel();
-
- const handleChange = async (event: React.ChangeEvent) => {
- const value = event.currentTarget.checked;
- try {
- const updatedWorkspace = await updateWorkspace({
- restrictApiToAdmins: value,
- });
- setChecked(value);
- setWorkspace(updatedWorkspace);
- } catch (err) {
- notifications.show({
- message: err?.response?.data?.message,
- color: "red",
- });
- }
- };
-
- return (
-
-
-
- {t("Restrict API key creation to admins")}
-
-
- {t(
- "Only admins and owners can create new API keys. Existing member keys will continue to work.",
- )}
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/api-key/components/revoke-api-key-modal.tsx b/apps/client/src/ee/api-key/components/revoke-api-key-modal.tsx
deleted file mode 100644
index cb4e2326..00000000
--- a/apps/client/src/ee/api-key/components/revoke-api-key-modal.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Modal, Text, Button, Group, Stack } from "@mantine/core";
-import { useTranslation } from "react-i18next";
-
-import { useRevokeApiKeyMutation } from "@/ee/api-key/queries/api-key-query.ts";
-import { IApiKey } from "@/ee/api-key";
-
-interface RevokeApiKeyModalProps {
- opened: boolean;
- onClose: () => void;
- apiKey: IApiKey | null;
-}
-
-export function RevokeApiKeyModal({
- opened,
- onClose,
- apiKey,
-}: RevokeApiKeyModalProps) {
- const { t } = useTranslation();
- const revokeApiKeyMutation = useRevokeApiKeyMutation();
-
- const handleRevoke = async () => {
- if (!apiKey) return;
- await revokeApiKeyMutation.mutateAsync({
- apiKeyId: apiKey.id,
- });
- onClose();
- };
-
- return (
-
-
-
- {t("Are you sure you want to revoke this {{credential}}", {
- credential: t("API key"),
- })}{" "}
- {apiKey?.name} ?
-
-
- {t(
- "This action cannot be undone. Any applications using this API key will stop working.",
- )}
-
-
-
-
- {t("Cancel")}
-
-
- {t("Revoke")}
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/api-key/components/update-api-key-modal.tsx b/apps/client/src/ee/api-key/components/update-api-key-modal.tsx
deleted file mode 100644
index a7d37a44..00000000
--- a/apps/client/src/ee/api-key/components/update-api-key-modal.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { Modal, TextInput, Button, Group, Stack } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { z } from "zod/v4";
-import { useTranslation } from "react-i18next";
-import { useUpdateApiKeyMutation } from "@/ee/api-key/queries/api-key-query";
-import { IApiKey } from "@/ee/api-key";
-import { useEffect } from "react";
-
-const formSchema = z.object({
- name: z.string().min(1, "Name is required"),
-});
-type FormValues = z.infer;
-
-interface UpdateApiKeyModalProps {
- opened: boolean;
- onClose: () => void;
- apiKey: IApiKey | null;
-}
-
-export function UpdateApiKeyModal({
- opened,
- onClose,
- apiKey,
-}: UpdateApiKeyModalProps) {
- const { t } = useTranslation();
- const updateApiKeyMutation = useUpdateApiKeyMutation();
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- name: "",
- },
- });
-
- useEffect(() => {
- if (opened && apiKey) {
- form.setValues({ name: apiKey.name });
- }
- }, [opened, apiKey]);
-
- const handleSubmit = async (data: { name?: string }) => {
- const apiKeyData = {
- apiKeyId: apiKey.id,
- name: data.name,
- };
-
- await updateApiKeyMutation.mutateAsync(apiKeyData);
- onClose();
- };
-
- return (
-
-
-
- );
-}
diff --git a/apps/client/src/ee/api-key/index.ts b/apps/client/src/ee/api-key/index.ts
deleted file mode 100644
index 24bc3d1c..00000000
--- a/apps/client/src/ee/api-key/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export { ApiKeyTable } from "./components/api-key-table";
-export { CreateApiKeyModal } from "./components/create-api-key-modal";
-export { ApiKeyCreatedModal } from "./components/api-key-created-modal";
-export { UpdateApiKeyModal } from "./components/update-api-key-modal";
-export { RevokeApiKeyModal } from "./components/revoke-api-key-modal";
-
-// Services
-export * from "./services/api-key-service";
-
-// Types
-export * from "./types/api-key.types";
diff --git a/apps/client/src/ee/api-key/pages/user-api-keys.tsx b/apps/client/src/ee/api-key/pages/user-api-keys.tsx
deleted file mode 100644
index c305f4af..00000000
--- a/apps/client/src/ee/api-key/pages/user-api-keys.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import React, { useState } from "react";
-import { Anchor, Alert, Button, Group, Space, Text } from "@mantine/core";
-import { IconInfoCircle } from "@tabler/icons-react";
-import { Helmet } from "react-helmet-async";
-import { Trans, useTranslation } from "react-i18next";
-import SettingsTitle from "@/components/settings/settings-title";
-import { getAppName, getAppUrl } from "@/lib/config";
-import { ApiKeyTable } from "@/ee/api-key/components/api-key-table";
-import { CreateApiKeyModal } from "@/ee/api-key/components/create-api-key-modal";
-import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-modal";
-import { UpdateApiKeyModal } from "@/ee/api-key/components/update-api-key-modal";
-import { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal";
-import Paginate from "@/components/common/paginate";
-import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
-import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts";
-import { IApiKey } from "@/ee/api-key";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import useUserRole from "@/hooks/use-user-role.tsx";
-
-export default function UserApiKeys() {
- const { t } = useTranslation();
- const { cursor, goNext, goPrev } = useCursorPaginate();
- const [createModalOpened, setCreateModalOpened] = useState(false);
- const [createdApiKey, setCreatedApiKey] = useState(null);
- const [updateModalOpened, setUpdateModalOpened] = useState(false);
- const [revokeModalOpened, setRevokeModalOpened] = useState(false);
- const [selectedApiKey, setSelectedApiKey] = useState(null);
- const { data, isLoading } = useGetApiKeysQuery({ cursor });
- const [workspace] = useAtom(workspaceAtom);
- const { isAdmin } = useUserRole();
- const mcpEnabled = workspace?.settings?.ai?.mcp === true;
- const restrictToAdmins = workspace?.settings?.api?.restrictToAdmins === true;
- const canCreate = !restrictToAdmins || isAdmin;
-
- const handleCreateSuccess = (response: IApiKey) => {
- setCreatedApiKey(response);
- };
-
- const handleUpdate = (apiKey: IApiKey) => {
- setSelectedApiKey(apiKey);
- setUpdateModalOpened(true);
- };
-
- const handleRevoke = (apiKey: IApiKey) => {
- setSelectedApiKey(apiKey);
- setRevokeModalOpened(true);
- };
-
- return (
- <>
-
-
- {t("API keys")} - {getAppName()}
-
-
-
-
-
-
- ,
- }}
- />
-
-
- {mcpEnabled && canCreate && (
- }>
-
- {t(
- "Your workspace has MCP enabled. Use your API key to connect AI assistants.",
- )}{" "}
-
- {t("Learn more")}
-
-
-
- {t("MCP server URL:")}{" "}
-
- {`${getAppUrl()}/mcp`}
-
-
-
- )}
-
- {canCreate ? (
-
- setCreateModalOpened(true)}>
- {t("Create API Key")}
-
-
- ) : restrictToAdmins ? (
- }>
-
- {t("API key creation is restricted to admins by your workspace administrator.")}
-
-
- ) : null}
-
-
-
-
-
- {data?.items.length > 0 && (
- goNext(data?.meta?.nextCursor)}
- onPrev={goPrev}
- />
- )}
-
- setCreateModalOpened(false)}
- onSuccess={handleCreateSuccess}
- />
-
- setCreatedApiKey(null)}
- apiKey={createdApiKey}
- />
-
- {
- setUpdateModalOpened(false);
- setSelectedApiKey(null);
- }}
- apiKey={selectedApiKey}
- />
-
- {
- setRevokeModalOpened(false);
- setSelectedApiKey(null);
- }}
- apiKey={selectedApiKey}
- />
- >
- );
-}
diff --git a/apps/client/src/ee/api-key/pages/workspace-api-keys.tsx b/apps/client/src/ee/api-key/pages/workspace-api-keys.tsx
deleted file mode 100644
index 8476f445..00000000
--- a/apps/client/src/ee/api-key/pages/workspace-api-keys.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import React, { useState } from "react";
-import { Anchor, Button, Divider, Group, Space, Text } from "@mantine/core";
-import { Helmet } from "react-helmet-async";
-import { Trans, useTranslation } from "react-i18next";
-import SettingsTitle from "@/components/settings/settings-title";
-import { getAppName } from "@/lib/config";
-import { ApiKeyTable } from "@/ee/api-key/components/api-key-table";
-import { CreateApiKeyModal } from "@/ee/api-key/components/create-api-key-modal";
-import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-modal";
-import { UpdateApiKeyModal } from "@/ee/api-key/components/update-api-key-modal";
-import { RevokeApiKeyModal } from "@/ee/api-key/components/revoke-api-key-modal";
-import Paginate from "@/components/common/paginate";
-import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
-import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts";
-import { IApiKey } from "@/ee/api-key";
-import useUserRole from '@/hooks/use-user-role.tsx';
-import RestrictApiToAdmins from "@/ee/api-key/components/restrict-api-to-admins";
-
-export default function WorkspaceApiKeys() {
- const { t } = useTranslation();
- const { cursor, goNext, goPrev } = useCursorPaginate();
- const [createModalOpened, setCreateModalOpened] = useState(false);
- const [createdApiKey, setCreatedApiKey] = useState(null);
- const [updateModalOpened, setUpdateModalOpened] = useState(false);
- const [revokeModalOpened, setRevokeModalOpened] = useState(false);
- const [selectedApiKey, setSelectedApiKey] = useState(null);
- const { data, isLoading } = useGetApiKeysQuery({ cursor, adminView: true });
- const { isAdmin } = useUserRole();
-
- if (!isAdmin) {
- return null;
- }
-
- const handleCreateSuccess = (response: IApiKey) => {
- setCreatedApiKey(response);
- };
-
- const handleUpdate = (apiKey: IApiKey) => {
- setSelectedApiKey(apiKey);
- setUpdateModalOpened(true);
- };
-
- const handleRevoke = (apiKey: IApiKey) => {
- setSelectedApiKey(apiKey);
- setRevokeModalOpened(true);
- };
-
- return (
- <>
-
-
- {t("API management")} - {getAppName()}
-
-
-
-
-
-
- ,
- }}
- />
-
-
-
-
-
-
- setCreateModalOpened(true)}>
- {t("Create API Key")}
-
-
-
-
-
-
-
- {data?.items.length > 0 && (
- goNext(data?.meta?.nextCursor)}
- onPrev={goPrev}
- />
- )}
-
- setCreateModalOpened(false)}
- onSuccess={handleCreateSuccess}
- />
-
- setCreatedApiKey(null)}
- apiKey={createdApiKey}
- />
-
- {
- setUpdateModalOpened(false);
- setSelectedApiKey(null);
- }}
- apiKey={selectedApiKey}
- />
-
- {
- setRevokeModalOpened(false);
- setSelectedApiKey(null);
- }}
- apiKey={selectedApiKey}
- />
- >
- );
-}
diff --git a/apps/client/src/ee/api-key/queries/api-key-query.ts b/apps/client/src/ee/api-key/queries/api-key-query.ts
deleted file mode 100644
index f27492da..00000000
--- a/apps/client/src/ee/api-key/queries/api-key-query.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { IPagination, QueryParams } from "@/lib/types.ts";
-import {
- keepPreviousData,
- useMutation,
- useQuery,
- useQueryClient,
- UseQueryResult,
-} from "@tanstack/react-query";
-import {
- createApiKey,
- getApiKeys,
- IApiKey,
- ICreateApiKeyRequest,
- IUpdateApiKeyRequest,
- revokeApiKey,
- updateApiKey,
-} from "@/ee/api-key";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-
-export function useGetApiKeysQuery(
- params?: QueryParams,
-): UseQueryResult, Error> {
- return useQuery({
- queryKey: ["api-key-list", params],
- queryFn: () => getApiKeys(params),
- staleTime: 0,
- gcTime: 0,
- placeholderData: keepPreviousData,
- });
-}
-
-export function useRevokeApiKeyMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation<
- void,
- Error,
- {
- apiKeyId: string;
- }
- >({
- mutationFn: (data) => revokeApiKey(data),
- onSuccess: (data, variables) => {
- notifications.show({ message: t("Revoked successfully") });
- queryClient.invalidateQueries({
- predicate: (item) =>
- ["api-key-list"].includes(item.queryKey[0] as string),
- });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({ message: errorMessage, color: "red" });
- },
- });
-}
-
-export function useCreateApiKeyMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => createApiKey(data),
- onSuccess: () => {
- notifications.show({
- message: t("{{credential}} created successfully", {
- credential: t("API key"),
- }),
- });
- queryClient.invalidateQueries({
- predicate: (item) =>
- ["api-key-list"].includes(item.queryKey[0] as string),
- });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({ message: errorMessage, color: "red" });
- },
- });
-}
-
-export function useUpdateApiKeyMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => updateApiKey(data),
- onSuccess: (data, variables) => {
- notifications.show({ message: t("Updated successfully") });
- queryClient.invalidateQueries({
- predicate: (item) =>
- ["api-key-list"].includes(item.queryKey[0] as string),
- });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({ message: errorMessage, color: "red" });
- },
- });
-}
diff --git a/apps/client/src/ee/api-key/services/api-key-service.ts b/apps/client/src/ee/api-key/services/api-key-service.ts
deleted file mode 100644
index c83e25d0..00000000
--- a/apps/client/src/ee/api-key/services/api-key-service.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import api from "@/lib/api-client";
-import {
- ICreateApiKeyRequest,
- IApiKey,
- IUpdateApiKeyRequest,
-} from "@/ee/api-key/types/api-key.types";
-import { IPagination, QueryParams } from "@/lib/types.ts";
-
-export async function getApiKeys(
- params?: QueryParams,
-): Promise> {
- const req = await api.post("/api-keys", { ...params });
- return req.data;
-}
-
-export async function createApiKey(
- data: ICreateApiKeyRequest,
-): Promise {
- const req = await api.post("/api-keys/create", data);
- return req.data;
-}
-
-export async function updateApiKey(
- data: IUpdateApiKeyRequest,
-): Promise {
- const req = await api.post("/api-keys/update", data);
- return req.data;
-}
-
-export async function revokeApiKey(data: { apiKeyId: string }): Promise {
- await api.post("/api-keys/revoke", data);
-}
diff --git a/apps/client/src/ee/api-key/types/api-key.types.ts b/apps/client/src/ee/api-key/types/api-key.types.ts
deleted file mode 100644
index 57890d1a..00000000
--- a/apps/client/src/ee/api-key/types/api-key.types.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { IUser } from "@/features/user/types/user.types.ts";
-
-export interface IApiKey {
- id: string;
- name: string;
- token?: string;
- creatorId: string;
- workspaceId: string;
- expiresAt: string | null;
- lastUsedAt: string | null;
- createdAt: string;
- creator: Partial;
-}
-
-export interface ICreateApiKeyRequest {
- name: string;
- expiresAt?: string;
-}
-
-export interface IUpdateApiKeyRequest {
- apiKeyId: string;
- name: string;
-}
diff --git a/apps/client/src/ee/audit/components/audit-logs-table.tsx b/apps/client/src/ee/audit/components/audit-logs-table.tsx
deleted file mode 100644
index 3fac73f5..00000000
--- a/apps/client/src/ee/audit/components/audit-logs-table.tsx
+++ /dev/null
@@ -1,333 +0,0 @@
-import { Fragment, useState } from "react";
-import {
- Table,
- Text,
- Group,
- Skeleton,
- Anchor,
- Collapse,
- Box,
-} from "@mantine/core";
-import { Link } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-import {
- IconChevronRight,
- IconChevronDown,
- IconArrowRight,
-} from "@tabler/icons-react";
-import { IAuditLog } from "@/ee/audit/types/audit.types";
-import { CustomAvatar } from "@/components/ui/custom-avatar";
-import { getEventLabel } from "@/ee/audit/lib/audit-event-labels";
-import { formattedDate } from "@/lib/time";
-import NoTableResults from "@/components/common/no-table-results";
-import classes from "./audit-logs.module.css";
-
-type AuditLogsTableProps = {
- items?: IAuditLog[];
- isLoading: boolean;
-};
-
-function hasDetails(entry: IAuditLog): boolean {
- return !!(entry.changes?.before || entry.changes?.after || entry.metadata);
-}
-
-function getResourceUrl(entry: IAuditLog): string | null {
- if (!entry.resource) return null;
-
- switch (entry.resourceType) {
- case "group":
- return `/settings/groups/${entry.resource.id}`;
- case "space":
- case "space_member":
- return entry.resource.slug ? `/s/${entry.resource.slug}` : null;
- default:
- return null;
- }
-}
-
-function formatValue(value: unknown): string {
- if (value === null || value === undefined) return "—";
- if (typeof value === "boolean") return value ? "true" : "false";
- if (typeof value === "object") return JSON.stringify(value);
- return String(value);
-}
-
-function ChangesDiff({ changes }: { changes: IAuditLog["changes"] }) {
- const { t } = useTranslation();
- if (!changes) return null;
-
- const { before, after } = changes;
- const allKeys = new Set([
- ...Object.keys(before ?? {}),
- ...Object.keys(after ?? {}),
- ]);
-
- if (allKeys.size === 0) return null;
-
- return (
-
-
- {t("Changes")}
-
- {[...allKeys].map((key) => {
- const hasBefore = before && key in before;
- const hasAfter = after && key in after;
-
- return (
-
-
- {key}:
-
- {hasBefore && (
-
- {formatValue(before[key])}
-
- )}
- {hasBefore && hasAfter && (
-
- )}
- {hasAfter && (
-
- {formatValue(after[key])}
-
- )}
-
- );
- })}
-
- );
-}
-
-function MetadataDisplay({ metadata }: { metadata: Record }) {
- const { t } = useTranslation();
- const entries = Object.entries(metadata);
- if (entries.length === 0) return null;
-
- return (
-
-
- {t("Metadata")}
-
- {entries.map(([key, value]) => (
-
-
- {key}:
-
- {formatValue(value)}
-
- ))}
-
- );
-}
-
-function TableSkeleton() {
- return (
- <>
- {Array.from({ length: 8 }).map((_, i) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
- >
- );
-}
-
-function ResourceCell({ entry }: { entry: IAuditLog }) {
- if (!entry.resource?.name) {
- return (
-
- —
-
- );
- }
-
- const url = getResourceUrl(entry);
-
- if (url) {
- return (
-
-
-
- {entry.resource.name}
-
-
-
- );
- }
-
- return (
-
- {entry.resource.name}
-
- );
-}
-
-export default function AuditLogsTable({
- items,
- isLoading,
-}: AuditLogsTableProps) {
- const { t } = useTranslation();
- const [expanded, setExpanded] = useState>(new Set());
-
- const toggleExpanded = (id: string) => {
- setExpanded((prev) => {
- const next = new Set(prev);
- if (next.has(id)) {
- next.delete(id);
- } else {
- next.add(id);
- }
- return next;
- });
- };
-
- return (
-
-
-
-
- {t("Actor")}
- {t("Event")}
- {t("Resource")}
- {t("Date")}
-
-
-
-
- {isLoading ? (
-
- ) : items && items.length > 0 ? (
- items.map((entry) => {
- const expandable = hasDetails(entry);
- const isExpanded = expanded.has(entry.id);
-
- return (
-
- toggleExpanded(entry.id) : undefined
- }
- style={{ cursor: expandable ? "pointer" : undefined }}
- >
-
-
- {expandable ? (
- isExpanded ? (
-
- ) : (
-
- )
- ) : (
-
- )}
- {entry.actor ? (
-
-
-
-
- {entry.actor.name}
-
-
- {entry.actor.email}
-
-
-
- ) : (
-
- {entry.actorType === "system"
- ? t("System")
- : t("System")}
-
- )}
-
-
-
-
- {t(getEventLabel(entry.event))}
-
-
-
-
-
-
-
-
- {formattedDate(new Date(entry.createdAt))}
-
-
-
-
- {expandable && (
-
-
-
-
-
- {entry.changes && (
-
- )}
- {entry.metadata && (
-
- )}
-
-
-
-
-
- )}
-
- );
- })
- ) : (
-
- )}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/audit/components/audit-logs.module.css b/apps/client/src/ee/audit/components/audit-logs.module.css
deleted file mode 100644
index 2d095682..00000000
--- a/apps/client/src/ee/audit/components/audit-logs.module.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.table {
- --table-border-color: var(--mantine-color-gray-2);
-
- @mixin dark {
- --table-border-color: var(--mantine-color-dark-5);
- }
-}
-
-.resourceLinkText {
- width: fit-content;
-
- @mixin light {
- border-bottom: 0.05em solid var(--mantine-color-dark-0);
- }
- @mixin dark {
- border-bottom: 0.05em solid var(--mantine-color-dark-2);
- }
-}
-
-.detailRow {
- &:hover {
- background: none !important;
- }
-}
-
-.detailContent {
- @mixin light {
- background: var(--mantine-color-gray-0);
- }
- @mixin dark {
- background: var(--mantine-color-dark-7);
- }
-}
diff --git a/apps/client/src/ee/audit/lib/audit-event-labels.ts b/apps/client/src/ee/audit/lib/audit-event-labels.ts
deleted file mode 100644
index 7fc55b3d..00000000
--- a/apps/client/src/ee/audit/lib/audit-event-labels.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-type EventOption = {
- value: string;
- label: string;
-};
-
-type EventGroup = {
- group: string;
- items: EventOption[];
-};
-
-export const auditEventLabels: Record = {
- "workspace.created": "Created workspace",
- "workspace.updated": "Updated workspace",
- "workspace.invite_created": "Created invitation",
- "workspace.invite_resent": "Resent invitation",
- "workspace.invite_revoked": "Revoked invitation",
-
- "user.created": "Created user",
- "user.deleted": "Deleted user",
- "user.login": "Logged in",
- "user.logout": "Logged out",
- "user.role_changed": "Changed user role",
- "user.password_changed": "Changed password",
- "user.password_reset": "Reset password",
- "user.updated": "Updated user",
- "user.deactivated": "Deactivated user",
- "user.activated": "Activated user",
- "user.mfa_enabled": "Enabled MFA",
- "user.mfa_disabled": "Disabled MFA",
- "user.mfa_backup_code_generated": "Generated MFA backup codes",
-
- "api_key.created": "Created API key",
- "api_key.updated": "Updated API key",
- "api_key.deleted": "Deleted API key",
-
- "scim_token.created": "Created SCIM token",
- "scim_token.updated": "Updated SCIM token",
- "scim_token.deleted": "Deleted SCIM token",
-
- "space.created": "Created space",
- "space.updated": "Updated space",
- "space.deleted": "Deleted space",
- "space.member_added": "Added space member",
- "space.member_removed": "Removed space member",
- "space.member_role_changed": "Changed space member role",
- "space.exported": "Exported space",
-
- "group.created": "Created group",
- "group.updated": "Updated group",
- "group.deleted": "Deleted group",
- "group.member_added": "Added group member",
- "group.member_removed": "Removed group member",
-
- "comment.deleted": "Deleted comment",
-
- "page.trashed": "Trashed page",
- "page.deleted": "Deleted page",
- "page.restored": "Restored page",
- "page.imported": "Imported page",
- "page.exported": "Exported page",
- "page.restricted": "Restricted page",
- "page.restriction_removed": "Removed page restriction",
- "page.permission_added": "Added page permission",
- "page.permission_removed": "Removed page permission",
- "page.verification_created": "Created page verification",
- "page.verification_updated": "Updated page verification",
- "page.verification_removed": "Removed page verification",
- "page.verified": "Verified page",
- "page.approval_requested": "Requested page approval",
- "page.approval_rejected": "Rejected page approval",
- "page.marked_obsolete": "Marked page as obsolete",
-
- "share.created": "Created share link",
- "share.deleted": "Deleted share link",
-
- "sso.provider_created": "Created SSO provider",
- "sso.provider_updated": "Updated SSO provider",
- "sso.provider_deleted": "Deleted SSO provider",
-
- "license.activated": "Activated license",
- "license.removed": "Removed license",
-};
-
-export function getEventLabel(event: string): string {
- return auditEventLabels[event] ?? event;
-}
-
-export const eventFilterOptions: EventGroup[] = [
- {
- group: "Workspace",
- items: [
- { value: "workspace.updated", label: "Updated workspace" },
- { value: "workspace.invite_created", label: "Created invitation" },
- { value: "workspace.invite_revoked", label: "Revoked invitation" },
- ],
- },
- {
- group: "User",
- items: [
- { value: "user.login", label: "Logged in" },
- { value: "user.logout", label: "Logged out" },
- { value: "user.created", label: "Created user" },
- { value: "user.deleted", label: "Deleted user" },
- { value: "user.deactivated", label: "Deactivated user" },
- { value: "user.activated", label: "Activated user" },
- { value: "user.role_changed", label: "Changed user role" },
- { value: "user.password_changed", label: "Changed password" },
- { value: "user.mfa_enabled", label: "Enabled MFA" },
- { value: "user.mfa_disabled", label: "Disabled MFA" },
- ],
- },
- {
- group: "Space",
- items: [
- { value: "space.created", label: "Created space" },
- { value: "space.updated", label: "Updated space" },
- { value: "space.deleted", label: "Deleted space" },
- { value: "space.member_added", label: "Added space member" },
- { value: "space.member_removed", label: "Removed space member" },
- ],
- },
- {
- group: "Group",
- items: [
- { value: "group.created", label: "Created group" },
- { value: "group.updated", label: "Updated group" },
- { value: "group.deleted", label: "Deleted group" },
- { value: "group.member_added", label: "Added group member" },
- { value: "group.member_removed", label: "Removed group member" },
- ],
- },
- {
- group: "Comment",
- items: [
- { value: "comment.deleted", label: "Deleted comment" },
- ],
- },
- {
- group: "Page",
- items: [
- { value: "page.trashed", label: "Trashed page" },
- { value: "page.deleted", label: "Deleted page" },
- { value: "page.restored", label: "Restored page" },
- { value: "page.imported", label: "Imported page" },
- { value: "page.exported", label: "Exported page" },
- { value: "page.restricted", label: "Restricted page" },
- { value: "page.restriction_removed", label: "Removed page restriction" },
- { value: "page.permission_added", label: "Added page permission" },
- { value: "page.permission_removed", label: "Removed page permission" },
- { value: "page.verification_created", label: "Created page verification" },
- { value: "page.verification_updated", label: "Updated page verification" },
- { value: "page.verification_removed", label: "Removed page verification" },
- { value: "page.verified", label: "Verified page" },
- { value: "page.approval_requested", label: "Requested page approval" },
- { value: "page.approval_rejected", label: "Rejected page approval" },
- { value: "page.marked_obsolete", label: "Marked page as obsolete" },
- ],
- },
- {
- group: "Share",
- items: [
- { value: "share.created", label: "Created share link" },
- { value: "share.deleted", label: "Deleted share link" },
- ],
- },
- {
- group: "SSO",
- items: [
- { value: "sso.provider_created", label: "Created SSO provider" },
- { value: "sso.provider_updated", label: "Updated SSO provider" },
- { value: "sso.provider_deleted", label: "Deleted SSO provider" },
- ],
- },
- {
- group: "API key",
- items: [
- { value: "api_key.created", label: "Created API key" },
- { value: "api_key.deleted", label: "Deleted API key" },
- ],
- },
- {
- group: "SCIM token",
- items: [
- { value: "scim_token.created", label: "Created SCIM token" },
- { value: "scim_token.updated", label: "Updated SCIM token" },
- { value: "scim_token.deleted", label: "Deleted SCIM token" },
- ],
- },
- {
- group: "License",
- items: [
- { value: "license.activated", label: "Activated license" },
- { value: "license.removed", label: "Removed license" },
- ],
- },
-];
diff --git a/apps/client/src/ee/audit/pages/audit-logs.tsx b/apps/client/src/ee/audit/pages/audit-logs.tsx
deleted file mode 100644
index 05f7881a..00000000
--- a/apps/client/src/ee/audit/pages/audit-logs.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-import { useState, useMemo, useEffect } from "react";
-import {
- ActionIcon,
- Button,
- Group,
- NumberInput,
- Popover,
- Select,
- Space,
- Text,
- Tooltip,
-} from "@mantine/core";
-import { Helmet } from "react-helmet-async";
-import { useTranslation } from "react-i18next";
-import { IconSettings } from "@tabler/icons-react";
-import SettingsTitle from "@/components/settings/settings-title";
-import { getAppName } from "@/lib/config";
-import Paginate from "@/components/common/paginate";
-import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
-import {
- useAuditLogsQuery,
- useAuditRetentionQuery,
- useUpdateAuditRetentionMutation,
-} from "@/ee/audit/queries/audit-query";
-import { IAuditLogParams } from "@/ee/audit/types/audit.types";
-import { eventFilterOptions } from "@/ee/audit/lib/audit-event-labels";
-import AuditLogsTable from "@/ee/audit/components/audit-logs-table";
-import useUserRole from "@/hooks/use-user-role";
-
-type RetentionUnit = "days" | "months" | "years";
-
-function daysToRetention(days: number): { amount: number; unit: RetentionUnit } {
- if (days >= 365 && days % 365 === 0) {
- return { amount: days / 365, unit: "years" };
- }
- if (days >= 30 && days % 30 === 0) {
- return { amount: days / 30, unit: "months" };
- }
- return { amount: days, unit: "days" };
-}
-
-function retentionToDays(amount: number, unit: RetentionUnit): number {
- if (unit === "years") return amount * 365;
- if (unit === "months") return amount * 30;
- return amount;
-}
-
-export default function AuditLogs() {
- const { t } = useTranslation();
- const { isOwner } = useUserRole();
- const { cursor, goNext, goPrev, resetCursor } = useCursorPaginate();
-
- const [eventFilter, setEventFilter] = useState(null);
- const [settingsOpen, setSettingsOpen] = useState(false);
-
- const { data: retentionData } = useAuditRetentionQuery();
- const updateRetention = useUpdateAuditRetentionMutation();
-
- const currentDays = retentionData?.retentionDays ?? 365;
- const parsed = daysToRetention(currentDays);
- const [retentionAmount, setRetentionAmount] = useState(parsed.amount);
- const [retentionUnit, setRetentionUnit] = useState(parsed.unit);
-
- useEffect(() => {
- if (retentionData) {
- const { amount, unit } = daysToRetention(retentionData.retentionDays);
- setRetentionAmount(amount);
- setRetentionUnit(unit);
- }
- }, [retentionData?.retentionDays]);
-
- const resetRetentionForm = () => {
- const { amount, unit } = daysToRetention(currentDays);
- setRetentionAmount(amount);
- setRetentionUnit(unit);
- };
-
- const params: IAuditLogParams = useMemo(
- () => ({
- cursor,
- limit: 50,
- event: eventFilter ?? undefined,
- }),
- [cursor, eventFilter],
- );
-
- const { data, isLoading } = useAuditLogsQuery(params);
-
- if (!isOwner) {
- return null;
- }
-
- const handleEventChange = (value: string | null) => {
- setEventFilter(value);
- resetCursor();
- };
-
- return (
- <>
-
-
- {t("Audit log")} - {getAppName()}
-
-
-
-
-
-
- ({
- group: t(group.group),
- items: group.items.map((item) => ({
- value: item.value,
- label: t(item.label),
- })),
- }))}
- value={eventFilter}
- onChange={handleEventChange}
- clearable
- searchable
- w={220}
- size="sm"
- />
-
- {
- if (!opened) resetRetentionForm();
- setSettingsOpen(opened);
- }}
- >
-
-
- setSettingsOpen((o) => !o)}>
-
-
-
-
-
-
- {t("Retention")}
-
-
- {t("Logs older than this period are automatically deleted.")}
-
-
- setRetentionAmount(val)}
- min={1}
- hideControls
- size="sm"
- w={60}
- />
- {
- if (value === "days" || value === "months" || value === "years") {
- setRetentionUnit(value);
- }
- }}
- size="sm"
- style={{ flex: 1 }}
- comboboxProps={{ withinPortal: false }}
- />
-
-
- {
- resetRetentionForm();
- setSettingsOpen(false);
- }}
- >
- {t("Cancel")}
-
- {
- const num = typeof retentionAmount === "number" ? retentionAmount : 1;
- const clamped = Math.max(1, num);
- setRetentionAmount(clamped);
- const days = retentionToDays(clamped, retentionUnit);
- if (days !== currentDays) {
- updateRetention.mutate({ auditRetentionDays: days });
- }
- setSettingsOpen(false);
- }}
- loading={updateRetention.isPending}
- >
- {t("Save")}
-
-
-
-
-
-
-
-
-
-
- {data?.items && data.items.length > 0 && (
- goNext(data?.meta?.nextCursor)}
- onPrev={goPrev}
- />
- )}
- >
- );
-}
diff --git a/apps/client/src/ee/audit/queries/audit-query.ts b/apps/client/src/ee/audit/queries/audit-query.ts
deleted file mode 100644
index 51888495..00000000
--- a/apps/client/src/ee/audit/queries/audit-query.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import {
- keepPreviousData,
- useMutation,
- useQuery,
- useQueryClient,
- UseQueryResult,
-} from "@tanstack/react-query";
-import {
- getAuditLogs,
- getAuditRetention,
- updateAuditRetention,
-} from "@/ee/audit/services/audit-service";
-import { IAuditLog, IAuditLogParams } from "@/ee/audit/types/audit.types";
-import { IPagination } from "@/lib/types";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-
-export function useAuditLogsQuery(
- params?: IAuditLogParams,
-): UseQueryResult, Error> {
- return useQuery({
- queryKey: ["audit-logs", params],
- queryFn: () => getAuditLogs(params),
- placeholderData: keepPreviousData,
- });
-}
-
-export function useAuditRetentionQuery() {
- return useQuery({
- queryKey: ["audit-retention"],
- queryFn: () => getAuditRetention(),
- });
-}
-
-export function useUpdateAuditRetentionMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data: { auditRetentionDays: number }) =>
- updateAuditRetention(data),
- onSuccess: () => {
- notifications.show({ message: t("Audit retention updated") });
- queryClient.invalidateQueries({ queryKey: ["audit-retention"] });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({ message: errorMessage, color: "red" });
- },
- });
-}
diff --git a/apps/client/src/ee/audit/services/audit-service.ts b/apps/client/src/ee/audit/services/audit-service.ts
deleted file mode 100644
index f0eb4938..00000000
--- a/apps/client/src/ee/audit/services/audit-service.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import api from "@/lib/api-client";
-import { IAuditLog, IAuditLogParams } from "@/ee/audit/types/audit.types";
-import { IPagination } from "@/lib/types";
-
-export async function getAuditLogs(
- params?: IAuditLogParams,
-): Promise> {
- const req = await api.post("/audit", { ...params });
- return req.data;
-}
-
-export async function getAuditRetention(): Promise<{ retentionDays: number }> {
- const req = await api.post("/audit/retention");
- return req.data;
-}
-
-export async function updateAuditRetention(data: {
- auditRetentionDays: number;
-}): Promise<{ retentionDays: number }> {
- const req = await api.post("/audit/retention/update", data);
- return req.data;
-}
diff --git a/apps/client/src/ee/audit/types/audit.types.ts b/apps/client/src/ee/audit/types/audit.types.ts
deleted file mode 100644
index 43813f97..00000000
--- a/apps/client/src/ee/audit/types/audit.types.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-export type IAuditLog = {
- id: string;
- workspaceId: string;
- actorId?: string;
- actorType: string;
- event: string;
- resourceType: string;
- resourceId?: string;
- spaceId?: string;
- changes?: {
- before?: Record;
- after?: Record;
- };
- metadata?: Record;
- ipAddress?: string;
- createdAt: string;
- actor?: {
- id: string;
- name: string;
- email: string;
- avatarUrl?: string;
- };
- resource?: {
- id: string;
- name: string;
- slug?: string;
- slugId?: string;
- };
-};
-
-export type IAuditLogParams = {
- event?: string;
- resourceType?: string;
- actorId?: string;
- spaceId?: string;
- startDate?: string;
- endDate?: string;
- cursor?: string;
- limit?: number;
-};
diff --git a/apps/client/src/ee/billing/components/billing-details.tsx b/apps/client/src/ee/billing/components/billing-details.tsx
deleted file mode 100644
index e4a5e5f8..00000000
--- a/apps/client/src/ee/billing/components/billing-details.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-import {
- useBillingPlans,
- useBillingQuery,
-} from "@/ee/billing/queries/billing-query.ts";
-import { Group, Text, SimpleGrid, Paper } from "@mantine/core";
-import classes from "./billing.module.css";
-import { formatInterval } from "@/ee/billing/utils.ts";
-import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
-
-export default function BillingDetails() {
- const { data: billing } = useBillingQuery();
- const { data: plans } = useBillingPlans();
- const locale = useDateFnsLocale();
-
- if (!billing || !plans) {
- return null;
- }
-
- return (
-
-
-
-
-
-
- Plan
-
-
- {plans.find(
- (plan) => plan.productId === billing.stripeProductId,
- )?.name ||
- billing.planName ||
- "Standard"}
-
-
-
-
-
-
-
-
-
- Billing Period
-
-
- {formatInterval(billing.interval)}
-
-
-
-
-
-
-
-
-
- {billing.cancelAtPeriodEnd
- ? "Cancellation date"
- : "Renewal date"}
-
-
- {formatLocalized(
- billing.periodEndAt,
- "dd MMM, yyyy",
- "PP",
- locale,
- )}
-
-
-
-
-
-
-
-
-
-
-
- Seat count
-
-
- {billing.quantity}
-
-
-
-
-
-
-
-
-
- Cost
-
- {billing.billingScheme === "tiered" && (
- <>
-
- ${billing.amount / 100} {billing.currency.toUpperCase()} /{" "}
- {billing.interval}
-
-
- per {billing.interval}
-
- >
- )}
-
- {billing.billingScheme !== "tiered" && (
- <>
-
- {(billing.amount / 100) * billing.quantity}{" "}
- {billing.currency.toUpperCase()} / {billing.interval}
-
-
- ${billing.amount / 100} /user/{billing.interval}
-
- >
- )}
-
-
-
-
- {billing.billingScheme === "tiered" && billing.tieredUpTo && (
-
-
-
-
- Current Tier
-
-
- For {billing.tieredUpTo} users
-
- {/*billing.tieredFlatAmount && (
-
-
- )*/}
-
-
-
- )}
-
-
- );
-}
diff --git a/apps/client/src/ee/billing/components/billing-incomplete.tsx b/apps/client/src/ee/billing/components/billing-incomplete.tsx
deleted file mode 100644
index d2e6b42f..00000000
--- a/apps/client/src/ee/billing/components/billing-incomplete.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Alert } from "@mantine/core";
-import React from "react";
-
-export default function BillingIncomplete() {
- return (
- <>
-
- Your subscription is in an incomplete state. Please refresh this page if
- you recently made your payment.
-
- >
- );
-}
diff --git a/apps/client/src/ee/billing/components/billing-plans.tsx b/apps/client/src/ee/billing/components/billing-plans.tsx
deleted file mode 100644
index b57643b2..00000000
--- a/apps/client/src/ee/billing/components/billing-plans.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import {
- Button,
- Card,
- List,
- ThemeIcon,
- Title,
- Text,
- Group,
- Select,
- Container,
- Stack,
- Badge,
- Flex,
- Switch,
- Alert,
-} from "@mantine/core";
-import { useState } from "react";
-import { IconCheck, IconInfoCircle } from "@tabler/icons-react";
-import { getCheckoutLink } from "@/ee/billing/services/billing-service.ts";
-import { useBillingPlans } from "@/ee/billing/queries/billing-query.ts";
-import { useAtomValue } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
-
-export default function BillingPlans() {
- const { data: plans } = useBillingPlans();
- const workspace = useAtomValue(workspaceAtom);
- const [isAnnual, setIsAnnual] = useState(true);
- const [selectedTierValue, setSelectedTierValue] = useState(
- null,
- );
-
- const handleCheckout = async (priceId: string) => {
- try {
- const checkoutLink = await getCheckoutLink({
- priceId: priceId,
- });
- window.location.href = checkoutLink.url;
- } catch (err) {
- console.error("Failed to get checkout link", err);
- }
- };
-
- // TODO: remove by July 30.
- // Check if workspace was created between June 28 and July 14, 2025
- const showTieredPricingNotice = (() => {
- if (!workspace?.createdAt) return false;
- const createdDate = new Date(workspace.createdAt);
- const startDate = new Date('2025-06-20');
- const endDate = new Date('2025-07-14');
- return createdDate >= startDate && createdDate <= endDate;
- })();
-
- if (!plans || plans.length === 0) {
- return null;
- }
-
- // Check if any plan is tiered
- const hasTieredPlans = plans.some(plan => plan.billingScheme === 'tiered' && plan.pricingTiers?.length > 0);
- const firstTieredPlan = plans.find(plan => plan.billingScheme === 'tiered' && plan.pricingTiers?.length > 0);
-
- // Set initial tier value if not set and we have tiered plans
- if (hasTieredPlans && !selectedTierValue && firstTieredPlan) {
- setSelectedTierValue(firstTieredPlan.pricingTiers[0].upTo.toString());
- return null;
- }
-
- // For tiered plans, ensure we have a selected tier
- if (hasTieredPlans && !selectedTierValue) {
- return null;
- }
-
- const selectData = firstTieredPlan?.pricingTiers
- ?.filter((tier) => !tier.custom)
- .map((tier, index) => {
- const prevMaxUsers =
- index > 0 ? firstTieredPlan.pricingTiers[index - 1].upTo : 0;
- return {
- value: tier.upTo.toString(),
- label: `${prevMaxUsers + 1}-${tier.upTo} users`,
- };
- }) || [];
-
- return (
-
- {/* Tiered pricing notice for eligible workspaces */}
- {showTieredPricingNotice && !hasTieredPlans && (
- }
- title="Want the old tiered pricing?"
- color="blue"
- mb="lg"
- >
- Contact support to switch back to our tiered pricing model.
-
- )}
-
- {/* Controls Section */}
-
- {/* Team Size and Billing Controls */}
-
- {hasTieredPlans && (
-
- )}
-
-
-
- Monthly
- setIsAnnual(event.target.checked)}
- size="sm"
- />
-
- Annually
-
- 15% OFF
-
-
-
-
-
-
-
- {/* Plans Grid */}
-
- {plans.map((plan, index) => {
- let price;
- let displayPrice;
- const priceId = isAnnual ? plan.yearlyId : plan.monthlyId;
-
- if (plan.billingScheme === 'tiered' && plan.pricingTiers?.length > 0) {
- // Tiered billing logic
- const planSelectedTier =
- plan.pricingTiers.find(
- (tier) => tier.upTo.toString() === selectedTierValue,
- ) || plan.pricingTiers[0];
-
- price = isAnnual
- ? planSelectedTier.yearly
- : planSelectedTier.monthly;
- displayPrice = isAnnual ? (price / 12).toFixed(0) : price;
- } else {
- // Per-unit billing logic
- const monthlyPrice = parseFloat(plan.price?.monthly || '0');
- const yearlyPrice = parseFloat(plan.price?.yearly || '0');
- price = isAnnual ? yearlyPrice : monthlyPrice;
- displayPrice = isAnnual ? (yearlyPrice / 12).toFixed(0) : monthlyPrice;
- }
-
- return (
-
-
- {/* Plan Header */}
-
-
- {plan.name}
-
- {plan.description && (
-
- {plan.description}
-
- )}
-
-
- {/* Pricing */}
-
-
-
- ${displayPrice}
-
-
- {plan.billingScheme === 'per_unit'
- ? `per user/month`
- : `per month`}
-
-
-
- {isAnnual ? "Billed annually" : "Billed monthly"}
-
- {plan.billingScheme === 'tiered' && plan.pricingTiers && (
-
- For {plan.pricingTiers.find(tier => tier.upTo.toString() === selectedTierValue)?.upTo || plan.pricingTiers[0].upTo} users
-
- )}
-
-
- {/* CTA Button */}
- handleCheckout(priceId)} fullWidth>
- Subscribe
-
-
- {/* Features */}
-
-
-
- }
- >
- {plan.features.map((feature, featureIndex) => (
- {feature}
- ))}
-
-
-
- );
- })}
-
-
- );
-}
diff --git a/apps/client/src/ee/billing/components/billing-trial.tsx b/apps/client/src/ee/billing/components/billing-trial.tsx
deleted file mode 100644
index 38acc17c..00000000
--- a/apps/client/src/ee/billing/components/billing-trial.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Alert } from "@mantine/core";
-import { useBillingQuery } from "@/ee/billing/queries/billing-query.ts";
-import useTrial from "@/ee/hooks/use-trial.tsx";
-import { getBillingTrialDays } from '@/lib/config.ts';
-
-export default function BillingTrial() {
- const { data: billing, isLoading } = useBillingQuery();
- const { trialDaysLeft } = useTrial();
-
- if (isLoading) {
- return null;
- }
-
- return (
- <>
- {trialDaysLeft > 0 && !billing && (
-
- You have {trialDaysLeft} {trialDaysLeft === 1 ? "day" : "days"} left
- in your {getBillingTrialDays()}-day free trial. Please subscribe to a paid plan before your trial
- ends.
-
- )}
-
- {trialDaysLeft === 0 && (
-
- Your {getBillingTrialDays()}-day free trial has come to an end. Please subscribe to a paid plan to
- continue using this service.
-
- )}
- >
- );
-}
diff --git a/apps/client/src/ee/billing/components/billing.module.css b/apps/client/src/ee/billing/components/billing.module.css
deleted file mode 100644
index 50398f33..00000000
--- a/apps/client/src/ee/billing/components/billing.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.root {
- padding-top: var(--mantine-spacing-xs);
- padding-bottom: var(--mantine-spacing-xs);
-}
-
-.label {
- font-family:
- Greycliff CF,
- var(--mantine-font-family);
-}
\ No newline at end of file
diff --git a/apps/client/src/ee/billing/components/manage-billing.tsx b/apps/client/src/ee/billing/components/manage-billing.tsx
deleted file mode 100644
index 2424d1e6..00000000
--- a/apps/client/src/ee/billing/components/manage-billing.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Button, Group, Text } from "@mantine/core";
-import React from "react";
-import { getBillingPortalLink } from "@/ee/billing/services/billing-service.ts";
-
-export default function ManageBilling() {
- const handleBillingPortal = async () => {
- try {
- const portalLink = await getBillingPortalLink();
- window.location.href = portalLink.url;
- } catch (err) {
- console.error("Failed to get billing portal link", err);
- }
- };
-
- return (
- <>
-
-
-
- Manage subscription
-
-
- Manage your your subscription, invoices, update payment details, and
- more.
-
-
-
-
- Manage
-
-
- >
- );
-}
diff --git a/apps/client/src/ee/billing/pages/billing.tsx b/apps/client/src/ee/billing/pages/billing.tsx
deleted file mode 100644
index a389a1e5..00000000
--- a/apps/client/src/ee/billing/pages/billing.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Helmet } from "react-helmet-async";
-import { getAppName } from "@/lib/config.ts";
-import SettingsTitle from "@/components/settings/settings-title.tsx";
-import BillingPlans from "@/ee/billing/components/billing-plans.tsx";
-import BillingTrial from "@/ee/billing/components/billing-trial.tsx";
-import ManageBilling from "@/ee/billing/components/manage-billing.tsx";
-import { Divider } from "@mantine/core";
-import React from "react";
-import BillingDetails from "@/ee/billing/components/billing-details.tsx";
-import { useBillingQuery } from "@/ee/billing/queries/billing-query.ts";
-import useUserRole from "@/hooks/use-user-role.tsx";
-
-export default function Billing() {
- const { data: billing, isError: isBillingError } = useBillingQuery();
- const { isAdmin } = useUserRole();
-
- if (!isAdmin) {
- return null;
- }
-
- return (
- <>
-
- Billing - {getAppName()}
-
-
-
-
-
-
- {isBillingError && }
-
- {billing && (
- <>
-
-
- >
- )}
- >
- );
-}
diff --git a/apps/client/src/ee/billing/queries/billing-query.ts b/apps/client/src/ee/billing/queries/billing-query.ts
deleted file mode 100644
index 261102f5..00000000
--- a/apps/client/src/ee/billing/queries/billing-query.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useQuery, UseQueryResult } from "@tanstack/react-query";
-import {
- getBilling,
- getBillingPlans,
-} from "@/ee/billing/services/billing-service.ts";
-import { IBilling, IBillingPlan } from "@/ee/billing/types/billing.types.ts";
-
-export function useBillingQuery(): UseQueryResult {
- return useQuery({
- queryKey: ["billing"],
- queryFn: () => getBilling(),
- });
-}
-
-export function useBillingPlans(): UseQueryResult {
- return useQuery({
- queryKey: ["billing-plans"],
- queryFn: () => getBillingPlans(),
- });
-}
diff --git a/apps/client/src/ee/billing/services/billing-service.ts b/apps/client/src/ee/billing/services/billing-service.ts
deleted file mode 100644
index c76f4ea5..00000000
--- a/apps/client/src/ee/billing/services/billing-service.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import api from "@/lib/api-client.ts";
-import {
- IBilling,
- IBillingPlan,
- IBillingPortal,
- ICheckoutLink,
-} from "@/ee/billing/types/billing.types.ts";
-
-export async function getBilling(): Promise {
- const req = await api.post("/billing/info");
- return req.data;
-}
-
-export async function getBillingPlans(): Promise {
- const req = await api.post("/billing/plans");
- return req.data;
-}
-
-export async function getCheckoutLink(data: {
- priceId: string;
-}): Promise {
- const req = await api.post("/billing/checkout", data);
- return req.data;
-}
-
-export async function getBillingPortalLink(): Promise {
- const req = await api.post("/billing/portal");
- return req.data;
-}
diff --git a/apps/client/src/ee/billing/types/billing.types.ts b/apps/client/src/ee/billing/types/billing.types.ts
deleted file mode 100644
index 58225519..00000000
--- a/apps/client/src/ee/billing/types/billing.types.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-export enum BillingPlan {
- STANDARD = "standard",
- BUSINESS = "business",
-}
-
-export interface IBilling {
- id: string;
- stripeSubscriptionId: string;
- stripeCustomerId: string;
- status: string;
- quantity: number;
- amount: number;
- interval: string;
- currency: string;
- metadata: Record;
- stripePriceId: string;
- stripeItemId: string;
- stripeProductId: string;
- periodStartAt: Date;
- periodEndAt: Date;
- cancelAtPeriodEnd: boolean;
- cancelAt: Date;
- canceledAt: Date;
- workspaceId: string;
- createdAt: Date;
- updatedAt: Date;
- deletedAt: Date;
- billingScheme: string | null;
- tieredUpTo: string | null;
- tieredFlatAmount: number | null;
- tieredUnitAmount: number | null;
- planName: string | null;
-}
-
-export interface ICheckoutLink {
- url: string;
-}
-
-export interface IBillingPortal {
- url: string;
-}
-
-export interface IBillingPlan {
- name: string;
- description: string;
- productId: string;
- monthlyId: string;
- yearlyId: string;
- currency: string;
- price?: {
- monthly: string;
- yearly: string;
- };
- features: string[];
- billingScheme: string | null;
- pricingTiers?: PricingTier[];
-}
-
-interface PricingTier {
- upTo: number;
- monthly?: number;
- yearly?: number;
- custom?: boolean;
-}
\ No newline at end of file
diff --git a/apps/client/src/ee/billing/utils.ts b/apps/client/src/ee/billing/utils.ts
deleted file mode 100644
index bf41dff7..00000000
--- a/apps/client/src/ee/billing/utils.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { differenceInCalendarDays } from "date-fns";
-
-export function formatInterval(interval: string): string {
- if (interval === "month") {
- return "monthly";
- }
- if (interval === "year") {
- return "yearly";
- }
-}
-
-export function getTrialDaysLeft(trialEndAt: Date) {
- if (!trialEndAt) return null;
-
- const daysLeft = differenceInCalendarDays(trialEndAt, new Date());
- return daysLeft > 0 ? daysLeft : 0;
-}
diff --git a/apps/client/src/ee/cloud/query/cloud-query.ts b/apps/client/src/ee/cloud/query/cloud-query.ts
deleted file mode 100644
index 367d89e3..00000000
--- a/apps/client/src/ee/cloud/query/cloud-query.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { useQuery, UseQueryResult } from "@tanstack/react-query";
-import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
-import { getJoinedWorkspaces } from "@/ee/cloud/service/cloud-service.ts";
-
-export function useJoinedWorkspacesQuery(): UseQueryResult<
- Partial,
- Error
-> {
- return useQuery({
- queryKey: ["joined-workspaces"],
- queryFn: () => getJoinedWorkspaces(),
- });
-}
diff --git a/apps/client/src/ee/cloud/service/cloud-service.ts b/apps/client/src/ee/cloud/service/cloud-service.ts
deleted file mode 100644
index 5411b802..00000000
--- a/apps/client/src/ee/cloud/service/cloud-service.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
-import api from "@/lib/api-client.ts";
-
-export async function getJoinedWorkspaces(): Promise> {
- const req = await api.post>("/workspace/joined");
- return req.data;
-}
-
-export async function findWorkspacesByEmail(email: string): Promise {
- await api.post("/workspace/find-by-email", { email });
-}
-
-export async function verifyEmail(data: { token: string }): Promise {
- await api.post("/workspace/verify-email", data);
-}
-
-export async function resendVerificationEmail(data: { email: string; sig: string }): Promise {
- await api.post("/workspace/resend-verification", data);
-}
diff --git a/apps/client/src/ee/comment/components/resolve-comment.tsx b/apps/client/src/ee/comment/components/resolve-comment.tsx
deleted file mode 100644
index 4e22fb71..00000000
--- a/apps/client/src/ee/comment/components/resolve-comment.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { ActionIcon, Tooltip } from "@mantine/core";
-import { IconCircleCheck, IconCircleCheckFilled } from "@tabler/icons-react";
-import { useResolveCommentMutation } from "@/ee/comment/queries/comment-query";
-import { useTranslation } from "react-i18next";
-import { Editor } from "@tiptap/react";
-
-interface ResolveCommentProps {
- editor: Editor;
- commentId: string;
- pageId: string;
- resolvedAt?: Date;
-}
-
-function ResolveComment({
- editor,
- commentId,
- pageId,
- resolvedAt,
-}: ResolveCommentProps) {
- const { t } = useTranslation();
- const resolveCommentMutation = useResolveCommentMutation();
-
- const isResolved = resolvedAt != null;
- const iconColor = isResolved ? "green" : "gray";
-
- const handleResolveToggle = async () => {
- try {
- await resolveCommentMutation.mutateAsync({
- commentId,
- pageId,
- resolved: !isResolved,
- });
-
- if (editor) {
- editor.commands.setCommentResolved(commentId, !isResolved);
- }
-
- //
- } catch (error) {
- console.error("Failed to toggle resolved state:", error);
- }
- };
-
- return (
-
-
- {isResolved ? (
-
- ) : (
-
- )}
-
-
- );
-}
-
-export default ResolveComment;
diff --git a/apps/client/src/ee/comment/queries/comment-query.ts b/apps/client/src/ee/comment/queries/comment-query.ts
deleted file mode 100644
index a7a5788a..00000000
--- a/apps/client/src/ee/comment/queries/comment-query.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import {
- useMutation,
- useQueryClient,
- InfiniteData,
-} from "@tanstack/react-query";
-import { resolveComment } from "@/features/comment/services/comment-service";
-import {
- IComment,
- IResolveComment,
-} from "@/features/comment/types/comment.types";
-import { notifications } from "@mantine/notifications";
-import { IPagination } from "@/lib/types.ts";
-import { useTranslation } from "react-i18next";
-import { RQ_KEY } from "@/features/comment/queries/comment-query";
-
-function updateCommentInCache(
- cache: InfiniteData>,
- commentId: string,
- updater: (comment: IComment) => IComment,
-): InfiniteData> {
- return {
- ...cache,
- pages: cache.pages.map((page) => ({
- ...page,
- items: page.items.map((comment) =>
- comment.id === commentId ? updater(comment) : comment,
- ),
- })),
- };
-}
-
-export function useResolveCommentMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data: IResolveComment) => resolveComment(data),
- onMutate: async (variables) => {
- await queryClient.cancelQueries({ queryKey: RQ_KEY(variables.pageId) });
- const previousCache = queryClient.getQueryData(RQ_KEY(variables.pageId));
-
- const cache = previousCache as InfiniteData> | undefined;
- if (cache) {
- queryClient.setQueryData(
- RQ_KEY(variables.pageId),
- updateCommentInCache(cache, variables.commentId, (comment) => ({
- ...comment,
- resolvedAt: variables.resolved ? new Date() : null,
- resolvedById: variables.resolved ? "optimistic" : null,
- resolvedBy: variables.resolved
- ? ({ id: "optimistic", name: "", avatarUrl: null } as IComment["resolvedBy"])
- : null,
- })),
- );
- }
-
- return { previousCache };
- },
- onError: (_err, variables, context) => {
- if (context?.previousCache) {
- queryClient.setQueryData(RQ_KEY(variables.pageId), context.previousCache);
- }
- notifications.show({
- message: t("Failed to resolve comment"),
- color: "red",
- });
- },
- onSuccess: (data: IComment, variables) => {
- const cache = queryClient.getQueryData(
- RQ_KEY(data.pageId),
- ) as InfiniteData> | undefined;
-
- if (cache) {
- queryClient.setQueryData(
- RQ_KEY(data.pageId),
- updateCommentInCache(cache, variables.commentId, (comment) => ({
- ...comment,
- resolvedAt: data.resolvedAt,
- resolvedById: data.resolvedById,
- resolvedBy: data.resolvedBy,
- })),
- );
- }
-
- notifications.show({
- message: variables.resolved
- ? t("Comment resolved successfully")
- : t("Comment re-opened successfully"),
- });
- },
- });
-}
\ No newline at end of file
diff --git a/apps/client/src/ee/components/cloud-login-form.tsx b/apps/client/src/ee/components/cloud-login-form.tsx
deleted file mode 100644
index 2357f9bb..00000000
--- a/apps/client/src/ee/components/cloud-login-form.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import { z } from "zod/v4";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import {
- Container,
- Title,
- TextInput,
- Button,
- Box,
- Text,
- Anchor,
- Divider,
-} from "@mantine/core";
-import classes from "../../features/auth/components/auth.module.css";
-import { getCheckHostname } from "@/features/workspace/services/workspace-service.ts";
-import { useState } from "react";
-import { getSubdomainHost } from "@/lib/config.ts";
-import { Link } from "react-router-dom";
-import APP_ROUTE from "@/lib/app-route.ts";
-import { useTranslation } from "react-i18next";
-import JoinedWorkspaces from "@/ee/components/joined-workspaces.tsx";
-import { useJoinedWorkspacesQuery } from "@/ee/cloud/query/cloud-query.ts";
-import { findWorkspacesByEmail } from "@/ee/cloud/service/cloud-service.ts";
-import { AuthLayout } from "@/features/auth/components/auth-layout.tsx";
-
-const formSchema = z.object({
- hostname: z.string().min(1, { message: "subdomain is required" }),
-});
-
-const findWorkspaceSchema = z.object({
- email: z.string().email({ message: "Please enter a valid email" }),
-});
-
-export function CloudLoginForm() {
- const { t } = useTranslation();
- const [isLoading, setIsLoading] = useState(false);
- const [isFindLoading, setIsFindLoading] = useState(false);
- const [findEmailSent, setFindEmailSent] = useState(false);
- const { data: joinedWorkspaces } = useJoinedWorkspacesQuery();
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- hostname: "",
- },
- });
-
- const findForm = useForm({
- validate: zod4Resolver(findWorkspaceSchema),
- initialValues: {
- email: "",
- },
- });
-
- async function onSubmit(data: { hostname: string }) {
- setIsLoading(true);
-
- try {
- const checkHostname = await getCheckHostname(data.hostname);
- window.location.href = checkHostname.hostname;
- } catch (err) {
- if (err?.status === 404) {
- form.setFieldError("hostname", "We could not find this workspace");
- } else {
- form.setFieldError("hostname", "An error occurred");
- }
- }
-
- setIsLoading(false);
- }
-
- async function onFindSubmit(data: { email: string }) {
- setIsFindLoading(true);
-
- try {
- await findWorkspacesByEmail(data.email);
- setFindEmailSent(true);
- } catch {
- findForm.setFieldError("email", "An error occurred. Please try again.");
- }
-
- setIsFindLoading(false);
- }
-
- return (
-
-
-
-
- {t("Login")}
-
-
-
-
- {joinedWorkspaces?.length > 0 && (
-
- )}
-
-
-
-
-
- {findEmailSent ? (
-
- {t("We've sent you an email with your associated workspaces.")}
-
- ) : (
-
- )}
-
-
-
-
- {t("Don't have a workspace?")}{" "}
-
- {t("Create new workspace")}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/components/joined-workspaces.module.css b/apps/client/src/ee/components/joined-workspaces.module.css
deleted file mode 100644
index 74871e66..00000000
--- a/apps/client/src/ee/components/joined-workspaces.module.css
+++ /dev/null
@@ -1,13 +0,0 @@
-.workspace {
- display: block;
- width: 100%;
- padding: var(--mantine-spacing-xs);
- margin-bottom: var(--mantine-spacing-xs);
- color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
- border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
- border-radius: var(--mantine-spacing-xs);
-
- @mixin hover {
- background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
- }
-}
\ No newline at end of file
diff --git a/apps/client/src/ee/components/joined-workspaces.tsx b/apps/client/src/ee/components/joined-workspaces.tsx
deleted file mode 100644
index 4029a501..00000000
--- a/apps/client/src/ee/components/joined-workspaces.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Group, Text, UnstyledButton } from "@mantine/core";
-import { useJoinedWorkspacesQuery } from "../cloud/query/cloud-query";
-import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
-import classes from "./joined-workspaces.module.css";
-import { IconChevronRight } from "@tabler/icons-react";
-import { getHostnameUrl } from "@/ee/utils.ts";
-import { Link } from "react-router-dom";
-import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
-
-export default function JoinedWorkspaces() {
- const { data, isLoading } = useJoinedWorkspacesQuery();
- if (isLoading || !data || data?.length === 0) {
- return null;
- }
-
- return (
- <>
- {data
- .sort((a, b) => a.name.localeCompare(b.name))
- .map((workspace: Partial, index) => (
-
-
-
-
-
-
- {workspace?.name}
-
-
-
- {getHostnameUrl(workspace?.hostname)?.split("//")[1]}
-
-
-
-
-
-
- ))}
- >
- );
-}
diff --git a/apps/client/src/ee/components/ldap-login-modal.tsx b/apps/client/src/ee/components/ldap-login-modal.tsx
deleted file mode 100644
index 77a9d356..00000000
--- a/apps/client/src/ee/components/ldap-login-modal.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import React, { useState } from "react";
-import { Modal, TextInput, PasswordInput, Button, Stack } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { z } from "zod/v4";
-import { notifications } from "@mantine/notifications";
-import { useNavigate } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-import { IAuthProvider } from "@/ee/security/types/security.types";
-import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
-import { ldapLogin } from "@/ee/security/services/ldap-auth-service";
-
-const formSchema = z.object({
- username: z.string().min(1, { message: "Username is required" }),
- password: z.string().min(1, { message: "Password is required" }),
-});
-
-interface LdapLoginModalProps {
- opened: boolean;
- onClose: () => void;
- provider: IAuthProvider;
- workspaceId: string;
-}
-
-export function LdapLoginModal({
- opened,
- onClose,
- provider,
- workspaceId,
-}: LdapLoginModalProps) {
- const { t } = useTranslation();
- const navigate = useNavigate();
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- username: "",
- password: "",
- },
- });
-
- const handleSubmit = async (values: {
- username: string;
- password: string;
- }) => {
- setIsLoading(true);
- setError(null);
-
- try {
- const response = await ldapLogin({
- username: values.username,
- password: values.password,
- providerId: provider.id,
- workspaceId,
- });
-
- // Handle MFA like the regular login
- if (response?.userHasMfa) {
- onClose();
- navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + window.location.search);
- } else if (response?.requiresMfaSetup) {
- onClose();
- navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + window.location.search);
- } else {
- onClose();
- navigate(getPostLoginRedirect());
- }
- } catch (err: any) {
- setIsLoading(false);
- const errorMessage =
- err.response?.data?.message || "Authentication failed";
- setError(errorMessage);
-
- notifications.show({
- message: errorMessage,
- color: "red",
- });
- }
- };
-
- const handleClose = () => {
- form.reset();
- setError(null);
- onClose();
- };
-
- return (
-
-
-
- );
-}
diff --git a/apps/client/src/ee/components/manage-hostname.tsx b/apps/client/src/ee/components/manage-hostname.tsx
deleted file mode 100644
index 50090206..00000000
--- a/apps/client/src/ee/components/manage-hostname.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { Button, Group, Text, Modal, TextInput } from "@mantine/core";
-import { z } from "zod/v4";
-import { useState } from "react";
-import { useDisclosure } from "@mantine/hooks";
-import * as React from "react";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-import { getSubdomainHost } from "@/lib/config.ts";
-import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
-import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
-import { getHostnameUrl } from "@/ee/utils.ts";
-import { useAtom } from "jotai";
-import {
- currentUserAtom,
- workspaceAtom,
-} from "@/features/user/atoms/current-user-atom.ts";
-import useUserRole from "@/hooks/use-user-role.tsx";
-import { RESET } from "jotai/utils";
-
-export default function ManageHostname() {
- const { t } = useTranslation();
- const [opened, { open, close }] = useDisclosure(false);
- const [workspace] = useAtom(workspaceAtom);
- const { isAdmin } = useUserRole();
-
- return (
-
-
- {t("Hostname")}
-
- {workspace?.hostname}.{getSubdomainHost()}
-
-
-
- {isAdmin && (
-
- {t("Change hostname")}
-
- )}
-
-
-
-
-
- );
-}
-
-const formSchema = z.object({
- hostname: z.string().min(4),
-});
-
-type FormValues = z.infer;
-
-interface ChangeHostnameFormProps {
- onClose?: () => void;
-}
-function ChangeHostnameForm({ onClose }: ChangeHostnameFormProps) {
- const { t } = useTranslation();
- const [isLoading, setIsLoading] = useState(false);
- const [currentUser, setCurrentUser] = useAtom(currentUserAtom);
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- hostname: currentUser?.workspace?.hostname,
- },
- });
-
- async function handleSubmit(data: Partial) {
- setIsLoading(true);
-
- if (data.hostname === currentUser?.workspace?.hostname) {
- onClose();
- return;
- }
-
- try {
- await updateWorkspace({
- hostname: data.hostname,
- });
- setCurrentUser(RESET);
- window.location.href = getHostnameUrl(data.hostname.toLowerCase());
- } catch (err) {
- notifications.show({
- message: err?.response?.data?.message,
- color: "red",
- });
- }
- setIsLoading(false);
- }
-
- return (
-
- );
-}
diff --git a/apps/client/src/ee/components/posthog-user.tsx b/apps/client/src/ee/components/posthog-user.tsx
deleted file mode 100644
index 893b0de9..00000000
--- a/apps/client/src/ee/components/posthog-user.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { usePostHog } from "posthog-js/react";
-import { useEffect } from "react";
-import { useAtom } from "jotai";
-import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
-
-export function PosthogUser() {
- const posthog = usePostHog();
- const [currentUser] = useAtom(currentUserAtom);
-
- useEffect(() => {
- if (currentUser) {
- const user = currentUser?.user;
- const workspace = currentUser?.workspace;
- if (!user || !workspace) return;
-
- posthog?.identify(user.id, {
- name: user.name,
- email: user.email,
- workspaceId: user.workspaceId,
- workspaceHostname: workspace.hostname,
- lastActiveAt: new Date().toISOString(),
- createdAt: user.createdAt,
- source: "docmost-app",
- });
- posthog?.group("workspace", workspace.id, {
- name: workspace.name,
- hostname: workspace.hostname,
- plan: workspace?.plan,
- status: workspace.status,
- isOnTrial: !!workspace.trialEndAt,
- hasStripeCustomerId: !!workspace.stripeCustomerId,
- memberCount: workspace.memberCount,
- lastActiveAt: new Date().toISOString(),
- createdAt: workspace.createdAt,
- source: "docmost-app",
- });
- }
- }, [posthog, currentUser]);
-
- return null;
-}
diff --git a/apps/client/src/ee/components/sso-cloud-signup.tsx b/apps/client/src/ee/components/sso-cloud-signup.tsx
deleted file mode 100644
index c8657955..00000000
--- a/apps/client/src/ee/components/sso-cloud-signup.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Button, Divider, Stack } from "@mantine/core";
-import { getGoogleSignupUrl } from "@/ee/security/sso.utils.ts";
-import { GoogleIcon } from "@/components/icons/google-icon.tsx";
-
-export default function SsoCloudSignup() {
- const handleSsoLogin = () => {
- window.location.href = getGoogleSignupUrl();
- };
-
- return (
- <>
-
- }
- variant="default"
- fullWidth
- >
- Signup with Google
-
-
-
- >
- );
-}
diff --git a/apps/client/src/ee/components/sso-login.tsx b/apps/client/src/ee/components/sso-login.tsx
deleted file mode 100644
index 3e84f8ef..00000000
--- a/apps/client/src/ee/components/sso-login.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { useWorkspacePublicDataQuery } from "@/features/workspace/queries/workspace-query.ts";
-import { Button, Divider, Stack } from "@mantine/core";
-import { IconLock, IconServer } from "@tabler/icons-react";
-import { IAuthProvider } from "@/ee/security/types/security.types.ts";
-import { buildSsoLoginUrl } from "@/ee/security/sso.utils.ts";
-import { SSO_PROVIDER } from "@/ee/security/contants.ts";
-import { GoogleIcon } from "@/components/icons/google-icon.tsx";
-import { LdapLoginModal } from "@/ee/components/ldap-login-modal.tsx";
-import { getRedirectParam } from "@/lib/app-route.ts";
-import useCurrentUser from "@/features/user/hooks/use-current-user.ts";
-
-const SSO_AUTO_ATTEMPT_KEY = "docmost:ssoAutoAttempt";
-const SSO_AUTO_ATTEMPT_TTL_MS = 5 * 60_000;
-
-function recentAutoAttempt(): boolean {
- try {
- const raw = window.sessionStorage.getItem(SSO_AUTO_ATTEMPT_KEY);
- if (!raw) return false;
- const ts = Number(raw);
- return Number.isFinite(ts) && Date.now() - ts < SSO_AUTO_ATTEMPT_TTL_MS;
- } catch {
- return false;
- }
-}
-
-function markAutoAttempt(): void {
- try {
- window.sessionStorage.setItem(SSO_AUTO_ATTEMPT_KEY, String(Date.now()));
- } catch {
- /* sessionStorage unavailable (private mode, etc.) — best effort */
- }
-}
-
-export default function SsoLogin() {
- const { data, isLoading } = useWorkspacePublicDataQuery();
- const { data: currentUser } = useCurrentUser();
- const [ldapModalOpened, setLdapModalOpened] = useState(false);
- const [selectedLdapProvider, setSelectedLdapProvider] = useState(null);
- const autoRedirectedRef = useRef(false);
-
- const handleSsoLogin = (provider: IAuthProvider) => {
- if (provider.type === SSO_PROVIDER.LDAP) {
- // Open modal for LDAP instead of redirecting
- setSelectedLdapProvider(provider);
- setLdapModalOpened(true);
- } else {
- // Redirect for other SSO providers
- window.location.href = buildSsoLoginUrl({
- providerId: provider.id,
- type: provider.type,
- workspaceId: data.id,
- redirect: getRedirectParam() ?? undefined,
- });
- }
- };
-
- // Auto-redirect when SSO is enforced and there is exactly one non-LDAP
- // provider. The user has no other option, so skip the extra click.
- useEffect(() => {
- if (autoRedirectedRef.current) return;
- if (!data?.enforceSso) return;
- if (!data.authProviders || data.authProviders.length !== 1) return;
- const onlyProvider = data.authProviders[0];
- if (onlyProvider.type === SSO_PROVIDER.LDAP) return;
-
- // Already signed in: let useRedirectIfAuthenticated handle navigation
- // instead of racing it through the IdP.
- if (currentUser?.user) return;
-
- // Explicit logout: don't immediately bounce them back to the IdP.
- const params = new URLSearchParams(window.location.search);
- if (params.has("logout")) return;
-
- // Circuit-breaker: if we already auto-redirected within the TTL, the
- // user came back (likely from an IdP failure). Show the page so they
- // can read errors or pick a different account.
- if (recentAutoAttempt()) return;
-
- autoRedirectedRef.current = true;
- markAutoAttempt();
- window.location.href = buildSsoLoginUrl({
- providerId: onlyProvider.id,
- type: onlyProvider.type,
- workspaceId: data.id,
- redirect: getRedirectParam() ?? undefined,
- });
- }, [data, currentUser]);
-
- if (!data?.authProviders || data?.authProviders?.length === 0) {
- return null;
- }
-
- const getProviderIcon = (provider: IAuthProvider) => {
- if (provider.type === SSO_PROVIDER.GOOGLE) {
- return ;
- } else if (provider.type === SSO_PROVIDER.LDAP) {
- return ;
- } else {
- return ;
- }
- };
-
- return (
- <>
- {selectedLdapProvider && (
- {
- setLdapModalOpened(false);
- setSelectedLdapProvider(null);
- }}
- provider={selectedLdapProvider}
- workspaceId={data.id}
- />
- )}
-
- {data.authProviders.length > 0 && (
- <>
-
- {data.authProviders.map((provider) => (
-
- handleSsoLogin(provider)}
- leftSection={getProviderIcon(provider)}
- variant="default"
- fullWidth
- >
- {provider.name}
-
-
- ))}
-
-
- {!data.enforceSso && (
-
- )}
- >
- )}
- >
- );
-}
diff --git a/apps/client/src/ee/entitlement/entitlement-atom.ts b/apps/client/src/ee/entitlement/entitlement-atom.ts
deleted file mode 100644
index e6d38512..00000000
--- a/apps/client/src/ee/entitlement/entitlement-atom.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { atomWithStorage } from "jotai/utils";
-import type { Entitlements } from "./entitlement.types";
-
-export const entitlementAtom = atomWithStorage(
- "entitlements",
- null,
-);
diff --git a/apps/client/src/ee/entitlement/entitlement-service.ts b/apps/client/src/ee/entitlement/entitlement-service.ts
deleted file mode 100644
index 0bc0c9ea..00000000
--- a/apps/client/src/ee/entitlement/entitlement-service.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import api from "@/lib/api-client";
-import { Entitlements } from "./entitlement.types";
-
-export async function getEntitlements(): Promise {
- const req = await api.post("/workspace/entitlements");
- return req.data as Entitlements;
-}
diff --git a/apps/client/src/ee/entitlement/entitlement.types.ts b/apps/client/src/ee/entitlement/entitlement.types.ts
deleted file mode 100644
index 2ec3ab3b..00000000
--- a/apps/client/src/ee/entitlement/entitlement.types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export type Tier = "free" | "standard" | "business" | "enterprise";
-
-export type Entitlements = {
- cloud: boolean;
- tier: Tier;
- features: string[];
-};
diff --git a/apps/client/src/ee/entitlement/use-entitlements.ts b/apps/client/src/ee/entitlement/use-entitlements.ts
deleted file mode 100644
index d4bfeaf8..00000000
--- a/apps/client/src/ee/entitlement/use-entitlements.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { useQuery, UseQueryResult } from "@tanstack/react-query";
-import { getEntitlements } from "./entitlement-service";
-import { Entitlements } from "./entitlement.types";
-
-export function useEntitlements(): UseQueryResult {
- return useQuery({
- queryKey: ["entitlements"],
- queryFn: getEntitlements,
- staleTime: 5 * 60 * 1000,
- });
-}
diff --git a/apps/client/src/ee/features.ts b/apps/client/src/ee/features.ts
deleted file mode 100644
index cacf851f..00000000
--- a/apps/client/src/ee/features.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export const Feature = {
- SSO_CUSTOM: 'sso:custom',
- SSO_GOOGLE: 'sso:google',
- MFA: 'mfa',
- API_KEYS: 'api:keys',
- COMMENT_RESOLUTION: 'comment:resolution',
- PAGE_PERMISSIONS: 'page:permissions',
- AI: 'ai',
- CONFLUENCE_IMPORT: 'import:confluence',
- DOCX_IMPORT: 'import:docx',
- PDF_IMPORT: 'import:pdf',
- ATTACHMENT_INDEXING: 'attachment:indexing',
- SECURITY_SETTINGS: 'security:settings',
- MCP: 'mcp',
- SCIM: 'scim',
- PAGE_VERIFICATION: 'page:verification',
- AUDIT_LOGS: 'audit:logs',
- RETENTION: 'retention',
- SHARING_CONTROLS: 'sharing:controls',
- TEMPLATES: 'templates',
- VIEWER_COMMENTS: 'comment:viewer',
-} as const;
diff --git a/apps/client/src/ee/hooks/use-feature.ts b/apps/client/src/ee/hooks/use-feature.ts
deleted file mode 100644
index 5521477c..00000000
--- a/apps/client/src/ee/hooks/use-feature.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { useAtom } from "jotai";
-import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
-
-export const useHasFeature = (feature: string): boolean => {
- const [entitlements] = useAtom(entitlementAtom);
- return entitlements?.features?.includes(feature) ?? false;
-};
diff --git a/apps/client/src/ee/hooks/use-plan.tsx b/apps/client/src/ee/hooks/use-plan.tsx
deleted file mode 100644
index a9296c58..00000000
--- a/apps/client/src/ee/hooks/use-plan.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import { BillingPlan } from "@/ee/billing/types/billing.types.ts";
-
-const usePlan = () => {
- const [workspace] = useAtom(workspaceAtom);
-
- const isStandard =
- typeof workspace?.plan === "string" &&
- workspace?.plan.toLowerCase() === BillingPlan.STANDARD.toLowerCase();
-
- const isBusiness =
- typeof workspace?.plan === "string" &&
- workspace?.plan.toLowerCase() === BillingPlan.BUSINESS.toLowerCase();
-
- return { isStandard, isBusiness };
-};
-
-export default usePlan;
diff --git a/apps/client/src/ee/hooks/use-redirect-to-cloud-select.tsx b/apps/client/src/ee/hooks/use-redirect-to-cloud-select.tsx
deleted file mode 100644
index a7d78047..00000000
--- a/apps/client/src/ee/hooks/use-redirect-to-cloud-select.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useEffect } from "react";
-import { useLocation, useNavigate } from "react-router-dom";
-import { getAppUrl, getServerAppUrl, isCloud } from "@/lib/config.ts";
-import APP_ROUTE from "@/lib/app-route.ts";
-
-export const useRedirectToCloudSelect = () => {
- const navigate = useNavigate();
- const pathname = useLocation().pathname;
-
- useEffect(() => {
- const pathsToRedirect = ["/login", "/home"];
- if (isCloud() && pathsToRedirect.includes(pathname)) {
- const frontendUrl = getAppUrl();
- const serverUrl = getServerAppUrl();
- if (frontendUrl === serverUrl) {
- navigate(APP_ROUTE.AUTH.SELECT_WORKSPACE);
- }
- }
- }, [navigate]);
-};
diff --git a/apps/client/src/ee/hooks/use-trial-end-action.tsx b/apps/client/src/ee/hooks/use-trial-end-action.tsx
deleted file mode 100644
index 09248c6a..00000000
--- a/apps/client/src/ee/hooks/use-trial-end-action.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useEffect } from "react";
-import { useLocation, useNavigate } from "react-router-dom";
-import { getBillingTrialDays, isCloud } from "@/lib/config.ts";
-import APP_ROUTE from "@/lib/app-route.ts";
-import useUserRole from "@/hooks/use-user-role.tsx";
-import { notifications } from "@mantine/notifications";
-import useTrial from "@/ee/hooks/use-trial.tsx";
-
-export const useTrialEndAction = () => {
- const navigate = useNavigate();
- const pathname = useLocation().pathname;
- const { isAdmin } = useUserRole();
- const { trialDaysLeft } = useTrial();
-
- useEffect(() => {
- if (isCloud() && trialDaysLeft === 0) {
- if (!pathname.startsWith("/settings")) {
- notifications.show({
- position: "top-right",
- color: "red",
- title: `Your ${getBillingTrialDays()}-day trial has ended`,
- message:
- "Please upgrade to a paid plan or contact your workspace admin.",
- autoClose: false,
- });
-
- // only admins can access the billing page
- if (isAdmin) {
- navigate(APP_ROUTE.SETTINGS.WORKSPACE.BILLING);
- } else {
- navigate(APP_ROUTE.SETTINGS.ACCOUNT.PROFILE);
- }
- }
- }
- }, [navigate]);
-};
diff --git a/apps/client/src/ee/hooks/use-trial.tsx b/apps/client/src/ee/hooks/use-trial.tsx
deleted file mode 100644
index 2ae68af2..00000000
--- a/apps/client/src/ee/hooks/use-trial.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { useAtom } from "jotai";
-import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
-import { getTrialDaysLeft } from "@/ee/billing/utils.ts";
-import { ICurrentUser } from "@/features/user/types/user.types.ts";
-
-export const useTrial = () => {
- const [currentUser] = useAtom(currentUserAtom);
- const workspace = currentUser?.workspace;
-
- const trialDaysLeft = getTrialDaysLeft(workspace?.trialEndAt);
- const isTrial = !!workspace?.trialEndAt && trialDaysLeft !== null;
-
- return { isTrial: isTrial, trialDaysLeft: trialDaysLeft };
-};
-
-export default useTrial;
diff --git a/apps/client/src/ee/hooks/use-upgrade-label.ts b/apps/client/src/ee/hooks/use-upgrade-label.ts
deleted file mode 100644
index 22253c7b..00000000
--- a/apps/client/src/ee/hooks/use-upgrade-label.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { useAtom } from "jotai";
-import { useTranslation } from "react-i18next";
-import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
-import { isCloud } from "@/lib/config";
-
-export function useUpgradeLabel(): string {
- const { t } = useTranslation();
- const [entitlements] = useAtom(entitlementAtom);
-
- if (!isCloud()) {
- return entitlements != null && entitlements.tier !== "free"
- ? t("Upgrade your license tier.")
- : t("Available with a paid license");
- }
- return t("Upgrade your plan");
-}
diff --git a/apps/client/src/ee/licence/components/activate-license-modal.tsx b/apps/client/src/ee/licence/components/activate-license-modal.tsx
deleted file mode 100644
index 64d854bb..00000000
--- a/apps/client/src/ee/licence/components/activate-license-modal.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { z } from "zod/v4";
-import React, { useRef } from "react";
-import { Button, Divider, Group, Modal, Stack, Textarea } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { useTranslation } from "react-i18next";
-import { useActivateMutation } from "@/ee/licence/queries/license-query.ts";
-import { useDisclosure } from "@mantine/hooks";
-import { useAtom } from "jotai";
-import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
-import RemoveLicense from "@/ee/licence/components/remove-license.tsx";
-
-export default function ActivateLicense() {
- const { t } = useTranslation();
- const [opened, { open, close }] = useDisclosure(false);
- const [entitlements] = useAtom(entitlementAtom);
- const hasLicense = entitlements != null && entitlements.tier !== "free";
-
- return (
-
-
- {hasLicense ? t("Update license") : t("Add license")}
-
-
- {hasLicense && }
-
-
-
-
-
- );
-}
-
-const formSchema = z.object({
- licenseKey: z.string().min(1),
-});
-
-type FormValues = z.infer;
-
-interface ActivateLicenseFormProps {
- onClose?: () => void;
-}
-export function ActivateLicenseForm({ onClose }: ActivateLicenseFormProps) {
- const { t } = useTranslation();
- const activateLicenseMutation = useActivateMutation();
- const fileInputRef = useRef(null);
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- licenseKey: "",
- },
- });
-
- async function handleSubmit(data: { licenseKey: string }) {
- await activateLicenseMutation.mutateAsync(data.licenseKey);
- form.reset();
- onClose?.();
- }
-
- function handleFileUpload(event: React.ChangeEvent) {
- const file = event.target.files?.[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.onload = (e) => {
- const content = (e.target?.result as string)?.trim();
- if (content) {
- form.setFieldValue("licenseKey", content);
- handleSubmit({ licenseKey: content });
- }
- };
- reader.readAsText(file);
-
- if (fileInputRef.current) {
- fileInputRef.current.value = "";
- }
- }
-
- return (
-
- );
-}
diff --git a/apps/client/src/ee/licence/components/installation-details.tsx b/apps/client/src/ee/licence/components/installation-details.tsx
deleted file mode 100644
index a9d63024..00000000
--- a/apps/client/src/ee/licence/components/installation-details.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React from "react";
-import useUserRole from "@/hooks/use-user-role.tsx";
-import classes from "@/ee/billing/components/billing.module.css";
-import {
- Group,
- Paper,
- SimpleGrid,
- Text,
- TextInput,
-} from "@mantine/core";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import CopyTextButton from "@/components/common/copy.tsx";
-
-export default function InstallationDetails() {
- const { isAdmin } = useUserRole();
- const [workspace] = useAtom(workspaceAtom);
-
- if (!isAdmin) {
- return null;
- }
-
- return (
- <>
-
-
-
-
-
- Workspace ID
-
- }
- />
-
-
-
-
-
-
-
-
- Member count
-
-
- {workspace?.memberCount}
-
-
-
-
-
- >
- );
-}
diff --git a/apps/client/src/ee/licence/components/license-details.tsx b/apps/client/src/ee/licence/components/license-details.tsx
deleted file mode 100644
index 0a805de9..00000000
--- a/apps/client/src/ee/licence/components/license-details.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Badge, Table } from "@mantine/core";
-import { useLicenseInfo } from "@/ee/licence/queries/license-query.ts";
-import { isLicenseExpired } from "@/ee/licence/license.utils.ts";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
-
-export default function LicenseDetails() {
- const { data: license, isError } = useLicenseInfo();
- const [workspace] = useAtom(workspaceAtom);
- const locale = useDateFnsLocale();
-
- if (!license) {
- return null;
- }
- if (isError) {
- return null;
- }
-
- return (
-
-
-
- Contact sales@docmost.com for support and enquiries.
-
-
-
- Edition
-
- {license.licenseType === "business" ? "Business" : "Enterprise"}{" "}
- {license.trial && Trial }
-
-
-
-
- Licensed to
- {license.customerName}
-
-
-
- Seat count
-
- {license.seatCount} ({workspace?.memberCount} used)
-
-
-
-
- Issued at
-
- {formatLocalized(license.issuedAt, "dd MMMM, yyyy", "PPP", locale)}
-
-
-
-
- Expires at
-
- {formatLocalized(license.expiresAt, "dd MMMM, yyyy", "PPP", locale)}
-
-
-
- License ID
- {license.id}
-
-
- Status
-
- {isLicenseExpired(license) ? (
-
- Expired
-
- ) : (
-
- Valid
-
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/licence/components/license-message.tsx b/apps/client/src/ee/licence/components/license-message.tsx
deleted file mode 100644
index e6bbedb0..00000000
--- a/apps/client/src/ee/licence/components/license-message.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function LicenseMessage() {
- return <>To unlock enterprise features, please contact sales@docmost.com to purchase a license.>;
-}
diff --git a/apps/client/src/ee/licence/components/oss-details.tsx b/apps/client/src/ee/licence/components/oss-details.tsx
deleted file mode 100644
index bee03970..00000000
--- a/apps/client/src/ee/licence/components/oss-details.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { Group, List, Stack, Table, Text, ThemeIcon } from "@mantine/core";
-import { IconCheck } from "@tabler/icons-react";
-
-const enterpriseFeatures = [
- "AI Integration (Chat, Search & Assistant)",
- "MCP Support",
- "SSO (SAML, OIDC, LDAP)",
- "SCIM Provisioning",
- "Multi-factor Authentication (2FA)",
- "Page-level Permissions",
- "Page Verification & Approval Workflow",
- "Audit Logs",
- "Enterprise Controls",
- "API Keys",
- "Advanced Search Engine Support",
- "Full-text Search in Attachments (PDF, DOCX)",
- "Resolve Comments",
- "Confluence Import",
- "PDF & DOCX Import",
- "Templates",
-];
-
-export default function OssDetails() {
- return (
-
-
-
-
-
- Edition
-
-
- Open Source
-
-
-
-
-
-
-
-
-
-
-
-
-
- Upgrade to the Enterprise Edition to unlock:
-
-
-
-
- }
- >
- {enterpriseFeatures.map((feature) => (
- {feature}
- ))}
-
-
-
- Get an enterprise trial key at{" "}
-
- customers.docmost.com
-
- .
-
-
-
- Visit{" "}
-
- docmost.com/pricing
- {" "}
- to purchase an enterprise license.
-
-
- For inquiries, contact{" "}
- sales@docmost.com
-
-
-
- );
-}
diff --git a/apps/client/src/ee/licence/components/remove-license.tsx b/apps/client/src/ee/licence/components/remove-license.tsx
deleted file mode 100644
index a17c9e3c..00000000
--- a/apps/client/src/ee/licence/components/remove-license.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { useRemoveLicenseMutation } from "@/ee/licence/queries/license-query.ts";
-import { Button, Group, Text } from "@mantine/core";
-import { modals } from "@mantine/modals";
-import React from "react";
-
-export default function RemoveLicense() {
- const { t } = useTranslation();
- const removeLicenseMutation = useRemoveLicenseMutation();
-
- const openDeleteModal = () =>
- modals.openConfirmModal({
- title: t("Remove license key"),
- centered: true,
- children: (
-
- {t(
- "Are you sure you want to remove your license key? Your workspace will be downgraded to the non-enterprise version.",
- )}
-
- ),
- labels: { confirm: t("Remove"), cancel: t("Don't") },
- confirmProps: { color: "red" },
- onConfirm: () => removeLicenseMutation.mutate(),
- });
-
- return (
-
- Remove license
-
- );
-}
-
diff --git a/apps/client/src/ee/licence/license.utils.ts b/apps/client/src/ee/licence/license.utils.ts
deleted file mode 100644
index 022c6b1d..00000000
--- a/apps/client/src/ee/licence/license.utils.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { ILicenseInfo } from "@/ee/licence/types/license.types.ts";
-import { differenceInDays, isAfter } from "date-fns";
-
-export const GRACE_PERIOD_DAYS = 10;
-
-export function isLicenseExpired(license: ILicenseInfo): boolean {
- return isAfter(new Date(), license.expiresAt);
-}
-
-export function daysToExpire(license: ILicenseInfo): number {
- const days = differenceInDays(license.expiresAt, new Date());
- return days > 0 ? days : 0;
-}
-
-export function isTrial(license: ILicenseInfo): boolean {
- return license.trial;
-}
-
-export function isValid(license: ILicenseInfo): boolean {
- return !isLicenseExpired(license);
-}
-
-export function hasExpiredGracePeriod(license: ILicenseInfo): boolean {
- if (!isLicenseExpired(license)) return false;
- return differenceInDays(new Date(), license.expiresAt) > GRACE_PERIOD_DAYS;
-}
diff --git a/apps/client/src/ee/licence/pages/license.tsx b/apps/client/src/ee/licence/pages/license.tsx
deleted file mode 100644
index 0aa9d2f5..00000000
--- a/apps/client/src/ee/licence/pages/license.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Helmet } from "react-helmet-async";
-import { getAppName } from "@/lib/config.ts";
-import SettingsTitle from "@/components/settings/settings-title.tsx";
-import React from "react";
-import useUserRole from "@/hooks/use-user-role.tsx";
-import LicenseDetails from "@/ee/licence/components/license-details.tsx";
-import ActivateLicenseForm from "@/ee/licence/components/activate-license-modal.tsx";
-import InstallationDetails from "@/ee/licence/components/installation-details.tsx";
-import OssDetails from "@/ee/licence/components/oss-details.tsx";
-import { useAtom } from "jotai/index";
-import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
-
-export default function License() {
- const [entitlements] = useAtom(entitlementAtom);
- const hasLicense = entitlements != null && entitlements.tier !== "free";
- const { isAdmin } = useUserRole();
-
- if (!isAdmin) {
- return null;
- }
-
- return (
- <>
-
- License - {getAppName()}
-
-
-
-
-
-
-
- {hasLicense ? : }
- >
- );
-}
diff --git a/apps/client/src/ee/licence/queries/license-query.ts b/apps/client/src/ee/licence/queries/license-query.ts
deleted file mode 100644
index 07f1d7e8..00000000
--- a/apps/client/src/ee/licence/queries/license-query.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import {
- useMutation,
- useQuery,
- useQueryClient,
- UseQueryResult,
-} from "@tanstack/react-query";
-import {
- activateLicense,
- removeLicense,
- getLicenseInfo,
-} from "@/ee/licence/services/license-service.ts";
-import { ILicenseInfo } from "@/ee/licence/types/license.types.ts";
-import { notifications } from "@mantine/notifications";
-
-export function useLicenseInfo(): UseQueryResult {
- return useQuery({
- queryKey: ["license"],
- queryFn: () => getLicenseInfo(),
- staleTime: 5 * 60 * 1000,
- });
-}
-
-export function useActivateMutation() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (licenseKey) => activateLicense(licenseKey),
- onSuccess: () => {
- notifications.show({ message: "License activated successfully" });
- queryClient.refetchQueries({
- queryKey: ["license"],
- });
- queryClient.refetchQueries({ queryKey: ["currentUser"] });
- queryClient.refetchQueries({ queryKey: ["entitlements"] });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({ message: errorMessage, color: "red" });
- },
- });
-}
-
-export function useRemoveLicenseMutation() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: () => removeLicense(),
- onSuccess: () => {
- queryClient.refetchQueries({ queryKey: ["license"] });
- queryClient.refetchQueries({ queryKey: ["currentUser"] });
- queryClient.refetchQueries({ queryKey: ["entitlements"] });
- },
- });
-}
diff --git a/apps/client/src/ee/licence/services/license-service.ts b/apps/client/src/ee/licence/services/license-service.ts
deleted file mode 100644
index 5472a38a..00000000
--- a/apps/client/src/ee/licence/services/license-service.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import api from "@/lib/api-client.ts";
-import { ILicenseInfo } from "@/ee/licence/types/license.types.ts";
-
-export async function getLicenseInfo(): Promise {
- const req = await api.post("/license/info");
- return req.data;
-}
-
-export async function activateLicense(
- licenseKey: string,
-): Promise {
- const req = await api.post("/license/activate", { licenseKey });
- return req.data;
-}
-
-export async function removeLicense(): Promise {
- await api.post("/license/remove");
-}
diff --git a/apps/client/src/ee/licence/types/license.types.ts b/apps/client/src/ee/licence/types/license.types.ts
deleted file mode 100644
index ec3c9a18..00000000
--- a/apps/client/src/ee/licence/types/license.types.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export type LicenseType = 'business' | 'enterprise';
-
-export interface ILicenseInfo {
- id: string;
- customerName: string;
- seatCount: number;
- licenseType: LicenseType;
- issuedAt: Date;
- expiresAt: Date;
- trial: boolean;
-}
\ No newline at end of file
diff --git a/apps/client/src/ee/mfa/components/mfa-backup-code-input.tsx b/apps/client/src/ee/mfa/components/mfa-backup-code-input.tsx
deleted file mode 100644
index 2b774060..00000000
--- a/apps/client/src/ee/mfa/components/mfa-backup-code-input.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React from "react";
-import {
- TextInput,
- Button,
- Stack,
- Text,
- Alert,
-} from "@mantine/core";
-import { IconKey, IconAlertCircle } from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-
-interface MfaBackupCodeInputProps {
- value: string;
- onChange: (value: string) => void;
- error?: string;
- onSubmit: () => void;
- onCancel: () => void;
- isLoading?: boolean;
-}
-
-export function MfaBackupCodeInput({
- value,
- onChange,
- error,
- onSubmit,
- onCancel,
- isLoading,
-}: MfaBackupCodeInputProps) {
- const { t } = useTranslation();
-
- return (
-
- } color="blue" variant="light">
-
- {t(
- "Enter one of your backup codes. Each backup code can only be used once.",
- )}
-
-
-
- onChange(e.currentTarget.value.toUpperCase())}
- error={error}
- autoFocus
- data-autofocus
- maxLength={8}
- styles={{
- input: {
- fontFamily: "monospace",
- letterSpacing: "0.1em",
- fontSize: "1rem",
- },
- }}
- />
-
-
- }
- >
- {t("Verify backup code")}
-
-
-
- {t("Use authenticator app instead")}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/client/src/ee/mfa/components/mfa-backup-codes-modal.tsx b/apps/client/src/ee/mfa/components/mfa-backup-codes-modal.tsx
deleted file mode 100644
index cee00031..00000000
--- a/apps/client/src/ee/mfa/components/mfa-backup-codes-modal.tsx
+++ /dev/null
@@ -1,213 +0,0 @@
-import React, { useState } from "react";
-import {
- Modal,
- Stack,
- Text,
- Button,
- Paper,
- Group,
- List,
- Code,
- Alert,
- PasswordInput,
-} from "@mantine/core";
-import { CopyButton } from "@/components/common/copy-button";
-import {
- IconRefresh,
- IconCopy,
- IconCheck,
- IconAlertCircle,
-} from "@tabler/icons-react";
-import { useMutation } from "@tanstack/react-query";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-import { regenerateBackupCodes } from "@/ee/mfa";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { z } from "zod/v4";
-import useCurrentUser from "@/features/user/hooks/use-current-user";
-
-interface MfaBackupCodesModalProps {
- opened: boolean;
- onClose: () => void;
-}
-
-export function MfaBackupCodesModal({
- opened,
- onClose,
-}: MfaBackupCodesModalProps) {
- const { t } = useTranslation();
- const { data: currentUser } = useCurrentUser();
- const [backupCodes, setBackupCodes] = useState([]);
- const [showNewCodes, setShowNewCodes] = useState(false);
- const requiresPassword = !currentUser?.user?.hasGeneratedPassword;
-
- const formSchema = requiresPassword
- ? z.object({
- confirmPassword: z.string().min(1, { message: "Password is required" }),
- })
- : z.object({
- confirmPassword: z.string().optional(),
- });
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- confirmPassword: "",
- },
- });
-
- const regenerateMutation = useMutation({
- mutationFn: (data: { confirmPassword?: string }) =>
- regenerateBackupCodes(data),
- onSuccess: (data) => {
- setBackupCodes(data.backupCodes);
- setShowNewCodes(true);
- form.reset();
- notifications.show({
- title: t("Success"),
- message: t("New backup codes have been generated"),
- });
- },
- onError: (error: any) => {
- notifications.show({
- title: t("Error"),
- message:
- error.response?.data?.message ||
- t("Failed to regenerate backup codes"),
- color: "red",
- });
- },
- });
-
- const handleRegenerate = (values: { confirmPassword?: string }) => {
- // Only send confirmPassword if it's required (non-SSO users)
- const payload = requiresPassword
- ? { confirmPassword: values.confirmPassword }
- : {};
- regenerateMutation.mutate(payload);
- };
-
- const handleClose = () => {
- setShowNewCodes(false);
- setBackupCodes([]);
- form.reset();
- onClose();
- };
-
- return (
-
-
- {!showNewCodes ? (
-
-
- }
- title={t("About backup codes")}
- color="blue"
- variant="light"
- >
-
- {t(
- "Backup codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.",
- )}
-
-
-
-
- {t(
- "You can regenerate new backup codes at any time. This will invalidate all existing codes.",
- )}
-
-
- {requiresPassword && (
-
- )}
-
- }
- >
- {t("Generate new backup codes")}
-
-
-
- ) : (
- <>
- }
- title={t("Save your new backup codes")}
- color="yellow"
- >
-
- {t(
- "Make sure to save these codes in a secure place. Your old backup codes are no longer valid.",
- )}
-
-
-
-
-
-
- {t("Your new backup codes")}
-
-
- {({ copied, copy }) => (
-
- ) : (
-
- )
- }
- >
- {copied ? t("Copied") : t("Copy")}
-
- )}
-
-
-
- {backupCodes.map((code, index) => (
-
- {code}
-
- ))}
-
-
-
- }
- >
- {t("I've saved my backup codes")}
-
- >
- )}
-
-
- );
-}
diff --git a/apps/client/src/ee/mfa/components/mfa-challenge.module.css b/apps/client/src/ee/mfa/components/mfa-challenge.module.css
deleted file mode 100644
index 45eb5df9..00000000
--- a/apps/client/src/ee/mfa/components/mfa-challenge.module.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.container {
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 1rem;
-}
-
-.paper {
- width: 100%;
- box-shadow: var(--mantine-shadow-lg);
-}
\ No newline at end of file
diff --git a/apps/client/src/ee/mfa/components/mfa-challenge.tsx b/apps/client/src/ee/mfa/components/mfa-challenge.tsx
deleted file mode 100644
index bfa16b22..00000000
--- a/apps/client/src/ee/mfa/components/mfa-challenge.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import React, { useState } from "react";
-import {
- Container,
- Title,
- Text,
- PinInput,
- Button,
- Stack,
- Anchor,
- Paper,
- Center,
- ThemeIcon,
-} from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { IconDeviceMobile, IconLock } from "@tabler/icons-react";
-import { useNavigate } from "react-router-dom";
-import { notifications } from "@mantine/notifications";
-import classes from "./mfa-challenge.module.css";
-import { verifyMfa } from "@/ee/mfa";
-import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
-import { useTranslation } from "react-i18next";
-import { z } from "zod/v4";
-import { MfaBackupCodeInput } from "./mfa-backup-code-input";
-import { AuthLayout } from "@/features/auth/components/auth-layout.tsx";
-
-const formSchema = z.object({
- code: z
- .string()
- .refine(
- (val) => (val.length === 6 && /^\d{6}$/.test(val)) || val.length === 8,
- {
- message: "Enter a 6-digit code or 8-character backup code",
- },
- ),
-});
-
-type MfaChallengeFormValues = z.infer;
-
-export function MfaChallenge() {
- const { t } = useTranslation();
- const navigate = useNavigate();
- const [isLoading, setIsLoading] = useState(false);
- const [useBackupCode, setUseBackupCode] = useState(false);
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- code: "",
- },
- });
-
- const handleSubmit = async (values: MfaChallengeFormValues) => {
- setIsLoading(true);
- try {
- await verifyMfa(values.code);
- navigate(getPostLoginRedirect());
- } catch (error: any) {
- setIsLoading(false);
- notifications.show({
- message:
- error.response?.data?.message || t("Invalid verification code"),
- color: "red",
- });
- form.setFieldValue("code", "");
- }
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {t("Two-factor authentication")}
-
-
- {useBackupCode
- ? t("Enter one of your backup codes")
- : t("Enter the 6-digit code found in your authenticator app")}
-
-
-
- {!useBackupCode ? (
-
-
-
-
-
- {form.errors.code && (
-
- {form.errors.code}
-
- )}
-
- }
- >
- {t("Verify")}
-
-
- {
- setUseBackupCode(true);
- form.setFieldValue("code", "");
- form.clearErrors();
- }}
- >
- {t("Use backup code")}
-
-
-
- ) : (
- form.setFieldValue("code", value)}
- error={form.errors.code?.toString()}
- onSubmit={() => handleSubmit(form.values)}
- onCancel={() => {
- setUseBackupCode(false);
- form.setFieldValue("code", "");
- form.clearErrors();
- }}
- isLoading={isLoading}
- />
- )}
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/mfa/components/mfa-disable-modal.tsx b/apps/client/src/ee/mfa/components/mfa-disable-modal.tsx
deleted file mode 100644
index 25e95161..00000000
--- a/apps/client/src/ee/mfa/components/mfa-disable-modal.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import React from "react";
-import {
- Modal,
- Stack,
- Text,
- Button,
- PasswordInput,
- Alert,
-} from "@mantine/core";
-import { IconShieldOff, IconAlertTriangle } from "@tabler/icons-react";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { useMutation } from "@tanstack/react-query";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-import { z } from "zod/v4";
-import { disableMfa } from "@/ee/mfa";
-import useCurrentUser from "@/features/user/hooks/use-current-user";
-
-interface MfaDisableModalProps {
- opened: boolean;
- onClose: () => void;
- onComplete: () => void;
-}
-
-export function MfaDisableModal({
- opened,
- onClose,
- onComplete,
-}: MfaDisableModalProps) {
- const { t } = useTranslation();
- const { data: currentUser } = useCurrentUser();
- const requiresPassword = !currentUser?.user?.hasGeneratedPassword;
-
- const formSchema = requiresPassword
- ? z.object({
- confirmPassword: z.string().min(1, { message: "Password is required" }),
- })
- : z.object({
- confirmPassword: z.string().optional(),
- });
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- confirmPassword: "",
- },
- });
-
- const disableMutation = useMutation({
- mutationFn: disableMfa,
- onSuccess: () => {
- onComplete();
- },
- onError: (error: any) => {
- notifications.show({
- title: t("Error"),
- message: error.response?.data?.message || t("Failed to disable MFA"),
- color: "red",
- });
- },
- });
-
- const handleSubmit = async (values: { confirmPassword?: string }) => {
- // Only send confirmPassword if it's required (non-SSO users)
- const payload = requiresPassword
- ? { confirmPassword: values.confirmPassword }
- : {};
- await disableMutation.mutateAsync(payload);
- };
-
- const handleClose = () => {
- form.reset();
- onClose();
- };
-
- return (
-
-
-
- }
- title={t("Warning")}
- color="red"
- variant="light"
- >
-
- {t(
- "Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.",
- )}
-
-
-
- {requiresPassword && (
- <>
-
- {t(
- "Please enter your password to disable two-factor authentication:",
- )}
-
-
-
- >
- )}
-
-
- }
- >
- {t("Disable two-factor authentication")}
-
-
- {t("Cancel")}
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/mfa/components/mfa-settings.tsx b/apps/client/src/ee/mfa/components/mfa-settings.tsx
deleted file mode 100644
index 620d67ac..00000000
--- a/apps/client/src/ee/mfa/components/mfa-settings.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import React, { useState } from "react";
-import { Group, Text, Button, Tooltip } from "@mantine/core";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-import { getMfaStatus } from "@/ee/mfa";
-import { MfaSetupModal } from "@/ee/mfa";
-import { MfaDisableModal } from "@/ee/mfa";
-import { MfaBackupCodesModal } from "@/ee/mfa";
-import { useHasFeature } from "@/ee/hooks/use-feature";
-import { Feature } from "@/ee/features";
-import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
-import { ResponsiveSettingsRow, ResponsiveSettingsContent, ResponsiveSettingsControl } from "@/components/ui/responsive-settings-row";
-
-export function MfaSettings() {
- const { t } = useTranslation();
- const queryClient = useQueryClient();
- const [setupModalOpen, setSetupModalOpen] = useState(false);
- const [disableModalOpen, setDisableModalOpen] = useState(false);
- const [backupCodesModalOpen, setBackupCodesModalOpen] = useState(false);
- const canUseMfa = useHasFeature(Feature.MFA);
- const upgradeLabel = useUpgradeLabel();
-
- const { data: mfaStatus, isLoading } = useQuery({
- queryKey: ["mfa-status"],
- queryFn: getMfaStatus,
- });
-
- if (isLoading || !mfaStatus) {
- return null;
- }
-
- // Check if MFA is truly enabled
- const isMfaEnabled = mfaStatus?.isEnabled === true;
-
- const handleSetupComplete = () => {
- setSetupModalOpen(false);
- queryClient.invalidateQueries({ queryKey: ["mfa-status"] });
- notifications.show({
- title: t("Success"),
- message: t("Two-factor authentication has been enabled"),
- });
- };
-
- const handleDisableComplete = () => {
- setDisableModalOpen(false);
- queryClient.invalidateQueries({ queryKey: ["mfa-status"] });
- notifications.show({
- title: t("Success"),
- message: t("Two-factor authentication has been disabled"),
- color: "blue",
- });
- };
-
- return (
- <>
-
-
- {t("2-step verification")}
-
- {!isMfaEnabled
- ? t(
- "Protect your account with an additional verification layer when signing in.",
- )
- : t("Two-factor authentication is active on your account.")}
-
-
-
-
- {!isMfaEnabled ? (
-
- setSetupModalOpen(true)}
- style={{ whiteSpace: "nowrap" }}
- >
- {t("Add 2FA method")}
-
-
- ) : (
-
- setBackupCodesModalOpen(true)}
- style={{ whiteSpace: "nowrap" }}
- >
- {t("Backup codes")} ({mfaStatus?.backupCodesCount || 0})
-
- setDisableModalOpen(true)}
- style={{ whiteSpace: "nowrap" }}
- >
- {t("Disable")}
-
-
- )}
-
-
-
- setSetupModalOpen(false)}
- onComplete={handleSetupComplete}
- />
-
- setDisableModalOpen(false)}
- onComplete={handleDisableComplete}
- />
-
- setBackupCodesModalOpen(false)}
- />
- >
- );
-}
diff --git a/apps/client/src/ee/mfa/components/mfa-setup-modal.tsx b/apps/client/src/ee/mfa/components/mfa-setup-modal.tsx
deleted file mode 100644
index 0046db6b..00000000
--- a/apps/client/src/ee/mfa/components/mfa-setup-modal.tsx
+++ /dev/null
@@ -1,348 +0,0 @@
-import React, { useState } from "react";
-import {
- Modal,
- Stack,
- Text,
- Button,
- Group,
- Stepper,
- Center,
- Image,
- PinInput,
- Alert,
- List,
- ActionIcon,
- Tooltip,
- Paper,
- Code,
- Loader,
- Collapse,
- UnstyledButton,
-} from "@mantine/core";
-import { CopyButton } from "@/components/common/copy-button";
-import {
- IconQrcode,
- IconShieldCheck,
- IconKey,
- IconCopy,
- IconCheck,
- IconAlertCircle,
- IconChevronDown,
- IconChevronRight,
- IconPrinter,
-} from "@tabler/icons-react";
-import { useForm } from "@mantine/form";
-import { useMutation } from "@tanstack/react-query";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-import { setupMfa, enableMfa } from "@/ee/mfa";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { z } from "zod/v4";
-
-interface MfaSetupModalProps {
- opened: boolean;
- onClose?: () => void;
- onComplete: () => void;
- isRequired?: boolean;
-}
-
-interface SetupData {
- secret: string;
- qrCode: string;
- manualKey: string;
-}
-
-const formSchema = z.object({
- verificationCode: z
- .string()
- .length(6, { message: "Please enter a 6-digit code" }),
-});
-
-export function MfaSetupModal({
- opened,
- onClose,
- onComplete,
- isRequired = false,
-}: MfaSetupModalProps) {
- const { t } = useTranslation();
- const [active, setActive] = useState(0);
- const [setupData, setSetupData] = useState(null);
- const [backupCodes, setBackupCodes] = useState([]);
- const [manualEntryOpen, setManualEntryOpen] = useState(false);
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- verificationCode: "",
- },
- });
-
- const setupMutation = useMutation({
- mutationFn: () => setupMfa({ method: "totp" }),
- onSuccess: (data) => {
- setSetupData(data);
- },
- onError: (error: any) => {
- notifications.show({
- title: t("Error"),
- message: error.response?.data?.message || t("Failed to setup MFA"),
- color: "red",
- });
- },
- });
-
- // Generate QR code when modal opens
- React.useEffect(() => {
- if (opened && !setupData && !setupMutation.isPending) {
- setupMutation.mutate();
- }
- }, [opened]);
-
- const enableMutation = useMutation({
- mutationFn: (verificationCode: string) =>
- enableMfa({
- secret: setupData!.secret,
- verificationCode,
- }),
- onSuccess: (data) => {
- setBackupCodes(data.backupCodes);
- setActive(1); // Move to backup codes step
- },
- onError: (error: any) => {
- notifications.show({
- title: t("Error"),
- message:
- error.response?.data?.message || t("Invalid verification code"),
- color: "red",
- });
- form.setFieldValue("verificationCode", "");
- },
- });
-
- const handleClose = () => {
- if (active === 1 && backupCodes.length > 0) {
- onComplete();
- }
- onClose();
- // Reset state
- setTimeout(() => {
- setActive(0);
- setSetupData(null);
- setBackupCodes([]);
- setManualEntryOpen(false);
- form.reset();
- }, 200);
- };
-
- const handleVerify = async (values: { verificationCode: string }) => {
- await enableMutation.mutateAsync(values.verificationCode);
- };
-
- const handlePrintBackupCodes = () => {
- window.print();
- };
-
- return (
-
-
- }
- >
-
-
- {setupMutation.isPending ? (
-
-
-
- ) : setupData ? (
- <>
-
- {t("1. Scan this QR code with your authenticator app")}
-
-
-
-
-
-
-
-
- setManualEntryOpen(!manualEntryOpen)}
- >
-
- {manualEntryOpen ? (
-
- ) : (
-
- )}
-
- {t("Can't scan the code?")}
-
-
-
-
-
- }
- color="gray"
- variant="light"
- >
-
- {t(
- "Enter this code manually in your authenticator app:",
- )}
-
-
- {setupData.manualKey}
-
- {({ copied, copy }) => (
-
-
- {copied ? (
-
- ) : (
-
- )}
-
-
- )}
-
-
-
-
-
-
- {t("2. Enter the 6-digit code from your authenticator")}
-
-
-
-
- {form.errors.verificationCode && (
-
- {form.errors.verificationCode}
-
- )}
-
-
- }
- >
- {t("Verify and enable")}
-
- >
- ) : (
-
-
- {t("Failed to generate QR code. Please try again.")}
-
-
- )}
-
-
-
-
- }
- >
-
- }
- title={t("Save your backup codes")}
- color="yellow"
- >
-
- {t(
- "These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.",
- )}
-
-
-
-
-
-
- {t("Backup codes")}
-
-
-
- {({ copied, copy }) => (
-
- ) : (
-
- )
- }
- >
- {copied ? t("Copied") : t("Copy")}
-
- )}
-
- }
- >
- {t("Print")}
-
-
-
-
- {backupCodes.map((code, index) => (
-
- {code}
-
- ))}
-
-
-
- }
- >
- {t("I've saved my backup codes")}
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/mfa/components/mfa-setup-required.tsx b/apps/client/src/ee/mfa/components/mfa-setup-required.tsx
deleted file mode 100644
index 880228a9..00000000
--- a/apps/client/src/ee/mfa/components/mfa-setup-required.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from "react";
-import { Container, Paper, Title, Text, Alert, Stack } from "@mantine/core";
-import { IconAlertCircle } from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import { MfaSetupModal } from "@/ee/mfa";
-import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route.ts";
-import { useNavigate } from "react-router-dom";
-import { AuthLayout } from "@/features/auth/components/auth-layout.tsx";
-
-export default function MfaSetupRequired() {
- const { t } = useTranslation();
- const navigate = useNavigate();
-
- const handleSetupComplete = () => {
- navigate(getPostLoginRedirect());
- };
-
- return (
-
-
-
-
-
- {t("Two-factor authentication required")}
-
-
- } color="yellow">
-
- {t(
- "Your workspace requires two-factor authentication. Please set it up to continue.",
- )}
-
-
-
-
- {t(
- "This adds an extra layer of security to your account by requiring a verification code from your authenticator app.",
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/mfa/components/mfa.module.css b/apps/client/src/ee/mfa/components/mfa.module.css
deleted file mode 100644
index 535704a5..00000000
--- a/apps/client/src/ee/mfa/components/mfa.module.css
+++ /dev/null
@@ -1,31 +0,0 @@
-.qrCodeContainer {
- background-color: white;
- padding: 1rem;
- border-radius: var(--mantine-radius-md);
- display: inline-block;
-}
-
-.backupCodesList {
- font-family: var(--mantine-font-family-monospace);
- background-color: var(--mantine-color-gray-0);
- padding: 1rem;
- border-radius: var(--mantine-radius-md);
-
- @mixin dark {
- background-color: var(--mantine-color-dark-7);
- }
-}
-
-.codeItem {
- padding: 0.25rem 0;
- font-size: 0.875rem;
-}
-
-.setupStep {
- min-height: 400px;
-}
-
-.verificationInput {
- max-width: 320px;
- margin: 0 auto;
-}
\ No newline at end of file
diff --git a/apps/client/src/ee/mfa/hooks/use-mfa-page-protection.ts b/apps/client/src/ee/mfa/hooks/use-mfa-page-protection.ts
deleted file mode 100644
index 30b27427..00000000
--- a/apps/client/src/ee/mfa/hooks/use-mfa-page-protection.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { useEffect, useState } from "react";
-import { useNavigate, useLocation } from "react-router-dom";
-import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
-import { validateMfaAccess } from "@/ee/mfa";
-
-export function useMfaPageProtection() {
- const navigate = useNavigate();
- const location = useLocation();
- const [isValidating, setIsValidating] = useState(true);
- const [isValid, setIsValid] = useState(false);
-
- useEffect(() => {
- const checkAccess = async () => {
- const result = await validateMfaAccess();
-
- const search = location.search;
-
- if (!result.valid) {
- navigate(APP_ROUTE.AUTH.LOGIN + search);
- return;
- }
-
- // Check if user is on the correct page based on their MFA state
- const isOnChallengePage =
- location.pathname === APP_ROUTE.AUTH.MFA_CHALLENGE;
- const isOnSetupPage =
- location.pathname === APP_ROUTE.AUTH.MFA_SETUP_REQUIRED;
-
- if (result.requiresMfaSetup && !isOnSetupPage) {
- // User needs to set up MFA but is on challenge page
- navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + search);
- } else if (
- !result.requiresMfaSetup &&
- result.userHasMfa &&
- !isOnChallengePage
- ) {
- // User has MFA and should be on challenge page
- navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + search);
- } else if (!result.isTransferToken) {
- // User has a regular auth token, shouldn't be on MFA pages
- navigate(getPostLoginRedirect());
- } else {
- setIsValid(true);
- }
-
- setIsValidating(false);
- };
-
- checkAccess();
- }, [navigate, location.pathname]);
-
- return { isValidating, isValid };
-}
diff --git a/apps/client/src/ee/mfa/index.ts b/apps/client/src/ee/mfa/index.ts
deleted file mode 100644
index 047b0a8d..00000000
--- a/apps/client/src/ee/mfa/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// Components
-export { MfaChallenge } from "./components/mfa-challenge";
-export { MfaSettings } from "./components/mfa-settings";
-export { MfaSetupModal } from "./components/mfa-setup-modal";
-export { MfaDisableModal } from "./components/mfa-disable-modal";
-export { MfaBackupCodesModal } from "./components/mfa-backup-codes-modal";
-
-// Pages
-export { MfaChallengePage } from "./pages/mfa-challenge-page";
-export { MfaSetupRequiredPage } from "./pages/mfa-setup-required-page";
-
-// Services
-export * from "./services/mfa-service";
-
-// Types
-export * from "./types/mfa.types";
-
-// Hooks
-export { useMfaPageProtection } from "./hooks/use-mfa-page-protection.ts";
diff --git a/apps/client/src/ee/mfa/pages/mfa-challenge-page.tsx b/apps/client/src/ee/mfa/pages/mfa-challenge-page.tsx
deleted file mode 100644
index 40949fc7..00000000
--- a/apps/client/src/ee/mfa/pages/mfa-challenge-page.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from "react";
-import { MfaChallenge } from "@/ee/mfa";
-import { useMfaPageProtection } from "@/ee/mfa";
-
-export function MfaChallengePage() {
- const { isValid } = useMfaPageProtection();
-
- if (!isValid) {
- return null;
- }
-
- return ;
-}
diff --git a/apps/client/src/ee/mfa/pages/mfa-setup-required-page.tsx b/apps/client/src/ee/mfa/pages/mfa-setup-required-page.tsx
deleted file mode 100644
index 0b5f756d..00000000
--- a/apps/client/src/ee/mfa/pages/mfa-setup-required-page.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import React, { useState } from "react";
-import { useNavigate } from "react-router-dom";
-import {
- Container,
- Title,
- Text,
- Button,
- Stack,
- Paper,
- Alert,
- Center,
- ThemeIcon,
-} from "@mantine/core";
-import { IconShieldCheck, IconAlertCircle } from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import APP_ROUTE from "@/lib/app-route";
-import { MfaSetupModal } from "@/ee/mfa";
-import classes from "@/features/auth/components/auth.module.css";
-import { notifications } from "@mantine/notifications";
-import { useMfaPageProtection } from "@/ee/mfa";
-
-export function MfaSetupRequiredPage() {
- const { t } = useTranslation();
- const navigate = useNavigate();
- const [setupModalOpen, setSetupModalOpen] = useState(false);
- const { isValid } = useMfaPageProtection();
-
- const handleSetupComplete = async () => {
- setSetupModalOpen(false);
-
- notifications.show({
- title: t("Success"),
- message: t(
- "Two-factor authentication has been set up. Please log in again.",
- ),
- });
-
- navigate(APP_ROUTE.AUTH.LOGIN);
- };
-
- const handleLogout = () => {
- navigate(APP_ROUTE.AUTH.LOGIN);
- };
-
- if (!isValid) {
- return null;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {t("Two-factor authentication required")}
-
-
- {t(
- "Your workspace requires two-factor authentication for all users",
- )}
-
-
-
- }
- color="blue"
- variant="light"
- w="100%"
- >
-
- {t(
- "To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.",
- )}
-
-
-
-
- setSetupModalOpen(true)}
- leftSection={ }
- >
- {t("Set up two-factor authentication")}
-
-
-
- {t("Cancel and logout")}
-
-
-
-
-
- setSetupModalOpen(false)}
- onComplete={handleSetupComplete}
- isRequired={true}
- />
-
- );
-}
diff --git a/apps/client/src/ee/mfa/services/mfa-service.ts b/apps/client/src/ee/mfa/services/mfa-service.ts
deleted file mode 100644
index 2c4956e1..00000000
--- a/apps/client/src/ee/mfa/services/mfa-service.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import api from "@/lib/api-client";
-import {
- MfaBackupCodesResponse,
- MfaDisableRequest,
- MfaEnableRequest,
- MfaEnableResponse,
- MfaSetupRequest,
- MfaSetupResponse,
- MfaStatusResponse,
- MfaAccessValidationResponse,
-} from "@/ee/mfa";
-
-export async function getMfaStatus(): Promise {
- const req = await api.post("/mfa/status");
- return req.data;
-}
-
-export async function setupMfa(
- data: MfaSetupRequest,
-): Promise {
- const req = await api.post("/mfa/setup", data);
- return req.data;
-}
-
-export async function enableMfa(
- data: MfaEnableRequest,
-): Promise {
- const req = await api.post("/mfa/enable", data);
- return req.data;
-}
-
-export async function disableMfa(
- data: MfaDisableRequest,
-): Promise<{ success: boolean }> {
- const req = await api.post<{ success: boolean }>("/mfa/disable", data);
- return req.data;
-}
-
-export async function regenerateBackupCodes(data: {
- confirmPassword?: string;
-}): Promise {
- const req = await api.post(
- "/mfa/generate-backup-codes",
- data,
- );
- return req.data;
-}
-
-export async function verifyMfa(code: string): Promise {
- const req = await api.post("/mfa/verify", { code });
- return req.data;
-}
-
-export async function validateMfaAccess(): Promise {
- try {
- const res = await api.post("/mfa/validate-access");
- return res.data;
- } catch {
- return { valid: false };
- }
-}
diff --git a/apps/client/src/ee/mfa/types/mfa.types.ts b/apps/client/src/ee/mfa/types/mfa.types.ts
deleted file mode 100644
index 9f3bbe7c..00000000
--- a/apps/client/src/ee/mfa/types/mfa.types.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-export interface MfaMethod {
- type: 'totp' | 'email';
- isEnabled: boolean;
-}
-
-export interface MfaSettings {
- isEnabled: boolean;
- methods: MfaMethod[];
- backupCodesCount: number;
- lastUpdated?: string;
-}
-
-export interface MfaSetupState {
- method: 'totp' | 'email';
- secret?: string;
- qrCode?: string;
- manualEntry?: string;
- backupCodes?: string[];
-}
-
-export interface MfaStatusResponse {
- isEnabled?: boolean;
- method?: string | null;
- backupCodesCount?: number;
-}
-
-export interface MfaSetupRequest {
- method: 'totp';
-}
-
-export interface MfaSetupResponse {
- method: string;
- qrCode: string;
- secret: string;
- manualKey: string;
-}
-
-export interface MfaEnableRequest {
- secret: string;
- verificationCode: string;
-}
-
-export interface MfaEnableResponse {
- success: boolean;
- backupCodes: string[];
-}
-
-export interface MfaDisableRequest {
- confirmPassword?: string;
-}
-
-export interface MfaBackupCodesResponse {
- backupCodes: string[];
-}
-
-export interface MfaAccessValidationResponse {
- valid: boolean;
- isTransferToken?: boolean;
- requiresMfaSetup?: boolean;
- userHasMfa?: boolean;
- isMfaEnforced?: boolean;
-}
diff --git a/apps/client/src/ee/page-permission/components/general-access-select.tsx b/apps/client/src/ee/page-permission/components/general-access-select.tsx
deleted file mode 100644
index 8bee6e4b..00000000
--- a/apps/client/src/ee/page-permission/components/general-access-select.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import { Group, Menu, Text, UnstyledButton } from "@mantine/core";
-import {
- IconChevronDown,
- IconLock,
- IconShieldLock,
- IconCheck,
-} from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import classes from "./page-permission.module.css";
-
-type AccessLevel = "open" | "restricted";
-
-type GeneralAccessSelectProps = {
- value: AccessLevel;
- onChange: (value: AccessLevel) => void;
- disabled?: boolean;
- hasInheritedRestriction?: boolean;
-};
-
-export function GeneralAccessSelect({
- value,
- onChange,
- disabled,
- hasInheritedRestriction,
-}: GeneralAccessSelectProps) {
- const { t } = useTranslation();
-
- const isDirectlyRestricted = value === "restricted";
- const showInheritedState = hasInheritedRestriction && !isDirectlyRestricted;
-
- const currentLabel = showInheritedState
- ? t("Restricted by parent")
- : isDirectlyRestricted
- ? t("Restricted")
- : t("Open");
-
- const currentDescription = showInheritedState
- ? t("Inherits restrictions from ancestor page")
- : isDirectlyRestricted
- ? t("Only people listed below can access this page")
- : t("Everyone in this space can access");
-
- const CurrentIcon = showInheritedState
- ? IconShieldLock
- : isDirectlyRestricted
- ? IconLock
- : IconShieldLock;
-
- const accessOptions = [
- {
- value: "open" as const,
- label: hasInheritedRestriction ? t("Restricted by parent") : t("Open"),
- description: hasInheritedRestriction
- ? t("Use only inherited restrictions")
- : t("No additional restrictions on this page"),
- icon: IconShieldLock,
- },
- {
- value: "restricted" as const,
- label: t("Restricted"),
- description: hasInheritedRestriction
- ? t("Add restrictions on top of inherited")
- : t("Only specific people can access"),
- icon: IconLock,
- },
- ];
-
- return (
-
-
-
-
-
-
-
-
-
- {currentLabel}
-
- {!disabled && }
-
-
- {currentDescription}
-
-
-
-
-
-
- {accessOptions.map((option) => (
- onChange(option.value)}
- leftSection={ }
- rightSection={
- option.value === value ? : null
- }
- >
-
- {option.label}
-
- {option.description}
-
-
-
- ))}
-
-
- );
-}
diff --git a/apps/client/src/ee/page-permission/components/page-permission-item.tsx b/apps/client/src/ee/page-permission/components/page-permission-item.tsx
deleted file mode 100644
index b0a5c5f4..00000000
--- a/apps/client/src/ee/page-permission/components/page-permission-item.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { Menu, Text, UnstyledButton, Group } from "@mantine/core";
-import { IconChevronDown, IconCheck } from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import { useAtomValue } from "jotai";
-import { CustomAvatar } from "@/components/ui/custom-avatar";
-import { AutoTooltipText } from "@/components/ui/auto-tooltip-text";
-import { IconGroupCircle } from "@/components/icons/icon-people-circle";
-import { userAtom } from "@/features/user/atoms/current-user-atom";
-import { formatMemberCount } from "@/lib";
-import {
- IPagePermissionMember,
- PagePermissionRole,
-} from "@/ee/page-permission/types/page-permission.types";
-import {
- pagePermissionRoleData,
- getPagePermissionRoleLabel,
-} from "@/ee/page-permission/types/page-permission-role-data";
-import classes from "./page-permission.module.css";
-
-type PagePermissionItemProps = {
- member: IPagePermissionMember;
- onRoleChange: (memberId: string, type: "user" | "group", role: string) => void;
- onRemove: (memberId: string, type: "user" | "group") => void;
- disabled?: boolean;
-};
-
-export function PagePermissionItem({
- member,
- onRoleChange,
- onRemove,
- disabled,
-}: PagePermissionItemProps) {
- const { t } = useTranslation();
- const currentUser = useAtomValue(userAtom);
- const isCurrentUser = member.type === "user" && member.id === currentUser?.id;
- const roleLabel = getPagePermissionRoleLabel(member.role);
-
- return (
-
-
- {member.type === "user" && (
-
- )}
- {member.type === "group" &&
}
-
-
-
- {member.name}
- {isCurrentUser && ({t("You")}) }
-
-
- {member.type === "user" ? member.email : formatMemberCount(member.memberCount, t)}
-
-
-
-
-
- {isCurrentUser || disabled ? (
-
- {t(roleLabel)}
-
- ) : (
-
-
-
-
- {t(roleLabel)}
-
-
-
-
-
-
- {pagePermissionRoleData.map((role) => (
- onRoleChange(member.id, member.type, role.value)}
- rightSection={
- role.value === member.role ? : null
- }
- >
-
- {t(role.label)}
-
- {t(role.description)}
-
-
-
- ))}
-
- onRemove(member.id, member.type)}
- >
- {t("Remove access")}
-
-
-
- )}
-
-
- );
-}
diff --git a/apps/client/src/ee/page-permission/components/page-permission-list.tsx b/apps/client/src/ee/page-permission/components/page-permission-list.tsx
deleted file mode 100644
index 111bb83f..00000000
--- a/apps/client/src/ee/page-permission/components/page-permission-list.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-import { Center, Group, Loader, ScrollArea, Text } from "@mantine/core";
-import { useTranslation } from "react-i18next";
-import { useAtomValue } from "jotai";
-import { useEffect, useRef } from "react";
-import { modals } from "@mantine/modals";
-import { userAtom } from "@/features/user/atoms/current-user-atom";
-import { PagePermissionRole } from "@/ee/page-permission/types/page-permission.types";
-import {
- usePagePermissionsQuery,
- useRemovePagePermissionMutation,
- useUpdatePagePermissionRoleMutation,
-} from "@/ee/page-permission/queries/page-permission-query";
-import { PagePermissionItem } from "@/ee/page-permission";
-import classes from "./page-permission.module.css";
-
-type PagePermissionListProps = {
- pageId: string;
- canManage: boolean;
- onRemoveAll?: () => void;
-};
-
-export function PagePermissionList({
- pageId,
- canManage,
- onRemoveAll,
-}: PagePermissionListProps) {
- const { t } = useTranslation();
- const currentUser = useAtomValue(userAtom);
- const updateRoleMutation = useUpdatePagePermissionRoleMutation();
- const removeMutation = useRemovePagePermissionMutation();
-
- const { data, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage } =
- usePagePermissionsQuery(pageId);
-
- const sentinelRef = useRef(null);
- const viewportRef = useRef(null);
-
- useEffect(() => {
- const sentinel = sentinelRef.current;
- if (!sentinel) return;
-
- const observer = new IntersectionObserver(
- (entries) => {
- if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
- fetchNextPage();
- }
- },
- { root: viewportRef.current, threshold: 0.1 },
- );
-
- observer.observe(sentinel);
- return () => observer.disconnect();
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
-
- const handleRoleChange = async (
- memberId: string,
- type: "user" | "group",
- newRole: string,
- ) => {
- await updateRoleMutation.mutateAsync({
- pageId,
- role: newRole as PagePermissionRole,
- ...(type === "user" ? { userId: memberId } : { groupId: memberId }),
- });
- };
-
- const handleRemove = (memberId: string, type: "user" | "group") => {
- modals.openConfirmModal({
- title: t("Remove access"),
- children: (
-
- {t(
- "Are you sure you want to remove this member's access to the page?",
- )}
-
- ),
- centered: true,
- labels: { confirm: t("Remove"), cancel: t("Cancel") },
- confirmProps: { color: "red" },
- onConfirm: async () => {
- await removeMutation.mutateAsync({
- pageId,
- ...(type === "user"
- ? { userIds: [memberId] }
- : { groupIds: [memberId] }),
- });
- },
- });
- };
-
- const handleRemoveAll = () => {
- modals.openConfirmModal({
- title: t("Remove all access"),
- children: (
-
- {t(
- "Are you sure you want to remove all specific access? This will make the page open to everyone in the space.",
- )}
-
- ),
- centered: true,
- labels: { confirm: t("Remove all"), cancel: t("Cancel") },
- confirmProps: { color: "red" },
- onConfirm: () => onRemoveAll?.(),
- });
- };
-
- const members = data?.pages.flatMap((page) => page.items) ?? [];
-
- const sortedMembers = [...members].sort((a, b) => {
- if (a.type === "user" && a.id === currentUser?.id) return -1;
- if (b.type === "user" && b.id === currentUser?.id) return 1;
- if (a.type === "group" && b.type === "user") return -1;
- if (a.type === "user" && b.type === "group") return 1;
- return 0;
- });
-
- if (isLoading) {
- return (
-
-
-
- );
- }
-
- if (members.length === 0) {
- return null;
- }
-
- return (
- <>
-
-
- {t("People with access")}
-
- {canManage && members.length > 0 && (
-
- {t("Remove all")}
-
- )}
-
-
-
- {sortedMembers.map((member) => (
-
- ))}
-
-
-
- {isFetchingNextPage && (
-
-
-
- )}
-
- >
- );
-}
diff --git a/apps/client/src/ee/page-permission/components/page-permission-tab.tsx b/apps/client/src/ee/page-permission/components/page-permission-tab.tsx
deleted file mode 100644
index 93f9277c..00000000
--- a/apps/client/src/ee/page-permission/components/page-permission-tab.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-import { useState } from "react";
-import {
- Box,
- Button,
- Divider,
- Group,
- Paper,
- Select,
- Stack,
- Text,
- ThemeIcon,
-} from "@mantine/core";
-import { useTranslation } from "react-i18next";
-import { Link, useParams } from "react-router-dom";
-import { IconArrowRight, IconLock, IconShieldLock } from "@tabler/icons-react";
-import { MultiMemberSelect } from "@/features/space/components/multi-member-select";
-import {
- IPageRestrictionInfo,
- PagePermissionRole,
-} from "@/ee/page-permission/types/page-permission.types";
-import {
- useAddPagePermissionMutation,
- useRestrictPageMutation,
- useUnrestrictPageMutation,
-} from "@/ee/page-permission/queries/page-permission-query";
-import { pagePermissionRoleData } from "@/ee/page-permission/types/page-permission-role-data";
-import { GeneralAccessSelect } from "@/ee/page-permission";
-import { PagePermissionList } from "@/ee/page-permission";
-import classes from "./page-permission.module.css";
-import { buildPageUrl } from "@/features/page/page.utils";
-
-type PagePermissionTabProps = {
- pageId: string;
- restrictionInfo: IPageRestrictionInfo;
-};
-
-export function PagePermissionTab({
- pageId,
- restrictionInfo,
-}: PagePermissionTabProps) {
- const { t } = useTranslation();
- const { spaceSlug } = useParams();
- const [memberIds, setMemberIds] = useState([]);
- const [role, setRole] = useState(PagePermissionRole.WRITER);
-
- const restrictMutation = useRestrictPageMutation();
- const unrestrictMutation = useUnrestrictPageMutation();
- const addPermissionMutation = useAddPagePermissionMutation();
-
- const hasInheritedRestriction = restrictionInfo.hasInheritedRestriction;
- const hasDirectRestriction = restrictionInfo.hasDirectRestriction;
- const canManage = restrictionInfo.userAccess.canManage;
-
- const handleDirectAccessChange = async (value: "open" | "restricted") => {
- if (value === "restricted" && !hasDirectRestriction) {
- await restrictMutation.mutateAsync(pageId);
- } else if (value === "open" && hasDirectRestriction) {
- await unrestrictMutation.mutateAsync(pageId);
- }
- };
-
- const handleAddMembers = async () => {
- if (memberIds.length === 0) return;
-
- const userIds = memberIds
- .filter((id) => id.startsWith("user-"))
- .map((id) => id.replace("user-", ""));
-
- const groupIds = memberIds
- .filter((id) => id.startsWith("group-"))
- .map((id) => id.replace("group-", ""));
-
- await addPermissionMutation.mutateAsync({
- pageId,
- role: role as PagePermissionRole,
- ...(userIds.length > 0 && { userIds }),
- ...(groupIds.length > 0 && { groupIds }),
- });
-
- setMemberIds([]);
- };
-
- const handleRemoveAll = async () => {
- await unrestrictMutation.mutateAsync(pageId);
- };
-
- return (
-
- {hasInheritedRestriction && (
-
-
-
-
-
-
-
- {t("Inherited restriction")}
-
-
-
- {t("Access limited by")}
-
- {restrictionInfo.inheritedFrom && (
-
-
-
- {restrictionInfo.inheritedFrom.title || t("Untitled")}
-
-
-
-
- )}
-
-
-
-
- )}
-
-
-
- {!hasDirectRestriction && !hasInheritedRestriction && (
-
- {t("Restrict access to control who can view and edit this page")}
-
- )}
- {!hasDirectRestriction && hasInheritedRestriction && (
-
- {t("Add additional restrictions specific to this page")}
-
- )}
-
-
- {hasDirectRestriction && (
- <>
-
-
- {canManage && (
-
-
-
-
- ({
- label: t(r.label),
- value: r.value,
- }))}
- value={role}
- onChange={(value) => value && setRole(value)}
- allowDeselect={false}
- variant="filled"
- w={120}
- />
-
- {t("Add")}
-
-
- )}
-
-
- >
- )}
-
- );
-}
diff --git a/apps/client/src/ee/page-permission/components/page-permission.module.css b/apps/client/src/ee/page-permission/components/page-permission.module.css
deleted file mode 100644
index 5c8b81b3..00000000
--- a/apps/client/src/ee/page-permission/components/page-permission.module.css
+++ /dev/null
@@ -1,128 +0,0 @@
-.generalAccessBox {
- display: flex;
- align-items: center;
- gap: var(--mantine-spacing-sm);
- padding: var(--mantine-spacing-xs) 0;
-}
-
-.generalAccessIcon {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 32px;
- height: 32px;
- border-radius: var(--mantine-radius-sm);
-
- @mixin light {
- background-color: var(--mantine-color-gray-1);
- }
- @mixin dark {
- background-color: var(--mantine-color-dark-5);
- }
-}
-
-.generalAccessIconRestricted {
- @mixin light {
- background-color: var(--mantine-color-red-0);
- color: var(--mantine-color-red-6);
- }
- @mixin dark {
- background-color: rgba(250, 82, 82, 0.1);
- color: var(--mantine-color-red-5);
- }
-}
-
-.permissionItem {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--mantine-spacing-xs) 0;
- gap: var(--mantine-spacing-sm);
-}
-
-.permissionItemInfo {
- display: flex;
- align-items: center;
- gap: var(--mantine-spacing-sm);
- flex: 1;
- min-width: 0;
- overflow: hidden;
-}
-
-.permissionItemDetails {
- min-width: 0;
- flex: 1;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
-}
-
-.permissionItemRole {
- flex-shrink: 0;
-}
-
-.avatarStack {
- display: flex;
- align-items: center;
-}
-
-.avatarStackItem {
- margin-left: -8px;
- border: 2px solid var(--mantine-color-body);
- border-radius: 50%;
-}
-
-.avatarStackItem:first-child {
- margin-left: 0;
-}
-
-.specificAccessHeader {
- display: flex;
- align-items: center;
- gap: var(--mantine-spacing-xs);
- margin-top: var(--mantine-spacing-md);
- margin-bottom: var(--mantine-spacing-xs);
-}
-
-.removeAllLink {
- cursor: pointer;
- font-size: var(--mantine-font-size-sm);
-
- @mixin light {
- color: var(--mantine-color-gray-6);
- }
- @mixin dark {
- color: var(--mantine-color-dark-2);
- }
-
- &:hover {
- text-decoration: underline;
- }
-}
-
-.inheritedInfo {
- display: flex;
- align-items: center;
- gap: var(--mantine-spacing-xs);
- padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
- border-radius: var(--mantine-radius-sm);
- margin-bottom: var(--mantine-spacing-sm);
-
- @mixin light {
- background-color: var(--mantine-color-gray-0);
- }
- @mixin dark {
- background-color: var(--mantine-color-dark-6);
- }
-}
-
-.inheritedSection {
- @mixin light {
- background-color: var(--mantine-color-orange-0);
- border: 1px solid var(--mantine-color-orange-2);
- }
- @mixin dark {
- background-color: rgba(255, 146, 43, 0.08);
- border: 1px solid rgba(255, 146, 43, 0.2);
- }
-}
diff --git a/apps/client/src/ee/page-permission/components/page-share-modal.tsx b/apps/client/src/ee/page-permission/components/page-share-modal.tsx
deleted file mode 100644
index 9b75f3ef..00000000
--- a/apps/client/src/ee/page-permission/components/page-share-modal.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { useState } from "react";
-import {
- Button,
- Indicator,
- Loader,
- Modal,
- Stack,
- Tabs,
- Text,
- Center,
-} from "@mantine/core";
-import { useDisclosure } from "@mantine/hooks";
-import { IconWorld, IconLock } from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import { useParams } from "react-router-dom";
-import { extractPageSlugId } from "@/lib";
-import { usePageQuery } from "@/features/page/queries/page-query";
-import { usePageRestrictionInfoQuery } from "@/ee/page-permission/queries/page-permission-query";
-import { PagePermissionTab } from "@/ee/page-permission";
-import { PublishTab } from "./publish-tab";
-import { useShareForPageQuery } from "@/features/share/queries/share-query";
-import { useHasFeature } from "@/ee/hooks/use-feature";
-import { Feature } from "@/ee/features";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
-import { useSpaceQuery } from "@/features/space/queries/space-query";
-
-type PageShareModalProps = {
- readOnly?: boolean;
-};
-
-export function PageShareModal({ readOnly }: PageShareModalProps) {
- const { t } = useTranslation();
- const { pageSlug, spaceSlug } = useParams();
- const pageSlugId = extractPageSlugId(pageSlug);
- const [opened, { open, close }] = useDisclosure(false);
- const hasPagePermissions = useHasFeature(Feature.PAGE_PERMISSIONS);
- const [activeTab, setActiveTab] = useState(
- hasPagePermissions ? "access" : "publish",
- );
-
- const [workspace] = useAtom(workspaceAtom);
- const { data: space } = useSpaceQuery(spaceSlug);
- const workspaceSharingDisabled = workspace?.settings?.sharing?.disabled === true;
- const spaceSharingDisabled = space?.settings?.sharing?.disabled === true;
-
- const { data: page } = usePageQuery({ pageId: pageSlugId });
- const pageId = page?.id;
- const isRestricted = page?.permissions?.hasRestriction ?? false;
-
- const { data: share } = useShareForPageQuery(pageId);
- const isPubliclyShared = !!share;
-
- const { data: restrictionInfo, isLoading: restrictionLoading } =
- usePageRestrictionInfoQuery(opened && hasPagePermissions ? pageId : undefined);
-
- return (
- <>
-
-
-
- ) : isPubliclyShared ? (
-
-
-
- ) : null
- }
- variant="default"
- onClick={() => {
- setActiveTab(isPubliclyShared ? "publish" : hasPagePermissions ? "access" : "publish");
- open();
- }}
- >
- {t("Share")}
-
-
-
-
-
- {t("Access")}
-
- ) : null
- }
- >
- {t("Publish")}
-
-
-
-
- {!hasPagePermissions ? (
-
-
-
- {t("Page permissions")}
-
-
- {t(
- "Control who can view and edit individual pages. Available with an enterprise license.",
- )}
-
-
- ) : restrictionLoading || !pageId || !restrictionInfo ? (
-
-
-
- ) : (
-
- )}
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/apps/client/src/ee/page-permission/components/publish-tab.tsx b/apps/client/src/ee/page-permission/components/publish-tab.tsx
deleted file mode 100644
index 5c14fa59..00000000
--- a/apps/client/src/ee/page-permission/components/publish-tab.tsx
+++ /dev/null
@@ -1,254 +0,0 @@
-import { useEffect, useMemo, useState } from "react";
-import {
- ActionIcon,
- Anchor,
- Button,
- Group,
- Stack,
- Switch,
- Text,
- TextInput,
-} from "@mantine/core";
-import { IconExternalLink, IconLock } from "@tabler/icons-react";
-import { Link, useNavigate, useParams } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-import { getPageIcon } from "@/lib";
-import CopyTextButton from "@/components/common/copy";
-import { getAppUrl, isCloud } from "@/lib/config";
-import { buildPageUrl } from "@/features/page/page.utils";
-import {
- useCreateShareMutation,
- useDeleteShareMutation,
- useShareForPageQuery,
- useUpdateShareMutation,
-} from "@/features/share/queries/share-query";
-import useTrial from "@/ee/hooks/use-trial";
-
-type PublishTabProps = {
- pageId: string;
- readOnly?: boolean;
- isRestricted?: boolean;
- workspaceSharingDisabled?: boolean;
- spaceSharingDisabled?: boolean;
-};
-
-export function PublishTab({ pageId, readOnly, isRestricted, workspaceSharingDisabled, spaceSharingDisabled }: PublishTabProps) {
- const { t } = useTranslation();
- const navigate = useNavigate();
- const { pageSlug, spaceSlug } = useParams();
- const { isTrial } = useTrial();
-
- const { data: share } = useShareForPageQuery(pageId);
- const createShareMutation = useCreateShareMutation();
- const updateShareMutation = useUpdateShareMutation();
- const deleteShareMutation = useDeleteShareMutation();
-
- const pageIsShared = share && share.level === 0;
- const isDescendantShared = share && share.level > 0;
-
- const publicLink = `${getAppUrl()}/share/${share?.key}/p/${pageSlug}`;
-
- const [isPagePublic, setIsPagePublic] = useState(false);
-
- useEffect(() => {
- setIsPagePublic(!!share);
- }, [share, pageId]);
-
- const handleChange = async (event: React.ChangeEvent) => {
- const value = event.currentTarget.checked;
-
- if (value) {
- createShareMutation.mutateAsync({
- pageId: pageId,
- includeSubPages: true,
- searchIndexing: false,
- });
- 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],
- );
-
- if (isCloud() && isTrial) {
- return (
-
-
-
- {t("Upgrade to share pages")}
-
-
- {t(
- "Page sharing is available on paid plans. Upgrade to share your pages publicly.",
- )}
-
- navigate("/settings/billing")}>
- {t("Upgrade Plan")}
-
-
- );
- }
-
- if (workspaceSharingDisabled || spaceSharingDisabled) {
- return (
-
-
-
- {t("Public sharing is disabled")}
-
-
- {workspaceSharingDisabled
- ? t("Public sharing has been disabled at the workspace level.")
- : t("Public sharing has been disabled for this space.")}
-
-
- );
- }
-
- if (isRestricted) {
- return (
-
-
-
- {t("Restricted page")}
-
-
- {t("Restricted pages cannot be shared publicly.")}
-
-
- );
- }
-
- if (isDescendantShared) {
- return (
-
- {t("Inherits public sharing from")}
-
-
- {getPageIcon(share.sharedPage.icon)}
-
- {share.sharedPage.title || t("untitled")}
-
-
-
- {shareLink}
-
- );
- }
-
- return (
-
-
-
-
- {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/ee/page-permission/hooks/use-page-permission.ts b/apps/client/src/ee/page-permission/hooks/use-page-permission.ts
deleted file mode 100644
index deaa8aea..00000000
--- a/apps/client/src/ee/page-permission/hooks/use-page-permission.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { useSpaceAbility } from "@/features/space/permissions/use-space-ability";
-import {
- SpaceCaslAction,
- SpaceCaslSubject,
-} from "@/features/space/permissions/permissions.type";
-import { usePageRestrictionInfoQuery } from "@/ee/page-permission/queries/page-permission-query";
-
-export function usePagePermission(pageId: string, spaceRules: any) {
- const spaceAbility = useSpaceAbility(spaceRules);
- const { data: restrictionInfo, isLoading } =
- usePageRestrictionInfoQuery(pageId);
-
- if (isLoading || !restrictionInfo) {
- return { canEdit: false, restrictionInfo: undefined };
- }
-
- const hasRestriction =
- restrictionInfo.hasDirectRestriction ||
- restrictionInfo.hasInheritedRestriction;
-
- const canEdit = hasRestriction
- ? (restrictionInfo.userAccess?.canEdit ?? false)
- : spaceAbility.can(SpaceCaslAction.Manage, SpaceCaslSubject.Page);
-
- return { canEdit, restrictionInfo };
-}
diff --git a/apps/client/src/ee/page-permission/index.ts b/apps/client/src/ee/page-permission/index.ts
deleted file mode 100644
index 4555ce54..00000000
--- a/apps/client/src/ee/page-permission/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export * from "./components/page-share-modal";
-export * from "./components/page-permission-tab";
-export * from "./components/publish-tab";
-export * from "./components/page-permission-list";
-export * from "./components/page-permission-item";
-export * from "./components/general-access-select";
-export * from "./hooks/use-page-permission";
-export * from "./queries/page-permission-query";
-export * from "./services/page-permission-service";
-export * from "./types/page-permission.types";
-export * from "./types/page-permission-role-data";
diff --git a/apps/client/src/ee/page-permission/queries/page-permission-query.ts b/apps/client/src/ee/page-permission/queries/page-permission-query.ts
deleted file mode 100644
index 69f5bc29..00000000
--- a/apps/client/src/ee/page-permission/queries/page-permission-query.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import {
- keepPreviousData,
- useInfiniteQuery,
- useMutation,
- useQuery,
- useQueryClient,
- UseQueryResult,
-} from "@tanstack/react-query";
-import {
- IAddPagePermission,
- IPageRestrictionInfo,
- IRemovePagePermission,
- IUpdatePagePermissionRole,
-} from "@/ee/page-permission/types/page-permission.types";
-import {
- addPagePermission,
- getPagePermissions,
- getPageRestrictionInfo,
- removePagePermission,
- restrictPage,
- unrestrictPage,
- updatePagePermissionRole,
-} from "@/ee/page-permission/services/page-permission-service";
-import { IPage } from "@/features/page/types/page.types";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-
-export function usePageRestrictionInfoQuery(
- pageId: string | undefined,
-): UseQueryResult {
- return useQuery({
- queryKey: ["page-restriction-info", pageId],
- queryFn: () => getPageRestrictionInfo(pageId),
- enabled: !!pageId,
- });
-}
-
-export function usePagePermissionsQuery(pageId: string) {
- return useInfiniteQuery({
- queryKey: ["page-permissions", pageId],
- queryFn: ({ pageParam }) => getPagePermissions(pageId, pageParam),
- enabled: !!pageId,
- //gcTime: 5000,
- placeholderData: keepPreviousData,
- initialPageParam: undefined as string | undefined,
- getNextPageParam: (lastPage) =>
- lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined,
- });
-}
-
-function updatePageRestrictionCache(
- queryClient: ReturnType,
- pageId: string,
- hasRestriction: boolean,
-) {
- queryClient.setQueriesData(
- { queryKey: ["pages"] },
- (old) => {
- if (old?.id === pageId) {
- return {
- ...old,
- permissions: { ...old.permissions, hasRestriction },
- };
- }
- return old;
- },
- );
- queryClient.invalidateQueries({
- queryKey: ["page-restriction-info", pageId],
- });
- queryClient.removeQueries({
- queryKey: ["page-permissions", pageId],
- });
-}
-
-export function useRestrictPageMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (pageId) => restrictPage(pageId),
- onSuccess: (_, pageId) => {
- updatePageRestrictionCache(queryClient, pageId, true);
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to restrict page"),
- color: "red",
- });
- },
- });
-}
-
-export function useUnrestrictPageMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (pageId) => unrestrictPage(pageId),
- onSuccess: (_, pageId) => {
- updatePageRestrictionCache(queryClient, pageId, false);
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to remove page restriction"),
- color: "red",
- });
- },
- });
-}
-
-export function useAddPagePermissionMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => addPagePermission(data),
- onSuccess: (_, variables) => {
- queryClient.invalidateQueries({
- queryKey: ["page-permissions", variables.pageId],
- });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to add permission"),
- color: "red",
- });
- },
- });
-}
-
-export function useRemovePagePermissionMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => removePagePermission(data),
- onSuccess: (_, variables) => {
- queryClient.invalidateQueries({
- queryKey: ["page-permissions", variables.pageId],
- });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to remove permission"),
- color: "red",
- });
- },
- });
-}
-
-export function useUpdatePagePermissionRoleMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => updatePagePermissionRole(data),
- onSuccess: (_, variables) => {
- queryClient.refetchQueries({
- queryKey: ["page-permissions", variables.pageId],
- });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to update permission"),
- color: "red",
- });
- },
- });
-}
diff --git a/apps/client/src/ee/page-permission/services/page-permission-service.ts b/apps/client/src/ee/page-permission/services/page-permission-service.ts
deleted file mode 100644
index be4bd57c..00000000
--- a/apps/client/src/ee/page-permission/services/page-permission-service.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import api from "@/lib/api-client";
-import { IPagination } from "@/lib/types";
-import {
- IAddPagePermission,
- IPagePermissionMember,
- IPageRestrictionInfo,
- IRemovePagePermission,
- IUpdatePagePermissionRole,
-} from "@/ee/page-permission/types/page-permission.types";
-
-export async function restrictPage(pageId: string): Promise {
- await api.post("/pages/restrict", { pageId });
-}
-
-export async function addPagePermission(
- data: IAddPagePermission,
-): Promise {
- await api.post("/pages/add-permission", data);
-}
-
-export async function removePagePermission(
- data: IRemovePagePermission,
-): Promise {
- await api.post("/pages/remove-permission", data);
-}
-
-export async function updatePagePermissionRole(
- data: IUpdatePagePermissionRole,
-): Promise {
- await api.post("/pages/update-permission", data);
-}
-
-export async function unrestrictPage(pageId: string): Promise {
- await api.post("/pages/remove-restriction", { pageId });
-}
-
-export async function getPagePermissions(
- pageId: string,
- cursor?: string,
-): Promise> {
- const req = await api.post>(
- "/pages/permissions",
- { pageId, ...(cursor && { cursor }) },
- );
- return req.data;
-}
-
-export async function getPageRestrictionInfo(
- pageId: string,
-): Promise {
- const req = await api.post("/pages/permission-info", {
- pageId,
- });
- return req.data;
-}
diff --git a/apps/client/src/ee/page-permission/types/page-permission-role-data.ts b/apps/client/src/ee/page-permission/types/page-permission-role-data.ts
deleted file mode 100644
index 057bc42a..00000000
--- a/apps/client/src/ee/page-permission/types/page-permission-role-data.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { IRoleData } from "@/lib/types";
-import { PagePermissionRole } from "./page-permission.types";
-
-export const pagePermissionRoleData: IRoleData[] = [
- {
- label: "Can edit",
- value: PagePermissionRole.WRITER,
- description: "Can edit page and manage access",
- },
- {
- label: "Can view",
- value: PagePermissionRole.READER,
- description: "Can only view page",
- },
-];
-
-export function getPagePermissionRoleLabel(value: string): string | undefined {
- const role = pagePermissionRoleData.find((item) => item.value === value);
- return role ? role.label : undefined;
-}
diff --git a/apps/client/src/ee/page-permission/types/page-permission.types.ts b/apps/client/src/ee/page-permission/types/page-permission.types.ts
deleted file mode 100644
index e4589269..00000000
--- a/apps/client/src/ee/page-permission/types/page-permission.types.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-export enum PagePermissionRole {
- READER = "reader",
- WRITER = "writer",
-}
-
-export type IAddPagePermission = {
- pageId: string;
- role: PagePermissionRole;
- userIds?: string[];
- groupIds?: string[];
-};
-
-export type IRemovePagePermission = {
- pageId: string;
- userIds?: string[];
- groupIds?: string[];
-};
-
-export type IUpdatePagePermissionRole = {
- pageId: string;
- role: PagePermissionRole;
- userId?: string;
- groupId?: string;
-};
-
-export type IPageRestrictionInfo = {
- restrictionId?: string;
- hasDirectRestriction: boolean;
- hasInheritedRestriction: boolean;
- inheritedFrom?: {
- id: string;
- slugId: string;
- title: string;
- };
- userAccess: {
- canView: boolean;
- canEdit: boolean;
- canManage: boolean;
- };
-};
-
-type IPagePermissionBase = {
- id: string;
- name: string;
- role: string;
- createdAt: string;
-};
-
-export type IPagePermissionUser = IPagePermissionBase & {
- type: "user";
- email: string;
- avatarUrl: string | null;
-};
-
-export type IPagePermissionGroup = IPagePermissionBase & {
- type: "group";
- memberCount: number;
- isDefault: boolean;
-};
-
-export type IPagePermissionMember = IPagePermissionUser | IPagePermissionGroup;
diff --git a/apps/client/src/ee/page-verification/components/expiration-fields.tsx b/apps/client/src/ee/page-verification/components/expiration-fields.tsx
deleted file mode 100644
index ad2102f9..00000000
--- a/apps/client/src/ee/page-verification/components/expiration-fields.tsx
+++ /dev/null
@@ -1,233 +0,0 @@
-import { Group, NumberInput, Select, Text } from "@mantine/core";
-import { DateInput } from "@mantine/dates";
-import { useTranslation } from "react-i18next";
-import i18n from "@/i18n.ts";
-import {
- ExpirationMode,
- PeriodUnit,
-} from "@/ee/page-verification/types/page-verification.types";
-
-export const PERIOD_UNIT_DAYS: Record = {
- day: 1,
- week: 7,
- month: 30,
- year: 365,
-};
-
-export const PERIOD_UNIT_MAX_AMOUNT: Record = {
- day: 3650,
- week: 520,
- month: 120,
- year: 20,
-};
-
-export const PERIOD_AMOUNT_MIN = 1;
-
-export function addDays(days: number, from?: Date): Date {
- const date = from ? new Date(from) : new Date();
- date.setDate(date.getDate() + days);
- return date;
-}
-
-function formatShortDate(date: Date): string {
- const crossesYear = date.getFullYear() !== new Date().getFullYear();
- return date.toLocaleDateString(i18n.language, {
- month: "short",
- day: "numeric",
- ...(crossesYear && { year: "numeric" }),
- });
-}
-
-function formatLongDate(date: Date): string {
- return date.toLocaleDateString(i18n.language, {
- month: "long",
- day: "numeric",
- year: "numeric",
- });
-}
-
-export function toLocalDateString(input: Date | string): string {
- const d = typeof input === "string" ? new Date(input) : input;
- const year = d.getFullYear();
- const month = String(d.getMonth() + 1).padStart(2, "0");
- const day = String(d.getDate()).padStart(2, "0");
- return `${year}-${month}-${day}`;
-}
-
-function pluralizeUnit(
- unit: PeriodUnit,
- amount: number,
- t: (key: string) => string,
-): string {
- switch (unit) {
- case "day":
- return amount === 1 ? t("day") : t("days");
- case "week":
- return amount === 1 ? t("week") : t("weeks");
- case "month":
- return amount === 1 ? t("month") : t("months");
- case "year":
- return amount === 1 ? t("year") : t("years");
- }
-}
-
-function buildModeOptions(
- t: (key: string) => string,
-): { value: ExpirationMode; label: string }[] {
- return [
- { value: "period", label: t("Period") },
- { value: "fixed", label: t("Fixed date") },
- { value: "indefinite", label: t("Indefinitely") },
- ];
-}
-
-function buildUnitOptions(
- t: (key: string) => string,
-): { value: PeriodUnit; label: string }[] {
- return [
- { value: "day", label: t("Days") },
- { value: "week", label: t("Weeks") },
- { value: "month", label: t("Months") },
- { value: "year", label: t("Years") },
- ];
-}
-
-type ExpirationFieldsProps = {
- mode: ExpirationMode;
- periodAmount: number;
- periodUnit: PeriodUnit;
- fixedDate: string;
- onModeChange: (mode: ExpirationMode) => void;
- onPeriodAmountChange: (amount: number) => void;
- onPeriodUnitChange: (unit: PeriodUnit) => void;
- onFixedDateChange: (iso: string) => void;
- baseDate?: Date;
-};
-
-export function ExpirationFields({
- mode,
- periodAmount,
- periodUnit,
- fixedDate,
- onModeChange,
- onPeriodAmountChange,
- onPeriodUnitChange,
- onFixedDateChange,
- baseDate,
-}: ExpirationFieldsProps) {
- const { t } = useTranslation();
- const modeOptions = buildModeOptions(t);
- const unitOptions = buildUnitOptions(t);
-
- const unitMax = PERIOD_UNIT_MAX_AMOUNT[periodUnit];
-
- const handleUnitChange = (nextUnit: PeriodUnit) => {
- const nextMax = PERIOD_UNIT_MAX_AMOUNT[nextUnit];
- if (periodAmount > nextMax) {
- onPeriodAmountChange(nextMax);
- }
- onPeriodUnitChange(nextUnit);
- };
-
- const amountValid =
- Number.isInteger(periodAmount) &&
- periodAmount >= PERIOD_AMOUNT_MIN &&
- periodAmount <= unitMax;
-
- const nextDueDate =
- mode === "period" && amountValid
- ? addDays(periodAmount * PERIOD_UNIT_DAYS[periodUnit], baseDate)
- : null;
-
- const fixedDateObj = fixedDate ? new Date(fixedDate) : null;
-
- let helperText: string | null = null;
- let helperError = false;
- if (mode === "period" && !amountValid) {
- helperText = t("Maximum is {{max}} {{unit}} for this unit", {
- max: unitMax,
- unit: pluralizeUnit(periodUnit, unitMax, t),
- });
- helperError = true;
- } else if (mode === "period" && nextDueDate && amountValid) {
- helperText = t(
- "Re-verifies every {{amount}} {{unit}} · Next due {{date}}",
- {
- amount: periodAmount,
- unit: pluralizeUnit(periodUnit, periodAmount, t),
- date: formatShortDate(nextDueDate),
- },
- );
- } else if (mode === "fixed" && fixedDateObj) {
- helperText = t(
- "Expires on {{date}}. Re-verifying won't change the deadline.",
- { date: formatLongDate(fixedDateObj) },
- );
- } else if (mode === "indefinite") {
- helperText = t("Never expires. Verifiers can re-verify at any time.");
- }
-
- return (
-
-
- val && onModeChange(val as ExpirationMode)}
- variant="filled"
- allowDeselect={false}
- style={{ flex: "1 1 140px", minWidth: 140 }}
- />
-
- {mode === "period" && (
-
- {
- const n =
- typeof val === "number" ? val : parseInt(String(val), 10);
- if (!Number.isNaN(n)) onPeriodAmountChange(n);
- }}
- min={PERIOD_AMOUNT_MIN}
- max={unitMax}
- clampBehavior="blur"
- variant="filled"
- style={{ flex: "0 0 80px" }}
- hideControls
- />
- val && handleUnitChange(val as PeriodUnit)}
- variant="filled"
- allowDeselect={false}
- style={{ flex: 1, minWidth: 120 }}
- />
-
- )}
-
- {mode === "fixed" && (
- onFixedDateChange(val ?? "")}
- placeholder={t("Pick a date")}
- variant="filled"
- minDate={addDays(1)}
- clearable
- style={{ flex: "1 1 200px", minWidth: 180 }}
- />
- )}
-
-
- {helperText && (
-
- {helperText}
-
- )}
-
- );
-}
diff --git a/apps/client/src/ee/page-verification/components/manage-verification-form.tsx b/apps/client/src/ee/page-verification/components/manage-verification-form.tsx
deleted file mode 100644
index 9d5214f7..00000000
--- a/apps/client/src/ee/page-verification/components/manage-verification-form.tsx
+++ /dev/null
@@ -1,637 +0,0 @@
-import { useState } from "react";
-import {
- Button,
- Center,
- Checkbox,
- Divider,
- Group,
- Loader,
- Stack,
- Text,
- Textarea,
-} from "@mantine/core";
-import { modals } from "@mantine/modals";
-import { useTranslation } from "react-i18next";
-import i18n from "@/i18n.ts";
-import {
- useMarkObsoleteMutation,
- usePageVerificationInfoQuery,
- useRejectApprovalMutation,
- useRemoveVerificationMutation,
- useSubmitForApprovalMutation,
- useUpdateVerificationMutation,
- useVerifyPageMutation,
-} from "@/ee/page-verification/queries/page-verification-query";
-import {
- ExpirationMode,
- IPageVerificationInfo,
- PeriodUnit,
-} from "@/ee/page-verification/types/page-verification.types";
-import { useTimeAgo } from "@/hooks/use-time-ago";
-import { VerifierList } from "./verifier-list";
-import {
- ExpirationFields,
- PERIOD_AMOUNT_MIN,
- PERIOD_UNIT_MAX_AMOUNT,
- toLocalDateString,
-} from "./expiration-fields";
-import { VerifierPicker } from "./verifier-picker";
-import { MAX_VERIFIERS } from "./user-option";
-
-type ManageVerificationFormProps = {
- pageId: string;
- onClose: () => void;
-};
-
-export function ManageVerificationForm({
- pageId,
- onClose,
-}: ManageVerificationFormProps) {
- const { data: info, isLoading } = usePageVerificationInfoQuery(pageId);
-
- if (isLoading || !info) {
- return (
-
-
-
- );
- }
-
- if (info.type === "qms") {
- return ;
- }
-
- return (
-
- );
-}
-
-type ManageContentProps = {
- pageId: string;
- info: IPageVerificationInfo;
- onClose: () => void;
-};
-
-function ExpiringManageContent({ pageId, info, onClose }: ManageContentProps) {
- const { t } = useTranslation();
- const verifyMutation = useVerifyPageMutation();
- const removeMutation = useRemoveVerificationMutation();
- const updateMutation = useUpdateVerificationMutation();
- const [confirmed, setConfirmed] = useState(false);
-
- const initialMode: ExpirationMode = (info.mode as ExpirationMode) ?? "period";
- const initialPeriodAmount = info.periodAmount ?? 1;
- const initialPeriodUnit: PeriodUnit =
- (info.periodUnit as PeriodUnit) ?? "month";
- const initialFixedDate =
- initialMode === "fixed" && info.expiresAt
- ? toLocalDateString(info.expiresAt)
- : "";
-
- const [mode, setMode] = useState(initialMode);
- const [periodAmount, setPeriodAmount] = useState(initialPeriodAmount);
- const [periodUnit, setPeriodUnit] = useState(initialPeriodUnit);
- const [fixedDate, setFixedDate] = useState(initialFixedDate);
-
- const verifiedAtAgo = useTimeAgo(info.verifiedAt ?? new Date().toISOString());
-
- const hasExpirationChange =
- mode !== initialMode ||
- (mode === "period" &&
- (periodAmount !== initialPeriodAmount ||
- periodUnit !== initialPeriodUnit)) ||
- (mode === "fixed" && fixedDate !== initialFixedDate);
-
- const periodValid =
- mode !== "period" ||
- (Number.isInteger(periodAmount) &&
- periodAmount >= PERIOD_AMOUNT_MIN &&
- periodAmount <= PERIOD_UNIT_MAX_AMOUNT[periodUnit]);
- const fixedDateValid =
- mode !== "fixed" ||
- (!!fixedDate && new Date(fixedDate).getTime() > Date.now());
- const canSaveExpiration = hasExpirationChange && periodValid && fixedDateValid;
-
- const storedFixedExpired =
- info.mode === "fixed" &&
- !!info.expiresAt &&
- new Date(info.expiresAt).getTime() <= Date.now();
-
- const existingVerifierIds = info.verifiers?.map((v) => v.id) ?? [];
-
- const handleVerify = () => {
- verifyMutation.mutate(pageId, {
- onSuccess: () => {
- setConfirmed(false);
- onClose();
- },
- });
- };
-
- const handleRemove = () => {
- modals.openConfirmModal({
- title: t("Remove verification"),
- children: (
-
- {t("Are you sure you want to remove verification from this page?")}
-
- ),
- labels: { confirm: t("Remove"), cancel: t("Cancel") },
- confirmProps: { color: "red" },
- onConfirm: () => removeMutation.mutate(pageId, { onSuccess: onClose }),
- });
- };
-
- const handleSaveExpiration = () => {
- if (!canSaveExpiration) return;
- updateMutation.mutate({
- pageId,
- mode,
- ...(mode === "period" && {
- periodAmount,
- periodUnit,
- }),
- ...(mode === "fixed" &&
- fixedDate && {
- fixedExpiresAt: new Date(fixedDate).toISOString(),
- }),
- });
- };
-
- const handleRemoveVerifier = (userId: string) => {
- if (!info.verifiers) return;
- const remaining = info.verifiers
- .filter((v) => v.id !== userId)
- .map((v) => v.id);
- updateMutation.mutate({ pageId, verifierIds: remaining });
- };
-
- const handleAddVerifier = (userId: string) => {
- if (!info.verifiers) return;
- if (info.verifiers.some((v) => v.id === userId)) return;
- const verifierIds = [...info.verifiers.map((v) => v.id), userId];
- updateMutation.mutate({ pageId, verifierIds });
- };
-
- const status = info.status;
-
- return (
-
-
- {t("Assigned verifiers must periodically re-verify this page.")}
-
-
- {info.verifiedBy && (
-
-
-
- {status === "expired"
- ? t("Last verified by {{name}} {{time}} (expired)", {
- name: info.verifiedBy.name,
- time: verifiedAtAgo,
- })
- : t("Verified by {{name}} {{time}}", {
- name: info.verifiedBy.name,
- time: verifiedAtAgo,
- })}
-
- {info.expiresAt && (
-
- {t(status === "expired" ? "Expired {{date}}" : "Expires {{date}}", {
- date: new Date(info.expiresAt).toLocaleDateString(
- i18n.language,
- {
- month: "long",
- day: "numeric",
- year: "numeric",
- },
- ),
- })}
-
- )}
-
-
- )}
-
-
-
- {info.verifiers && info.verifiers.length > 0 && (
- <>
-
-
- {t("Verifiers")}
-
-
- {info.permissions?.canManage &&
- info.verifiers.length < MAX_VERIFIERS && (
-
- handleAddVerifier(user.value)}
- />
-
- )}
-
-
- >
- )}
-
- {info.permissions?.canManage && (
- <>
-
-
- {t("Expiration")}
-
-
- {hasExpirationChange && (
-
- {t("Save")}
-
- )}
-
-
- >
- )}
-
- {info.permissions?.canVerify && (
-
-
- {t("Confirm")}
-
- setConfirmed(event.currentTarget.checked)}
- color="dark"
- />
- {storedFixedExpired && (
-
- {t("The fixed expiration date has passed.")}
-
- )}
-
- )}
-
-
- {info.permissions?.canManage && (
-
- {t("Remove verification")}
-
- )}
-
- {info.permissions?.canVerify && (
-
- {t("Verify")}
-
- )}
-
-
- );
-}
-
-function QmsManageContent({ pageId, info, onClose }: ManageContentProps) {
- const { t } = useTranslation();
- const verifyMutation = useVerifyPageMutation();
- const submitMutation = useSubmitForApprovalMutation();
- const rejectMutation = useRejectApprovalMutation();
- const obsoleteMutation = useMarkObsoleteMutation();
- const removeMutation = useRemoveVerificationMutation();
- const updateMutation = useUpdateVerificationMutation();
- const [confirmed, setConfirmed] = useState(false);
- const [showRejectForm, setShowRejectForm] = useState(false);
- const [rejectComment, setRejectComment] = useState("");
- const verifiedAtAgo = useTimeAgo(info.verifiedAt ?? new Date().toISOString());
- const requestedAtAgo = useTimeAgo(
- info.requestedAt ?? new Date().toISOString(),
- );
- const rejectedAtAgo = useTimeAgo(info.rejectedAt ?? new Date().toISOString());
-
- const status = info.status;
-
- const existingVerifierIds = info.verifiers?.map((v) => v.id) ?? [];
-
- const handleSubmitForApproval = () => {
- submitMutation.mutate(pageId, { onSuccess: onClose });
- };
-
- const handleVerify = () => {
- verifyMutation.mutate(pageId, {
- onSuccess: () => {
- setConfirmed(false);
- onClose();
- },
- });
- };
-
- const handleReject = () => {
- rejectMutation.mutate(
- { pageId, comment: rejectComment || undefined },
- {
- onSuccess: () => {
- setShowRejectForm(false);
- setRejectComment("");
- onClose();
- },
- },
- );
- };
-
- const handleMarkObsolete = () => {
- modals.openConfirmModal({
- title: t("Mark as obsolete"),
- children: (
-
-
- {t(
- "Are you sure you want to mark this page as obsolete? This action cannot be undone.",
- )}
-
-
- {t(
- "To restore this page, you will need to remove verification and set it up again.",
- )}
-
-
- ),
- labels: { confirm: t("Mark obsolete"), cancel: t("Cancel") },
- confirmProps: { color: "red" },
- onConfirm: () =>
- obsoleteMutation.mutate(pageId, { onSuccess: onClose }),
- });
- };
-
- const handleRemove = () => {
- modals.openConfirmModal({
- title: t("Remove verification"),
- children: (
-
- {t("Are you sure you want to remove verification from this page?")}
-
- ),
- labels: { confirm: t("Remove"), cancel: t("Cancel") },
- confirmProps: { color: "red" },
- onConfirm: () => removeMutation.mutate(pageId, { onSuccess: onClose }),
- });
- };
-
- const handleRemoveVerifier = (userId: string) => {
- if (!info.verifiers) return;
- const remaining = info.verifiers
- .filter((v) => v.id !== userId)
- .map((v) => v.id);
- updateMutation.mutate({ pageId, verifierIds: remaining });
- };
-
- const handleAddVerifier = (userId: string) => {
- if (!info.verifiers) return;
- if (info.verifiers.some((v) => v.id === userId)) return;
- const verifierIds = [...info.verifiers.map((v) => v.id), userId];
- updateMutation.mutate({ pageId, verifierIds });
- };
-
- const canManageVerifiers =
- info.permissions?.canManage && status !== "obsolete";
-
- return (
-
-
- {t("Pages move through draft, approval, and approved stages.")}
-
-
- {status === "draft" && (
- <>
- {info.rejectedBy && info.rejectedAt && (
-
-
- {t("Returned by {{name}} {{time}}", {
- name: info.rejectedBy.name,
- time: rejectedAtAgo,
- })}
-
- {info.rejectionComment && (
-
- “{info.rejectionComment}”
-
- )}
-
- )}
- {!info.rejectedBy && (
- {t("No approval has been requested yet.")}
- )}
- >
- )}
-
- {status === "in_approval" && (
-
-
- {t("Submitted by {{name}} {{time}}", {
- name: info.requestedBy?.name ?? t("Someone"),
- time: requestedAtAgo,
- })}
-
-
- )}
-
- {status === "approved" && info.verifiedBy && (
-
-
- {t("Approved by {{name}} {{time}}", {
- name: info.verifiedBy.name,
- time: verifiedAtAgo,
- })}
-
-
- )}
-
- {status === "obsolete" && (
-
- {t("This document has been marked as obsolete.")}
-
- )}
-
-
-
- {info.verifiers && info.verifiers.length > 0 && (
- <>
-
-
- {t("Verifiers")}
-
-
- {canManageVerifiers && info.verifiers.length < MAX_VERIFIERS && (
-
- handleAddVerifier(user.value)}
- />
-
- )}
-
-
- >
- )}
-
- {status === "in_approval" && info.permissions?.canVerify && (
- <>
- {showRejectForm ? (
-
-
- {t("Rejection comment")}
-
- setRejectComment(e.currentTarget.value)}
- placeholder={t("Reason for returning this document...")}
- minRows={2}
- variant="filled"
- maxLength={500}
- />
-
- {
- setShowRejectForm(false);
- setRejectComment("");
- }}
- >
- {t("Cancel")}
-
-
- {t("Confirm rejection")}
-
-
-
- ) : (
-
- setConfirmed(event.currentTarget.checked)}
- color="dark"
- />
-
- )}
- >
- )}
-
-
- {info.permissions?.canManage && (
-
- {t("Remove verification")}
-
- )}
-
-
- {status === "draft" && info.permissions?.canSubmitForApproval && (
-
- {t("Submit for approval")}
-
- )}
-
- {status === "in_approval" &&
- info.permissions?.canVerify &&
- !showRejectForm && (
- <>
- setShowRejectForm(true)}
- >
- {t("Reject")}
-
-
- {t("Approve")}
-
- >
- )}
-
- {status === "approved" && (
- <>
- {info.permissions?.canSubmitForApproval && (
-
- {t("Re-submit for approval")}
-
- )}
- {info.permissions?.canMarkObsolete && (
-
- {t("Mark obsolete")}
-
- )}
- >
- )}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/page-verification/components/page-verification-modal.module.css b/apps/client/src/ee/page-verification/components/page-verification-modal.module.css
deleted file mode 100644
index 8cf4d285..00000000
--- a/apps/client/src/ee/page-verification/components/page-verification-modal.module.css
+++ /dev/null
@@ -1,278 +0,0 @@
-.chooser {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.subhead {
- font-size: 12px;
- line-height: 1.5;
- color: light-dark(
- var(--mantine-color-gray-6),
- var(--mantine-color-dark-2)
- );
- margin-bottom: 2px;
- max-width: 52ch;
-}
-
-.card {
- position: relative;
- display: block;
- width: 100%;
- padding: 14px 16px 12px;
- border: 1px solid
- light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
- border-radius: 10px;
- background: light-dark(
- var(--mantine-color-white),
- var(--mantine-color-dark-7)
- );
- cursor: pointer;
- text-align: left;
- overflow: hidden;
- transition:
- border-color 220ms cubic-bezier(0.16, 1, 0.3, 1),
- transform 220ms cubic-bezier(0.16, 1, 0.3, 1),
- box-shadow 220ms cubic-bezier(0.16, 1, 0.3, 1),
- background-color 220ms cubic-bezier(0.16, 1, 0.3, 1);
-}
-
-.card::before {
- content: "";
- position: absolute;
- inset: 0;
- background: radial-gradient(
- 120% 90% at 100% 0%,
- light-dark(rgba(15, 15, 20, 0.035), rgba(255, 255, 255, 0.04)),
- transparent 55%
- );
- opacity: 0;
- transition: opacity 260ms cubic-bezier(0.16, 1, 0.3, 1);
- pointer-events: none;
-}
-
-.card:hover {
- border-color: light-dark(
- var(--mantine-color-dark-9),
- var(--mantine-color-gray-3)
- );
- transform: translateY(-2px);
- box-shadow:
- 0 1px 0 0
- light-dark(
- rgba(15, 15, 20, 0.04),
- rgba(255, 255, 255, 0.04)
- ),
- 0 18px 36px -22px
- light-dark(rgba(15, 15, 20, 0.22), rgba(0, 0, 0, 0.6));
-}
-
-.card:hover::before {
- opacity: 1;
-}
-
-.card:focus-visible {
- outline: none;
- border-color: light-dark(
- var(--mantine-color-dark-9),
- var(--mantine-color-gray-3)
- );
- box-shadow: 0 0 0 3px
- light-dark(
- rgba(15, 15, 20, 0.08),
- rgba(255, 255, 255, 0.12)
- );
-}
-
-.titleRow {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 2px;
-}
-
-.iconStamp {
- width: 26px;
- height: 26px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 7px;
- background: light-dark(
- var(--mantine-color-gray-1),
- var(--mantine-color-dark-6)
- );
- color: light-dark(
- var(--mantine-color-dark-7),
- var(--mantine-color-gray-2)
- );
- transition:
- background-color 220ms cubic-bezier(0.16, 1, 0.3, 1),
- color 220ms cubic-bezier(0.16, 1, 0.3, 1),
- transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
- flex-shrink: 0;
-}
-
-.card:hover .iconStamp {
- background: light-dark(
- var(--mantine-color-dark-9),
- var(--mantine-color-gray-1)
- );
- color: light-dark(
- var(--mantine-color-gray-0),
- var(--mantine-color-dark-9)
- );
- transform: rotate(-4deg);
-}
-
-.title {
- font-size: 15px;
- font-weight: 600;
- letter-spacing: -0.01em;
- color: light-dark(
- var(--mantine-color-dark-9),
- var(--mantine-color-gray-0)
- );
- line-height: 1.25;
- margin: 0;
-}
-
-.description {
- font-size: 12px;
- color: light-dark(
- var(--mantine-color-gray-7),
- var(--mantine-color-dark-1)
- );
- margin: 0;
- line-height: 1.45;
- max-width: 52ch;
-}
-
-.rule {
- height: 1px;
- background: light-dark(
- var(--mantine-color-gray-2),
- var(--mantine-color-dark-5)
- );
- margin: 10px 0 8px;
-}
-
-.meta {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-bottom: 10px;
-}
-
-.metaItem {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 11.5px;
- color: light-dark(
- var(--mantine-color-gray-7),
- var(--mantine-color-dark-1)
- );
- line-height: 1.35;
-}
-
-.metaIcon {
- flex-shrink: 0;
- color: light-dark(
- var(--mantine-color-gray-5),
- var(--mantine-color-dark-2)
- );
-}
-
-.cardFooter {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding-top: 8px;
- border-top: 1px dashed
- light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
- gap: 12px;
-}
-
-.bestFor {
- font-size: 10.5px;
- color: light-dark(
- var(--mantine-color-gray-6),
- var(--mantine-color-dark-2)
- );
- font-style: italic;
- letter-spacing: 0.005em;
-}
-
-.arrow {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 22px;
- height: 22px;
- color: light-dark(
- var(--mantine-color-gray-5),
- var(--mantine-color-dark-2)
- );
- transition:
- transform 260ms cubic-bezier(0.16, 1, 0.3, 1),
- color 220ms cubic-bezier(0.16, 1, 0.3, 1);
-}
-
-.card:hover .arrow {
- transform: translateX(4px);
- color: light-dark(
- var(--mantine-color-dark-9),
- var(--mantine-color-gray-0)
- );
-}
-
-.backButton {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- font-weight: 500;
- color: light-dark(
- var(--mantine-color-gray-6),
- var(--mantine-color-dark-2)
- );
- background: none;
- border: none;
- cursor: pointer;
- padding: 4px 8px;
- margin-left: -8px;
- border-radius: 6px;
- transition:
- color 150ms ease,
- background-color 150ms ease;
-}
-
-.backButton:hover {
- color: light-dark(
- var(--mantine-color-dark-9),
- var(--mantine-color-gray-0)
- );
- background: light-dark(
- var(--mantine-color-gray-1),
- var(--mantine-color-dark-6)
- );
-}
-
-.configureHeader {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 4px;
-}
-
-.configureEyebrow {
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.14em;
- color: light-dark(
- var(--mantine-color-gray-6),
- var(--mantine-color-dark-2)
- );
-}
diff --git a/apps/client/src/ee/page-verification/components/page-verification-modal.tsx b/apps/client/src/ee/page-verification/components/page-verification-modal.tsx
deleted file mode 100644
index a27d3a29..00000000
--- a/apps/client/src/ee/page-verification/components/page-verification-modal.tsx
+++ /dev/null
@@ -1,221 +0,0 @@
-import {
- ActionIcon,
- Group,
- Menu,
- Modal,
- Text,
- Tooltip,
- UnstyledButton,
-} from "@mantine/core";
-import { useDisclosure } from "@mantine/hooks";
-import {
- IconRosetteDiscountCheckFilled,
- IconShieldCheck,
-} from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import i18n from "@/i18n.ts";
-import { useParams } from "react-router-dom";
-import { extractPageSlugId } from "@/lib";
-import { usePageQuery } from "@/features/page/queries/page-query";
-import { usePageVerificationInfoQuery } from "@/ee/page-verification/queries/page-verification-query";
-import { useHasFeature } from "@/ee/hooks/use-feature";
-import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
-import { Feature } from "@/ee/features";
-import { SetupVerificationForm } from "./setup-verification-form";
-import { ManageVerificationForm } from "./manage-verification-form";
-import { getStatusColor, getStatusLabel } from "./verification-status";
-
-type PageVerificationModalProps = {
- pageId: string;
- opened: boolean;
- onClose: () => void;
-};
-
-export function PageVerificationModal({
- pageId,
- opened,
- onClose,
-}: PageVerificationModalProps) {
- const { t } = useTranslation();
- const { data: verificationInfo } = usePageVerificationInfoQuery(
- opened ? pageId : undefined,
- );
-
- const status = verificationInfo?.status ?? "none";
-
- return (
-
-
-
- {status === "none" ? t("Set up verification") : t("Verify page")}
-
-
- }
- size={520}
- >
- {status === "none" ? (
-
- ) : (
-
- )}
-
- );
-}
-
-type PageVerificationBadgeProps = {
- readOnly?: boolean;
-};
-
-export function PageVerificationBadge({
- readOnly,
-}: PageVerificationBadgeProps) {
- const { t } = useTranslation();
- const { pageSlug } = useParams();
- const pageSlugId = extractPageSlugId(pageSlug);
- const hasVerificationFeature = useHasFeature(Feature.PAGE_VERIFICATION);
- const [opened, { open, close }] = useDisclosure(false);
-
- const { data: page } = usePageQuery({ pageId: pageSlugId });
- const pageId = page?.id;
-
- const { data: verificationInfo, isLoading } = usePageVerificationInfoQuery(
- hasVerificationFeature ? pageId : undefined,
- );
- const upgradeLabel = useUpgradeLabel();
-
- if (!pageId) return null;
- if (!hasVerificationFeature) {
- if (readOnly) return null;
- const lockedLabel = `${t("Add verification")} — ${upgradeLabel}`;
- // Use ActionIcon (a real ) instead of a ThemeIcon so the tooltip
- // is reachable on keyboard focus, and screen readers announce the upgrade
- // hint via the accessible name. Click is a no-op since the feature is
- // gated; the tooltip explains why.
- return (
-
-
-
-
-
- );
- }
- if (isLoading) return null;
-
- const status = verificationInfo?.status ?? "none";
-
- if (status === "none" && readOnly) return null;
-
- const tooltipLabel =
- status === "verified" && verificationInfo?.expiresAt
- ? t("Verified until {{date}}", {
- date: new Date(verificationInfo.expiresAt).toLocaleDateString(
- i18n.language,
- { month: "long", day: "numeric", year: "numeric" },
- ),
- })
- : getStatusLabel(status, t);
-
- return (
- <>
- {status !== "none" ? (
-
-
-
-
- {getStatusLabel(status, t)}
-
-
-
- ) : !readOnly ? (
-
-
-
-
-
- ) : null}
-
-
- >
- );
-}
-
-type PageVerificationMenuItemProps = {
- pageId?: string;
- onClick: () => void;
-};
-
-export function PageVerificationMenuItem({
- pageId,
- onClick,
-}: PageVerificationMenuItemProps) {
- const { t } = useTranslation();
- const hasVerificationFeature = useHasFeature(Feature.PAGE_VERIFICATION);
- const upgradeLabel = useUpgradeLabel();
-
- const { data: verificationInfo } = usePageVerificationInfoQuery(
- hasVerificationFeature ? pageId : undefined,
- );
-
- const hasVerification =
- !!verificationInfo && verificationInfo.status !== "none";
- const label = hasVerification
- ? t("Edit verification")
- : t("Add verification");
-
- const menuItem = (
- }
- onClick={hasVerificationFeature ? onClick : undefined}
- >
- {label}
-
- );
-
- if (!hasVerificationFeature) {
- return (
-
- {menuItem}
-
- );
- }
-
- return menuItem;
-}
diff --git a/apps/client/src/ee/page-verification/components/setup-verification-form.tsx b/apps/client/src/ee/page-verification/components/setup-verification-form.tsx
deleted file mode 100644
index b58b8048..00000000
--- a/apps/client/src/ee/page-verification/components/setup-verification-form.tsx
+++ /dev/null
@@ -1,335 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import {
- Button,
- Checkbox,
- Divider,
- Group,
- Stack,
- Text,
- UnstyledButton,
-} from "@mantine/core";
-import {
- IconArrowLeft,
- IconArrowRight,
- IconCertificate2,
- IconCheck,
- IconRefresh,
-} from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import { useAtom } from "jotai";
-import classes from "./page-verification-modal.module.css";
-import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
-import { useSetupVerificationMutation } from "@/ee/page-verification/queries/page-verification-query";
-import {
- ExpirationMode,
- PeriodUnit,
- VerificationType,
-} from "@/ee/page-verification/types/page-verification.types";
-import {
- ExpirationFields,
- PERIOD_AMOUNT_MIN,
- PERIOD_UNIT_MAX_AMOUNT,
-} from "./expiration-fields";
-import { VerifierPicker } from "./verifier-picker";
-import { VerifierList } from "./verifier-list";
-import { MAX_VERIFIERS, UserOptionItem } from "./user-option";
-
-type WorkflowChooserProps = {
- onSelect: (type: VerificationType) => void;
-};
-
-function WorkflowChooser({ onSelect }: WorkflowChooserProps) {
- const { t } = useTranslation();
-
- return (
-
-
- {t("Choose how this page should stay accurate.")}
-
-
-
-
onSelect("expiring" as VerificationType)}
- >
-
-
-
-
-
{t("Recurring verification")}
-
-
- {t("Verifiers re-confirm this page on a schedule.")}
-
-
-
-
-
-
-
- {t("Re-verify on a schedule (e.g every 30 days )")}
-
-
-
-
-
- {t("Best for runbooks, FAQs, living documentation")}
-
-
-
-
-
-
-
-
onSelect("qms" as VerificationType)}
- >
-
-
-
-
-
{t("Approval workflow")}
-
-
- {t("Formal document lifecycle with named approvers.")}
-
-
-
-
-
-
-
- {t("Draft → In approval → Approved → Obsolete")}
-
-
-
- {t("Designed for ISO 9001, ISO 13485, and FDA")}
-
-
-
-
-
- {t("Best for SOPs and controlled documents")}
-
-
-
-
-
-
-
-
- );
-}
-
-type SetupVerificationFormProps = {
- pageId: string;
- onClose: () => void;
-};
-
-export function SetupVerificationForm({
- pageId,
- onClose,
-}: SetupVerificationFormProps) {
- const { t } = useTranslation();
- const setupMutation = useSetupVerificationMutation();
- const [currentUser] = useAtom(currentUserAtom);
- const [type, setType] = useState(null);
- const [mode, setMode] = useState("period");
- const [periodAmount, setPeriodAmount] = useState(1);
- const [periodUnit, setPeriodUnit] = useState("month");
- const [fixedDate, setFixedDate] = useState("");
- const [confirmed, setConfirmed] = useState(false);
- const [selectedVerifiers, setSelectedVerifiers] = useState(
- [],
- );
- const didInitCurrentUser = useRef(false);
-
- useEffect(() => {
- if (!didInitCurrentUser.current && currentUser?.user) {
- didInitCurrentUser.current = true;
- const u = currentUser.user;
- setSelectedVerifiers([
- {
- value: u.id,
- label: u.name,
- email: u.email,
- avatarUrl: u.avatarUrl,
- },
- ]);
- }
- }, [currentUser]);
-
- const isQms = type === "qms";
- const canAddMore = selectedVerifiers.length < MAX_VERIFIERS;
-
- if (type === null) {
- return ;
- }
-
- const handleAddVerifier = (user: UserOptionItem) => {
- setSelectedVerifiers((prev) =>
- prev.some((v) => v.value === user.value) ? prev : [...prev, user],
- );
- };
-
- const handleRemoveVerifier = (userId: string) => {
- setSelectedVerifiers((prev) => prev.filter((v) => v.value !== userId));
- };
-
- const handleSetup = () => {
- if (selectedVerifiers.length === 0) return;
- setupMutation.mutate(
- {
- pageId,
- type,
- ...(!isQms && {
- mode,
- ...(mode === "period" && {
- periodAmount,
- periodUnit,
- }),
- ...(mode === "fixed" &&
- fixedDate && {
- fixedExpiresAt: new Date(fixedDate).toISOString(),
- }),
- }),
- verifierIds: selectedVerifiers.map((v) => v.value),
- },
- {
- onSuccess: () => {
- if (!isQms) {
- onClose();
- }
- },
- },
- );
- };
-
- const periodValid =
- mode !== "period" ||
- (Number.isInteger(periodAmount) &&
- periodAmount >= PERIOD_AMOUNT_MIN &&
- periodAmount <= PERIOD_UNIT_MAX_AMOUNT[periodUnit]);
- const fixedDateValid =
- mode !== "fixed" ||
- (!!fixedDate && new Date(fixedDate).getTime() > Date.now());
- const hasVerifiers = selectedVerifiers.length > 0;
-
- const canSubmit = isQms
- ? hasVerifiers
- : hasVerifiers && confirmed && periodValid && fixedDateValid;
-
- return (
-
-
-
setType(null)}
- >
-
- {t("Back")}
-
-
-
- {isQms ? (
-
- ) : (
-
- )}
-
-
-
- {isQms ? t("Quality management") : t("Recurring")}
-
-
- {isQms
- ? t("Pages move through draft, approval, and approved stages.")
- : t(
- "Assigned verifiers must periodically re-verify this page.",
- )}
-
-
-
-
-
-
-
- {t("Verifiers")}
-
- {selectedVerifiers.length > 0 && (
-
- ({
- id: v.value,
- name: v.label,
- email: v.email,
- avatarUrl: v.avatarUrl,
- }))}
- canManage
- onRemove={handleRemoveVerifier}
- />
-
- )}
- {canAddMore && (
-
v.value)}
- onSelect={handleAddVerifier}
- />
- )}
-
-
- {!isQms && (
- <>
-
-
-
-
- {t("Expiration")}
-
-
-
-
-
-
-
-
- {t("Confirm")}
-
- setConfirmed(event.currentTarget.checked)}
- color="dark"
- />
-
- >
- )}
-
-
-
- {isQms ? t("Set up") : t("Verify")}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/page-verification/components/user-option.tsx b/apps/client/src/ee/page-verification/components/user-option.tsx
deleted file mode 100644
index f4f16ba4..00000000
--- a/apps/client/src/ee/page-verification/components/user-option.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Group, SelectProps, Text } from "@mantine/core";
-import { CustomAvatar } from "@/components/ui/custom-avatar";
-import { IUser } from "@/features/user/types/user.types";
-
-export const MAX_VERIFIERS = 5;
-
-export type UserOptionItem = {
- value: string;
- label: string;
- email: string;
- avatarUrl: string;
-};
-
-export function toUserOptions(users: IUser[] | undefined): UserOptionItem[] {
- return (users ?? []).map((user) => ({
- value: user.id,
- label: user.name,
- email: user.email,
- avatarUrl: user.avatarUrl,
- }));
-}
-
-export const renderUserSelectOption: SelectProps["renderOption"] = ({
- option,
-}) => (
-
-
-
-
- {option.label}
-
- {option["email"] && (
-
- {option["email"]}
-
- )}
-
-
-);
diff --git a/apps/client/src/ee/page-verification/components/verification-list-table.tsx b/apps/client/src/ee/page-verification/components/verification-list-table.tsx
deleted file mode 100644
index 832d7a4f..00000000
--- a/apps/client/src/ee/page-verification/components/verification-list-table.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import {
- Table,
- Text,
- Group,
- Skeleton,
- Anchor,
- Badge,
- Avatar,
- Tooltip,
-} from "@mantine/core";
-import { Link } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-import {
- IVerificationListItem,
- VerificationStatus,
-} from "@/ee/page-verification/types/page-verification.types";
-import { CustomAvatar } from "@/components/ui/custom-avatar";
-import { buildPageUrl } from "@/features/page/page.utils";
-import NoTableResults from "@/components/common/no-table-results";
-import rowClasses from "@/components/ui/clickable-table-row.module.css";
-import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
-import type { Locale } from "date-fns";
-
-const MAX_VISIBLE_VERIFIERS = 5;
-
-type VerificationListTableProps = {
- items?: IVerificationListItem[];
- isLoading: boolean;
-};
-
-function statusBadge(status: VerificationStatus | null, t: (s: string) => string) {
- switch (status) {
- case "verified":
- return {t("Verified")} ;
- case "expiring":
- return {t("Expiring")} ;
- case "expired":
- return {t("Expired")} ;
- case "approved":
- return {t("Approved")} ;
- case "draft":
- return {t("Draft")} ;
- case "in_approval":
- return {t("In approval")} ;
- case "obsolete":
- return {t("Obsolete")} ;
- default:
- return null;
- }
-}
-
-function verifiedUntilText(
- item: IVerificationListItem,
- t: (s: string) => string,
- locale: Locale,
-): string {
- if (item.type === "qms") {
- if (item.status === "approved") return t("Indefinitely");
- return "—";
- }
-
- if (!item.expiresAt) return t("Indefinitely");
-
- const expires = new Date(item.expiresAt);
- const now = new Date();
-
- if (expires <= now) return t("Expired");
- return formatLocalized(expires, "MMM d, yyyy", "PP", locale);
-}
-
-function TableSkeleton() {
- return (
- <>
- {Array.from({ length: 8 }).map((_, i) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
- >
- );
-}
-
-export default function VerificationListTable({
- items,
- isLoading,
-}: VerificationListTableProps) {
- const { t } = useTranslation();
- const locale = useDateFnsLocale();
-
- return (
-
-
-
-
- {t("Page")}
- {t("Verifiers")}
- {t("Verified until")}
- {t("Status")}
-
-
-
-
- {isLoading ? (
-
- ) : items && items.length > 0 ? (
- items.map((item) => {
- const verifiers = item.verifiers ?? [];
-
- const pageUrl = buildPageUrl(
- item.spaceSlug,
- item.pageSlugId,
- item.pageTitle ?? undefined,
- );
-
- return (
-
-
-
-
- {item.pageIcon ? `${item.pageIcon} ` : ""}
- {item.pageTitle || t("Untitled")}
-
-
-
- {item.spaceName}
-
-
-
-
- {verifiers.length === 1 ? (
-
-
-
- {verifiers[0].name}
-
-
- ) : verifiers.length > 1 ? (
-
-
- {verifiers
- .slice(0, MAX_VISIBLE_VERIFIERS)
- .map((verifier) => (
-
-
-
- ))}
- {verifiers.length > MAX_VISIBLE_VERIFIERS && (
- (
- {v.name}
- ))}
- >
-
- +{verifiers.length - MAX_VISIBLE_VERIFIERS}
-
-
- )}
-
-
- ) : (
-
- —
-
- )}
-
-
-
-
- {verifiedUntilText(item, t, locale)}
-
-
-
-
- {statusBadge(item.status as VerificationStatus, t)}
-
-
- );
- })
- ) : (
-
- )}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/page-verification/components/verification-status.ts b/apps/client/src/ee/page-verification/components/verification-status.ts
deleted file mode 100644
index 3667902e..00000000
--- a/apps/client/src/ee/page-verification/components/verification-status.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { VerificationStatus } from "@/ee/page-verification/types/page-verification.types";
-
-export function getStatusColor(status: VerificationStatus): string {
- switch (status) {
- case "verified":
- case "approved":
- return "blue.7";
- case "expiring":
- case "in_approval":
- return "orange.8";
- case "expired":
- return "red.7";
- case "draft":
- case "obsolete":
- return "gray.6";
- default:
- return "gray.6";
- }
-}
-
-export function getStatusLabel(
- status: VerificationStatus,
- t: (key: string) => string,
-): string {
- switch (status) {
- case "verified":
- return t("Verified");
- case "expiring":
- return t("Review needed");
- case "expired":
- return t("Verification expired");
- case "draft":
- return t("Draft");
- case "in_approval":
- return t("In Approval");
- case "approved":
- return t("Approved");
- case "obsolete":
- return t("Obsolete");
- default:
- return "";
- }
-}
diff --git a/apps/client/src/ee/page-verification/components/verifier-list.tsx b/apps/client/src/ee/page-verification/components/verifier-list.tsx
deleted file mode 100644
index 38169b97..00000000
--- a/apps/client/src/ee/page-verification/components/verifier-list.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { ActionIcon, Group, Text, Tooltip } from "@mantine/core";
-import { IconX } from "@tabler/icons-react";
-import { CustomAvatar } from "@/components/ui/custom-avatar";
-import { IVerifier } from "@/ee/page-verification/types/page-verification.types";
-import { useTranslation } from "react-i18next";
-
-type VerifierListProps = {
- verifiers: IVerifier[];
- canManage?: boolean;
- onRemove?: (userId: string) => void;
-};
-
-export function VerifierList({
- verifiers,
- canManage,
- onRemove,
-}: VerifierListProps) {
- const { t } = useTranslation();
-
- if (verifiers.length === 0) return null;
-
- return (
- <>
- {verifiers.map((verifier, index) => (
-
-
-
-
-
- {verifier.name}
-
- {verifier.email && (
-
- {verifier.email}
-
- )}
-
-
- {canManage && onRemove && (
-
- onRemove(verifier.id)}
- >
-
-
-
- )}
-
- ))}
- >
- );
-}
diff --git a/apps/client/src/ee/page-verification/components/verifier-picker.tsx b/apps/client/src/ee/page-verification/components/verifier-picker.tsx
deleted file mode 100644
index 23f0ad85..00000000
--- a/apps/client/src/ee/page-verification/components/verifier-picker.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { useState } from "react";
-import { Select } from "@mantine/core";
-import { useDebouncedValue } from "@mantine/hooks";
-import { useTranslation } from "react-i18next";
-import { useSearchSuggestionsQuery } from "@/features/search/queries/search-query";
-import {
- renderUserSelectOption,
- toUserOptions,
- UserOptionItem,
-} from "./user-option";
-
-type VerifierPickerProps = {
- excludeIds: string[];
- disabled?: boolean;
- onSelect: (user: UserOptionItem) => void;
- placeholder?: string;
-};
-
-export function VerifierPicker({
- excludeIds,
- disabled,
- onSelect,
- placeholder,
-}: VerifierPickerProps) {
- const { t } = useTranslation();
- const [searchValue, setSearchValue] = useState("");
- const [debouncedQuery] = useDebouncedValue(searchValue, 300);
-
- const { data: suggestion } = useSearchSuggestionsQuery({
- query: debouncedQuery,
- includeUsers: true,
- includeGroups: false,
- preload: true,
- });
-
- const excludeSet = new Set(excludeIds);
- const options = toUserOptions(suggestion?.users).filter(
- (u) => !excludeSet.has(u.value),
- );
-
- const handleChange = (userId: string | null) => {
- if (!userId) return;
- const picked = options.find((u) => u.value === userId);
- if (!picked) return;
- onSelect(picked);
- setSearchValue("");
- };
-
- return (
- options}
- variant="filled"
- disabled={disabled}
- nothingFoundMessage={t("No user found")}
- />
- );
-}
diff --git a/apps/client/src/ee/page-verification/index.ts b/apps/client/src/ee/page-verification/index.ts
deleted file mode 100644
index 7378ef39..00000000
--- a/apps/client/src/ee/page-verification/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export * from "./components/page-verification-modal";
-export * from "./components/verifier-list";
-export * from "./queries/page-verification-query";
-export * from "./services/page-verification-service";
-export * from "./types/page-verification.types";
diff --git a/apps/client/src/ee/page-verification/pages/verified-pages.tsx b/apps/client/src/ee/page-verification/pages/verified-pages.tsx
deleted file mode 100644
index 51786e5a..00000000
--- a/apps/client/src/ee/page-verification/pages/verified-pages.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import { useState, useMemo } from "react";
-import { Group, MultiSelect, Select, Space, TextInput } from "@mantine/core";
-import { useDebouncedValue } from "@mantine/hooks";
-import { Helmet } from "react-helmet-async";
-import { useTranslation } from "react-i18next";
-import { IconSearch } from "@tabler/icons-react";
-import SettingsTitle from "@/components/settings/settings-title";
-import { getAppName } from "@/lib/config";
-import Paginate from "@/components/common/paginate";
-import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
-import { useVerificationListQuery } from "@/ee/page-verification/queries/page-verification-query";
-import { IVerificationListParams } from "@/ee/page-verification/types/page-verification.types";
-import VerificationListTable from "@/ee/page-verification/components/verification-list-table";
-import { useGetSpacesQuery } from "@/features/space/queries/space-query";
-
-export default function VerifiedPages() {
- const { t } = useTranslation();
- const { cursor, goNext, goPrev, resetCursor } = useCursorPaginate();
-
- const [searchValue, setSearchValue] = useState("");
- const [debouncedSearch] = useDebouncedValue(searchValue, 300);
- const [spaceFilter, setSpaceFilter] = useState([]);
- const [typeFilter, setTypeFilter] = useState(null);
-
- const { data: spacesData } = useGetSpacesQuery({ limit: 100 });
-
- const spaceOptions = useMemo(
- () =>
- spacesData?.items?.map((space) => ({
- value: space.id,
- label: space.name,
- })) ?? [],
- [spacesData],
- );
-
- const typeOptions = [
- { value: "expiring", label: t("Expiring") },
- { value: "qms", label: t("QMS") },
- ];
-
- const params: IVerificationListParams = useMemo(
- () => ({
- cursor,
- limit: 50,
- spaceIds: spaceFilter.length > 0 ? spaceFilter : undefined,
- type: typeFilter as IVerificationListParams["type"],
- query: debouncedSearch || undefined,
- }),
- [cursor, spaceFilter, typeFilter, debouncedSearch],
- );
-
- const { data, isLoading } = useVerificationListQuery(params);
-
- const handleSpaceChange = (value: string[]) => {
- setSpaceFilter(value);
- resetCursor();
- };
-
- const handleTypeChange = (value: string | null) => {
- setTypeFilter(value);
- resetCursor();
- };
-
- const handleSearchChange = (e: React.ChangeEvent) => {
- setSearchValue(e.currentTarget.value);
- resetCursor();
- };
-
- return (
- <>
-
-
- {t("Verified pages")} - {getAppName()}
-
-
-
-
-
-
- }
- value={searchValue}
- onChange={handleSearchChange}
- size="sm"
- w={220}
- />
-
- {/*
-
-
-
- */}
-
-
-
-
-
-
- {data?.items && data.items.length > 0 && (
- goNext(data?.meta?.nextCursor)}
- onPrev={goPrev}
- />
- )}
- >
- );
-}
diff --git a/apps/client/src/ee/page-verification/queries/page-verification-query.ts b/apps/client/src/ee/page-verification/queries/page-verification-query.ts
deleted file mode 100644
index a8aabb93..00000000
--- a/apps/client/src/ee/page-verification/queries/page-verification-query.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-import {
- keepPreviousData,
- useMutation,
- useQuery,
- useQueryClient,
- UseQueryResult,
-} from "@tanstack/react-query";
-import {
- IPageVerificationInfo,
- ISetupVerification,
- IUpdateVerification,
- IVerificationListItem,
- IVerificationListParams,
-} from "@/ee/page-verification/types/page-verification.types";
-import {
- getVerificationInfo,
- getVerificationList,
- markObsolete,
- rejectApproval,
- removeVerification,
- setupVerification,
- submitForApproval,
- updateVerification,
- verifyPage,
-} from "@/ee/page-verification/services/page-verification-service";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-import { IPagination } from "@/lib/types";
-
-export function usePageVerificationInfoQuery(
- pageId: string | undefined,
-): UseQueryResult {
- return useQuery({
- queryKey: ["page-verification-info", pageId],
- queryFn: () => getVerificationInfo(pageId!),
- enabled: !!pageId,
- });
-}
-
-export function useSetupVerificationMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => setupVerification(data),
- onSuccess: (_, variables) => {
- queryClient.invalidateQueries({
- queryKey: ["page-verification-info", variables.pageId],
- });
- notifications.show({ message: t("Verification enabled") });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to enable verification"),
- color: "red",
- });
- },
- });
-}
-
-export function useUpdateVerificationMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => updateVerification(data),
- onSuccess: (_, variables) => {
- queryClient.invalidateQueries({
- queryKey: ["page-verification-info", variables.pageId],
- });
- notifications.show({ message: t("Verification updated") });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to update verification"),
- color: "red",
- });
- },
- });
-}
-
-export function useRemoveVerificationMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (pageId) => removeVerification(pageId),
- onSuccess: (_, pageId) => {
- queryClient.invalidateQueries({
- queryKey: ["page-verification-info", pageId],
- });
- notifications.show({ message: t("Verification removed") });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to remove verification"),
- color: "red",
- });
- },
- });
-}
-
-export function useVerifyPageMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (pageId) => verifyPage(pageId),
- onSuccess: (_, pageId) => {
- queryClient.invalidateQueries({
- queryKey: ["page-verification-info", pageId],
- });
- notifications.show({ message: t("Page verified") });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to verify page"),
- color: "red",
- });
- },
- });
-}
-
-export function useSubmitForApprovalMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (pageId) => submitForApproval(pageId),
- onSuccess: (_, pageId) => {
- queryClient.invalidateQueries({
- queryKey: ["page-verification-info", pageId],
- });
- notifications.show({ message: t("Submitted for approval") });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to submit for approval"),
- color: "red",
- });
- },
- });
-}
-
-export function useRejectApprovalMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => rejectApproval(data),
- onSuccess: (_, variables) => {
- queryClient.invalidateQueries({
- queryKey: ["page-verification-info", variables.pageId],
- });
- notifications.show({ message: t("Approval rejected") });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to reject approval"),
- color: "red",
- });
- },
- });
-}
-
-export function useMarkObsoleteMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (pageId) => markObsolete(pageId),
- onSuccess: (_, pageId) => {
- queryClient.invalidateQueries({
- queryKey: ["page-verification-info", pageId],
- });
- notifications.show({ message: t("Page marked as obsolete") });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({
- message: errorMessage || t("Failed to mark as obsolete"),
- color: "red",
- });
- },
- });
-}
-
-export function useVerificationListQuery(
- params?: IVerificationListParams,
-): UseQueryResult, Error> {
- return useQuery({
- queryKey: ["verification-list", params],
- queryFn: () => getVerificationList(params),
- placeholderData: keepPreviousData,
- });
-}
diff --git a/apps/client/src/ee/page-verification/services/page-verification-service.ts b/apps/client/src/ee/page-verification/services/page-verification-service.ts
deleted file mode 100644
index ddac7ecb..00000000
--- a/apps/client/src/ee/page-verification/services/page-verification-service.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import api from "@/lib/api-client";
-import {
- IPageVerificationInfo,
- ISetupVerification,
- IUpdateVerification,
- IVerificationListItem,
- IVerificationListParams,
-} from "@/ee/page-verification/types/page-verification.types";
-import { IPagination } from "@/lib/types";
-
-export async function getVerificationInfo(
- pageId: string,
-): Promise {
- const req = await api.post(
- "/pages/verification-info",
- { pageId },
- );
- return req.data;
-}
-
-export async function setupVerification(
- data: ISetupVerification,
-): Promise {
- await api.post("/pages/create-verification", data);
-}
-
-export async function updateVerification(
- data: IUpdateVerification,
-): Promise {
- await api.post("/pages/update-verification", data);
-}
-
-export async function removeVerification(pageId: string): Promise {
- await api.post("/pages/delete-verification", { pageId });
-}
-
-export async function verifyPage(pageId: string): Promise {
- await api.post("/pages/verify", { pageId });
-}
-
-export async function submitForApproval(pageId: string): Promise {
- await api.post("/pages/submit-for-approval", { pageId });
-}
-
-export async function rejectApproval(data: {
- pageId: string;
- comment?: string;
-}): Promise {
- await api.post("/pages/reject-approval", data);
-}
-
-export async function markObsolete(pageId: string): Promise {
- await api.post("/pages/mark-obsolete", { pageId });
-}
-
-export async function getVerificationList(
- params?: IVerificationListParams,
-): Promise> {
- const req = await api.post("/pages/verifications", { ...params });
- return req.data;
-}
diff --git a/apps/client/src/ee/page-verification/types/page-verification.types.ts b/apps/client/src/ee/page-verification/types/page-verification.types.ts
deleted file mode 100644
index c0f4e811..00000000
--- a/apps/client/src/ee/page-verification/types/page-verification.types.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-export type VerificationType = "expiring" | "qms";
-
-export type ExpirationMode = "period" | "fixed" | "indefinite";
-
-export type PeriodUnit = "day" | "week" | "month" | "year";
-
-export type VerificationStatus =
- | "verified"
- | "expiring"
- | "expired"
- | "draft"
- | "in_approval"
- | "approved"
- | "obsolete"
- | "none";
-
-export type IUserRef = {
- id: string;
- name: string;
- avatarUrl: string | null;
-};
-
-export type IVerifier = {
- id: string;
- name: string;
- avatarUrl: string | null;
- email: string;
-};
-
-export type IPageVerificationInfo = {
- id?: string;
- pageId?: string;
- type?: VerificationType;
- mode?: ExpirationMode | null;
- periodAmount?: number | null;
- periodUnit?: PeriodUnit | null;
- status: VerificationStatus;
- verifiedAt?: string | null;
- verifiedBy?: IUserRef | null;
- expiresAt?: string | null;
- requestedAt?: string | null;
- requestedBy?: IUserRef | null;
- rejectedAt?: string | null;
- rejectedBy?: IUserRef | null;
- rejectionComment?: string | null;
- verifiers?: IVerifier[];
- permissions?: IPageVerificationPermissions;
-};
-
-export type IPageVerificationPermissions = {
- canVerify: boolean;
- canManage: boolean;
- canSubmitForApproval: boolean;
- canMarkObsolete: boolean;
-};
-
-export type ISetupVerification = {
- pageId: string;
- type?: VerificationType;
- mode?: ExpirationMode;
- periodAmount?: number;
- periodUnit?: PeriodUnit;
- fixedExpiresAt?: string;
- verifierIds: string[];
-};
-
-export type IUpdateVerification = {
- pageId: string;
- mode?: ExpirationMode;
- periodAmount?: number;
- periodUnit?: PeriodUnit;
- fixedExpiresAt?: string;
- verifierIds?: string[];
-};
-
-export type IVerificationListItem = {
- id: string;
- pageId: string;
- spaceId: string;
- type: VerificationType;
- status: VerificationStatus | null;
- mode: ExpirationMode | null;
- periodAmount: number | null;
- periodUnit: PeriodUnit | null;
- verifiedAt: string | null;
- expiresAt: string | null;
- createdAt: string;
- pageTitle: string | null;
- pageSlugId: string;
- pageIcon: string | null;
- spaceName: string;
- spaceSlug: string;
- verifiers: IUserRef[];
-};
-
-export type IVerificationListParams = {
- spaceIds?: string[];
- verifierId?: string;
- type?: VerificationType;
- cursor?: string;
- beforeCursor?: string;
- limit?: number;
- query?: string;
-};
diff --git a/apps/client/src/ee/pages/cloud-login.tsx b/apps/client/src/ee/pages/cloud-login.tsx
deleted file mode 100644
index c0a40a0d..00000000
--- a/apps/client/src/ee/pages/cloud-login.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Helmet } from "react-helmet-async";
-import { getAppName } from "@/lib/config.ts";
-import { CloudLoginForm } from "@/ee/components/cloud-login-form.tsx";
-import { useTranslation } from "react-i18next";
-
-export default function CloudLogin() {
- const { t } = useTranslation();
-
- return (
- <>
-
-
- {t("Login")} - {getAppName()}
-
-
-
-
- >
- );
-}
diff --git a/apps/client/src/ee/pages/create-workspace.tsx b/apps/client/src/ee/pages/create-workspace.tsx
deleted file mode 100644
index fb335d66..00000000
--- a/apps/client/src/ee/pages/create-workspace.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { SetupWorkspaceForm } from "@/features/auth/components/setup-workspace-form.tsx";
-import { Helmet } from "react-helmet-async";
-import React from "react";
-import { getAppName } from "@/lib/config.ts";
-
-export default function CreateWorkspace() {
- return (
- <>
-
- Create Workspace - {getAppName()}
-
-
- >
- );
-}
diff --git a/apps/client/src/ee/pages/verify-email.tsx b/apps/client/src/ee/pages/verify-email.tsx
deleted file mode 100644
index aef5711d..00000000
--- a/apps/client/src/ee/pages/verify-email.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import { useEffect, useState } from "react";
-import { useSearchParams, useNavigate } from "react-router-dom";
-import { Container, Title, Text, Button, Box } from "@mantine/core";
-import classes from "../../features/auth/components/auth.module.css";
-import {
- verifyEmail,
- resendVerificationEmail,
-} from "@/ee/cloud/service/cloud-service.ts";
-import { notifications } from "@mantine/notifications";
-import APP_ROUTE from "@/lib/app-route.ts";
-import { useTranslation } from "react-i18next";
-import { AuthLayout } from "@/features/auth/components/auth-layout.tsx";
-
-export default function VerifyEmail() {
- const { t } = useTranslation();
- const [searchParams] = useSearchParams();
- const navigate = useNavigate();
- const token = searchParams.get("token");
- const rawEmail = searchParams.get("email");
- const email = rawEmail && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(rawEmail) ? rawEmail : null;
- const sig = searchParams.get("sig");
- const [isResending, setIsResending] = useState(false);
- const [resent, setResent] = useState(false);
-
- useEffect(() => {
- if (token) {
- handleVerify(token);
- }
- }, [token]);
-
- async function handleVerify(verifyToken: string) {
- try {
- await verifyEmail({ token: verifyToken });
- navigate(APP_ROUTE.HOME);
- } catch (err) {
- notifications.show({
- message: t("Verification failed. The link may have expired."),
- color: "red",
- });
- navigate(APP_ROUTE.AUTH.LOGIN);
- }
- }
-
- async function handleResend() {
- if (!email || !sig) return;
- setIsResending(true);
-
- try {
- await resendVerificationEmail({ email, sig });
- setResent(true);
- } catch {
- notifications.show({
- message: t("Failed to resend verification email. Please try again."),
- color: "red",
- });
- }
-
- setIsResending(false);
- }
-
- if (token) {
- return (
-
-
-
-
- {t("Verifying your email")}
-
-
- {t("Please wait...")}
-
-
-
-
- );
- }
-
- return (
-
-
-
-
- {t("Check your email")}
-
-
- {email
- ? t("We sent a verification link to {{email}}.", { email })
- : t("We sent a verification link to your email.")}
-
-
- {t("Click the link to verify your email and access your workspace.")}
-
- {email && sig && !resent && (
-
- {t("Resend verification email")}
-
- )}
- {resent && (
-
- {t("Verification email sent. Please check your inbox.")}
-
- )}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/pdf-export/pdf-render-page.tsx b/apps/client/src/ee/pdf-export/pdf-render-page.tsx
deleted file mode 100644
index 8705f9b6..00000000
--- a/apps/client/src/ee/pdf-export/pdf-render-page.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import "@/features/editor/styles/index.css";
-import { useEffect, useState } from "react";
-import { useParams, useSearchParams } from "react-router-dom";
-import ReadonlyPageEditor from "@/features/editor/readonly-page-editor";
-import { Container } from "@mantine/core";
-
-type PdfRenderData = {
- pageId: string;
- title: string;
- content: any;
-};
-
-export default function PdfRenderPage() {
- const { pageId } = useParams<{ pageId: string }>();
- const [searchParams] = useSearchParams();
- const token = searchParams.get("token");
-
- const [data, setData] = useState(null);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- if (!pageId || !token) {
- setError("Missing page ID or token");
- return;
- }
-
- fetch('/api/pdf-export/render', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ pageId, token }),
- })
- .then((res) => {
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
- return res.json();
- })
- .then((result) => setData(result.data))
- .catch((err) => setError(err.message));
- }, [pageId, token]);
-
- useEffect(() => {
- if (data?.title) {
- document.title = data.title;
- }
- }, [data?.title]);
-
- if (error) {
- return {error}
;
- }
-
- if (!data) {
- return null;
- }
-
- return (
-
-
-
- );
-}
diff --git a/apps/client/src/ee/scim/components/create-scim-token-modal.tsx b/apps/client/src/ee/scim/components/create-scim-token-modal.tsx
deleted file mode 100644
index c542f8cf..00000000
--- a/apps/client/src/ee/scim/components/create-scim-token-modal.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Modal, TextInput, Button, Group, Stack } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { z } from "zod/v4";
-import { useTranslation } from "react-i18next";
-import { useCreateScimTokenMutation } from "@/ee/scim/queries/scim-token-query";
-import { IScimToken } from "@/ee/scim/types/scim-token.types";
-
-interface CreateScimTokenModalProps {
- opened: boolean;
- onClose: () => void;
- onSuccess: (response: IScimToken) => void;
-}
-
-const formSchema = z.object({
- name: z.string().min(1, "Name is required"),
-});
-type FormValues = z.infer;
-
-export function CreateScimTokenModal({
- opened,
- onClose,
- onSuccess,
-}: CreateScimTokenModalProps) {
- const { t } = useTranslation();
- const createMutation = useCreateScimTokenMutation();
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: { name: "" },
- });
-
- const handleSubmit = async (data: FormValues) => {
- try {
- const created = await createMutation.mutateAsync({ name: data.name });
- onSuccess(created);
- form.reset();
- onClose();
- } catch (err) {
- //
- }
- };
-
- const handleClose = () => {
- form.reset();
- onClose();
- };
-
- return (
-
- handleSubmit(values))}>
-
-
-
-
-
- {t("Cancel")}
-
-
- {t("Create")}
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/scim/components/enable-scim.tsx b/apps/client/src/ee/scim/components/enable-scim.tsx
deleted file mode 100644
index 0f8204eb..00000000
--- a/apps/client/src/ee/scim/components/enable-scim.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Group, Text, Switch, Tooltip } from "@mantine/core";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import React, { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
-import { notifications } from "@mantine/notifications";
-import { useHasFeature } from "@/ee/hooks/use-feature.ts";
-import { Feature } from "@/ee/features.ts";
-import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
-
-export default function EnableScim() {
- const { t } = useTranslation();
- const [workspace, setWorkspace] = useAtom(workspaceAtom);
- const [checked, setChecked] = useState(workspace?.isScimEnabled ?? false);
- const hasAccess = useHasFeature(Feature.SCIM);
- const upgradeLabel = useUpgradeLabel();
-
- const handleChange = async (event: React.ChangeEvent) => {
- const value = event.currentTarget.checked;
- try {
- const updatedWorkspace = await updateWorkspace({ isScimEnabled: value });
- setChecked(value);
- setWorkspace(updatedWorkspace);
- } catch (err) {
- notifications.show({
- message: err?.response?.data?.message,
- color: "red",
- });
- }
- };
-
- return (
-
-
- {t("Enable SCIM")}
-
- {t(
- "Automatically provision users and groups from your identity provider via SCIM.",
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/scim/components/revoke-scim-token-modal.tsx b/apps/client/src/ee/scim/components/revoke-scim-token-modal.tsx
deleted file mode 100644
index 826a3d18..00000000
--- a/apps/client/src/ee/scim/components/revoke-scim-token-modal.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Modal, Text, Button, Group, Stack } from "@mantine/core";
-import { useTranslation } from "react-i18next";
-import { useRevokeScimTokenMutation } from "@/ee/scim/queries/scim-token-query";
-import { IScimToken } from "@/ee/scim/types/scim-token.types";
-
-interface RevokeScimTokenModalProps {
- opened: boolean;
- onClose: () => void;
- scimToken: IScimToken | null;
-}
-
-export function RevokeScimTokenModal({
- opened,
- onClose,
- scimToken,
-}: RevokeScimTokenModalProps) {
- const { t } = useTranslation();
- const revokeMutation = useRevokeScimTokenMutation();
-
- const handleRevoke = async () => {
- if (!scimToken) return;
- await revokeMutation.mutateAsync({ tokenId: scimToken.id });
- onClose();
- };
-
- return (
-
-
-
- {t("Are you sure you want to revoke this {{credential}}", {
- credential: t("SCIM token"),
- })}{" "}
- {scimToken?.name} ?
-
-
- {t(
- "This action cannot be undone. Your identity provider will stop syncing immediately.",
- )}
-
-
-
-
- {t("Cancel")}
-
-
- {t("Revoke")}
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/scim/components/scim-token-created-modal.tsx b/apps/client/src/ee/scim/components/scim-token-created-modal.tsx
deleted file mode 100644
index 1a95259c..00000000
--- a/apps/client/src/ee/scim/components/scim-token-created-modal.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import {
- Modal,
- Text,
- Stack,
- Alert,
- Group,
- Button,
- TextInput,
-} from "@mantine/core";
-import { IconAlertTriangle } from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import CopyTextButton from "@/components/common/copy.tsx";
-import { IScimToken } from "@/ee/scim/types/scim-token.types";
-
-interface ScimTokenCreatedModalProps {
- opened: boolean;
- onClose: () => void;
- scimToken: IScimToken | null;
-}
-
-export function ScimTokenCreatedModal({
- opened,
- onClose,
- scimToken,
-}: ScimTokenCreatedModalProps) {
- const { t } = useTranslation();
- if (!scimToken) return null;
-
- return (
-
-
- }
- title={t("Important")}
- color="red"
- >
- {t(
- "Make sure to copy your {{credential}} now. You won't be able to see it again!",
- { credential: t("SCIM token") },
- )}
-
-
-
-
- {t("SCIM token")}
-
-
-
-
-
-
-
-
- {t("I've saved my {{credential}}", { credential: t("SCIM token") })}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/scim/components/scim-token-table.tsx b/apps/client/src/ee/scim/components/scim-token-table.tsx
deleted file mode 100644
index 90572be3..00000000
--- a/apps/client/src/ee/scim/components/scim-token-table.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core";
-import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
-import { useTranslation } from "react-i18next";
-import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
-import React from "react";
-import NoTableResults from "@/components/common/no-table-results";
-import { IScimToken } from "@/ee/scim/types/scim-token.types";
-import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
-
-interface ScimTokenTableProps {
- tokens: IScimToken[];
- isLoading?: boolean;
- onUpdate?: (token: IScimToken) => void;
- onRevoke?: (token: IScimToken) => void;
-}
-
-export function ScimTokenTable({
- tokens,
- isLoading,
- onUpdate,
- onRevoke,
-}: ScimTokenTableProps) {
- const { t } = useTranslation();
- const locale = useDateFnsLocale();
-
- const formatDate = (date: Date | string | null) => {
- if (!date) return t("Never");
- return formatLocalized(date, "MMM dd, yyyy", "PP", locale);
- };
-
- return (
-
-
-
-
- {t("Name")}
- {t("Token")}
- {t("Created by")}
- {t("Last used")}
- {t("Created")}
-
-
-
-
-
- {tokens && tokens.length > 0 ? (
- tokens.map((token) => (
-
-
-
- {token.name}
-
-
-
-
-
- ••••{token.tokenLastFour}
-
-
-
- {token.creator ? (
-
-
-
-
- {token.creator.name}
-
-
-
- ) : (
-
-
- —
-
-
- )}
-
-
-
- {formatDate(token.lastUsedAt)}
-
-
-
-
-
- {formatDate(token.createdAt)}
-
-
-
-
-
-
-
-
-
-
-
- {onUpdate && (
- }
- onClick={() => onUpdate(token)}
- >
- {t("Rename")}
-
- )}
- {onRevoke && (
- }
- color="red"
- onClick={() => onRevoke(token)}
- >
- {t("Revoke")}
-
- )}
-
-
-
-
- ))
- ) : (
-
- )}
-
-
-
- );
-}
diff --git a/apps/client/src/ee/scim/components/scim-url-panel.tsx b/apps/client/src/ee/scim/components/scim-url-panel.tsx
deleted file mode 100644
index 6aa78820..00000000
--- a/apps/client/src/ee/scim/components/scim-url-panel.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Group, Stack, Text, TextInput } from "@mantine/core";
-import { useTranslation } from "react-i18next";
-import CopyTextButton from "@/components/common/copy.tsx";
-
-export function ScimUrlPanel() {
- const { t } = useTranslation();
- const scimUrl = `${window.location.origin}/api/scim/v2`;
-
- return (
-
-
- {t("SCIM endpoint URL")}
-
-
- {t(
- "Configure your identity provider with this URL to provision users and groups.",
- )}
-
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/scim/components/update-scim-token-modal.tsx b/apps/client/src/ee/scim/components/update-scim-token-modal.tsx
deleted file mode 100644
index 45ffd782..00000000
--- a/apps/client/src/ee/scim/components/update-scim-token-modal.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { Modal, TextInput, Button, Group, Stack } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { z } from "zod/v4";
-import { useTranslation } from "react-i18next";
-import { useEffect } from "react";
-import { useUpdateScimTokenMutation } from "@/ee/scim/queries/scim-token-query";
-import { IScimToken } from "@/ee/scim/types/scim-token.types";
-
-const formSchema = z.object({
- name: z.string().min(1, "Name is required"),
-});
-type FormValues = z.infer;
-
-interface UpdateScimTokenModalProps {
- opened: boolean;
- onClose: () => void;
- scimToken: IScimToken | null;
-}
-
-export function UpdateScimTokenModal({
- opened,
- onClose,
- scimToken,
-}: UpdateScimTokenModalProps) {
- const { t } = useTranslation();
- const updateMutation = useUpdateScimTokenMutation();
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: { name: "" },
- });
-
- useEffect(() => {
- if (opened && scimToken) {
- form.setValues({ name: scimToken.name });
- }
- }, [opened, scimToken]);
-
- const handleSubmit = async (data: FormValues) => {
- if (!scimToken) return;
- await updateMutation.mutateAsync({
- tokenId: scimToken.id,
- name: data.name,
- });
- onClose();
- };
-
- return (
-
- handleSubmit(values))}>
-
-
-
-
-
- {t("Cancel")}
-
-
- {t("Update")}
-
-
-
-
-
- );
-}
diff --git a/apps/client/src/ee/scim/index.ts b/apps/client/src/ee/scim/index.ts
deleted file mode 100644
index e246200d..00000000
--- a/apps/client/src/ee/scim/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from "./types/scim-token.types";
-export * from "./services/scim-token-service";
diff --git a/apps/client/src/ee/scim/queries/scim-token-query.ts b/apps/client/src/ee/scim/queries/scim-token-query.ts
deleted file mode 100644
index 999f4d20..00000000
--- a/apps/client/src/ee/scim/queries/scim-token-query.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { IPagination, QueryParams } from "@/lib/types.ts";
-import {
- keepPreviousData,
- useMutation,
- useQuery,
- useQueryClient,
- UseQueryResult,
-} from "@tanstack/react-query";
-import {
- createScimToken,
- getScimTokens,
- revokeScimToken,
- updateScimToken,
-} from "@/ee/scim/services/scim-token-service";
-import {
- IScimToken,
- ICreateScimTokenRequest,
- IRevokeScimTokenRequest,
- IUpdateScimTokenRequest,
-} from "@/ee/scim/types/scim-token.types";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-
-export function useGetScimTokensQuery(
- params?: QueryParams,
-): UseQueryResult, Error> {
- return useQuery({
- queryKey: ["scim-token-list", params],
- queryFn: () => getScimTokens(params),
- placeholderData: keepPreviousData,
- });
-}
-
-export function useCreateScimTokenMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => createScimToken(data),
- onSuccess: () => {
- notifications.show({
- message: t("{{credential}} created successfully", {
- credential: t("SCIM token"),
- }),
- });
- queryClient.invalidateQueries({
- predicate: (item) =>
- ["scim-token-list"].includes(item.queryKey[0] as string),
- });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({ message: errorMessage, color: "red" });
- },
- });
-}
-
-export function useUpdateScimTokenMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => updateScimToken(data),
- onSuccess: () => {
- notifications.show({ message: t("Updated successfully") });
- queryClient.invalidateQueries({
- predicate: (item) =>
- ["scim-token-list"].includes(item.queryKey[0] as string),
- });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({ message: errorMessage, color: "red" });
- },
- });
-}
-
-export function useRevokeScimTokenMutation() {
- const queryClient = useQueryClient();
- const { t } = useTranslation();
-
- return useMutation({
- mutationFn: (data) => revokeScimToken(data),
- onSuccess: () => {
- notifications.show({ message: t("Revoked successfully") });
- queryClient.invalidateQueries({
- predicate: (item) =>
- ["scim-token-list"].includes(item.queryKey[0] as string),
- });
- },
- onError: (error) => {
- const errorMessage = error["response"]?.data?.message;
- notifications.show({ message: errorMessage, color: "red" });
- },
- });
-}
diff --git a/apps/client/src/ee/scim/services/scim-token-service.ts b/apps/client/src/ee/scim/services/scim-token-service.ts
deleted file mode 100644
index 27e73035..00000000
--- a/apps/client/src/ee/scim/services/scim-token-service.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import api from "@/lib/api-client";
-import {
- IScimToken,
- ICreateScimTokenRequest,
- IRevokeScimTokenRequest,
- IUpdateScimTokenRequest,
-} from "@/ee/scim/types/scim-token.types";
-import { IPagination, QueryParams } from "@/lib/types.ts";
-
-export async function getScimTokens(
- params?: QueryParams,
-): Promise> {
- const req = await api.post("/scim-tokens", { ...params });
- return req.data;
-}
-
-export async function createScimToken(
- data: ICreateScimTokenRequest,
-): Promise {
- const req = await api.post("/scim-tokens/create", data);
- return req.data;
-}
-
-export async function updateScimToken(
- data: IUpdateScimTokenRequest,
-): Promise {
- await api.post("/scim-tokens/update", data);
-}
-
-export async function revokeScimToken(
- data: IRevokeScimTokenRequest,
-): Promise {
- await api.post("/scim-tokens/revoke", data);
-}
diff --git a/apps/client/src/ee/scim/types/scim-token.types.ts b/apps/client/src/ee/scim/types/scim-token.types.ts
deleted file mode 100644
index 07650129..00000000
--- a/apps/client/src/ee/scim/types/scim-token.types.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { IUser } from "@/features/user/types/user.types.ts";
-
-export interface IScimToken {
- id: string;
- name: string;
- token?: string;
- tokenLastFour: string;
- isEnabled: boolean;
- creatorId: string;
- workspaceId: string;
- lastUsedAt: string | null;
- createdAt: string;
- creator?: Partial;
-}
-
-export interface ICreateScimTokenRequest {
- name: string;
-}
-
-export interface IUpdateScimTokenRequest {
- tokenId: string;
- name: string;
-}
-
-export interface IRevokeScimTokenRequest {
- tokenId: string;
-}
diff --git a/apps/client/src/ee/security/components/allow-member-templates.tsx b/apps/client/src/ee/security/components/allow-member-templates.tsx
deleted file mode 100644
index 8acc8ba6..00000000
--- a/apps/client/src/ee/security/components/allow-member-templates.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { Group, Text, Switch, Tooltip } from "@mantine/core";
-import { useAtom } from "jotai";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import { useState } from "react";
-import { useTranslation } from "react-i18next";
-import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
-import { notifications } from "@mantine/notifications";
-import { useHasFeature } from "@/ee/hooks/use-feature";
-import { Feature } from "@/ee/features";
-import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label.ts";
-
-export default function AllowMemberTemplates() {
- const { t } = useTranslation();
-
- return (
-
-
- {t("Allow members to create templates")}
-
- {t(
- "Allow non-admin members to create and manage templates in their spaces.",
- )}
-
-
-
-
-
- );
-}
-
-function AllowMemberTemplatesToggle() {
- const { t } = useTranslation();
- const [workspace, setWorkspace] = useAtom(workspaceAtom);
- const [checked, setChecked] = useState(
- workspace?.settings?.templates?.allowMemberTemplates === true,
- );
- const hasTemplates = useHasFeature(Feature.TEMPLATES);
- const upgradeLabel = useUpgradeLabel();
-
- const handleChange = async (event: React.ChangeEvent) => {
- const value = event.currentTarget.checked;
- try {
- const updatedWorkspace = await updateWorkspace({
- allowMemberTemplates: value,
- });
- setChecked(value);
- setWorkspace(updatedWorkspace);
- } catch (err) {
- notifications.show({
- message: err?.response?.data?.message,
- color: "red",
- });
- }
- };
-
- return (
-
-
-
- );
-}
diff --git a/apps/client/src/ee/security/components/allowed-domains.tsx b/apps/client/src/ee/security/components/allowed-domains.tsx
deleted file mode 100644
index 8c22b001..00000000
--- a/apps/client/src/ee/security/components/allowed-domains.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { useAtom } from "jotai";
-import { z } from "zod/v4";
-import { useForm } from "@mantine/form";
-import { zod4Resolver } from "mantine-form-zod-resolver";
-import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
-import React, { useState } from "react";
-import { Button, Text, TagsInput } from "@mantine/core";
-import { notifications } from "@mantine/notifications";
-import { useTranslation } from "react-i18next";
-import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
-import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
-
-const formSchema = z.object({
- emailDomains: z.array(z.string()),
-});
-
-type FormValues = z.infer;
-export default function AllowedDomains() {
- const { t } = useTranslation();
- const [isLoading, setIsLoading] = useState(false);
- const [workspace, setWorkspace] = useAtom(workspaceAtom);
- const [, setDomains] = useState([]);
-
- const form = useForm({
- validate: zod4Resolver(formSchema),
- initialValues: {
- emailDomains: workspace?.emailDomains || [],
- },
- });
-
- async function handleSubmit(data: Partial) {
- setIsLoading(true);
- try {
- const updatedWorkspace = await updateWorkspace({
- emailDomains: data.emailDomains,
- });
- setWorkspace(updatedWorkspace);
-
- notifications.show({
- message: t("Updated successfully"),
- });
- } catch (err) {
- console.log(err);
- notifications.show({
- message: err.response.data.message,
- color: "red",
- });
- }
-
- form.resetDirty();
-
- setIsLoading(false);
- }
-
- return (
- <>
-
-