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

configure biome for app routes excluding apiv3 and attachment routes

Futa Arai 4 месяцев назад
Родитель
Сommit
116b79702f

+ 2 - 0
apps/app/.eslintrc.js

@@ -58,6 +58,8 @@ module.exports = {
     'src/server/util/**',
     'src/server/app.ts',
     'src/server/repl.ts',
+    'src/server/routes/*.js',
+    'src/server/routes/*.ts',
   ],
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript

+ 77 - 44
apps/app/src/server/routes/admin.js

@@ -8,7 +8,7 @@ const logger = loggerFactory('growi:routes:admin');
 
 /* eslint-disable no-use-before-define */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
-module.exports = function(crowi, app) {
+module.exports = (crowi, app) => {
   const ApiResponse = require('../util/apiResponse');
   const importer = require('../util/importer')(crowi);
 
@@ -20,40 +20,52 @@ module.exports = function(crowi, app) {
 
   const api = {};
 
-
   // Importer management
   actions.importer = {};
   actions.importer.api = api;
   api.validators = {};
   api.validators.importer = {};
 
-  api.validators.importer.esa = function() {
+  api.validators.importer.esa = () => {
     const validator = [
-      check('importer:esa:team_name').not().isEmpty().withMessage('Error. Empty esa:team_name'),
-      check('importer:esa:access_token').not().isEmpty().withMessage('Error. Empty esa:access_token'),
+      check('importer:esa:team_name')
+        .not()
+        .isEmpty()
+        .withMessage('Error. Empty esa:team_name'),
+      check('importer:esa:access_token')
+        .not()
+        .isEmpty()
+        .withMessage('Error. Empty esa:access_token'),
     ];
     return validator;
   };
 
-  api.validators.importer.qiita = function() {
+  api.validators.importer.qiita = () => {
     const validator = [
-      check('importer:qiita:team_name').not().isEmpty().withMessage('Error. Empty qiita:team_name'),
-      check('importer:qiita:access_token').not().isEmpty().withMessage('Error. Empty qiita:access_token'),
+      check('importer:qiita:team_name')
+        .not()
+        .isEmpty()
+        .withMessage('Error. Empty qiita:team_name'),
+      check('importer:qiita:access_token')
+        .not()
+        .isEmpty()
+        .withMessage('Error. Empty qiita:access_token'),
     ];
     return validator;
   };
 
-
   // Export management
   actions.export = {};
   actions.export.api = api;
   api.validators.export = {};
 
-  api.validators.export.download = function() {
+  api.validators.export.download = () => {
     const validator = [
       // https://regex101.com/r/mD4eZs/6
       // prevent from pass traversal attack
-      param('fileName').not().matches(/(\.\.\/|\.\.\\)/),
+      param('fileName')
+        .not()
+        .matches(/(\.\.\/|\.\.\\)/),
     ];
     return validator;
   };
@@ -63,13 +75,17 @@ module.exports = function(crowi, app) {
     const { validationResult } = require('express-validator');
     const errors = validationResult(req);
     if (!errors.isEmpty()) {
-      return res.status(422).json({ errors: `${fileName} is invalid. Do not use path like '../'.` });
+      return res
+        .status(422)
+        .json({
+          errors: `${fileName} is invalid. Do not use path like '../'.`,
+        });
     }
 
     try {
       const zipFile = exportService.getFile(fileName);
       const parameters = {
-        ip:  req.ip,
+        ip: req.ip,
         endpoint: req.originalUrl,
         action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_DOWNLOAD,
         user: req.user?._id,
@@ -79,8 +95,7 @@ module.exports = function(crowi, app) {
       };
       crowi.activityService.createActivity(parameters);
       return res.download(zipFile);
-    }
-    catch (err) {
+    } catch (err) {
       // TODO: use ApiV3Error
       logger.error(err);
       return res.json(ApiResponse.error());
@@ -100,10 +115,14 @@ module.exports = function(crowi, app) {
    */
   function isValidFormKeys(form, allowedKeys, res) {
     const receivedKeys = Object.keys(form);
-    const unexpectedKeys = receivedKeys.filter(key => !allowedKeys.includes(key));
+    const unexpectedKeys = receivedKeys.filter(
+      (key) => !allowedKeys.includes(key),
+    );
 
     if (unexpectedKeys.length > 0) {
-      logger.warn('Unexpected keys were found in request body.', { unexpectedKeys });
+      logger.warn('Unexpected keys were found in request body.', {
+        unexpectedKeys,
+      });
       res.json(ApiResponse.error('Invalid config keys provided.'));
       return false;
     }
@@ -117,7 +136,7 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  actions.api.importerSettingEsa = async(req, res) => {
+  actions.api.importerSettingEsa = async (req, res) => {
     const form = req.body;
 
     const { validationResult } = require('express-validator');
@@ -126,12 +145,17 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('esa.io form is blank'));
     }
 
-    const ALLOWED_KEYS = ['importer:esa:team_name', 'importer:esa:access_token'];
+    const ALLOWED_KEYS = [
+      'importer:esa:team_name',
+      'importer:esa:access_token',
+    ];
     if (!isValidFormKeys(form, ALLOWED_KEYS, res)) return;
 
     await configManager.updateConfigs(form);
     importer.initializeEsaClient(); // let it run in the back aftert res
-    const parameters = { action: SupportedAction.ACTION_ADMIN_ESA_DATA_UPDATED };
+    const parameters = {
+      action: SupportedAction.ACTION_ADMIN_ESA_DATA_UPDATED,
+    };
     activityEvent.emit('update', res.locals.activity._id, parameters);
     return res.json(ApiResponse.success());
   };
@@ -142,7 +166,7 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  actions.api.importerSettingQiita = async(req, res) => {
+  actions.api.importerSettingQiita = async (req, res) => {
     const form = req.body;
 
     const { validationResult } = require('express-validator');
@@ -151,12 +175,17 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('Qiita form is blank'));
     }
 
-    const ALLOWED_KEYS = ['importer:qiita:team_name', 'importer:qiita:access_token'];
+    const ALLOWED_KEYS = [
+      'importer:qiita:team_name',
+      'importer:qiita:access_token',
+    ];
     if (!isValidFormKeys(form, ALLOWED_KEYS, res)) return;
 
     await configManager.updateConfigs(form);
     importer.initializeQiitaClient(); // let it run in the back aftert res
-    const parameters = { action: SupportedAction.ACTION_ADMIN_QIITA_DATA_UPDATED };
+    const parameters = {
+      action: SupportedAction.ACTION_ADMIN_QIITA_DATA_UPDATED,
+    };
     activityEvent.emit('update', res.locals.activity._id, parameters);
     return res.json(ApiResponse.success());
   };
@@ -167,16 +196,17 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  actions.api.importDataFromEsa = async(req, res) => {
+  actions.api.importDataFromEsa = async (req, res) => {
     const user = req.user;
     let errors;
 
     try {
       errors = await importer.importDataFromEsa(user);
-      const parameters = { action: SupportedAction.ACTION_ADMIN_ESA_DATA_IMPORTED };
+      const parameters = {
+        action: SupportedAction.ACTION_ADMIN_ESA_DATA_IMPORTED,
+      };
       activityEvent.emit('update', res.locals.activity._id, parameters);
-    }
-    catch (err) {
+    } catch (err) {
       errors = [err];
     }
 
@@ -192,16 +222,17 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  actions.api.importDataFromQiita = async(req, res) => {
+  actions.api.importDataFromQiita = async (req, res) => {
     const user = req.user;
     let errors;
 
     try {
       errors = await importer.importDataFromQiita(user);
-      const parameters = { action: SupportedAction.ACTION_ADMIN_QIITA_DATA_IMPORTED };
+      const parameters = {
+        action: SupportedAction.ACTION_ADMIN_QIITA_DATA_IMPORTED,
+      };
       activityEvent.emit('update', res.locals.activity._id, parameters);
-    }
-    catch (err) {
+    } catch (err) {
       errors = [err];
     }
 
@@ -217,14 +248,15 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  actions.api.testEsaAPI = async(req, res) => {
+  actions.api.testEsaAPI = async (req, res) => {
     try {
       await importer.testConnectionToEsa();
-      const parameters = { action: SupportedAction.ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA };
+      const parameters = {
+        action: SupportedAction.ACTION_ADMIN_CONNECTION_TEST_OF_ESA_DATA,
+      };
       activityEvent.emit('update', res.locals.activity._id, parameters);
       return res.json(ApiResponse.success());
-    }
-    catch (err) {
+    } catch (err) {
       return res.json(ApiResponse.error(err));
     }
   };
@@ -235,29 +267,30 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  actions.api.testQiitaAPI = async(req, res) => {
+  actions.api.testQiitaAPI = async (req, res) => {
     try {
       await importer.testConnectionToQiita();
-      const parameters = { action: SupportedAction.ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA };
+      const parameters = {
+        action: SupportedAction.ACTION_ADMIN_CONNECTION_TEST_OF_QIITA_DATA,
+      };
       activityEvent.emit('update', res.locals.activity._id, parameters);
       return res.json(ApiResponse.success());
-    }
-    catch (err) {
+    } catch (err) {
       return res.json(ApiResponse.error(err));
     }
   };
 
-
-  actions.api.searchBuildIndex = async function(req, res) {
+  actions.api.searchBuildIndex = async (req, res) => {
     const search = crowi.getSearcher();
     if (!search) {
-      return res.json(ApiResponse.error('ElasticSearch Integration is not set up.'));
+      return res.json(
+        ApiResponse.error('ElasticSearch Integration is not set up.'),
+      );
     }
 
     try {
       search.buildIndex();
-    }
-    catch (err) {
+    } catch (err) {
       return res.json(ApiResponse.error(err));
     }
 

+ 1 - 3
apps/app/src/server/routes/avoid-session-routes.js

@@ -1,3 +1 @@
-module.exports = [
-  /^\/api-docs\//,
-];
+module.exports = [/^\/api-docs\//];

+ 80 - 42
apps/app/src/server/routes/comment.js

@@ -2,7 +2,11 @@ import { getIdStringForRef } from '@growi/core';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
 import { Comment, CommentEvent, commentEvent } from '~/features/comment/server';
-import { SupportedAction, SupportedTargetModel, SupportedEventModel } from '~/interfaces/activity';
+import {
+  SupportedAction,
+  SupportedEventModel,
+  SupportedTargetModel,
+} from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 
 import { GlobalNotificationSettingEvent } from '../models/GlobalNotificationSetting';
@@ -14,7 +18,6 @@ import { preNotifyService } from '../service/pre-notify';
  *    name: Comments
  */
 
-
 /**
  * @swagger
  *
@@ -55,7 +58,7 @@ import { preNotifyService } from '../service/pre-notify';
  */
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
-module.exports = function(crowi, app) {
+module.exports = (crowi, app) => {
   const logger = loggerFactory('growi:routes:comment');
   const User = crowi.model('User');
   const Page = crowi.model('Page');
@@ -122,14 +125,16 @@ module.exports = function(crowi, app) {
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} revision_id Revision Id.
    */
-  api.get = async function(req, res) {
+  api.get = async (req, res) => {
     const pageId = req.query.page_id;
     const revisionId = req.query.revision_id;
 
     // check whether accessible
     const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user);
     if (!isAccessible) {
-      return res.json(ApiResponse.error('Current user is not accessible to this page.'));
+      return res.json(
+        ApiResponse.error('Current user is not accessible to this page.'),
+      );
     }
 
     let query = null;
@@ -137,12 +142,10 @@ module.exports = function(crowi, app) {
     try {
       if (revisionId) {
         query = Comment.findCommentsByRevisionId(revisionId);
-      }
-      else {
+      } else {
         query = Comment.findCommentsByPageId(pageId);
       }
-    }
-    catch (err) {
+    } catch (err) {
       return res.json(ApiResponse.error(err));
     }
 
@@ -156,19 +159,21 @@ module.exports = function(crowi, app) {
     res.json(ApiResponse.success({ comments }));
   };
 
-  api.validators.add = function() {
+  api.validators.add = () => {
     const validator = [
       body('commentForm.page_id').exists(),
       body('commentForm.revision_id').exists(),
       body('commentForm.comment').exists(),
       body('commentForm.comment_position').isInt(),
       body('commentForm.is_markdown').isBoolean(),
-      body('commentForm.replyTo').exists().custom((value) => {
-        if (value === '') {
-          return undefined;
-        }
-        return ObjectId(value);
-      }),
+      body('commentForm.replyTo')
+        .exists()
+        .custom((value) => {
+          if (value === '') {
+            return undefined;
+          }
+          return ObjectId(value);
+        }),
 
       body('slackNotificationForm.isSlackEnabled').isBoolean().exists(),
     ];
@@ -230,7 +235,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} comment Comment body
    * @apiParam {Number} comment_position=-1 Line number of the comment
    */
-  api.add = async function(req, res) {
+  api.add = async (req, res) => {
     const { commentForm, slackNotificationForm } = req.body;
     const { validationResult } = require('express-validator');
 
@@ -248,7 +253,9 @@ module.exports = function(crowi, app) {
     // check whether accessible
     const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user);
     if (!isAccessible) {
-      return res.json(ApiResponse.error('Current user is not accessible to this page.'));
+      return res.json(
+        ApiResponse.error('Current user is not accessible to this page.'),
+      );
     }
 
     if (comment === '') {
@@ -257,10 +264,16 @@ module.exports = function(crowi, app) {
 
     let createdComment;
     try {
-      createdComment = await Comment.add(pageId, req.user._id, revisionId, comment, position, replyTo);
+      createdComment = await Comment.add(
+        pageId,
+        req.user._id,
+        revisionId,
+        comment,
+        position,
+        replyTo,
+      );
       commentEvent.emit(CommentEvent.CREATE, createdComment);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       return res.json(ApiResponse.error(err));
     }
@@ -282,23 +295,36 @@ module.exports = function(crowi, app) {
     };
 
     /** @type {import('../service/pre-notify').GetAdditionalTargetUsers} */
-    const getAdditionalTargetUsers = async(activity) => {
-      const mentionedUsers = await crowi.commentService.getMentionedUsers(activity.event);
+    const getAdditionalTargetUsers = async (activity) => {
+      const mentionedUsers = await crowi.commentService.getMentionedUsers(
+        activity.event,
+      );
 
       return mentionedUsers;
     };
 
-    activityEvent.emit('update', res.locals.activity._id, parameters, page, preNotifyService.generatePreNotify, getAdditionalTargetUsers);
+    activityEvent.emit(
+      'update',
+      res.locals.activity._id,
+      parameters,
+      page,
+      preNotifyService.generatePreNotify,
+      getAdditionalTargetUsers,
+    );
 
     res.json(ApiResponse.success({ comment: createdComment }));
 
     // global notification
     try {
-      await globalNotificationService.fire(GlobalNotificationSettingEvent.COMMENT, page, req.user, {
-        comment: createdComment,
-      });
-    }
-    catch (err) {
+      await globalNotificationService.fire(
+        GlobalNotificationSettingEvent.COMMENT,
+        page,
+        req.user,
+        {
+          comment: createdComment,
+        },
+      );
+    } catch (err) {
       logger.error('Comment notification failed', err);
     }
 
@@ -307,14 +333,20 @@ module.exports = function(crowi, app) {
       const { slackChannels } = slackNotificationForm;
 
       try {
-        const results = await userNotificationService.fire(page, req.user, slackChannels, 'comment', {}, createdComment);
+        const results = await userNotificationService.fire(
+          page,
+          req.user,
+          slackChannels,
+          'comment',
+          {},
+          createdComment,
+        );
         results.forEach((result) => {
           if (result.status === 'rejected') {
             logger.error('Create user notification failed', result.reason);
           }
         });
-      }
-      catch (err) {
+      } catch (err) {
         logger.error('Create user notification failed', err);
       }
     }
@@ -373,7 +405,7 @@ module.exports = function(crowi, app) {
    * @apiName UpdateComment
    * @apiGroup Comment
    */
-  api.update = async function(req, res) {
+  api.update = async (req, res) => {
     const { commentForm } = req.body;
 
     const commentStr = commentForm?.comment;
@@ -385,7 +417,7 @@ module.exports = function(crowi, app) {
     }
 
     if (commentId == null) {
-      return res.json(ApiResponse.error('\'comment_id\' is undefined'));
+      return res.json(ApiResponse.error("'comment_id' is undefined"));
     }
 
     let updatedComment;
@@ -398,7 +430,10 @@ module.exports = function(crowi, app) {
 
       // check whether accessible
       const pageId = comment.page;
-      const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user);
+      const isAccessible = await Page.isAccessiblePageByViewer(
+        pageId,
+        req.user,
+      );
       if (!isAccessible) {
         throw new Error('Current user is not accessible to this page.');
       }
@@ -411,8 +446,7 @@ module.exports = function(crowi, app) {
         { $set: { comment: commentStr, revision } },
       );
       commentEvent.emit(CommentEvent.UPDATE, updatedComment);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       return res.json(ApiResponse.error(err));
     }
@@ -462,10 +496,12 @@ module.exports = function(crowi, app) {
    *
    * @apiParam {String} comment_id Comment Id.
    */
-  api.remove = async function(req, res) {
+  api.remove = async (req, res) => {
     const commentId = req.body.comment_id;
     if (!commentId) {
-      return Promise.resolve(res.json(ApiResponse.error('\'comment_id\' is undefined')));
+      return Promise.resolve(
+        res.json(ApiResponse.error("'comment_id' is undefined")),
+      );
     }
 
     try {
@@ -478,7 +514,10 @@ module.exports = function(crowi, app) {
 
       // check whether accessible
       const pageId = getIdStringForRef(comment.page);
-      const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user);
+      const isAccessible = await Page.isAccessiblePageByViewer(
+        pageId,
+        req.user,
+      );
       if (!isAccessible) {
         throw new Error('Current user is not accessible to this page.');
       }
@@ -489,8 +528,7 @@ module.exports = function(crowi, app) {
       await Comment.removeWithReplies(comment);
       await Page.updateCommentCount(comment.page);
       commentEvent.emit(CommentEvent.DELETE, comment);
-    }
-    catch (err) {
+    } catch (err) {
       return res.json(ApiResponse.error(err));
     }
 

+ 38 - 19
apps/app/src/server/routes/forgot-password.ts

@@ -1,6 +1,4 @@
-import type {
-  NextFunction, Request, Response,
-} from 'express';
+import type { NextFunction, Request, Response } from 'express';
 import createError from 'http-errors';
 
 import { forgotPasswordErrorCode } from '~/interfaces/errors/forgot-password';
@@ -10,37 +8,45 @@ import type { IPasswordResetOrder } from '../models/password-reset-order';
 
 const logger = loggerFactory('growi:routes:forgot-password');
 
-
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
-export const checkForgotPasswordEnabledMiddlewareFactory = (crowi: any, forApi = false) => {
-
+export const checkForgotPasswordEnabledMiddlewareFactory = (
+  crowi: any,
+  forApi = false,
+) => {
   return (req: Request, res: Response, next: NextFunction): void => {
-    const isPasswordResetEnabled = crowi.configManager.getConfig('security:passport-local:isPasswordResetEnabled');
-    const isLocalStrategySetup = crowi.passportService.isLocalStrategySetup as boolean ?? false;
+    const isPasswordResetEnabled = crowi.configManager.getConfig(
+      'security:passport-local:isPasswordResetEnabled',
+    );
+    const isLocalStrategySetup =
+      (crowi.passportService.isLocalStrategySetup as boolean) ?? false;
 
     const isEnabled = isLocalStrategySetup && isPasswordResetEnabled;
 
     if (!isEnabled) {
-      const message = 'Forgot-password function is unavailable because neither LocalStrategy and LdapStrategy is not setup.';
+      const message =
+        'Forgot-password function is unavailable because neither LocalStrategy and LdapStrategy is not setup.';
       logger.error(message);
 
       const statusCode = forApi ? 405 : 404;
-      return next(createError(statusCode, message, { code: forgotPasswordErrorCode.PASSWORD_RESET_IS_UNAVAILABLE }));
+      return next(
+        createError(statusCode, message, {
+          code: forgotPasswordErrorCode.PASSWORD_RESET_IS_UNAVAILABLE,
+        }),
+      );
     }
 
     next();
   };
-
 };
 
 type Crowi = {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  nextApp: any,
-}
+  nextApp: any;
+};
 
 type CrowiReq = Request & {
-  crowi: Crowi,
-}
+  crowi: Crowi;
+};
 
 export const renderForgotPassword = (crowi: Crowi) => {
   return (req: CrowiReq, res: Response, next: NextFunction): void => {
@@ -52,24 +58,37 @@ export const renderForgotPassword = (crowi: Crowi) => {
 };
 
 export const renderResetPassword = (crowi: Crowi) => {
-  return (req: CrowiReq & { passwordResetOrder: IPasswordResetOrder }, res: Response, next: NextFunction): void => {
+  return (
+    req: CrowiReq & { passwordResetOrder: IPasswordResetOrder },
+    res: Response,
+    next: NextFunction,
+  ): void => {
     const { nextApp } = crowi;
     req.crowi = crowi;
-    nextApp.render(req, res, '/reset-password', { email: req.passwordResetOrder.email });
+    nextApp.render(req, res, '/reset-password', {
+      email: req.passwordResetOrder.email,
+    });
     return;
   };
 };
 
 // middleware to handle error
 export const handleErrorsMiddleware = (crowi: Crowi) => {
-  return (error: Error & { code: string, statusCode: number }, req: CrowiReq, res: Response, next: NextFunction): void => {
+  return (
+    error: Error & { code: string; statusCode: number },
+    req: CrowiReq,
+    res: Response,
+    next: NextFunction,
+  ): void => {
     if (error != null) {
       const { nextApp } = crowi;
 
       req.crowi = crowi;
       res.status(error.statusCode);
 
-      nextApp.render(req, res, '/forgot-password-errors', { errorCode: error.code });
+      nextApp.render(req, res, '/forgot-password-errors', {
+        errorCode: error.code,
+      });
       return;
     }
 

+ 342 - 75
apps/app/src/server/routes/index.js

@@ -7,30 +7,35 @@ import { accessTokenParser } from '../middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '../middlewares/add-activity';
 import apiV1FormValidator from '../middlewares/apiv1-form-validator';
 import * as applicationNotInstalled from '../middlewares/application-not-installed';
-import { excludeReadOnlyUser, excludeReadOnlyUserIfCommentNotAllowed } from '../middlewares/exclude-read-only-user';
+import {
+  excludeReadOnlyUser,
+  excludeReadOnlyUserIfCommentNotAllowed,
+} from '../middlewares/exclude-read-only-user';
 import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
 import injectUserRegistrationOrderByTokenMiddleware from '../middlewares/inject-user-registration-order-by-token-middleware';
 import * as loginFormValidator from '../middlewares/login-form-validator';
 import {
-  generateUnavailableWhenMaintenanceModeMiddleware, generateUnavailableWhenMaintenanceModeMiddlewareForApi,
+  generateUnavailableWhenMaintenanceModeMiddleware,
+  generateUnavailableWhenMaintenanceModeMiddlewareForApi,
 } from '../middlewares/unavailable-when-maintenance-mode';
-
 import * as attachment from './attachment';
 import { routesFactory as attachmentApiRoutesFactory } from './attachment/api';
 import * as forgotPassword from './forgot-password';
 import nextFactory from './next';
 import * as userActivation from './user-activation';
 
-
 const multer = require('multer');
 const autoReap = require('multer-autoreap');
 
 autoReap.options.reapOnError = true; // continue reaping the file even if an error occurs
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
-module.exports = function(crowi, app) {
-  const autoReconnectToSearch = require('../middlewares/auto-reconnect-to-search')(crowi);
-  const applicationInstalled = require('../middlewares/application-installed')(crowi);
+module.exports = (crowi, app) => {
+  const autoReconnectToSearch =
+    require('../middlewares/auto-reconnect-to-search')(crowi);
+  const applicationInstalled = require('../middlewares/application-installed')(
+    crowi,
+  );
   const loginRequiredStrictly = require('../middlewares/login-required')(crowi);
   const loginRequired = require('../middlewares/login-required')(crowi, true);
   const adminRequired = require('../middlewares/admin-required')(crowi);
@@ -50,13 +55,17 @@ module.exports = function(crowi, app) {
 
   const next = nextFactory(crowi);
 
-  const unavailableWhenMaintenanceMode = generateUnavailableWhenMaintenanceModeMiddleware(crowi);
-  const unavailableWhenMaintenanceModeForApi = generateUnavailableWhenMaintenanceModeMiddlewareForApi(crowi);
-
+  const unavailableWhenMaintenanceMode =
+    generateUnavailableWhenMaintenanceModeMiddleware(crowi);
+  const unavailableWhenMaintenanceModeForApi =
+    generateUnavailableWhenMaintenanceModeMiddlewareForApi(crowi);
 
   /* eslint-disable max-len, comma-spacing, no-multi-spaces */
 
-  const [apiV3Router, apiV3AdminRouter, apiV3AuthRouter] = require('./apiv3')(crowi, app);
+  const [apiV3Router, apiV3AdminRouter, apiV3AuthRouter] = require('./apiv3')(
+    crowi,
+    app,
+  );
 
   // Rate limiter
   app.use(rateLimiterFactory());
@@ -67,47 +76,163 @@ module.exports = function(crowi, app) {
   // API v3 for auth
   app.use('/_api/v3', apiV3AuthRouter);
 
-  app.get('/_next/*'                  , next.delegateToNext);
+  app.get('/_next/*', next.delegateToNext);
 
-  app.get('/'                         ,  applicationInstalled, unavailableWhenMaintenanceMode, loginRequired, autoReconnectToSearch, next.delegateToNext);
+  app.get(
+    '/',
+    applicationInstalled,
+    unavailableWhenMaintenanceMode,
+    loginRequired,
+    autoReconnectToSearch,
+    next.delegateToNext,
+  );
 
-  app.get('/login/error/:reason'      , applicationInstalled, next.delegateToNext);
-  app.get('/login'                    , applicationInstalled, login.preLogin, next.delegateToNext);
-  app.get('/invited'                  , applicationInstalled, next.delegateToNext);
+  app.get('/login/error/:reason', applicationInstalled, next.delegateToNext);
+  app.get('/login', applicationInstalled, login.preLogin, next.delegateToNext);
+  app.get('/invited', applicationInstalled, next.delegateToNext);
   // app.post('/login'                   , applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation, csrfProtection,  addActivity, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
 
   // NOTE: get method "/admin/export/:fileName" should be loaded before "/admin/*"
-  app.get('/admin/export/:fileName'   , accessTokenParser([SCOPE.READ.ADMIN.EXPORT_DATA]), loginRequiredStrictly , adminRequired ,admin.export.api.validators.export.download(), admin.export.download);
+  app.get(
+    '/admin/export/:fileName',
+    accessTokenParser([SCOPE.READ.ADMIN.EXPORT_DATA]),
+    loginRequiredStrictly,
+    adminRequired,
+    admin.export.api.validators.export.download(),
+    admin.export.download,
+  );
 
   // TODO: If you want to use accessTokenParser, you need to add scope ANY e.g. accessTokenParser([SCOPE.READ.ADMIN.ANY])
-  app.get('/admin/*'                  , applicationInstalled, loginRequiredStrictly , adminRequired , next.delegateToNext);
-  app.get('/admin'                    , applicationInstalled, loginRequiredStrictly , adminRequired , next.delegateToNext);
+  app.get(
+    '/admin/*',
+    applicationInstalled,
+    loginRequiredStrictly,
+    adminRequired,
+    next.delegateToNext,
+  );
+  app.get(
+    '/admin',
+    applicationInstalled,
+    loginRequiredStrictly,
+    adminRequired,
+    next.delegateToNext,
+  );
 
   // installer
-  app.get('/installer',
+  app.get(
+    '/installer',
     applicationNotInstalled.generateCheckerMiddleware(crowi),
     next.delegateToNext,
-    applicationNotInstalled.redirectToTopOnError);
+    applicationNotInstalled.redirectToTopOnError,
+  );
 
   // OAuth
-  app.get('/passport/google'                      , loginPassport.loginWithGoogle, loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/github'                      , loginPassport.loginWithGitHub, loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/oidc'                        , loginPassport.loginWithOidc,   loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/saml'                        , loginPassport.loginWithSaml,   loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/google/callback'             , loginPassport.injectRedirectTo, loginPassport.loginPassportGoogleCallback   , loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/github/callback'             , loginPassport.injectRedirectTo, loginPassport.loginPassportGitHubCallback   , loginPassport.loginFailureForExternalAccount);
-  app.get('/passport/oidc/callback'               , loginPassport.injectRedirectTo, loginPassport.loginPassportOidcCallback     , loginPassport.loginFailureForExternalAccount);
-  app.post('/passport/saml/callback'              , addActivity, loginPassport.injectRedirectTo, loginPassport.loginPassportSamlCallback, loginPassport.loginFailureForExternalAccount);
-
-  app.post('/_api/login/testLdap'    ,  accessTokenParser([SCOPE.WRITE.USER_SETTINGS.EXTERNAL_ACCOUNT]), loginRequiredStrictly , loginFormValidator.loginRules() , loginFormValidator.loginValidation , loginPassport.testLdapCredentials);
+  app.get(
+    '/passport/google',
+    loginPassport.loginWithGoogle,
+    loginPassport.loginFailureForExternalAccount,
+  );
+  app.get(
+    '/passport/github',
+    loginPassport.loginWithGitHub,
+    loginPassport.loginFailureForExternalAccount,
+  );
+  app.get(
+    '/passport/oidc',
+    loginPassport.loginWithOidc,
+    loginPassport.loginFailureForExternalAccount,
+  );
+  app.get(
+    '/passport/saml',
+    loginPassport.loginWithSaml,
+    loginPassport.loginFailureForExternalAccount,
+  );
+  app.get(
+    '/passport/google/callback',
+    loginPassport.injectRedirectTo,
+    loginPassport.loginPassportGoogleCallback,
+    loginPassport.loginFailureForExternalAccount,
+  );
+  app.get(
+    '/passport/github/callback',
+    loginPassport.injectRedirectTo,
+    loginPassport.loginPassportGitHubCallback,
+    loginPassport.loginFailureForExternalAccount,
+  );
+  app.get(
+    '/passport/oidc/callback',
+    loginPassport.injectRedirectTo,
+    loginPassport.loginPassportOidcCallback,
+    loginPassport.loginFailureForExternalAccount,
+  );
+  app.post(
+    '/passport/saml/callback',
+    addActivity,
+    loginPassport.injectRedirectTo,
+    loginPassport.loginPassportSamlCallback,
+    loginPassport.loginFailureForExternalAccount,
+  );
+
+  app.post(
+    '/_api/login/testLdap',
+    accessTokenParser([SCOPE.WRITE.USER_SETTINGS.EXTERNAL_ACCOUNT]),
+    loginRequiredStrictly,
+    loginFormValidator.loginRules(),
+    loginFormValidator.loginValidation,
+    loginPassport.testLdapCredentials,
+  );
 
   // importer management for admin
-  app.post('/_api/admin/settings/importerEsa'   , accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]), loginRequiredStrictly , adminRequired , addActivity, admin.importer.api.validators.importer.esa(),admin.api.importerSettingEsa);
-  app.post('/_api/admin/settings/importerQiita' , accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]), loginRequiredStrictly , adminRequired , addActivity, admin.importer.api.validators.importer.qiita(), admin.api.importerSettingQiita);
-  app.post('/_api/admin/import/esa'             , accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]), loginRequiredStrictly , adminRequired , addActivity, admin.api.importDataFromEsa);
-  app.post('/_api/admin/import/testEsaAPI'      , accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]), loginRequiredStrictly , adminRequired , addActivity, admin.api.testEsaAPI);
-  app.post('/_api/admin/import/qiita'           , accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]), loginRequiredStrictly , adminRequired , addActivity, admin.api.importDataFromQiita);
-  app.post('/_api/admin/import/testQiitaAPI'    , accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]), loginRequiredStrictly , adminRequired , addActivity, admin.api.testQiitaAPI);
+  app.post(
+    '/_api/admin/settings/importerEsa',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]),
+    loginRequiredStrictly,
+    adminRequired,
+    addActivity,
+    admin.importer.api.validators.importer.esa(),
+    admin.api.importerSettingEsa,
+  );
+  app.post(
+    '/_api/admin/settings/importerQiita',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]),
+    loginRequiredStrictly,
+    adminRequired,
+    addActivity,
+    admin.importer.api.validators.importer.qiita(),
+    admin.api.importerSettingQiita,
+  );
+  app.post(
+    '/_api/admin/import/esa',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]),
+    loginRequiredStrictly,
+    adminRequired,
+    addActivity,
+    admin.api.importDataFromEsa,
+  );
+  app.post(
+    '/_api/admin/import/testEsaAPI',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]),
+    loginRequiredStrictly,
+    adminRequired,
+    addActivity,
+    admin.api.testEsaAPI,
+  );
+  app.post(
+    '/_api/admin/import/qiita',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]),
+    loginRequiredStrictly,
+    adminRequired,
+    addActivity,
+    admin.api.importDataFromQiita,
+  );
+  app.post(
+    '/_api/admin/import/testQiitaAPI',
+    accessTokenParser([SCOPE.WRITE.ADMIN.IMPORT_DATA]),
+    loginRequiredStrictly,
+    adminRequired,
+    addActivity,
+    admin.api.testQiitaAPI,
+  );
 
   // brand logo
   app.use('/attachment', attachment.getBrandLogoRouterFactory(crowi));
@@ -121,58 +246,200 @@ module.exports = function(crowi, app) {
 
   const apiV1Router = createApiRouter();
 
-  apiV1Router.get('/search'              , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }) , loginRequired , search.api.search);
+  apiV1Router.get(
+    '/search',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired,
+    search.api.search,
+  );
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
-  apiV1Router.get('/pages.updatePost'    , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, page.api.getUpdatePost);
-  apiV1Router.get('/pages.getPageTag'    , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }) , loginRequired , page.api.getPageTag);
+  apiV1Router.get(
+    '/pages.updatePost',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired,
+    page.api.getUpdatePost,
+  );
+  apiV1Router.get(
+    '/pages.getPageTag',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired,
+    page.api.getPageTag,
+  );
   // allow posting to guests because the client doesn't know whether the user logged in
-  apiV1Router.post('/pages.remove'       , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly , excludeReadOnlyUser, page.validator.remove, apiV1FormValidator, page.api.remove); // (Avoid from API Token)
-  apiV1Router.post('/pages.revertRemove' , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly , excludeReadOnlyUser, page.validator.revertRemove, apiV1FormValidator, page.api.revertRemove); // (Avoid from API Token)
-  apiV1Router.post('/pages.unlink'       , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]), loginRequiredStrictly , excludeReadOnlyUser, page.api.unlink); // (Avoid from API Token)
-  apiV1Router.get('/tags.list'           , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, tag.api.list);
-  apiV1Router.get('/tags.search'         , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequired, tag.api.search);
-  apiV1Router.post('/tags.update'        , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly, excludeReadOnlyUser, addActivity, tag.api.update);
-  apiV1Router.get('/comments.get'        , accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }) , loginRequired , comment.api.get);
-  apiV1Router.post('/comments.add'       , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), comment.api.validators.add(), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.add);
-  apiV1Router.post('/comments.update'    , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), comment.api.validators.add(), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.update);
-  apiV1Router.post('/comments.remove'    , accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.remove);
-
-  apiV1Router.post('/attachments.uploadProfileImage'   , accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], { acceptLegacy: true }), loginRequiredStrictly , uploads.single('file'), autoReap, attachmentApi.uploadProfileImage);
-  apiV1Router.post('/attachments.remove'               , accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], { acceptLegacy: true }), loginRequiredStrictly , excludeReadOnlyUser, addActivity ,attachmentApi.remove);
-  apiV1Router.post('/attachments.removeProfileImage'   , accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], { acceptLegacy: true }), loginRequiredStrictly , attachmentApi.removeProfileImage);
+  apiV1Router.post(
+    '/pages.remove',
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    loginRequiredStrictly,
+    excludeReadOnlyUser,
+    page.validator.remove,
+    apiV1FormValidator,
+    page.api.remove,
+  ); // (Avoid from API Token)
+  apiV1Router.post(
+    '/pages.revertRemove',
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    loginRequiredStrictly,
+    excludeReadOnlyUser,
+    page.validator.revertRemove,
+    apiV1FormValidator,
+    page.api.revertRemove,
+  ); // (Avoid from API Token)
+  apiV1Router.post(
+    '/pages.unlink',
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE]),
+    loginRequiredStrictly,
+    excludeReadOnlyUser,
+    page.api.unlink,
+  ); // (Avoid from API Token)
+  apiV1Router.get(
+    '/tags.list',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired,
+    tag.api.list,
+  );
+  apiV1Router.get(
+    '/tags.search',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired,
+    tag.api.search,
+  );
+  apiV1Router.post(
+    '/tags.update',
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequiredStrictly,
+    excludeReadOnlyUser,
+    addActivity,
+    tag.api.update,
+  );
+  apiV1Router.get(
+    '/comments.get',
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequired,
+    comment.api.get,
+  );
+  apiV1Router.post(
+    '/comments.add',
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
+    comment.api.validators.add(),
+    loginRequiredStrictly,
+    excludeReadOnlyUserIfCommentNotAllowed,
+    addActivity,
+    comment.api.add,
+  );
+  apiV1Router.post(
+    '/comments.update',
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
+    comment.api.validators.add(),
+    loginRequiredStrictly,
+    excludeReadOnlyUserIfCommentNotAllowed,
+    addActivity,
+    comment.api.update,
+  );
+  apiV1Router.post(
+    '/comments.remove',
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
+    loginRequiredStrictly,
+    excludeReadOnlyUserIfCommentNotAllowed,
+    addActivity,
+    comment.api.remove,
+  );
+
+  apiV1Router.post(
+    '/attachments.uploadProfileImage',
+    accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], {
+      acceptLegacy: true,
+    }),
+    loginRequiredStrictly,
+    uploads.single('file'),
+    autoReap,
+    attachmentApi.uploadProfileImage,
+  );
+  apiV1Router.post(
+    '/attachments.remove',
+    accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], {
+      acceptLegacy: true,
+    }),
+    loginRequiredStrictly,
+    excludeReadOnlyUser,
+    addActivity,
+    attachmentApi.remove,
+  );
+  apiV1Router.post(
+    '/attachments.removeProfileImage',
+    accessTokenParser([SCOPE.WRITE.FEATURES.ATTACHMENT], {
+      acceptLegacy: true,
+    }),
+    loginRequiredStrictly,
+    attachmentApi.removeProfileImage,
+  );
 
   // API v1
   app.use('/_api', unavailableWhenMaintenanceModeForApi, apiV1Router);
 
   app.use(unavailableWhenMaintenanceMode);
 
-  app.get('/me'                                   , loginRequiredStrictly, next.delegateToNext);
-  app.get('/me/*'                                 , loginRequiredStrictly, next.delegateToNext);
-
-  app.use('/attachment', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), attachment.getRouterFactory(crowi));
-  app.use('/download', accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), attachment.downloadRouterFactory(crowi));
-
-  app.get('/_search'                              , loginRequired, next.delegateToNext);
-
-  app.use('/forgot-password', express.Router()
-    .use(forgotPassword.checkForgotPasswordEnabledMiddlewareFactory(crowi))
-    .get('/', forgotPassword.renderForgotPassword(crowi))
-    .get('/:token', injectResetOrderByTokenMiddleware, forgotPassword.renderResetPassword(crowi))
-    .use(forgotPassword.handleErrorsMiddleware(crowi)));
+  app.get('/me', loginRequiredStrictly, next.delegateToNext);
+  app.get('/me/*', loginRequiredStrictly, next.delegateToNext);
+
+  app.use(
+    '/attachment',
+    accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]),
+    attachment.getRouterFactory(crowi),
+  );
+  app.use(
+    '/download',
+    accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]),
+    attachment.downloadRouterFactory(crowi),
+  );
+
+  app.get('/_search', loginRequired, next.delegateToNext);
+
+  app.use(
+    '/forgot-password',
+    express
+      .Router()
+      .use(forgotPassword.checkForgotPasswordEnabledMiddlewareFactory(crowi))
+      .get('/', forgotPassword.renderForgotPassword(crowi))
+      .get(
+        '/:token',
+        injectResetOrderByTokenMiddleware,
+        forgotPassword.renderResetPassword(crowi),
+      )
+      .use(forgotPassword.handleErrorsMiddleware(crowi)),
+  );
 
   app.get('/_private-legacy-pages', next.delegateToNext);
 
