Kaynağa Gözat

Merge branch 'master' into feat/unified-merge-view

Shun Miyazawa 11 ay önce
ebeveyn
işleme
93a428c7a1
30 değiştirilmiş dosya ile 279 ekleme ve 166 silme
  1. 1 1
      .github/workflows/reusable-app-reg-suit.yml
  2. 1 1
      apps/app/package.json
  3. 16 2
      apps/app/playwright/20-basic-features/use-tools.spec.ts
  4. 13 0
      apps/app/src/client/components/PageComment/CommentEditor.module.scss
  5. 6 1
      apps/app/src/client/components/PageComment/CommentEditor.tsx
  6. 1 1
      apps/app/src/client/components/PageEditor/_page-editor-inheritance.scss
  7. 8 6
      apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group-relation.ts
  8. 3 3
      apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts
  9. 4 2
      apps/app/src/features/growi-plugin/server/routes/apiv3/admin/index.ts
  10. 4 1
      apps/app/src/features/openai/server/routes/thread.ts
  11. 2 2
      apps/app/src/features/openai/server/services/openai.ts
  12. 20 1
      apps/app/src/features/templates/server/routes/apiv3/index.ts
  13. 2 2
      apps/app/src/server/middlewares/inject-reset-order-by-token-middleware.ts
  14. 1 1
      apps/app/src/server/models/password-reset-order.ts
  15. 6 3
      apps/app/src/server/routes/apiv3/activity.ts
  16. 12 9
      apps/app/src/server/routes/apiv3/export.js
  17. 2 2
      apps/app/src/server/routes/apiv3/forgot-password.js
  18. 2 2
      apps/app/src/server/routes/apiv3/g2g-transfer.ts
  19. 16 7
      apps/app/src/server/routes/apiv3/page-listing.ts
  20. 15 8
      apps/app/src/server/routes/apiv3/page/index.ts
  21. 3 1
      apps/app/src/server/routes/apiv3/pages/index.js
  22. 18 9
      apps/app/src/server/routes/apiv3/personal-setting.js
  23. 2 2
      apps/app/src/server/routes/apiv3/security-settings/index.js
  24. 55 47
      apps/app/src/server/routes/apiv3/slack-integration-settings.js
  25. 21 13
      apps/app/src/server/routes/apiv3/slack-integration.js
  26. 18 14
      apps/app/src/server/routes/apiv3/users.js
  27. 4 3
      apps/app/src/server/routes/user-activation.ts
  28. 3 2
      apps/app/src/server/service/s2s-messaging/base.ts
  29. 1 1
      package.json
  30. 19 19
      pnpm-lock.yaml

+ 1 - 1
.github/workflows/reusable-app-reg-suit.yml

@@ -1,4 +1,4 @@
-name: Reusable build app workflow for production
+name: Reusable VRT reporting workflow for production
 
 on:
   workflow_call:

+ 1 - 1
apps/app/package.json

@@ -31,7 +31,7 @@
     "lint:styles": "stylelint \"src/**/*.scss\"",
     "lint:swagger2openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
     "lint:swagger2openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
-    "lint": "run-p lint:*",
+    "lint": "run-p lint:**",
     "prelint:swagger2openapi:apiv3": "pnpm run swagger2openapi:apiv3",
     "prelint:swagger2openapi:apiv1": "pnpm run swagger2openapi:apiv1",
     "test": "run-p test:*",

+ 16 - 2
apps/app/playwright/20-basic-features/use-tools.spec.ts

@@ -1,8 +1,22 @@
 import { test, expect, type Page } from '@playwright/test';
 
 const openPageItemControl = async(page: Page): Promise<void> => {
-  await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
-  await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+  const nav = page.getByTestId('grw-contextual-sub-nav');
+  const button = nav.getByTestId('open-page-item-control-btn');
+
+  // Wait for navigation element to be visible and attached
+  await expect(nav).toBeVisible();
+  await nav.waitFor({ state: 'visible' });
+
+  // Wait for button to be visible, enabled and attached
+  await expect(button).toBeVisible();
+  await expect(button).toBeEnabled();
+  await button.waitFor({ state: 'visible' });
+
+  // Add a small delay to ensure the button is fully interactive
+  await page.waitForTimeout(100);
+
+  await button.click();
 };
 
 test('Page Deletion and PutBack is executed successfully', async({ page }) => {

+ 13 - 0
apps/app/src/client/components/PageComment/CommentEditor.module.scss

@@ -23,6 +23,12 @@
 
 // adjust height
 .comment-editor-styles :global {
+  // Set `display: flex` instead of `display: block` to make it work with `flex: 1` of the children
+  // This helps users focus on the editor by clicking on the broader area
+  .tab-pane.active {
+    display: flex;
+  }
+
   .cm-editor {
     min-height: comment-inheritance.$codemirror-default-height !important;
     max-height: #{2 * comment-inheritance.$codemirror-default-height};
@@ -42,3 +48,10 @@
     border-radius: var(--bs-border-radius);
   }
 }
+
+// remove outline
+.comment-editor-styles :global {
+  .cm-editor {
+    outline: none;
+  }
+}

+ 6 - 1
apps/app/src/client/components/PageComment/CommentEditor.tsx

@@ -1,6 +1,6 @@
 import type { ReactNode, JSX } from 'react';
 import React, {
-  useCallback, useState, useEffect,
+  useCallback, useState, useEffect, useLayoutEffect,
   useMemo,
 } from 'react';
 
@@ -224,6 +224,11 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     codeMirrorEditor?.initDoc(commentBody);
   }, [codeMirrorEditor, commentBody]);
 
