From 7d275968bc7dd694c74efaad618afa6b634d8c6b Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sun, 29 Jun 2025 01:21:50 -0700 Subject: [PATCH] ui --- .../ee/components/manage-custom-domain.tsx | 310 ++++++++++++++++++ .../workspace/services/workspace-service.ts | 16 + .../settings/workspace/workspace-settings.tsx | 3 + apps/server/src/ee | 2 +- 4 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 apps/client/src/ee/components/manage-custom-domain.tsx diff --git a/apps/client/src/ee/components/manage-custom-domain.tsx b/apps/client/src/ee/components/manage-custom-domain.tsx new file mode 100644 index 00000000..7460fcfd --- /dev/null +++ b/apps/client/src/ee/components/manage-custom-domain.tsx @@ -0,0 +1,310 @@ +import { Button, Group, Text, Modal, TextInput, Alert, Code, Stack, Table } from "@mantine/core"; +import * as z from "zod"; +import { useState, useMemo } from "react"; +import { useDisclosure } from "@mantine/hooks"; +import * as React from "react"; +import { useForm, zodResolver } from "@mantine/form"; +import { notifications } from "@mantine/notifications"; +import { useTranslation } from "react-i18next"; +import { addCustomDomain, removeCustomDomain, verifyDnsConfiguration } from "@/features/workspace/services/workspace-service.ts"; +import { useAtom } from "jotai/index"; +import { + currentUserAtom, + workspaceAtom, +} from "@/features/user/atoms/current-user-atom.ts"; +import useUserRole from "@/hooks/use-user-role.tsx"; +import { RESET } from "jotai/utils"; +import { IconAlertCircle, IconCheck, IconX } from "@tabler/icons-react"; + +export default function ManageCustomDomain() { + const { t } = useTranslation(); + const [customDomainOpened, { open: openCustomDomain, close: closeCustomDomain }] = useDisclosure(false); + const [workspace] = useAtom(workspaceAtom); + const { isAdmin } = useUserRole(); + + return ( + + {workspace?.customDomain && ( + + + {t("Custom Domain")} + + {workspace.customDomain} + + + + {isAdmin && ( + + {t("Remove custom domain")} + + )} + + )} + + {!workspace?.customDomain && isAdmin && ( + + + {t("Custom Domain")} + + {t("Add a custom domain to your workspace")} + + + + + {t("Add custom domain")} + + + )} + + + {workspace?.customDomain ? ( + + ) : ( + + )} + + + ); +} + +interface AddCustomDomainFormProps { + onClose: () => void; +} + +const customDomainSchema = z.object({ + domain: z.string().min(1, { message: "Domain is required" }).regex(/^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?\.[a-zA-Z]{2,}$/, { + message: "Please enter a valid domain (e.g., example.com)" + }), +}); + +type CustomDomainFormValues = z.infer; + +function AddCustomDomainForm({ onClose }: AddCustomDomainFormProps) { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [isVerifying, setIsVerifying] = useState(false); + const [verificationResult, setVerificationResult] = useState<{ + isValid: boolean; + message: string; + isSubdomain: boolean; + } | null>(null); + const [currentUser, setCurrentUser] = useAtom(currentUserAtom); + + const form = useForm({ + validate: zodResolver(customDomainSchema), + initialValues: { + domain: "", + }, + }); + + // Memoize table content to prevent unnecessary re-renders + const tableContent = useMemo(() => { + const isSubdomain = verificationResult?.isSubdomain; + + return ( + + + + Record Type + Host + Value + + + + {isSubdomain ? ( + + CNAME + {form.values.domain} + app.docmost.com + + ) : ( + <> + + CNAME + www.{form.values.domain} + app.docmost.com + + + A + {form.values.domain} + YOUR_APP_IP + + > + )} + + + ); + }, [verificationResult?.isSubdomain, form.values.domain]); + + async function handleVerifyDns() { + const domain = form.values.domain; + if (!domain) return; + + setIsVerifying(true); + // Don't reset verification result immediately to prevent flicker + // Only reset if we're starting a new verification + + try { + const result = await verifyDnsConfiguration({ domain }); + setVerificationResult({ + isValid: result.isValid, + message: result.message, + isSubdomain: result.isSubdomain, + }); + } catch (err) { + setVerificationResult({ + isValid: false, + message: err?.response?.data?.message || "Failed to verify DNS configuration", + isSubdomain: false, + }); + } + setIsVerifying(false); + } + + async function handleSubmit(data: CustomDomainFormValues) { + setIsLoading(true); + + try { + await addCustomDomain({ domain: data.domain }); + setCurrentUser(RESET); + notifications.show({ + message: "Custom domain added successfully! Redirecting...", + color: "green", + icon: , + }); + + // Redirect to the new custom domain + setTimeout(() => { + window.location.href = `https://${data.domain}`; + }, 2000); + } catch (err) { + notifications.show({ + message: err?.response?.data?.message || "Failed to add custom domain", + color: "red", + icon: , + }); + } + setIsLoading(false); + } + + const isSubdomain = verificationResult?.isSubdomain; + const isValid = verificationResult?.isValid; + + return ( + + + + + } title="DNS Configuration Required" color="blue"> + + Before adding your custom domain, you need to configure your DNS settings: + + + {tableContent} + + + + {t("Verify DNS Configuration")} + + + {verificationResult && ( + : } + title={isValid ? "DNS Configuration Valid" : "DNS Configuration Invalid"} + color={isValid ? "green" : "red"} + > + {verificationResult.message} + + )} + + + + {t("Add Custom Domain")} + + + + + ); +} + +interface RemoveCustomDomainFormProps { + onClose: () => void; +} + +function RemoveCustomDomainForm({ onClose }: RemoveCustomDomainFormProps) { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [currentUser, setCurrentUser] = useAtom(currentUserAtom); + + async function handleRemove() { + if (!currentUser?.workspace?.customDomain) return; + + setIsLoading(true); + + try { + await removeCustomDomain({ domain: currentUser.workspace.customDomain }); + setCurrentUser(RESET); + notifications.show({ + message: "Custom domain removed successfully!", + color: "green", + icon: , + }); + onClose(); + } catch (err) { + notifications.show({ + message: err?.response?.data?.message || "Failed to remove custom domain", + color: "red", + icon: , + }); + } + setIsLoading(false); + } + + return ( + + } title="Remove Custom Domain" color="red"> + + Are you sure you want to remove the custom domain {currentUser?.workspace?.customDomain}? + This action cannot be undone. + + + + + + {t("Cancel")} + + + {t("Remove Custom Domain")} + + + + ); +} \ No newline at end of file diff --git a/apps/client/src/features/workspace/services/workspace-service.ts b/apps/client/src/features/workspace/services/workspace-service.ts index 293629fe..33c2ec27 100644 --- a/apps/client/src/features/workspace/services/workspace-service.ts +++ b/apps/client/src/features/workspace/services/workspace-service.ts @@ -120,3 +120,19 @@ export async function uploadLogo(file: File) { }); return req.data; } + +// Custom Domain Functions +export async function addCustomDomain(data: { domain: string }) { + const req = await api.post("/custom-domain/add", data); + return req.data; +} + +export async function removeCustomDomain(data: { domain: string }) { + const req = await api.delete("/custom-domain/remove", { data }); + return req.data; +} + +export async function verifyDnsConfiguration(data: { domain: string }) { + const req = await api.post("/custom-domain/verify-dns", data); + return req.data; +} diff --git a/apps/client/src/pages/settings/workspace/workspace-settings.tsx b/apps/client/src/pages/settings/workspace/workspace-settings.tsx index 4bfede64..5038559e 100644 --- a/apps/client/src/pages/settings/workspace/workspace-settings.tsx +++ b/apps/client/src/pages/settings/workspace/workspace-settings.tsx @@ -5,6 +5,7 @@ import { getAppName, isCloud } from "@/lib/config.ts"; import { Helmet } from "react-helmet-async"; import ManageHostname from "@/ee/components/manage-hostname.tsx"; import { Divider } from "@mantine/core"; +import ManageCustomDomain from "@/ee/components/manage-custom-domain"; export default function WorkspaceSettings() { const { t } = useTranslation(); @@ -20,6 +21,8 @@ export default function WorkspaceSettings() { <> + + > )} > diff --git a/apps/server/src/ee b/apps/server/src/ee index 1e127cec..1a02886f 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 1e127cec1d9f47f5ed8bf5a9de017e78a36793ee +Subproject commit 1a02886f1fd328703111597053067ab8d00b4e70
{currentUser?.workspace?.customDomain}