diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx index 769ca60417..03035e148c 100644 --- a/apps/client/src/features/page/components/header/page-header-menu.tsx +++ b/apps/client/src/features/page/components/header/page-header-menu.tsx @@ -44,6 +44,7 @@ import { PageStateSegmentedControl } from "@/features/user/components/page-state import MovePageModal from "@/features/page/components/move-page-modal.tsx"; import { useTimeAgo } from "@/hooks/use-time-ago.tsx"; import ShareModal from "@/features/share/components/share-modal.tsx"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; import { useWatchStatusQuery, useWatchPageMutation, @@ -58,6 +59,9 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { const toggleAside = useToggleAside(); const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom); const [editor, setEditor] = useAtom(pageEditorAtom); + const [workspace] = useAtom(workspaceAtom); + const workspaceSharingDisabled = + workspace?.settings?.sharing?.disabled === true; useHotkeys( [ @@ -71,7 +75,10 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { [ "mod+H", () => { - const event = new CustomEvent("openFindAndReplaceDialogFromEditor", {}); + const event = new CustomEvent( + "openFindAndReplaceDialogFromEditor", + {}, + ); document.dispatchEvent(event); }, ], @@ -100,7 +107,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { {!readOnly && } - + {!workspaceSharingDisabled && } { const event = new CustomEvent("openFindDialogFromEditor", {}); document.dispatchEvent(event); - } + }; const openFindAndReplaceDialog = () => { const event = new CustomEvent("openFindAndReplaceDialogFromEditor", {}); document.dispatchEvent(event); - } + }; return ( <> @@ -250,7 +257,6 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { {t("Replace")} - {watchStatus?.watching ? ( } diff --git a/apps/client/src/pages/settings/shares/shares.tsx b/apps/client/src/pages/settings/shares/shares.tsx index 1a5a118e4e..7462fb1de1 100644 --- a/apps/client/src/pages/settings/shares/shares.tsx +++ b/apps/client/src/pages/settings/shares/shares.tsx @@ -3,12 +3,38 @@ import { Helmet } from "react-helmet-async"; import { getAppName } from "@/lib/config.ts"; import { useTranslation } from "react-i18next"; import ShareList from "@/features/share/components/share-list.tsx"; -import { Alert, Text } from "@mantine/core"; -import { IconInfoCircle } from "@tabler/icons-react"; -import React from "react"; +import { Alert, Switch, Text, Divider, Group } from "@mantine/core"; +import { IconInfoCircle, IconWorld } from "@tabler/icons-react"; +import React, { useState } from "react"; +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts"; +import { notifications } from "@mantine/notifications"; +import useUserRole from "@/hooks/use-user-role.tsx"; export default function Shares() { const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [workspace, setWorkspace] = useAtom(workspaceAtom); + const { isAdmin } = useUserRole(); + const sharingDisabled = workspace?.settings?.sharing?.disabled === true; + + async function handleSharingToggle(checked: boolean) { + setIsLoading(true); + try { + const updatedWorkspace = await updateWorkspace({ + disablePublicSharing: !checked, + }); + setWorkspace(updatedWorkspace); + notifications.show({ message: t("Updated successfully") }); + } catch (err) { + notifications.show({ + message: t("Failed to update data"), + color: "red", + }); + } + setIsLoading(false); + } return ( <> @@ -19,13 +45,63 @@ export default function Shares() { - }> - {t( - "Publicly shared pages from spaces you are a member of will appear here", - )} - + {isAdmin && ( + <> +
+
+ {t("Allow public sharing")} + + {t( + "When enabled, users can share pages publicly via a link. When disabled, existing shared links become inactive.", + )} + +
+
+ + handleSharingToggle(event.currentTarget.checked) + } + disabled={isLoading || !isAdmin} + /> +
+
+ + + + )} + + {sharingDisabled ? ( + }> + + {t("Public sharing is disabled")} + + + {t( + "Public sharing has been disabled at the workspace level. Existing shared links are inactive.", + )} + + + ) : ( + <> + }> + {t( + "Publicly shared pages from spaces you are a member of will appear here", + )} + - + + + )} ); } diff --git a/apps/server/src/core/workspace/services/workspace.service.ts b/apps/server/src/core/workspace/services/workspace.service.ts index 2c5ac9a7be..1853d2de4d 100644 --- a/apps/server/src/core/workspace/services/workspace.service.ts +++ b/apps/server/src/core/workspace/services/workspace.service.ts @@ -41,7 +41,10 @@ import { generateRandomSuffixNumbers, hashPassword, } from '../../../common/helpers'; -import { ChangePasswordDto, ChangeWorkspaceMemberPasswordDto } from '../../auth/dto/change-password.dto'; +import { + ChangePasswordDto, + ChangeWorkspaceMemberPasswordDto, +} from '../../auth/dto/change-password.dto'; import ChangePasswordEmail from '@docmost/transactional/emails/change-password-email'; import ChangeUserPasswordEmail from '@docmost/transactional/emails/change-user-password-email'; import { MailService } from '../../../integrations/mail/mail.service'; @@ -96,7 +99,15 @@ export class WorkspaceService { async getWorkspacePublicData(workspaceId: string) { const workspace = await this.db .selectFrom('workspaces') - .select(['id', 'name', 'logo', 'hostname', 'enforceSso', 'licenseKey', 'plan']) + .select([ + 'id', + 'name', + 'logo', + 'hostname', + 'enforceSso', + 'licenseKey', + 'plan', + ]) .select((eb) => jsonArrayFrom( eb @@ -427,9 +438,6 @@ export class WorkspaceService { updateWorkspaceDto.disablePublicSharing, trx, ); - if (updateWorkspaceDto.disablePublicSharing) { - await this.shareRepo.deleteByWorkspaceId(workspaceId, trx); - } } if (typeof updateWorkspaceDto.mcpEnabled !== 'undefined') { @@ -483,13 +491,7 @@ export class WorkspaceService { }); const columnChanges = diffAuditTrackedFields( - [ - 'name', - 'logo', - 'enforceSso', - 'enforceMfa', - 'emailDomains', - ], + ['name', 'logo', 'enforceSso', 'enforceMfa', 'emailDomains'], updateWorkspaceDto, workspaceBefore, workspace, @@ -520,7 +522,11 @@ export class WorkspaceService { if (user.role === 'owner' || user.role === 'admin') { return this.userRepo.getUsersPaginated(workspaceId, pagination); } - return this.userRepo.getUsersInSpacesOfUser(workspaceId, user.id, pagination); + return this.userRepo.getUsersInSpacesOfUser( + workspaceId, + user.id, + pagination, + ); } async updateWorkspaceUserRole( @@ -825,9 +831,7 @@ export class WorkspaceService { throw new NotFoundException('User not found'); } - if ( - (actorUser.role === UserRole.ADMIN && user.role === UserRole.OWNER) - ) { + if (actorUser.role === UserRole.ADMIN && user.role === UserRole.OWNER) { throw new ForbiddenException(); } @@ -850,7 +854,10 @@ export class WorkspaceService { workspaceId, ); - const emailTemplate = ChangeUserPasswordEmail({ username: user.name, actorUsername: actorUser.name }); + const emailTemplate = ChangeUserPasswordEmail({ + username: user.name, + actorUsername: actorUser.name, + }); await this.mailService.sendToQueue({ to: user.email, subject: 'Your password has been changed', diff --git a/apps/server/src/database/types/entity.types.ts b/apps/server/src/database/types/entity.types.ts index ed56e89bec..125860263e 100644 --- a/apps/server/src/database/types/entity.types.ts +++ b/apps/server/src/database/types/entity.types.ts @@ -58,11 +58,11 @@ export type Graph = { title: string | null; parentPageId: string | null; backlinks: GraphBacklink[]; -} +}; export type GraphBacklink = { sourcePageId: string; targetPageId: string; -} +}; // SpaceMember export type SpaceMember = Selectable;