Преглед изворни кода

Merge pull request #10735 from growilabs/feat/disable-user-pages

feat: Disable user page
Shun Miyazawa пре 2 месеци
родитељ
комит
af906eba52
33 измењених фајлова са 213 додато и 150 уклоњено
  1. 4 4
      apps/app/public/static/locales/en_US/admin.json
  2. 4 4
      apps/app/public/static/locales/fr_FR/admin.json
  3. 4 4
      apps/app/public/static/locales/ja_JP/admin.json
  4. 4 4
      apps/app/public/static/locales/ko_KR/admin.json
  5. 4 4
      apps/app/public/static/locales/zh_CN/admin.json
  6. 12 6
      apps/app/src/client/components/Admin/Security/SecuritySetting/UserPageVisibilitySettings.tsx
  7. 2 2
      apps/app/src/client/components/Admin/Security/SecuritySetting/index.tsx
  8. 59 60
      apps/app/src/client/components/Sidebar/SidebarNav/PersonalDropdown.tsx
  9. 5 5
      apps/app/src/client/services/AdminGeneralSecurityContainer.js
  10. 4 4
      apps/app/src/features/search/client/components/PrivateLegacyPages.tsx
  11. 4 4
      apps/app/src/features/search/client/components/SearchPage/SearchControl.tsx
  12. 3 3
      apps/app/src/features/search/client/components/SearchPage/SearchOptionModal/SearchOptionModal.tsx
  13. 1 1
      apps/app/src/features/search/client/components/SearchPage/SearchOptionModal/dynamic.tsx
  14. 4 4
      apps/app/src/features/search/client/components/SearchPage/SearchPage.tsx
  15. 2 4
      apps/app/src/pages/[[...path]]/page-data-props.ts
  16. 1 3
      apps/app/src/pages/_search/get-server-side-props/index.ts
  17. 1 1
      apps/app/src/pages/_search/types.ts
  18. 2 2
      apps/app/src/pages/_search/use-hydrate-server-configurations.ts
  19. 1 3
      apps/app/src/pages/general-page/configuration-props.ts
  20. 2 2
      apps/app/src/pages/general-page/hydrate.ts
  21. 1 1
      apps/app/src/pages/general-page/types.ts
  22. 26 0
      apps/app/src/pages/share/[[...path]]/page-data-props.ts
  23. 2 2
      apps/app/src/server/models/page.ts
  24. 3 3
      apps/app/src/server/routes/apiv3/bookmarks.ts
  25. 3 3
      apps/app/src/server/routes/apiv3/page-listing.ts
  26. 11 0
      apps/app/src/server/routes/apiv3/page/create-page.ts
  27. 3 3
      apps/app/src/server/routes/apiv3/page/index.ts
  28. 13 0
      apps/app/src/server/routes/apiv3/page/update-page.ts
  29. 17 3
      apps/app/src/server/routes/apiv3/pages/index.js
  30. 7 7
      apps/app/src/server/routes/apiv3/security-settings/index.js
  31. 2 2
      apps/app/src/server/service/config-manager/config-definition.ts
  32. 1 1
      apps/app/src/server/service/page/index.ts
  33. 1 1
      apps/app/src/states/server-configurations/server-configurations.ts

+ 4 - 4
apps/app/public/static/locales/en_US/admin.json

@@ -56,10 +56,10 @@
       "enable_force_delete_user_homepage_on_user_deletion": "When you delete a user, the user's homepage and all its sub pages will be completely deleted",
       "desc": "You will be able to delete a deleted user's homepage."
     },
-    "user_page_visibility": {
-      "user_page_visibility": "User page visibility",
-      "hide_user_pages": "Hide user pages",
-      "desc": "When enabled, all pages under /user will be hidden. Accessing hidden pages will return a 403 error, and they will not appear in page lists or search results."
+    "disable_user_pages": {
+      "disable_user_pages": "Disable user pages",
+      "disable_user_pages_label": "Disable user pages",
+      "desc": "By disabling user pages, creating, viewing, editing, and duplicating all user pages will be disabled.</br>Additionally, user pages will not appear in page trees, recent changes, or search results."
     },
     "session": "Session",
     "max_age": "Max age (msec)",

+ 4 - 4
apps/app/public/static/locales/fr_FR/admin.json

@@ -56,10 +56,10 @@
       "enable_force_delete_user_homepage_on_user_deletion": "Supprimer la page d'accueil et ses pages enfants",
       "desc": "Les pages d'accueil utilisateurs pourront être supprimées."
     },
-    "user_page_visibility": {
-      "user_page_visibility": "Visibilité de la page utilisateur",
-      "hide_user_pages": "Masquer les pages utilisateur",
-      "desc": "Lorsque cette option est activée, toutes les pages sous /user seront masquées. L'accès aux pages masquées renverra une erreur 403, et elles n'apparaîtront pas dans les listes de pages ni dans les résultats de recherche."
+    "disable_user_pages": {
+      "disable_user_pages": "Désactiver les pages utilisateur",
+      "disable_user_pages_label": "Désactiver les pages utilisateur",
+      "desc": "En désactivant les pages utilisateur, la création, la consultation, la modification et la duplication de toutes les pages utilisateur seront désactivées.</br>De plus, les pages utilisateur n'apparaîtront pas dans l'arborescence des pages, les modifications récentes ou les résultats de recherche."
     },
     "session": "Session",
     "max_age": "Âge maximal (ms)",

+ 4 - 4
apps/app/public/static/locales/ja_JP/admin.json

@@ -65,10 +65,10 @@
       "enable_force_delete_user_homepage_on_user_deletion": "ユーザーを削除したとき、ユーザーホームページとその配下のページを完全削除する",
       "desc": "削除済みユーザーのユーザーホームページを削除できるようになります。"
     },
