Sfoglia il codice sorgente

Merge branch 'reactify-admin/security' into reactify-admin/create-apiV3-update-twitter-setting

# Conflicts:
#	src/server/routes/apiv3/security-setting.js
itizawa 6 anni fa
parent
commit
51f7a2d5dd

+ 95 - 0
.github/workflows/test.yml

@@ -0,0 +1,95 @@
+name: Node CI
+
+# on: [push]
+on:
+  push:
+    branches:
+      - support/github-actions
+
+jobs:
+
+  resolve-dependencies:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        node-version: [10.x, 12.x]
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: install dependencies
+      run: |
+        yarn
+    - name: install plugins
+      run: |
+        yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
+        yarn add -D react-images react-motion
+    - name: print dependencies
+      run: |
+        echo -n "node " && node -v
+        echo -n "npm " && npm -v
+        yarn list --depth=0
+
+
+  lint:
+    runs-on: ubuntu-latest
+    needs: resolve-dependencies
+
+    steps:
+    - uses: actions/checkout@v1
+    - name: yarn lint
+      run: |
+        yarn lint
+
+
+  test:
+    runs-on: ubuntu-latest
+    needs: resolve-dependencies
+
+    steps:
+    - name: Launch MongoDB
+      uses: wbari/start-mongoDB@v0.2
+      with:
+        mongoDBVersion: 3.6
+
+    - name: yarn test
+      run: |
+        yarn test
+      env:
+        MONGO_URI: mongodb://localhost:27017/growi_test
+
+
+  build-dev:
+    runs-on: ubuntu-latest
+    needs: resolve-dependencies
+
+    steps:
+    - name: yarn build:dev
+      run: |
+        yarn build:dev
+
+
+  build-prod:
+    runs-on: ubuntu-latest
+    needs: resolve-dependencies
+
+    steps:
+    - name: Launch MongoDB
+      uses: wbari/start-mongoDB@v0.2
+      with:
+        mongoDBVersion: 3.6
+    - name: yarn build:prod:analyze
+      run: |
+        yarn build:prod:analyze
+    - name: shrink dependencies for production
+      run: |
+        yarn install --production
+    - name: yarn server:prod:ci
+      run: |
+        yarn server:prod:ci
+      env:
+        MONGO_URI: mongodb://localhost:27017/growi

+ 59 - 22
docker/Dockerfile

@@ -5,51 +5,85 @@ ARG flavor=default
 
 
 
 
 ##
 ##
-## setupper-default
+## deps-resolver
 ##
 ##
-FROM node:12-slim AS setupper-default
+FROM node:12-slim AS deps-resolver
 LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 
 
-RUN mkdir -p ${appDir}
-RUN mv .* ${appDir}/
+ENV appDir /opt/growi
 
 
+COPY ./package.json ${appDir}/
+COPY ./yarn.lock ${appDir}/
 WORKDIR ${appDir}
 WORKDIR ${appDir}
 
 
 # setup
 # setup
 RUN yarn config set network-timeout 300000
 RUN yarn config set network-timeout 300000
-RUN yarn
+RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
+  yarn
 # install official plugins
 # install official plugins
-RUN yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
+RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
+  yarn add growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs
 # install peerDependencies
 # install peerDependencies
-RUN yarn add -D react-images react-motion
+RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
+  yarn add -D react-images react-motion
+
+
+
+##
+## deps-resolver-prod
+##
+FROM deps-resolver AS deps-resolver-prod
+
+# shrink dependencies for production
+RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
+  yarn install --production
+
+
+
+##
+## prebuilder-default
+##
+FROM node:12-slim AS prebuilder-default
+LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
+
+ENV appDir /opt/growi
+
+COPY . ${appDir}
 
 
 
 
 
 
 ##
 ##
-## setupper-nocdn
+## prebuilder-nocdn
 ##
 ##
-FROM setupper-default AS setupper-nocdn
+FROM prebuilder-default AS prebuilder-nocdn
 
 
 # replace env.prod.js for NO_CDN
 # replace env.prod.js for NO_CDN
-COPY nocdn/env.prod.js config
+COPY docker/nocdn/env.prod.js ${appDir}/config/
+
+
+
+##
+## prebuilder (alias)
+##
+FROM prebuilder-${flavor} AS prebuilder
 
 
 
 
 
 
 ##
 ##
 ## builder
 ## builder
 ##
 ##
-FROM setupper-${flavor} AS builder
+FROM deps-resolver AS builder
 
 
 ENV appDir /opt/growi
 ENV appDir /opt/growi
 
 
+COPY --from=prebuilder ${appDir} ${appDir}
+
 # build
 # build
 RUN yarn build:prod
 RUN yarn build:prod
-# shrink dependencies for production
-RUN yarn install --production
 
 
-# remove unnecessary files
+# remove except artifacts
 WORKDIR /tmp
 WORKDIR /tmp
-RUN --mount=target=. sh bin/remove-unnecessary-files.sh
+RUN --mount=target=. sh docker/bin/remove-except-artifacts.sh
 WORKDIR ${appDir}
 WORKDIR ${appDir}
 
 
 
 
