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

Merge pull request #10507 from growilabs/support/156162-173947-app-middlewares-biome

support: Configure biome for app server middlewares
Yuki Takei 4 месяцев назад
Родитель
Сommit
8269c8c652
41 измененных файлов с 486 добавлено и 297 удалено
  1. 1 0
      apps/app/.eslintrc.js
  2. 25 16
      apps/app/src/server/middlewares/access-token-parser/access-token.integ.ts
  3. 10 7
      apps/app/src/server/middlewares/access-token-parser/access-token.ts
  4. 8 12
      apps/app/src/server/middlewares/access-token-parser/api-token.integ.ts
  5. 9 4
      apps/app/src/server/middlewares/access-token-parser/api-token.ts
  6. 3 1
      apps/app/src/server/middlewares/access-token-parser/extract-bearer-token.ts
  7. 9 3
      apps/app/src/server/middlewares/access-token-parser/index.ts
  8. 7 6
      apps/app/src/server/middlewares/access-token-parser/interfaces.ts
  9. 30 26
      apps/app/src/server/middlewares/add-activity.ts
  10. 2 4
      apps/app/src/server/middlewares/admin-required.js
  11. 2 1
      apps/app/src/server/middlewares/apiv1-form-validator.ts
  12. 8 3
      apps/app/src/server/middlewares/apiv3-form-validator.ts
  13. 3 2
      apps/app/src/server/middlewares/application-installed.ts
  14. 32 14
      apps/app/src/server/middlewares/application-not-installed.ts
  15. 4 1
      apps/app/src/server/middlewares/auto-reconnect-to-s2s-msg-server.js
  16. 11 5
      apps/app/src/server/middlewares/auto-reconnect-to-search.js
  17. 1 3
      apps/app/src/server/middlewares/certify-brand-logo.ts
  18. 13 9
      apps/app/src/server/middlewares/certify-origin.ts
  19. 38 19
      apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.spec.ts
  20. 15 9
      apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.ts
  21. 2 2
      apps/app/src/server/middlewares/certify-shared-page-attachment/interfaces.ts
  22. 19 8
      apps/app/src/server/middlewares/certify-shared-page-attachment/retrieve-valid-share-link.ts
  23. 10 5
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-attachment.ts
  24. 0 4
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.spec.ts
  25. 7 6
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.ts
  26. 5 8
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.spec.ts
  27. 17 10
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.ts
  28. 5 4
      apps/app/src/server/middlewares/certify-shared-page.js
  29. 8 6
      apps/app/src/server/middlewares/exclude-read-only-user.spec.ts
  30. 23 11
      apps/app/src/server/middlewares/exclude-read-only-user.ts
  31. 6 9
      apps/app/src/server/middlewares/http-error-handler.js
  32. 24 10
      apps/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts
  33. 26 8
      apps/app/src/server/middlewares/inject-user-registration-order-by-token-middleware.ts
  34. 10 5
      apps/app/src/server/middlewares/invited-form-validator.ts
  35. 6 3
      apps/app/src/server/middlewares/login-form-validator.ts
  36. 4 6
      apps/app/src/server/middlewares/login-required.js
  37. 18 4
      apps/app/src/server/middlewares/register-form-validator.ts
  38. 18 13
      apps/app/src/server/middlewares/safe-redirect.spec.ts
  39. 29 21
      apps/app/src/server/middlewares/safe-redirect.ts
  40. 18 8
      apps/app/src/server/middlewares/unavailable-when-maintenance-mode.ts
  41. 0 1
      biome.json

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

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

+ 25 - 16
apps/app/src/server/middlewares/access-token-parser/access-token.integ.ts

@@ -1,9 +1,9 @@
 import { faker } from '@faker-js/faker';
+import { SCOPE } from '@growi/core/dist/interfaces';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import type { Response } from 'express';
 import { mock } from 'vitest-mock-extended';
 
-import { SCOPE } from '@growi/core/dist/interfaces';
 import type Crowi from '~/server/crowi';
 import type UserEvent from '~/server/events/user';
 import { AccessToken } from '~/server/models/access-token';
@@ -13,12 +13,11 @@ import type { AccessTokenParserReq } from './interfaces';
 
 vi.mock('@growi/core/dist/models/serializers', { spy: true });
 