-    "user_page_visibility": {
-      "user_page_visibility": "ユーザーページの表示/非表示",
-      "hide_user_pages": "ユーザーページを非表示にする",
-      "desc": "有効にすると /user 配下のページがすべて非表示になります。非表示のページにアクセスした場合は403エラーを返し、ページリストや検索結果にも表示されなくなります。"
+    "disable_user_pages": {
+      "disable_user_pages": "ユーザーページの無効化",
+      "disable_user_pages_label": "ユーザーページを無効にする",
+      "desc": "ユーザーページを無効にすることで、すべてのユーザーページに対する作成・閲覧・編集・複製ができなくなります。</br>また、ページツリーや最近の変更、検索結果などでもユーザーページが表示されなくなります。"
     },
     "session": "セッション",
     "max_age": "有効期間 (ミリ秒)",

+ 4 - 4
apps/app/public/static/locales/ko_KR/admin.json

@@ -56,10 +56,10 @@
       "enable_force_delete_user_homepage_on_user_deletion": "사용자를 삭제할 때, 사용자의 홈페이지와 모든 하위 페이지가 완전히 삭제됩니다.",
       "desc": "삭제된 사용자의 홈페이지를 삭제할 수 있습니다."
     },
-    "user_page_visibility": {
-      "user_page_visibility": "사용자 페이지 가시성",
-      "hide_user_pages": "사용자 페이지 숨기기",
-      "desc": "활성화하면 /user 하위의 모든 페이지가 숨겨집니다. 숨겨진 페이지에 접근하면 403 오류가 반환되며, 페이지 목록이나 검색 결과에도 표시되지 않습니다."
+    "disable_user_pages": {
+      "disable_user_pages": "사용자 페이지 비활성화",
+      "disable_user_pages_label": "사용자 페이지 비활성화",
+      "desc": "사용자 페이지를 비활성화하면 모든 사용자 페이지의 생성, 조회, 편집 및 복제가 비활성화됩니다.</br>또한, 사용자 페이지는 페이지 트리, 최근 변경 사항 또는 검색 결과에도 표시되지 않습니다."
     },
     "session": "세션",
     "max_age": "최대 수명 (밀리초)",

+ 4 - 4
apps/app/public/static/locales/zh_CN/admin.json

@@ -65,10 +65,10 @@
       "enable_force_delete_user_homepage_on_user_deletion": "删除用户时,该用户的主页及其所有子页面将被完全删除",
       "desc": "您可以删除已删除用户的主页。"
     },