@@ -63,18 +97,21 @@ LABEL maintainer Yuki Takei <yuki@weseek.co.jp>
 ENV appDir /opt/growi
 ENV appDir /opt/growi
 
 
 # install tini
 # install tini
-RUN apk add --no-cache tini
+RUN --mount=type=cache,target=/var/cache/apk \
+  apk add tini su-exec
 
 
-COPY --from=builder ${appDir} ${appDir}
+COPY docker/docker-entrypoint.sh /
+RUN chmod 700 /docker-entrypoint.sh
+
+COPY --from=deps-resolver-prod --chown=node:node \
+  ${appDir}/node_modules ${appDir}/node_modules
+COPY --from=builder --chown=node:node \
+  ${appDir} ${appDir}
 
 
-# create symlink for FILE_UPLOAD=local
-WORKDIR /tmp
-RUN --mount=target=. sh bin/symlink-for-uploading-to-local.sh
 WORKDIR ${appDir}
 WORKDIR ${appDir}
 
 
-USER node
 VOLUME /data
 VOLUME /data
 EXPOSE 3000
 EXPOSE 3000
 
 
-ENTRYPOINT ["/sbin/tini", "-e", "143", "--"]
+ENTRYPOINT ["/sbin/tini", "-e", "143", "--", "/docker-entrypoint.sh"]
 CMD ["yarn", "server:prod"]
 CMD ["yarn", "server:prod"]

+ 14 - 0
docker/Dockerfile.dockerignore

@@ -0,0 +1,14 @@
+.git
+.github
+.vscode
+node_modules
+src/linter-checker
+src/test
+.editorconfig
+.eslint*
+.gitignore
+.prettier*
+.stylelint*
+app.json
+Procfile
+wercker.yml

+ 10 - 0
docker/bin/remove-except-artifacts.sh

@@ -0,0 +1,10 @@
+#!/bin/sh
+
+set -e
+
+rm -rf \
+  ${appDir}/bin \
+  ${appDir}/docker \
+  ${appDir}/node_modules \
+  ${appDir}/src/client \
+  ${appDir}/babel.config.js \

+ 0 - 21
docker/bin/remove-unnecessary-files.sh

@@ -1,21 +0,0 @@
-#!/bin/sh
-
-set -e
-
-rm -rf \
-  ${appDir}/.github \
-  ${appDir}/.vscode \
-  ${appDir}/bin \
-  ${appDir}/docker \
-  ${appDir}/src/client \
-  ${appDir}/src/linter-checker \
-  ${appDir}/src/test \
-  ${appDir}/.editorconfig \
-  ${appDir}/.eslint* \
-  ${appDir}/.gitignore \
-  ${appDir}/.prettier* \
-  ${appDir}/.stylelint* \
-  ${appDir}/app.json \
-  ${appDir}/babel.config.js \
-  ${appDir}/Procfile \
-  ${appDir}/wercker.yml

+ 6 - 1
docker/bin/symlink-for-uploading-to-local.sh → docker/docker-entrypoint.sh

@@ -2,8 +2,13 @@
 
 
 set -e
 set -e
 
 
-# Corresponds to `FILE_UPLOAD=local`
+# Support `FILE_UPLOAD=local`
 mkdir -p /data/uploads
 mkdir -p /data/uploads
 if [ ! -e "$appDir/public/uploads" ]; then
 if [ ! -e "$appDir/public/uploads" ]; then
   ln -s /data/uploads $appDir/public/uploads
   ln -s /data/uploads $appDir/public/uploads
 fi
 fi
+
+chown node:node /data/uploads
+chown -h node:node $appDir/public/uploads
+
+su-exec node $@

+ 1 - 0
resource/locales/en-US/translation.json

@@ -475,6 +475,7 @@
     "desc_of_callback_URL": "Use it in the setting of the {{AuthName}} Identity provider",
     "desc_of_callback_URL": "Use it in the setting of the {{AuthName}} Identity provider",
     "clientID": "Client ID",
     "clientID": "Client ID",
     "client_secret": "Client Secret",
     "client_secret": "Client Secret",
