Просмотр исходного кода

Merge branch 'master' into feat/10814-remark-drawio-plugin

Yuki Takei 3 лет назад
Родитель
Сommit
b6b6395ded
61 измененных файлов с 424 добавлено и 216 удалено
  1. 2 1
      packages/app/cypress.json
  2. 1 1
      packages/app/public/static/locales/en_US/translation.json
  3. 1 1
      packages/app/public/static/locales/ja_JP/translation.json
  4. 1 1
      packages/app/public/static/locales/zh_CN/translation.json
  5. 1 1
      packages/app/src/client/interfaces/global-notification.ts
  6. 8 0
      packages/app/src/client/interfaces/notification.ts
  7. 28 3
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  8. 1 3
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  9. 1 1
      packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  10. 0 43
      packages/app/src/components/Admin/Notification/NotificationTypeIcon.jsx
  11. 30 0
      packages/app/src/components/Admin/Notification/NotificationTypeIcon.tsx
  12. 1 1
      packages/app/src/components/Admin/Notification/UserNotificationRow.jsx
  13. 24 0
      packages/app/src/components/CompleteUserRegistration.tsx
  14. 24 5
      packages/app/src/components/CompleteUserRegistrationForm.tsx
  15. 22 12
      packages/app/src/components/LoginForm.tsx
  16. 14 3
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  17. 1 0
      packages/app/src/components/Navbar/PageEditorModeManager.jsx
  18. 23 18
      packages/app/src/components/Navbar/SubNavButtons.tsx
  19. 2 6
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  20. 1 1
      packages/app/src/components/PageEditor/Editor.tsx
  21. 14 3
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  22. 16 9
      packages/app/src/components/UncontrolledCodeMirror.tsx
  23. 7 0
      packages/app/src/interfaces/registration-mode.ts
  24. 5 5
      packages/app/src/pages/[[...path]].page.tsx
  25. 5 1
      packages/app/src/pages/_search.page.tsx
  26. 2 0
      packages/app/src/pages/admin/[...path].page.tsx
  27. 4 0
      packages/app/src/pages/admin/app.page.tsx
  28. 2 1
      packages/app/src/pages/admin/audit-log.page.tsx
  29. 2 1
      packages/app/src/pages/admin/customize.page.tsx
  30. 2 0
      packages/app/src/pages/admin/export.page.tsx
  31. 3 0
      packages/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx
  32. 2 0
      packages/app/src/pages/admin/global-notification/new.page.tsx
  33. 2 0
      packages/app/src/pages/admin/importer.page.tsx
  34. 2 2
      packages/app/src/pages/admin/index.page.tsx
  35. 3 0
      packages/app/src/pages/admin/markdown.page.tsx
  36. 2 0
      packages/app/src/pages/admin/notification.page.tsx
  37. 2 1
      packages/app/src/pages/admin/search.page.tsx
  38. 2 1
      packages/app/src/pages/admin/security.page.tsx
  39. 2 0
      packages/app/src/pages/admin/slack-integration-legacy.page.tsx
  40. 2 1
      packages/app/src/pages/admin/slack-integration.page.tsx
  41. 2 1
      packages/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx
  42. 2 1
      packages/app/src/pages/admin/user-groups.page.tsx
  43. 2 0
      packages/app/src/pages/admin/users/external-accounts.page.tsx
  44. 2 6
      packages/app/src/pages/admin/users/index.page.tsx
  45. 7 6
      packages/app/src/pages/login.page.tsx
  46. 1 4
      packages/app/src/pages/me/[[...path]].page.tsx
  47. 1 6
      packages/app/src/pages/share/[[...path]].page.tsx
  48. 4 0
      packages/app/src/pages/user-activation.page.tsx
  49. 14 4
      packages/app/src/pages/utils/commons.ts
  50. 1 3
      packages/app/src/server/routes/apiv3/customize-setting.js
  51. 1 0
      packages/app/src/server/routes/apiv3/index.js
  52. 37 1
      packages/app/src/server/routes/apiv3/user-activation.ts
  53. 44 39
      packages/app/src/server/routes/login.js
  54. 0 7
      packages/app/src/server/service/page.ts
  55. 4 0
      packages/app/src/stores/context.tsx
  56. 13 2
      packages/app/src/utils/admin-page-util.ts
  57. 7 2
      packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts
  58. 3 1
      packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts
  59. 9 6
      packages/app/test/cypress/integration/30-search/search.spec.ts
  60. 3 0
      packages/app/test/cypress/support/commands.ts
  61. 0 1
      packages/core/src/interfaces/page.ts

+ 2 - 1
packages/app/cypress.json

@@ -13,5 +13,6 @@
   "viewportWidth": 1400,
   "viewportHeight": 1024,
 
-  "experimentalSessionSupport": true
+  "experimentalSessionSupport": true,
+  "defaultCommandTimeout": 30000
 }

+ 1 - 1
packages/app/public/static/locales/en_US/translation.json

