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

Merge branch 'master' into feat/page-bulk-export

Futa Arai 1 год назад
Родитель
Сommit
812f0b8dd3
70 измененных файлов с 421 добавлено и 313 удалено
  1. 3 1
      .github/workflows/reusable-app-prod.yml
  2. 1 1
      apps/app/package.json
  3. 1 1
      apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx
  4. 5 5
      apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group-relation.ts
  5. 2 2
      apps/app/src/interfaces/crowi-request.ts
  6. 2 1
      apps/app/src/server/middlewares/access-token-parser.js
  7. 1 1
      apps/app/src/server/models/attachment.ts
  8. 1 1
      apps/app/src/server/models/serializers/bookmark-serializer.js
  9. 0 1
      apps/app/src/server/models/serializers/index.ts
  10. 1 1
      apps/app/src/server/models/serializers/page-serializer.js
  11. 1 1
      apps/app/src/server/models/serializers/revision-serializer.js
  12. 1 1
      apps/app/src/server/models/serializers/user-group-relation-serializer.js
  13. 0 35
      apps/app/src/server/models/serializers/user-serializer.js
  14. 1 2
      apps/app/src/server/models/user.js
  15. 12 10
      apps/app/src/server/routes/apiv3/activity.ts
  16. 4 5
      apps/app/src/server/routes/apiv3/attachment.js
  17. 2 1
      apps/app/src/server/routes/apiv3/bookmarks.js
  18. 1 1
      apps/app/src/server/routes/apiv3/customize-setting.js
  19. 1 1
      apps/app/src/server/routes/apiv3/forgot-password.js
  20. 8 5
      apps/app/src/server/routes/apiv3/g2g-transfer.ts
  21. 31 17
      apps/app/src/server/routes/apiv3/in-app-notification.ts
  22. 9 4
      apps/app/src/server/routes/apiv3/invited.ts
  23. 1 1
      apps/app/src/server/routes/apiv3/notification-setting.js
  24. 4 4
      apps/app/src/server/routes/apiv3/page-listing.ts
  25. 2 3
      apps/app/src/server/routes/apiv3/page/create-page.ts
  26. 3 3
      apps/app/src/server/routes/apiv3/page/update-page.ts
  27. 2 2
      apps/app/src/server/routes/apiv3/pages/index.js
  28. 2 3
      apps/app/src/server/routes/apiv3/revisions.js
  29. 2 3
      apps/app/src/server/routes/apiv3/user-group-relation.js
  30. 6 9
      apps/app/src/server/routes/apiv3/user-group.js
  31. 7 9
      apps/app/src/server/routes/apiv3/users.js
  32. 1 3
      apps/app/src/server/routes/attachment/api.js
  33. 1 1
      apps/app/src/server/routes/attachment/get-brand-logo.ts
  34. 1 1
      apps/app/src/server/routes/attachment/get.ts
  35. 9 7
      apps/app/src/server/routes/comment.js
  36. 4 3
      apps/app/src/server/routes/ogp.ts
  37. 1 1
      apps/app/src/server/routes/page.js
  38. 5 3
      apps/app/src/server/service/activity.ts
  39. 1 1
      apps/app/src/server/service/attachment.js
  40. 1 1
      apps/app/src/server/service/file-uploader/aws/index.ts
  41. 1 1
      apps/app/src/server/service/file-uploader/azure.ts
  42. 1 1
      apps/app/src/server/service/file-uploader/file-uploader.ts
  43. 1 1
      apps/app/src/server/service/file-uploader/gcs/index.ts
  44. 1 1
      apps/app/src/server/service/file-uploader/gridfs.ts
  45. 1 1
      apps/app/src/server/service/file-uploader/local.ts
  46. 1 1
      apps/app/src/server/service/file-uploader/utils/headers.ts
  47. 6 3
      apps/app/src/server/service/g2g-transfer.ts
  48. 1 1
      apps/app/src/server/service/global-notification/global-notification-mail.js
  49. 1 1
      apps/app/src/server/service/global-notification/global-notification-slack.js
  50. 5 5
      apps/app/src/server/service/page/index.ts
  51. 3 4
      apps/app/src/server/service/pre-notify.ts
  52. 6 12
      apps/app/src/server/service/search-delegator/private-legacy-pages.ts
  53. 1 1
      apps/app/src/server/service/search.ts
  54. 1 1
      apps/app/src/server/service/slack-command-handler/keep.js
  55. 1 2
      apps/app/src/server/service/slack-command-handler/note.js
  56. 1 2
      apps/app/src/server/service/slack-command-handler/togetter.js
  57. 1 1
      package.json
  58. 6 0
      packages/core/CHANGELOG.md
  59. 5 1
      packages/core/package.json
  60. 11 1
      packages/core/src/interfaces/common.ts
  61. 40 0
      packages/core/src/models/serializers/attachment-serializer.ts
  62. 2 0
      packages/core/src/models/serializers/index.ts
  63. 37 0
      packages/core/src/models/serializers/user-serializer.ts
  64. 3 3
      packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TextFormatTools.tsx
  65. 26 12
      packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/Toolbar.tsx
  66. 3 1
      packages/remark-attachment-refs/package.json
  67. 5 1
      packages/remark-attachment-refs/src/client/components/RefsImg.tsx
  68. 10 5
      packages/remark-attachment-refs/src/client/stores/refs.tsx
  69. 78 75
      packages/remark-attachment-refs/src/server/routes/refs.ts
  70. 21 19
      yarn.lock

+ 3 - 1
.github/workflows/reusable-app-prod.yml

@@ -348,7 +348,9 @@ jobs:
 
     runs-on: ubuntu-latest
     container:
-      image: mcr.microsoft.com/playwright:latest
+      # Match the Playwright version
+      # https://github.com/microsoft/playwright/issues/20010
+      image: mcr.microsoft.com/playwright:v1.46.0-jammy
 
     strategy:
       fail-fast: false

+ 1 - 1
apps/app/package.json

@@ -210,7 +210,7 @@
     "usehooks-ts": "^2.6.0",
     "validator": "^13.7.0",
     "ws": "^8.17.1",
-    "xss": "^1.0.14",
+    "xss": "^1.0.15",
     "y-mongodb-provider": "^0.2.0",
     "y-socket.io": "^1.1.3",
     "yjs": "^13.6.18"

+ 1 - 1
apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx

@@ -15,7 +15,7 @@ type Props = {
   activityList: IActivityHasId[]
 }
 