+  // set handler to focus
+  useLayoutEffect(() => {
+    if (showPreview) return;
+    codeMirrorEditor?.focus();
+  }, [codeMirrorEditor, showPreview]);
 
   const errorMessage = useMemo(() => <span className="text-danger text-end me-2">{error}</span>, [error]);
   const cancelButton = useMemo(() => (

+ 1 - 1
apps/app/src/client/components/PageEditor/_page-editor-inheritance.scss

@@ -1 +1 @@
-$navbar-editor-height: 32.8px;
+$navbar-editor-height: 37.8px;

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

@@ -41,16 +41,18 @@ module.exports = (crowi: Crowi): Router => {
    *       - name: groupIds
    *         in: query
    *         description: The group IDs to get relations for
-   *         type: array
-   *         items:
-   *           type: string
+   *         schema:
+   *           type: array
+   *           items:
+   *             type: string
    *       - name: childGroupIds
    *         in: query
    *         description: The child group IDs to get relations for
    *         required: false
-   *         type: array
-   *         items:
-   *           type: string
+   *         schema:
+   *           type: array
+   *           items:
+   *             type: string
    *     responses:
    *       200:
    *         description: The user group relations

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

@@ -444,13 +444,13 @@ module.exports = (crowi: Crowi): Router => {
   /**
    * @swagger
    *   paths:
-   *     /external-user-groups/:id/external-user-group-relations:
+   *     /external-user-groups/{id}/external-user-group-relations:
    *       get:
    *         tags: [ExternalUserGroups]
    *         security:
    *           - cookieAuth: []
    *         operationId: getExternalUserGroupRelations
-   *         summary: /external-user-groups/:id/external-user-group-relations
+   *         summary: /external-user-groups/{id}/external-user-group-relations
    *         parameters:
    *           - name: id
    *             in: path
@@ -801,7 +801,7 @@ module.exports = (crowi: Crowi): Router => {
    *             content:
    *               application/json:
    *                 schema:
-   *                 type: object
+   *                   type: object
    */
   router.put('/keycloak/sync', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
     if (isExecutingSync()) {

+ 4 - 2
apps/app/src/features/growi-plugin/server/routes/apiv3/admin/index.ts

@@ -105,7 +105,8 @@ module.exports = (crowi: Crowi): Router => {
    *       - name: id
    *         in: path
    *         required: true
-   *         type: string
+   *         schema:
+   *           type: string
    *     responses:
    *       200:
    *         description: OK
@@ -158,7 +159,8 @@ module.exports = (crowi: Crowi): Router => {
    *       - name: id
    *         in: path
    *         required: true
-   *         type: string
+   *         schema:
+   *           type: string
    *     responses:
    *       200:
    *         description: OK

+ 4 - 1
apps/app/src/features/openai/server/routes/thread.ts

@@ -45,8 +45,11 @@ export const createThreadHandlersFactory: CreateThreadFactory = (crowi) => {
         return res.apiv3Err(new ErrorV3('GROWI AI is not enabled'), 501);
       }
 
+      const { type, aiAssistantId, initialUserMessage } = req.body;
+
+      // express-validator ensures aiAssistantId is a string
+
       try {
-        const { type, aiAssistantId, initialUserMessage } = req.body;
         const thread = await openaiService.createThread(req.user._id, type, aiAssistantId, initialUserMessage);
         return res.apiv3(thread);
       }

+ 2 - 2
apps/app/src/features/openai/server/services/openai.ts

@@ -224,7 +224,7 @@ class OpenaiService implements IOpenaiService {
 
 
   async getVectorStoreRelationByAiAssistantId(aiAssistantId: string): Promise<VectorStoreDocument> {
-    const aiAssistant = await AiAssistantModel.findById({ _id: aiAssistantId }).populate('vectorStore');
+    const aiAssistant = await AiAssistantModel.findOne({ _id: { $eq: aiAssistantId } }).populate('vectorStore');
     if (aiAssistant == null) {
       throw createError(404, 'AiAssistant document does not exist');
     }
@@ -725,7 +725,7 @@ class OpenaiService implements IOpenaiService {
   }
 
   async isAiAssistantUsable(aiAssistantId: string, user: IUserHasId): Promise<boolean> {
-    const aiAssistant = await AiAssistantModel.findById(aiAssistantId);
+    const aiAssistant = await AiAssistantModel.findOne({ _id: { $eq: aiAssistantId } });
 
     if (aiAssistant == null) {
       throw createError(404, 'AiAssistant document does not exist');

+ 20 - 1
apps/app/src/features/templates/server/routes/apiv3/index.ts

@@ -51,7 +51,8 @@ module.exports = (crowi: Crowi) => {
    *         in: query
    *         description: Whether to include invalid templates
    *         required: false
-   *         type: boolean
+   *         schema:
+   *           type: boolean
    *     responses:
    *       200:
    *         description: OK
@@ -126,9 +127,15 @@ module.exports = (crowi: Crowi) => {
    *       - name: templateId
    *         in: path
    *         description: The template ID
+   *         required: true
+   *         schema:
+   *           type: string
    *       - name: locale
    *         in: path
    *         description: The locale
+   *         required: true
+   *         schema:
+   *           type: string
    *     responses:
    *       200:
    *         description: OK
@@ -170,15 +177,27 @@ module.exports = (crowi: Crowi) => {
    *       - name: organizationId
    *         in: path
    *         description: The organization ID
+   *         required: true
+   *         schema:
+   *           type: string
    *       - name: reposId
    *         in: path
    *         description: The repository ID
+   *         required: true
+   *         schema:
+   *           type: string
    *       - name: templateId
    *         in: path
    *         description: The template ID
+   *         required: true
+   *         schema:
+   *           type: string
    *       - name: locale
    *         in: path
    *         description: The locale
+   *         required: true
+   *         schema:
+   *           type: string
    *     responses:
    *       200:
    *         description: OK

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

@@ -15,14 +15,14 @@ export type ReqWithPasswordResetOrder = Request & {
 
 // eslint-disable-next-line import/no-anonymous-default-export
 export default async(req: ReqWithPasswordResetOrder, res: Response, next: NextFunction): Promise<void> => {
-  const token = req.params.token || req.body.token;
+  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 }));
   }
 
-  const passwordResetOrder = await PasswordResetOrder.findOne({ token });
+  const passwordResetOrder = await PasswordResetOrder.findOne({ token: { $eq: token } });
 
   // check if the token is valid
   if (passwordResetOrder == null || passwordResetOrder.isExpired() || passwordResetOrder.isRevoked) {

+ 1 - 1
apps/app/src/server/models/password-reset-order.ts

@@ -2,7 +2,7 @@ import crypto from 'crypto';
 
 import { addMinutes } from 'date-fns/addMinutes';
 import type { Model, Document } from 'mongoose';
-import mongoose, {
+import {
   Schema,
 } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';

+ 6 - 3
apps/app/src/server/routes/apiv3/activity.ts

@@ -190,15 +190,18 @@ module.exports = (crowi: Crowi): Router => {
    *       - name: limit
    *         in: query
    *         required: false
-   *         type: integer
+   *         schema:
+   *           type: integer
    *       - name: offset
    *         in: query
    *         required: false
-   *         type: integer
+   *         schema:
+   *           type: integer
    *       - name: searchFilter
    *         in: query
    *         required: false
-   *         type: string
+   *         schema:
+   *           type: string
    *     responses:
    *       200:
    *         description: Activity fetched successfully

+ 12 - 9
apps/app/src/server/routes/apiv3/export.js

@@ -30,7 +30,8 @@ const router = express.Router();
  *          isExporting:
  *            type: boolean
  *          progressList:
- *            type: [array, null]
+ *            type: array
+ *            nullable: true
  *            items:
  *              type: string
  *      ExportZipFileStat:
@@ -107,14 +108,16 @@ const router = express.Router();
  *          collectionName:
  *            type: string
  *          meta:
- *           progressList:
- *             type: array
- *             items:
- *               type: object
- *               description: progress data for each exporting collections
- *           isExporting:
- *             type: boolean
- *             description: whether the current exporting job exists or not
+ *            type: object
+ *            properties:
+ *              progressList:
+ *                type: array
+ *                items:
+ *                  type: object
+ *                  description: progress data for each exporting collections
+ *              isExporting:
+ *                type: boolean
+ *                description: whether the current exporting job exists or not
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {

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

@@ -98,7 +98,7 @@ module.exports = (crowi) => {
    *      summary: Request password reset
    *      tags: [Users]
    *      security:
-   *        -
+   *        - cookieAuth: []
    *      requestBody:
    *        required: true
    *        content:
@@ -160,7 +160,7 @@ module.exports = (crowi) => {
    *      summary: Reset password
    *      tags: [Users]
    *      security:
-   *        -
+   *        - cookieAuth: []
    *      requestBody:
    *        required: true
    *        content:

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

@@ -356,7 +356,7 @@ module.exports = (crowi: Crowi): Router => {
    *                  description: Metadata of the attachment
    *      responses:
    *        '200':
-   *          description:
+   *          description: Successfully imported attachment file
    *          content:
    *            application/json:
    *              schema:
@@ -406,7 +406,7 @@ module.exports = (crowi: Crowi): Router => {
    *        - transferHeaderAuth: []
    *      responses:
    *        '200':
-   *          description:
+   *          description: Successfully got GROWI information
    *          content:
    *            application/json:
    *              schema:

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

@@ -114,7 +114,8 @@ const routerFactory = (crowi: Crowi): Router => {
    *       - name: path
    *         in: query
    *         required: true
-   *         type: string
+   *         schema:
+   *           type: string
    *     responses:
    *       200:
    *         description: Get the ancestors and children of a page
@@ -177,10 +178,12 @@ const routerFactory = (crowi: Crowi): Router => {
    *     parameters:
    *       - name: id
    *         in: query
-   *         type: string
+   *         schema:
+   *           type: string
    *       - name: path
    *         in: query
-   *         type: string
+   *         schema:
+   *           type: string
    *     responses:
    *       200:
    *         description: Get the children of a page
@@ -231,16 +234,22 @@ const routerFactory = (crowi: Crowi): Router => {
    *     parameters:
    *       - name: pageIds
    *         in: query
-   *         type: array
+   *         schema:
+   *           type: array
+   *           items:
+   *             type: string
    *       - name: path
    *         in: query
-   *         type: string
+   *         schema:
+   *           type: string
    *       - name: attachBookmarkCount
    *         in: query
-   *         type: boolean
+   *         schema:
+   *           type: boolean
    *       - name: attachShortBody
    *         in: query
-   *         type: boolean
+   *         schema:
+   *           type: boolean
    *     responses:
    *       200:
    *         description: Get the information of a page

+ 15 - 8
apps/app/src/server/routes/apiv3/page/index.ts

@@ -760,7 +760,7 @@ module.exports = (crowi) => {
 
   /**
    * @swagger
-   *   /:pageId/grant:
+   *   /{pageId}/grant:
    *     put:
    *       tags: [Page]
    *       security:
@@ -825,12 +825,19 @@ module.exports = (crowi) => {
   /**
   * @swagger
   *
-  *    /page/export:
+  *    /page/export/{pageId}:
   *      get:
   *        tags: [Page]
   *        security:
   *          - cookieAuth: []
   *        description: return page's markdown
+  *        parameters:
+  *          - name: pageId
+  *            in: path
+  *            description: ID of the page
+  *            required: true
+  *            schema:
+  *              type: string
   *        responses:
   *          200:
   *            description: Return page's markdown
@@ -873,7 +880,7 @@ module.exports = (crowi) => {
     try {
       const revisionIdForFind = revisionId ?? page.revision;
 
-      revision = await Revision.findById(revisionIdForFind);
+      revision = await Revision.findOne({ id: { $eq: revisionIdForFind } });
       pagePath = page.path;
 
       // Error if pageId and revison's pageIds do not match
@@ -1046,7 +1053,7 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *   /:pageId/content-width:
+   *   /{pageId}/content-width:
    *     put:
    *       tags: [Page]
    *       summary: Update content width
@@ -1099,7 +1106,7 @@ module.exports = (crowi) => {
 
   /**
    * @swagger
-   *   /:pageId/publish:
+   *   /{pageId}/publish:
    *     put:
    *       tags: [Page]
    *       summary: Publish page
@@ -1123,7 +1130,7 @@ module.exports = (crowi) => {
 
   /**
    * @swagger
-   *   /:pageId/unpublish:
+   *   /{pageId}/unpublish:
    *     put:
    *       tags: [Page]
    *       summary: Unpublish page
@@ -1147,7 +1154,7 @@ module.exports = (crowi) => {
 
   /**
    * @swagger
-   *   /:pageId/yjs-data:
+   *   /{pageId}/yjs-data:
    *     get:
    *       tags: [Page]
    *       summary: Get Yjs data
@@ -1182,7 +1189,7 @@ module.exports = (crowi) => {
 
   /**
    * @swagger
-   *   /:pageId/sync-latest-revision-body-to-yjs-draft:
+   *   /{pageId}/sync-latest-revision-body-to-yjs-draft:
    *     put:
    *       tags: [Page]
    *       summary: Sync latest revision body to Yjs draft

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

@@ -368,7 +368,9 @@ module.exports = (crowi) => {
     *          200:
     *            description: Succeeded to resume rename page operation.
     *            content:
-    *              description: Empty response
+    *              application/json:
+    *                schema:
+    *                  type: object
     */
   router.post('/resume-rename', accessTokenParser, loginRequiredStrictly, validator.resumeRenamePage, apiV3FormValidator,
     async(req, res) => {

+ 18 - 9
apps/app/src/server/routes/apiv3/personal-setting.js

@@ -276,9 +276,11 @@ module.exports = (crowi) => {
    *          required: true
    *          content:
    *            application/json:
-   *              properties:
-   *                isGravatarEnabled:
-   *                  type: boolean
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  isGravatarEnabled:
+   *                    type: boolean
    *        responses:
    *          200:
    *            description: succeded to update user image type
@@ -354,11 +356,13 @@ module.exports = (crowi) => {
    *          required: true
    *          content:
    *            application/json:
-   *              properties:
-   *                oldPassword:
-   *                  type: string
-   *                newPassword:
-   *                  type: string
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  oldPassword:
+   *                    type: string
+   *                  newPassword:
+   *                    type: string
    *        responses:
    *          200:
    *            description: user password
@@ -446,6 +450,7 @@ module.exports = (crowi) => {
    *          content:
    *            application/json:
    *              schema:
+   *                type: object
    *                properties:
    *                  username:
    *                    type: string
@@ -523,7 +528,11 @@ module.exports = (crowi) => {
       if (user.password == null && count <= 1) {
         return res.apiv3Err('disassociate-ldap-account-failed');
       }
-      const disassociateUser = await ExternalAccount.findOneAndRemove({ providerType, accountId, user });
+      const disassociateUser = await ExternalAccount.findOneAndRemove({
+        providerType: { $eq: providerType },
+        accountId: { $eq: accountId },
+        user,
+      });
 
       const parameters = { action: SupportedAction.ACTION_USER_LDAP_ACCOUNT_DISCONNECT };
       activityEvent.emit('update', res.locals.activity._id, parameters);

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

@@ -274,7 +274,7 @@ const validator = {
  *            description: certificate for saml
  *          samlEnvVarCert:
  *            type: string
- *            desription: certificate for saml
+ *            description: certificate for saml
  *          samlAttrMapId:
  *            type: string
  *            description: attribute mapping id for saml
@@ -452,7 +452,7 @@ module.exports = (crowi) => {
    *                        googleOAuth:
    *                          $ref: '#/components/schemas/GoogleOAuthSetting'
    *                        githubOAuth:
-   *                          $ref: '#/components/schemas/GitHubOAuth
+   *                          $ref: '#/components/schemas/GitHubOAuthSetting'
    */
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
 

+ 55 - 47
apps/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -170,32 +170,34 @@ module.exports = (crowi) => {
    *            description: Succeeded to get info.
    *            content:
    *              application/json:
-   *                properties:
-   *                  currentBotType:
-   *                    type: string
-   *                  settings:
-   *                    type: object
-   *                    properties:
-   *                      slackSigningSecretEnvVars:
-   *                        type: string
-   *                      slackBotTokenEnvVars:
-   *                        type: string
-   *                      slackSigningSecret:
-   *                        type: string
-   *                      slackBotToken:
-   *                        type: string
-   *                      commandPermission:
-   *                        type: object
-   *                      eventActionsPermission:
-   *                        type: object
-   *                      proxyServerUri:
-   *                        type: string
-   *                  connectionStatuses:
-   *                    type: object
-   *                  errorMsg:
-   *                    type: string
-   *                  errorCode:
-   *                    type: string
+   *                schema:
+   *                  type: object
+   *                  properties:
+   *                    currentBotType:
+   *                      type: string
+   *                    settings:
+   *                      type: object
+   *                      properties:
+   *                        slackSigningSecretEnvVars:
+   *                          type: string
+   *                        slackBotTokenEnvVars:
+   *                          type: string
+   *                        slackSigningSecret:
+   *                          type: string
+   *                        slackBotToken:
+   *                          type: string
+   *                        commandPermission:
+   *                          type: object
+   *                        eventActionsPermission:
+   *                          type: object
+   *                        proxyServerUri:
+   *                          type: string
+   *                    connectionStatuses:
+   *                      type: object
+   *                    errorMsg:
+   *                      type: string
+   *                    errorCode:
+   *                      type: string
    */
   router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
 
@@ -399,6 +401,7 @@ module.exports = (crowi) => {
    *          content:
    *            application/json:
    *              schema:
+   *                type: object
    *                properties:
    *                  slackSigningSecret:
    *                    type: string
@@ -451,6 +454,7 @@ module.exports = (crowi) => {
    *          content:
    *            application/json:
    *              schema:
+   *                type: object
    *                properties:
    *                  commandPermission:
    *                    type: object
@@ -506,19 +510,21 @@ module.exports = (crowi) => {
    *            description: Succeeded to create slack app integration
    *            content:
    *              application/json:
-   *                properties:
-   *                  tokenGtoP:
-   *                    type: string
-   *                  tokenPtoG:
-   *                    type: string
-   *                  permissionsForBroadcastUseCommands:
-   *                    type: object
-   *                  permissionsForSingleUseCommands:
-   *                    type: object
-   *                  permissionsForSlackEvents:
-   *                    type: object
-   *                  isPrimary:
-   *                    type: boolean
+   *                schema:
+   *                  type: object
+   *                  properties:
+   *                    tokenGtoP:
+   *                      type: string
+   *                    tokenPtoG:
+   *                      type: string
+   *                    permissionsForBroadcastUseCommands:
+   *                      type: object
+   *                    permissionsForSingleUseCommands:
+   *                      type: object
+   *                    permissionsForSlackEvents:
+   *                      type: object
+   *                    isPrimary:
+   *                      type: boolean
    */
   router.post('/slack-app-integrations', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
     const SlackAppIntegrationRecordsNum = await SlackAppIntegration.countDocuments();
@@ -560,7 +566,7 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /slack-integration-settings/slack-app-integrations/:id:
+   *    /slack-integration-settings/slack-app-integrations/{id}:
    *      delete:
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
@@ -579,9 +585,11 @@ module.exports = (crowi) => {
    *            description: Succeeded to delete access tokens for slack
    *            content:
    *              application/json:
-   *                properties:
-   *                  response:
-   *                    type: object
+   *                schema:
+   *                  type: object
+   *                  properties:
+   *                    response:
+   *                      type: object
    */
   router.delete('/slack-app-integrations/:id', loginRequiredStrictly, adminRequired, validator.deleteIntegration, apiV3FormValidator, addActivity,
     async(req, res) => {
@@ -657,7 +665,7 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /slack-integration-settings/slack-app-integrations/:id/makeprimary:
+   *    /slack-integration-settings/slack-app-integrations/{id}/makeprimary:
    *      put:
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
@@ -712,7 +720,7 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /slack-integration-settings/slack-app-integrations/:id/regenerate-tokens:
+   *    /slack-integration-settings/slack-app-integrations/{id}/regenerate-tokens:
    *      put:
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
@@ -757,7 +765,7 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /slack-integration-settings/slack-app-integrations/:id/permissions:
+   *    /slack-integration-settings/slack-app-integrations/{id}/permissions:
    *      put:
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
@@ -840,7 +848,7 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /slack-integration-settings/slack-app-integrations/:id/relation-test:
+   *    /slack-integration-settings/slack-app-integrations/{id}/relation-test:
    *      post:
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:

+ 21 - 13
apps/app/src/server/routes/apiv3/slack-integration.js

@@ -320,9 +320,11 @@ module.exports = (crowi) => {
    *     responses:
    *       200:
    *         description: OK
-   *         schema:
-   *           type: string
-   *           example: "No text."
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: string
+   *               example: "No text."
    */
   router.post('/commands', addSigningSecretToReq, verifySlackRequest, checkCommandsPermission, async(req, res) => {
     const { body } = req;
@@ -365,11 +367,13 @@ module.exports = (crowi) => {
    *     responses:
    *       200:
    *         description: OK
-   *         schema:
-   *           type: object
-   *           properties:
-   *             challenge:
-   *               type: string
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 challenge:
+   *                   type: string
    */
   router.post('/proxied/verify', verifyAccessTokenFromProxy, async(req, res) => {
     const { body } = req;
@@ -400,9 +404,11 @@ module.exports = (crowi) => {
    *     responses:
    *       200:
    *         description: OK
-   *         schema:
-   *           type: string
-   *           example: "No text."
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: string
+   *               example: "No text."
    */
   router.post('/proxied/commands', verifyAccessTokenFromProxy, checkCommandsPermission, async(req, res) => {
     const { body } = req;
@@ -554,8 +560,10 @@ module.exports = (crowi) => {
    *     responses:
    *       200:
    *         description: OK
-   *         schema:
-   *           type: object
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
    */
   router.post('/events', verifyUrlMiddleware, addSigningSecretToReq, verifySlackRequest, async(req, res) => {
     const { event } = req.body;

+ 18 - 14
apps/app/src/server/routes/apiv3/users.js

@@ -1,9 +1,9 @@
-
 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 escapeStringRegexp from 'escape-string-regexp';
 import express from 'express';
 import { body, query } from 'express-validator';
 import { isEmail } from 'validator';
@@ -28,7 +28,6 @@ const logger = loggerFactory('growi:routes:apiv3:users');
 
 const router = express.Router();
 
-
 const PAGE_ITEMS = 50;
 
 const validator = {};
@@ -135,15 +134,15 @@ module.exports = (crowi) => {
   };
 
   validator.statusList = [
-    query('selectedStatusList').if(value => value != null).custom((value, { req }) => {
-
-      const { user } = req;
-
-      if (user != null && user.admin) {
-        return value;
-      }
-      throw new Error('the param \'selectedStatusList\' is not allowed to use by the users except administrators');
-    }),
+    query('selectedStatusList').if(value => value != null).isArray().withMessage('selectedStatusList must be an array')
+      .custom((value, { req }) => {
+        const { user } = req;
+        if (user != null && user.admin) {
+          return value;
+        }
+        throw new Error('the param \'selectedStatusList\' is not allowed to use by the users except administrators');
+      }),
+    query('forceIncludeAttributes').if(value => value != null).isArray().withMessage('forceIncludeAttributes must be an array'),
     // validate sortOrder : asc or desc
     query('sortOrder').isIn(['asc', 'desc']),
     // validate sort : what column you will sort
@@ -290,15 +289,20 @@ module.exports = (crowi) => {
   router.get('/', accessTokenParser, loginRequired, validator.statusList, apiV3FormValidator, async(req, res) => {
 
     const page = parseInt(req.query.page) || 1;
+
     // status
-    const { forceIncludeAttributes } = req.query;
-    const selectedStatusList = req.query.selectedStatusList || ['active'];
+    const forceIncludeAttributes = Array.isArray(req.query.forceIncludeAttributes)
+      ? req.query.forceIncludeAttributes
+      : [];
+    const selectedStatusList = Array.isArray(req.query.selectedStatusList)
+      ? req.query.selectedStatusList
+      : ['active'];
 
     const statusNoList = (selectedStatusList.includes('all')) ? Object.values(statusNo) : selectedStatusList.map(element => statusNo[element]);
 
     // Search from input
     const searchText = req.query.searchText || '';
-    const searchWord = new RegExp(`${searchText}`);
+    const searchWord = new RegExp(escapeStringRegexp(searchText));
     // Sort
     const { sort, sortOrder } = req.query;
     const sortOutput = {

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

@@ -15,15 +15,16 @@ type CrowiReq = ReqWithUserRegistrationOrder & {
 /**
  * @swagger
  *
- * /user-activation/:token:
+ * /user-activation/{token}:
  *   get:
- *     summary: /user-activation/:token
+ *     summary: /user-activation/{token}
  *     tags: [Users]
  *     parameters:
  *       - name: token
  *         in: path
  *         required: true
- *         type: string
+ *         schema:
+ *           type: string
  *     responses:
  *       200:
  *         description: User activation successful

+ 3 - 2
apps/app/src/server/service/s2s-messaging/base.ts

@@ -1,7 +1,8 @@
+import crypto from 'crypto';
+
 import type S2sMessage from '~/server/models/vo/s2s-message';
 import loggerFactory from '~/utils/logger';
 
-
 import type { S2sMessageHandlable } from './handlable';
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -48,7 +49,7 @@ export abstract class AbstractS2sMessagingService implements S2sMessagingService
   handlableList: S2sMessageHandlable[];
 
   constructor(uri: string) {
-    this.uid = Math.floor(Math.random() * 100000);
+    this.uid = crypto.randomInt(100000);
     this.uri = uri;
 
     if (uri == null) {

+ 1 - 1
package.json

@@ -96,7 +96,7 @@
     "turbo": "^2.1.3",
     "typescript": "~5.0.0",
     "typescript-transform-paths": "^3.4.7",
-    "vite": "^5.4.16",
+    "vite": "^5.4.17",
     "vite-plugin-dts": "^3.9.1",
     "vite-tsconfig-paths": "^5.0.1",
     "vitest": "^2.1.1",

+ 19 - 19
pnpm-lock.yaml

@@ -57,7 +57,7 @@ importers:
         version: 5.59.7(eslint@8.41.0)(typescript@5.0.4)
       '@vitejs/plugin-react':
         specifier: ^4.3.1
-        version: 4.3.1(vite@5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))
+        version: 4.3.1(vite@5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))
       '@vitest/coverage-v8':
         specifier: ^2.1.1
         version: 2.1.1(vitest@2.1.1)
@@ -173,14 +173,14 @@ importers:
         specifier: ^3.4.7
         version: 3.4.7(typescript@5.0.4)
       vite:
-        specifier: ^5.4.16
-        version: 5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
+        specifier: ^5.4.17
+        version: 5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
       vite-plugin-dts:
         specifier: ^3.9.1
-        version: 3.9.1(@types/node@20.14.0)(rollup@4.39.0)(typescript@5.0.4)(vite@5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))
+        version: 3.9.1(@types/node@20.14.0)(rollup@4.39.0)(typescript@5.0.4)(vite@5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))
       vite-tsconfig-paths:
         specifier: ^5.0.1
-        version: 5.0.1(typescript@5.0.4)(vite@5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))
+        version: 5.0.1(typescript@5.0.4)(vite@5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))
       vitest:
         specifier: ^2.1.1
         version: 2.1.1(@types/node@20.14.0)(@vitest/ui@2.1.1)(happy-dom@15.7.4)(sass@1.77.6)(terser@5.39.0)
@@ -14136,8 +14136,8 @@ packages:
       vite:
         optional: true
 
-  vite@5.4.16:
-    resolution: {integrity: sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==}
+  vite@5.4.17:
+    resolution: {integrity: sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:
@@ -20533,14 +20533,14 @@ snapshots:
 
   '@unts/get-tsconfig@4.1.1': {}
 
-  '@vitejs/plugin-react@4.3.1(vite@5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))':
+  '@vitejs/plugin-react@4.3.1(vite@5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))':
     dependencies:
       '@babel/core': 7.24.6
       '@babel/plugin-transform-react-jsx-self': 7.24.6(@babel/core@7.24.6)
       '@babel/plugin-transform-react-jsx-source': 7.24.6(@babel/core@7.24.6)
       '@types/babel__core': 7.20.5
       react-refresh: 0.14.2
-      vite: 5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
+      vite: 5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -20569,13 +20569,13 @@ snapshots:
       chai: 5.1.1
       tinyrainbow: 1.2.0
 
-  '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))':
+  '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))':
     dependencies:
       '@vitest/spy': 2.1.1
       estree-walker: 3.0.3
       magic-string: 0.30.11
     optionalDependencies:
-      vite: 5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
+      vite: 5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
 
   '@vitest/pretty-format@2.1.1':
     dependencies:
@@ -30541,7 +30541,7 @@ snapshots:
       cac: 6.7.14
       debug: 4.4.0(supports-color@5.5.0)
       pathe: 1.1.2
-      vite: 5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
+      vite: 5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -30553,7 +30553,7 @@ snapshots:
       - supports-color
       - terser
 
-  vite-plugin-dts@3.9.1(@types/node@20.14.0)(rollup@4.39.0)(typescript@5.0.4)(vite@5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)):
+  vite-plugin-dts@3.9.1(@types/node@20.14.0)(rollup@4.39.0)(typescript@5.0.4)(vite@5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)):
     dependencies:
       '@microsoft/api-extractor': 7.43.0(@types/node@20.14.0)
       '@rollup/pluginutils': 5.1.0(rollup@4.39.0)
@@ -30564,24 +30564,24 @@ snapshots:
       typescript: 5.0.4
       vue-tsc: 1.8.27(typescript@5.0.4)
     optionalDependencies:
-      vite: 5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
+      vite: 5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
     transitivePeerDependencies:
       - '@types/node'
       - rollup
       - supports-color
 
-  vite-tsconfig-paths@5.0.1(typescript@5.0.4)(vite@5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)):
+  vite-tsconfig-paths@5.0.1(typescript@5.0.4)(vite@5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)):
     dependencies:
       debug: 4.4.0(supports-color@5.5.0)
       globrex: 0.1.2
       tsconfck: 3.0.3(typescript@5.0.4)
     optionalDependencies:
-      vite: 5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
+      vite: 5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  vite@5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0):
+  vite@5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0):
     dependencies:
       esbuild: 0.21.5
       postcss: 8.5.3
@@ -30601,7 +30601,7 @@ snapshots:
   vitest@2.1.1(@types/node@20.14.0)(@vitest/ui@2.1.1)(happy-dom@15.7.4)(sass@1.77.6)(terser@5.39.0):
     dependencies:
       '@vitest/expect': 2.1.1
-      '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))
+      '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0))
       '@vitest/pretty-format': 2.1.1
       '@vitest/runner': 2.1.1
       '@vitest/snapshot': 2.1.1
@@ -30616,7 +30616,7 @@ snapshots:
       tinyexec: 0.3.0
       tinypool: 1.0.1
       tinyrainbow: 1.2.0
-      vite: 5.4.16(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
+      vite: 5.4.17(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
       vite-node: 2.1.1(@types/node@20.14.0)(sass@1.77.6)(terser@5.39.0)
       why-is-node-running: 2.3.0
     optionalDependencies: