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

Merge branch 'feat/auditlog' of https://github.com/weseek/growi into feat/93691-93694-swrize-activity-list-and-show-on-table

Shun Miyazawa 4 лет назад
Родитель
Сommit
70e70e38c5

+ 13 - 10
packages/app/src/client/app.jsx

@@ -21,33 +21,34 @@ import loggerFactory from '~/utils/logger';
 import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import ErrorBoundary from '../components/ErrorBoudary';
-import RedirectedAlert from '../components/Page/RedirectedAlert';
-import TrashPageList from '../components/TrashPageList';
-import TrashPageAlert from '../components/Page/TrashPageAlert';
-import NotFoundPage from '../components/NotFoundPage';
-import NotFoundAlert from '../components/Page/NotFoundAlert';
+import Fab from '../components/Fab';
 import ForbiddenPage from '../components/ForbiddenPage';
-import PageStatusAlert from '../components/PageStatusAlert';
-import RecentCreated from '../components/RecentCreated/RecentCreated';
 import RecentlyCreatedIcon from '../components/Icons/RecentlyCreatedIcon';
-import MyDraftList from '../components/MyDraftList/MyDraftList';
-import BookmarkList from '../components/PageList/BookmarkList';
-import Fab from '../components/Fab';
 import InAppNotificationPage from '../components/InAppNotification/InAppNotificationPage';
+import MaintenanceModeContent from '../components/MaintenanceModeContent';
 import PersonalSettings from '../components/Me/PersonalSettings';
+import MyDraftList from '../components/MyDraftList/MyDraftList';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
 import GrowiSubNavigationSwitcher from '../components/Navbar/GrowiSubNavigationSwitcher';
+import NotFoundPage from '../components/NotFoundPage';
 import Page from '../components/Page';
 import DisplaySwitcher from '../components/Page/DisplaySwitcher';
+import NotFoundAlert from '../components/Page/NotFoundAlert';
+import RedirectedAlert from '../components/Page/RedirectedAlert';
 import ShareLinkAlert from '../components/Page/ShareLinkAlert';
+import TrashPageAlert from '../components/Page/TrashPageAlert';
 import PageComment from '../components/PageComment';
 import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
 import PageContentFooter from '../components/PageContentFooter';
 import { defaultEditorOptions, defaultPreviewOptions } from '../components/PageEditor/OptionsSelector';
+import BookmarkList from '../components/PageList/BookmarkList';
+import PageStatusAlert from '../components/PageStatusAlert';
 import PageTimeline from '../components/PageTimeline';
+import RecentCreated from '../components/RecentCreated/RecentCreated';
 import { SearchPage } from '../components/SearchPage';
 import Sidebar from '../components/Sidebar';
 import TagPage from '../components/TagPage';
+import TrashPageList from '../components/TrashPageList';
 
 import { appContainer, componentMappings } from './base';
 import { toastError } from './util/apiNotification';