-    "user_page_visibility": {
-      "user_page_visibility": "用户页面可见性",
-      "hide_user_pages": "隐藏用户页面",
-      "desc": "启用后,/user 下的所有页面都将被隐藏。访问隐藏页面时将返回403错误,并且这些页面不会出现在页面列表或搜索结果中。"
+    "disable_user_pages": {
+      "disable_user_pages": "禁用用户页面",
+      "disable_user_pages_label": "禁用用户页面",
+      "desc": "通过禁用用户页面,将无法创建、查看、编辑和复制所有用户页面。</br>此外,用户页面也不会出现在页面树、最近更改或搜索结果中。"
     },
     "session": "会议",
     "max_age": "有效期间  (msec)",

+ 12 - 6
apps/app/src/client/components/Admin/Security/SecuritySetting/UserPageVisibilitySettings.tsx

@@ -15,7 +15,7 @@ export const UserPageVisibilitySettings: React.FC<Props> = ({
   return (
     <>
       <h4 className="mb-3">
-        {t('security_settings.user_page_visibility.user_page_visibility')}
+        {t('security_settings.disable_user_pages.disable_user_pages')}
       </h4>
       <div className="row mb-4">
         <div className="col-md-10 offset-md-2">
@@ -24,7 +24,7 @@ export const UserPageVisibilitySettings: React.FC<Props> = ({
               type="checkbox"
               className="form-check-input"
               id="is-user-pages-visible"
-              checked={adminGeneralSecurityContainer.state.isHidingUserPages}
+              checked={adminGeneralSecurityContainer.state.disableUserPages}
               onChange={() => {
                 adminGeneralSecurityContainer.changeUserPageVisibility();
               }}
@@ -33,12 +33,18 @@ export const UserPageVisibilitySettings: React.FC<Props> = ({
               className="form-label form-check-label"
               htmlFor="is-user-pages-visible"
             >
-              {t('security_settings.user_page_visibility.hide_user_pages')}
+              {t(
+                'security_settings.disable_user_pages.disable_user_pages_label',
+              )}
             </label>
           </div>
-          <p className="form-text text-muted small mt-2">
-            {t('security_settings.user_page_visibility.desc')}
-          </p>
+          <p
+            className="form-text text-muted small mt-2"
+            // biome-ignore lint/security/noDangerouslySetInnerHtml: includes <br> and <code> from i18n strings
+            dangerouslySetInnerHTML={{
+              __html: t('security_settings.disable_user_pages.desc'),
+            }}
+          />
         </div>
       </div>
     </>

+ 2 - 2
apps/app/src/client/components/Admin/Security/SecuritySetting/index.tsx

@@ -64,8 +64,8 @@ const SecuritySettingComponent: React.FC<Props> = ({
           hideRestrictedByOwner:
             adminGeneralSecurityContainer.state
               .currentOwnerRestrictionDisplayMode === 'Hidden',
-          isHidingUserPages:
-            adminGeneralSecurityContainer.state.isHidingUserPages,
+          disableUserPages:
+            adminGeneralSecurityContainer.state.disableUserPages,
           isUsersHomepageDeletionEnabled:
             adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled,
           isForceDeleteUserHomepageOnUserDeletion:

+ 59 - 60
apps/app/src/client/components/Sidebar/SidebarNav/PersonalDropdown.tsx

@@ -2,6 +2,7 @@ import type { JSX } from 'react';
 import Link from 'next/link';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
+import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
 import {
   DropdownItem,
@@ -13,6 +14,7 @@ import {
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { useCurrentUser } from '~/states/global';
+import { disableUserPagesAtom } from '~/states/server-configurations';
 
 import { SkeletonItem } from './SkeletonItem';
 
@@ -22,6 +24,8 @@ export const PersonalDropdown = (): JSX.Element => {
   const { t } = useTranslation('commons');
   const currentUser = useCurrentUser();
 
+  const disableUserPages = useAtomValue(disableUserPagesAtom);
+
   if (currentUser == null) {
     return <SkeletonItem />;
   }
@@ -36,41 +40,41 @@ export const PersonalDropdown = (): JSX.Element => {
   };
 
   return (
-    <>
-      <UncontrolledDropdown direction="end">
-        <DropdownToggle
-          className={`btn btn-primary ${styles['btn-personal-dropdown']} opacity-100`}
-          data-testid="personal-dropdown-button"
-        >
-          <UserPicture user={currentUser} noLink noTooltip />
-        </DropdownToggle>
+    <UncontrolledDropdown direction="end">
+      <DropdownToggle
+        className={`btn btn-primary ${styles['btn-personal-dropdown']} opacity-100`}
+        data-testid="personal-dropdown-button"
+      >
+        <UserPicture user={currentUser} noLink noTooltip />
+      </DropdownToggle>
 
-        <DropdownMenu
-          container="body"
-          data-testid="personal-dropdown-menu"
-          className={styles['personal-dropdown-menu']}
-        >
-          <DropdownItem className={styles['personal-dropdown-header']} header>
-            <div className="mt-2 mb-3">
-              <UserPicture user={currentUser} size="lg" noLink noTooltip />
-            </div>
-            <div className="ms-1 fs-6">{currentUser.name}</div>
-            <div className="d-flex align-items-center my-2">
-              <small className="material-symbols-outlined me-1 pb-0 fs-6">
-                person
-              </small>
-              <span>{currentUser.username}</span>
-            </div>
-            <div className="d-flex align-items-center">
-              <span className="material-symbols-outlined me-1 pb-0 fs-6">
-                mail
-              </span>
-              <span className="item-text-email">{currentUser.email}</span>
-            </div>
-          </DropdownItem>
+      <DropdownMenu
+        container="body"
+        data-testid="personal-dropdown-menu"
+        className={styles['personal-dropdown-menu']}
+      >
+        <DropdownItem className={styles['personal-dropdown-header']} header>
+          <div className="mt-2 mb-3">
+            <UserPicture user={currentUser} size="lg" noLink noTooltip />
+          </div>
+          <div className="ms-1 fs-6">{currentUser.name}</div>
+          <div className="d-flex align-items-center my-2">
+            <small className="material-symbols-outlined me-1 pb-0 fs-6">
+              person
+            </small>
+            <span>{currentUser.username}</span>
+          </div>
+          <div className="d-flex align-items-center">
+            <span className="material-symbols-outlined me-1 pb-0 fs-6">
+              mail
+            </span>
+            <span className="item-text-email">{currentUser.email}</span>
+          </div>
+        </DropdownItem>
 
-          <DropdownItem className="my-3" divider />
+        <DropdownItem className="my-3" divider />
 
+        {!disableUserPages && (
           <Link
             href={pagePathUtils.userHomepagePath(currentUser)}
             data-testid="grw-personal-dropdown-menu-user-home"
@@ -86,39 +90,34 @@ export const PersonalDropdown = (): JSX.Element => {
               </span>
             </DropdownItem>
           </Link>
+        )}
 
-          <Link
-            href="/me"
-            data-testid="grw-personal-dropdown-menu-user-settings"
-          >
-            <DropdownItem
-              className={`my-1 ${styles['personal-dropdown-item']}`}
-            >
-              <span className="d-flex align-items-center">
-                <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">
-                  discover_tune
-                </span>
-                <span className="item-text">
-                  {t('personal_dropdown.settings')}
-                </span>
-              </span>
-            </DropdownItem>
-          </Link>
-
-          <DropdownItem
-            data-testid="logout-button"
-            onClick={logoutHandler}
-            className={`my-1 ${styles['personal-dropdown-item']}`}
-          >
+        <Link href="/me" data-testid="grw-personal-dropdown-menu-user-settings">
+          <DropdownItem className={`my-1 ${styles['personal-dropdown-item']}`}>
             <span className="d-flex align-items-center">
               <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">
-                logout
+                discover_tune
+              </span>
+              <span className="item-text">
+                {t('personal_dropdown.settings')}
               </span>
-              <span className="item-text">{t('Sign out')}</span>
             </span>
           </DropdownItem>
-        </DropdownMenu>
-      </UncontrolledDropdown>
-    </>
+        </Link>
+
+        <DropdownItem
+          data-testid="logout-button"
+          onClick={logoutHandler}
+          className={`my-1 ${styles['personal-dropdown-item']}`}
+        >
+          <span className="d-flex align-items-center">
+            <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">
+              logout
+            </span>
+            <span className="item-text">{t('Sign out')}</span>
+          </span>
+        </DropdownItem>
+      </DropdownMenu>
+    </UncontrolledDropdown>
   );
 };

+ 5 - 5
apps/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -45,7 +45,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       expandOtherOptionsForCompleteDeletion: false,
       isShowRestrictedByOwner: false,
       isUsersHomepageDeletionEnabled: false,
-      isHidingUserPages: false,
+      disableUserPages: false,
       isForceDeleteUserHomepageOnUserDeletion: false,
       isRomUserAllowedToComment: false,
       isLocalEnabled: false,
@@ -107,7 +107,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       isForceDeleteUserHomepageOnUserDeletion:
         generalSetting.isForceDeleteUserHomepageOnUserDeletion,
       isRomUserAllowedToComment: generalSetting.isRomUserAllowedToComment,
-      isHidingUserPages: generalSetting.isHidingUserPages,
+      disableUserPages: generalSetting.disableUserPages,
       sessionMaxAge: generalSetting.sessionMaxAge,
       wikiMode: generalSetting.wikiMode,
       disableLinkSharing: shareLinkSetting.disableLinkSharing,
@@ -180,7 +180,7 @@ export default class AdminGeneralSecurityContainer extends Container {
   }
 
   changeUserPageVisibility() {
-    this.setState({ isHidingUserPages: !this.state.isHidingUserPages });
+    this.setState({ disableUserPages: !this.state.disableUserPages });
   }
 
   /**
@@ -291,7 +291,7 @@ export default class AdminGeneralSecurityContainer extends Container {
               formData.isAllGroupMembershipRequiredForPageCompleteDeletion,
             hideRestrictedByGroup: formData.hideRestrictedByGroup,
             hideRestrictedByOwner: formData.hideRestrictedByOwner,
-            isHidingUserPages: formData.isHidingUserPages,
+            disableUserPages: formData.disableUserPages,
             isUsersHomepageDeletionEnabled:
               formData.isUsersHomepageDeletionEnabled,
             isForceDeleteUserHomepageOnUserDeletion:
@@ -314,7 +314,7 @@ export default class AdminGeneralSecurityContainer extends Container {
               this.state.currentGroupRestrictionDisplayMode === 'Hidden',
             hideRestrictedByOwner:
               this.state.currentOwnerRestrictionDisplayMode === 'Hidden',
-            isHidingUserPages: this.state.isHidingUserPages,
+            disableUserPages: this.state.disableUserPages,
             isUsersHomepageDeletionEnabled:
               this.state.isUsersHomepageDeletionEnabled,
             isForceDeleteUserHomepageOnUserDeletion:

+ 4 - 4
apps/app/src/features/search/client/components/PrivateLegacyPages.tsx

@@ -36,7 +36,7 @@ import type { PageMigrationErrorData } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import { useIsAdmin } from '~/states/context';
 import { useSearchKeyword, useSetSearchKeyword } from '~/states/search';
-import { isHidingUserPagesAtom } from '~/states/server-configurations';
+import { disableUserPagesAtom } from '~/states/server-configurations';
 import { useGlobalSocket } from '~/states/socket-io';
 import type { ILegacyPrivatePage } from '~/states/ui/modal/private-legacy-pages-migration';
 import { usePrivateLegacyPagesMigrationModalActions } from '~/states/ui/modal/private-legacy-pages-migration';
@@ -270,7 +270,7 @@ const PrivateLegacyPages = (): JSX.Element => {
   const keyword = useSearchKeyword();
   const setSearchKeyword = useSetSearchKeyword('/_private-legacy-pages');
 
-  const isHidingUserPages = useAtomValue(isHidingUserPagesAtom);
+  const disableUserPages = useAtomValue(disableUserPagesAtom);
 
   const [offset, setOffset] = useState<number>(0);
   const [limit, setLimit] = useState<number>(INITIAL_PAGING_SIZE);
@@ -519,7 +519,7 @@ const PrivateLegacyPages = (): JSX.Element => {
   const searchControl = useMemo(() => {
     return (
       <SearchControl
-        isHidingUserPages={isHidingUserPages}
+        disableUserPages={disableUserPages}
         isEnableSort={false}
         isEnableFilter={false}
         initialSearchConditions={{ keyword: initQ, limit: INITIAL_PAGING_SIZE }}
@@ -527,7 +527,7 @@ const PrivateLegacyPages = (): JSX.Element => {
         extraControls={extraControls}
       />
     );
-  }, [searchInvokedHandler, extraControls, isHidingUserPages]);
+  }, [searchInvokedHandler, extraControls, disableUserPages]);
 
   const searchResultListHead = useMemo(() => {
     if (data == null) {

+ 4 - 4
apps/app/src/features/search/client/components/SearchPage/SearchControl.tsx

@@ -12,7 +12,7 @@ import SortControl from './SortControl';
 type Props = {
   isEnableSort: boolean;
   isEnableFilter: boolean;
-  isHidingUserPages: boolean;
+  disableUserPages: boolean;
   initialSearchConditions: Partial<ISearchConditions>;
 
   onSearchInvoked?: (
@@ -30,7 +30,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
   const {
     isEnableSort,
     isEnableFilter,
-    isHidingUserPages,
+    disableUserPages,
     initialSearchConditions,
     onSearchInvoked,
     extraControls,
@@ -154,7 +154,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
               </button>
             </div>
             <div className="d-none d-lg-flex align-items-center search-control-include-options">
-              {isHidingUserPages === false && (
+              {disableUserPages === false && (
                 <div className="px-2 py-1">
                   <div className="form-check form-check-succsess">
                     <input
@@ -212,7 +212,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
       <SearchOptionModalLazyLoaded
         isOpen={isFileterOptionModalShown || false}
         onClose={() => setIsFileterOptionModalShown(false)}
-        isHidingUserPages={isHidingUserPages}
+        disableUserPages={disableUserPages}
         includeUserPages={includeUserPages}
         includeTrashPages={includeTrashPages}
         onIncludeUserPagesSwitched={setIncludeUserPages}

+ 3 - 3
apps/app/src/features/search/client/components/SearchPage/SearchOptionModal/SearchOptionModal.tsx

@@ -6,7 +6,7 @@ type Props = {
   isOpen: boolean;
   includeUserPages: boolean;
   includeTrashPages: boolean;
-  isHidingUserPages: boolean;
+  disableUserPages: boolean;
   onClose?: () => void;
   onIncludeUserPagesSwitched?: (isChecked: boolean) => void;
   onIncludeTrashPagesSwitched?: (isChecked: boolean) => void;
@@ -19,7 +19,7 @@ export const SearchOptionModal: FC<Props> = (props: Props) => {
     isOpen,
     includeUserPages,
     includeTrashPages,
-    isHidingUserPages,
+    disableUserPages,
     onClose,
     onIncludeUserPagesSwitched,
     onIncludeTrashPagesSwitched,
@@ -57,7 +57,7 @@ export const SearchOptionModal: FC<Props> = (props: Props) => {
       </ModalHeader>
       <ModalBody>
         <div className="d-flex p-2">
-          {!isHidingUserPages && (
+          {!disableUserPages && (
             <div className="me-3">
               <label className="form-label px-3 py-2 mb-0 d-flex align-items-center">
                 <input

+ 1 - 1
apps/app/src/features/search/client/components/SearchPage/SearchOptionModal/dynamic.tsx

@@ -6,7 +6,7 @@ type SearchOptionModalProps = {
   isOpen: boolean;
   includeUserPages: boolean;
   includeTrashPages: boolean;
-  isHidingUserPages: boolean;
+  disableUserPages: boolean;
   onClose?: () => void;
   onIncludeUserPagesSwitched?: (isChecked: boolean) => void;
   onIncludeTrashPagesSwitched?: (isChecked: boolean) => void;

+ 4 - 4
apps/app/src/features/search/client/components/SearchPage/SearchPage.tsx

@@ -12,7 +12,7 @@ import type {
 import type { IFormattedSearchResult } from '~/interfaces/search';
 import { useSearchKeyword, useSetSearchKeyword } from '~/states/search';
 import {
-  isHidingUserPagesAtom,
+  disableUserPagesAtom,
   showPageLimitationLAtom,
 } from '~/states/server-configurations';
 import {
@@ -109,7 +109,7 @@ export const SearchPage = (): JSX.Element => {
   const keyword = useSearchKeyword();
   const setSearchKeyword = useSetSearchKeyword();
 
-  const isHidingUserPages = useAtomValue(isHidingUserPagesAtom);
+  const disableUserPages = useAtomValue(disableUserPagesAtom);
 
   const [offset, setOffset] = useState<number>(0);
   const [limit, setLimit] = useState<number>(
@@ -291,7 +291,7 @@ export const SearchPage = (): JSX.Element => {
       <SearchControl
         isEnableSort
         isEnableFilter
-        isHidingUserPages={isHidingUserPages}
+        disableUserPages={disableUserPages}
         initialSearchConditions={initialSearchConditions}
         onSearchInvoked={searchInvokedHandler}
         extraControls={extraControls}
@@ -304,7 +304,7 @@ export const SearchPage = (): JSX.Element => {
     collapseContents,
     initialSearchConditions,
     isCollapsed,
-    isHidingUserPages,
+    disableUserPages,
     searchInvokedHandler,
   ]);
 

+ 2 - 4
apps/app/src/pages/[[...path]]/page-data-props.ts

@@ -164,11 +164,9 @@ export async function getPageDataForInitial(
     { pageId, path: resolvedPagePath, user },
   );
 
-  const isHidingUserPages = configManager.getConfig(
-    'security:isHidingUserPages',
-  );
+  const disableUserPages = configManager.getConfig('security:disableUserPages');
 
-  if (isHidingUserPages && pageWithMeta.data != null) {
+  if (disableUserPages && pageWithMeta.data != null) {
     const pagePath = pageWithMeta.data.path;
     const isTargetUserPage = isUserPage(pagePath) || isUsersTopPage(pagePath);
 

+ 1 - 3
apps/app/src/pages/_search/get-server-side-props/index.ts

@@ -31,9 +31,7 @@ const getServerSideConfigurationProps: GetServerSideProps<
         showPageLimitationL: configManager.getConfig(
           'customize:showPageLimitationL',
         ),
-        isHidingUserPages: configManager.getConfig(
-          'security:isHidingUserPages',
-        ),
+        disableUserPages: configManager.getConfig('security:disableUserPages'),
       },
     },
   };

+ 1 - 1
apps/app/src/pages/_search/types.ts

@@ -2,6 +2,6 @@ export type ServerConfigurationProps = {
   serverConfig: {
     isContainerFluid: boolean;
     showPageLimitationL: number;
-    isHidingUserPages: boolean;
+    disableUserPages: boolean;
   };
 };

+ 2 - 2
apps/app/src/pages/_search/use-hydrate-server-configurations.ts

@@ -2,8 +2,8 @@ import { useHydrateAtoms } from 'jotai/utils';
 
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import {
+  disableUserPagesAtom,
   isContainerFluidAtom,
-  isHidingUserPagesAtom,
   rendererConfigAtom,
   showPageLimitationLAtom,
 } from '~/states/server-configurations';
@@ -26,7 +26,7 @@ export const useHydrateServerConfigurationAtoms = (
           [isContainerFluidAtom, serverConfig.isContainerFluid],
           [showPageLimitationLAtom, serverConfig.showPageLimitationL],
           [rendererConfigAtom, rendererConfigs],
-          [isHidingUserPagesAtom, serverConfig.isHidingUserPages],
+          [disableUserPagesAtom, serverConfig.disableUserPages],
         ],
   );
 };

+ 1 - 3
apps/app/src/pages/general-page/configuration-props.ts

@@ -120,9 +120,7 @@ export const getServerSideGeneralPageProps: GetServerSideProps<
         isEnabledAttachTitleHeader: configManager.getConfig(
           'customize:isEnabledAttachTitleHeader',
         ),
-        isHidingUserPages: configManager.getConfig(
-          'security:isHidingUserPages',
-        ),
+        disableUserPages: configManager.getConfig('security:disableUserPages'),
       },
     },
   };

+ 2 - 2
apps/app/src/pages/general-page/hydrate.ts

@@ -5,6 +5,7 @@ import {
   aiEnabledAtom,
   defaultIndentSizeAtom,
   disableLinkSharingAtom,
+  disableUserPagesAtom,
   drawioUriAtom,
   elasticsearchMaxBodyLengthToIndexAtom,
   isAclEnabledAtom,
@@ -13,7 +14,6 @@ import {
   isContainerFluidAtom,
   isEnabledAttachTitleHeaderAtom,
   isEnabledStaleNotificationAtom,
-  isHidingUserPagesAtom,
   isIndentSizeForcedAtom,
   isLocalAccountRegistrationEnabledAtom,
   isPdfBulkExportEnabledAtom,
@@ -85,7 +85,7 @@ export const useHydrateGeneralPageConfigurationAtoms = (
             serverConfig.isLocalAccountRegistrationEnabled,
           ],
           [rendererConfigAtom, rendererConfigs],
-          [isHidingUserPagesAtom, serverConfig.isHidingUserPages],
+          [disableUserPagesAtom, serverConfig.disableUserPages],
         ],
   );
 };

+ 1 - 1
apps/app/src/pages/general-page/types.ts

@@ -32,7 +32,7 @@ export type ServerConfigurationProps = {
     isEnabledStaleNotification: boolean;
     disableLinkSharing: boolean;
     isIndentSizeForced: boolean;
-    isHidingUserPages: boolean;
+    disableUserPages: boolean;
     isEnabledAttachTitleHeader: boolean;
     isSlackConfigured: boolean;
     isAclEnabled: boolean;

+ 26 - 0
apps/app/src/pages/share/[[...path]]/page-data-props.ts

@@ -1,6 +1,10 @@
 import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
 import type { IPage } from '@growi/core';
 import { getIdStringForRef } from '@growi/core';
+import {
+  isUserPage,
+  isUsersTopPage,
+} from '@growi/core/dist/utils/page-path-utils';
 import type { model } from 'mongoose';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -68,6 +72,28 @@ export const getPageDataForInitial = async (
     return notFoundProps;
   }
 
+  const disableUserPages = configManager.getConfig('security:disableUserPages');
+  if (
+    disableUserPages &&
+    (isUserPage(pageWithMeta.data.path) ||
+      isUsersTopPage(pageWithMeta.data.path))
+  ) {
+    return {
+      props: {
+        isNotFound: true,
+        pageWithMeta: {
+          data: null,
+          meta: {
+            isNotFound: true,
+            isForbidden: true,
+          },
+        },
+        isExpired: undefined,
+        shareLink: undefined,
+      },
+    };
+  }
+
   // expired
   if (shareLink.isExpired()) {
     const populatedPage =

+ 2 - 2
apps/app/src/server/models/page.ts

@@ -89,7 +89,7 @@ export type FindRecentUpdatedPagesOption = {
   desc: number;
   hideRestrictedByOwner: boolean;
   hideRestrictedByGroup: boolean;
-  hideUserPages: boolean;
+  disableUserPages: boolean;
 };
 
 export type CreateMethod = (
@@ -937,7 +937,7 @@ schema.statics.findRecentUpdatedPages = async function (
   const baseQuery = this.find({});
   const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
 
-  if (options.hideUserPages) {
+  if (options.disableUserPages) {
     queryBuilder.addConditionToListByNotMatchPathAndChildren('/user');
   }
 

+ 3 - 3
apps/app/src/server/routes/apiv3/bookmarks.ts

@@ -257,11 +257,11 @@ module.exports = (crowi) => {
           })
           .exec();
 
-        const isHidingUserPages = configManager.getConfig(
-          'security:isHidingUserPages',
+        const disabledUserPage = configManager.getConfig(
+          'security:disableUserPages',
         );
 
-        const filteredBookmarks = isHidingUserPages
+        const filteredBookmarks = disabledUserPage
           ? userRootBookmarks.filter(
               (bookmark) =>
                 bookmark.page != null &&

+ 3 - 3
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -159,8 +159,8 @@ const routerFactory = (crowi: Crowi): Router => {
       const hideRestrictedByGroup = await configManager.getConfig(
         'security:list-policy:hideRestrictedByGroup',
       );
-      const hideUserPages = await configManager.getConfig(
-        'security:isHidingUserPages',
+      const disableUserPages = await configManager.getConfig(
+        'security:disableUserPages',
       );
 
       try {
@@ -172,7 +172,7 @@ const routerFactory = (crowi: Crowi): Router => {
             !hideRestrictedByGroup,
           );
 
-        if (hideUserPages === true) {
+        if (disableUserPages) {
           pages = pages.filter(
             (page) => !isUserPage(page.path) && !isUsersTopPage(page.path),
           );

+ 11 - 0
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -6,6 +6,7 @@ import {
   isCreatablePage,
   isUserPage,
   isUsersHomepage,
+  isUsersTopPage,
 } from '@growi/core/dist/utils/page-path-utils';
 import {
   attachTitleHeader,
@@ -310,6 +311,16 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
         );
       }
 
+      const disableUserPages = configManager.getConfig(
+        'security:disableUserPages',
+      );
+      if (
+        disableUserPages &&
+        (isUsersTopPage(pathToCreate) || isUserPage(pathToCreate))
+      ) {
+        return res.apiv3Err('User pages are disabled');
+      }
+
       if (isUserPage(pathToCreate)) {
         const isExistUser = await User.isExistUserByUserPagePath(pathToCreate);
         if (!isExistUser) {

+ 3 - 3
apps/app/src/server/routes/apiv3/page/index.ts

@@ -197,8 +197,8 @@ module.exports = (crowi: Crowi) => {
       const { pageId, path, findAll, revisionId, shareLinkId, includeEmpty } =
         req.query;
 
-      const isHidingUserPages = crowi.configManager.getConfig(
-        'security:isHidingUserPages',
+      const disableUserPages = crowi.configManager.getConfig(
+        'security:disableUserPages',
       );
 
       const respondWithSinglePage = async (
@@ -227,7 +227,7 @@ module.exports = (crowi: Crowi) => {
           );
         }
 
-        if (isHidingUserPages && page != null) {
+        if (disableUserPages && page != null) {
           const isTargetUserPage =
             isUserPage(page.path) || isUsersTopPage(page.path);
 

+ 13 - 0
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -5,7 +5,9 @@ import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import {
   isTopPage,
+  isUserPage,
   isUsersProtectedPages,
+  isUsersTopPage,
 } from '@growi/core/dist/utils/page-path-utils';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
@@ -29,6 +31,7 @@ import {
   serializePageSecurely,
   serializeRevisionSecurely,
 } from '~/server/models/serializers';
+import { configManager } from '~/server/service/config-manager/config-manager';
 import { preNotifyService } from '~/server/service/pre-notify';
 import { normalizeLatestRevisionIfBroken } from '~/server/service/revision/normalize-latest-revision-if-broken';
 import { getYjsService } from '~/server/service/yjs';
@@ -224,6 +227,16 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
         );
       }
 
+      const disableUserPages = configManager.getConfig(
+        'security:disableUserPages',
+      );
+      if (
+        disableUserPages &&
+        (isUsersTopPage(currentPage.path) || isUserPage(currentPage.path))
+      ) {
+        return res.apiv3Err('User pages are disabled');
+      }
+
       const isGrantImmutable =
         isTopPage(currentPage.path) || isUsersProtectedPages(currentPage.path);
 

+ 17 - 3
apps/app/src/server/routes/apiv3/pages/index.js

@@ -6,6 +6,7 @@ import {
   isCreatablePage,
   isTrashPage,
   isUserPage,
+  isUsersTopPage,
 } from '@growi/core/dist/utils/page-path-utils';
 import {
   addHeadingSlash,
@@ -193,8 +194,8 @@ module.exports = (crowi) => {
       const hideRestrictedByGroup = configManager.getConfig(
         'security:list-policy:hideRestrictedByGroup',
       );
-      const hideUserPages = configManager.getConfig(
-        'security:isHidingUserPages',
+      const disableUserPages = configManager.getConfig(
+        'security:disableUserPages',
       );
 
       /**
@@ -210,7 +211,7 @@ module.exports = (crowi) => {
         desc: -1,
         hideRestrictedByOwner,
         hideRestrictedByGroup,
-        hideUserPages,
+        disableUserPages,
       };
 
       try {
@@ -782,6 +783,19 @@ module.exports = (crowi) => {
       }
 
       const page = await Page.findByIdAndViewer(pageId, req.user, null, true);
+      const disableUserPages = configManager.getConfig(
+        'security:disableUserPages',
+      );
+      if (disableUserPages) {
+        if (
+          isUsersTopPage(newPagePath) ||
+          isUserPage(newPagePath) ||
+          isUsersTopPage(page.path) ||
+          isUserPage(page.path)
+        ) {
+          return res.apiv3Err('User pages are disabled');
+        }
+      }
 
       const isEmptyAndNotRecursively = page?.isEmpty && !isRecursively;
       if (page == null || isEmptyAndNotRecursively) {

+ 7 - 7
apps/app/src/server/routes/apiv3/security-settings/index.js

@@ -47,7 +47,7 @@ const validator = {
     body('hideRestrictedByGroup')
       .if((value) => value != null)
       .isBoolean(),
-    body('isHidingUserPages')
+    body('disableUserPages')
       .if((value) => value != null)
       .isBoolean(),
     body('isUsersHomepageDeletionEnabled')
@@ -220,7 +220,7 @@ const validator = {
  *          pageCompleteDeletionAuthority:
  *            type: string
  *            description: type of pageDeletionAuthority
- *          isHidingUserPages:
+ *          disableUserPages:
  *            type: boolean
  *            description: hide all user pages from general users
  *          hideRestrictedByOwner:
@@ -511,8 +511,8 @@ module.exports = (crowi) => {
           hideRestrictedByGroup: await configManager.getConfig(
             'security:list-policy:hideRestrictedByGroup',
           ),
-          isHidingUserPages: await configManager.getConfig(
-            'security:isHidingUserPages',
+          disableUserPages: await configManager.getConfig(
+            'security:disableUserPages',
           ),
           isUsersHomepageDeletionEnabled: await configManager.getConfig(
             'security:user-homepage-deletion:isEnabled',
@@ -1004,7 +1004,7 @@ module.exports = (crowi) => {
           req.body.hideRestrictedByOwner,
         'security:list-policy:hideRestrictedByGroup':
           req.body.hideRestrictedByGroup,
-        'security:isHidingUserPages': req.body.isHidingUserPages,
+        'security:disableUserPages': req.body.disableUserPages,
         'security:user-homepage-deletion:isEnabled':
           req.body.isUsersHomepageDeletionEnabled,
         // Validate user-homepage-deletion config
@@ -1077,8 +1077,8 @@ module.exports = (crowi) => {
           hideRestrictedByGroup: await configManager.getConfig(
             'security:list-policy:hideRestrictedByGroup',
           ),
-          isHidingUserPages: await configManager.getConfig(
-            'security:isHidingUserPages',
+          disableUserPages: await configManager.getConfig(
+            'security:disableUserPages',
           ),
           isUsersHomepageDeletionEnabled: await configManager.getConfig(
             'security:user-homepage-deletion:isEnabled',

+ 2 - 2
apps/app/src/server/service/config-manager/config-definition.ts

@@ -115,7 +115,7 @@ export const CONFIG_KEYS = [
   'security:pageRecursiveDeletionAuthority',
   'security:pageRecursiveCompleteDeletionAuthority',
   'security:isAllGroupMembershipRequiredForPageCompleteDeletion',
-  'security:isHidingUserPages',
+  'security:disableUserPages',
   'security:user-homepage-deletion:isEnabled',
   'security:user-homepage-deletion:isForceDeleteUserHomepageOnUserDeletion',
   'security:isRomUserAllowedToComment',
@@ -679,7 +679,7 @@ export const CONFIG_DEFINITIONS = {
     defineConfig<boolean>({
       defaultValue: true,
     }),
-  'security:isHidingUserPages': defineConfig<boolean>({
+  'security:disableUserPages': defineConfig<boolean>({
     defaultValue: false,
   }),
   'security:user-homepage-deletion:isEnabled': defineConfig<boolean>({

+ 1 - 1
apps/app/src/server/service/page/index.ts

@@ -799,7 +799,7 @@ class PageService implements IPageService {
   getExcludedPathsBySystem(): string[] {
     const excludedPaths: string[] = [];
 
-    if (configManager.getConfig('security:isHidingUserPages')) {
+    if (configManager.getConfig('security:disableUserPages')) {
       excludedPaths.push('/user');
     }
 

+ 1 - 1
apps/app/src/states/server-configurations/server-configurations.ts

@@ -146,7 +146,7 @@ export const isPdfBulkExportEnabledAtom = atom<boolean>(false);
 /**
  * Atom for hiding user pages setting enabled
  */
-export const isHidingUserPagesAtom = atom<boolean>(false);
+export const disableUserPagesAtom = atom<boolean>(false);
 
 /**
  * Atom for local account registration enabled