+    "updated_general_security_setting": "Succeeded to update security setting",
     "guest_mode": {
     "guest_mode": {
       "deny": "Deny (Registered Users Only)",
       "deny": "Deny (Registered Users Only)",
       "readonly": "Accept (Guests can read only)"
       "readonly": "Accept (Guests can read only)"

+ 1 - 0
resource/locales/ja/translation.json

@@ -470,6 +470,7 @@
     "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
     "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
     "clientID": "クライアントID",
     "clientID": "クライアントID",
     "client_secret": "クライアントシークレット",
     "client_secret": "クライアントシークレット",
+    "updated_general_security_setting": "セキュリティ設定を更新しました。",
     "guest_mode": {
     "guest_mode": {
       "deny": "拒否 (アカウントを持つユーザーのみ利用可能)",
       "deny": "拒否 (アカウントを持つユーザーのみ利用可能)",
       "readonly": "許可 (ゲストユーザーも閲覧のみ可能)"
       "readonly": "許可 (ゲストユーザーも閲覧のみ可能)"

+ 2 - 76
src/client/js/components/Admin/Security/SecurityManagement.jsx

@@ -9,6 +9,7 @@ import LdapSecuritySetting from './LdapSecuritySetting';
 import LocalSecuritySetting from './LocalSecuritySetting';
 import LocalSecuritySetting from './LocalSecuritySetting';
 import SamlSecuritySetting from './SamlSecuritySetting';
 import SamlSecuritySetting from './SamlSecuritySetting';
 import OidcSecuritySetting from './OidcSecuritySetting';
 import OidcSecuritySetting from './OidcSecuritySetting';
+import SecuritySetting from './SecuritySetting';
 import BasicSecuritySetting from './BasicSecuritySetting';
 import BasicSecuritySetting from './BasicSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import GithubSecuritySetting from './GithubSecuritySetting';
 import GithubSecuritySetting from './GithubSecuritySetting';
@@ -26,82 +27,7 @@ class SecurityManagement extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
     return (
     return (
       <Fragment>
       <Fragment>
-        {/* TODO GW-582 reactify-admin */}
-        <fieldset>
-          <legend className="alert-anchor">{ t('security_settings') }</legend>
-          <div className="form-group">
-            <label htmlFor="settingForm[security:restrictGuestMode]" className="col-xs-3 control-label">{ t('security_setting.Guest Users Access') }</label>
-            <div className="col-xs-6">
-              <select
-                className="form-control selectpicker"
-                name="settingForm[security:restrictGuestMode]"
-                value="{ getConfig('crowi', 'security:restrictGuestMode') }"
-              >
-                <option value="{ t(modeValue) }">{ t('modeLabel') }</option>
-              </select>
-              <p className="alert alert-warning mt-2">
-                <i className="icon-exclamation icon-fw">
-                </i><b>FIXED</b>
-                { t('security_setting.Fixed by env var', 'FORCE_WIKI_MODE') }<br></br>
-              </p>
-            </div>
-          </div>
-          <div className="form-group">
-            <label htmlFor="{{configName}}" className="col-xs-3 control-label">{ t('security_setting.page_listing_1') }</label>
-            <div className="col-xs-9">
-              <div className="btn-group btn-toggle" data-toggle="buttons">
-                <label className="btn btn-default btn-rounded btn-outline {% if isEnabled %}active{% endif %}" data-active-class="primary">
-                  <input name="{{configName}}" value="false" type="radio"></input>
-                </label>
-                <label className="btn btn-default btn-rounded btn-outline {% if !isEnabled %}active{% endif %}" data-active-class="default">
-                  <input name="{{configName}}" value="true" type="radio"></input>
-                </label>
-              </div>
-              <p className="help-block small">
-                { t('security_setting.page_listing_1_desc') }
-              </p>
-            </div>
-          </div>
-
-          <div className="form-group">
-            <label htmlFor="{{configName}}" className="col-xs-3 control-label">{ t('security_setting.page_listing_2') }</label>
-            <div className="col-xs-9">
-              <div className="btn-group btn-toggle" data-toggle="buttons">
-                <label className="btn btn-default btn-rounded btn-outline {% if isEnabled %}active{% endif %}" data-active-class="primary">
-                  <input name="{{configName}}" value="false" type="radio" />
-                </label>
-                <label className="btn btn-default btn-rounded btn-outline {% if !isEnabled %}active{% endif %}" data-active-class="default">
-                  <input name="{{configName}}" value="true" type="radio" />
-                </label>
-              </div>
-
-              <p className="help-block small">
-                { t('security_setting.page_listing_2_desc') }
-              </p>
-            </div>
-          </div>
-
-          <div className="form-group">
-            <label htmlFor="{{configName}}" className="col-xs-3 control-label">{ t('security_setting.complete_deletion') }</label>
-            <div className="col-xs-6">
-              <select className="form-control selectpicker" name="settingForm[security:pageCompleteDeletionAuthority]" value="{{ configValue }}">
-                <option value="anyOne">{ t('security_setting.anyone') }</option>
-                <option value="adminOnly">{ t('security_setting.admin_only') }</option>
-                <option value="adminAndAuthor">{ t('security_setting.admin_and_author') }</option>
-              </select>
-
-              <p className="help-block small">
-                { t('security_setting.complete_deletion_explain') }
-              </p>
-            </div>
-          </div>
-          {/* TODO GW-540 */}
-          <div className="form-group">
-            <div className="col-xs-offset-3 col-xs-6">
-              <input type="hidden" name="_csrf" value={this.props.csrf} />
-            </div>
-          </div>
-        </fieldset>
+        <div><SecuritySetting /></div>
 
 
         {/* XSS configuration link */}
         {/* XSS configuration link */}
         <div className="mb-5">
         <div className="mb-5">

+ 196 - 0
src/client/js/components/Admin/Security/SecuritySetting.jsx

@@ -0,0 +1,196 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+import AdminGeneralSecurityContainer from '../../../services/AdminGeneralSecurityContainer';
+
+class SecuritySetting extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.putSecuritySetting = this.putSecuritySetting.bind(this);
+  }
+
+  async putSecuritySetting() {
+    const { t } = this.props;
+    try {
+      await this.props.adminGeneralSecurityContainer.updateGeneralSecuritySetting();
+      toastSuccess(t('security_setting.updated_general_security_setting'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  render() {
+    const { t, adminGeneralSecurityContainer } = this.props;
+    const helpPageListingByOwner = { __html: t('security_setting.page_listing_1') };
+    const helpPageListingByGroup = { __html: t('security_setting.page_listing_2') };
+    return (
+      <React.Fragment>
+        <fieldset>
+          <legend className="alert-anchor">{ t('security_settings') }</legend>
+          {/* TODO adjust layout */}
+          <div className="row mb-5">
+            <strong className="col-xs-3 text-right"> { t('security_setting.Guest Users Access') } </strong>
+            <div className="col-xs-9 text-left">
+              <div className="my-0 btn-group">
+                <div className="dropdown">
+                  <button
+                    className="btn btn-default dropdown-toggle w-100"
+                    type="button"
+                    data-toggle="dropdown"
+                    aria-haspopup="true"
+                    aria-expanded="false"
+                    disabled={adminGeneralSecurityContainer.state.isWikiModeForced}
+                  >
+                    <span className="pull-left">{t(`security_setting.guest_mode.${adminGeneralSecurityContainer.state.currentRestrictGuestMode}`)}</span>
+                    <span className="bs-caret pull-right">
+                      <span className="caret" />
+                    </span>
+                  </button>
+                  {/* TODO adjust dropdown after BS4 */}
+                  <ul className="dropdown-menu" role="menu">
+                    <li
+                      key="deny"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('deny') }}
+                    >
+                      <a role="menuitem">{ t('security_setting.guest_mode.deny') }</a>
+                    </li>
+                    <li
+                      key="readonly"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('readonly') }}
+                    >
+                      <a role="menuitem">{ t('security_setting.guest_mode.readonly') }</a>
+                    </li>
+                  </ul>
+                </div>
+              </div>
+            </div>
+          </div>
+          {adminGeneralSecurityContainer.state.isWikiModeForced && (
+            <div className="row mb-5">
+              <div className="col-xs-6">
+                <p className="alert alert-warning mt-2">
+                  <i className="icon-exclamation icon-fw">
+                  </i><b>FIXED</b>
+                  { t('security_setting.Fixed by env var', 'FORCE_WIKI_MODE') }<br></br>
+                </p>
+              </div>
+            </div>
+          )}
+          <div className="row mb-5">
+            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={helpPageListingByOwner} />
+            <div className="col-xs-6 text-left">
+              <div className="checkbox checkbox-success">
+                <input
+                  id="isHideRestrictedByOwner"
+                  type="checkbox"
+                  checked={adminGeneralSecurityContainer.state.isHideRestrictedByOwner}
+                  onChange={() => { adminGeneralSecurityContainer.switchIsHideRestrictedByOwner() }}
+                />
+                <label htmlFor="isHideRestrictedByOwner">
+                  <p className="help-block small">{ t('security_setting.page_listing_1_desc') }</p>
+                </label>
+              </div>
+            </div>
+          </div>
+
+          <div className="row mb-5">
+            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={helpPageListingByGroup} />
+            <div className="col-xs-6 text-left">
+              <div className="checkbox checkbox-success">
+                <input
+                  id="isHideRestrictedByGroup"
+                  type="checkbox"
+                  checked={adminGeneralSecurityContainer.state.isHideRestrictedByGroup}
+                  onChange={() => { adminGeneralSecurityContainer.switchIsHideRestrictedByGroup() }}
+                />
+                <label htmlFor="isHideRestrictedByGroup">
+                  <p className="help-block small">{ t('security_setting.page_listing_2_desc') }</p>
+                </label>
+              </div>
+            </div>
+          </div>
+
+          <div className="row mb-5">
+            <strong className="col-xs-3 text-right"> { t('security_setting.complete_deletion') } </strong>
+            <div className="col-xs-9 text-left">
+              <div className="my-0 btn-group">
+                <div className="dropdown">
+                  <button className="btn btn-default dropdown-toggle w-100" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                    <span className="pull-left">{t(`security_setting.${adminGeneralSecurityContainer.state.currentpageCompleteDeletionAuthority}`)}</span>
+                    <span className="bs-caret pull-right">
+                      <span className="caret" />
+                    </span>
+                  </button>
+                  {/* TODO adjust dropdown after BS4 */}
+                  <ul className="dropdown-menu" role="menu">
+                    <li
+                      key="anyone"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('anyone') }}
+                    >
+                      <a role="menuitem">{ t('security_setting.anyone') }</a>
+                    </li>
+                    <li
+                      key="admin_only"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('admin_only') }}
+                    >
+                      <a role="menuitem">{ t('security_setting.admin_only') }</a>
+                    </li>
+                    <li
+                      key="admin_and_author"
+                      role="presentation"
+                      type="button"
+                      onClick={() => { adminGeneralSecurityContainer.changePageCompleteDeletionAuthority('admin_and_author') }}
+                    >
+                      <a role="menuitem">{ t('security_setting.admin_and_author') }</a>
+                    </li>
+                  </ul>
+                  <p className="help-block small">
+                    { t('security_setting.complete_deletion_explain') }
+                  </p>
+                </div>
+              </div>
+            </div>
+          </div>
+          {/* TODO GW-540 */}
+          <div className="form-group">
+            <div className="col-xs-offset-3 col-xs-6">
+              <input type="hidden" name="_csrf" value={this.props.csrf} />
+              <button type="submit" className="btn btn-primary" onClick={this.putSecuritySetting}>{ t('Update') }</button>
+            </div>
+          </div>
+        </fieldset>
+      </React.Fragment>
+    );
+  }
+
+}
+
+SecuritySetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  csrf: PropTypes.string,
+  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
+};
+
+const SecuritySettingWrapper = (props) => {
+  return createSubscribedElement(SecuritySetting, props, [AppContainer, AdminGeneralSecurityContainer]);
+};
+
+export default withTranslation()(SecuritySettingWrapper);

