diff --git a/apps/app/.eslintrc.js b/apps/app/.eslintrc.js index 48a45b97148..9711bf3f71b 100644 --- a/apps/app/.eslintrc.js +++ b/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 diff --git a/apps/app/src/server/middlewares/access-token-parser/access-token.integ.ts b/apps/app/src/server/middlewares/access-token-parser/access-token.integ.ts index 11afa8a79b8..a608548a633 100644 --- a/apps/app/src/server/middlewares/access-token-parser/access-token.integ.ts +++ b/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({ 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({ 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({ 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({ 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({ 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({ 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({ 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(); }); - }); diff --git a/apps/app/src/server/middlewares/access-token-parser/access-token.ts b/apps/app/src/server/middlewares/access-token-parser/access-token.ts index 2b10d216e47..24e59d79889 100644 --- a/apps/app/src/server/middlewares/access-token-parser/access-token.ts +++ b/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 => { + return async (req: AccessTokenParserReq, res: Response): Promise => { // 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; - }; }; diff --git a/apps/app/src/server/middlewares/access-token-parser/api-token.integ.ts b/apps/app/src/server/middlewares/access-token-parser/api-token.integ.ts index 2e3e94e882b..003291e04e2 100644 --- a/apps/app/src/server/middlewares/access-token-parser/api-token.integ.ts +++ b/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({ 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({ 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({ 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({ 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({ 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({ 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({ user: undefined, @@ -178,5 +175,4 @@ describe('access-token-parser middleware', () => { expect(reqMock.user).toBeUndefined(); expect(serializeUserSecurely).not.toHaveBeenCalled(); }); - }); diff --git a/apps/app/src/server/middlewares/access-token-parser/api-token.ts b/apps/app/src/server/middlewares/access-token-parser/api-token.ts index fd1e78f0b17..136aaca01bc 100644 --- a/apps/app/src/server/middlewares/access-token-parser/api-token.ts +++ b/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 => { +export const parserForApiToken = async ( + req: AccessTokenParserReq, + res: Response, +): Promise => { // 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, { findUserByApiToken }>('User'); + const User = mongoose.model, { findUserByApiToken }>( + 'User', + ); const userByApiToken: IUserHasId = await User.findUserByApiToken(accessToken); if (userByApiToken == null) { diff --git a/apps/app/src/server/middlewares/access-token-parser/extract-bearer-token.ts b/apps/app/src/server/middlewares/access-token-parser/extract-bearer-token.ts index 35cf77b76b1..72cf2de1495 100644 --- a/apps/app/src/server/middlewares/access-token-parser/extract-bearer-token.ts +++ b/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; } diff --git a/apps/app/src/server/middlewares/access-token-parser/index.ts b/apps/app/src/server/middlewares/access-token-parser/index.ts index a35956ed72a..0862439a4ff 100644 --- a/apps/app/src/server/middlewares/access-token-parser/index.ts +++ b/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 +export type AccessTokenParser = ( + scopes?: Scope[], + opts?: { acceptLegacy: boolean }, +) => ( + req: AccessTokenParserReq, + res: Response, + next: NextFunction, +) => Promise; export const accessTokenParser: AccessTokenParser = (scopes, opts) => { - return async(req, res, next): Promise => { + return async (req, res, next): Promise => { if (scopes == null || scopes.length === 0) { logger.warn('scopes is empty'); return next(); diff --git a/apps/app/src/server/middlewares/access-token-parser/interfaces.ts b/apps/app/src/server/middlewares/access-token-parser/interfaces.ts index 16b5154350e..10468d8081d 100644 --- a/apps/app/src/server/middlewares/access-token-parser/interfaces.ts +++ b/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 { - user?: IUserSerializedSecurely, +export interface AccessTokenParserReq + extends Request { + user?: IUserSerializedSecurely; } diff --git a/apps/app/src/server/middlewares/add-activity.ts b/apps/app/src/server/middlewares/add-activity.ts index 48b18067c37..c4e72292452 100644 --- a/apps/app/src/server/middlewares/add-activity.ts +++ b/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 => { - 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 => { + 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(); -}; diff --git a/apps/app/src/server/middlewares/admin-required.js b/apps/app/src/server/middlewares/admin-required.js index 9a646bea10d..a2e93db1550 100644 --- a/apps/app/src/server/middlewares/admin-required.js +++ b/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(); } diff --git a/apps/app/src/server/middlewares/apiv1-form-validator.ts b/apps/app/src/server/middlewares/apiv1-form-validator.ts index 482258286d2..0e15539dfe9 100644 --- a/apps/app/src/server/middlewares/apiv1-form-validator.ts +++ b/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) => { diff --git a/apps/app/src/server/middlewares/apiv3-form-validator.ts b/apps/app/src/server/middlewares/apiv3-form-validator.ts index 8ea216daf22..8c657a40d83 100644 --- a/apps/app/src/server/middlewares/apiv3-form-validator.ts +++ b/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); }; diff --git a/apps/app/src/server/middlewares/application-installed.ts b/apps/app/src/server/middlewares/application-installed.ts index bab4c0c2e64..2f4451d59b3 100644 --- a/apps/app/src/server/middlewares/application-installed.ts +++ b/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); diff --git a/apps/app/src/server/middlewares/application-not-installed.ts b/apps/app/src/server/middlewares/application-not-installed.ts index a382a63d7bb..904d01405a3 100644 --- a/apps/app/src/server/middlewares/application-not-installed.ts +++ b/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 => { - const { appService } = crowi; +export const generateCheckerMiddleware = + (crowi: Crowi) => + async (req: Request, res: Response, next: NextFunction): Promise => { + 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 => { +export const allreadyInstalledMiddleware = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { 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(); }; diff --git a/apps/app/src/server/middlewares/auto-reconnect-to-s2s-msg-server.js b/apps/app/src/server/middlewares/auto-reconnect-to-s2s-msg-server.js index 14239207296..1c90ccb156d 100644 --- a/apps/app/src/server/middlewares/auto-reconnect-to-s2s-msg-server.js +++ b/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(); } diff --git a/apps/app/src/server/middlewares/auto-reconnect-to-search.js b/apps/app/src/server/middlewares/auto-reconnect-to-search.js index 9a8c0f5ac52..3993d89945c 100644 --- a/apps/app/src/server/middlewares/auto-reconnect-to-search.js +++ b/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); diff --git a/apps/app/src/server/middlewares/certify-brand-logo.ts b/apps/app/src/server/middlewares/certify-brand-logo.ts index d8d816500aa..954a756653c 100644 --- a/apps/app/src/server/middlewares/certify-brand-logo.ts +++ b/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(); }; - }; diff --git a/apps/app/src/server/middlewares/certify-origin.ts b/apps/app/src/server/middlewares/certify-origin.ts index 12e7ffefe26..0a1d99c2725 100644 --- a/apps/app/src/server/middlewares/certify-origin.ts +++ b/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(); diff --git a/apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.spec.ts b/apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.spec.ts index afe65d24e2c..fa0e03492f1 100644 --- a/apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.spec.ts +++ b/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(); 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(); 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(); 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(); 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(); req.params = { id: 'file id string' }; @@ -93,7 +101,9 @@ describe('certifySharedPageAttachmentMiddleware', () => { mocks.validateRefererMock.mockImplementation(() => validReferer); const shareLinkMock = mock(); - 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(); 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(); diff --git a/apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.ts b/apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.ts index bf3a6a1de34..f4af014f42a 100644 --- a/apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.ts +++ b/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 => { - +export const certifySharedPageAttachmentMiddleware = async ( + req: RequestToAllowShareLink, + res: Response, + next: NextFunction, +): Promise => { 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(); - }; diff --git a/apps/app/src/server/middlewares/certify-shared-page-attachment/interfaces.ts b/apps/app/src/server/middlewares/certify-shared-page-attachment/interfaces.ts index ee64bf92094..e83f94368a8 100644 --- a/apps/app/src/server/middlewares/certify-shared-page-attachment/interfaces.ts +++ b/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; }; diff --git a/apps/app/src/server/middlewares/certify-shared-page-attachment/retrieve-valid-share-link.ts b/apps/app/src/server/middlewares/certify-shared-page-attachment/retrieve-valid-share-link.ts index 6e8bed9e515..1c0eb1445eb 100644 --- a/apps/app/src/server/middlewares/certify-shared-page-attachment/retrieve-valid-share-link.ts +++ b/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 => { - const ShareLink = getModelSafely('ShareLink'); +export const retrieveValidShareLinkByReferer = async ( + referer: ValidReferer, +): Promise => { + const ShareLink = getModelSafely( + '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; } diff --git a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-attachment.ts b/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-attachment.ts index 675558e5c28..0434755bd5b 100644 --- a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-attachment.ts +++ b/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 => { +export const validateAttachment = async ( + fileId: string, + shareLink: ShareLinkDocument, +): Promise => { const Attachment = getModelSafely('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; } diff --git a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.spec.ts b/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.spec.ts index 944576db745..9ee7a8a61ca 100644 --- a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.spec.ts +++ b/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)); }); - }); diff --git a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.ts b/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.ts index d620e027862..7e9c8b430c2 100644 --- a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.ts +++ b/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; } diff --git a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.spec.ts b/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.spec.ts index ea4dbef4c94..a8ec842e5e9 100644 --- a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.spec.ts +++ b/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, }); }); - }); diff --git a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.ts b/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.ts index abf527bf20b..9b92fda8416 100644 --- a/apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.ts +++ b/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\/(?[a-f0-9]{24})$/i); + const match = refererUrl.pathname.match( + /^\/share\/(?[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; } diff --git a/apps/app/src/server/middlewares/certify-shared-page.js b/apps/app/src/server/middlewares/certify-shared-page.js index 0457d7799f2..957356be9b5 100644 --- a/apps/app/src/server/middlewares/certify-shared-page.js +++ b/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(); }; - }; diff --git a/apps/app/src/server/middlewares/exclude-read-only-user.spec.ts b/apps/app/src/server/middlewares/exclude-read-only-user.spec.ts index 8d11e4a6c82..aa97560c5df 100644 --- a/apps/app/src/server/middlewares/exclude-read-only-user.spec.ts +++ b/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 }; + let next: NextFunction; beforeEach(() => { req = { user: {}, - }; + } as unknown as Request; res = { apiv3Err: vi.fn(), - }; - next = vi.fn(); + } as unknown as Response & { apiv3Err: ReturnType }; + next = vi.fn() as unknown as NextFunction; }); test('should call next if user is not found', () => { diff --git a/apps/app/src/server/middlewares/exclude-read-only-user.ts b/apps/app/src/server/middlewares/exclude-read-only-user.ts index f543a125f74..d3cb5603dd3 100644 --- a/apps/app/src/server/middlewares/exclude-read-only-user.ts +++ b/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(); }; diff --git a/apps/app/src/server/middlewares/http-error-handler.js b/apps/app/src/server/middlewares/http-error-handler.js index 5cc95d361dc..aab35c55d38 100644 --- a/apps/app/src/server/middlewares/http-error-handler.js +++ b/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); } } diff --git a/apps/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts b/apps/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts index 5536fe5b788..eff732db37c 100644 --- a/apps/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts +++ b/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 => { +export default async ( + req: ReqWithPasswordResetOrder, + res: Response, + next: NextFunction, +): Promise => { 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; diff --git a/apps/app/src/server/middlewares/inject-user-registration-order-by-token-middleware.ts b/apps/app/src/server/middlewares/inject-user-registration-order-by-token-middleware.ts index 19e9abc91de..5a478eb9b53 100644 --- a/apps/app/src/server/middlewares/inject-user-registration-order-by-token-middleware.ts +++ b/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 => { +export default async ( + req: ReqWithUserRegistrationOrder, + res: Response, + next: NextFunction, +): Promise => { 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; diff --git a/apps/app/src/server/middlewares/invited-form-validator.ts b/apps/app/src/server/middlewares/invited-form-validator.ts index 7e16039bfe2..0b13ce3f04b 100644 --- a/apps/app/src/server/middlewares/invited-form-validator.ts +++ b/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 }); } diff --git a/apps/app/src/server/middlewares/login-form-validator.ts b/apps/app/src/server/middlewares/login-form-validator.ts index 9f28f58e8ec..3a1898186b8 100644 --- a/apps/app/src/server/middlewares/login-form-validator.ts +++ b/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, diff --git a/apps/app/src/server/middlewares/login-required.js b/apps/app/src/server/middlewares/login-required.js index 8e0730a8d38..0ab00201414 100644 --- a/apps/app/src/server/middlewares/login-required.js +++ b/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'); }; - }; diff --git a/apps/app/src/server/middlewares/register-form-validator.ts b/apps/app/src/server/middlewares/register-form-validator.ts index 283a2483049..31e9e89482a 100644 --- a/apps/app/src/server/middlewares/register-form-validator.ts +++ b/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, diff --git a/apps/app/src/server/middlewares/safe-redirect.spec.ts b/apps/app/src/server/middlewares/safe-redirect.spec.ts index 7bd7777e895..5d397f8c8f1 100644 --- a/apps/app/src/server/middlewares/safe-redirect.spec.ts +++ b/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', + ); }); - }); - }); diff --git a/apps/app/src/server/middlewares/safe-redirect.ts b/apps/app/src/server/middlewares/safe-redirect.ts index 0c7d61b18f4..05b3e803e4a 100644 --- a/apps/app/src/server/middlewares/safe-redirect.ts +++ b/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; diff --git a/apps/app/src/server/middlewares/unavailable-when-maintenance-mode.ts b/apps/app/src/server/middlewares/unavailable-when-maintenance-mode.ts index c313a8d2d6d..ba3cb227e75 100644 --- a/apps/app/src/server/middlewares/unavailable-when-maintenance-mode.ts +++ b/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; +type IMiddleware = ( + req: CrowiReq, + res: Response, + next: NextFunction, +) => Promise; -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) { diff --git a/biome.json b/biome.json index 233d0823e38..edadbabf0bd 100644 --- a/biome.json +++ b/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" ]