@@ -94,6 +95,8 @@ Object.assign(componentMappings, {
 
   'grw-page-status-alert-container': <PageStatusAlert />,
 
+  'maintenance-mode-content': <MaintenanceModeContent />,
+
   'trash-page-alert': <TrashPageAlert />,
 
   'trash-page-list-container': <TrashPageList />,

+ 10 - 6
packages/app/src/client/nologin.jsx

@@ -1,17 +1,19 @@
 import React from 'react';
+
 import ReactDOM from 'react-dom';
-import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
+import { Provider } from 'unstated';
 
-import { i18nFactory } from './util/i18n';
 
 import AppContainer from '~/client/services/AppContainer';
+import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationForm';
 
 import InstallerForm from '../components/InstallerForm';
 import LoginForm from '../components/LoginForm';
-import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
 import PasswordResetExecutionForm from '../components/PasswordResetExecutionForm';
-import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationForm';
+import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
+
+import { i18nFactory } from './util/i18n';
 
 const i18n = i18nFactory();
 
@@ -85,10 +87,12 @@ if (loginFormElem) {
   );
 }
 
-// render PasswordResetRequestForm
-const passwordResetRequestFormElem = document.getElementById('password-reset-request-form');
 const appContainer = new AppContainer();
 appContainer.initApp();
+
+
+// render PasswordResetRequestForm
+const passwordResetRequestFormElem = document.getElementById('password-reset-request-form');
 if (passwordResetRequestFormElem) {
 
   ReactDOM.render(

+ 9 - 7
packages/app/src/client/services/ContextExtractor.tsx

@@ -1,6 +1,15 @@
 import React, { FC, useEffect, useState } from 'react';
+
 import { pagePathUtils } from '@growi/core';
 
+import { IUserUISettings } from '~/interfaces/user-ui-settings';
+import {
+  useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
+  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
+  useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+} from '~/stores/ui';
+import { useSetupGlobalSocket, useSetupGlobalAdminSocket } from '~/stores/websocket';
+
 import {
   useSiteUrl,
   useCurrentCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd,
@@ -10,13 +19,6 @@ import {
   useSlackChannels, useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
 } from '../../stores/context';
-import {
-  useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
-  usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
-  useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
-} from '~/stores/ui';
-import { useSetupGlobalSocket, useSetupGlobalAdminSocket } from '~/stores/websocket';
-import { IUserUISettings } from '~/interfaces/user-ui-settings';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
 

+ 52 - 0
packages/app/src/components/MaintenanceModeContent.tsx

@@ -0,0 +1,52 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { useCurrentUser } from '~/stores/context';
+
+
+const MaintenanceModeContent = () => {
+  const { t } = useTranslation();
+
+  const { data: currentUser } = useCurrentUser();
+
+  const logoutHandler = async() => {
+    try {
+      await apiv3Post('/logout');
+      window.location.reload();
+    }
+    catch (err) {
+      toastError(err);
+    }
+
+  };
+
+  return (
+    <div className="text-left">
+      <p>
+        <i className="icon-arrow-right"></i>
+        <a className="btn btn-link" href="/admin">{ t('maintenance_mode.admin_page') }</a>
+      </p>
+      {currentUser != null
+        ? (
+          <p>
+            <i className="icon-arrow-right"></i>
+            <a className="btn btn-link" onClick={logoutHandler} id="maintanounse-mode-logout">{ t('maintenance_mode.logout') }</a>
+          </p>
+        )
+        : (
+          <p>
+            <i className="icon-arrow-right"></i>
+            <a className="btn btn-link" href="/login">{ t('maintenance_mode.login') }</a>
+          </p>
+        )
+      }
+    </div>
+  );
+
+};
+
+
+export default MaintenanceModeContent;

+ 7 - 5
packages/app/src/components/Me/ProfileImageSettings.jsx

@@ -1,13 +1,15 @@
 import React from 'react';
+
+import md5 from 'md5';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-import md5 from 'md5';
 
+import AppContainer from '~/client/services/AppContainer';
+import PersonalContainer from '~/client/services/PersonalContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
 import { withUnstatedContainers } from '../UnstatedUtils';
 
-import AppContainer from '~/client/services/AppContainer';
-import PersonalContainer from '~/client/services/PersonalContainer';
 
 import ImageCropModal from './ImageCropModal';
 
@@ -115,14 +117,14 @@ class ProfileImageSettings extends React.Component {
                   onChange={() => { personalContainer.changeIsGravatarEnabled(true) }}
                 />
                 <label className="custom-control-label" htmlFor="radioGravatar">
-                  <img src="https://gravatar.com/avatar/00000000000000000000000000000000?s=24" /> Gravatar
+                  <img src="https://gravatar.com/avatar/00000000000000000000000000000000?s=24" data-hide-in-vrt /> Gravatar
                 </label>
                 <a href="https://gravatar.com/">
                   <small><i className="icon-arrow-right-circle" aria-hidden="true"></i></small>
                 </a>
               </div>
             </h4>
-            <img src={this.generateGravatarSrc()} width="64" />
+            <img src={this.generateGravatarSrc()} width="64" data-hide-in-vrt />
           </div>
 
           <div className="col-md-6 col-12">

+ 17 - 27
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -1,13 +1,12 @@
 import React, { useState, useCallback } from 'react';
 
 import { UserPicture } from '@growi/ui';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 
-
-import AppContainer from '~/client/services/AppContainer';
 import { useUserUISettings } from '~/client/services/user-ui-settings';
+import { toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
 import {
   isUserPreferenceExists,
   isDarkMode as isDarkModeByUtil,
@@ -16,6 +15,7 @@ import {
   updateUserPreference,
   updateUserPreferenceWithOsSettings,
 } from '~/client/util/color-scheme';
+import { useCurrentUser } from '~/stores/context';
 import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
 
 
@@ -23,13 +23,13 @@ import MoonIcon from '../Icons/MoonIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';
 import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
 import SunIcon from '../Icons/SunIcon';
-import { withUnstatedContainers } from '../UnstatedUtils';
 
 
-const PersonalDropdown = (props) => {
+const PersonalDropdown = () => {
+  const { t } = useTranslation();
+  const { data: currentUser } = useCurrentUser();
 
-  const { t, appContainer } = props;
-  const user = appContainer.currentUser || {};
+  const user = currentUser || {};
 
   const [useOsSettings, setOsSettings] = useState(!isUserPreferenceExists());
   const [isDarkMode, setIsDarkMode] = useState(isDarkModeByUtil());
@@ -38,13 +38,14 @@ const PersonalDropdown = (props) => {
   const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
   const { scheduleToPut } = useUserUISettings();
 
-  const logoutHandler = () => {
-    const { interceptorManager } = appContainer;
-
-    const context = {};
-    interceptorManager.process('logout', context);
-
-    window.location.href = '/logout';
+  const logoutHandler = async() => {
+    try {
+      await apiv3Post('/logout');
+      window.location.reload();
+    }
+    catch (err) {
+      toastError(err);
+    }
   };
 
   const preferDrawerModeSwitchModifiedHandler = useCallback((bool) => {
@@ -229,15 +230,4 @@ const PersonalDropdown = (props) => {
 
 };
 
-/**
- * Wrapper component for using unstated
- */
-const PersonalDropdownWrapper = withUnstatedContainers(PersonalDropdown, [AppContainer]);
-
-
-PersonalDropdown.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default withTranslation()(PersonalDropdownWrapper);
+export default PersonalDropdown;

+ 2 - 0
packages/app/src/interfaces/activity.ts

@@ -8,6 +8,7 @@ const MODEL_COMMENT = 'Comment';
 // Action
 const ACTION_PAGE_LIKE = 'PAGE_LIKE';
 const ACTION_PAGE_BOOKMARK = 'PAGE_BOOKMARK';
+const ACTION_PAGE_CREATE = 'PAGE_CREATE';
 const ACTION_PAGE_UPDATE = 'PAGE_UPDATE';
 const ACTION_PAGE_RENAME = 'PAGE_RENAME';
 const ACTION_PAGE_DUPLICATE = 'PAGE_DUPLICATE';
@@ -29,6 +30,7 @@ export const SUPPORTED_EVENT_MODEL_TYPE = {
 export const SUPPORTED_ACTION_TYPE = {
   ACTION_PAGE_LIKE,
   ACTION_PAGE_BOOKMARK,
+  ACTION_PAGE_CREATE,
   ACTION_PAGE_UPDATE,
   ACTION_PAGE_RENAME,
   ACTION_PAGE_DUPLICATE,

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

@@ -11,6 +11,7 @@ const express = require('express');
 
 const router = express.Router();
 const routerForAdmin = express.Router();
+const routerForAuth = express.Router();
 
 module.exports = (crowi) => {
 
@@ -36,6 +37,9 @@ module.exports = (crowi) => {
   routerForAdmin.use('/slack-integration-legacy-settings', require('./slack-integration-legacy-settings')(crowi));
   routerForAdmin.use('/activity', require('./activity')(crowi));
 
+  // auth
+  routerForAuth.use('/logout', require('./logout')(crowi));
+
 
   router.use('/in-app-notification', require('./in-app-notification')(crowi));
 
@@ -77,5 +81,5 @@ module.exports = (crowi) => {
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 
 
-  return [router, routerForAdmin];
+  return [router, routerForAdmin, routerForAuth];
 };

+ 16 - 0
packages/app/src/server/routes/apiv3/logout.js

@@ -0,0 +1,16 @@
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:routes:apiv3:logout'); // eslint-disable-line no-unused-vars
+
+const express = require('express');
+
+const router = express.Router();
+
+module.exports = (crowi) => {
+  router.post('/', async(req, res) => {
+    req.session.destroy();
+    return res.send();
+  });
+
+  return router;
+};

+ 16 - 0
packages/app/src/server/routes/apiv3/pages.js

@@ -1,6 +1,8 @@
+import { SUPPORTED_TARGET_MODEL_TYPE, SUPPORTED_ACTION_TYPE } from '~/interfaces/activity';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
 import loggerFactory from '~/utils/logger';
 
+
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
@@ -339,6 +341,20 @@ module.exports = (crowi) => {
       }
     }
 
+    // create activity
+    try {
+      const parameters = {
+        user: req.user._id,
+        targetModel: SUPPORTED_TARGET_MODEL_TYPE.MODEL_PAGE,
+        target: createdPage,
+        action: SUPPORTED_ACTION_TYPE.PAGE_CREATE,
+      };
+      await crowi.activityService.createByParameter(parameters);
+    }
+    catch (err) {
+      logger.error('Failed to create activity', err);
+    }
+
     // create subscription
     try {
       await crowi.inAppNotificationService.createSubscription(req.user.id, createdPage._id, subscribeRuleNames.PAGE_CREATE);

+ 5 - 3
packages/app/src/server/routes/index.js

@@ -44,7 +44,6 @@ module.exports = function(crowi, app) {
   const page = require('./page')(crowi, app);
   const login = require('./login')(crowi, app);
   const loginPassport = require('./login-passport')(crowi, app);
-  const logout = require('./logout')(crowi, app);
   const me = require('./me')(crowi, app);
   const admin = require('./admin')(crowi, app);
   const user = require('./user')(crowi, app);
@@ -62,13 +61,16 @@ module.exports = function(crowi, app) {
 
   /* eslint-disable max-len, comma-spacing, no-multi-spaces */
 
-  const [apiV3Router, apiV3AdminRouter] = require('./apiv3')(crowi);
+  const [apiV3Router, apiV3AdminRouter, apiV3AuthRouter] = require('./apiv3')(crowi);
 
   app.use('/api-docs', require('./apiv3/docs')(crowi));
 
   // API v3 for admin
   app.use('/_api/v3', apiV3AdminRouter);
 
+  // API v3 for auth
+  app.use('/_api/v3', apiV3AuthRouter);
+
   app.get('/'                         , applicationInstalled, unavailableWhenMaintenanceMode, loginRequired, autoReconnectToSearch, injectUserUISettings, page.showTopPage);
 
   app.get('/login/error/:reason'      , applicationInstalled, login.error);
@@ -76,10 +78,10 @@ module.exports = function(crowi, app) {
   app.get('/login/invited'            , applicationInstalled, login.invited);
   app.post('/login/activateInvited'   , apiLimiter , applicationInstalled, loginFormValidator.inviteRules(), loginFormValidator.inviteValidation, csrf, login.invited);
   app.post('/login'                   , apiLimiter , applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation, csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
+  app.post('/login'                   , apiLimiter , applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation, csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
 
   app.post('/register'                , apiLimiter , applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, csrf, login.register);
   app.get('/register'                 , applicationInstalled, login.preLogin, login.register);
-  app.get('/logout'                   , applicationInstalled, logout.logout);
 
   app.get('/admin'                    , applicationInstalled, loginRequiredStrictly , adminRequired , admin.index);
   app.get('/admin/app'                , applicationInstalled, loginRequiredStrictly , adminRequired , admin.app.index);

+ 3 - 19
packages/app/src/server/views/maintenance-mode.html

@@ -3,13 +3,12 @@
 {% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('maintenance_mode.maintenance_mode')) }}{% endblock %}
 
 
+
 {#
   # Remove default contents
   #}
  {% block html_head_loading_legacy %}
  {% endblock %}
- {% block html_head_loading_app %}
- {% endblock %}
  {% block layout_head_nav %}
  {% endblock %}
  {% block sidebar %}
@@ -19,6 +18,7 @@
  {% block fixed-controls %}
  {% endblock %}
 
+
 {% block layout_main %}
 <div id="main" class="main">
   <div id="content-main" class="content-main container-lg">
@@ -30,23 +30,7 @@
             <h1 class="text-center">{{ t('maintenance_mode.maintenance_mode') }}</h1>
             <h3>{{ t('maintenance_mode.growi_is_under_maintenance') }}</h3>
             <hr />
-            <div class="text-left">
-              <p>
-                <i class="icon-arrow-right"></i>
-                <a href="/admin">{{ t('maintenance_mode.admin_page') }}</a>
-              </p>
-              {% if not user %}
-                <p>
-                  <i class="icon-arrow-right"></i>
-                  <a href="/login">{{ t('maintenance_mode.login') }}</a>
-                </p>
-              {% else %}
-                <p>
-                  <i class="icon-arrow-right"></i>
-                  <a href="/logout">{{ t('maintenance_mode.logout') }}</a>
-                </p>
-              {% endif %}
-            </div>
+            <div id="maintenance-mode-content"></div>
           </div>
         </div>
       </div>

+ 3 - 2
packages/app/test/cypress/integration/6-home/home.spec.ts

@@ -59,12 +59,13 @@ context('Access User settings', () => {
     cy.getByTestid('grw-associate-modal').should('be.visible');
     cy.screenshot(`${ssPrefix}-external-account-2`);
     cy.getByTestid('grw-associate-modal').find('.modal-footer button').click(); // click add button in modal form
+    cy.get('.toast').should('be.visible').invoke('attr', 'style', 'opacity: 1');
     cy.screenshot(`${ssPrefix}-external-account-3`);
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+    cy.get('.toast').should('not.exist');
     cy.getByTestid('grw-associate-modal').find('.close').click();
-    cy.get('.toast').should('be.visible').invoke('attr', 'style', 'opacity: 1');
     cy.screenshot(`${ssPrefix}-external-account-4`);
 
-    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
     cy.get('.toast').should('not.exist');
 
     // Access Password setting