+ 56 - 1
src/client/js/services/AdminGeneralSecurityContainer.js

@@ -6,7 +6,7 @@ import loggerFactory from '@alias/logger';
 const logger = loggerFactory('growi:security:AdminGeneralSecurityContainer');
 const logger = loggerFactory('growi:security:AdminGeneralSecurityContainer');
 
 
 /**
 /**
- * Service container for admin security page (SecurityManagement.jsx)
+ * Service container for admin security page (SecuritySetting.jsx)
  * @extends {Container} unstated Container
  * @extends {Container} unstated Container
  */
  */
 export default class AdminGeneralSecurityContainer extends Container {
 export default class AdminGeneralSecurityContainer extends Container {
@@ -18,6 +18,11 @@ export default class AdminGeneralSecurityContainer extends Container {
 
 
     this.state = {
     this.state = {
       // TODO GW-583 set value
       // TODO GW-583 set value
+      isWikiModeForced: false,
+      currentRestrictGuestMode: 'deny',
+      currentPageCompleteDeletionAuthority: 'anyone',
+      isHideRestrictedByOwner: true,
+      isHideRestrictedByGroup: true,
       useOnlyEnvVarsForSomeOptions: true,
       useOnlyEnvVarsForSomeOptions: true,
       appSiteUrl: appContainer.config.crowi.url || '',
       appSiteUrl: appContainer.config.crowi.url || '',
       isLocalEnabled: true,
       isLocalEnabled: true,
@@ -36,6 +41,11 @@ export default class AdminGeneralSecurityContainer extends Container {
 
 
     this.switchIsLocalEnabled = this.switchIsLocalEnabled.bind(this);
     this.switchIsLocalEnabled = this.switchIsLocalEnabled.bind(this);
     this.changeRegistrationMode = this.changeRegistrationMode.bind(this);
     this.changeRegistrationMode = this.changeRegistrationMode.bind(this);
+    this.changeRestrictGuestMode = this.changeRestrictGuestMode.bind(this);
+    this.changePageCompleteDeletionAuthority = this.changePageCompleteDeletionAuthority.bind(this);
+    this.switchIsHideRestrictedByGroup = this.switchIsHideRestrictedByGroup.bind(this);
+    this.switchIsHideRestrictedByOwner = this.switchIsHideRestrictedByOwner.bind(this);
+    this.changePageCompleteDeletionAuthority = this.changePageCompleteDeletionAuthority.bind(this);
   }
   }
 
 
   init() {
   init() {
@@ -50,6 +60,51 @@ export default class AdminGeneralSecurityContainer extends Container {
     return 'AdminGeneralSecurityContainer';
     return 'AdminGeneralSecurityContainer';
   }
   }
 
 
+  /**
+   * Change restrictGuestMode
+   */
+  changeRestrictGuestMode(restrictGuestModeLabel) {
+    this.setState({ currentRestrictGuestMode: restrictGuestModeLabel });
+  }
+
+  /**
+   * Change pageCompleteDeletionAuthority
+   */
+  changePageCompleteDeletionAuthority(pageCompleteDeletionAuthorityLabel) {
+    this.setState({ currentPageCompleteDeletionAuthority: pageCompleteDeletionAuthorityLabel });
+  }
+
+  /**
+   * Switch hideRestrictedByOwner
+   */
+  switchIsHideRestrictedByOwner() {
+    this.setState({ isHideRestrictedByOwner:  !this.state.isHideRestrictedByOwner });
+  }
+
+  /**
+   * Switch hideRestrictedByGroup
+   */
+  switchIsHideRestrictedByGroup() {
+    this.setState({ isHideRestrictedByGroup:  !this.state.isHideRestrictedByGroup });
+  }
+
+
+  /**
+   * Update restrictGuestMode
+   * @memberOf AdminGeneralSecuritySContainer
+   * @return {string} Appearance
+   */
+  async updateGeneralSecuritySetting() {
+    const response = await this.appContainer.apiv3.put('/security-setting/general-setting', {
+      restrictGuestMode: this.state.currentRestrictGuestMode,
+      pageCompleteDeletionAuthority: this.state.currentPageCompleteDeletionAuthority,
+      hideRestrictedByGroup: this.state.isHideRestrictedByGroup,
+      hideRestrictedByOwner: this.state.isHideRestrictedByOwner,
+    });
+    const { securitySettingParams } = response.data;
+    return securitySettingParams;
+  }
+
   /**
   /**
    * Switch local enabled
    * Switch local enabled
    */
    */

+ 0 - 114
src/client/js/services/AdminSecurityContainer.js

@@ -1,114 +0,0 @@
-import { Container } from 'unstated';
-
-import loggerFactory from '@alias/logger';
-
-// eslint-disable-next-line no-unused-vars
-const logger = loggerFactory('growi:services:AdminSecurityContainer');
-
-/**
- * Service container for admin security setting page (SecuritySetting.jsx)
- * @extends {Container} unstated Container
- */
-export default class AdminSecurityContainer extends Container {
-
-  constructor(appContainer) {
-    super();
-
-    this.appContainer = appContainer;
-
-    this.state = {
-      // TODO GW-583 set Data from apiv3
-      currentRestrictGuestMode: appContainer.config.restrictGuestMode,
-      currentpageCompleteDeletionAuthority: appContainer.config.pageCompleteDeletionAuthority,
-      hideRestrictedByOwner: appContainer.config.hideRestrictedByOwner,
-      hideRestrictedByGroup: appContainer.config.hideRestrictedByGroup,
-    };
-
-    this.init();
-
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'AdminSecrityContainer';
-  }
-
-  /**
-   * retrieve security data
-   */
-  async init() {
-    // TODO GW-583 init state by apiv3
-  }
-
-
-  /**
-   * Switch restrictGuestMode
-   */
-  switchRestrictGuestMode(restrictGuestModeLabel) {
-    this.setState({ currentRestrictGuestMode: restrictGuestModeLabel });
-  }
-
-  /**
-   * Switch pageCompleteDeletionAuthority
-   */
-  switchPageCompleteDeletionAuthority(pageCompleteDeletionAuthorityLabel) {
-    this.setState({ currentpageCompleteDeletionAuthority: pageCompleteDeletionAuthorityLabel });
-  }
-
-  /**
-   * Switch hideRestrictedByOwner
-   */
-  switchHideRestrictedByOwner() {
-    this.setState({ hideRestrictedByOwner:  !this.state.hideRestrictedByOwner });
-  }
-
-  /**
-   * Switch hideRestrictedByGroup
-   */
-  switchHideRestrictedByGroup() {
-    this.setState({ hideRestrictedByGroup:  !this.state.hideRestrictedByGroup });
-  }
-
-  /**
-   * Update restrictGuestMode
-   * @memberOf AdminSecurityContainer
-   * @return {string} Appearance
-   */
-  async updateRestrictGuestMode() {
-    const response = await this.appContainer.apiv3.put('/security-setting/guest-mode', {
-      restrictGuestMode: this.state.currentRestrictGuestMode,
-    });
-    const { securitySettingParams } = response.data;
-    return securitySettingParams;
-  }
-
-  /**
-   * Update pageDeletion
-   * @memberOf AdminSecurityContainer
-   * @return {string} pageDeletion
-   */
-  async updatePageCompleteDeletionAuthority() {
-    const response = await this.appContainer.apiv3.put('/security-setting/page-deletion', {
-      pageCompleteDeletionAuthority: this.state.currentPageCompleteDeletionAuthority,
-    });
-    const { securitySettingParams } = response.data;
-    return securitySettingParams;
-  }
-
-  /**
-   * Update function
-   * @memberOf AdminSecucityContainer
-   * @return {string} Functions
-   */
-  async updateSecurityFunction() {
-    const response = await this.appContainer.apiv3.put('/security-setting/function', {
-      hideRestrictedByGroup: this.state.hideRestrictedByGroup,
-      hideRestrictedByOwner: this.state.hideRestrictedByOwner,
-    });
-    const { securitySettingParams } = response.data;
-    return securitySettingParams;
-  }
-
-}

+ 41 - 0
src/migrations/20191126173016-adjust-pages-path.js

@@ -0,0 +1,41 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:adjust-pages-path');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+const pathUtils = require('growi-commons').pathUtils;
+
+module.exports = {
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Page = require('@server/models/page')();
+
+    // retrieve target data
+    const pages = await Page.find({ path: /^(?!\/)/ });
+
+
+    // create requests for bulkWrite
+    const requests = pages.map((page) => {
+      const adjustedPath = pathUtils.addHeadingSlash(page.path);
+      return {
+        updateOne: {
+          filter: { _id: page._id },
+          update: { $set: { path: adjustedPath } },
+        },
+      };
+    });
+
+    if (requests.length > 0) {
+      await db.collection('pages').bulkWrite(requests);
+    }
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+};

+ 18 - 104
src/server/routes/apiv3/security-setting.js

@@ -13,13 +13,9 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 const validator = {
 const validator = {
   // TODO correct validator
   // TODO correct validator
-  guestMode: [
+  generalSetting: [
     body('restrictGuestMode').isString(),
     body('restrictGuestMode').isString(),
-  ],
-  pageDeletion: [
     body('pageCompleteDeletionAuthority').isString(),
     body('pageCompleteDeletionAuthority').isString(),
-  ],
-  function: [
     body('hideRestrictedByOwner').isBoolean(),
     body('hideRestrictedByOwner').isBoolean(),
     body('hideRestrictedByGroup').isBoolean(),
     body('hideRestrictedByGroup').isBoolean(),
   ],
   ],
@@ -122,10 +118,10 @@ module.exports = (crowi) => {
   /**
   /**
    * @swagger
    * @swagger
    *
    *
-   *    /security-setting/guest-mode:
+   *    /security-setting/general-setting:
    *      put:
    *      put:
    *        tags: [SecuritySetting]
    *        tags: [SecuritySetting]
-   *        description: Update restrictGuestMode
+   *        description: Update GeneralSetting
    *        requestBody:
    *        requestBody:
    *          required: true
    *          required: true
    *          content:
    *          content:
@@ -136,113 +132,29 @@ module.exports = (crowi) => {
    *                  restrictGuestMode:
    *                  restrictGuestMode:
    *                    description: type of restrictGuestMode
    *                    description: type of restrictGuestMode
    *                    type: string
    *                    type: string
-   *        responses:
-   *          200:
-   *            description: Succeeded to update restrictGuestMode
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    status:
-   *                      $ref: '#/components/schemas/GuestModeParams'
-   */
-  router.put('/guest-mode', loginRequiredStrictly, adminRequired, csrf, validator.guestMode, ApiV3FormValidator, async(req, res) => {
-    const requestParams = {
-      'security:restrictGuestMode': req.body.restrictGuestMode,
-    };
-
-    try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
-      const securitySettingParams = {
-        restrictGuestMode: await crowi.configManager.getConfig('crowi', 'security:restrictGuestMode'),
-      };
-      return res.apiv3({ securitySettingParams });
-    }
-    catch (err) {
-      const msg = 'Error occurred in updating restrict guest mode';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-restrictGuestMode-failed'));
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   *    /security-setting/page-deletion:
-   *      put:
-   *        tags: [SecuritySetting]
-   *        description: Update pageDeletion Setting
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                type: object
-   *                properties:
-   *                 pageCompleteDeletionAuthority:
-   *                    description: type of pageCompleteDeletionAuthority
+   *                  pageCompleteDeletionAuthority:
    *                    type: string
    *                    type: string
-   *        responses:
-   *          200:
-   *            description: Succeeded to update pageDeletion
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    status:
-   *                      $ref: '#/components/schemas/PageDeletionParams'
-   */
-  router.put('/page-deletion', loginRequiredStrictly, adminRequired, csrf, validator.pageDeletion, ApiV3FormValidator, async(req, res) => {
-    const requestParams = {
-      'security:pageCompleteDeletionAuthority': req.body.pageCompleteDeletionAuthority,
-    };
-
-    try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
-      const securitySettingParams = {
-        pageCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
-      };
-      return res.apiv3({ securitySettingParams });
-    }
-    catch (err) {
-      const msg = 'Error occurred in updating page-deletion-setting';
-      logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-page-deletion-setting-failed'));
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   *    /security-setting/function:
-   *      put:
-   *        tags: [SecuritySetting]
-   *        description: Update function
-   *        requestBody:
-   *          required: true
-   *          content:
-   *            application/json:
-   *              schema:
-   *                type: object
-   *                properties:
+   *                    description: type of pageDeletionAuthority
    *                  hideRestrictedByOwner:
    *                  hideRestrictedByOwner:
-   *                    description: is enabled hideRestrictedByOwner
    *                    type: boolean
    *                    type: boolean
-   *                  ihideRestrictedByGroup:
-   *                    description: is enabled hideRestrictedBygroup
+   *                    description: enable hide by owner
+   *                  hideRestrictedByGroup:
    *                    type: boolean
    *                    type: boolean
+   *                    description: enable hide by group
    *        responses:
    *        responses:
    *          200:
    *          200:
-   *            description: Succeeded to update function
+   *            description: Succeeded to update general Setting
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
    *                  properties:
    *                  properties:
    *                    status:
    *                    status:
-   *                      $ref: '#/components/schemas/Function'
+   *                      $ref: '#/components/schemas/SecurityParams/GeneralSetting'
    */
    */
-  router.put('/function', loginRequiredStrictly, adminRequired, csrf, validator.function, ApiV3FormValidator, async(req, res) => {
+  router.put('/general-setting', loginRequiredStrictly, adminRequired, csrf, validator.generalSetting, ApiV3FormValidator, async(req, res) => {
     const requestParams = {
     const requestParams = {
+      'security:restrictGuestMode': req.body.restrictGuestMode,
+      'security:pageCompleteDeletionAuthority': req.body.pageCompleteDeletionAuthority,
       'security:list-policy:hideRestrictedByOwner': req.body.hideRestrictedByOwner,
       'security:list-policy:hideRestrictedByOwner': req.body.hideRestrictedByOwner,
       'security:list-policy:hideRestrictedByGroup': req.body.hideRestrictedByGroup,
       'security:list-policy:hideRestrictedByGroup': req.body.hideRestrictedByGroup,
     };
     };
@@ -250,15 +162,17 @@ module.exports = (crowi) => {
     try {
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const securitySettingParams = {
       const securitySettingParams = {
+        restrictGuestMode: await crowi.configManager.getConfig('crowi', 'security:restrictGuestMode'),
+        pageCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
-        hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'customize:security:list-policy:hideRestrictedByGroup'),
+        hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
       };
       };
       return res.apiv3({ securitySettingParams });
       return res.apiv3({ securitySettingParams });
     }
     }
     catch (err) {
     catch (err) {
-      const msg = 'Error occurred in updating function';
+      const msg = 'Error occurred in updating security setting';
       logger.error('Error', err);
       logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-function-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'update-secuirty-setting failed'));
     }
     }
   });
   });
 
 

+ 12 - 3
src/server/routes/page.js

@@ -573,7 +573,7 @@ module.exports = function(crowi, app) {
    */
    */
   api.create = async function(req, res) {
   api.create = async function(req, res) {
     const body = req.body.body || null;
     const body = req.body.body || null;
-    const pagePath = req.body.path || null;
+    let pagePath = req.body.path || null;
     const grant = req.body.grant || null;
     const grant = req.body.grant || null;
     const grantUserGroupId = req.body.grantUserGroupId || null;
     const grantUserGroupId = req.body.grantUserGroupId || null;
     const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
     const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
@@ -586,6 +586,9 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('Parameters body and path are required.'));
       return res.json(ApiResponse.error('Parameters body and path are required.'));
     }
     }
 
 
+    // check whether path starts slash
+    pagePath = pathUtils.addHeadingSlash(pagePath);
+
     // check page existence
     // check page existence
     const isExist = await Page.count({ path: pagePath }) > 0;
     const isExist = await Page.count({ path: pagePath }) > 0;
     if (isExist) {
     if (isExist) {
@@ -1064,7 +1067,7 @@ module.exports = function(crowi, app) {
   api.rename = async function(req, res) {
   api.rename = async function(req, res) {
     const pageId = req.body.page_id;
     const pageId = req.body.page_id;
     const previousRevision = req.body.revision_id || null;
     const previousRevision = req.body.revision_id || null;
-    const newPagePath = pathUtils.normalizePath(req.body.new_path);
+    let newPagePath = pathUtils.normalizePath(req.body.new_path);
     const options = {
     const options = {
       createRedirectPage: (req.body.create_redirect != null),
       createRedirectPage: (req.body.create_redirect != null),
       updateMetadata: (req.body.remain_metadata == null),
       updateMetadata: (req.body.remain_metadata == null),
@@ -1076,6 +1079,9 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error(`Could not use the path '${newPagePath})'`, 'invalid_path'));
       return res.json(ApiResponse.error(`Could not use the path '${newPagePath})'`, 'invalid_path'));
     }
     }
 
 
+    // check whether path starts slash
+    newPagePath = pathUtils.addHeadingSlash(newPagePath);
+
     const isExist = await Page.count({ path: newPagePath }) > 0;
     const isExist = await Page.count({ path: newPagePath }) > 0;
     if (isExist) {
     if (isExist) {
       // if page found, cannot cannot rename to that path
       // if page found, cannot cannot rename to that path
@@ -1130,7 +1136,7 @@ module.exports = function(crowi, app) {
    */
    */
   api.duplicate = async function(req, res) {
   api.duplicate = async function(req, res) {
     const pageId = req.body.page_id;
     const pageId = req.body.page_id;
-    const newPagePath = pathUtils.normalizePath(req.body.new_path);
+    let newPagePath = pathUtils.normalizePath(req.body.new_path);
 
 
     const page = await Page.findByIdAndViewer(pageId, req.user);
     const page = await Page.findByIdAndViewer(pageId, req.user);
 
 
@@ -1138,6 +1144,9 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
       return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
     }
     }
 
 
+    // check whether path starts slash
+    newPagePath = pathUtils.addHeadingSlash(newPagePath);
+
     await page.populateDataToShowRevision();
     await page.populateDataToShowRevision();
     const originTags = await page.findRelatedTagsById();
     const originTags = await page.findRelatedTagsById();