-
 describe('access-token-parser middleware for access token with scopes', () => {
-
+  // biome-ignore lint/suspicious/noImplicitAnyLet: ignore
   let User;
 
-  beforeAll(async() => {
+  beforeAll(async () => {
     const crowiMock = mock<Crowi>({
       event: vi.fn().mockImplementation((eventName) => {
         if (eventName === 'user') {
@@ -32,7 +31,7 @@ describe('access-token-parser middleware for access token with scopes', () => {
     User = userModelFactory(crowiMock);
   });
 
-  it('should call next if no access token is provided', async() => {
+  it('should call next if no access token is provided', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -44,7 +43,7 @@ describe('access-token-parser middleware for access token with scopes', () => {
     expect(reqMock.user).toBeUndefined();
   });
 
-  it('should not authenticate with no scopes', async() => {
+  it('should not authenticate with no scopes', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -76,7 +75,7 @@ describe('access-token-parser middleware for access token with scopes', () => {
     expect(serializeUserSecurely).not.toHaveBeenCalled();
   });
 
-  it('should authenticate with specific scope', async() => {
+  it('should authenticate with specific scope', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -102,7 +101,10 @@ describe('access-token-parser middleware for access token with scopes', () => {
 
     // act
     reqMock.query.access_token = token;
-    await parserForAccessToken([SCOPE.READ.USER_SETTINGS.INFO])(reqMock, resMock);
+    await parserForAccessToken([SCOPE.READ.USER_SETTINGS.INFO])(
+      reqMock,
+      resMock,
+    );
 
     // assert
     expect(reqMock.user).toBeDefined();
@@ -110,7 +112,7 @@ describe('access-token-parser middleware for access token with scopes', () => {
     expect(serializeUserSecurely).toHaveBeenCalledOnce();
   });
 
-  it('should reject with insufficient scopes', async() => {
+  it('should reject with insufficient scopes', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -119,7 +121,6 @@ describe('access-token-parser middleware for access token with scopes', () => {
 
     expect(reqMock.user).toBeUndefined();
 
-
     // prepare a user
     const targetUser = await User.create({
       name: faker.person.fullName(),
@@ -137,14 +138,17 @@ describe('access-token-parser middleware for access token with scopes', () => {
 
     // act - try to access with write:user:info scope
     reqMock.query.access_token = token;
-    await parserForAccessToken([SCOPE.WRITE.USER_SETTINGS.INFO])(reqMock, resMock);
+    await parserForAccessToken([SCOPE.WRITE.USER_SETTINGS.INFO])(
+      reqMock,
+      resMock,
+    );
 
     // // assert
     expect(reqMock.user).toBeUndefined();
     expect(serializeUserSecurely).not.toHaveBeenCalled();
   });
 
-  it('should authenticate with write scope implying read scope', async() => {
+  it('should authenticate with write scope implying read scope', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -170,7 +174,10 @@ describe('access-token-parser middleware for access token with scopes', () => {
 
     // act - try to access with read:user:info scope
     reqMock.query.access_token = token;
-    await parserForAccessToken([SCOPE.READ.USER_SETTINGS.INFO])(reqMock, resMock);
+    await parserForAccessToken([SCOPE.READ.USER_SETTINGS.INFO])(
+      reqMock,
+      resMock,
+    );
 
     // assert
     expect(reqMock.user).toBeDefined();
@@ -178,7 +185,7 @@ describe('access-token-parser middleware for access token with scopes', () => {
     expect(serializeUserSecurely).toHaveBeenCalledOnce();
   });
 
-  it('should authenticate with wildcard scope', async() => {
+  it('should authenticate with wildcard scope', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -202,12 +209,14 @@ describe('access-token-parser middleware for access token with scopes', () => {
 
     // act - try to access with read:user:info scope
     reqMock.query.access_token = token;
-    await parserForAccessToken([SCOPE.READ.USER_SETTINGS.INFO, SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN])(reqMock, resMock);
+    await parserForAccessToken([
+      SCOPE.READ.USER_SETTINGS.INFO,
+      SCOPE.READ.USER_SETTINGS.API.ACCESS_TOKEN,
+    ])(reqMock, resMock);
 
     // assert
     expect(reqMock.user).toBeDefined();
     expect(reqMock.user?._id).toStrictEqual(targetUser._id);
     expect(serializeUserSecurely).toHaveBeenCalledOnce();
   });
-
 });

+ 10 - 7
apps/app/src/server/middlewares/access-token-parser/access-token.ts

@@ -8,15 +8,18 @@ import loggerFactory from '~/utils/logger';
 import { extractBearerToken } from './extract-bearer-token';
 import type { AccessTokenParserReq } from './interfaces';
 
-const logger = loggerFactory('growi:middleware:access-token-parser:access-token');
+const logger = loggerFactory(
+  'growi:middleware:access-token-parser:access-token',
+);
 
 export const parserForAccessToken = (scopes: Scope[]) => {
-  return async(req: AccessTokenParserReq, res: Response): Promise<void> => {
+  return async (req: AccessTokenParserReq, res: Response): Promise<void> => {
     // Extract token from Authorization header first
     // It is more efficient to call it only once in "AccessTokenParser," which is the caller of the method
     const bearerToken = extractBearerToken(req.headers.authorization);
 
-    const accessToken = bearerToken ?? req.query.access_token ?? req.body.access_token;
+    const accessToken =
+      bearerToken ?? req.query.access_token ?? req.body.access_token;
     if (accessToken == null || typeof accessToken !== 'string') {
       return;
     }
@@ -33,14 +36,15 @@ export const parserForAccessToken = (scopes: Scope[]) => {
     }
 
     // check the user is valid
-    const { user: userByAccessToken }: {user: IUserHasId} = await userId.populate('user');
+    const { user: userByAccessToken }: { user: IUserHasId } =
+      await userId.populate('user');
     if (userByAccessToken == null) {
-      logger.debug('The access token\'s associated user is invalid');
+      logger.debug("The access token's associated user is invalid");
       return;
     }
 
     if (userByAccessToken.readOnly) {
-      logger.debug('The access token\'s associated user is read-only');
+      logger.debug("The access token's associated user is read-only");
       return;
     }
 
@@ -52,6 +56,5 @@ export const parserForAccessToken = (scopes: Scope[]) => {
 
     logger.debug('Access token parsed.');
     return;
-
   };
 };

+ 8 - 12
apps/app/src/server/middlewares/access-token-parser/api-token.integ.ts

@@ -1,4 +1,3 @@
-
 import { faker } from '@faker-js/faker';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import type { Response } from 'express';
@@ -10,15 +9,13 @@ import type UserEvent from '~/server/events/user';
 import { parserForApiToken } from './api-token';
 import type { AccessTokenParserReq } from './interfaces';
 
-
 vi.mock('@growi/core/dist/models/serializers', { spy: true });
 
-
 describe('access-token-parser middleware', () => {
-
+  // biome-ignore lint/suspicious/noImplicitAnyLet: ignore
   let User;
 
-  beforeAll(async() => {
+  beforeAll(async () => {
     const crowiMock = mock<Crowi>({
       event: vi.fn().mockImplementation((eventName) => {
         if (eventName === 'user') {
@@ -32,7 +29,7 @@ describe('access-token-parser middleware', () => {
     User = userModelFactory(crowiMock);
   });
 
-  it('should call next if no access token is provided', async() => {
+  it('should call next if no access token is provided', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -49,7 +46,7 @@ describe('access-token-parser middleware', () => {
     expect(serializeUserSecurely).not.toHaveBeenCalled();
   });
 
-  it('should call next if the given access token is invalid', async() => {
+  it('should call next if the given access token is invalid', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -67,7 +64,7 @@ describe('access-token-parser middleware', () => {
     expect(serializeUserSecurely).not.toHaveBeenCalled();
   });
 
-  it('should set req.user with a valid api token in query', async() => {
+  it('should set req.user with a valid api token in query', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -96,7 +93,7 @@ describe('access-token-parser middleware', () => {
     expect(serializeUserSecurely).toHaveBeenCalledOnce();
   });
 
-  it('should set req.user with a valid api token in body', async() => {
+  it('should set req.user with a valid api token in body', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -124,7 +121,7 @@ describe('access-token-parser middleware', () => {
     expect(serializeUserSecurely).toHaveBeenCalledOnce();
   });
 
-  it('should set req.user with a valid Bearer token in Authorization header', async() => {
+  it('should set req.user with a valid Bearer token in Authorization header', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -155,7 +152,7 @@ describe('access-token-parser middleware', () => {
     expect(serializeUserSecurely).toHaveBeenCalledOnce();
   });
 
-  it('should ignore non-Bearer Authorization header', async() => {
+  it('should ignore non-Bearer Authorization header', async () => {
     // arrange
     const reqMock = mock<AccessTokenParserReq>({
       user: undefined,
@@ -178,5 +175,4 @@ describe('access-token-parser middleware', () => {
     expect(reqMock.user).toBeUndefined();
     expect(serializeUserSecurely).not.toHaveBeenCalled();
   });
-
 });

+ 9 - 4
apps/app/src/server/middlewares/access-token-parser/api-token.ts

@@ -11,14 +11,17 @@ import type { AccessTokenParserReq } from './interfaces';
 
 const logger = loggerFactory('growi:middleware:access-token-parser:api-token');
 
-
-export const parserForApiToken = async(req: AccessTokenParserReq, res: Response): Promise<void> => {
+export const parserForApiToken = async (
+  req: AccessTokenParserReq,
+  res: Response,
+): Promise<void> => {
   // Extract token from Authorization header first
   // It is more efficient to call it only once in "AccessTokenParser," which is the caller of the method
   const bearerToken = extractBearerToken(req.headers.authorization);
 
   // Try all possible token sources in order of priority
-  const accessToken = bearerToken ?? req.query.access_token ?? req.body.access_token;
+  const accessToken =
+    bearerToken ?? req.query.access_token ?? req.body.access_token;
 
   if (accessToken == null || typeof accessToken !== 'string') {
     return;
@@ -26,7 +29,9 @@ export const parserForApiToken = async(req: AccessTokenParserReq, res: Response)
 
   logger.debug('accessToken is', accessToken);
 
-  const User = mongoose.model<HydratedDocument<IUser>, { findUserByApiToken }>('User');
+  const User = mongoose.model<HydratedDocument<IUser>, { findUserByApiToken }>(
+    'User',
+  );
   const userByApiToken: IUserHasId = await User.findUserByApiToken(accessToken);
 
   if (userByApiToken == null) {

+ 3 - 1
apps/app/src/server/middlewares/access-token-parser/extract-bearer-token.ts

@@ -1,4 +1,6 @@
-export const extractBearerToken = (authHeader: string | undefined): string | null => {
+export const extractBearerToken = (
+  authHeader: string | undefined,
+): string | null => {
   if (authHeader == null) {
     return null;
   }

+ 9 - 3
apps/app/src/server/middlewares/access-token-parser/index.ts

@@ -9,11 +9,17 @@ import type { AccessTokenParserReq } from './interfaces';
 
 const logger = loggerFactory('growi:middleware:access-token-parser');
 
-export type AccessTokenParser = (scopes?: Scope[], opts?: {acceptLegacy: boolean})
-  => (req: AccessTokenParserReq, res: Response, next: NextFunction) => Promise<void>
+export type AccessTokenParser = (
+  scopes?: Scope[],
+  opts?: { acceptLegacy: boolean },
+) => (
+  req: AccessTokenParserReq,
+  res: Response,
+  next: NextFunction,
+) => Promise<void>;
 
 export const accessTokenParser: AccessTokenParser = (scopes, opts) => {
-  return async(req, res, next): Promise<void> => {
+  return async (req, res, next): Promise<void> => {
     if (scopes == null || scopes.length === 0) {
       logger.warn('scopes is empty');
       return next();

+ 7 - 6
apps/app/src/server/middlewares/access-token-parser/interfaces.ts

@@ -3,12 +3,13 @@ import type { IUserSerializedSecurely } from '@growi/core/dist/models/serializer
 import type { Request } from 'express';
 
 type ReqQuery = {
-  access_token?: string,
-}
+  access_token?: string;
+};
 type ReqBody = {
-  access_token?: string,
-}
+  access_token?: string;
+};
 
-export interface AccessTokenParserReq extends Request<undefined, undefined, ReqBody, ReqQuery> {
-  user?: IUserSerializedSecurely<IUserHasId>,
+export interface AccessTokenParserReq
+  extends Request<undefined, undefined, ReqBody, ReqQuery> {
+  user?: IUserSerializedSecurely<IUserHasId>;
 }

+ 30 - 26
apps/app/src/server/middlewares/add-activity.ts

@@ -5,36 +5,40 @@ import { SupportedAction } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:middlewares:add-activity');
 
 interface AuthorizedRequest extends Request {
-  user?: IUserHasId
+  user?: IUserHasId;
 }
 
-export const generateAddActivityMiddleware = () => async(req: AuthorizedRequest, res: Response, next: NextFunction): Promise<void> => {
-  if (req.method === 'GET') {
-    logger.warn('This middleware is not available for GET requests');
-    return next();
-  }
+export const generateAddActivityMiddleware =
+  () =>
+  async (
+    req: AuthorizedRequest,
+    res: Response,
+    next: NextFunction,
+  ): Promise<void> => {
+    if (req.method === 'GET') {
+      logger.warn('This middleware is not available for GET requests');
+      return next();
+    }
+
+    const parameter = {
+      ip: req.ip,
+      endpoint: req.originalUrl,
+      action: SupportedAction.ACTION_UNSETTLED,
+      user: req.user?._id,
+      snapshot: {
+        username: req.user?.username,
+      },
+    };
+
+    try {
+      const activity = await Activity.createByParameters(parameter);
+      res.locals.activity = activity;
+    } catch (err) {
+      logger.error('Create activity failed', err);
+    }
 
-  const parameter = {
-    ip:  req.ip,
-    endpoint: req.originalUrl,
-    action: SupportedAction.ACTION_UNSETTLED,
-    user: req.user?._id,
-    snapshot: {
-      username: req.user?.username,
-    },
+    return next();
   };
-
-  try {
-    const activity = await Activity.createByParameters(parameter);
-    res.locals.activity = activity;
-  }
-  catch (err) {
-    logger.error('Create activity failed', err);
-  }
-
-  return next();
-};

+ 2 - 4
apps/app/src/server/middlewares/admin-required.js

@@ -4,10 +4,8 @@ const logger = loggerFactory('growi:middleware:admin-required');
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi, fallback = null) => {
-
-  return function(req, res, next) {
-
-    if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
+  return (req, res, next) => {
+    if (req.user != null && req.user instanceof Object && '_id' in req.user) {
       if (req.user.admin) {
         return next();
       }

+ 2 - 1
apps/app/src/server/middlewares/apiv1-form-validator.ts

@@ -14,7 +14,8 @@ export default (req: Request, res: Response, next: NextFunction): void => {
 
   const errObjArray = validationResult(req);
   if (errObjArray.isEmpty()) {
-    return next();
+    next();
+    return;
   }
 
   const errs = errObjArray.array().map((err) => {

+ 8 - 3
apps/app/src/server/middlewares/apiv3-form-validator.ts

@@ -6,14 +6,19 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:middlewares:ApiV3FormValidator');
 const { validationResult } = require('express-validator');
 
-export const apiV3FormValidator = (req: Request, res: Response & { apiv3Err }, next: NextFunction): void => {
+export const apiV3FormValidator = (
+  req: Request,
+  res: Response & { apiv3Err },
+  next: NextFunction,
+): void => {
   logger.debug('req.query', req.query);
   logger.debug('req.params', req.params);
   logger.debug('req.body', req.body);
 
   const errObjArray = validationResult(req);
   if (errObjArray.isEmpty()) {
-    return next();
+    next();
+    return;
   }
 
   const errs = errObjArray.array().map((err) => {
@@ -21,5 +26,5 @@ export const apiV3FormValidator = (req: Request, res: Response & { apiv3Err }, n
     return new ErrorV3(`${err.param}: ${err.msg}`, 'validation_failed');
   });
 
-  return res.apiv3Err(errs);
+  res.apiv3Err(errs);
 };

+ 3 - 2
apps/app/src/server/middlewares/application-installed.ts

@@ -2,7 +2,7 @@
 module.exports = (crowi) => {
   const { appService } = crowi;
 
-  return async(req, res, next) => {
+  return async (req, res, next) => {
     const isDBInitialized = await appService.isDBInitialized();
 
     // when already installed
@@ -11,7 +11,8 @@ module.exports = (crowi) => {
     }
 
     // when other server have initialized DB
-    const isDBInitializedAfterForceReload = await appService.isDBInitialized(true);
+    const isDBInitializedAfterForceReload =
+      await appService.isDBInitialized(true);
     if (isDBInitializedAfterForceReload) {
       await appService.setupAfterInstall();
       return res.safeRedirect(req.originalUrl);

+ 32 - 14
apps/app/src/server/middlewares/application-not-installed.ts

@@ -6,31 +6,43 @@ import type Crowi from '../crowi';
 /**
  * Middleware factory to check if the application is already installed
  */
-export const generateCheckerMiddleware = (crowi: Crowi) => async(req: Request, res: Response, next: NextFunction): Promise<void> => {
-  const { appService } = crowi;
+export const generateCheckerMiddleware =
+  (crowi: Crowi) =>
+  async (req: Request, res: Response, next: NextFunction): Promise<void> => {
+    const { appService } = crowi;
 
-  const isDBInitialized = await appService.isDBInitialized(true);
+    const isDBInitialized = await appService.isDBInitialized(true);
 
-  if (isDBInitialized) {
-    return next(createError(409, 'Application is already installed'));
-  }
+    if (isDBInitialized) {
+      return next(createError(409, 'Application is already installed'));
+    }
 
-  return next();
-};
+    return next();
+  };
 
 /**
  * Middleware to return HttpError 409 if the application is already installed
  */
-export const allreadyInstalledMiddleware = async(req: Request, res: Response, next: NextFunction): Promise<void> => {
+export const allreadyInstalledMiddleware = async (
+  req: Request,
+  res: Response,
+  next: NextFunction,
+): Promise<void> => {
   return next(createError(409, 'Application is already installed'));
 };
 
 /**
  * Error handler to handle errors as API errors
  */
-export const handleAsApiError = (error: Error, req: Request, res: Response, next: NextFunction): void => {
+export const handleAsApiError = (
+  error: Error,
+  req: Request,
+  res: Response,
+  next: NextFunction,
+): void => {
   if (error == null) {
-    return next();
+    next();
+    return;
   }
 
   if (isHttpError(error)) {
@@ -45,9 +57,15 @@ export const handleAsApiError = (error: Error, req: Request, res: Response, next
 /**
  * Error handler to redirect to top page on error
  */
-export const redirectToTopOnError = (error: Error, req: Request, res: Response, next: NextFunction): void => {
+export const redirectToTopOnError = (
+  error: Error,
+  req: Request,
+  res: Response,
+  next: NextFunction,
+): void => {
   if (error != null) {
-    return res.redirect('/');
+    res.redirect('/');
+    return;
   }
-  return next();
+  next();
 };

+ 4 - 1
apps/app/src/server/middlewares/auto-reconnect-to-s2s-msg-server.js

@@ -3,7 +3,10 @@ module.exports = (crowi) => {
   const { s2sMessagingService } = crowi;
 
   return (req, res, next) => {
-    if (s2sMessagingService != null && s2sMessagingService.shouldResubscribe()) {
+    if (
+      s2sMessagingService != null &&
+      s2sMessagingService.shouldResubscribe()
+    ) {
       s2sMessagingService.subscribe();
     }
 

+ 11 - 5
apps/app/src/server/middlewares/auto-reconnect-to-search.js

@@ -1,6 +1,9 @@
 import loggerFactory from '~/utils/logger';
 
-const { ReconnectContext, nextTick } = require('../service/search-reconnect-context/reconnect-context');
+const {
+  ReconnectContext,
+  nextTick,
+} = require('../service/search-reconnect-context/reconnect-context');
 
 const logger = loggerFactory('growi:middlewares:auto-reconnect-to-search');
 
@@ -9,12 +12,11 @@ module.exports = (crowi) => {
   const { searchService } = crowi;
   const reconnectContext = new ReconnectContext();
 
-  const reconnectHandler = async() => {
+  const reconnectHandler = async () => {
     try {
       logger.info('Auto reconnection is started.');
       await searchService.reconnectClient();
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Auto reconnection failed.', err);
     }
 
@@ -22,7 +24,11 @@ module.exports = (crowi) => {
   };
 
   return (req, res, next) => {
-    if (searchService != null && searchService.isConfigured && !searchService.isReachable) {
+    if (
+      searchService != null &&
+      searchService.isConfigured &&
+      !searchService.isReachable
+    ) {
       // NON-BLOCKING CALL
       // for the latency of the response
       nextTick(reconnectContext, reconnectHandler);

+ 1 - 3
apps/app/src/server/middlewares/certify-brand-logo.ts

@@ -1,10 +1,8 @@
 import type Crowi from '../crowi';
 
 export const generateCertifyBrandLogoMiddleware = (crowi: Crowi) => {
-
-  return async(req, res, next) => {
+  return async (req, res, next) => {
     req.isBrandLogo = true;
     next();
   };
-
 };

+ 13 - 9
apps/app/src/server/middlewares/certify-origin.ts

@@ -4,37 +4,41 @@ import type { NextFunction, Response } from 'express';
 import loggerFactory from '../../utils/logger';
 import { configManager } from '../service/config-manager';
 import isSimpleRequest from '../util/is-simple-request';
-
 import type { AccessTokenParserReq } from './access-token-parser/interfaces';
 
-
 const logger = loggerFactory('growi:middleware:certify-origin');
 
 type Apiv3ErrFunction = (error: ErrorV3) => void;
 
-const certifyOrigin = (req: AccessTokenParserReq, res: Response & { apiv3Err: Apiv3ErrFunction }, next: NextFunction): void => {
-
+const certifyOrigin = (
+  req: AccessTokenParserReq,
+  res: Response & { apiv3Err: Apiv3ErrFunction },
+  next: NextFunction,
+): void => {
   const appSiteUrl = configManager.getConfig('app:siteUrl');
   const configuredOrigin = appSiteUrl ? new URL(appSiteUrl).origin : null;
   const requestOrigin = req.headers.origin;
   const runtimeOrigin = `${req.protocol}://${req.get('host')}`;
 
-  const isSameOriginReq = requestOrigin == null
-  || requestOrigin === configuredOrigin
-  || requestOrigin === runtimeOrigin;
+  const isSameOriginReq =
+    requestOrigin == null ||
+    requestOrigin === configuredOrigin ||
+    requestOrigin === runtimeOrigin;
 
   const accessToken = req.query.access_token ?? req.body.access_token;
 
   if (!isSameOriginReq && req.headers.origin != null && isSimpleRequest(req)) {
     const message = 'Invalid request (origin check failed but simple request)';
     logger.error(message);
-    return res.apiv3Err(new ErrorV3(message));
+    res.apiv3Err(new ErrorV3(message));
+    return;
   }
 
   if (!isSameOriginReq && accessToken == null && !isSimpleRequest(req)) {
     const message = 'Invalid request (origin check failed and no access token)';
     logger.error(message);
-    return res.apiv3Err(new ErrorV3(message));
+    res.apiv3Err(new ErrorV3(message));
+    return;
   }
 
   next();

+ 38 - 19
apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.spec.ts

@@ -3,7 +3,10 @@ import { mock } from 'vitest-mock-extended';
 
 import type { ShareLinkDocument } from '~/server/models/share-link';
 
-import { certifySharedPageAttachmentMiddleware, type RequestToAllowShareLink } from './certify-shared-page-attachment';
+import {
+  certifySharedPageAttachmentMiddleware,
+  type RequestToAllowShareLink,
+} from './certify-shared-page-attachment';
 import type { ValidReferer } from './interfaces';
 
 const mocks = vi.hoisted(() => {
@@ -14,19 +17,22 @@ const mocks = vi.hoisted(() => {
   };
 });
 
-vi.mock('./validate-referer', () => ({ validateReferer: mocks.validateRefererMock }));
-vi.mock('./retrieve-valid-share-link', () => ({ retrieveValidShareLinkByReferer: mocks.retrieveValidShareLinkByRefererMock }));
-vi.mock('./validate-attachment', () => ({ validateAttachment: mocks.validateAttachmentMock }));
-
+vi.mock('./validate-referer', () => ({
+  validateReferer: mocks.validateRefererMock,
+}));
+vi.mock('./retrieve-valid-share-link', () => ({
+  retrieveValidShareLinkByReferer: mocks.retrieveValidShareLinkByRefererMock,
+}));
+vi.mock('./validate-attachment', () => ({
+  validateAttachment: mocks.validateAttachmentMock,
+}));
 
 describe('certifySharedPageAttachmentMiddleware', () => {
-
   const res = mock<Response>();
   const next = vi.fn();
 
   describe('should called next() without req.isSharedPage set', () => {
-
-    it('when the fileId param is null', async() => {
+    it('when the fileId param is null', async () => {
       // setup
       const req = mock<RequestToAllowShareLink>();
       req.params = {}; // id: undefined
@@ -41,7 +47,7 @@ describe('certifySharedPageAttachmentMiddleware', () => {
       expect(next).toHaveBeenCalledOnce();
     });
 
-    it('when validateReferer returns null', async() => {
+    it('when validateReferer returns null', async () => {
       // setup
       const req = mock<RequestToAllowShareLink>();
       req.params = { id: 'file id string' };
@@ -57,7 +63,7 @@ describe('certifySharedPageAttachmentMiddleware', () => {
       expect(next).toHaveBeenCalledOnce();
     });
 
-    it('when retrieveValidShareLinkByReferer returns null', async() => {
+    it('when retrieveValidShareLinkByReferer returns null', async () => {
       // setup
       const req = mock<RequestToAllowShareLink>();
       req.params = { id: 'file id string' };
@@ -78,12 +84,14 @@ describe('certifySharedPageAttachmentMiddleware', () => {
       expect(mocks.validateRefererMock).toHaveBeenCalledOnce();
       expect(mocks.validateRefererMock).toHaveBeenCalledWith('referer string');
       expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledOnce();
-      expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledWith(validReferer);
+      expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledWith(
+        validReferer,
+      );
       expect(req.isSharedPage === true).toBeFalsy();
       expect(next).toHaveBeenCalledOnce();
     });
 
-    it('when validateAttachment returns false', async() => {
+    it('when validateAttachment returns false', async () => {
       // setup
       const req = mock<RequestToAllowShareLink>();
       req.params = { id: 'file id string' };
@@ -93,7 +101,9 @@ describe('certifySharedPageAttachmentMiddleware', () => {
       mocks.validateRefererMock.mockImplementation(() => validReferer);
 
       const shareLinkMock = mock<ShareLinkDocument>();
-      mocks.retrieveValidShareLinkByRefererMock.mockResolvedValue(shareLinkMock);
+      mocks.retrieveValidShareLinkByRefererMock.mockResolvedValue(
+        shareLinkMock,
+      );
 
       mocks.validateAttachmentMock.mockResolvedValue(false);
 
@@ -104,16 +114,20 @@ describe('certifySharedPageAttachmentMiddleware', () => {
       expect(mocks.validateRefererMock).toHaveBeenCalledOnce();
       expect(mocks.validateRefererMock).toHaveBeenCalledWith('referer string');
       expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledOnce();
-      expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledWith(validReferer);
+      expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledWith(
+        validReferer,
+      );
       expect(mocks.validateAttachmentMock).toHaveBeenCalledOnce();
-      expect(mocks.validateAttachmentMock).toHaveBeenCalledWith('file id string', shareLinkMock);
+      expect(mocks.validateAttachmentMock).toHaveBeenCalledWith(
+        'file id string',
+        shareLinkMock,
+      );
       expect(req.isSharedPage === true).toBeFalsy();
       expect(next).toHaveBeenCalledOnce();
     });
-
   });
 
-  it('should set req.isSharedPage true', async() => {
+  it('should set req.isSharedPage true', async () => {
     // setup
     const req = mock<RequestToAllowShareLink>();
     req.params = { id: 'file id string' };
@@ -134,9 +148,14 @@ describe('certifySharedPageAttachmentMiddleware', () => {
     expect(mocks.validateRefererMock).toHaveBeenCalledOnce();
     expect(mocks.validateRefererMock).toHaveBeenCalledWith('referer string');
     expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledOnce();
-    expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledWith(validReferer);
+    expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledWith(
+      validReferer,
+    );
     expect(mocks.validateAttachmentMock).toHaveBeenCalledOnce();
-    expect(mocks.validateAttachmentMock).toHaveBeenCalledWith('file id string', shareLinkMock);
+    expect(mocks.validateAttachmentMock).toHaveBeenCalledWith(
+      'file id string',
+      shareLinkMock,
+    );
 
     expect(req.isSharedPage === true).toBeTruthy();
 

+ 15 - 9
apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.ts

@@ -6,21 +6,24 @@ import { retrieveValidShareLinkByReferer } from './retrieve-valid-share-link';
 import { validateAttachment } from './validate-attachment';
 import { validateReferer } from './validate-referer';
 
-
 const logger = loggerFactory('growi:middleware:certify-shared-page-attachment');
 
-
 export interface RequestToAllowShareLink extends Request {
-  isSharedPage?: boolean,
+  isSharedPage?: boolean;
 }
 
-export const certifySharedPageAttachmentMiddleware = async(req: RequestToAllowShareLink, res: Response, next: NextFunction): Promise<void> => {
-
+export const certifySharedPageAttachmentMiddleware = async (
+  req: RequestToAllowShareLink,
+  res: Response,
+  next: NextFunction,
+): Promise<void> => {
   const fileId: string | undefined = req.params.id;
   const { referer } = req.headers;
 
   if (fileId == null) {
-    logger.error('The param fileId is required. Please confirm to usage of this middleware.');
+    logger.error(
+      'The param fileId is required. Please confirm to usage of this middleware.',
+    );
     return next();
   }
 
@@ -31,16 +34,19 @@ export const certifySharedPageAttachmentMiddleware = async(req: RequestToAllowSh
 
   const shareLink = await retrieveValidShareLinkByReferer(validReferer);
   if (shareLink == null) {
-    logger.warn(`No valid ShareLink document found by the referer (${validReferer.referer}})`);
+    logger.warn(
+      `No valid ShareLink document found by the referer (${validReferer.referer}})`,
+    );
     return next();
   }
 
   if (!(await validateAttachment(fileId, shareLink))) {
-    logger.warn(`No valid ShareLink document found by the fileId (${fileId}) and referer (${validReferer.referer}})`);
+    logger.warn(
+      `No valid ShareLink document found by the fileId (${fileId}) and referer (${validReferer.referer}})`,
+    );
     return next();
   }
 
   req.isSharedPage = true;
   next();
-
 };

+ 2 - 2
apps/app/src/server/middlewares/certify-shared-page-attachment/interfaces.ts

@@ -1,4 +1,4 @@
 export type ValidReferer = {
-  referer: string,
-  shareLinkId: string,
+  referer: string;
+  shareLinkId: string;
 };

+ 19 - 8
apps/app/src/server/middlewares/certify-shared-page-attachment/retrieve-valid-share-link.ts

@@ -1,17 +1,26 @@
-import type { ShareLinkDocument, ShareLinkModel } from '~/server/models/share-link';
+import type {
+  ShareLinkDocument,
+  ShareLinkModel,
+} from '~/server/models/share-link';
 import { getModelSafely } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 import type { ValidReferer } from './interfaces';
 
+const logger = loggerFactory(
+  'growi:middleware:certify-shared-page-attachment:retrieve-valid-share-link',
+);
 
-const logger = loggerFactory('growi:middleware:certify-shared-page-attachment:retrieve-valid-share-link');
-
-
-export const retrieveValidShareLinkByReferer = async(referer: ValidReferer): Promise<ShareLinkDocument | null> => {
-  const ShareLink = getModelSafely<ShareLinkDocument, ShareLinkModel>('ShareLink');
+export const retrieveValidShareLinkByReferer = async (
+  referer: ValidReferer,
+): Promise<ShareLinkDocument | null> => {
+  const ShareLink = getModelSafely<ShareLinkDocument, ShareLinkModel>(
+    'ShareLink',
+  );
   if (ShareLink == null) {
-    logger.warn('Could not get ShareLink model. next() will be called without processing anything.');
+    logger.warn(
+      'Could not get ShareLink model. next() will be called without processing anything.',
+    );
     return null;
   }
 
@@ -20,7 +29,9 @@ export const retrieveValidShareLinkByReferer = async(referer: ValidReferer): Pro
     _id: shareLinkId,
   });
   if (shareLink == null || shareLink.isExpired()) {
-    logger.info(`ShareLink ('${shareLinkId}') is not found or has already expired.`);
+    logger.info(
+      `ShareLink ('${shareLinkId}') is not found or has already expired.`,
+    );
     return null;
   }
 

+ 10 - 5
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-attachment.ts

@@ -4,14 +4,19 @@ import type { ShareLinkDocument } from '~/server/models/share-link';
 import { getModelSafely } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
+const logger = loggerFactory(
+  'growi:middleware:certify-shared-page-attachment:validate-attachment',
+);
 
-const logger = loggerFactory('growi:middleware:certify-shared-page-attachment:validate-attachment');
-
-
-export const validateAttachment = async(fileId: string, shareLink: ShareLinkDocument): Promise<boolean> => {
+export const validateAttachment = async (
+  fileId: string,
+  shareLink: ShareLinkDocument,
+): Promise<boolean> => {
   const Attachment = getModelSafely<IAttachment>('Attachment');
   if (Attachment == null) {
-    logger.warn('Could not get Attachment model. next() will be called without processing anything.');
+    logger.warn(
+      'Could not get Attachment model. next() will be called without processing anything.',
+    );
     return false;
   }
 

+ 0 - 4
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.spec.ts

@@ -12,11 +12,8 @@ vi.mock('~/server/service/config-manager', () => {
   return { configManager: mocks.configManagerMock };
 });
 
-
 describe('retrieveSiteUrl', () => {
-
   describe('returns null', () => {
-
     it('when the siteUrl is not set', () => {
       // setup
       mocks.configManagerMock.getConfig.mockImplementation(() => {
@@ -55,5 +52,4 @@ describe('retrieveSiteUrl', () => {
     // then
     expect(result).toEqual(new URL(siteUrl));
   });
-
 });

+ 7 - 6
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.ts

@@ -1,21 +1,22 @@
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:middlewares:certify-shared-page-attachment:validate-referer:retrieve-site-url');
-
+const logger = loggerFactory(
+  'growi:middlewares:certify-shared-page-attachment:validate-referer:retrieve-site-url',
+);
 
 export const retrieveSiteUrl = (): URL | null => {
   const siteUrlString = configManager.getConfig('app:siteUrl');
   if (siteUrlString == null) {
-    logger.warn("Verification referer does not work because 'Site URL' is NOT set. All of attachments in share link page is invisible.");
+    logger.warn(
+      "Verification referer does not work because 'Site URL' is NOT set. All of attachments in share link page is invisible.",
+    );
     return null;
   }
 
   try {
     return new URL(siteUrlString);
-  }
-  catch (err) {
+  } catch (err) {
     logger.error(`Parsing 'app:siteUrl' ('${siteUrlString}') has failed.`);
     return null;
   }

+ 5 - 8
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.spec.ts

@@ -8,11 +8,11 @@ const mocks = vi.hoisted(() => {
   };
 });
 
-vi.mock('./retrieve-site-url', () => ({ retrieveSiteUrl: mocks.retrieveSiteUrlMock }));
-
+vi.mock('./retrieve-site-url', () => ({
+  retrieveSiteUrl: mocks.retrieveSiteUrlMock,
+}));
 
 describe('validateReferer', () => {
-
   const isValidObjectIdSpy = vi.spyOn(objectIdUtils, 'isValidObjectId');
 
   beforeEach(() => {
@@ -20,7 +20,6 @@ describe('validateReferer', () => {
   });
 
   describe('refurns false', () => {
-
     it('when the referer argument is undefined', () => {
       // setup
 
@@ -98,7 +97,8 @@ describe('validateReferer', () => {
       });
 
       // when
-      const refererString = 'https://example.com/share/FFFFFFFFFFFFFFFFFFFFFFFF';
+      const refererString =
+        'https://example.com/share/FFFFFFFFFFFFFFFFFFFFFFFF';
       const result = validateReferer(refererString);
 
       // then
@@ -106,7 +106,6 @@ describe('validateReferer', () => {
       expect(mocks.retrieveSiteUrlMock).toHaveBeenCalledOnce();
       expect(isValidObjectIdSpy).toHaveBeenCalledOnce();
     });
-
   });
 
   it('returns ValidReferer instance', () => {
@@ -115,7 +114,6 @@ describe('validateReferer', () => {
       return new URL('https://example.com');
     });
 
-
     // when
     const shareLinkId = '65436ba09ae6983bd608b89c';
     const refererString = `https://example.com/share/${shareLinkId}`;
@@ -127,5 +125,4 @@ describe('validateReferer', () => {
       shareLinkId,
     });
   });
-
 });

+ 17 - 10
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.ts

@@ -3,14 +3,15 @@ import { objectIdUtils } from '@growi/core/dist/utils';
 import loggerFactory from '~/utils/logger';
 
 import type { ValidReferer } from '../interfaces';
-
 import { retrieveSiteUrl } from './retrieve-site-url';
 
+const logger = loggerFactory(
+  'growi:middlewares:certify-shared-page-attachment:validate-referer',
+);
 
-const logger = loggerFactory('growi:middlewares:certify-shared-page-attachment:validate-referer');
-
-
-export const validateReferer = (referer: string | undefined): ValidReferer | false => {
+export const validateReferer = (
+  referer: string | undefined,
+): ValidReferer | false => {
   // not null
   if (referer == null) {
     logger.info('The referer string is undefined');
@@ -20,8 +21,7 @@ export const validateReferer = (referer: string | undefined): ValidReferer | fal
   let refererUrl: URL;
   try {
     refererUrl = new URL(referer);
-  }
-  catch (err) {
+  } catch (err) {
     logger.info(`Parsing referer ('${referer}') has failed`);
     return false;
   }
@@ -34,7 +34,10 @@ export const validateReferer = (referer: string | undefined): ValidReferer | fal
   }
 
   // validate hostname and port
-  if (refererUrl.hostname !== siteUrl.hostname || refererUrl.port !== siteUrl.port) {
+  if (
+    refererUrl.hostname !== siteUrl.hostname ||
+    refererUrl.port !== siteUrl.port
+  ) {
     logger.warn('The hostname or port mismatched.', {
       refererUrl: {
         hostname: refererUrl.hostname,
@@ -50,7 +53,9 @@ export const validateReferer = (referer: string | undefined): ValidReferer | fal
 
   // validate pathname
   // https://regex101.com/r/M5Bp6E/1
-  const match = refererUrl.pathname.match(/^\/share\/(?<shareLinkId>[a-f0-9]{24})$/i);
+  const match = refererUrl.pathname.match(
+    /^\/share\/(?<shareLinkId>[a-f0-9]{24})$/i,
+  );
   if (match == null) {
     return false;
   }
@@ -61,7 +66,9 @@ export const validateReferer = (referer: string | undefined): ValidReferer | fal
 
   // validate shareLinkId is an correct ObjectId
   if (!objectIdUtils.isValidObjectId(match.groups.shareLinkId)) {
-    logger.warn(`The shareLinkId ('${match.groups.shareLinkId}') is invalid as an ObjectId.`);
+    logger.warn(
+      `The shareLinkId ('${match.groups.shareLinkId}') is invalid as an ObjectId.`,
+    );
     return false;
   }
 

+ 5 - 4
apps/app/src/server/middlewares/certify-shared-page.js

@@ -5,15 +5,17 @@ const logger = loggerFactory('growi:middleware:certify-shared-page');
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
-
-  return async(req, res, next) => {
+  return async (req, res, next) => {
     const pageId = req.query.pageId || req.body.pageId || null;
     const shareLinkId = req.query.shareLinkId || req.body.shareLinkId || null;
     if (pageId == null || shareLinkId == null) {
       return next();
     }
 
-    const sharelink = await ShareLink.findOne({ _id: shareLinkId, relatedPage: pageId });
+    const sharelink = await ShareLink.findOne({
+      _id: { $eq: shareLinkId },
+      relatedPage: { $eq: pageId },
+    });
 
     // check sharelink enabled
     if (sharelink == null || sharelink.isExpired()) {
@@ -28,5 +30,4 @@ module.exports = (crowi) => {
 
     next();
   };
-
 };

+ 8 - 6
apps/app/src/server/middlewares/exclude-read-only-user.spec.ts

@@ -1,20 +1,22 @@
 import { ErrorV3 } from '@growi/core/dist/models';
+import type { NextFunction, Response } from 'express';
+import type { Request } from 'express-validator/src/base';
 
 import { excludeReadOnlyUser } from './exclude-read-only-user';
 
 describe('excludeReadOnlyUser', () => {
-  let req;
-  let res;
-  let next;
+  let req: Request;
+  let res: Response & { apiv3Err: ReturnType<typeof vi.fn> };
+  let next: NextFunction;
 
   beforeEach(() => {
     req = {
       user: {},
-    };
+    } as unknown as Request;
     res = {
       apiv3Err: vi.fn(),
-    };
-    next = vi.fn();
+    } as unknown as Response & { apiv3Err: ReturnType<typeof vi.fn> };
+    next = vi.fn() as unknown as NextFunction;
   });
 
   test('should call next if user is not found', () => {

+ 23 - 11
apps/app/src/server/middlewares/exclude-read-only-user.ts

@@ -6,44 +6,56 @@ import loggerFactory from '~/utils/logger';
 
 import { configManager } from '../service/config-manager';
 
-
 const logger = loggerFactory('growi:middleware:exclude-read-only-user');
 
-export const excludeReadOnlyUser = (req: Request, res: Response & { apiv3Err }, next: () => NextFunction): NextFunction => {
+export const excludeReadOnlyUser = (
+  req: Request,
+  res: Response & { apiv3Err },
+  next: NextFunction,
+): void => {
   const user = req.user;
 
   if (user == null) {
     logger.warn('req.user is null');
-    return next();
+    next();
+    return;
   }
 
   if (user.readOnly) {
     const message = 'This user is read only user';
     logger.warn(message);
 
-    return res.apiv3Err(new ErrorV3(message, 'validation_failed'));
+    res.apiv3Err(new ErrorV3(message, 'validation_failed'));
+    return;
   }
 
-  return next();
+  next();
 };
 
-export const excludeReadOnlyUserIfCommentNotAllowed = (req: Request, res: Response & { apiv3Err }, next: () => NextFunction): NextFunction => {
+export const excludeReadOnlyUserIfCommentNotAllowed = (
+  req: Request,
+  res: Response & { apiv3Err },
+  next: NextFunction,
+): void => {
   const user = req.user;
 
-  const isRomUserAllowedToComment = configManager.getConfig('security:isRomUserAllowedToComment');
+  const isRomUserAllowedToComment = configManager.getConfig(
+    'security:isRomUserAllowedToComment',
+  );
 
   if (user == null) {
     logger.warn('req.user is null');
-    return next();
+    next();
+    return;
   }
 
   if (user.readOnly && !isRomUserAllowedToComment) {
     const message = 'This user is read only user and comment is not allowed';
     logger.warn(message);
 
-    return res.apiv3Err(new ErrorV3(message, 'validation_failed'));
+    res.apiv3Err(new ErrorV3(message, 'validation_failed'));
+    return;
   }
 
-  return next();
-
+  next();
 };

+ 6 - 9
apps/app/src/server/middlewares/http-error-handler.js

@@ -4,20 +4,17 @@ import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:middleware:htto-error-handler');
 
-module.exports = async(err, req, res, next) => {
+module.exports = async (err, req, res, next) => {
   // handle if the err is a HttpError instance
   if (isHttpError(err)) {
     const httpError = err;
 
     try {
-      return res
-        .status(httpError.status)
-        .send({
-          status: httpError.status,
-          message: httpError.message,
-        });
-    }
-    catch (err) {
+      return res.status(httpError.status).send({
+        status: httpError.status,
+        message: httpError.message,
+      });
+    } catch (err) {
       logger.error('Cannot call res.send() twice:', err);
     }
   }

+ 24 - 10
apps/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts

@@ -10,29 +10,43 @@ import PasswordResetOrder from '../models/password-reset-order';
 const logger = loggerFactory('growi:routes:forgot-password');
 
 export type ReqWithPasswordResetOrder = Request & {
-  passwordResetOrder: IPasswordResetOrder,
+  passwordResetOrder: IPasswordResetOrder;
 };
 
 // eslint-disable-next-line import/no-anonymous-default-export
-export default async(req: ReqWithPasswordResetOrder, res: Response, next: NextFunction): Promise<void> => {
+export default async (
+  req: ReqWithPasswordResetOrder,
+  res: Response,
+  next: NextFunction,
+): Promise<void> => {
   const token: string = req.params.token || req.body.token;
 
   if (token == null) {
     logger.error('Token not found');
-    return next(createError(400, 'Token not found', { code: forgotPasswordErrorCode.TOKEN_NOT_FOUND }));
+    return next(
+      createError(400, 'Token not found', {
+        code: forgotPasswordErrorCode.TOKEN_NOT_FOUND,
+      }),
+    );
   }
 
-  const passwordResetOrder = await PasswordResetOrder.findOne({ token: { $eq: token } });
+  const passwordResetOrder = await PasswordResetOrder.findOne({
+    token: { $eq: token },
+  });
 
   // check if the token is valid
-  if (passwordResetOrder == null || passwordResetOrder.isExpired() || passwordResetOrder.isRevoked) {
+  if (
+    passwordResetOrder == null ||
+    passwordResetOrder.isExpired() ||
+    passwordResetOrder.isRevoked
+  ) {
     const message = 'passwordResetOrder is null or expired or revoked';
     logger.error(message);
-    return next(createError(
-      400,
-      'passwordResetOrder is null or expired or revoked',
-      { code: forgotPasswordErrorCode.PASSWORD_RESET_ORDER_IS_NOT_APPROPRIATE },
-    ));
+    return next(
+      createError(400, 'passwordResetOrder is null or expired or revoked', {
+        code: forgotPasswordErrorCode.PASSWORD_RESET_ORDER_IS_NOT_APPROPRIATE,
+      }),
+    );
   }
 
   req.passwordResetOrder = passwordResetOrder;

+ 26 - 8
apps/app/src/server/middlewares/inject-user-registration-order-by-token-middleware.ts

@@ -1,4 +1,4 @@
-import type { Request, Response, NextFunction } from 'express';
+import type { NextFunction, Request, Response } from 'express';
 import createError from 'http-errors';
 
 import { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
@@ -10,33 +10,51 @@ import UserRegistrationOrder from '../models/user-registration-order';
 const logger = loggerFactory('growi:routes:user-activation');
 
 export type ReqWithUserRegistrationOrder = Request & {
-  userRegistrationOrder: IUserRegistrationOrder
+  userRegistrationOrder: IUserRegistrationOrder;
 };
 
 // eslint-disable-next-line import/no-anonymous-default-export
-export default async(req: ReqWithUserRegistrationOrder, res: Response, next: NextFunction): Promise<void> => {
+export default async (
+  req: ReqWithUserRegistrationOrder,
+  res: Response,
+  next: NextFunction,
+): Promise<void> => {
   const token = req.params.token || req.body.token;
 
   if (token == null) {
     const msg = 'Token not found';
     logger.error(msg);
-    return next(createError(400, msg, { code: UserActivationErrorCode.TOKEN_NOT_FOUND }));
+    return next(
+      createError(400, msg, { code: UserActivationErrorCode.TOKEN_NOT_FOUND }),
+    );
   }
 
   if (typeof token !== 'string') {
     const msg = 'Invalid token format';
     logger.error(msg);
-    return next(createError(400, msg, { code: UserActivationErrorCode.INVALID_TOKEN }));
+    return next(
+      createError(400, msg, { code: UserActivationErrorCode.INVALID_TOKEN }),
+    );
   }
 
   // exec query safely with $eq
-  const userRegistrationOrder = await UserRegistrationOrder.findOne({ token: { $eq: token } });
+  const userRegistrationOrder = await UserRegistrationOrder.findOne({
+    token: { $eq: token },
+  });
 
   // check if the token is valid
-  if (userRegistrationOrder == null || userRegistrationOrder.isExpired() || userRegistrationOrder.isRevoked) {
+  if (
+    userRegistrationOrder == null ||
+    userRegistrationOrder.isExpired() ||
+    userRegistrationOrder.isRevoked
+  ) {
     const msg = 'userRegistrationOrder is null or expired or revoked';
     logger.error(msg);
-    return next(createError(400, msg, { code: UserActivationErrorCode.USER_REGISTRATION_ORDER_IS_NOT_APPROPRIATE }));
+    return next(
+      createError(400, msg, {
+        code: UserActivationErrorCode.USER_REGISTRATION_ORDER_IS_NOT_APPROPRIATE,
+      }),
+    );
   }
 
   req.userRegistrationOrder = userRegistrationOrder;

+ 10 - 5
apps/app/src/server/middlewares/invited-form-validator.ts

@@ -21,23 +21,28 @@ export const invitedRules = (): ValidationChain[] => {
       .matches(/^[\x20-\x7F]*$/)
       .withMessage('message.Password has invalid character')
       .isLength({ min: MININUM_PASSWORD_LENGTH })
-      .withMessage(`message.Password minimum character should be more than ${MININUM_PASSWORD_LENGTH} characters`)
+      .withMessage(
+        `message.Password minimum character should be more than ${MININUM_PASSWORD_LENGTH} characters`,
+      )
       .not()
       .isEmpty()
       .withMessage('message.Password field is required'),
   ];
 };
 
-export const invitedValidation = (req: Request, _res: Response, next: () => NextFunction): any => {
+export const invitedValidation = (
+  req: Request,
+  _res: Response,
+  next: () => NextFunction,
+): any => {
   const form = req.body;
   const errors = validationResult(req);
   const extractedErrors: string[] = [];
 
   if (errors.isEmpty()) {
     Object.assign(form, { isValid: true });
-  }
-  else {
-    errors.array().map(err => extractedErrors.push(err.msg));
+  } else {
+    errors.array().map((err) => extractedErrors.push(err.msg));
     Object.assign(form, { isValid: false, errors: extractedErrors });
   }
 

+ 6 - 3
apps/app/src/server/middlewares/login-form-validator.ts

@@ -1,7 +1,10 @@
-import { body, validationResult, type ValidationChain } from 'express-validator';
+import {
+  body,
+  type ValidationChain,
+  validationResult,
+} from 'express-validator';
 // form rules
 export const loginRules = (): ValidationChain[] => {
-
   return [
     body('loginForm.username')
       .matches(/^[\da-zA-Z\-_.+@]+$/)
@@ -30,7 +33,7 @@ export const loginValidation = (req, res, next): ValidationChain[] => {
   }
 
   const extractedErrors: string[] = [];
-  errors.array().map(err => extractedErrors.push(err.msg));
+  errors.array().map((err) => extractedErrors.push(err.msg));
 
   Object.assign(form, {
     isValid: false,

+ 4 - 6
apps/app/src/server/middlewares/login-required.js

@@ -10,19 +10,18 @@ const logger = loggerFactory('growi:middleware:login-required');
  * @param {function} fallback fallback function which will be triggered when the check cannot be passed
  */
 module.exports = (crowi, isGuestAllowed = false, fallback = null) => {
-
-  return function(req, res, next) {
-
+  return (req, res, next) => {
     const User = crowi.model('User');
 
     // check the user logged in
-    if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
+    if (req.user != null && req.user instanceof Object && '_id' in req.user) {
       if (req.user.status === User.STATUS_ACTIVE) {
         // Active の人だけ先に進める
         return next();
       }
 
-      const redirectTo = createRedirectToForUnauthenticated(req.user.status) ?? '/login';
+      const redirectTo =
+        createRedirectToForUnauthenticated(req.user.status) ?? '/login';
       return res.redirect(redirectTo);
     }
 
@@ -59,5 +58,4 @@ module.exports = (crowi, isGuestAllowed = false, fallback = null) => {
     req.session.redirectTo = req.originalUrl;
     return res.redirect('/login');
   };
-
 };

+ 18 - 4
apps/app/src/server/middlewares/register-form-validator.ts

@@ -1,5 +1,9 @@
 import { ErrorV3 } from '@growi/core/dist/models';
-import { body, validationResult, type ValidationChain } from 'express-validator';
+import {
+  body,
+  type ValidationChain,
+  validationResult,
+} from 'express-validator';
 
 // form rules
 export const registerRules = (minPasswordLength: number): ValidationChain[] => {
@@ -10,7 +14,10 @@ export const registerRules = (minPasswordLength: number): ValidationChain[] => {
       .not()
       .isEmpty()
       .withMessage('message.Username field is required'),
-    body('registerForm.name').not().isEmpty().withMessage('message.Name field is required'),
+    body('registerForm.name')
+      .not()
+      .isEmpty()
+      .withMessage('message.Name field is required'),
     body('registerForm.email')
       .isEmail()
       .withMessage('message.Email format is invalid')
@@ -20,7 +27,14 @@ export const registerRules = (minPasswordLength: number): ValidationChain[] => {
       .matches(/^[\x20-\x7F]*$/)
       .withMessage('message.Password has invalid character')
       .isLength({ min: minPasswordLength })
-      .withMessage(new ErrorV3('message.Password minimum character should be more than n characters', undefined, undefined, { number: minPasswordLength }))
+      .withMessage(
+        new ErrorV3(
+          'message.Password minimum character should be more than n characters',
+          undefined,
+          undefined,
+          { number: minPasswordLength },
+        ),
+      )
       .not()
       .isEmpty()
       .withMessage('message.Password field is required'),
@@ -40,7 +54,7 @@ export const registerValidation = (req, res, next): ValidationChain[] => {
   }
 
   const extractedErrors: string[] = [];
-  errors.array().map(err => extractedErrors.push(err.msg));
+  errors.array().map((err) => extractedErrors.push(err.msg));
 
   Object.assign(form, {
     isValid: false,

+ 18 - 13
apps/app/src/server/middlewares/safe-redirect.spec.ts

@@ -1,12 +1,11 @@
 import type { Request } from 'express';
 
-import registerSafeRedirectFactory, { type ResWithSafeRedirect } from './safe-redirect';
+import registerSafeRedirectFactory, {
+  type ResWithSafeRedirect,
+} from './safe-redirect';
 
 describe('safeRedirect', () => {
-  const whitelistOfHosts = [
-    'white1.example.com:8080',
-    'white2.example.com',
-  ];
+  const whitelistOfHosts = ['white1.example.com:8080', 'white2.example.com'];
   const registerSafeRedirect = registerSafeRedirectFactory(whitelistOfHosts);
 
   describe('res.safeRedirect', () => {
@@ -24,7 +23,7 @@ describe('safeRedirect', () => {
     } as any as ResWithSafeRedirect;
     const next = vi.fn();
 
-    test('redirects to \'/\' because specified url causes open redirect vulnerability', () => {
+    test("redirects to '/' because specified url causes open redirect vulnerability", () => {
       registerSafeRedirect(req, res, next);
 
       res.safeRedirect('//evil.example.com');
@@ -35,7 +34,7 @@ describe('safeRedirect', () => {
       expect(res.redirect).toHaveBeenCalledWith('/');
     });
 
-    test('redirects to \'/\' because specified host without port is not in whitelist', () => {
+    test("redirects to '/' because specified host without port is not in whitelist", () => {
       registerSafeRedirect(req, res, next);
 
       res.safeRedirect('http://white1.example.com/path/to/page');
@@ -54,7 +53,9 @@ describe('safeRedirect', () => {
       expect(next).toHaveBeenCalledTimes(1);
       expect(req.get).toHaveBeenCalledWith('host');
       expect(res.redirect).toHaveBeenCalledTimes(1);
-      expect(res.redirect).toHaveBeenCalledWith('http://example.com/path/to/page');
+      expect(res.redirect).toHaveBeenCalledWith(
+        'http://example.com/path/to/page',
+      );
     });
 
     test('redirects to the specified local url (fqdn)', () => {
@@ -65,7 +66,9 @@ describe('safeRedirect', () => {
       expect(next).toHaveBeenCalledTimes(1);
       expect(req.get).toHaveBeenCalledWith('host');
       expect(res.redirect).toHaveBeenCalledTimes(1);
-      expect(res.redirect).toHaveBeenCalledWith('http://example.com/path/to/page');
+      expect(res.redirect).toHaveBeenCalledWith(
+        'http://example.com/path/to/page',
+      );
     });
 
     test('redirects to the specified whitelisted url (white1.example.com:8080)', () => {
@@ -76,7 +79,9 @@ describe('safeRedirect', () => {
       expect(next).toHaveBeenCalledTimes(1);
       expect(req.get).toHaveBeenCalledWith('host');
       expect(res.redirect).toHaveBeenCalledTimes(1);
-      expect(res.redirect).toHaveBeenCalledWith('http://white1.example.com:8080/path/to/page');
+      expect(res.redirect).toHaveBeenCalledWith(
+        'http://white1.example.com:8080/path/to/page',
+      );
     });
 
     test('redirects to the specified whitelisted url (white2.example.com:8080)', () => {
@@ -87,9 +92,9 @@ describe('safeRedirect', () => {
       expect(next).toHaveBeenCalledTimes(1);
       expect(req.get).toHaveBeenCalledWith('host');
       expect(res.redirect).toHaveBeenCalledTimes(1);
-      expect(res.redirect).toHaveBeenCalledWith('http://white2.example.com:8080/path/to/page');
+      expect(res.redirect).toHaveBeenCalledWith(
+        'http://white2.example.com:8080/path/to/page',
+      );
     });
-
   });
-
 });

+ 29 - 21
apps/app/src/server/middlewares/safe-redirect.ts

@@ -4,9 +4,7 @@
  * Usage: app.use(require('middlewares/safe-redirect')(['example.com', 'some.example.com:8080']))
  */
 
-import type {
-  Request, Response, NextFunction,
-} from 'express';
+import type { NextFunction, Request, Response } from 'express';
 
 import loggerFactory from '~/utils/logger';
 
@@ -15,39 +13,44 @@ const logger = loggerFactory('growi:middleware:safe-redirect');
 /**
  * Check whether the redirect url host is in specified whitelist
  */
-function isInWhitelist(whitelistOfHosts: string[], redirectToFqdn: string): boolean {
+function isInWhitelist(
+  whitelistOfHosts: string[],
+  redirectToFqdn: string,
+): boolean {
   if (whitelistOfHosts == null || whitelistOfHosts.length === 0) {
     return false;
   }
 
   try {
     const redirectUrl = new URL(redirectToFqdn);
-    return whitelistOfHosts.includes(redirectUrl.hostname) || whitelistOfHosts.includes(redirectUrl.host);
-  }
-  catch (err) {
+    return (
+      whitelistOfHosts.includes(redirectUrl.hostname) ||
+      whitelistOfHosts.includes(redirectUrl.host)
+    );
+  } catch (err) {
     logger.warn(err);
     return false;
   }
 }
 
-
 export type ResWithSafeRedirect = Response & {
-  safeRedirect: (redirectTo?: string) => void,
-}
+  safeRedirect: (redirectTo?: string) => void;
+};
 
 const factory = (whitelistOfHosts: string[]) => {
-
   return (req: Request, res: ResWithSafeRedirect, next: NextFunction): void => {
-
     // extend res object
-    res.safeRedirect = function(redirectTo?: string) {
+    res.safeRedirect = (redirectTo?: string) => {
       if (redirectTo == null) {
         return res.redirect('/');
       }
 
       try {
         // check inner redirect
-        const redirectUrl = new URL(redirectTo, `${req.protocol}://${req.get('host')}`);
+        const redirectUrl = new URL(
+          redirectTo,
+          `${req.protocol}://${req.get('host')}`,
+        );
         if (redirectUrl.hostname === req.hostname) {
           logger.debug(`Requested redirect URL (${redirectTo}) is local.`);
           return res.redirect(redirectUrl.href);
@@ -57,23 +60,28 @@ const factory = (whitelistOfHosts: string[]) => {
         // check whitelisted redirect
         const isWhitelisted = isInWhitelist(whitelistOfHosts, redirectTo);
         if (isWhitelisted) {
-          logger.debug(`Requested redirect URL (${redirectTo}) is in whitelist.`, `whitelist=${whitelistOfHosts}`);
+          logger.debug(
+            `Requested redirect URL (${redirectTo}) is in whitelist.`,
+            `whitelist=${whitelistOfHosts}`,
+          );
           return res.redirect(redirectTo);
         }
-        logger.debug(`Requested redirect URL (${redirectTo}) is NOT in whitelist.`, `whitelist=${whitelistOfHosts}`);
-      }
-      catch (err) {
+        logger.debug(
+          `Requested redirect URL (${redirectTo}) is NOT in whitelist.`,
+          `whitelist=${whitelistOfHosts}`,
+        );
+      } catch (err) {
         logger.warn(`Requested redirect URL (${redirectTo}) is invalid.`, err);
       }
 
-      logger.warn(`Requested redirect URL (${redirectTo}) is UNSAFE, redirecting to root page.`);
+      logger.warn(
+        `Requested redirect URL (${redirectTo}) is UNSAFE, redirecting to root page.`,
+      );
       return res.redirect('/');
     };
 
     next();
-
   };
-
 };
 
 export default factory;

+ 18 - 8
apps/app/src/server/middlewares/unavailable-when-maintenance-mode.ts

@@ -4,16 +4,24 @@ import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../crowi';
 
-const logger = loggerFactory('growi:middlewares:unavailable-when-maintenance-mode');
+const logger = loggerFactory(
+  'growi:middlewares:unavailable-when-maintenance-mode',
+);
 
 type CrowiReq = Request & {
-  crowi: Crowi,
-}
+  crowi: Crowi;
+};
 
-type IMiddleware = (req: CrowiReq, res: Response, next: NextFunction) => Promise<void>;
+type IMiddleware = (
+  req: CrowiReq,
+  res: Response,
+  next: NextFunction,
+) => Promise<void>;
 
-export const generateUnavailableWhenMaintenanceModeMiddleware = (crowi: Crowi): IMiddleware => {
-  return async(req, res, next) => {
+export const generateUnavailableWhenMaintenanceModeMiddleware = (
+  crowi: Crowi,
+): IMiddleware => {
+  return async (req, res, next) => {
     const isMaintenanceMode = crowi.appService.isMaintenanceMode();
 
     if (!isMaintenanceMode) {
@@ -27,8 +35,10 @@ export const generateUnavailableWhenMaintenanceModeMiddleware = (crowi: Crowi):
   };
 };
 
-export const generateUnavailableWhenMaintenanceModeMiddlewareForApi = (crowi: Crowi): IMiddleware => {
-  return async(req, res, next) => {
+export const generateUnavailableWhenMaintenanceModeMiddlewareForApi = (
+  crowi: Crowi,
+): IMiddleware => {
+  return async (req, res, next) => {
     const isMaintenanceMode = crowi.appService.isMaintenanceMode();
 
     if (!isMaintenanceMode) {

+ 0 - 1
biome.json

@@ -29,7 +29,6 @@
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/specs",
       "!apps/app/src/client",
-      "!apps/app/src/server/middlewares",
       "!apps/app/src/server/routes",
       "!apps/app/src/server/service"
     ]