-  app.use('/user-activation', express.Router()
-    .get('/:token', applicationInstalled, injectUserRegistrationOrderByTokenMiddleware, userActivation.renderUserActivationPage(crowi))
-    .use(userActivation.tokenErrorHandlerMiddeware(crowi)));
+  app.use(
+    '/user-activation',
+    express
+      .Router()
+      .get(
+        '/:token',
+        applicationInstalled,
+        injectUserRegistrationOrderByTokenMiddleware,
+        userActivation.renderUserActivationPage(crowi),
+      )
+      .use(userActivation.tokenErrorHandlerMiddeware(crowi)),
+  );
 
   app.get('/share$', (req, res) => res.redirect('/'));
   app.get('/share/:linkId', next.delegateToNext);
 
-  app.use('/ogp', express.Router().get('/:pageId([0-9a-z]{0,})', loginRequired, ogp.pageIdRequired, ogp.ogpValidator, ogp.renderOgp));
-
-  app.get('/*/$'                   , loginRequired, next.delegateToNext);
-  app.get('/*'                     , loginRequired, autoReconnectToSearch, next.delegateToNext);
-
+  app.use(
+    '/ogp',
+    express
+      .Router()
+      .get(
+        '/:pageId([0-9a-z]{0,})',
+        loginRequired,
+        ogp.pageIdRequired,
+        ogp.ogpValidator,
+        ogp.renderOgp,
+      ),
+  );
+
+  app.get('/*/$', loginRequired, next.delegateToNext);
+  app.get('/*', loginRequired, autoReconnectToSearch, next.delegateToNext);
 };