@@ -627,7 +627,7 @@
   },
   "login": {
     "Sign in error": "Login error",
-    "Registration successful": "Registration successful",
+    "Registration successful": "Registration successful. Please wait for administrator approval.",
     "Setup": "Setup",
     "enabled_ldap_has_configuration_problem":"LDAP is enabled but the configuration has something wrong.",
     "set_env_var_for_logs": "(Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)"

+ 1 - 1
packages/app/public/static/locales/ja_JP/translation.json

@@ -621,7 +621,7 @@
   },
   "login": {
     "Sign in error": "ログインエラー",
-    "Registration successful": "登録完了",
+    "Registration successful": "登録完了しました。管理者の承認をお待ちください。",
     "Setup": "セットアップ",
     "enabled_ldap_has_configuration_problem":"LDAPは有効ですが、設定に問題があります。",
     "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"

+ 1 - 1
packages/app/public/static/locales/zh_CN/translation.json

@@ -629,7 +629,7 @@
   },
 	"login": {
 		"Sign in error": "登录错误",
-		"Registration successful": "注册成功",
+		"Registration successful": "注册成功。请等待管理员批准",
 		"Setup": "安装程序",
     "enabled_ldap_has_configuration_problem":"启用了LDAP,但配置有问题。",
     "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"

+ 1 - 1
packages/app/src/client/interfaces/global-notification.ts

@@ -1,5 +1,5 @@
 export const NotifyType = {
-  Email: 'email',
+  Email: 'mail',
   SLACK: 'slack',
 } as const;
 

+ 8 - 0
packages/app/src/client/interfaces/notification.ts

@@ -0,0 +1,8 @@
+import type { NotifyType } from './global-notification';
+
+export type INotificationType = {
+  __t?: NotifyType
+  _id: string
+  // TOOD: Define the provider type
+  provider?: any
+}

+ 28 - 3
packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -1,4 +1,6 @@
-import React, { useCallback, useState } from 'react';
+import React, {
+  useCallback, useEffect, useState,
+} from 'react';
 
 import { useTranslation } from 'next-i18next';
 
@@ -6,16 +8,31 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { useSWRxLayoutSetting } from '~/stores/admin/customize';
 import { useNextThemes } from '~/stores/use-next-themes';
 
+const useIsContainerFluid = () => {
+  const { data: layoutSetting, update: updateLayoutSetting } = useSWRxLayoutSetting();
+  const [isContainerFluid, setIsContainerFluid] = useState<boolean>();
+
+  useEffect(() => {
+    setIsContainerFluid(layoutSetting?.isContainerFluid);
+  }, [layoutSetting?.isContainerFluid]);
+
+  return {
+    isContainerFluid,
+    setIsContainerFluid,
+    updateLayoutSetting,
+  };
+};
+
 const CustomizeLayoutSetting = (): JSX.Element => {
   const { t } = useTranslation('admin');
 
   const { resolvedTheme } = useNextThemes();
-  const { data: layoutSetting, update: updateLayoutSetting } = useSWRxLayoutSetting();
 
-  const [isContainerFluid, setIsContainerFluid] = useState<boolean>(layoutSetting?.isContainerFluid ?? false);
+  const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } = useIsContainerFluid();
   const [retrieveError, setRetrieveError] = useState<any>();
 
   const onClickSubmit = useCallback(async() => {
+    if (isContainerFluid == null) { return }
     try {
       await updateLayoutSetting({ isContainerFluid });
       toastSuccess(t('toaster.update_successed', { target: t('customize_settings.layout'), ns: 'commons' }));
@@ -25,6 +42,14 @@ const CustomizeLayoutSetting = (): JSX.Element => {
     }
   }, [isContainerFluid, updateLayoutSetting, t]);
 
+  if (isContainerFluid == null) {
+    return (
+      <div className="text-muted text-center">
+        <i className="fa fa-2x fa-spinner fa-pulse"></i>
+      </div>
+    );
+  }
+
   return (
     <React.Fragment>
       <div className="row">

+ 1 - 3
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -54,17 +54,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
     try {
       const response = await apiv3Put('/customize-setting/customize-logo', {
         isDefaultLogo,
-        customizedLogoSrc,
       });
       const { customizedParams } = response.data;
       setIsDefaultLogo(customizedParams.isDefaultLogo);
-      setCustomizedLogoSrc(customizedParams.customizedLogoSrc);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
     }
-  }, [t, isDefaultLogo, customizedLogoSrc]);
+  }, [t, isDefaultLogo]);
 
   const onClickDeleteBtn = useCallback(async() => {
     try {

+ 1 - 1
packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx

@@ -12,7 +12,7 @@ import loggerFactory from '~/utils/logger';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import NotificationDeleteModal from './NotificationDeleteModal';
-import NotificationTypeIcon from './NotificationTypeIcon';
+import { NotificationTypeIcon } from './NotificationTypeIcon';
 
 
 const logger = loggerFactory('growi:GolobalNotificationList');

+ 0 - 43
packages/app/src/components/Admin/Notification/NotificationTypeIcon.jsx

@@ -1,43 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UncontrolledTooltip } from 'reactstrap';
-
-const SlackIcon = (props) => {
-  const { __t, _id, provider } = props.notification;
-
-  let type = 'slack';
-
-  // User trigger notification
-  if (provider != null) {
-    // only slack type
-  }
-
-  // Global notification
-  if (__t != null) {
-    if (__t === 'mail') {
-      type = 'mail';
-    }
-  }
-
-  const elemId = `notification-${type}-${_id}`;
-  const className = type === 'mail'
-    ? 'icon-fw fa fa-envelope-o'
-    : 'icon-fw fa fa-hashtag';
-
-  return (
-    <>
-      <i id={elemId} className={className}></i>
-      <UncontrolledTooltip target={elemId}>Slack</UncontrolledTooltip>
-    </>
-  );
-};
-
-SlackIcon.propTypes = {
-  // supports 2 types:
-  //   User trigger notification -> has 'provider: slack'
-  //   Global notification -> has '__t: slack|mail'
-  notification: PropTypes.object.isRequired,
-};
-
-export default SlackIcon;

+ 30 - 0
packages/app/src/components/Admin/Notification/NotificationTypeIcon.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+
+import { UncontrolledTooltip } from 'reactstrap';
+
+import type { INotificationType } from '~/client/interfaces/notification';
+
+
+type NotificationTypeIconProps = {
+  // supports 2 types:
+  //   User trigger notification -> has 'provider: slack'
+  //   Global notification -> has '__t: slack|mail'
+  notification: INotificationType
+}
+
+export const NotificationTypeIcon = (props: NotificationTypeIconProps): JSX.Element => {
+  const { __t, _id, provider } = props.notification;
+
+  const type = __t != null && __t === 'mail' ? 'mail' : 'slack';
+
+  // User trigger notification
+  if (provider != null) {
+    // only slack type
+  }
+
+  const elemId = `notification-${type}-${_id}`;
+  const className = type === 'mail' ? 'icon-fw fa fa-envelope-o' : 'icon-fw fa fa-hashtag';
+  const toolChip = type === 'mail' ? 'Mail' : 'Slack';
+
+  return <><i id={elemId} className={className}></i><UncontrolledTooltip target={elemId}>{toolChip}</UncontrolledTooltip></>;
+};

+ 1 - 1
packages/app/src/components/Admin/Notification/UserNotificationRow.jsx

@@ -7,7 +7,7 @@ import AdminNotificationContainer from '~/client/services/AdminNotificationConta
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import NotificationTypeIcon from './NotificationTypeIcon';
+import { NotificationTypeIcon } from './NotificationTypeIcon';
 
 class UserNotificationRow extends React.PureComponent {
 

+ 24 - 0
packages/app/src/components/CompleteUserRegistration.tsx

@@ -0,0 +1,24 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'next-i18next';
+import Link from 'next/link';
+
+export const CompleteUserRegistration: FC = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
+      <div className="row mx-0">
+        <div className="col-12 mb-3 text-center">
+          <p className="alert alert-success">
+            <span>{t('login.Registration successful')}</span>
+          </p>
+          {/* If the transition source is "/login", use <a /> tag since the transition will not occur if next/link is used. */}
+          <a href='/login'>
+            <i className="icon-login mr-1" />{t('Sign in is here')}
+          </a>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 24 - 5
packages/app/src/components/CompleteUserRegistrationForm.tsx

@@ -1,16 +1,21 @@
 import React, { useState, useEffect, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { useRouter } from 'next/router';
 
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
 import { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
+import { RegistrationMode } from '~/interfaces/registration-mode';
 
-import { toastSuccess, toastError } from '../client/util/apiNotification';
+import { toastError } from '../client/util/apiNotification';
+
+import { CompleteUserRegistration } from './CompleteUserRegistration';
 
 interface Props {
   email: string,
   token: string,
   errorCode?: UserActivationErrorCode,
+  registrationMode: RegistrationMode,
   isEmailAuthenticationEnabled: boolean,
 }
 
@@ -21,6 +26,7 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
     email,
     token,
     errorCode,
+    registrationMode,
     isEmailAuthenticationEnabled,
   } = props;
 
@@ -31,6 +37,9 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
   const [name, setName] = useState('');
   const [password, setPassword] = useState('');
   const [disableForm, setDisableForm] = useState(forceDisableForm);
+  const [isSuccessToRagistration, setIsSuccessToRagistration] = useState(false);
+
+  const router = useRouter();
 
   useEffect(() => {
     const delayDebounceFn = setTimeout(async() => {
@@ -52,17 +61,27 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
     e.preventDefault();
     setDisableForm(true);
     try {
-      await apiv3Post('/complete-registration', {
+      const res = await apiv3Post('/complete-registration', {
         username, name, password, token,
       });
-      toastSuccess('Registration succeed');
-      window.location.href = '/login';
+
+      setIsSuccessToRagistration(true);
+
+      const { redirectTo } = res.data;
+      if (redirectTo != null) {
+        router.push(redirectTo);
+      }
     }
     catch (err) {
       toastError(err, 'Registration failed');
       setDisableForm(false);
+      setIsSuccessToRagistration(false);
     }
-  }, [name, password, token, username]);
+  }, [username, name, password, token, router]);
+
+  if (isSuccessToRagistration && registrationMode === RegistrationMode.RESTRICTED) {
+    return <CompleteUserRegistration />;
+  }
 
   return (
     <>

+ 22 - 12
packages/app/src/components/LoginForm.tsx

@@ -9,15 +9,17 @@ import ReactCardFlip from 'react-card-flip';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { LoginErrorCode } from '~/interfaces/errors/login-error';
 import { IErrorV3 } from '~/interfaces/errors/v3-error';
+import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 
+import { CompleteUserRegistration } from './CompleteUserRegistration';
+
 type LoginFormProps = {
   username?: string,
   name?: string,
   email?: string,
-  isRegistrationEnabled: boolean,
   isEmailAuthenticationEnabled: boolean,
-  registrationMode?: string,
+  registrationMode: RegistrationMode,
   registrationWhiteList: string[],
   isPasswordResetEnabled: boolean,
   isLocalStrategySetup: boolean,
@@ -31,7 +33,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const router = useRouter();
 
   const {
-    isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled, isRegistrationEnabled,
+    isLocalStrategySetup, isLdapStrategySetup, isLdapSetupFailed, isPasswordResetEnabled,
     isEmailAuthenticationEnabled, registrationMode, registrationWhiteList, isMailerSetup, objOfIsExternalAuthEnableds,
   } = props;
   const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
@@ -51,8 +53,10 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const [registerErrors, setRegisterErrors] = useState<IErrorV3[]>([]);
   // For UserActivation
   const [emailForRegistrationOrder, setEmailForRegistrationOrder] = useState('');
-  const [isSuccessToSendRegistrationOrderEmail, setIsSuccessToSendRegistrationOrderEmail] = useState(false);
 
+  const [isSuccessToRagistration, setIsSuccessToRagistration] = useState(false);
+
+  const isRegistrationEnabled = isLocalStrategySetup && registrationMode !== RegistrationMode.CLOSED;
 
   useEffect(() => {
     const { hash } = window.location;
@@ -271,7 +275,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
     e.preventDefault();
     setEmailForRegistrationOrder('');
-    setIsSuccessToSendRegistrationOrderEmail(false);
+    setIsSuccessToRagistration(false);
 
     const registerForm = {
       username: usernameForRegister,
@@ -282,14 +286,17 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     try {
       const res = await apiv3Post(requestPath, { registerForm });
 
+      setIsSuccessToRagistration(true);
       resetRegisterErrors();
 
       const { redirectTo } = res.data;
-      router.push(redirectTo ?? '/');
+      if (redirectTo != null) {
+        router.push(redirectTo);
+      }
 
       if (isEmailAuthenticationEnabled) {
         setEmailForRegistrationOrder(emailForRegister);
-        setIsSuccessToSendRegistrationOrderEmail(true);
+        return;
       }
     }
     catch (err) {
@@ -318,7 +325,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
     return (
       <React.Fragment>
-        {registrationMode === 'Restricted' && (
+        {registrationMode === RegistrationMode.RESTRICTED && (
           <p className="alert alert-warning">
             {t('page_register.notice.restricted')}
             <br />
@@ -346,7 +353,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         }
 
         {
-          (isEmailAuthenticationEnabled && isSuccessToSendRegistrationOrderEmail) && (
+          (isEmailAuthenticationEnabled && isSuccessToRagistration) && (
             <p className="alert alert-success">
               <span>{t('message.successfully_send_email_auth', { email: emailForRegistrationOrder })}</span>
             </p>
@@ -476,11 +483,14 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       </React.Fragment>
     );
   }, [
-    handleRegisterFormSubmit, isEmailAuthenticationEnabled, isMailerSetup,
-    isSuccessToSendRegistrationOrderEmail, props.email, props.name, props.username,
-    registerErrors, registrationMode, registrationWhiteList, emailForRegistrationOrder, switchForm, t,
+    t, isEmailAuthenticationEnabled, registrationMode, isMailerSetup, registerErrors, isSuccessToRagistration,
+    emailForRegistrationOrder, props.username, props.name, props.email, registrationWhiteList, switchForm, handleRegisterFormSubmit,
   ]);
 
+  if (registrationMode === RegistrationMode.RESTRICTED && isSuccessToRagistration && !isEmailAuthenticationEnabled) {
+    return <CompleteUserRegistration />;
+  }
+
   return (
     <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
       <div className="row mx-0">

+ 14 - 3
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -6,7 +6,7 @@ import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { DropdownItem } from 'reactstrap';
 
-import { exportAsMarkdown } from '~/client/services/page-operation';
+import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
 import {
@@ -17,7 +17,7 @@ import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/in
 import {
   useCurrentPageId, useCurrentPathname,
   useIsNotFound,
-  useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useTemplateTagData,
+  useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useTemplateTagData, useIsContainerFluid,
 } from '~/stores/context';
 import { usePageTagsForEditors } from '~/stores/editor';
 import {
@@ -202,6 +202,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
+  const { data: isContainerFluid } = useIsContainerFluid();
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowTagLabel } = useIsAbleToShowTagLabel();
@@ -313,6 +314,11 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
   }, [openDeleteModal, reload, router]);
 
+  const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
+    await updateContentWidth(pageId, value);
+    mutateCurrentPage();
+  }, [mutateCurrentPage]);
+
   const templateMenuItemClickHandler = useCallback(() => {
     setIsPageTempleteModalShown(true);
   }, []);
@@ -356,12 +362,14 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
                     revisionId={revisionId}
                     shareLinkId={shareLinkId}
                     path={path ?? currentPathname} // If the page is empty, "path" is undefined
+                    expandContentWidth={currentPage?.expandContentWidth ?? isContainerFluid}
                     disableSeenUserInfoPopover={isSharedUser}
                     showPageControlDropdown={isAbleToShowPageManagement}
                     additionalMenuItemRenderer={additionalMenuItemsRenderer}
                     onClickDuplicateMenuItem={duplicateItemClickedHandler}
                     onClickRenameMenuItem={renameItemClickedHandler}
                     onClickDeleteMenuItem={deleteItemClickedHandler}
+                    onClickSwitchContentWidth={switchContentWidthHandler}
                   />
                 ) }
               </div>
@@ -402,7 +410,10 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       </>
     );
   // eslint-disable-next-line max-len
-  }, [isCompactMode, isViewMode, pageId, revisionId, shareLinkId, path, currentPathname, isSharedUser, isAbleToShowPageManagement, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, isAbleToShowPageEditorModeManager, isGuestUser, editorMode, isAbleToShowPageAuthors, currentPage, currentUser, isPageTemplateModalShown, isLinkSharingDisabled, templateMenuItemClickHandler, mutateEditorMode]);
+  }, [isCompactMode, isViewMode, pageId, revisionId, shareLinkId, path, currentPathname, isSharedUser, isAbleToShowPageManagement,
+      duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, isAbleToShowPageEditorModeManager, isGuestUser,
+      editorMode, isAbleToShowPageAuthors, currentPage, currentUser, isPageTemplateModalShown, isLinkSharingDisabled, templateMenuItemClickHandler,
+      mutateEditorMode, switchContentWidthHandler]);
 
 
   const pagePath = isNotFound

+ 1 - 0
packages/app/src/components/Navbar/PageEditorModeManager.jsx

@@ -27,6 +27,7 @@ const PageEditorModeButtonWrapper = React.memo(({
       className={classNames.join(' ')}
       onClick={() => { onClick(targetMode) }}
       id={id}
+      data-testId={`${targetMode}-button`}
     >
       <span className="d-flex flex-column flex-md-row justify-content-center">
         <span className="grw-page-editor-mode-manager-icon mr-md-1">{icon}</span>

+ 23 - 18
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
 import { DropdownItem } from 'reactstrap';
 
 import {
-  toggleBookmark, toggleLike, toggleSubscribe, updateContentWidth,
+  toggleBookmark, toggleLike, toggleSubscribe,
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/apiNotification';
 import {
@@ -28,22 +28,19 @@ import SeenUserInfo from '../User/SeenUserInfo';
 
 type WideViewMenuItemProps = AdditionalMenuItemsRendererProps & {
   onClickMenuItem: (newValue: boolean) => void,
+  expandContentWidth?: boolean,
 }
 
 const WideViewMenuItem = (props: WideViewMenuItemProps): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    pageInfo, onClickMenuItem,
+    onClickMenuItem, expandContentWidth,
   } = props;
 
-  if (!isIPageInfoForEntity(pageInfo)) {
-    return <></>;
-  }
-
   return (
     <DropdownItem
-      onClick={() => onClickMenuItem(!pageInfo.expandContentWidth)}
+      onClick={() => onClickMenuItem(!(expandContentWidth))}
       className="grw-page-control-dropdown-item"
     >
       <div className="custom-control custom-switch ml-1">
@@ -51,7 +48,7 @@ const WideViewMenuItem = (props: WideViewMenuItemProps): JSX.Element => {
           id="switchContentWidth"
           className="custom-control-input"
           type="checkbox"
-          checked={pageInfo.expandContentWidth}
+          checked={expandContentWidth}
           onChange={() => {}}
         />
         <label className="custom-control-label" htmlFor="switchContentWidth">
@@ -72,6 +69,7 @@ type CommonProps = {
   onClickDuplicateMenuItem?: (pageToDuplicate: IPageForPageDuplicateModal) => void,
   onClickRenameMenuItem?: (pageToRename: IPageToRenameWithMeta) => void,
   onClickDeleteMenuItem?: (pageToDelete: IPageToDeleteWithMeta) => void,
+  onClickSwitchContentWidth?: (pageId: string, value: boolean) => void,
 }
 
 type SubNavButtonsSubstanceProps = CommonProps & {
@@ -80,14 +78,15 @@ type SubNavButtonsSubstanceProps = CommonProps & {
   revisionId: string | null,
   path?: string | null,
   pageInfo: IPageInfoForOperation,
+  expandContentWidth?: boolean,
 }
 
 const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element => {
   const {
     pageInfo,
-    pageId, revisionId, path, shareLinkId,
+    pageId, revisionId, path, shareLinkId, expandContentWidth,
     isCompactMode, disableSeenUserInfoPopover, showPageControlDropdown, forceHideMenuItems, additionalMenuItemRenderer,
-    onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem,
+    onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
   } = props;
 
   const { data: isGuestUser } = useIsGuestUser();
@@ -185,28 +184,30 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
 
   const switchContentWidthClickHandler = useCallback(async(newValue: boolean) => {
-    if (isGuestUser == null || isGuestUser) {
+    if (onClickSwitchContentWidth == null || isGuestUser == null || isGuestUser) {
       return;
     }
     if (!isIPageInfoForEntity(pageInfo)) {
       return;
     }
     try {
-      await updateContentWidth(pageId, newValue);
-      mutatePageInfo();
+      onClickSwitchContentWidth(pageId, newValue);
     }
     catch (err) {
       toastError(err);
     }
-  }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
+  }, [isGuestUser, onClickSwitchContentWidth, pageId, pageInfo]);
 
   const additionalMenuItemOnTopRenderer = useMemo(() => {
     if (!isIPageInfoForEntity(pageInfo)) {
       return undefined;
     }
-    const wideviewMenuItemRenderer = (props: WideViewMenuItemProps) => <WideViewMenuItem {...props} onClickMenuItem={switchContentWidthClickHandler} />;
+    const wideviewMenuItemRenderer = (props: WideViewMenuItemProps) => {
+
+      return <WideViewMenuItem {...props} onClickMenuItem={switchContentWidthClickHandler} expandContentWidth={expandContentWidth} />;
+    };
     return wideviewMenuItemRenderer;
-  }, [pageInfo, switchContentWidthClickHandler]);
+  }, [pageInfo, switchContentWidthClickHandler, expandContentWidth]);
 
   if (!isIPageInfoForOperation(pageInfo)) {
     return <></>;
@@ -274,12 +275,14 @@ export type SubNavButtonsProps = CommonProps & {
   pageId: string,
   shareLinkId?: string | null,
   revisionId?: string | null,
-  path?: string | null
+  path?: string | null,
+  expandContentWidth?: boolean,
 };
 
 export const SubNavButtons = (props: SubNavButtonsProps): JSX.Element => {
   const {
-    pageId, revisionId, path, shareLinkId, onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem,
+    pageId, revisionId, path, shareLinkId, expandContentWidth,
+    onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
   } = props;
 
   const { data: pageInfo, error } = useSWRxPageInfo(pageId ?? null, shareLinkId);
@@ -302,6 +305,8 @@ export const SubNavButtons = (props: SubNavButtonsProps): JSX.Element => {
       onClickDuplicateMenuItem={onClickDuplicateMenuItem}
       onClickRenameMenuItem={onClickRenameMenuItem}
       onClickDeleteMenuItem={onClickDeleteMenuItem}
+      onClickSwitchContentWidth={onClickSwitchContentWidth}
+      expandContentWidth={expandContentWidth}
     />
   );
 };

+ 2 - 6
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -1064,12 +1064,6 @@ class CodeMirrorEditor extends AbstractEditor {
           ref={this.cm}
           className={additionalClasses}
           placeholder="search"
-          // == temporary deactivate editorDidMount to use https://github.com/scniro/react-codemirror2/issues/284#issuecomment-1155928554
-          // editorDidMount={(editor) => {
-          // // add event handlers
-          //   editor.on('paste', this.pasteHandler);
-          //   editor.on('scrollCursorIntoView', this.scrollCursorIntoViewHandler);
-          // }}
           value={this.props.value}
           options={{
             indentUnit: this.props.indentSize,
@@ -1116,6 +1110,8 @@ class CodeMirrorEditor extends AbstractEditor {
           }}
           onKeyPress={this.keyPressHandler}
           onKeyDown={this.keyDownHandler}
+          onPasteFiles={this.pasteHandler}
+          onScrollCursorIntoView={this.scrollCursorIntoViewHandler}
         />
 
         { this.renderLoadingKeymapOverlay() }

+ 1 - 1
packages/app/src/components/PageEditor/Editor.tsx

@@ -229,7 +229,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
 
   const renderNavbar = useCallback(() => {
     return (
-      <div className="m-0 navbar navbar-default navbar-editor" style={{ minHeight: 'unset' }}>
+      <div className="m-0 navbar navbar-default navbar-editor" data-testId="navbar-editor" style={{ minHeight: 'unset' }}>
         <ul className="pl-2 nav nav-navbar">
           { (editorSubstance()?.getNavbarItems() ?? []).map((item, idx) => {
             // eslint-disable-next-line react/no-array-index-key

+ 14 - 3
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -9,12 +9,12 @@ import { animateScroll } from 'react-scroll';
 import { DropdownItem } from 'reactstrap';
 
 
-import { exportAsMarkdown } from '~/client/services/page-operation';
+import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/apiNotification';
 import { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
 import { IPageWithSearchMeta } from '~/interfaces/search';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser, useIsContainerFluid } from '~/stores/context';
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
 } from '~/stores/modal';
@@ -156,6 +156,9 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   const { open: openDeleteModal } = usePageDeleteModal();
   const { data: rendererOptions } = useSearchResultOptions(pageWithMeta.data.path, highlightKeywords);
   const { data: currentUser } = useCurrentUser();
+  const { data: isContainerFluid } = useIsContainerFluid();
+
+  const [isExpandContentWidth, setIsExpandContentWidth] = useState(page.expandContentWidth);
 
   const duplicateItemClickedHandler = useCallback(async(pageToDuplicate) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -201,6 +204,11 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
   }, [onDeletedHandler, openDeleteModal]);
 
+  const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
+    await updateContentWidth(pageId, value);
+    setIsExpandContentWidth(value);
+  }, []);
+
   const RightComponent = useCallback(() => {
     if (page == null) {
       return <></>;
@@ -214,6 +222,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           pageId={page._id}
           revisionId={revisionId}
           path={page.path}
+          expandContentWidth={isExpandContentWidth ?? isContainerFluid}
           showPageControlDropdown={showPageControlDropdown}
           forceHideMenuItems={forceHideMenuItems}
           additionalMenuItemRenderer={props => <AdditionalMenuItems {...props} pageId={page._id} revisionId={revisionId} />}
@@ -221,10 +230,12 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           onClickDuplicateMenuItem={duplicateItemClickedHandler}
           onClickRenameMenuItem={renameItemClickedHandler}
           onClickDeleteMenuItem={deleteItemClickedHandler}
+          onClickSwitchContentWidth={switchContentWidthHandler}
         />
       </div>
     );
-  }, [page, showPageControlDropdown, forceHideMenuItems, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler]);
+  }, [page, isExpandContentWidth, showPageControlDropdown, forceHideMenuItems, isContainerFluid,
+      duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, switchContentWidthHandler]);
 
   // return if page or growiRenderer is null
   if (page == null || rendererOptions == null) return <></>;

+ 16 - 9
packages/app/src/components/UncontrolledCodeMirror.tsx

@@ -22,21 +22,33 @@ export interface UncontrolledCodeMirrorProps extends ICodeMirror {
   value: string;
   isGfmMode?: boolean;
   lineNumbers?: boolean;
-  onScrollCursorIntoView?: (line: number) => void;
   onSave?: () => Promise<void>;
-  onPasteFiles?: (event: Event) => void;
   onCtrlEnter?: (event: Event) => void;
+  onPasteFiles?: (editor: any, event: Event) => void;
+  onScrollCursorIntoView?: (editor: any, event: Event) => void;
 }
 
 export const UncontrolledCodeMirror = React.forwardRef<CodeMirror|null, UncontrolledCodeMirrorProps>((props, forwardedRef): JSX.Element => {
 
-  const wrapperRef = useRef<CodeMirror|null>();
+  const {
+    value, lineNumbers, options,
+    onPasteFiles, onScrollCursorIntoView,
+    ...rest
+  } = props;
 
   const editorRef = useRef<Editor>();
 
+  const wrapperRef = useRef<CodeMirror|null>();
+
   const editorDidMountHandler = useCallback((editor: Editor): void => {
     editorRef.current = editor;
-  }, []);
+    if (onPasteFiles != null) {
+      editor.on('paste', onPasteFiles);
+    }
+    if (onScrollCursorIntoView != null) {
+      editor.on('scrollCursorIntoView', onScrollCursorIntoView);
+    }
+  }, [onPasteFiles, onScrollCursorIntoView]);
 
   const editorWillUnmountHandler = useCallback((): void => {
     // workaround to fix editor duplicating by https://github.com/scniro/react-codemirror2/issues/284#issuecomment-1155928554
@@ -48,11 +60,6 @@ export const UncontrolledCodeMirror = React.forwardRef<CodeMirror|null, Uncontro
     }
   }, []);
 
-  const {
-    value, lineNumbers, options,
-    ...rest
-  } = props;
-
   // default true
   const isGfmMode = rest.isGfmMode ?? true;
 

+ 7 - 0
packages/app/src/interfaces/registration-mode.ts

@@ -0,0 +1,7 @@
+export const RegistrationMode = {
+  OPEN: 'Open',
+  RESTRICTED: 'Restricted',
+  CLOSED: 'Closed',
+} as const;
+
+export type RegistrationMode = typeof RegistrationMode[keyof typeof RegistrationMode];

+ 5 - 5
packages/app/src/pages/[[...path]].page.tsx

@@ -65,7 +65,7 @@ import {
   useIsAclEnabled, useIsSearchPage,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useEditingMarkdown,
-  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useCustomizedLogoSrc,
+  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useCustomizedLogoSrc, useIsContainerFluid,
 } from '../stores/context';
 
 import {
@@ -197,6 +197,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   // page
   useIsLatestRevision(props.isLatestRevision);
+  useIsContainerFluid(props.isContainerFluid);
   // useOwnerOfCurrentPage(props.pageUser != null ? JSON.parse(props.pageUser) : null);
   useIsForbidden(props.isForbidden);
   useIsNotFound(props.isNotFound);
@@ -241,10 +242,9 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   useCurrentPathname(props.currentPathname);
 
-  useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
+  const { data: currentPage } = useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
   useEditingMarkdown(pageWithMeta?.data.revision?.body ?? '');
 
-  const { data: dataPageInfo } = useSWRxPageInfo(pageId);
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
 
@@ -274,9 +274,9 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   const isTopPagePath = isTopPage(pageWithMeta?.data.path ?? '');
 
-  const isContainerFluidEachPage = dataPageInfo == null || !('expandContentWidth' in dataPageInfo)
+  const isContainerFluidEachPage = currentPage == null || !('expandContentWidth' in currentPage)
     ? null
-    : dataPageInfo.expandContentWidth;
+    : currentPage.expandContentWidth;
   const isContainerFluidDefault = props.isContainerFluid;
   const isContainerFluid = isContainerFluidEachPage ?? isContainerFluidDefault;
 

+ 5 - 1
packages/app/src/pages/_search.page.tsx

@@ -12,7 +12,7 @@ import type { IUser, IUserHasId } from '~/interfaces/user';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import {
-  useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
+  useCsrfToken, useCurrentUser, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL,
 } from '~/stores/context';
 import {
@@ -46,6 +46,8 @@ type Props = CommonProps & {
   // search limit
   showPageLimitationL: number
 
+  isContainerFluid: boolean,
+
 };
 
 const SearchResultPage: NextPage<Props> = (props: Props) => {
@@ -73,6 +75,7 @@ const SearchResultPage: NextPage<Props> = (props: Props) => {
   useRendererConfig(props.rendererConfig);
 
   useShowPageLimitationL(props.showPageLimitationL);
+  useIsContainerFluid(props.isContainerFluid);
 
   const PutbackPageModal = (): JSX.Element => {
     const PutbackPageModal = dynamic(() => import('../components/PutbackPageModal'), { ssr: false });
@@ -125,6 +128,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+  props.isContainerFluid = configManager.getConfig('crowi', 'customize:isContainerFluid');
 
   props.sidebarConfig = {
     isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),

+ 2 - 0
packages/app/src/pages/admin/[...path].page.tsx

@@ -4,6 +4,7 @@ import {
 import dynamic from 'next/dynamic';
 
 import { CommonProps } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -14,6 +15,7 @@ const AdminNotFoundPage = dynamic(() => import('~/components/Admin/NotFoundPage'
 
 const AdminAppPage: NextPage<CommonProps> = (props) => {
   useIsMaintenanceMode(props.isMaintenanceMode);
+  useCurrentUser(props.currentUser ?? null);
 
 
   return (

+ 4 - 0
packages/app/src/pages/admin/app.page.tsx

@@ -6,12 +6,15 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { Container, Provider } from 'unstated';
 
+
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
+
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const AppSettingsPageContents = dynamic(() => import('~/components/Admin/App/AppSettingsPageContents'), { ssr: false });
 
@@ -19,6 +22,7 @@ const AppSettingsPageContents = dynamic(() => import('~/components/Admin/App/App
 const AdminAppPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('commons');
   useIsMaintenanceMode(props.isMaintenanceMode);
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('headers.app_settings');
   const injectableContainers: Container<any>[] = [];

+ 2 - 1
packages/app/src/pages/admin/audit-log.page.tsx

@@ -7,7 +7,7 @@ import dynamic from 'next/dynamic';
 import { SupportedActionType } from '~/interfaces/activity';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
-import { useAuditLogEnabled, useAuditLogAvailableActions } from '~/stores/context';
+import { useCurrentUser, useAuditLogEnabled, useAuditLogAvailableActions } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -26,6 +26,7 @@ const AdminAuditLogPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useAuditLogEnabled(props.auditLogEnabled);
   useAuditLogAvailableActions(props.auditLogAvailableActions);
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('audit_log_management.audit_log');
 

+ 2 - 1
packages/app/src/pages/admin/customize.page.tsx

@@ -9,7 +9,7 @@ import { Container, Provider } from 'unstated';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
-import { useCustomizeTitle } from '~/stores/context';
+import { useCustomizeTitle, useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -25,6 +25,7 @@ type Props = CommonProps & {
 const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useCustomizeTitle(props.customizeTitle);
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('customize_settings.customize_settings');
   const injectableContainers: Container<any>[] = [];

+ 2 - 0
packages/app/src/pages/admin/export.page.tsx

@@ -8,6 +8,7 @@ import { Container, Provider } from 'unstated';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -17,6 +18,7 @@ const ExportArchiveDataPage = dynamic(() => import('~/components/Admin/ExportArc
 
 const AdminExportDataArchivePage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('export_management.export_archive_data');
   const injectableContainers: Container<any>[] = [];

+ 3 - 0
packages/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx

@@ -9,9 +9,11 @@ import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { Container, Provider } from 'unstated';
 
+
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
@@ -22,6 +24,7 @@ const ManageGlobalNotification = dynamic(() => import('~/components/Admin/Notifi
 
 const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
   const router = useRouter();
   const { globalNotificationId } = router.query;
   const currentGlobalNotificationId = Array.isArray(globalNotificationId) ? globalNotificationId[0] : globalNotificationId;

+ 2 - 0
packages/app/src/pages/admin/global-notification/new.page.tsx

@@ -8,6 +8,7 @@ import { Container, Provider } from 'unstated';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
@@ -17,6 +18,7 @@ const ManageGlobalNotification = dynamic(() => import('~/components/Admin/Notifi
 
 const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('external_notification.external_notification');
   const injectableContainers: Container<any>[] = [];

+ 2 - 0
packages/app/src/pages/admin/importer.page.tsx

@@ -8,6 +8,7 @@ import { Container, Provider } from 'unstated';
 
 import AdminImportContainer from '~/client/services/AdminImportContainer';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -17,6 +18,7 @@ const DataImportPageContents = dynamic(() => import('~/components/Admin/ImportDa
 
 const AdminDataImportPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('importer_management.import_data');
   const injectableContainers: Container<any>[] = [];

+ 2 - 2
packages/app/src/pages/admin/index.page.tsx

@@ -10,8 +10,8 @@ import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
 import PluginUtils from '~/server/plugins/plugin-utils';
+import { useCurrentUser, useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '~/stores/context';
 
-import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '../../stores/context';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
@@ -29,7 +29,7 @@ type Props = CommonProps & {
 
 
 const AdminHomePage: NextPage<Props> = (props) => {
-
+  useCurrentUser(props.currentUser ?? null);
   useGrowiCloudUri(props.growiCloudUri);
   useGrowiAppIdForGrowiCloud(props.growiAppIdForGrowiCloud);
 

+ 3 - 0
packages/app/src/pages/admin/markdown.page.tsx

@@ -6,8 +6,10 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { Container, Provider } from 'unstated';
 
+
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -17,6 +19,7 @@ const MarkDownSettingContents = dynamic(() => import('~/components/Admin/Markdow
 
 const AdminMarkdownPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('markdown_settings.markdown_settings');
   const injectableContainers: Container<any>[] = [];

+ 2 - 0
packages/app/src/pages/admin/notification.page.tsx

@@ -8,6 +8,7 @@ import { Container, Provider } from 'unstated';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -17,6 +18,7 @@ const NotificationSetting = dynamic(() => import('~/components/Admin/Notificatio
 
 const AdminExternalNotificationPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('external_notification.external_notification');
   const injectableContainers: Container<any>[] = [];

+ 2 - 1
packages/app/src/pages/admin/search.page.tsx

@@ -6,7 +6,7 @@ import dynamic from 'next/dynamic';
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
-import { useIsSearchServiceReachable } from '~/stores/context';
+import { useIsSearchServiceReachable, useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -23,6 +23,7 @@ type Props = CommonProps & {
 
 const AdminFullTextSearchManagementPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
 
   const title = t('full_text_search_management.full_text_search_management');

+ 2 - 1
packages/app/src/pages/admin/security.page.tsx

@@ -18,7 +18,7 @@ import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityConta
 import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
-import { useIsMailerSetup, useSiteUrl } from '~/stores/context';
+import { useCurrentUser, useIsMailerSetup, useSiteUrl } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -34,6 +34,7 @@ type Props = CommonProps & {
 
 const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
   useSiteUrl(props.siteUrl);
   useIsMailerSetup(props.isMailerSetup);
 

+ 2 - 0
packages/app/src/pages/admin/slack-integration-legacy.page.tsx

@@ -8,6 +8,7 @@ import { Container, Provider } from 'unstated';
 
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -17,6 +18,7 @@ const LegacySlackIntegration = dynamic(() => import('~/components/Admin/LegacySl
 
 const AdminLegacySlackIntegrationPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('slack_integration_legacy.slack_integration_legacy');
   const injectableContainers: Container<any>[] = [];

+ 2 - 1
packages/app/src/pages/admin/slack-integration.page.tsx

@@ -6,7 +6,7 @@ import dynamic from 'next/dynamic';
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
-import { useSiteUrl } from '~/stores/context';
+import { useCurrentUser, useSiteUrl } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -22,6 +22,7 @@ type Props = CommonProps & {
 
 const AdminSlackIntegrationPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
   useSiteUrl(props.siteUrl);
 
   const title = t('slack_integration.slack_integration');

+ 2 - 1
packages/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx

@@ -7,7 +7,7 @@ import { useRouter } from 'next/router';
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
-import { useIsAclEnabled } from '~/stores/context';
+import { useIsAclEnabled, useCurrentUser } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
@@ -22,6 +22,7 @@ type Props = CommonProps & {
 const AdminUserGroupDetailPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation('admin');
   useIsMaintenanceMode(props.isMaintenanceMode);
+  useCurrentUser(props.currentUser ?? null);
   const router = useRouter();
   const { userGroupId } = router.query;
 

+ 2 - 1
packages/app/src/pages/admin/user-groups.page.tsx

@@ -6,7 +6,7 @@ import dynamic from 'next/dynamic';
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
-import { useIsAclEnabled } from '~/stores/context';
+import { useIsAclEnabled, useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -21,6 +21,7 @@ type Props = CommonProps & {
 
 const AdminUserGroupPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
   useIsAclEnabled(props.isAclEnabled);
 
   const title = t('user_group_management.user_group_management');

+ 2 - 0
packages/app/src/pages/admin/users/external-accounts.page.tsx

@@ -8,6 +8,7 @@ import { Container, Provider } from 'unstated';
 
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useCurrentUser } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
@@ -17,6 +18,7 @@ const ManageExternalAccount = dynamic(() => import('~/components/Admin/ManageExt
 
 const AdminUserManagementPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
+  useCurrentUser(props.currentUser ?? null);
 
   const title = t('user_management.external_account');
   const injectableContainers: Container<any>[] = [];

+ 2 - 6
packages/app/src/pages/admin/users/index.page.tsx

@@ -19,14 +19,13 @@ const UserManagement = dynamic(() => import('~/components/Admin/UserManagement')
 
 
 type Props = CommonProps & {
-  currentUser: any,
   isMailerSetup: boolean,
 };
 
 
 const AdminUserManagementPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
-  useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
+  useCurrentUser(props.currentUser ?? null);
   useIsMailerSetup(props.isMailerSetup);
 
   const title = t('user_management.user_management');
@@ -51,12 +50,9 @@ const AdminUserManagementPage: NextPage<Props> = (props) => {
 
 const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
-  const { crowi, user } = req;
+  const { crowi } = req;
   const { mailService } = crowi;
 
-  if (user != null) {
-    props.currentUser = JSON.stringify(user);
-  }
   props.isMailerSetup = mailService.isMailerSetup;
 };
 

+ 7 - 6
packages/app/src/pages/login.page.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
 
-
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -9,19 +8,19 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import { LoginForm } from '~/components/LoginForm';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { RegistrationMode } from '~/interfaces/registration-mode';
 
 import {
   useCsrfToken,
   useCurrentPathname,
 } from '../stores/context';
 
-
 import {
   CommonProps, getServerSideCommonProps, useCustomTitle, getNextI18NextConfig,
 } from './utils/commons';
 
 type Props = CommonProps & {
-
+  registrationMode: RegistrationMode,
   pageWithMetaStr: string,
   isMailerSetup: boolean,
   enabledStrategies: unknown,
@@ -29,6 +28,7 @@ type Props = CommonProps & {
   isLocalStrategySetup: boolean,
   isLdapStrategySetup: boolean,
   isLdapSetupFailed: boolean,
+  isPasswordResetEnabled: boolean,
   isEmailAuthenticationEnabled: boolean,
 };
 
@@ -45,16 +45,15 @@ const LoginPage: NextPage<Props> = (props: Props) => {
   return (
     <NoLoginLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
       <LoginForm
-        // Todo: These props should be set properly. https://redmine.weseek.co.jp/issues/104847
         objOfIsExternalAuthEnableds={props.enabledStrategies}
         isLocalStrategySetup={props.isLocalStrategySetup}
         isLdapStrategySetup={props.isLdapStrategySetup}
         isLdapSetupFailed={props.isLdapSetupFailed}
         isEmailAuthenticationEnabled={props.isEmailAuthenticationEnabled}
-        isRegistrationEnabled={true}
         registrationWhiteList={props.registrationWhiteList}
-        isPasswordResetEnabled={true}
+        isPasswordResetEnabled={props.isPasswordResetEnabled}
         isMailerSetup={props.isMailerSetup}
+        registrationMode={props.registrationMode}
       />
     </NoLoginLayout>
   );
@@ -100,12 +99,14 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
     passportService,
   } = crowi;
 
+  props.isPasswordResetEnabled = crowi.configManager.getConfig('crowi', 'security:passport-local:isPasswordResetEnabled');
   props.isMailerSetup = mailService.isMailerSetup;
   props.isLocalStrategySetup = passportService.isLocalStrategySetup;
   props.isLdapStrategySetup = passportService.isLdapStrategySetup;
   props.isLdapSetupFailed = configManager.getConfig('crowi', 'security:passport-ldap:isEnabled') && !props.isLdapStrategySetup;
   props.registrationWhiteList = configManager.getConfig('crowi', 'security:registrationWhiteList');
   props.isEmailAuthenticationEnabled = configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
+  props.registrationMode = configManager.getConfig('crowi', 'security:registrationMode');
 }
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

+ 1 - 4
packages/app/src/pages/me/[[...path]].page.tsx

@@ -1,8 +1,6 @@
 import React, { useMemo } from 'react';
 
-import {
-  IUser, IUserHasId,
-} from '@growi/core';
+import { IUserHasId } from '@growi/core';
 import { model as mongooseModel } from 'mongoose';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
@@ -36,7 +34,6 @@ import {
 const logger = loggerFactory('growi:pages:me');
 
 type Props = CommonProps & {
-  currentUser: IUser,
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
   isSearchScopeChildrenAsDefault: boolean,

+ 1 - 6
packages/app/src/pages/share/[[...path]].page.tsx

@@ -38,7 +38,6 @@ const ForbiddenPage = dynamic(() => import('~/components/ForbiddenPage'), { ssr:
 type Props = CommonProps & {
   shareLink?: IShareLinkHasId,
   isExpired: boolean,
-  currentUser: IUser,
   disableLinkSharing: boolean,
   isSearchServiceConfigured: boolean,
   isSearchServiceReachable: boolean,
@@ -213,7 +212,7 @@ async function addActivity(context: GetServerSidePropsContext, action: Supported
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
   const req = context.req as CrowiRequest<IUserHasId & any>;
-  const { user, crowi, params } = req;
+  const { crowi, params } = req;
   const result = await getServerSideCommonProps(context);
 
   if (!('props' in result)) {
@@ -221,10 +220,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   }
   const props: Props = result.props as Props;
 
-  if (user != null) {
-    props.currentUser = user.toObject();
-  }
-
   try {
     const ShareLinkModel = crowi.model('ShareLink');
     const shareLink = await ShareLinkModel.findOne({ _id: params.linkId }).populate('relatedPage');

+ 4 - 0
packages/app/src/pages/user-activation.page.tsx

@@ -5,6 +5,7 @@ import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationF
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
+import type { RegistrationMode } from '~/interfaces/registration-mode';
 import { IUserRegistrationOrder } from '~/server/models/user-registration-order';
 
 import {
@@ -15,6 +16,7 @@ type Props = CommonProps & {
   token: string
   email: string
   errorCode?: UserActivationErrorCode
+  registrationMode: RegistrationMode
   isEmailAuthenticationEnabled: boolean
 }
 
@@ -25,6 +27,7 @@ const UserActivationPage: NextPage<Props> = (props: Props) => {
         token={props.token}
         email={props.email}
         errorCode={props.errorCode}
+        registrationMode={props.registrationMode}
         isEmailAuthenticationEnabled={props.isEmailAuthenticationEnabled}
       />
     </NoLoginLayout>
@@ -64,6 +67,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.errorCode = context.query.errorCode as UserActivationErrorCode;
   }
 
+  props.registrationMode = req.crowi.configManager.getConfig('crowi', 'security:registrationMode');
   props.isEmailAuthenticationEnabled = req.crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
 
   await injectNextI18NextConfigurations(context, props, ['translation']);

+ 14 - 4
packages/app/src/pages/utils/commons.ts

@@ -1,4 +1,6 @@
-import { DevidedPagePath, Lang, AllLang } from '@growi/core';
+import {
+  DevidedPagePath, Lang, AllLang, IUser, IUserHasId,
+} from '@growi/core';
 import { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { SSRConfig, UserConfig } from 'next-i18next';
 
@@ -22,13 +24,14 @@ export type CommonProps = {
   isMaintenanceMode: boolean,
   redirectDestination: string | null,
   customizedLogoSrc?: string,
+  currentUser?: IUser,
 } & Partial<SSRConfig>;
 
 // eslint-disable-next-line max-len
 export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(context: GetServerSidePropsContext) => {
 
-  const req: CrowiRequest = context.req as CrowiRequest;
-  const { crowi } = req;
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { crowi, user } = req;
   const {
     appService, configManager, customizeService,
   } = crowi;
@@ -38,8 +41,14 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
 
   const isMaintenanceMode = appService.isMaintenanceMode();
 
+  let currentUser;
+  if (user != null) {
+    currentUser = user.toObject();
+  }
+
   // eslint-disable-next-line max-len, no-nested-ternary
   const redirectDestination = !isMaintenanceMode && currentPathname === '/maintenance' ? '/' : isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance') ? '/maintenance' : null;
+  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
 
   const props: CommonProps = {
     namespacesRequired: ['translation'],
@@ -54,7 +63,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     growiVersion: crowi.version,
     isMaintenanceMode,
     redirectDestination,
-    customizedLogoSrc: configManager.getConfig('crowi', 'customize:customizedLogoSrc'),
+    customizedLogoSrc: isDefaultLogo ? null : configManager.getConfig('crowi', 'customize:customizedLogoSrc'),
+    currentUser,
   };
 
   return { props };

+ 1 - 3
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -686,18 +686,16 @@ module.exports = (crowi) => {
   router.put('/customize-logo', loginRequiredStrictly, adminRequired, validator.logo, apiV3FormValidator, async(req, res) => {
 
     const {
-      isDefaultLogo, customizedLogoSrc,
+      isDefaultLogo,
     } = req.body;
 
     const requestParams = {
       'customize:isDefaultLogo': isDefaultLogo,
-      'customize:customizedLogoSrc': customizedLogoSrc,
     };
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
         isDefaultLogo: await crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo'),
-        customizedLogoSrc: await crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc'),
       };
       return res.apiv3({ customizedParams });
     }

+ 1 - 0
packages/app/src/server/routes/apiv3/index.js

@@ -97,6 +97,7 @@ module.exports = (crowi, app, isInstalled) => {
   router.get('/check-username', user.api.checkUsername);
 
   router.post('/complete-registration',
+    addActivity,
     injectUserRegistrationOrderByTokenMiddleware,
     userActivation.completeRegistrationRules(),
     userActivation.validateCompleteRegistration,

+ 37 - 1
packages/app/src/server/routes/apiv3/user-activation.ts

@@ -4,9 +4,12 @@ import { ErrorV3 } from '@growi/core';
 import { format, subSeconds } from 'date-fns';
 import { body, validationResult } from 'express-validator';
 
+import { SupportedAction } from '~/interfaces/activity';
+import { RegistrationMode } from '~/interfaces/registration-mode';
 import UserRegistrationOrder from '~/server/models/user-registration-order';
 import loggerFactory from '~/utils/logger';
 
+
 const logger = loggerFactory('growi:routes:apiv3:user-activation');
 
 const PASSOWRD_MINIMUM_NUMBER = 8;
@@ -64,6 +67,7 @@ async function sendEmailToAllAdmins(userData, admins, appTitle, mailService, tem
 
 export const completeRegistrationAction = (crowi) => {
   const User = crowi.model('User');
+  const activityEvent = crowi.event('activity');
   const {
     configManager,
     aclService,
@@ -127,6 +131,9 @@ export const completeRegistrationAction = (crowi) => {
           return res.apiv3Err(new ErrorV3(errorMessage, 'registration-failed'), 403);
         }
 
+        const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
+        activityEvent.emit('update', res.locals.activity._id, parameters);
+
         userRegistrationOrder.revokeOneTimeToken();
 
         if (configManager.getConfig('crowi', 'security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
@@ -145,9 +152,28 @@ export const completeRegistrationAction = (crowi) => {
           else {
             logger.warn('E-mail Settings must be set up.');
           }
+
+          return res.apiv3({});
         }
 
-        res.apiv3({ status: 'ok' });
+        req.login(userData, (err) => {
+          if (err) {
+            logger.debug(err);
+          }
+          else {
+            // update lastLoginAt
+            userData.updateLastLoginAt(new Date(), (err) => {
+              if (err) {
+                logger.error(`updateLastLoginAt dumps error: ${err}`);
+              }
+            });
+          }
+
+          // userData.password cann't be empty but, prepare redirect because password property in User Model is optional
+          // https://github.com/weseek/growi/pull/6670
+          const redirectTo = userData.password != null ? '/' : '/me#password';
+          return res.apiv3({ redirectTo });
+        });
       });
     });
   };
@@ -222,12 +248,22 @@ export const registerAction = (crowi) => {
     const registerForm = req.body.registerForm || {};
     const email = registerForm.email;
     const isRegisterableEmail = await User.isRegisterableEmail(email);
+    const registrationMode = crowi.configManager.getConfig('crowi', 'security:registrationMode') as RegistrationMode;
+    const isEmailValid = await User.isEmailValid(email);
+
+    if (registrationMode === RegistrationMode.CLOSED) {
+      return res.apiv3Err(['message.registration_closed'], 400);
+    }
 
     if (!isRegisterableEmail) {
       req.body.registerForm.email = email;
       return res.apiv3Err(['message.email_address_is_already_registered'], 400);
     }
 
+    if (!isEmailValid) {
+      return res.apiv3Err(['message.email_address_could_not_be_used'], 400);
+    }
+
     try {
       await makeRegistrationEmailToken(email, crowi);
     }

+ 44 - 39
packages/app/src/server/routes/login.js

@@ -16,35 +16,6 @@ module.exports = function(crowi, app) {
 
   const actions = {};
 
-  const registerSuccessHandler = function(req, res, userData) {
-    req.login(userData, (err) => {
-      if (err) {
-        logger.debug(err);
-      }
-      else {
-        // update lastLoginAt
-        userData.updateLastLoginAt(new Date(), (err) => {
-          if (err) {
-            logger.error(`updateLastLoginAt dumps error: ${err}`);
-          }
-        });
-      }
-
-
-      // userData.password cann't be empty but, prepare redirect because password property in User Model is optional
-      // https://github.com/weseek/growi/pull/6670
-      const redirectTo = userData.password ? req.session.redirectTo : '/me#password';
-
-      // remove session.redirectTo
-      delete req.session.redirectTo;
-
-      const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-
-      return res.apiv3({ redirectTo });
-    });
-  };
-
   async function sendEmailToAllAdmins(userData) {
     // send mails to all admin users (derived from crowi) -- 2020.06.18 Yuki Takei
     const admins = await User.findAdmins();
@@ -71,6 +42,46 @@ module.exports = function(crowi, app) {
       .forEach(result => logger.error(result.reason));
   }
 
+  const registerSuccessHandler = async function(req, res, userData, registrationMode) {
+    const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
+    activityEvent.emit('update', res.locals.activity._id, parameters);
+
+    if (registrationMode === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
+      await sendEmailToAllAdmins(userData);
+      return res.apiv3({});
+    }
+
+    req.login(userData, (err) => {
+      if (err) {
+        logger.debug(err);
+      }
+      else {
+        // update lastLoginAt
+        userData.updateLastLoginAt(new Date(), (err) => {
+          if (err) {
+            logger.error(`updateLastLoginAt dumps error: ${err}`);
+          }
+        });
+      }
+
+      let redirectTo;
+      if (userData.password == null) {
+        // userData.password cann't be empty but, prepare redirect because password property in User Model is optional
+        // https://github.com/weseek/growi/pull/6670
+        redirectTo = '/me#password';
+      }
+      else if (req.session.redirectTo != null) {
+        redirectTo = req.session.redirectTo;
+        delete req.session.redirectTo;
+      }
+      else {
+        redirectTo = '/';
+      }
+
+      return res.apiv3({ redirectTo });
+    });
+  };
+
   actions.error = function(req, res) {
     const reason = req.params.reason;
 
@@ -133,14 +144,14 @@ module.exports = function(crowi, app) {
     User.isRegisterable(email, username, (isRegisterable, errOn) => {
       const errors = [];
       if (!User.isEmailValid(email)) {
-        errors.push('email_address_could_not_be_used');
+        errors.push('message.email_address_could_not_be_used');
       }
       if (!isRegisterable) {
         if (!errOn.username) {
-          errors.push('user_id_is_not_available');
+          errors.push('message.user_id_is_not_available');
         }
         if (!errOn.email) {
-          errors.push('email_address_is_already_registered');
+          errors.push('message.email_address_is_already_registered');
         }
       }
       if (errors.length > 0) {
@@ -166,13 +177,7 @@ module.exports = function(crowi, app) {
           }
           return res.apiv3Err(errors, 405);
         }
-
-        if (registrationMode === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
-          // send mail asynchronous
-          sendEmailToAllAdmins(userData);
-        }
-
-        return registerSuccessHandler(req, res, userData);
+        return registerSuccessHandler(req, res, userData, registrationMode);
       });
     });
   };

+ 0 - 7
packages/app/src/server/service/page.ts

@@ -2280,7 +2280,6 @@ class PageService {
 
     const likers = page.liker.slice(0, 15) as Ref<IUserHasId>[];
     const seenUsers = page.seenUsers.slice(0, 15) as Ref<IUserHasId>[];
-    const expandContentWidth = page.expandContentWidth ?? this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
 
     return {
       isV5Compatible: isTopPage(page.path) || page.parent != null,
@@ -2294,7 +2293,6 @@ class PageService {
       isAbleToDeleteCompletely: false,
       isRevertible: isTrashPage(page.path),
       contentAge: page.getContentAge(),
-      expandContentWidth,
     };
 
   }
@@ -3464,8 +3462,6 @@ class PageService {
   async create(path: string, body: string, user, options: PageCreateOptions = {}): Promise<PageDocument> {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
-    const expandContentWidth = this.crowi.configManager.getConfig('crowi', 'customize:isContainerFluid');
-
     // Switch method
     const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
     if (!isV5Compatible) {
@@ -3512,9 +3508,6 @@ class PageService {
       const parent = await this.getParentAndFillAncestorsByUser(user, path);
       page.parent = parent._id;
     }
-    if (expandContentWidth != null) {
-      page.expandContentWidth = expandContentWidth;
-    }
     // Save
     let savedPage = await page.save();
 

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -221,6 +221,10 @@ export const useGrowiAppIdForGrowiCloud = (initialData?: number): SWRResponse<nu
   return useStaticSWR('growiAppIdForGrowiCloud', initialData);
 };
 
+export const useIsContainerFluid = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isContainerFluid', initialData);
+};
+
 /** **********************************************************
  *                     Computed contexts
  *********************************************************** */

+ 13 - 2
packages/app/src/utils/admin-page-util.ts

@@ -1,10 +1,13 @@
+import type { IUserHasId } from '@growi/core';
 import { GetServerSidePropsContext } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 import {
-  getServerSideCommonProps, getNextI18NextConfig,
+  getServerSideCommonProps, getNextI18NextConfig, CommonProps,
 } from '~/pages/utils/commons';
 
+
 /**
  * for Server Side Translations
  * @param context
@@ -21,6 +24,9 @@ async function injectNextI18NextConfigurations(context: GetServerSidePropsContex
 export const retrieveServerSideProps: any = async(
     context: GetServerSidePropsContext, injectServerConfigurations?:(context: GetServerSidePropsContext, props) => Promise<void>,
 ) => {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+
   const result = await getServerSideCommonProps(context);
 
   // check for presence
@@ -29,10 +35,15 @@ export const retrieveServerSideProps: any = async(
     throw new Error('invalid getSSP result');
   }
 
-  const props = result.props;
+  const props: CommonProps = result.props as CommonProps;
   if (injectServerConfigurations != null) {
     await injectServerConfigurations(context, props);
   }
+
+  if (user != null) {
+    props.currentUser = user.toObject();
+  }
+
   await injectNextI18NextConfigurations(context, props, ['admin', 'commons']);
 
   return {

+ 7 - 2
packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts

@@ -40,8 +40,13 @@ context('Access to page', () => {
   });
 
   it('/Sandbox with edit is successfully loaded', () => {
-    cy.visit('/Sandbox#edit');
-    cy.screenshot(`${ssPrefix}-sandbox-edit-page`);
+    cy.visit('/Sandbox');
+    cy.get('.grw-skelton', { timeout: 30000 }).should('not.exist');
+    cy.get('#grw-subnav-container', { timeout: 30000 }).should('be.visible').within(()=>{
+      cy.getByTestid('editor-button', { timeout: 30000 }).should('be.visible').click();
+    })
+    cy.getByTestid('navbar-editor', { timeout: 30000 }).should('be.visible');
+    cy.screenshot(`${ssPrefix}-Sandbox-edit-page`);
   })
 
   it('/user/admin is successfully loaded', () => {

+ 3 - 1
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

@@ -206,6 +206,7 @@ context('Page Accessories Modal', () => {
       cy.getByTestid('open-page-item-control-btn').within(() => {
         cy.get('button.btn-page-item-control').click({force: true});
       });
+      cy.getByTestid('open-page-accessories-modal-btn-with-share-link-management-data-tab').should('be.visible');
       cy.getByTestid('open-page-accessories-modal-btn-with-share-link-management-data-tab').click();
    });
 
@@ -304,7 +305,8 @@ context('Tag Oprations', () =>{
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-    // cy.get('#wiki').should('be.visible');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(300);
     cy.screenshot(`${ssPrefix}1-click-tag-name`, {capture: 'viewport'});
 
     cy.getByTestid('search-result-list').within(() => {

+ 9 - 6
packages/app/test/cypress/integration/30-search/search.spec.ts

@@ -128,24 +128,27 @@ context('Search all pages', () => {
 
     // Duplicate page
     cy.getByTestid('open-page-duplicate-modal-btn').first().click({force: true});
-    cy.getByTestid('page-duplicate-modal').should('be.visible');
-    cy.screenshot(`${ssPrefix}6-duplicate-page`, {capture: 'viewport'});
+    cy.getByTestid('page-duplicate-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}6-duplicate-page`);
+    });
 
     // Close Modal
     cy.get('body').type('{esc}');
 
     // Move / Rename Page
     cy.getByTestid('open-page-move-rename-modal-btn').first().click({force: true});
-    cy.getByTestid('page-rename-modal').should('be.visible');
-    cy.screenshot(`${ssPrefix}7-move-rename-page`, {capture: 'viewport'});
+    cy.getByTestid('page-rename-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}7-move-rename-page`);
+    });
 
     // Close Modal
     cy.get('body').type('{esc}');
 
     // Delete page
     cy.getByTestid('open-page-delete-modal-btn').first().click({ force: true});
-    cy.getByTestid('page-delete-modal').should('be.visible');
-    cy.screenshot(`${ssPrefix}8-delete-page`, {capture: 'viewport'});
+    cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}8-delete-page`);
+    });
   });
 
   it(`Search all pages by tag is successfully loaded `, () => {

+ 3 - 0
packages/app/test/cypress/support/commands.ts

@@ -34,7 +34,10 @@ Cypress.Commands.add('login', (username, password) => {
     cy.visit('/page-to-return-after-login');
     cy.getByTestid('tiUsernameForLogin').type(username);
     cy.getByTestid('tiPasswordForLogin').type(password);
+
+    cy.intercept('POST', '/_api/v3/login').as('login');
     cy.getByTestid('btnSubmitForLogin').click();
+    cy.wait('@login')
   });
 });
 

+ 0 - 1
packages/core/src/interfaces/page.ts

@@ -74,7 +74,6 @@ export type IPageInfoForEntity = IPageInfo & {
   sumOfSeenUsers: number,
   seenUserIds: string[],
   contentAge: number,
-  expandContentWidth?: boolean,
 }
 
 export type IPageInfoForOperation = IPageInfoForEntity & {