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

Merge pull request #6651 from weseek/imprv/use-xhr-installer-modify-request

imprv: Use xhr installer modify request
Haku Mizuki 3 лет назад
Родитель
Сommit
17b83ddcef

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

@@ -191,7 +191,9 @@
     "setup": "Setup",
     "setup": "Setup",
     "create_initial_account": "Create an initial account",
     "create_initial_account": "Create an initial account",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
-    "unavaliable_user_id": "This 'User ID' is unavailable."
+    "unavaliable_user_id": "This 'User ID' is unavailable.",
+    "failed_to_install": "Failed to install GROWI. Please try again.",
+    "failed_to_login_after_install": "Failed to login after installation. Redirecting to the login form ..."
   },
   },
   "breaking_changes": {
   "breaking_changes": {
     "v346_using_basic_auth": "Basic Authentication currently in use will <strong>no longer be available</strong> in the near future. Remove settings from %s"
     "v346_using_basic_auth": "Basic Authentication currently in use will <strong>no longer be available</strong> in the near future. Remove settings from %s"

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

@@ -184,7 +184,9 @@
     "setup": "セットアップ",
     "setup": "セットアップ",
     "create_initial_account": "最初のアカウントの作成",
     "create_initial_account": "最初のアカウントの作成",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
-    "unavaliable_user_id": "このユーザーIDは利用できません。"
+    "unavaliable_user_id": "このユーザーIDは利用できません。",
+    "failed_to_install": "GROWI のインストールに失敗しました。再度お試しください。",
+    "failed_to_login_after_install": "インストール後、ログインに失敗しました。ログインフォームに遷移しています ..."
   },
   },
   "breaking_changes": {
   "breaking_changes": {
     "v346_using_basic_auth": "現在利用中の Basic 認証機能は、近い将来<strong>廃止されます</strong>。%s から設定を削除してください。"
     "v346_using_basic_auth": "現在利用中の Basic 認証機能は、近い将来<strong>廃止されます</strong>。%s から設定を削除してください。"

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

@@ -186,7 +186,9 @@
 		"setup": "安装",
 		"setup": "安装",
 		"create_initial_account": "创建初始用户",
 		"create_initial_account": "创建初始用户",
 		"initial_account_will_be_administrator_automatically": "初始帐户将自动成为管理员。",
 		"initial_account_will_be_administrator_automatically": "初始帐户将自动成为管理员。",
-		"unavaliable_user_id": "用户ID不可用"
+		"unavaliable_user_id": "用户ID不可用",
+    "failed_to_install": "GROWI安装失败。请再试一次。",
+    "failed_to_login_after_install": "安装后登录失败。重定向到登录表格..."
 	},
 	},
 	"breaking_changes": {
 	"breaking_changes": {
 		"v346_using_basic_auth": "当前使用的基本身份验证在不久的将来将不再可用。从%s中删除设置"
 		"v346_using_basic_auth": "当前使用的基本身份验证在不久的将来将不再可用。从%s中删除设置"

+ 50 - 5
packages/app/src/components/InstallerForm.tsx

@@ -1,10 +1,15 @@
-import { memo, useCallback, useState } from 'react';
+import {
+  FormEventHandler, memo, useCallback, useState,
+} from 'react';
 
 
 import i18next from 'i18next';
 import i18next from 'i18next';
 import { useTranslation, i18n } from 'next-i18next';
 import { useTranslation, i18n } from 'next-i18next';
 
 
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 
 
+import { toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
+
 const InstallerForm = memo((): JSX.Element => {
 const InstallerForm = memo((): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
@@ -23,8 +28,9 @@ const InstallerForm = memo((): JSX.Element => {
     setValidUserName(res.data.valid);
     setValidUserName(res.data.valid);
   }, []);
   }, []);
 
 
-  // TODO: XHRize https://redmine.weseek.co.jp/issues/105252
-  const submitHandler = useCallback(() => {
+  const submitHandler: FormEventHandler = useCallback(async(e: any) => {
+    e.preventDefault();
+
     if (isSubmittingDisabled) {
     if (isSubmittingDisabled) {
       return;
       return;
     }
     }
@@ -33,7 +39,46 @@ const InstallerForm = memo((): JSX.Element => {
     setTimeout(() => {
     setTimeout(() => {
       setSubmittingDisabled(false);
       setSubmittingDisabled(false);
     }, 3000);
     }, 3000);
-  }, [isSubmittingDisabled]);
+
+    if (e.target.elements == null) {
+      return;
+    }
+
+    const formData = e.target.elements;
+
+    const {
+      'registerForm[username]': { value: username },
+      'registerForm[name]': { value: name },
+      'registerForm[email]': { value: email },
+      'registerForm[password]': { value: password },
+    } = formData;
+
+    const data = {
+      registerForm: {
+        username,
+        name,
+        email,
+        password,
+        'app:globalLang': formData['registerForm[app:globalLang]'].value,
+      },
+    };
+
+    try {
+      await apiv3Post('/installer', data);
+      window.location.href = '/';
+    }
+    catch (errs) {
+      const err = errs[0];
+      const code = err.code;
+
+      if (code === 'failed_to_login_after_install') {
+        toastError(t('installer.failed_to_login_after_install'));
+        setTimeout(() => { window.location.href = '/login' }, 700); // Wait 700 ms to show toastr
+      }
+
+      toastError(t('installer.failed_to_install'));
+    }
+  }, [isSubmittingDisabled, t]);
 
 
   const hasErrorClass = isValidUserName ? '' : ' has-error';
   const hasErrorClass = isValidUserName ? '' : ' has-error';
   const unavailableUserId = isValidUserName
   const unavailableUserId = isValidUserName
@@ -51,7 +96,7 @@ const InstallerForm = memo((): JSX.Element => {
         </div>
         </div>
       </div>
       </div>
       <div className="row">
       <div className="row">
-        <form role="form" action="/installer" method="post" id="register-form" className="col-md-12" onSubmit={submitHandler}>
+        <form role="form" id="register-form" className="col-md-12" onSubmit={submitHandler}>
           <div className="dropdown mb-3">
           <div className="dropdown mb-3">
             <div className="d-flex dropdown-with-icon">
             <div className="d-flex dropdown-with-icon">
               <i className="icon-bubbles border-0 rounded-0" />
               <i className="icon-bubbles border-0 rounded-0" />

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

@@ -13,7 +13,7 @@ const router = express.Router();
 const routerForAdmin = express.Router();
 const routerForAdmin = express.Router();
 const routerForAuth = express.Router();
 const routerForAuth = express.Router();
 
 
-module.exports = (crowi) => {
+module.exports = (crowi, isInstalled) => {
 
 
   // add custom functions to express response
   // add custom functions to express response
   require('./response')(express, crowi);
   require('./response')(express, crowi);
@@ -40,6 +40,11 @@ module.exports = (crowi) => {
   // auth
   // auth
   routerForAuth.use('/logout', require('./logout')(crowi));
   routerForAuth.use('/logout', require('./logout')(crowi));
 
 
+  // installer
+  if (!isInstalled) {
+    routerForAdmin.use('/installer', require('./installer')(crowi));
+    return [router, routerForAdmin, routerForAuth];
+  }
 
 
   router.use('/in-app-notification', require('./in-app-notification')(crowi));
   router.use('/in-app-notification', require('./in-app-notification')(crowi));
 
 

+ 76 - 0
packages/app/src/server/routes/apiv3/installer.ts

@@ -0,0 +1,76 @@
+import express, { Request, Router } from 'express';
+
+import { SupportedAction } from '~/interfaces/activity';
+import ErrorV3 from '~/server/models/vo/error-apiv3';
+import loggerFactory from '~/utils/logger';
+
+import Crowi from '../../crowi';
+import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
+import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
+import { registerRules } from '../../middlewares/register-form-validator';
+import { InstallerService, FailedToCreateAdminUserError } from '../../service/installer';
+
+import { ApiV3Response } from './interfaces/apiv3-response';
+
+
+const logger = loggerFactory('growi:routes:apiv3:installer');
+
+
+type FormRequest = Request & { form: any, logIn: any };
+
+module.exports = (crowi: Crowi): Router => {
+  const addActivity = generateAddActivityMiddleware(crowi);
+
+  const activityEvent = crowi.event('activity');
+
+  const router = express.Router();
+
+  // eslint-disable-next-line max-len
+  router.post('/', registerRules(), apiV3FormValidator, addActivity, async(req: FormRequest, res: ApiV3Response) => {
+    const appService = crowi.appService;
+    if (appService == null) {
+      return res.apiv3Err(new ErrorV3('GROWI cannot be installed due to an internal error', 'app_service_not_setup'), 500);
+    }
+    const registerForm = req.body.registerForm || {};
+
+    const name = registerForm.name;
+    const username = registerForm.username;
+    const email = registerForm.email;
+    const password = registerForm.password;
+    const language = registerForm['app:globalLang'] || 'en_US';
+
+    const installerService = new InstallerService(crowi);
+
+    let adminUser;
+    try {
+      adminUser = await installerService.install({
+        name,
+        username,
+        email,
+        password,
+      }, language);
+    }
+    catch (err) {
+      if (err instanceof FailedToCreateAdminUserError) {
+        return res.apiv3Err(new ErrorV3(err.message, 'failed_to_install'));
+      }
+      return res.apiv3Err(new ErrorV3(err, 'failed_to_install'));
+    }
+
+    await appService.setupAfterInstall();
+
+    const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
+    activityEvent.emit('update', res.locals.activity._id, parameters);
+
+    // login with passport
+    req.logIn(adminUser, (err) => {
+      if (err != null) {
+        return res.apiv3Err(new ErrorV3(err, 'failed_to_login_after_install'));
+      }
+
+      return res.apiv3({ message: 'Installation completed (Logged in as an admin user)' });
+    });
+  });
+
+  return router;
+};

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

@@ -61,7 +61,7 @@ module.exports = function(crowi, app) {
 
 
   /* eslint-disable max-len, comma-spacing, no-multi-spaces */
   /* eslint-disable max-len, comma-spacing, no-multi-spaces */
 
 
-  const [apiV3Router, apiV3AdminRouter, apiV3AuthRouter] = require('./apiv3')(crowi);
+  const [apiV3Router, apiV3AdminRouter, apiV3AuthRouter] = require('./apiv3')(crowi, isInstalled);
 
 
   app.use('/api-docs', require('./apiv3/docs')(crowi));
   app.use('/api-docs', require('./apiv3/docs')(crowi));
 
 
@@ -93,9 +93,7 @@ module.exports = function(crowi, app) {
 
 
   // installer
   // installer
   if (!isInstalled) {
   if (!isInstalled) {
-    const installer = require('./installer')(crowi);
     app.get('/installer'              , applicationNotInstalled, next.delegateToNext);
     app.get('/installer'              , applicationNotInstalled, next.delegateToNext);
-    app.post('/installer'             , applicationNotInstalled , registerFormValidator.registerRules(), registerFormValidator.registerValidation, csrfProtection, addActivity, installer.install);
     return;
     return;
   }
   }
 
 

+ 0 - 70
packages/app/src/server/routes/installer.js

@@ -1,70 +0,0 @@
-import { SupportedAction } from '~/interfaces/activity';
-import loggerFactory from '~/utils/logger';
-
-import { InstallerService, FailedToCreateAdminUserError } from '../service/installer';
-
-const logger = loggerFactory('growi:routes:installer');
-
-module.exports = function(crowi) {
-
-  const actions = {};
-
-  const activityEvent = crowi.event('activity');
-
-  actions.index = function(req, res) {
-    return res.render('installer');
-  };
-
-  actions.install = async function(req, res, next) {
-    const registerForm = req.body.registerForm || {};
-
-    if (!req.form.isValid) {
-      return res.render('installer');
-    }
-
-    const name = registerForm.name;
-    const username = registerForm.username;
-    const email = registerForm.email;
-    const password = registerForm.password;
-    const language = registerForm['app:globalLang'] || 'en_US';
-
-    const installerService = new InstallerService(crowi);
-
-    let adminUser;
-    try {
-      adminUser = await installerService.install({
-        name,
-        username,
-        email,
-        password,
-      }, language);
-    }
-    catch (err) {
-      if (err instanceof FailedToCreateAdminUserError) {
-        req.form.errors.push(req.t('message.failed_to_create_admin_user', { errMessage: err.message }));
-      }
-      return res.render('installer');
-    }
-
-    const appService = crowi.appService;
-    appService.setupAfterInstall();
-
-    // login with passport
-    req.logIn(adminUser, (err) => {
-      if (err) {
-        req.flash('successMessage', req.t('message.complete_to_install1'));
-        req.session.redirectTo = '/';
-        return res.redirect('/login');
-      }
-
-      req.flash('successMessage', req.t('message.complete_to_install2'));
-
-      const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-
-      return res.redirect('/');
-    });
-  };
-
-  return actions;
-};

+ 1 - 1
packages/app/src/server/service/installer.ts

@@ -109,7 +109,7 @@ export class InstallerService {
     return configManager.updateConfigsInTheSameNamespace('crowi', initialConfig, true);
     return configManager.updateConfigsInTheSameNamespace('crowi', initialConfig, true);
   }
   }
 
 
-  async install(firstAdminUserToSave: IUser, globalLang: Lang, options?: AutoInstallOptions): Promise<IUser> {
+  async install(firstAdminUserToSave: Pick<IUser, 'name' | 'username' | 'email' | 'password'>, globalLang: Lang, options?: AutoInstallOptions): Promise<IUser> {
     await this.initDB(globalLang, options);
     await this.initDB(globalLang, options);
 
 
     // TODO typescriptize models/user.js and remove eslint-disable-next-line
     // TODO typescriptize models/user.js and remove eslint-disable-next-line