+ 256 - 106
apps/app/src/server/routes/login-passport.js

@@ -11,7 +11,7 @@ import { externalAccountService } from '../service/external-account';
 /* eslint-disable no-use-before-define */
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
-module.exports = function(crowi, app) {
+module.exports = (crowi, app) => {
   const logger = loggerFactory('growi:routes:login-passport');
   const passport = require('passport');
   const passportService = crowi.passportService;
@@ -23,14 +23,18 @@ module.exports = function(crowi, app) {
   const promisifiedPassportAuthentication = (strategyName, req, res) => {
     return new Promise((resolve, reject) => {
       passport.authenticate(strategyName, (err, response, info) => {
-        if (res.headersSent) { // dirty hack -- 2017.09.25
+        if (res.headersSent) {
+          // dirty hack -- 2017.09.25
           return; //              cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
         }
 
         logger.debug(`--- authenticate with ${strategyName} strategy ---`);
 
         if (err) {
-          logger.error(`'${strategyName}' passport authentication error: `, err);
+          logger.error(
+            `'${strategyName}' passport authentication error: `,
+            err,
+          );
           reject(err);
         }
 
@@ -52,8 +56,13 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  const loginSuccessHandler = async(req, res, user, action, isExternalAccount = false) => {
-
+  const loginSuccessHandler = async (
+    req,
+    res,
+    user,
+    action,
+    isExternalAccount = false,
+  ) => {
     // update lastLoginAt
     user.updateLastLoginAt(new Date(), (err, userData) => {
       if (err) {
@@ -63,7 +72,7 @@ module.exports = function(crowi, app) {
     });
 
     const parameters = {
-      ip:  req.ip,
+      ip: req.ip,
       endpoint: req.originalUrl,
       action,
       user: req.user?._id,
@@ -74,8 +83,11 @@ module.exports = function(crowi, app) {
 
     await crowi.activityService.createActivity(parameters);
 
-    const redirectToForUnauthenticated = createRedirectToForUnauthenticated(req.user.status);
-    const redirectTo = redirectToForUnauthenticated ?? res.locals.redirectTo ?? '/';
+    const redirectToForUnauthenticated = createRedirectToForUnauthenticated(
+      req.user.status,
+    );
+    const redirectTo =
+      redirectToForUnauthenticated ?? res.locals.redirectTo ?? '/';
 
     if (isExternalAccount) {
       return res.safeRedirect(redirectTo);
@@ -85,7 +97,6 @@ module.exports = function(crowi, app) {
   };
 
   const injectRedirectTo = (req, res, next) => {
-
     // Move "req.session.redirectTo" to "res.locals.redirectTo"
     // Because the session is regenerated when req.login() is called
     const redirectTo = req.session.redirectTo;
@@ -97,9 +108,17 @@ module.exports = function(crowi, app) {
   };
 
   const isEnableLoginWithLocalOrLdap = (req, res, next) => {
-    if (!passportService.isLocalStrategySetup && !passportService.isLdapStrategySetup) {
+    if (
+      !passportService.isLocalStrategySetup &&
+      !passportService.isLdapStrategySetup
+    ) {
       logger.error('LocalStrategy and LdapStrategy has not been set up');
-      const error = new ErrorV3('message.strategy_has_not_been_set_up', '', undefined, { strategy: 'LocalStrategy and LdapStrategy' });
+      const error = new ErrorV3(
+        'message.strategy_has_not_been_set_up',
+        '',
+        undefined,
+        { strategy: 'LocalStrategy and LdapStrategy' },
+      );
       return next(error);
     }
 
@@ -120,15 +139,14 @@ module.exports = function(crowi, app) {
    * @param {*} next
    */
   const loginFailure = (error, req, res, next) => {
-
     const parameters = { action: SupportedAction.ACTION_USER_LOGIN_FAILURE };
     activityEvent.emit('update', res.locals.activity._id, parameters);
     return res.apiv3Err(error);
   };
 
-  const loginFailureForExternalAccount = async(error, req, res, next) => {
+  const loginFailureForExternalAccount = async (error, req, res, next) => {
     const parameters = {
-      ip:  req.ip,
+      ip: req.ip,
       endpoint: req.originalUrl,
       action: SupportedAction.ACTION_USER_LOGIN_FAILURE,
     };
@@ -162,7 +180,7 @@ module.exports = function(crowi, app) {
    * @param {*} res
    * @param {*} next
    */
-  const loginWithLdap = async(req, res, next) => {
+  const loginWithLdap = async (req, res, next) => {
     if (!passportService.isLdapStrategySetup) {
       logger.debug('LdapStrategy has not been set up');
       return next();
@@ -177,9 +195,12 @@ module.exports = function(crowi, app) {
     let ldapAccountInfo;
 
     try {
-      ldapAccountInfo = await promisifiedPassportAuthentication(strategyName, req, res);
-    }
-    catch (err) {
+      ldapAccountInfo = await promisifiedPassportAuthentication(
+        strategyName,
+        req,
+        res,
+      );
+    } catch (err) {
       logger.debug(err.message);
       return next(err);
     }
@@ -190,8 +211,8 @@ module.exports = function(crowi, app) {
     }
 
     /*
-      * authentication success
-      */
+     * authentication success
+     */
     // it is guaranteed that username that is input from form can be acquired
     // because this processes after authentication
     const ldapAccountId = passportService.getLdapAccountIdFromReq(req);
@@ -211,9 +232,11 @@ module.exports = function(crowi, app) {
 
     let externalAccount;
     try {
-      externalAccount = await externalAccountService.getOrCreateUser(userInfo, providerId);
-    }
-    catch (error) {
+      externalAccount = await externalAccountService.getOrCreateUser(
+        userInfo,
+        providerId,
+      );
+    } catch (error) {
       return next(error);
     }
 
@@ -231,7 +254,13 @@ module.exports = function(crowi, app) {
         return next(err);
       }
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LDAP, true);
+      return loginSuccessHandler(
+        req,
+        res,
+        user,
+        SupportedAction.ACTION_USER_LOGIN_WITH_LDAP,
+        true,
+      );
     });
   };
 
@@ -241,54 +270,69 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} res
    */
-  const testLdapCredentials = async(req, res) => {
+  const testLdapCredentials = async (req, res) => {
     const { t } = await getTranslation({ lang: req.user.lang });
 
     if (!passportService.isLdapStrategySetup) {
       logger.debug('LdapStrategy has not been set up');
-      return res.json(ApiResponse.success({
-        status: 'warning',
-        message: t('message.strategy_has_not_been_set_up', { strategy: 'LdapStrategy' }),
-      }));
+      return res.json(
+        ApiResponse.success({
+          status: 'warning',
+          message: t('message.strategy_has_not_been_set_up', {
+            strategy: 'LdapStrategy',
+          }),
+        }),
+      );
     }
 
     passport.authenticate('ldapauth', (err, user, info) => {
-      if (res.headersSent) { // dirty hack -- 2017.09.25
+      if (res.headersSent) {
+        // dirty hack -- 2017.09.25
         return; //              cz: somehow passport.authenticate called twice when ECONNREFUSED error occurred
       }
 
-      if (err) { // DB Error
+      if (err) {
+        // DB Error
         logger.error('LDAP Server Error: ', err);
-        return res.json(ApiResponse.success({
-          status: 'warning',
-          message: 'LDAP Server Error occured.',
-          err,
-        }));
+        return res.json(
+          ApiResponse.success({
+            status: 'warning',
+            message: 'LDAP Server Error occured.',
+            err,
+          }),
+        );
       }
       if (info && info.message) {
-        return res.json(ApiResponse.success({
-          status: 'warning',
-          message: info.message,
-          ldapConfiguration: req.ldapConfiguration,
-          ldapAccountInfo: req.ldapAccountInfo,
-        }));
+        return res.json(
+          ApiResponse.success({
+            status: 'warning',
+            message: info.message,
+            ldapConfiguration: req.ldapConfiguration,
+            ldapAccountInfo: req.ldapAccountInfo,
+          }),
+        );
       }
       if (user) {
         // check groups
         if (!isValidLdapUserByGroupFilter(user)) {
-          return res.json(ApiResponse.success({
-            status: 'warning',
-            message: 'This user does not belong to any groups designated by the group search filter.',
+          return res.json(
+            ApiResponse.success({
+              status: 'warning',
+              message:
+                'This user does not belong to any groups designated by the group search filter.',
+              ldapConfiguration: req.ldapConfiguration,
+              ldapAccountInfo: req.ldapAccountInfo,
+            }),
+          );
+        }
+        return res.json(
+          ApiResponse.success({
+            status: 'success',
+            message: 'Successfully authenticated.',
             ldapConfiguration: req.ldapConfiguration,
             ldapAccountInfo: req.ldapAccountInfo,
-          }));
-        }
-        return res.json(ApiResponse.success({
-          status: 'success',
-          message: 'Successfully authenticated.',
-          ldapConfiguration: req.ldapConfiguration,
-          ldapAccountInfo: req.ldapAccountInfo,
-        }));
+          }),
+        );
       }
     })(req, res, () => {});
   };
@@ -314,7 +358,8 @@ module.exports = function(crowi, app) {
       logger.debug('user', user);
       logger.debug('info', info);
 
-      if (err) { // DB Error
+      if (err) {
+        // DB Error
         logger.error('Database Server Error: ', err);
         return next(err);
       }
@@ -327,15 +372,23 @@ module.exports = function(crowi, app) {
           return next(err);
         }
 
-        return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LOCAL);
+        return loginSuccessHandler(
+          req,
+          res,
+          user,
+          SupportedAction.ACTION_USER_LOGIN_WITH_LOCAL,
+        );
       });
     })(req, res, next);
   };
 
-  const loginWithGoogle = function(req, res, next) {
+  const loginWithGoogle = (req, res, next) => {
     if (!passportService.isGoogleStrategySetup) {
       logger.debug('GoogleStrategy has not been set up');
-      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'GoogleStrategy' });
+      const error = new ExternalAccountLoginError(
+        'message.strategy_has_not_been_set_up',
+        { strategy: 'GoogleStrategy' },
+      );
       return next(error);
     }
 
@@ -344,7 +397,7 @@ module.exports = function(crowi, app) {
     })(req, res);
   };
 
-  const loginPassportGoogleCallback = async(req, res, next) => {
+  const loginPassportGoogleCallback = async (req, res, next) => {
     const globalLang = crowi.configManager.getConfig('app:globalLang');
 
     const providerId = 'google';
@@ -352,9 +405,12 @@ module.exports = function(crowi, app) {
 
     let response;
     try {
-      response = await promisifiedPassportAuthentication(strategyName, req, res);
-    }
-    catch (err) {
+      response = await promisifiedPassportAuthentication(
+        strategyName,
+        req,
+        res,
+      );
+    } catch (err) {
       return next(new ExternalAccountLoginError(err.message));
     }
 
@@ -387,7 +443,10 @@ module.exports = function(crowi, app) {
       userInfo.username = userInfo.email.slice(0, userInfo.email.indexOf('@'));
     }
 
-    const externalAccount = await externalAccountService.getOrCreateUser(userInfo, providerId);
+    const externalAccount = await externalAccountService.getOrCreateUser(
+      userInfo,
+      providerId,
+    );
     if (!externalAccount) {
       return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
@@ -395,32 +454,47 @@ module.exports = function(crowi, app) {
     const user = (await externalAccount.populate('user')).user;
 
     // login
-    req.logIn(user, async(err) => {
-      if (err) { logger.debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
+    req.logIn(user, async (err) => {
+      if (err) {
+        logger.debug(err.message);
+        return next(new ExternalAccountLoginError(err.message));
+      }
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GOOGLE, true);
+      return loginSuccessHandler(
+        req,
+        res,
+        user,
+        SupportedAction.ACTION_USER_LOGIN_WITH_GOOGLE,
+        true,
+      );
     });
   };
 
-  const loginWithGitHub = function(req, res, next) {
+  const loginWithGitHub = (req, res, next) => {
     if (!passportService.isGitHubStrategySetup) {
       logger.debug('GitHubStrategy has not been set up');
-      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'GitHubStrategy' });
+      const error = new ExternalAccountLoginError(
+        'message.strategy_has_not_been_set_up',
+        { strategy: 'GitHubStrategy' },
+      );
       return next(error);
     }
 
     passport.authenticate('github')(req, res);
   };
 
-  const loginPassportGitHubCallback = async(req, res, next) => {
+  const loginPassportGitHubCallback = async (req, res, next) => {
     const providerId = 'github';
     const strategyName = 'github';
 
     let response;
     try {
-      response = await promisifiedPassportAuthentication(strategyName, req, res);
-    }
-    catch (err) {
+      response = await promisifiedPassportAuthentication(
+        strategyName,
+        req,
+        res,
+      );
+    } catch (err) {
       return next(new ExternalAccountLoginError(err.message));
     }
 
@@ -430,7 +504,10 @@ module.exports = function(crowi, app) {
       name: response.displayName,
     };
 
-    const externalAccount = await externalAccountService.getOrCreateUser(userInfo, providerId);
+    const externalAccount = await externalAccountService.getOrCreateUser(
+      userInfo,
+      providerId,
+    );
     if (!externalAccount) {
       return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
@@ -438,36 +515,59 @@ module.exports = function(crowi, app) {
     const user = (await externalAccount.populate('user')).user;
 
     // login
-    req.logIn(user, async(err) => {
-      if (err) { logger.debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
+    req.logIn(user, async (err) => {
+      if (err) {
+        logger.debug(err.message);
+        return next(new ExternalAccountLoginError(err.message));
+      }
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GITHUB, true);
+      return loginSuccessHandler(
+        req,
+        res,
+        user,
+        SupportedAction.ACTION_USER_LOGIN_WITH_GITHUB,
+        true,
+      );
     });
   };
 
-  const loginWithOidc = function(req, res, next) {
+  const loginWithOidc = (req, res, next) => {
     if (!passportService.isOidcStrategySetup) {
       logger.debug('OidcStrategy has not been set up');
-      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'OidcStrategy' });
+      const error = new ExternalAccountLoginError(
+        'message.strategy_has_not_been_set_up',
+        { strategy: 'OidcStrategy' },
+      );
       return next(error);
     }
 
     passport.authenticate('oidc')(req, res);
   };
 
-  const loginPassportOidcCallback = async(req, res, next) => {
+  const loginPassportOidcCallback = async (req, res, next) => {
     const providerId = 'oidc';
     const strategyName = 'oidc';
-    const attrMapId = crowi.configManager.getConfig('security:passport-oidc:attrMapId');
-    const attrMapUserName = crowi.configManager.getConfig('security:passport-oidc:attrMapUserName');
-    const attrMapName = crowi.configManager.getConfig('security:passport-oidc:attrMapName');
-    const attrMapMail = crowi.configManager.getConfig('security:passport-oidc:attrMapMail');
+    const attrMapId = crowi.configManager.getConfig(
+      'security:passport-oidc:attrMapId',
+    );
+    const attrMapUserName = crowi.configManager.getConfig(
+      'security:passport-oidc:attrMapUserName',
+    );
+    const attrMapName = crowi.configManager.getConfig(
+      'security:passport-oidc:attrMapName',
+    );
+    const attrMapMail = crowi.configManager.getConfig(
+      'security:passport-oidc:attrMapMail',
+    );
 
     let response;
     try {
-      response = await promisifiedPassportAuthentication(strategyName, req, res);
-    }
-    catch (err) {
+      response = await promisifiedPassportAuthentication(
+        strategyName,
+        req,
+        res,
+      );
+    } catch (err) {
       logger.debug(err);
       return next(new ExternalAccountLoginError(err.message));
     }
@@ -478,46 +578,82 @@ module.exports = function(crowi, app) {
       name: response[attrMapName],
       email: response[attrMapMail],
     };
-    logger.debug('mapping response to userInfo', userInfo, response, attrMapId, attrMapUserName, attrMapMail);
-
-    const externalAccount = await externalAccountService.getOrCreateUser(userInfo, providerId);
+    logger.debug(
+      'mapping response to userInfo',
+      userInfo,
+      response,
+      attrMapId,
+      attrMapUserName,
+      attrMapMail,
+    );
+
+    const externalAccount = await externalAccountService.getOrCreateUser(
+      userInfo,
+      providerId,
+    );
     if (!externalAccount) {
       return new ExternalAccountLoginError('message.sign_in_failure');
     }
 
     // login
     const user = (await externalAccount.populate('user')).user;
-    req.logIn(user, async(err) => {
-      if (err) { logger.debug(err.message); return next(new ExternalAccountLoginError(err.message)) }
+    req.logIn(user, async (err) => {
+      if (err) {
+        logger.debug(err.message);
+        return next(new ExternalAccountLoginError(err.message));
+      }
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_OIDC, true);
+      return loginSuccessHandler(
+        req,
+        res,
+        user,
+        SupportedAction.ACTION_USER_LOGIN_WITH_OIDC,
+        true,
+      );
     });
   };
 
-  const loginWithSaml = function(req, res, next) {
+  const loginWithSaml = (req, res, next) => {
     if (!passportService.isSamlStrategySetup) {
       logger.debug('SamlStrategy has not been set up');
-      const error = new ExternalAccountLoginError('message.strategy_has_not_been_set_up', { strategy: 'SamlStrategy' });
+      const error = new ExternalAccountLoginError(
+        'message.strategy_has_not_been_set_up',
+        { strategy: 'SamlStrategy' },
+      );
       return next(error);
     }
 
     passport.authenticate('saml')(req, res);
   };
 
-  const loginPassportSamlCallback = async(req, res, next) => {
+  const loginPassportSamlCallback = async (req, res, next) => {
     const providerId = 'saml';
     const strategyName = 'saml';
-    const attrMapId = crowi.configManager.getConfig('security:passport-saml:attrMapId');
-    const attrMapUsername = crowi.configManager.getConfig('security:passport-saml:attrMapUsername');
-    const attrMapMail = crowi.configManager.getConfig('security:passport-saml:attrMapMail');
-    const attrMapFirstName = crowi.configManager.getConfig('security:passport-saml:attrMapFirstName') || 'firstName';
-    const attrMapLastName = crowi.configManager.getConfig('security:passport-saml:attrMapLastName') || 'lastName';
+    const attrMapId = crowi.configManager.getConfig(
+      'security:passport-saml:attrMapId',
+    );
+    const attrMapUsername = crowi.configManager.getConfig(
+      'security:passport-saml:attrMapUsername',
+    );
+    const attrMapMail = crowi.configManager.getConfig(
+      'security:passport-saml:attrMapMail',
+    );
+    const attrMapFirstName =
+      crowi.configManager.getConfig(
+        'security:passport-saml:attrMapFirstName',
+      ) || 'firstName';
+    const attrMapLastName =
+      crowi.configManager.getConfig('security:passport-saml:attrMapLastName') ||
+      'lastName';
 
     let response;
     try {
-      response = await promisifiedPassportAuthentication(strategyName, req, res);
-    }
-    catch (err) {
+      response = await promisifiedPassportAuthentication(
+        strategyName,
+        req,
+        res,
+      );
+    } catch (err) {
       return next(new ExternalAccountLoginError(err.message));
     }
 
@@ -531,15 +667,23 @@ module.exports = function(crowi, app) {
     const firstName = response[attrMapFirstName];
     const lastName = response[attrMapLastName];
     if (firstName != null || lastName != null) {
-      userInfo.name = `${response[attrMapFirstName]} ${response[attrMapLastName]}`.trim();
+      userInfo.name =
+        `${response[attrMapFirstName]} ${response[attrMapLastName]}`.trim();
     }
 
     // Attribute-based Login Control
     if (!crowi.passportService.verifySAMLResponseByABLCRule(response)) {
-      return next(new ExternalAccountLoginError('Sign in failure due to insufficient privileges.'));
+      return next(
+        new ExternalAccountLoginError(
+          'Sign in failure due to insufficient privileges.',
+        ),
+      );
     }
 
-    const externalAccount = await externalAccountService.getOrCreateUser(userInfo, providerId);
+    const externalAccount = await externalAccountService.getOrCreateUser(
+      userInfo,
+      providerId,
+    );
     if (!externalAccount) {
       return next(new ExternalAccountLoginError('message.sign_in_failure'));
     }
@@ -553,7 +697,13 @@ module.exports = function(crowi, app) {
         return next(new ExternalAccountLoginError(err.message));
       }
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_SAML, true);
+      return loginSuccessHandler(
+        req,
+        res,
+        user,
+        SupportedAction.ACTION_USER_LOGIN_WITH_SAML,
+        true,
+      );
     });
   };
 

+ 52 - 34
apps/app/src/server/routes/login.js

@@ -8,13 +8,11 @@ import { growiInfoService } from '../service/growi-info';
 // because this file is a deprecated legacy of Crowi
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
-module.exports = function(crowi, app) {
+module.exports = (crowi, app) => {
   const logger = loggerFactory('growi:routes:login');
   const path = require('path');
   const User = crowi.model('User');
-  const {
-    appService, aclService, mailService, activityService,
-  } = crowi;
+  const { appService, aclService, mailService, activityService } = crowi;
   const activityEvent = crowi.event('activity');
 
   const actions = {};
@@ -29,7 +27,10 @@ module.exports = function(crowi, app) {
       return mailService.send({
         to: admin.email,
         subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
-        template: path.join(crowi.localeDir, `${locale}/admin/userWaitingActivation.ejs`),
+        template: path.join(
+          crowi.localeDir,
+          `${locale}/admin/userWaitingActivation.ejs`,
+        ),
         vars: {
           adminUser: admin,
           createdUser: userData,
@@ -41,12 +42,11 @@ module.exports = function(crowi, app) {
 
     const results = await Promise.allSettled(promises);
     results
-      .filter(result => result.status === 'rejected')
-      .forEach(result => logger.error(result.reason));
+      .filter((result) => result.status === 'rejected')
+      .forEach((result) => logger.error(result.reason));
   }
 
   async function sendNotificationToAllAdmins(user) {
-
     const activity = await activityService.createActivity({
       action: SupportedAction.ACTION_USER_REGISTRATION_APPROVAL_REQUEST,
       target: user,
@@ -56,7 +56,7 @@ module.exports = function(crowi, app) {
     /**
      * @param {import('../service/pre-notify').PreNotifyProps} props
      */
-    const preNotify = async(props) => {
+    const preNotify = async (props) => {
       /** @type {(import('mongoose').HydratedDocument<import('@growi/core').IUser>)[]} */
       const adminUsers = await User.findAdmins();
 
@@ -68,13 +68,23 @@ module.exports = function(crowi, app) {
     return;
   }
 
-  const registerSuccessHandler = async function(req, res, userData, registrationMode) {
-    const parameters = { action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS };
+  const registerSuccessHandler = async (
+    req,
+    res,
+    userData,
+    registrationMode,
+  ) => {
+    const parameters = {
+      action: SupportedAction.ACTION_USER_REGISTRATION_SUCCESS,
+    };
     activityEvent.emit('update', res.locals.activity._id, parameters);
 
     const isMailerSetup = mailService.isMailerSetup ?? false;
 
-    if (registrationMode === aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED) {
+    if (
+      registrationMode ===
+      aclService.labels.SECURITY_REGISTRATION_MODE_RESTRICTED
+    ) {
       sendNotificationToAllAdmins(userData);
       if (isMailerSetup) {
         await sendEmailToAllAdmins(userData);
@@ -117,8 +127,7 @@ module.exports = function(crowi, app) {
     req.login(userData, (err) => {
       if (err) {
         logger.debug(err);
-      }
-      else {
+      } else {
         // update lastLoginAt
         userData.updateLastLoginAt(new Date(), (err) => {
           if (err) {
@@ -132,12 +141,10 @@ module.exports = function(crowi, app) {
         // userData.password can't be empty but, prepare redirect because password property in User Model is optional
         // https://github.com/growilabs/growi/pull/6670
         redirectTo = '/me#password_settings';
-      }
-      else if (req.session.redirectTo != null) {
+      } else if (req.session.redirectTo != null) {
         redirectTo = req.session.redirectTo;
         delete req.session.redirectTo;
-      }
-      else {
+      } else {
         redirectTo = '/';
       }
 
@@ -145,7 +152,7 @@ module.exports = function(crowi, app) {
     });
   };
 
-  actions.preLogin = function(req, res, next) {
+  actions.preLogin = (req, res, next) => {
     // user has already logged in
     const { user } = req;
     if (user != null && user.status === User.STATUS_ACTIVE) {
@@ -199,13 +206,16 @@ module.exports = function(crowi, app) {
    *                 redirectTo:
    *                   type: string
    */
-  actions.register = function(req, res) {
+  actions.register = (req, res) => {
     if (req.user != null) {
       return res.apiv3Err('message.user_already_logged_in', 403);
     }
 
     // config で closed ならさよなら
-    if (configManager.getConfig('security:registrationMode') === aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED) {
+    if (
+      configManager.getConfig('security:registrationMode') ===
+      aclService.labels.SECURITY_REGISTRATION_MODE_CLOSED
+    ) {
       return res.apiv3Err('message.registration_closed', 403);
     }
 
@@ -240,21 +250,29 @@ module.exports = function(crowi, app) {
         return res.apiv3Err(errors, 400);
       }
 
-      const registrationMode = configManager.getConfig('security:registrationMode');
+      const registrationMode = configManager.getConfig(
+        'security:registrationMode',
+      );
 
-      User.createUserByEmailAndPassword(name, username, email, password, undefined, async(err, userData) => {
-        if (err) {
-          const errors = [];
-          if (err.name === 'UserUpperLimitException') {
-            errors.push('message.can_not_register_maximum_number_of_users');
-          }
-          else {
-            errors.push('message.failed_to_register');
+      User.createUserByEmailAndPassword(
+        name,
+        username,
+        email,
+        password,
+        undefined,
+        async (err, userData) => {
+          if (err) {
+            const errors = [];
+            if (err.name === 'UserUpperLimitException') {
+              errors.push('message.can_not_register_maximum_number_of_users');
+            } else {
+              errors.push('message.failed_to_register');
+            }
+            return res.apiv3Err(errors, 405);
           }
-          return res.apiv3Err(errors, 405);
-        }
-        return registerSuccessHandler(req, res, userData, registrationMode);
-      });
+          return registerSuccessHandler(req, res, userData, registrationMode);
+        },
+      );
     });
   };
 

+ 10 - 10
apps/app/src/server/routes/next.ts

@@ -1,25 +1,26 @@
-import type { IncomingMessage } from 'http';
-
 import type { NextServer, RequestHandler } from 'next/dist/server/next';
+import type { IncomingMessage } from 'http';
 
 type Crowi = {
-  nextApp: NextServer,
-}
+  nextApp: NextServer;
+};
 
 type CrowiReq = IncomingMessage & {
-  crowi: Crowi,
-}
+  crowi: Crowi;
+};
 
 type NextDelegatorResult = {
-  delegateToNext: RequestHandler,
+  delegateToNext: RequestHandler;
 };
 
 const delegator = (crowi: Crowi): NextDelegatorResult => {
-
   const { nextApp } = crowi;
   const handle = nextApp.getRequestHandler();
 
-  const delegateToNext: RequestHandler = (req: CrowiReq, res): Promise<void> => {
+  const delegateToNext: RequestHandler = (
+    req: CrowiReq,
+    res,
+  ): Promise<void> => {
     req.crowi = crowi;
     return handle(req, res);
   };
@@ -27,7 +28,6 @@ const delegator = (crowi: Crowi): NextDelegatorResult => {
   return {
     delegateToNext,
   };
-
 };
 
 export default delegator;

+ 52 - 41
apps/app/src/server/routes/ogp.ts

@@ -1,17 +1,14 @@
-import * as fs from 'fs';
-import path from 'path';
-
 import { getIdStringForRef, type IUser } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 // biome-ignore lint/style/noRestrictedImports: Direct axios usage for OGP image fetching
 import axios from 'axios';
-import type {
-  Request, Response, NextFunction,
-} from 'express';
+import type { NextFunction, Request, Response } from 'express';
 import type { ValidationError } from 'express-validator';
 import { param, validationResult } from 'express-validator';
+import * as fs from 'fs';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
+import path from 'path';
 
 import { projectRoot } from '~/server/util/project-dir-utils';
 import loggerFactory from '~/utils/logger';
@@ -33,15 +30,14 @@ fs.readFile(path.join(projectRoot, DEFAULT_USER_IMAGE_PATH), (err, buffer) => {
   bufferedDefaultUserImageCache = buffer;
 });
 
-
-module.exports = function(crowi: Crowi) {
-
+module.exports = (crowi: Crowi) => {
   const isUserImageAttachment = (userImageUrlCached: string): boolean => {
     return /^\/attachment\/.+/.test(userImageUrlCached);
   };
 
-  const getBufferedUserImage = async(userImageUrlCached: string): Promise<Buffer | null> => {
-
+  const getBufferedUserImage = async (
+    userImageUrlCached: string,
+  ): Promise<Buffer | null> => {
     let bufferedUserImage: Buffer;
 
     if (isUserImageAttachment(userImageUrlCached)) {
@@ -57,25 +53,25 @@ module.exports = function(crowi: Crowi) {
       return bufferedUserImage;
     }
 
-    return (await axios.get(
-      userImageUrlCached, {
+    return (
+      await axios.get(userImageUrlCached, {
         responseType: 'arraybuffer',
-      },
-    )).data;
-
+      })
+    ).data;
   };
 
-  const renderOgp = async(req: Request, res: Response) => {
-
+  const renderOgp = async (req: Request, res: Response) => {
     const ogpUri = configManager.getConfig('app:ogpUri');
 
     if (ogpUri == null) {
-      return res.status(501).send('OGP_URI for growi-unique-ogp has not been setup');
+      return res
+        .status(501)
+        .send('OGP_URI for growi-unique-ogp has not been setup');
     }
 
     const page: PageDocument = req.body.page; // asserted by ogpValidator
 
-    const title = (new DevidedPagePath(page.path)).latter;
+    const title = new DevidedPagePath(page.path).latter;
 
     let user: IUser | null = null;
     let userName = '(unknown)';
@@ -88,13 +84,14 @@ module.exports = function(crowi: Crowi) {
 
         if (user != null) {
           userName = user.username;
-          userImage = user.imageUrlCached !== DEFAULT_USER_IMAGE_URL
-            ? bufferedDefaultUserImageCache
-            : await getBufferedUserImage(user.imageUrlCached) ?? bufferedDefaultUserImageCache;
+          userImage =
+            user.imageUrlCached !== DEFAULT_USER_IMAGE_URL
+              ? bufferedDefaultUserImageCache
+              : ((await getBufferedUserImage(user.imageUrlCached)) ??
+                bufferedDefaultUserImageCache);
         }
       }
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       return res.status(500).send(`error: ${err}`);
     }
@@ -102,18 +99,19 @@ module.exports = function(crowi: Crowi) {
     let result;
     try {
       result = await axios.post(
-        ogpUri, {
+        ogpUri,
+        {
           data: {
             title,
             userName,
             userImage,
           },
-        }, {
+        },
+        {
           responseType: 'stream',
         },
       );
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       return res.status(500).send(`error: ${err}`);
     }
@@ -122,36 +120,50 @@ module.exports = function(crowi: Crowi) {
       'Content-Type': 'image/jpeg',
     });
     result.data.pipe(res);
-
   };
 
-  const pageIdRequired = param('pageId').not().isEmpty().withMessage('page id is not included in the parameter');
+  const pageIdRequired = param('pageId')
+    .not()
+    .isEmpty()
+    .withMessage('page id is not included in the parameter');
 
-  const ogpValidator = async(req:Request, res:Response, next:NextFunction) => {
+  const ogpValidator = async (
+    req: Request,
+    res: Response,
+    next: NextFunction,
+  ) => {
     const { aclService, fileUploadService, configManager } = crowi;
 
     const ogpUri = configManager.getConfig('app:ogpUri');
 
-    if (ogpUri == null) return res.status(400).send('OGP URI for GROWI has not been setup');
-    if (!fileUploadService.getIsUploadable()) return res.status(501).send('This GROWI can not upload file');
-    if (!aclService.isGuestAllowedToRead()) return res.status(501).send('This GROWI is not public');
+    if (ogpUri == null)
+      return res.status(400).send('OGP URI for GROWI has not been setup');
+    if (!fileUploadService.getIsUploadable())
+      return res.status(501).send('This GROWI can not upload file');
+    if (!aclService.isGuestAllowedToRead())
+      return res.status(501).send('This GROWI is not public');
 
     const errors = validationResult(req);
 
     if (errors.isEmpty()) {
-
-      const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
+      const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
+        'Page',
+      );
 
       try {
         const page = await Page.findByIdAndViewer(req.params.pageId, null);
 
-        if (page == null || page.status !== Page.STATUS_PUBLISHED || (page.grant !== Page.GRANT_PUBLIC && page.grant !== Page.GRANT_RESTRICTED)) {
+        if (
+          page == null ||
+          page.status !== Page.STATUS_PUBLISHED ||
+          (page.grant !== Page.GRANT_PUBLIC &&
+            page.grant !== Page.GRANT_RESTRICTED)
+        ) {
           return res.status(400).send('the page does not exist');
         }
 
         req.body.page = page;
-      }
-      catch (error) {
+      } catch (error) {
         logger.error(error);
         return res.status(500).send(`error: ${error}`);
       }
@@ -170,5 +182,4 @@ module.exports = function(crowi: Crowi) {
     pageIdRequired,
     ogpValidator,
   };
-
 };

+ 119 - 48
apps/app/src/server/routes/page.js

@@ -3,8 +3,8 @@ import mongoose from 'mongoose';
 
 import loggerFactory from '~/utils/logger';
 
-import { GlobalNotificationSettingEvent } from '../models/GlobalNotificationSetting';
 import { PathAlreadyExistsError } from '../models/errors';
+import { GlobalNotificationSettingEvent } from '../models/GlobalNotificationSetting';
 import PageTagRelation from '../models/page-tag-relation';
 import UpdatePost from '../models/update-post';
 
@@ -16,7 +16,7 @@ import UpdatePost from '../models/update-post';
 
 /* eslint-disable no-use-before-define */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
-module.exports = function(crowi, app) {
+module.exports = (crowi, app) => {
   const logger = loggerFactory('growi:routes:page');
 
   const { pagePathUtils } = require('@growi/core/dist/utils');
@@ -66,7 +66,6 @@ module.exports = function(crowi, app) {
   //   return res.render('page_presentation', renderVars);
   // }
 
-
   /**
    * switch action
    *   - presentation mode
@@ -81,7 +80,6 @@ module.exports = function(crowi, app) {
   //   return showPageForGrowiBehavior(req, res, next);
   // };
 
-
   const api = {};
   const validator = {};
 
@@ -145,12 +143,11 @@ module.exports = function(crowi, app) {
    *
    * @apiParam {String} pageId
    */
-  api.getPageTag = async function(req, res) {
+  api.getPageTag = async (req, res) => {
     const result = {};
     try {
       result.tags = await PageTagRelation.listTagNamesByPage(req.query.pageId);
-    }
-    catch (err) {
+    } catch (err) {
       return res.json(ApiResponse.error(err));
     }
     return res.json(ApiResponse.success(result));
@@ -219,7 +216,7 @@ module.exports = function(crowi, app) {
    *
    * @apiParam {String} path
    */
-  api.getUpdatePost = function(req, res) {
+  api.getUpdatePost = (req, res) => {
     const path = req.query.path;
 
     if (!path) {
@@ -244,11 +241,15 @@ module.exports = function(crowi, app) {
 
   validator.remove = [
     body('completely')
-      .custom(v => v === 'true' || v === true || v == null)
-      .withMessage('The body property "completely" must be "true" or true. (Omit param for false)'),
+      .custom((v) => v === 'true' || v === true || v == null)
+      .withMessage(
+        'The body property "completely" must be "true" or true. (Omit param for false)',
+      ),
     body('recursively')
-      .custom(v => v === 'true' || v === true || v == null)
-      .withMessage('The body property "recursively" must be "true" or true. (Omit param for false)'),
+      .custom((v) => v === 'true' || v === true || v == null)
+      .withMessage(
+        'The body property "recursively" must be "true" or true. (Omit param for false)',
+      ),
   ];
 
   /**
@@ -349,7 +350,7 @@ module.exports = function(crowi, app) {
    *       500:
    *         $ref: '#/components/responses/InternalServerError'
    */
-  api.remove = async function(req, res) {
+  api.remove = async (req, res) => {
     const pageId = req.body.page_id;
     const previousRevision = req.body.revision_id || null;
 
@@ -366,11 +367,21 @@ module.exports = function(crowi, app) {
     const page = await Page.findByIdAndViewer(pageId, req.user, null, true);
 
     if (page == null) {
-      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',
+        ),
+      );
     }
 
     if (page.isEmpty && !isRecursively) {
-      return res.json(ApiResponse.error('Empty pages cannot be single deleted', 'single_deletion_empty_pages'));
+      return res.json(
+        ApiResponse.error(
+          'Empty pages cannot be single deleted',
+          'single_deletion_empty_pages',
+        ),
+      );
     }
 
     const creatorId = await crowi.pageService.getCreatorIdForCanDelete(page);
@@ -379,51 +390,97 @@ module.exports = function(crowi, app) {
 
     try {
       if (isCompletely) {
-        const userRelatedGroups = await crowi.pageGrantService.getUserRelatedGroups(req.user);
-        const canDeleteCompletely = crowi.pageService.canDeleteCompletely(page, creatorId, req.user, isRecursively, userRelatedGroups);
+        const userRelatedGroups =
+          await crowi.pageGrantService.getUserRelatedGroups(req.user);
+        const canDeleteCompletely = crowi.pageService.canDeleteCompletely(
+          page,
+          creatorId,
+          req.user,
+          isRecursively,
+          userRelatedGroups,
+        );
         if (!canDeleteCompletely) {
-          return res.json(ApiResponse.error('You cannot delete this page completely', 'complete_deletion_not_allowed_for_user'));
+          return res.json(
+            ApiResponse.error(
+              'You cannot delete this page completely',
+              'complete_deletion_not_allowed_for_user',
+            ),
+          );
         }
 
         if (pagePathUtils.isUsersHomepage(page.path)) {
           if (!crowi.pageService.canDeleteUserHomepageByConfig()) {
-            return res.json(ApiResponse.error('Could not delete user homepage'));
+            return res.json(
+              ApiResponse.error('Could not delete user homepage'),
+            );
           }
-          if (!await crowi.pageService.isUsersHomepageOwnerAbsent(page.path)) {
-            return res.json(ApiResponse.error('Could not delete user homepage'));
+          if (
+            !(await crowi.pageService.isUsersHomepageOwnerAbsent(page.path))
+          ) {
+            return res.json(
+              ApiResponse.error('Could not delete user homepage'),
+            );
           }
         }
 
-        await crowi.pageService.deleteCompletely(page, req.user, options, isRecursively, false, activityParameters);
-      }
-      else {
+        await crowi.pageService.deleteCompletely(
+          page,
+          req.user,
+          options,
+          isRecursively,
+          false,
+          activityParameters,
+        );
+      } else {
         // behave like not found
         const notRecursivelyAndEmpty = page.isEmpty && !isRecursively;
         if (notRecursivelyAndEmpty) {
-          return res.json(ApiResponse.error(`Page '${pageId}' is not found.`, 'notfound'));
+          return res.json(
+            ApiResponse.error(`Page '${pageId}' is not found.`, 'notfound'),
+          );
         }
 
         if (!page.isEmpty && !page.isUpdatable(previousRevision)) {
-          return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
+          return res.json(
+            ApiResponse.error(
+              "Someone could update this page, so couldn't delete.",
+              'outdated',
+            ),
+          );
         }
 
-        if (!crowi.pageService.canDelete(page, creatorId, req.user, isRecursively)) {
-          return res.json(ApiResponse.error('You cannot delete this page', 'user_not_admin'));
+        if (
+          !crowi.pageService.canDelete(page, creatorId, req.user, isRecursively)
+        ) {
+          return res.json(
+            ApiResponse.error('You cannot delete this page', 'user_not_admin'),
+          );
         }
 
         if (pagePathUtils.isUsersHomepage(page.path)) {
           if (!crowi.pageService.canDeleteUserHomepageByConfig()) {
-            return res.json(ApiResponse.error('Could not delete user homepage'));
+            return res.json(
+              ApiResponse.error('Could not delete user homepage'),
+            );
           }
-          if (!await crowi.pageService.isUsersHomepageOwnerAbsent(page.path)) {
-            return res.json(ApiResponse.error('Could not delete user homepage'));
+          if (
+            !(await crowi.pageService.isUsersHomepageOwnerAbsent(page.path))
+          ) {
+            return res.json(
+              ApiResponse.error('Could not delete user homepage'),
+            );
           }
         }
 
-        await crowi.pageService.deletePage(page, req.user, options, isRecursively, activityParameters);
+        await crowi.pageService.deletePage(
+          page,
+          req.user,
+          options,
+          isRecursively,
+          activityParameters,
+        );
       }
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Error occured while get setting', err);
       return res.json(ApiResponse.error('Failed to delete page.', err.message));
     }
@@ -438,9 +495,12 @@ module.exports = function(crowi, app) {
 
     try {
       // global notification
-      await globalNotificationService.fire(GlobalNotificationSettingEvent.PAGE_DELETE, page, req.user);
-    }
-    catch (err) {
+      await globalNotificationService.fire(
+        GlobalNotificationSettingEvent.PAGE_DELETE,
+        page,
+        req.user,
+      );
+    } catch (err) {
       logger.error('Delete notification failed', err);
     }
   };
@@ -448,8 +508,10 @@ module.exports = function(crowi, app) {
   validator.revertRemove = [
     body('recursively')
       .optional()
-      .custom(v => v === 'true' || v === true || v == null)
-      .withMessage('The body property "recursively" must be "true" or true. (Omit param for false)'),
+      .custom((v) => v === 'true' || v === true || v == null)
+      .withMessage(
+        'The body property "recursively" must be "true" or true. (Omit param for false)',
+      ),
   ];
 
   /**
@@ -537,7 +599,7 @@ module.exports = function(crowi, app) {
    *       500:
    *         $ref: '#/components/responses/InternalServerError'
    */
-  api.revertRemove = async function(req, res, options) {
+  api.revertRemove = async (req, res, options) => {
     const pageId = req.body.page_id;
 
     // get recursively flag
@@ -552,14 +614,24 @@ module.exports = function(crowi, app) {
     try {
       page = await Page.findByIdAndViewer(pageId, req.user);
       if (page == null) {
-        throw new Error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden');
+        throw new Error(
+          `Page '${pageId}' is not found or forbidden`,
+          'notfound_or_forbidden',
+        );
       }
-      page = await crowi.pageService.revertDeletedPage(page, req.user, {}, isRecursively, activityParameters);
-    }
-    catch (err) {
+      page = await crowi.pageService.revertDeletedPage(
+        page,
+        req.user,
+        {},
+        isRecursively,
+        activityParameters,
+      );
+    } catch (err) {
       if (err instanceof PathAlreadyExistsError) {
         logger.error('Path already exists', err);
-        return res.json(ApiResponse.error(err, 'already_exists', err.targetPath));
+        return res.json(
+          ApiResponse.error(err, 'already_exists', err.targetPath),
+        );
       }
       logger.error('Error occured while get setting', err);
       return res.json(ApiResponse.error(err));
@@ -628,14 +700,13 @@ module.exports = function(crowi, app) {
    *       500:
    *         $ref: '#/components/responses/InternalServerError'
    */
-  api.unlink = async function(req, res) {
+  api.unlink = async (req, res) => {
     const path = req.body.path;
 
     try {
       await PageRedirect.removePageRedirectsByToPath(path);
       logger.debug('Redirect Page deleted', path);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Error occured while get setting', err);
       return res.json(ApiResponse.error('Failed to delete redirect page.'));
     }

+ 39 - 18
apps/app/src/server/routes/search.ts

@@ -6,7 +6,6 @@ import type Crowi from '../crowi';
 import UserGroupRelation from '../models/user-group-relation';
 import { isSearchError } from '../models/vo/search-error';
 
-
 const logger = loggerFactory('growi:routes:search');
 
 /**
@@ -37,7 +36,7 @@ const logger = loggerFactory('growi:routes:search');
  *           meta:
  *             $ref: '#/components/schemas/ElasticsearchResultMeta'
  */
-module.exports = function(crowi: Crowi, app) {
+module.exports = (crowi: Crowi, app) => {
   const ApiResponse = require('../util/apiResponse');
   const ApiPaginate = require('../util/apiPaginate');
 
@@ -110,17 +109,21 @@ module.exports = function(crowi: Crowi, app) {
    * @apiParam {String} offset
    * @apiParam {String} limit
    */
-  api.search = async function(req, res) {
+  api.search = async (req, res) => {
     const user = req.user;
     const {
-      q = null, nq = null, type = null, sort = null, order = null, vector = null,
+      q = null,
+      nq = null,
+      type = null,
+      sort = null,
+      order = null,
+      vector = null,
     } = req.query;
     let paginateOpts;
 
     try {
       paginateOpts = ApiPaginate.parseOptionsForElasticSearch(req.query);
-    }
-    catch (e) {
+    } catch (e) {
       res.json(ApiResponse.error(e));
     }
 
@@ -133,13 +136,22 @@ module.exports = function(crowi: Crowi, app) {
       return res.json(ApiResponse.error('SearchService is not reachable.'));
     }
 
-    const userGroups = user != null ? [
-      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-    ] : null;
+    const userGroups =
+      user != null
+        ? [
+            ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+            ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(
+              user,
+            )),
+          ]
+        : null;
 
     const searchOpts = {
-      ...paginateOpts, type, sort, order, vector,
+      ...paginateOpts,
+      type,
+      sort,
+      order,
+      vector,
     };
 
     let searchResult;
@@ -147,9 +159,14 @@ module.exports = function(crowi: Crowi, app) {
     try {
       const query = decodeURIComponent(q);
       const nqName = nq ?? decodeURIComponent(nq);
-      [searchResult, delegatorName] = await searchService.searchKeyword(query, nqName, user, userGroups, searchOpts);
-    }
-    catch (err) {
+      [searchResult, delegatorName] = await searchService.searchKeyword(
+        query,
+        nqName,
+        user,
+        userGroups,
+        searchOpts,
+      );
+    } catch (err) {
       logger.error('Failed to search', err);
 
       if (isSearchError(err)) {
@@ -162,15 +179,19 @@ module.exports = function(crowi: Crowi, app) {
 
     let result;
     try {
-      result = await searchService.formatSearchResult(searchResult, delegatorName, user, userGroups);
-    }
-    catch (err) {
+      result = await searchService.formatSearchResult(
+        searchResult,
+        delegatorName,
+        user,
+        userGroups,
+      );
+    } catch (err) {
       logger.error(err);
       return res.json(ApiResponse.error(err));
     }
 
     const parameters = {
-      ip:  req.ip,
+      ip: req.ip,
       endpoint: req.originalUrl,
       action: SupportedAction.ACTION_SEARCH_PAGE,
       user: req.user?._id,

+ 27 - 17
apps/app/src/server/routes/tag.js

@@ -6,8 +6,7 @@ import { Revision } from '../models/revision';
 import ApiResponse from '../util/apiResponse';
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
-module.exports = function(crowi, app) {
-
+module.exports = (crowi, app) => {
   const activityEvent = crowi.event('activity');
   const actions = {};
   const api = {};
@@ -56,14 +55,18 @@ module.exports = function(crowi, app) {
    *
    * @apiParam {String} q keyword
    */
-  api.search = async function(req, res) {
+  api.search = async (req, res) => {
     // https://regex101.com/r/J1cN6O/1
     // prevent from unexpecting attack doing regular expression on tag search (DoS attack)
     // Search for regular expressions as normal characters
     // e.g. user*$ -> user\*\$ (escape a regular expression)
     const escapeRegExp = req.query.q.replace(/[\\^$/.*+?()[\]{}|]/g, '\\$&');
-    let tags = await Tag.find({ name: new RegExp(`^${escapeRegExp}`) }).select('_id name');
-    tags = tags.map((tag) => { return tag.name });
+    let tags = await Tag.find({ name: new RegExp(`^${escapeRegExp}`) }).select(
+      '_id name',
+    );
+    tags = tags.map((tag) => {
+      return tag.name;
+    });
     return res.json(ApiResponse.success({ tags }));
   };
 
@@ -113,7 +116,7 @@ module.exports = function(crowi, app) {
    * @apiParam {String} PageId
    * @apiParam {array} tags
    */
-  api.update = async function(req, res) {
+  api.update = async (req, res) => {
     const Page = crowi.model('Page');
     const User = crowi.model('User');
     const tagEvent = crowi.event('tag');
@@ -128,22 +131,30 @@ module.exports = function(crowi, app) {
       const page = await Page.findById(pageId);
       const user = await User.findById(userId);
 
-      if (!await Page.isAccessiblePageByViewer(page._id, user)) {
-        return res.json(ApiResponse.error("You don't have permission to update this page."));
+      if (!(await Page.isAccessiblePageByViewer(page._id, user))) {
+        return res.json(
+          ApiResponse.error("You don't have permission to update this page."),
+        );
       }
 
       const previousRevision = await Revision.findById(revisionId);
-      result.savedPage = await crowi.pageService.updatePage(page, previousRevision.body, previousRevision.body, req.user);
+      result.savedPage = await crowi.pageService.updatePage(
+        page,
+        previousRevision.body,
+        previousRevision.body,
+        req.user,
+      );
       await PageTagRelation.updatePageTags(pageId, tags);
       result.tags = await PageTagRelation.listTagNamesByPage(pageId);
 
       tagEvent.emit('update', page, tags);
-    }
-    catch (err) {
+    } catch (err) {
       return res.json(ApiResponse.error(err));
     }
 
-    activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_TAG_UPDATE });
+    activityEvent.emit('update', res.locals.activity._id, {
+      action: SupportedAction.ACTION_TAG_UPDATE,
+    });
 
     return res.json(ApiResponse.success(result));
   };
@@ -194,7 +205,7 @@ module.exports = function(crowi, app) {
    * @apiParam {Number} limit
    * @apiParam {Number} offset
    */
-  api.list = async function(req, res) {
+  api.list = async (req, res) => {
     const limit = +req.query.limit || 50;
     const offset = +req.query.offset || 0;
     const sortOpt = { count: -1, _id: -1 };
@@ -202,15 +213,14 @@ module.exports = function(crowi, app) {
 
     try {
       // get tag list contains id name and count properties
-      const tagsWithCount = await PageTagRelation.createTagListWithCount(queryOptions);
+      const tagsWithCount =
+        await PageTagRelation.createTagListWithCount(queryOptions);
 
       return res.json(ApiResponse.success(tagsWithCount));
-    }
-    catch (err) {
+    } catch (err) {
       return res.json(ApiResponse.error(err));
     }
   };
 
-
   return actions;
 };

+ 11 - 6
apps/app/src/server/routes/user-activation.ts

@@ -1,16 +1,16 @@
-import type { Response, NextFunction } from 'express';
+import type { NextFunction, Response } from 'express';
 
 import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
 import type { ReqWithUserRegistrationOrder } from '~/server/middlewares/inject-user-registration-order-by-token-middleware';
 
 type Crowi = {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  nextApp: any,
-}
+  nextApp: any;
+};
 
 type CrowiReq = ReqWithUserRegistrationOrder & {
-  crowi: Crowi,
-}
+  crowi: Crowi;
+};
 
 /**
  * @swagger
@@ -45,7 +45,12 @@ export const renderUserActivationPage = (crowi: Crowi) => {
 
 // middleware to handle error
 export const tokenErrorHandlerMiddeware = (crowi: Crowi) => {
-  return (error: Error & { code: UserActivationErrorCode, statusCode: number }, req: CrowiReq, res: Response, next: NextFunction): void => {
+  return (
+    error: Error & { code: UserActivationErrorCode; statusCode: number },
+    req: CrowiReq,
+    res: Response,
+    next: NextFunction,
+  ): void => {
     if (error != null) {
       const { nextApp } = crowi;
       req.crowi = crowi;

+ 3 - 5
apps/app/src/server/routes/user.js

@@ -45,18 +45,17 @@
  *            example: 2010-01-01T00:00:00.000Z
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
-module.exports = function(crowi, app) {
+module.exports = (crowi, app) => {
   const User = crowi.model('User');
   const ApiResponse = require('../util/apiResponse');
 
   const actions = {};
 
-
   const api = {};
 
   actions.api = api;
 
-  api.checkUsername = async function(req, res) {
+  api.checkUsername = async (req, res) => {
     const username = req.query.username;
 
     let valid = false;
@@ -64,8 +63,7 @@ module.exports = function(crowi, app) {
       .then((userData) => {
         if (userData) {
           valid = false;
-        }
-        else {
+        } else {
           valid = true;
         }
       })

+ 2 - 1
biome.json

@@ -29,7 +29,8 @@
       "!packages/pdf-converter-client/specs",
       "!apps/app/src/client",
       "!apps/app/src/server/middlewares",
-      "!apps/app/src/server/routes",
+      "!apps/app/src/server/routes/apiv3",
+      "!apps/app/src/server/routes/attachment",
       "!apps/app/src/server/service"
     ]
   },