-const formatDate = (date) => {
+const formatDate = (date: Date): string => {
   return format(new Date(date), 'yyyy/MM/dd HH:mm:ss');
 };
 

+ 5 - 5
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group-relation.ts

@@ -1,19 +1,19 @@
 import { ErrorV3 } from '@growi/core/dist/models';
-import { Router, Request } from 'express';
+import type { Router, Request } from 'express';
 
-import { IExternalUserGroupRelationHasId } from '~/features/external-user-group/interfaces/external-user-group';
+import type { IExternalUserGroupRelationHasId } from '~/features/external-user-group/interfaces/external-user-group';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
-import Crowi from '~/server/crowi';
+import type Crowi from '~/server/crowi';
+import { serializeUserGroupRelationSecurely } from '~/server/models/serializers/user-group-relation-serializer';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import loggerFactory from '~/utils/logger';
 
-import { ApiV3Response } from '../../../../../server/routes/apiv3/interfaces/apiv3-response';
 
 const logger = loggerFactory('growi:routes:apiv3:user-group-relation'); // eslint-disable-line no-unused-vars
 
 const express = require('express');
 const { query } = require('express-validator');
 
-const { serializeUserGroupRelationSecurely } = require('~/server/models/serializers/user-group-relation-serializer');
 
 const router = express.Router();
 

+ 2 - 2
apps/app/src/interfaces/crowi-request.ts

@@ -1,11 +1,11 @@
 import type { IUser } from '@growi/core';
 import type { Request } from 'express';
-import type { Document } from 'mongoose';
+import type { HydratedDocument } from 'mongoose';
 
 
 export interface CrowiProperties {
 
-  user?: IUser & Document,
+  user?: HydratedDocument<IUser>,
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   crowi: any,

+ 2 - 1
apps/app/src/server/middlewares/access-token-parser.js

@@ -1,6 +1,7 @@
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+
 import loggerFactory from '~/utils/logger';
 
-const { serializeUserSecurely } = require('../models/serializers/user-serializer');
 
 const logger = loggerFactory('growi:middleware:access-token-parser');
 

+ 1 - 1
apps/app/src/server/models/attachment.ts

@@ -3,7 +3,7 @@ import path from 'path';
 import type { IAttachment } from '@growi/core';
 import { addSeconds } from 'date-fns/addSeconds';
 import {
-  Schema, type Model, type Document, Types,
+  Schema, type Model, type Document,
 } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';

+ 1 - 1
apps/app/src/server/models/serializers/bookmark-serializer.js

@@ -1,4 +1,4 @@
-const { serializePageSecurely } = require('./page-serializer');
+import { serializePageSecurely } from './page-serializer';
 
 function serializeInsecurePageAttributes(bookmark) {
   if (bookmark.page != null && bookmark.page._id != null) {

+ 0 - 1
apps/app/src/server/models/serializers/index.ts

@@ -2,4 +2,3 @@ export * from './bookmark-serializer';
 export * from './page-serializer';
 export * from './revision-serializer';
 export * from './user-group-relation-serializer';
-export * from './user-serializer';

+ 1 - 1
apps/app/src/server/models/serializers/page-serializer.js

@@ -1,4 +1,4 @@
-const { serializeUserSecurely } = require('./user-serializer');
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
 function depopulate(page, attributeName) {
   // revert the ObjectID

+ 1 - 1
apps/app/src/server/models/serializers/revision-serializer.js

@@ -1,4 +1,4 @@
-const { serializeUserSecurely } = require('./user-serializer');
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
 function serializeInsecureUserAttributes(revision) {
   if (revision.author != null && revision.author._id != null) {

+ 1 - 1
apps/app/src/server/models/serializers/user-group-relation-serializer.js

@@ -1,4 +1,4 @@
-const { serializeUserSecurely } = require('./user-serializer');
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
 function serializeInsecureUserAttributes(userGroupRelation) {
   if (userGroupRelation.relatedUser != null && userGroupRelation.relatedUser._id != null) {

+ 0 - 35
apps/app/src/server/models/serializers/user-serializer.js

@@ -1,35 +0,0 @@
-const mongoose = require('mongoose');
-
-
-export function omitInsecureAttributes(user) {
-  // omit password
-  delete user.password;
-  // omit apiToken
-  delete user.apiToken;
-
-  // omit email
-  if (!user.isEmailPublished) {
-    delete user.email;
-  }
-  return user;
-}
-
-export function serializeUserSecurely(user) {
-  const User = mongoose.model('User');
-
-  // return when it is not a user object
-  if (user == null || !(user instanceof User)) {
-    return user;
-  }
-
-  let serialized = user;
-
-  // invoke toObject if page is a model instance
-  if (user.toObject != null) {
-    serialized = user.toObject();
-  }
-
-  omitInsecureAttributes(serialized);
-
-  return serialized;
-}

+ 1 - 2
apps/app/src/server/models/user.js

@@ -1,4 +1,5 @@
 /* eslint-disable no-use-before-define */
+import { omitInsecureAttributes } from '@growi/core/dist/models/serializers';
 import { pagePathUtils } from '@growi/core/dist/utils';
 
 import { i18n } from '^/config/next-i18next.config';
@@ -15,8 +16,6 @@ const mongoose = require('mongoose');
 const mongoosePaginate = require('mongoose-paginate-v2');
 const uniqueValidator = require('mongoose-unique-validator');
 
-const { omitInsecureAttributes } = require('./serializers/user-serializer');
-
 const logger = loggerFactory('growi:models:user');
 
 module.exports = function(crowi) {

+ 12 - 10
apps/app/src/server/routes/apiv3/activity.ts

@@ -1,16 +1,17 @@
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { parseISO, addMinutes, isValid } from 'date-fns';
-import express, { Request, Router } from 'express';
+import type { Request, Router } from 'express';
+import express from 'express';
 import { query } from 'express-validator';
 
-import { IActivity, ISearchFilter } from '~/interfaces/activity';
+import type { IActivity, ISearchFilter } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import loggerFactory from '~/utils/logger';
 
-import Crowi from '../../crowi';
+import type Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
-import { serializeUserSecurely } from '../../models/serializers/user-serializer';
 
-import { ApiV3Response } from './interfaces/apiv3-response';
+import type { ApiV3Response } from './interfaces/apiv3-response';
 
 
 const logger = loggerFactory('growi:routes:apiv3:activity');
@@ -96,6 +97,7 @@ module.exports = (crowi: Crowi): Router => {
       const paginateResult = await Activity.paginate(
         query,
         {
+          lean: true,
           limit,
           offset,
           sort: { createdAt: -1 },
@@ -103,12 +105,12 @@ module.exports = (crowi: Crowi): Router => {
         },
       );
 
-      const User = crowi.model('User');
       const serializedDocs = paginateResult.docs.map((doc: IActivity) => {
-        if (doc.user != null && doc.user instanceof User) {
-          doc.user = serializeUserSecurely(doc.user);
-        }
-        return doc;
+        const { user, ...rest } = doc;
+        return {
+          user: serializeUserSecurely(user),
+          ...rest,
+        };
       });
 
       const serializedPaginationResult = {

+ 4 - 5
apps/app/src/server/routes/apiv3/attachment.js

@@ -1,10 +1,13 @@
 import { ErrorV3 } from '@growi/core/dist/models';
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+import express from 'express';
 import multer from 'multer';
 import autoReap from 'multer-autoreap';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
-import { Attachment } from '~/server/models';
+import { Attachment } from '~/server/models/attachment';
+import { serializePageSecurely, serializeRevisionSecurely } from '~/server/models/serializers';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
@@ -14,16 +17,12 @@ import { excludeReadOnlyUser } from '../../middlewares/exclude-read-only-user';
 
 
 const logger = loggerFactory('growi:routes:apiv3:attachment'); // eslint-disable-line no-unused-vars
-const express = require('express');
 
 const router = express.Router();
 const {
   query, param, body,
 } = require('express-validator');
 
-const { serializePageSecurely } = require('../../models/serializers/page-serializer');
-const { serializeRevisionSecurely } = require('../../models/serializers/revision-serializer');
-const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 /**
  * @swagger

+ 2 - 1
apps/app/src/server/routes/apiv3/bookmarks.js

@@ -1,3 +1,5 @@
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { serializeBookmarkSecurely } from '~/server/models/serializers/bookmark-serializer';
@@ -12,7 +14,6 @@ const logger = loggerFactory('growi:routes:apiv3:bookmarks'); // eslint-disable-
 const express = require('express');
 const { body, query, param } = require('express-validator');
 
-const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 const router = express.Router();
 

+ 1 - 1
apps/app/src/server/routes/apiv3/customize-setting.js

@@ -9,7 +9,7 @@ import multer from 'multer';
 import { GrowiPlugin } from '~/features/growi-plugin/server/models';
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
-import { Attachment } from '~/server/models';
+import { Attachment } from '~/server/models/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';

+ 1 - 1
apps/app/src/server/routes/apiv3/forgot-password.js

@@ -1,4 +1,5 @@
 import { ErrorV3 } from '@growi/core/dist/models';
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { format, subSeconds } from 'date-fns';
 
 import { SupportedAction } from '~/interfaces/activity';
@@ -17,7 +18,6 @@ const logger = loggerFactory('growi:routes:apiv3:forgotPassword'); // eslint-dis
 const express = require('express');
 const { body } = require('express-validator');
 
-const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 const router = express.Router();
 

+ 8 - 5
apps/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -2,21 +2,24 @@ import { createReadStream } from 'fs';
 import path from 'path';
 
 import { ErrorV3 } from '@growi/core/dist/models';
-import express, { NextFunction, Request, Router } from 'express';
+import type { NextFunction, Request, Router } from 'express';
+import express from 'express';
 import { body } from 'express-validator';
 import multer from 'multer';
 
 import { isG2GTransferError } from '~/server/models/vo/g2g-transfer-error';
+import { configManager } from '~/server/service/config-manager';
 import { exportService } from '~/server/service/export';
-import { IDataGROWIInfo, X_GROWI_TRANSFER_KEY_HEADER_NAME } from '~/server/service/g2g-transfer';
+import type { IDataGROWIInfo } from '~/server/service/g2g-transfer';
+import { X_GROWI_TRANSFER_KEY_HEADER_NAME } from '~/server/service/g2g-transfer';
 import loggerFactory from '~/utils/logger';
 import { TransferKey } from '~/utils/vo/transfer-key';
 
 
-import Crowi from '../../crowi';
+import type Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
-import { ApiV3Response } from './interfaces/apiv3-response';
+import type { ApiV3Response } from './interfaces/apiv3-response';
 
 interface AuthorizedRequest extends Request {
   user?: any
@@ -38,7 +41,7 @@ const validator = {
 module.exports = (crowi: Crowi): Router => {
   const {
     g2gTransferPusherService, g2gTransferReceiverService, importService,
-    growiBridgeService, configManager,
+    growiBridgeService,
   } = crowi;
   if (g2gTransferPusherService == null || g2gTransferReceiverService == null || exportService == null || importService == null
     || growiBridgeService == null || configManager == null) {

+ 31 - 17
apps/app/src/server/routes/apiv3/in-app-notification.ts

@@ -1,12 +1,14 @@
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+import express from 'express';
+
 import { SupportedAction } from '~/interfaces/activity';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 
+import type { IInAppNotification } from '../../../interfaces/in-app-notification';
 
-import { IInAppNotification } from '../../../interfaces/in-app-notification';
-
-const express = require('express');
+import type { ApiV3Response } from './interfaces/apiv3-response';
 
-const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 const router = express.Router();
 
@@ -22,14 +24,18 @@ module.exports = (crowi) => {
 
   const activityEvent = crowi.event('activity');
 
-  router.get('/list', accessTokenParser, loginRequiredStrictly, async(req, res) => {
-    const user = req.user;
+  router.get('/list', accessTokenParser, loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
+    // user must be set by loginRequiredStrictly
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const user = req.user!;
 
-    const limit = parseInt(req.query.limit) || 10;
+    const limit = req.query.limit != null
+      ? parseInt(req.query.limit.toString()) || 10
+      : 10;
 
     let offset = 0;
-    if (req.query.offset) {
-      offset = parseInt(req.query.offset, 10);
+    if (req.query.offset != null) {
+      offset = parseInt(req.query.offset.toString(), 10);
     }
 
     const queryOptions = {
@@ -73,10 +79,13 @@ module.exports = (crowi) => {
     return res.apiv3(serializedPaginationResult);
   });
 
-  router.get('/status', accessTokenParser, loginRequiredStrictly, async(req, res) => {
-    const userId = req.user._id;
+  router.get('/status', accessTokenParser, loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
+    // user must be set by loginRequiredStrictly
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const user = req.user!;
+
     try {
-      const count = await inAppNotificationService.getUnreadCountByUser(userId);
+      const count = await inAppNotificationService.getUnreadCountByUser(user._id);
       return res.apiv3({ count });
     }
     catch (err) {
@@ -84,7 +93,7 @@ module.exports = (crowi) => {
     }
   });
 
-  router.post('/read', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+  router.post('/read', accessTokenParser, loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
     const user = req.user;
 
     try {
@@ -96,8 +105,11 @@ module.exports = (crowi) => {
     }
   });
 
-  router.post('/open', accessTokenParser, loginRequiredStrictly, async(req, res) => {
-    const user = req.user;
+  router.post('/open', accessTokenParser, loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
+    // user must be set by loginRequiredStrictly
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const user = req.user!;
+
     const id = req.body.id;
 
     try {
@@ -110,8 +122,10 @@ module.exports = (crowi) => {
     }
   });
 
-  router.put('/all-statuses-open', accessTokenParser, loginRequiredStrictly, addActivity, async(req, res) => {
-    const user = req.user;
+  router.put('/all-statuses-open', accessTokenParser, loginRequiredStrictly, addActivity, async(req: CrowiRequest, res: ApiV3Response) => {
+    // user must be set by loginRequiredStrictly
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const user = req.user!;
 
     try {
       await inAppNotificationService.updateAllNotificationsAsOpened(user);

+ 9 - 4
apps/app/src/server/routes/apiv3/invited.ts

@@ -1,16 +1,18 @@
-import express, { Request, Router } from 'express';
+import type { IUser } from '@growi/core';
+import type { Request, Router } from 'express';
+import express from 'express';
+import mongoose from 'mongoose';
 
-import Crowi from '../../crowi';
+import type Crowi from '../../crowi';
 import { invitedRules, invitedValidation } from '../../middlewares/invited-form-validator';
 
-import { ApiV3Response } from './interfaces/apiv3-response';
+import type { ApiV3Response } from './interfaces/apiv3-response';
 
 type InvitedFormRequest = Request & { form: any, user: any };
 
 module.exports = (crowi: Crowi): Router => {
   const applicationInstalled = require('../../middlewares/application-installed')(crowi);
   const debug = require('debug')('growi:routes:login');
-  const User = crowi.model('User');
   const router = express.Router();
 
   router.post('/', applicationInstalled, invitedRules(), invitedValidation, async(req: InvitedFormRequest, res: ApiV3Response) => {
@@ -22,6 +24,9 @@ module.exports = (crowi: Crowi): Router => {
       return res.apiv3Err(req.form.errors, 400);
     }
 
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const User = mongoose.model<IUser, any>('User');
+
     const user = req.user;
     const invitedForm = req.form.invitedForm || {};
     const username = invitedForm.username;

+ 1 - 1
apps/app/src/server/routes/apiv3/notification-setting.js

@@ -1,7 +1,7 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 
 import { SupportedAction } from '~/interfaces/activity';
-import { GlobalNotificationSettingType } from '~/server/models';
+import { GlobalNotificationSettingType } from '~/server/models/GlobalNotificationSetting';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 

+ 4 - 4
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -1,5 +1,5 @@
 import type {
-  IPageInfoForListing, IPageInfo,
+  IPageInfoForListing, IPageInfo, IPage,
 } from '@growi/core';
 import { getIdForRef, isIPageInfoForEntity } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
@@ -9,7 +9,6 @@ import { query, oneOf } from 'express-validator';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 
-
 import type { IPageGrantService } from '~/server/service/page-grant';
 import loggerFactory from '~/utils/logger';
 
@@ -66,7 +65,7 @@ const routerFactory = (crowi: Crowi): Router => {
 
 
   router.get('/root', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const Page: PageModel = crowi.model('Page');
+    const Page = mongoose.model<IPage, PageModel>('Page');
 
     let rootPage;
     try {
@@ -124,7 +123,8 @@ const routerFactory = (crowi: Crowi): Router => {
     const attachShortBody: boolean = attachShortBodyParam === 'true';
 
     const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
-    const Bookmark = crowi.model('Bookmark');
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const Bookmark = mongoose.model<any, any>('Bookmark');
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     const pageService = crowi.pageService;
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion

+ 2 - 3
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -16,11 +16,10 @@ import { subscribeRuleNames } from '~/interfaces/in-app-notification';
 import type { IOptionsForCreate } from '~/interfaces/page';
 import type Crowi from '~/server/crowi';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
-import {
-  GlobalNotificationSettingEvent, serializePageSecurely, serializeRevisionSecurely,
-} from '~/server/models';
+import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import type { PageDocument, PageModel } from '~/server/models/page';
 import PageTagRelation from '~/server/models/page-tag-relation';
+import { serializePageSecurely, serializeRevisionSecurely } from '~/server/models/serializers';
 import { configManager } from '~/server/service/config-manager';
 import { getTranslation } from '~/server/service/i18next';
 import loggerFactory from '~/utils/logger';

+ 3 - 3
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -3,6 +3,7 @@ import type {
   IPage, IRevisionHasId, IUserHasId,
 } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { body } from 'express-validator';
@@ -13,10 +14,9 @@ import { type IApiv3PageUpdateParams, PageUpdateErrorCode } from '~/interfaces/a
 import type { IOptionsForUpdate } from '~/interfaces/page';
 import type Crowi from '~/server/crowi';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
-import {
-  GlobalNotificationSettingEvent, serializePageSecurely, serializeRevisionSecurely, serializeUserSecurely,
-} from '~/server/models';
+import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import type { PageDocument, PageModel } from '~/server/models/page';
+import { serializePageSecurely, serializeRevisionSecurely } from '~/server/models/serializers';
 import { preNotifyService } from '~/server/service/pre-notify';
 import { normalizeLatestRevisionIfBroken } from '~/server/service/revision/normalize-latest-revision-if-broken';
 import { getYjsService } from '~/server/service/yjs';

+ 2 - 2
apps/app/src/server/routes/apiv3/pages/index.js

@@ -1,6 +1,7 @@
 
 import { PageGrant } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { isCreatablePage, isTrashPage, isUserPage } from '@growi/core/dist/utils/page-path-utils';
 import { normalizePath, addHeadingSlash } from '@growi/core/dist/utils/path-utils';
 import express from 'express';
@@ -8,7 +9,7 @@ import { body, query } from 'express-validator';
 
 import { SupportedTargetModel, SupportedAction } from '~/interfaces/activity';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
-import { GlobalNotificationSettingEvent } from '~/server/models';
+import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import PageTagRelation from '~/server/models/page-tag-relation';
 import { preNotifyService } from '~/server/service/pre-notify';
 import loggerFactory from '~/utils/logger';
@@ -17,7 +18,6 @@ import { generateAddActivityMiddleware } from '../../../middlewares/add-activity
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import { excludeReadOnlyUser } from '../../../middlewares/exclude-read-only-user';
 import { serializePageSecurely } from '../../../models/serializers/page-serializer';
-import { serializeUserSecurely } from '../../../models/serializers/user-serializer';
 import { isV5ConversionError } from '../../../models/vo/v5-conversion-error';
 
 

+ 2 - 3
apps/app/src/server/routes/apiv3/revisions.js

@@ -1,4 +1,6 @@
 import { ErrorV3 } from '@growi/core/dist/models';
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+import express from 'express';
 
 import { Revision } from '~/server/models/revision';
 import { normalizeLatestRevisionIfBroken } from '~/server/service/revision/normalize-latest-revision-if-broken';
@@ -8,11 +10,8 @@ import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
 const logger = loggerFactory('growi:routes:apiv3:pages');
 
-const express = require('express');
 const { query, param } = require('express-validator');
 
-const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
-
 const router = express.Router();
 
 /**

+ 2 - 3
apps/app/src/server/routes/apiv3/user-group-relation.js

@@ -1,15 +1,14 @@
 import { ErrorV3 } from '@growi/core/dist/models';
+import express from 'express';
 
+import { serializeUserGroupRelationSecurely } from '~/server/models/serializers';
 import UserGroupRelation from '~/server/models/user-group-relation';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:user-group-relation'); // eslint-disable-line no-unused-vars
 
-const express = require('express');
 const { query } = require('express-validator');
 
-const { serializeUserGroupRelationSecurely } = require('../../models/serializers/user-group-relation-serializer');
-
 const router = express.Router();
 
 const validator = {};

+ 6 - 9
apps/app/src/server/routes/apiv3/user-group.js

@@ -1,12 +1,17 @@
 import { GroupType } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
-
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+import express from 'express';
+import {
+  body, param, query, sanitizeQuery,
+} from 'express-validator';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { serializeUserGroupRelationSecurely } from '~/server/models/serializers/user-group-relation-serializer';
 import UserGroup from '~/server/models/user-group';
 import UserGroupRelation from '~/server/models/user-group-relation';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
+import { toPagingLimit, toPagingOffset } from '~/server/util/express-validator/sanitizer';
 import { generalXssFilter } from '~/services/general-xss-filter';
 import loggerFactory from '~/utils/logger';
 
@@ -16,16 +21,8 @@ import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
 const logger = loggerFactory('growi:routes:apiv3:user-group'); // eslint-disable-line no-unused-vars
 
-const express = require('express');
-
 const router = express.Router();
 
-const { body, param, query } = require('express-validator');
-const { sanitizeQuery } = require('express-validator');
-
-const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
-const { toPagingLimit, toPagingOffset } = require('../../util/express-validator/sanitizer');
-
 
 /**
  * @swagger

+ 7 - 9
apps/app/src/server/routes/apiv3/users.js

@@ -1,11 +1,18 @@
 
+import path from 'path';
+
 import { ErrorV3 } from '@growi/core/dist/models';
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
+import express from 'express';
+import { body, query } from 'express-validator';
+import { isEmail } from 'validator';
 
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { SupportedAction } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import ExternalAccount from '~/server/models/external-account';
+import { serializePageSecurely } from '~/server/models/serializers';
 import UserGroupRelation from '~/server/models/user-group-relation';
 import { configManager } from '~/server/service/config-manager';
 import { deleteCompletelyUserHomeBySystem } from '~/server/service/page/delete-completely-user-home-by-system';
@@ -16,17 +23,8 @@ import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
 const logger = loggerFactory('growi:routes:apiv3:users');
 
-const path = require('path');
-
-const express = require('express');
-
 const router = express.Router();
 
-const { body, query } = require('express-validator');
-const { isEmail } = require('validator');
-
-const { serializePageSecurely } = require('../../models/serializers/page-serializer');
-const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 const PAGE_ITEMS = 50;
 

+ 1 - 3
apps/app/src/server/routes/attachment/api.js

@@ -1,11 +1,9 @@
-import { isCreatablePage, isUserPage } from '@growi/core/dist/utils/page-path-utils';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
 
-import { Attachment, serializePageSecurely, serializeRevisionSecurely } from '../../models';
-
+import { Attachment } from '../../models/attachment';
 /* eslint-disable no-use-before-define */
 
 

+ 1 - 1
apps/app/src/server/routes/attachment/get-brand-logo.ts

@@ -9,7 +9,7 @@ import loggerFactory from '~/utils/logger';
 import type Crowi from '../../crowi';
 import { AttachmentType } from '../../interfaces/attachment';
 import { generateCertifyBrandLogoMiddleware } from '../../middlewares/certify-brand-logo';
-import { Attachment } from '../../models';
+import { Attachment } from '../../models/attachment';
 import ApiResponse from '../../util/apiResponse';
 
 import { getActionFactory } from './get';

+ 1 - 1
apps/app/src/server/routes/attachment/get.ts

@@ -17,7 +17,7 @@ import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../../crowi';
 import { certifySharedPageAttachmentMiddleware } from '../../middlewares/certify-shared-page-attachment';
-import { Attachment, type IAttachmentDocument } from '../../models';
+import { Attachment, type IAttachmentDocument } from '../../models/attachment';
 import ApiResponse from '../../util/apiResponse';
 
 

+ 9 - 7
apps/app/src/server/routes/comment.js

@@ -1,9 +1,11 @@
 
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+
 import { Comment, CommentEvent, commentEvent } from '~/features/comment/server';
 import { SupportedAction, SupportedTargetModel, SupportedEventModel } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
 
-import { GlobalNotificationSettingEvent } from '../models';
+import { GlobalNotificationSettingEvent } from '../models/GlobalNotificationSetting';
 import { preNotifyService } from '../service/pre-notify';
 
 /**
@@ -12,7 +14,6 @@ import { preNotifyService } from '../service/pre-notify';
  *    name: Comments
  */
 
-const { serializeUserSecurely } = require('../models/serializers/user-serializer');
 
 /**
  * @swagger
@@ -274,6 +275,7 @@ module.exports = function(crowi, app) {
       action: SupportedAction.ACTION_COMMENT_CREATE,
     };
 
+    /** @type {import('../service/pre-notify').GetAdditionalTargetUsers} */
     const getAdditionalTargetUsers = async(activity) => {
       const mentionedUsers = await crowi.commentService.getMentionedUsers(activity.event);
 
@@ -366,9 +368,9 @@ module.exports = function(crowi, app) {
   api.update = async function(req, res) {
     const { commentForm } = req.body;
 
-    const commentStr = commentForm.comment;
-    const commentId = commentForm.comment_id;
-    const revision = commentForm.revision_id;
+    const commentStr = commentForm?.comment;
+    const commentId = commentForm?.comment_id;
+    const revision = commentForm?.revision_id;
 
     if (commentStr === '') {
       return res.json(ApiResponse.error('Comment text is required'));
@@ -392,7 +394,7 @@ module.exports = function(crowi, app) {
       if (!isAccessible) {
         throw new Error('Current user is not accessible to this page.');
       }
-      if (req.user.id !== comment.creator.toString()) {
+      if (req.user._id.toString() !== comment.creator.toString()) {
         throw new Error('Current user is not operatable to this comment.');
       }
 
@@ -475,7 +477,7 @@ module.exports = function(crowi, app) {
       if (!isAccessible) {
         throw new Error('Current user is not accessible to this page.');
       }
-      if (req.user.id !== comment.creator.toString()) {
+      if (req.user._id !== comment.creator.toString()) {
         throw new Error('Current user is not operatable to this comment.');
       }
 

+ 4 - 3
apps/app/src/server/routes/ogp.ts

@@ -4,15 +4,16 @@ import path from 'path';
 import { DevidedPagePath } from '@growi/core/dist/models';
 // eslint-disable-next-line no-restricted-imports
 import axios from 'axios';
-import {
+import type {
   Request, Response, NextFunction,
 } from 'express';
-import { param, validationResult, ValidationError } from 'express-validator';
+import type { ValidationError } from 'express-validator';
+import { param, validationResult } from 'express-validator';
 
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 
-import { Attachment } from '../models';
+import { Attachment } from '../models/attachment';
 import { convertStreamToBuffer } from '../util/stream';
 
 const logger = loggerFactory('growi:routes:ogp');

+ 1 - 1
apps/app/src/server/routes/page.js

@@ -3,7 +3,7 @@ import mongoose from 'mongoose';
 
 import loggerFactory from '~/utils/logger';
 
-import { GlobalNotificationSettingEvent } from '../models';
+import { GlobalNotificationSettingEvent } from '../models/GlobalNotificationSetting';
 import { PathAlreadyExistsError } from '../models/errors';
 import PageTagRelation from '../models/page-tag-relation';
 import UpdatePost from '../models/update-post';

+ 5 - 3
apps/app/src/server/service/activity.ts

@@ -1,14 +1,16 @@
 import type { IPage } from '@growi/core';
 import mongoose from 'mongoose';
 
+import type { IActivity, SupportedActionType } from '~/interfaces/activity';
 import {
-  IActivity, SupportedActionType, AllSupportedActions, ActionGroupSize,
+  AllSupportedActions, ActionGroupSize,
   AllEssentialActions, AllSmallGroupActions, AllMediumGroupActions, AllLargeGroupActions,
 } from '~/interfaces/activity';
-import Activity, { ActivityDocument } from '~/server/models/activity';
+import type { ActivityDocument } from '~/server/models/activity';
+import Activity from '~/server/models/activity';
 
 import loggerFactory from '../../utils/logger';
-import Crowi from '../crowi';
+import type Crowi from '../crowi';
 
 
 import type { GeneratePreNotify, GetAdditionalTargetUsers } from './pre-notify';

+ 1 - 1
apps/app/src/server/service/attachment.js

@@ -1,7 +1,7 @@
 import loggerFactory from '~/utils/logger';
 
 import { AttachmentType } from '../interfaces/attachment';
-import { Attachment } from '../models';
+import { Attachment } from '../models/attachment';
 
 const fs = require('fs');
 

+ 1 - 1
apps/app/src/server/service/file-uploader/aws/index.ts

@@ -18,7 +18,7 @@ import urljoin from 'url-join';
 import {
   AttachmentType, FilePathOnStoragePrefix, ResponseMode, type RespondOptions,
 } from '~/server/interfaces/attachment';
-import type { IAttachmentDocument } from '~/server/models';
+import type { IAttachmentDocument } from '~/server/models/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { configManager } from '../../config-manager';

+ 1 - 1
apps/app/src/server/service/file-uploader/azure.ts

@@ -19,7 +19,7 @@ import {
 } from '@azure/storage-blob';
 
 import { FilePathOnStoragePrefix, ResponseMode, type RespondOptions } from '~/server/interfaces/attachment';
-import type { IAttachmentDocument } from '~/server/models';
+import type { IAttachmentDocument } from '~/server/models/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { configManager } from '../config-manager';

+ 1 - 1
apps/app/src/server/service/file-uploader/file-uploader.ts

@@ -5,7 +5,7 @@ import type { Response } from 'express';
 
 import type { ICheckLimitResult } from '~/interfaces/attachment';
 import { type RespondOptions, ResponseMode } from '~/server/interfaces/attachment';
-import { Attachment, type IAttachmentDocument } from '~/server/models';
+import { Attachment, type IAttachmentDocument } from '~/server/models/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { configManager } from '../config-manager';

+ 1 - 1
apps/app/src/server/service/file-uploader/gcs/index.ts

@@ -8,7 +8,7 @@ import type Crowi from '~/server/crowi';
 import {
   AttachmentType, FilePathOnStoragePrefix, ResponseMode, type RespondOptions,
 } from '~/server/interfaces/attachment';
-import type { IAttachmentDocument } from '~/server/models';
+import type { IAttachmentDocument } from '~/server/models/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { configManager } from '../../config-manager';

+ 1 - 1
apps/app/src/server/service/file-uploader/gridfs.ts

@@ -6,7 +6,7 @@ import mongoose from 'mongoose';
 import { createModel } from 'mongoose-gridfs';
 
 import type { RespondOptions } from '~/server/interfaces/attachment';
-import type { IAttachmentDocument } from '~/server/models';
+import type { IAttachmentDocument } from '~/server/models/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { configManager } from '../config-manager';

+ 1 - 1
apps/app/src/server/service/file-uploader/local.ts

@@ -4,7 +4,7 @@ import { Readable } from 'stream';
 import type { Response } from 'express';
 
 import { FilePathOnStoragePrefix, ResponseMode, type RespondOptions } from '~/server/interfaces/attachment';
-import type { IAttachmentDocument } from '~/server/models';
+import type { IAttachmentDocument } from '~/server/models/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { configManager } from '../config-manager';

+ 1 - 1
apps/app/src/server/service/file-uploader/utils/headers.ts

@@ -1,7 +1,7 @@
 import type { Response } from 'express';
 
 import type { ExpressHttpHeader, IContentHeaders } from '~/server/interfaces/attachment';
-import type { IAttachmentDocument } from '~/server/models';
+import type { IAttachmentDocument } from '~/server/models/attachment';
 
 
 export class ContentHeaders implements IContentHeaders {

+ 6 - 3
apps/app/src/server/service/g2g-transfer.ts

@@ -4,9 +4,10 @@ import { basename } from 'path';
 import type { Readable } from 'stream';
 
 // eslint-disable-next-line no-restricted-imports
+import type { IUser } from '@growi/core';
 import rawAxios, { type AxiosRequestConfig } from 'axios';
 import FormData from 'form-data';
-import { Types as MongooseTypes } from 'mongoose';
+import mongoose, { Types as MongooseTypes } from 'mongoose';
 
 import { G2G_PROGRESS_STATUS } from '~/interfaces/g2g-transfer';
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
@@ -19,7 +20,7 @@ import loggerFactory from '~/utils/logger';
 import { TransferKey } from '~/utils/vo/transfer-key';
 
 import type Crowi from '../crowi';
-import { Attachment } from '../models';
+import { Attachment } from '../models/attachment';
 import { G2GTransferError, G2GTransferErrorCode } from '../models/vo/g2g-transfer-error';
 
 import { configManager } from './config-manager';
@@ -258,7 +259,9 @@ export class G2GTransferPusherService implements Pusher {
       };
     }
 
-    const activeUserCount = await this.crowi.model('User').countActiveUsers();
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const User = mongoose.model<IUser, any>('User');
+    const activeUserCount = await User.countActiveUsers();
     if ((destGROWIInfo.userUpperLimit ?? Infinity) < activeUserCount) {
       return {
         canTransfer: false,

+ 1 - 1
apps/app/src/server/service/global-notification/global-notification-mail.js

@@ -1,6 +1,6 @@
 import nodePath from 'path';
 
-import { GlobalNotificationSettingEvent, GlobalNotificationSettingType } from '~/server/models';
+import { GlobalNotificationSettingEvent, GlobalNotificationSettingType } from '~/server/models/GlobalNotificationSetting';
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/server/service/global-notification/global-notification-slack.js

@@ -1,6 +1,6 @@
 import { pagePathUtils } from '@growi/core/dist/utils';
 
-import { GlobalNotificationSettingEvent, GlobalNotificationSettingType } from '~/server/models';
+import { GlobalNotificationSettingEvent, GlobalNotificationSettingType } from '~/server/models/GlobalNotificationSetting';
 import loggerFactory from '~/utils/logger';
 
 import {

+ 5 - 5
apps/app/src/server/service/page/index.ts

@@ -711,7 +711,7 @@ class PageService implements IPageService {
       await this.renameDescendantsWithStream(page, newPagePath, user, options, false, descendantsSubscribedSets);
       const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
 
-      const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+      const preNotify = preNotifyService.generatePreNotify(activity, async() => { return descendantsSubscribedUsers });
 
       this.activityEvent.emit('updated', activity, page, preNotify);
     }
@@ -1702,7 +1702,7 @@ class PageService implements IPageService {
 
     const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
 
-    const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+    const preNotify = preNotifyService.generatePreNotify(activity, async() => { return descendantsSubscribedUsers });
 
     this.activityEvent.emit('updated', activity, page, preNotify);
 
@@ -2021,7 +2021,7 @@ class PageService implements IPageService {
     await this.deleteCompletelyDescendantsWithStream(page, user, options, false, descendantsSubscribedSets);
     const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
 
-    const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+    const preNotify = preNotifyService.generatePreNotify(activity, async() => { return descendantsSubscribedUsers });
 
     this.activityEvent.emit('updated', activity, page, preNotify);
 
@@ -2068,7 +2068,7 @@ class PageService implements IPageService {
     const pages = await this.deleteCompletelyDescendantsWithStream(page, user, options, true, descendantsSubscribedSets);
     const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
 
-    const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+    const preNotify = preNotifyService.generatePreNotify(activity, async() => { return descendantsSubscribedUsers });
 
     this.activityEvent.emit('updated', activity, page, preNotify);
 
@@ -2304,7 +2304,7 @@ class PageService implements IPageService {
     await this.revertDeletedDescendantsWithStream(page, user, options, false, descendantsSubscribedSets);
     const descendantsSubscribedUsers = Array.from(descendantsSubscribedSets) as Ref<IUser>[];
 
-    const preNotify = preNotifyService.generatePreNotify(activity, () => { return descendantsSubscribedUsers });
+    const preNotify = preNotifyService.generatePreNotify(activity, async() => { return descendantsSubscribedUsers });
 
     this.activityEvent.emit('updated', activity, page, preNotify);
 

+ 3 - 4
apps/app/src/server/service/pre-notify.ts

@@ -12,9 +12,8 @@ export type PreNotifyProps = {
 }
 
 export type PreNotify = (props: PreNotifyProps) => Promise<void>;
-export type GeneratePreNotify = (activity: ActivityDocument, getAdditionalTargetUsers?: (activity?: ActivityDocument) => Ref<IUser>[]) => PreNotify;
-
-export type GetAdditionalTargetUsers = (activity: ActivityDocument) => Ref<IUser>[];
+export type GetAdditionalTargetUsers = (activity: ActivityDocument) => Promise<Ref<IUser>[]>;
+export type GeneratePreNotify = (activity: ActivityDocument, getAdditionalTargetUsers?: GetAdditionalTargetUsers) => PreNotify;
 
 interface IPreNotifyService {
   generateInitialPreNotifyProps: (PreNotifyProps) => { notificationTargetUsers?: Ref<IUser>[] },
@@ -49,7 +48,7 @@ class PreNotifyService implements IPreNotifyService {
         notificationTargetUsers?.push(...activeNotificationUsers);
       }
       else {
-        const AdditionalTargetUsers = getAdditionalTargetUsers(activity);
+        const AdditionalTargetUsers = await getAdditionalTargetUsers(activity);
 
         notificationTargetUsers?.push(
           ...activeNotificationUsers,

+ 6 - 12
apps/app/src/server/service/search-delegator/private-legacy-pages.ts

@@ -2,14 +2,14 @@ import type { IPage } from '@growi/core';
 import mongoose from 'mongoose';
 
 import { SearchDelegatorName } from '~/interfaces/named-query';
-import { ISearchResult } from '~/interfaces/search';
-import { PageModel, PageDocument, PageQueryBuilder } from '~/server/models/page';
+import type { ISearchResult } from '~/interfaces/search';
+import type { PageModel, PageDocument, PageQueryBuilder } from '~/server/models/page';
+import { serializePageSecurely } from '~/server/models/serializers';
 
-import {
+import type {
   QueryTerms, MongoTermsKey,
   SearchableData, SearchDelegator, UnavailableTermsKey, MongoQueryTerms,
 } from '../../interfaces/search';
-import { serializeUserSecurely } from '../../models/serializers/user-serializer';
 
 
 const AVAILABLE_KEYS = ['match', 'not_match', 'prefix', 'not_prefix'];
@@ -47,21 +47,15 @@ class PrivateLegacyPagesDelegator implements SearchDelegator<IPage, MongoTermsKe
 
     const total = await countQueryBuilder.query.count();
 
-    const _pages: PageDocument[] = await findQueryBuilder
+    const pages: PageDocument[] = await findQueryBuilder
       .addConditionToPagenate(offset, limit)
       .query
       .populate('creator')
       .populate('lastUpdateUser')
       .exec();
 
-    const pages = _pages.map((page) => {
-      page.creator = serializeUserSecurely(page.creator);
-      page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
-      return page;
-    });
-
     return {
-      data: pages,
+      data: pages.map(page => serializePageSecurely(page)),
       meta: {
         total,
         hitsCount: pages.length,

+ 1 - 1
apps/app/src/server/service/search.ts

@@ -1,4 +1,5 @@
 import type { IPageHasId } from '@growi/core';
+import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import mongoose from 'mongoose';
 import { FilterXSS } from 'xss';
 
@@ -13,7 +14,6 @@ import type {
 } from '../interfaces/search';
 import NamedQuery from '../models/named-query';
 import type { PageModel } from '../models/page';
-import { serializeUserSecurely } from '../models/serializers/user-serializer';
 import { SearchError } from '../models/vo/search-error';
 import { hasIntersection } from '../util/compare-objectId';
 

+ 1 - 1
apps/app/src/server/service/slack-command-handler/keep.js

@@ -4,11 +4,11 @@ import {
 import { format } from 'date-fns/format';
 import { parse } from 'date-fns/parse';
 
+import { SlackCommandHandlerError } from '~/server/models/vo/slack-command-handler-error';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:service:SlackBotService:keep');
 
-const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
 
 module.exports = (crowi) => {
   const CreatePageService = require('./create-page-service');

+ 1 - 2
apps/app/src/server/service/slack-command-handler/note.js

@@ -2,10 +2,9 @@ import {
   markdownHeaderBlock, inputSectionBlock, inputBlock, actionsBlock, buttonElement,
 } from '@growi/slack/dist/utils/block-kit-builder';
 
+import { SlackCommandHandlerError } from '~/server/models/vo/slack-command-handler-error';
 import loggerFactory from '~/utils/logger';
 
-const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
-
 const logger = loggerFactory('growi:service:SlackCommandHandler:note');
 
 module.exports = (crowi) => {

+ 1 - 2
apps/app/src/server/service/slack-command-handler/togetter.js

@@ -5,14 +5,13 @@ import { respond, deleteOriginal } from '@growi/slack/dist/utils/response-url';
 import { format, formatDate } from 'date-fns/format';
 import { parse } from 'date-fns/parse';
 
+import { SlackCommandHandlerError } from '~/server/models/vo/slack-command-handler-error';
 import loggerFactory from '~/utils/logger';
 
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:service:SlackBotService:togetter');
 
 
-const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
-
 module.exports = (crowi) => {
   const CreatePageService = require('./create-page-service');
   const createPageService = new CreatePageService(crowi);

+ 1 - 1
package.json

@@ -57,7 +57,7 @@
   "devDependencies": {
     "@changesets/changelog-github": "^0.5.0",
     "@changesets/cli": "^2.27.3",
-    "@playwright/test": "^1.45.0",
+    "@playwright/test": "^1.46.0",
     "@swc-node/register": "^1.9.1",
     "@swc/core": "^1.5.25",
     "@swc/helpers": "^0.5.11",

+ 6 - 0
packages/core/CHANGELOG.md

@@ -1,5 +1,11 @@
 # @growi/core
 
+## 1.2.0
+
+### Minor Changes
+
+- [#9019](https://github.com/weseek/growi/pull/9019) [`60097ac`](https://github.com/weseek/growi/commit/60097ac686928cca76715a83a10b506576889108) Thanks [@yuki-takei](https://github.com/yuki-takei)! - Transplant and re-implement serializers for User and Attachment
+
 ## 1.1.0
 
 ### Minor Changes

+ 5 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "1.1.0",
+  "version": "1.2.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [
@@ -30,6 +30,10 @@
       "import": "./dist/models/index.js",
       "require": "./dist/models/index.cjs"
     },
+    "./dist/models/serializers": {
+      "import": "./dist/models/serializers/index.js",
+      "require": "./dist/models/serializers/index.cjs"
+    },
     "./dist/remark-plugins": {
       "import": "./dist/remark-plugins/index.js",
       "require": "./dist/remark-plugins/index.cjs"

+ 11 - 1
packages/core/src/interfaces/common.ts

@@ -2,9 +2,10 @@
  * Common types and interfaces
  */
 
-
 import type { Types } from 'mongoose';
 
+import { isValidObjectId } from '../utils/objectid-utils';
+
 type ObjectId = Types.ObjectId;
 
 // Foreign key field
@@ -12,6 +13,15 @@ export type Ref<T> = string | ObjectId | T & { _id: string | ObjectId };
 
 export type Nullable<T> = T | null | undefined;
 
+export const isRef = <T>(obj: unknown): obj is Ref<T> => {
+  return obj != null
+    && (
+      (typeof obj === 'string' && isValidObjectId(obj))
+        || (typeof obj === 'object' && '_bsontype' in obj && obj._bsontype === 'ObjectID')
+        || (typeof obj === 'object' && '_id' in obj)
+    );
+};
+
 export const isPopulated = <T>(ref: Ref<T>): ref is T & { _id: string | ObjectId } => {
   return ref != null
     && typeof ref !== 'string'

+ 40 - 0
packages/core/src/models/serializers/attachment-serializer.ts

@@ -0,0 +1,40 @@
+import { Document } from 'mongoose';
+
+import type { IAttachment, IUser } from '~/interfaces';
+
+import { isPopulated, isRef, type Ref } from '../../interfaces/common';
+
+import { serializeUserSecurely, type IUserSerializedSecurely } from './user-serializer';
+
+export type IAttachmentSerializedSecurely<A extends IAttachment> = Omit<A, 'creator'> & { creator?: Ref<IUserSerializedSecurely<IUser>> };
+
+const omitInsecureAttributes = <A extends IAttachment>(attachment: A): IAttachmentSerializedSecurely<A> => {
+
+  const leanDoc = (attachment instanceof Document)
+    ? attachment.toObject<A>()
+    : attachment;
+
+  const { creator, ...rest } = leanDoc;
+
+  const secureCreator = creator == null
+    ? undefined
+    : serializeUserSecurely(creator);
+
+  return {
+    creator: secureCreator,
+    ...rest,
+  };
+};
+
+
+export function serializeAttachmentSecurely<A extends IAttachment>(attachment?: A): IAttachmentSerializedSecurely<A>;
+export function serializeAttachmentSecurely<A extends IAttachment>(attachment?: Ref<A>): Ref<IAttachmentSerializedSecurely<A>>;
+export function serializeAttachmentSecurely<A extends IAttachment>(attachment?: A | Ref<A>)
+    : undefined | IAttachmentSerializedSecurely<A> | Ref<IAttachmentSerializedSecurely<A>> {
+
+  if (attachment == null) return attachment;
+
+  if (isRef(attachment) && !isPopulated(attachment)) return attachment;
+
+  return omitInsecureAttributes(attachment);
+}

+ 2 - 0
packages/core/src/models/serializers/index.ts

@@ -0,0 +1,2 @@
+export * from './user-serializer';
+export * from './attachment-serializer';

+ 37 - 0
packages/core/src/models/serializers/user-serializer.ts

@@ -0,0 +1,37 @@
+import { Document } from 'mongoose';
+
+import { isPopulated, isRef, type Ref } from '../../interfaces/common';
+import type { IUser } from '../../interfaces/user';
+
+export type IUserSerializedSecurely<U extends IUser> = Omit<U, 'password' | 'apiToken' | 'email'> & { email?: string };
+
+export const omitInsecureAttributes = <U extends IUser>(user: U): IUserSerializedSecurely<U> => {
+
+  const leanDoc = (user instanceof Document)
+    ? user.toObject<U>()
+    : user;
+
+  const {
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    password, apiToken, email, ...rest
+  } = leanDoc;
+
+  const secureUser: IUserSerializedSecurely<U> = rest;
+
+  // omit email
+  if (secureUser.isEmailPublished) {
+    secureUser.email = email;
+  }
+
+  return secureUser;
+};
+
+export function serializeUserSecurely<U extends IUser>(user?: U): IUserSerializedSecurely<U>;
+export function serializeUserSecurely<U extends IUser>(user?: Ref<U>): Ref<IUserSerializedSecurely<U>>;
+export function serializeUserSecurely<U extends IUser>(user?: U | Ref<U>): undefined | IUserSerializedSecurely<U> | Ref<IUserSerializedSecurely<U>> {
+  if (user == null) return user;
+
+  if (isRef(user) && !isPopulated(user)) return user;
+
+  return omitInsecureAttributes(user);
+}

+ 3 - 3
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/TextFormatTools.tsx

@@ -9,7 +9,6 @@ import styles from './TextFormatTools.module.scss';
 
 const btnTextFormatToolsTogglerClass = styles['btn-text-format-tools-toggler'];
 
-
 type TogglarProps = {
   isOpen: boolean,
   onClick?: () => void,
@@ -34,10 +33,11 @@ const TextFormatToolsToggler = (props: TogglarProps): JSX.Element => {
 
 type TextFormatToolsType = {
   editorKey: string | GlobalCodeMirrorEditorKey,
+  onTextFormatToolsCollapseChange: () => void,
 }
 
 export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
-  const { editorKey } = props;
+  const { editorKey, onTextFormatToolsCollapseChange } = props;
   const [isOpen, setOpen] = useState(false);
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
 
@@ -57,7 +57,7 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
     <div className="d-flex">
       <TextFormatToolsToggler isOpen={isOpen} onClick={toggle} />
 
-      <Collapse isOpen={isOpen} horizontal>
+      <Collapse isOpen={isOpen} horizontal onEntered={onTextFormatToolsCollapseChange} onExited={onTextFormatToolsCollapseChange}>
         <div className="d-flex px-1 gap-1" style={{ width: '220px' }}>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertMarkdownElements('**', '**')}>
             <span className="material-symbols-outlined fs-5">format_bold</span>

+ 26 - 12
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/Toolbar.tsx

@@ -1,6 +1,7 @@
-import { memo } from 'react';
+import { memo, useCallback, useRef } from 'react';
 
 import type { AcceptedUploadFileType } from '@growi/core';
+import SimpleBar from 'simplebar-react';
 
 import type { GlobalCodeMirrorEditorKey } from '../../../../consts';
 
@@ -20,18 +21,31 @@ type Props = {
 }
 
 export const Toolbar = memo((props: Props): JSX.Element => {
-
   const { editorKey, acceptedUploadFileType, onUpload } = props;
+  const simpleBarRef = useRef<SimpleBar>(null);
+
+  const onTextFormatToolsCollapseChange = useCallback(() => {
+    if (simpleBarRef.current) {
+      simpleBarRef.current.recalculate();
+    }
+  }, [simpleBarRef]);
+
   return (
-    <div className={`d-flex gap-2 py-1 px-2 px-md-3 border-top ${styles['codemirror-editor-toolbar']}`}>
-      <AttachmentsDropup editorKey={editorKey} onUpload={onUpload} acceptedUploadFileType={acceptedUploadFileType} />
-      <TextFormatTools editorKey={editorKey} />
-      <EmojiButton
-        editorKey={editorKey}
-      />
-      <TableButton editorKey={editorKey} />
-      <DiagramButton editorKey={editorKey} />
-      <TemplateButton editorKey={editorKey} />
-    </div>
+    <>
+      <div className={`d-flex gap-2 py-1 px-2 px-md-3 border-top ${styles['codemirror-editor-toolbar']}`}>
+        <AttachmentsDropup editorKey={editorKey} onUpload={onUpload} acceptedUploadFileType={acceptedUploadFileType} />
+        <div className="flex-grow-1">
+          <SimpleBar ref={simpleBarRef} autoHide style={{ overflowY: 'hidden' }}>
+            <div className="d-flex gap-2">
+              <TextFormatTools editorKey={editorKey} onTextFormatToolsCollapseChange={onTextFormatToolsCollapseChange} />
+              <EmojiButton editorKey={editorKey} />
+              <TableButton editorKey={editorKey} />
+              <DiagramButton editorKey={editorKey} />
+              <TemplateButton editorKey={editorKey} />
+            </div>
+          </SimpleBar>
+        </div>
+      </div>
+    </>
   );
 });

+ 3 - 1
packages/remark-attachment-refs/package.json

@@ -49,10 +49,12 @@
     "@growi/ui": "link:../ui",
     "axios": "^0.24.0",
     "bunyan": "^1.8.15",
+    "express": "^4.19.2",
     "hast-util-select": "^5.0.5",
     "mongoose": "^6.11.3",
     "swr": "^2.0.3",
-    "universal-bunyan": "^0.9.2"
+    "universal-bunyan": "^0.9.2",
+    "xss": "^1.0.15"
   },
   "devDependencies": {
     "csstype": "^3.0.2",

+ 5 - 1
packages/remark-attachment-refs/src/client/components/RefsImg.tsx

@@ -50,7 +50,7 @@ export const RefsImgSubstance = React.memo(({
       width, height, maxWidth, maxHeight,
       display, grid, gridGap, noCarousel]);
 
-  const { data, error, isLoading } = useSWRxRefs(pagePath, prefix, {
+  const { data, error: axiosError, isLoading } = useSWRxRefs(pagePath, prefix, {
     depth,
     regexp,
     width,
@@ -64,6 +64,10 @@ export const RefsImgSubstance = React.memo(({
   }, isImmutable);
   const attachments = data != null ? data : [];
 
+  const error = axiosError != null
+    ? new Error(axiosError.response?.data ?? axiosError.message)
+    : undefined;
+
   return (
     <AttachmentList
       refsContext={refsContext}

+ 10 - 5
packages/remark-attachment-refs/src/client/stores/refs.tsx

@@ -1,6 +1,9 @@
-import { IAttachmentHasId } from '@growi/core';
+import type { IAttachmentHasId } from '@growi/core';
+import type { AxiosError } from 'axios';
 import axios from 'axios';
-import useSWR, { SWRResponse } from 'swr';
+import type { SWRResponse } from 'swr';
+// eslint-disable-next-line camelcase
+import useSWR, { unstable_serialize } from 'swr';
 
 export const useSWRxRef = (
     pagePath: string, fileNameOrId: string, isImmutable?: boolean,
@@ -27,10 +30,12 @@ export const useSWRxRef = (
 
 export const useSWRxRefs = (
     pagePath: string, prefix?: string, options?: Record<string, string | undefined>, isImmutable?: boolean,
-): SWRResponse<IAttachmentHasId[], Error> => {
+): SWRResponse<IAttachmentHasId[], AxiosError<string>> => {
+  const serializedOptions = unstable_serialize(options);
+
   return useSWR(
-    ['/_api/attachment-refs/refs', pagePath, prefix, options, isImmutable],
-    ([endpoint, pagePath, prefix, options]) => {
+    ['/_api/attachment-refs/refs', pagePath, prefix, serializedOptions, isImmutable],
+    async([endpoint, pagePath, prefix]) => {
       return axios.get(endpoint, {
         params: {
           pagePath,

+ 78 - 75
packages/remark-attachment-refs/src/server/routes/refs.ts

@@ -1,84 +1,89 @@
-import type { IAttachment } from '@growi/core';
+import type { IPage, IUser, IAttachment } from '@growi/core';
+import { serializeAttachmentSecurely } from '@growi/core/dist/models/serializers';
 import { OptionParser } from '@growi/core/dist/remark-plugins';
-import { model } from 'mongoose';
+import type { Request } from 'express';
+import { Router } from 'express';
+import type { Model, HydratedDocument } from 'mongoose';
+import mongoose, { model, Types } from 'mongoose';
+import { FilterXSS } from 'xss';
 
 import loggerFactory from '../../utils/logger';
 
 const logger = loggerFactory('growi:remark-attachment-refs:routes:refs');
 
 
-const loginRequiredFallback = (req, res) => {
-  return res.status(403).send('login required');
-};
+function generateRegexp(expression: string): RegExp {
+  // https://regex101.com/r/uOrwqt/2
+  const matches = expression.match(/^\/(.+)\/(.*)?$/);
+
+  return (matches != null)
+    ? new RegExp(matches[1], matches[2])
+    : new RegExp(expression);
+}
+
+/**
+ * add depth condition that limit fetched pages
+ *
+ * @param {any} query
+ * @param {any} pagePath
+ * @param {any} optionsDepth
+ * @returns query
+ */
+function addDepthCondition(query, pagePath, optionsDepth) {
+  // when option strings is 'depth=', the option value is true
+  if (optionsDepth == null || optionsDepth === true) {
+    throw new Error('The value of depth option is invalid.');
+  }
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export const routesFactory = (crowi): any => {
-  const express = crowi.require('express');
-  const mongoose = crowi.require('mongoose');
+  const range = OptionParser.parseRange(optionsDepth);
 
-  const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
-  const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
-  const { serializeUserSecurely } = crowi.require('../models/serializers/user-serializer');
+  if (range == null) {
+    return query;
+  }
 
-  const router = express.Router();
+  const start = range.start;
+  const end = range.end;
 
-  const ObjectId = mongoose.Types.ObjectId;
+  if (start < 1 || end < 1) {
+    throw new Error(`specified depth is [${start}:${end}] : start and end are must be larger than 1`);
+  }
 
-  const User = crowi.model('User');
-  const Page = crowi.model('Page');
+  // count slash
+  const slashNum = pagePath.split('/').length - 1;
+  const depthStart = slashNum; // start is not affect to fetch page
+  const depthEnd = slashNum + end - 1;
 
-  const { PageQueryBuilder } = Page;
+  return query.and({
+    path: new RegExp(`^(\\/[^\\/]*){${depthStart},${depthEnd}}$`),
+  });
+}
 
-  function generateRegexp(expression: string): RegExp {
-    // https://regex101.com/r/uOrwqt/2
-    const matches = expression.match(/^\/(.+)\/(.*)?$/);
 
-    return (matches != null)
-      ? new RegExp(matches[1], matches[2])
-      : new RegExp(expression);
-  }
+type RequestWithUser = Request & { user: HydratedDocument<IUser> };
 
-  /**
-   * add depth condition that limit fetched pages
-   *
-   * @param {any} query
-   * @param {any} pagePath
-   * @param {any} optionsDepth
-   * @returns query
-   */
-  function addDepthCondition(query, pagePath, optionsDepth) {
-    // when option strings is 'depth=', the option value is true
-    if (optionsDepth == null || optionsDepth === true) {
-      throw new Error('The value of depth option is invalid.');
-    }
+const loginRequiredFallback = (req, res) => {
+  return res.status(403).send('login required');
+};
 
-    const range = OptionParser.parseRange(optionsDepth);
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export const routesFactory = (crowi): any => {
 
-    if (range == null) {
-      return query;
-    }
+  const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
+  const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
 
-    const start = range.start;
-    const end = range.end;
+  const router = Router();
 
-    if (start < 1 || end < 1) {
-      throw new Error(`specified depth is [${start}:${end}] : start and end are must be larger than 1`);
-    }
+  const ObjectId = Types.ObjectId;
 
-    // count slash
-    const slashNum = pagePath.split('/').length - 1;
-    const depthStart = slashNum; // start is not affect to fetch page
-    const depthEnd = slashNum + end - 1;
+  const Page = mongoose.model <HydratedDocument<IPage>, Model<any> & any>('Page');
 
-    return query.and({
-      path: new RegExp(`^(\\/[^\\/]*){${depthStart},${depthEnd}}$`),
-    });
-  }
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const { PageQueryBuilder } = Page as any;
 
   /**
    * return an Attachment model
    */
-  router.get('/ref', accessTokenParser, loginRequired, async(req, res) => {
+  router.get('/ref', accessTokenParser, loginRequired, async(req: RequestWithUser, res) => {
     const user = req.user;
     const { pagePath, fileNameOrId } = req.query;
 
@@ -96,9 +101,10 @@ export const routesFactory = (crowi): any => {
     }
 
     // convert ObjectId
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     const orConditions: any[] = [{ originalName: fileNameOrId }];
-    if (ObjectId.isValid(fileNameOrId)) {
-      orConditions.push({ _id: ObjectId(fileNameOrId) });
+    if (fileNameOrId != null && ObjectId.isValid(fileNameOrId.toString())) {
+      orConditions.push({ _id: new ObjectId(fileNameOrId.toString()) });
     }
 
     const Attachment = model<IAttachment>('Attachment');
@@ -125,19 +131,16 @@ export const routesFactory = (crowi): any => {
       return;
     }
 
-    // serialize User data
-    attachment.creator = serializeUserSecurely(attachment.creator);
-
-    res.status(200).send({ attachment });
+    res.status(200).send({ attachment: serializeAttachmentSecurely(attachment) });
   });
 
   /**
    * return a list of Attachment
    */
-  router.get('/refs', accessTokenParser, loginRequired, async(req, res) => {
+  router.get('/refs', accessTokenParser, loginRequired, async(req: RequestWithUser, res) => {
     const user = req.user;
     const { prefix, pagePath } = req.query;
-    const options = JSON.parse(req.query.options);
+    const options: Record<string, string | undefined> = JSON.parse(req.query.options?.toString() ?? '');
 
     // check either 'prefix' or 'pagePath ' is specified
     if (prefix == null && pagePath == null) {
@@ -146,14 +149,20 @@ export const routesFactory = (crowi): any => {
     }
 
     // check regex
-    let regex;
-    const regexOptionValue = options.regexp || options.regex;
+    let regex: RegExp | null = null;
+    const regexOptionValue = options.regexp ?? options.regex;
     if (regexOptionValue != null) {
+      // check the length to avoid ReDoS
+      if (regexOptionValue.length > 400) {
+        res.status(400).send('the length of the \'regex\' option is too long.');
+        return;
+      }
+
       try {
         regex = generateRegexp(regexOptionValue);
       }
       catch (err) {
-        res.status(400).send(`the 'regex=${options.regex}' option is invalid as RegExp.`);
+        res.status(400).send('the \'regex\' option is invalid as RegExp.');
         return;
       }
     }
@@ -182,7 +191,8 @@ export const routesFactory = (crowi): any => {
       }
     }
     catch (err) {
-      return res.status(400).send(err);
+      const filterXSS = new FilterXSS();
+      return res.status(400).send(filterXSS.process(err.toString()));
     }
 
     const results = await pageQuery.select('id').exec();
@@ -207,14 +217,7 @@ export const routesFactory = (crowi): any => {
       .populate('creator')
       .exec();
 
-    // serialize User data
-    attachments.forEach((doc) => {
-      if (doc.creator != null && doc.creator instanceof User) {
-        doc.creator = serializeUserSecurely(doc.creator);
-      }
-    });
-
-    res.status(200).send({ attachments });
+    res.status(200).send({ attachments: attachments.map(attachment => serializeAttachmentSecurely(attachment)) });
   });
 
   return router;

+ 21 - 19
yarn.lock

@@ -2137,7 +2137,7 @@
   version "1.0.0"
 
 "@growi/core@link:packages/core":
-  version "1.1.0"
+  version "1.2.0"
   dependencies:
     bson-objectid "^2.0.4"
     escape-string-regexp "^4.0.0"
@@ -2178,10 +2178,12 @@
     "@growi/ui" "link:packages/ui"
     axios "^0.24.0"
     bunyan "^1.8.15"
+    express "^4.19.2"
     hast-util-select "^5.0.5"
     mongoose "^6.11.3"
     swr "^2.0.3"
     universal-bunyan "^0.9.2"
+    xss "^1.0.15"
 
 "@growi/remark-drawio@link:packages/remark-drawio":
   version "1.0.0"
@@ -3132,12 +3134,12 @@
     tiny-glob "^0.2.9"
     tslib "^2.4.0"
 
-"@playwright/test@^1.45.0":
-  version "1.45.0"
-  resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.45.0.tgz#790a66165a46466c0d7099dd260881802f5aba7e"
-  integrity sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw==
+"@playwright/test@^1.46.0":
+  version "1.46.0"
+  resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.46.0.tgz#ccea6d22c40ee7fa567e4192fafbdf2a907e2714"
+  integrity sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==
   dependencies:
-    playwright "1.45.0"
+    playwright "1.46.0"
 
 "@polka/url@^1.0.0-next.24":
   version "1.0.0-next.25"
@@ -14920,17 +14922,17 @@ plantuml-encoder@^1.2.5, plantuml-encoder@^1.4.0:
   resolved "https://registry.yarnpkg.com/plantuml-encoder/-/plantuml-encoder-1.4.0.tgz#7899302cf785de956bf1a167e15420feee5975f7"
   integrity sha512-sxMwpDw/ySY1WB2CE3+IdMuEcWibJ72DDOsXLkSmEaSzwEUaYBT6DWgOfBiHGCux4q433X6+OEFWjlVqp7gL6g==
 
-playwright-core@1.45.0:
-  version "1.45.0"
-  resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.45.0.tgz#5741a670b7c9060ce06852c0051d84736fb94edc"
-  integrity sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==
+playwright-core@1.46.0:
+  version "1.46.0"
+  resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.46.0.tgz#2336ac453a943abf0dc95a76c117f9d3ebd390eb"
+  integrity sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==
 
-playwright@1.45.0:
-  version "1.45.0"
-  resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.45.0.tgz#400c709c64438690f13705cb9c88ef93089c5c27"
-  integrity sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA==
+playwright@1.46.0:
+  version "1.46.0"
+  resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.46.0.tgz#c7ff490deae41fc1e814bf2cb62109dd9351164d"
+  integrity sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==
   dependencies:
-    playwright-core "1.45.0"
+    playwright-core "1.46.0"
   optionalDependencies:
     fsevents "2.3.2"
 
@@ -19385,10 +19387,10 @@ xpath@0.0.32:
   resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af"
   integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==
 
-xss@^1.0.14:
-  version "1.0.14"
-  resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.14.tgz#4f3efbde75ad0d82e9921cc3c95e6590dd336694"
-  integrity sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==
+xss@^1.0.14, xss@^1.0.15:
+  version "1.0.15"
+  resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.15.tgz#96a0e13886f0661063028b410ed1b18670f4e59a"
+  integrity sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==
   dependencies:
     commander "^2.20.3"
     cssfilter "0.0.10"