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

Revert "Merge pull request #10532 from growilabs/support/156162-174082-app-apiv3-routes-biome-2"

This reverts commit 892f257f185a628451913ca2555357f2d28c9ad8, reversing
changes made to 9cbee49555a7130dc42a48fb0496fbaf0ec97185.
Yuki Takei 4 месяцев назад
Родитель
Сommit
a0f5481eea

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

@@ -69,8 +69,6 @@ module.exports = {
     'src/server/routes/apiv3/user/**',
     'src/server/routes/apiv3/personal-setting/**',
     'src/server/routes/apiv3/security-settings/**',
-    'src/server/routes/apiv3/app-settings/**',
-    'src/server/routes/apiv3/page/**',
     'src/server/routes/apiv3/*.ts',
   ],
   settings: {

+ 37 - 72
apps/app/src/server/routes/apiv3/app-settings/file-upload-setting.integ.ts

@@ -17,34 +17,25 @@ const mockActivityId = '507f1f77bcf86cd799439011';
 mockRequire.stopAll();
 
 mockRequire('~/server/middlewares/access-token-parser', {
-  accessTokenParser:
-    () => (_req: Request, _res: ApiV3Response, next: () => void) =>
-      next(),
+  accessTokenParser: () => (_req: Request, _res: ApiV3Response, next: () => void) => next(),
 });
 
-mockRequire(
-  '../../../middlewares/login-required',
-  () => (_req: Request, _res: ApiV3Response, next: () => void) => next(),
-);
-mockRequire(
-  '../../../middlewares/admin-required',
-  () => (_req: Request, _res: ApiV3Response, next: () => void) => next(),
-);
+mockRequire('../../../middlewares/login-required', () => (_req: Request, _res: ApiV3Response, next: () => void) => next());
+mockRequire('../../../middlewares/admin-required', () => (_req: Request, _res: ApiV3Response, next: () => void) => next());
 
 mockRequire('../../../middlewares/add-activity', {
-  generateAddActivityMiddleware:
-    () => (_req: Request, res: ApiV3Response, next: () => void) => {
-      res.locals = res.locals || {};
-      res.locals.activity = { _id: mockActivityId };
-      next();
-    },
+  generateAddActivityMiddleware: () => (_req: Request, res: ApiV3Response, next: () => void) => {
+    res.locals = res.locals || {};
+    res.locals.activity = { _id: mockActivityId };
+    next();
+  },
 });
 
 describe('file-upload-setting route', () => {
   let app: express.Application;
   let crowiMock: Crowi;
 
-  beforeEach(async () => {
+  beforeEach(async() => {
     // Initialize configManager for each test
     const s2sMessagingServiceMock = mock<S2sMessagingService>();
     configManager.setS2sMessagingService(s2sMessagingServiceMock);
@@ -68,16 +59,14 @@ describe('file-upload-setting route', () => {
     // Mock apiv3 response methods
     app.use((_req, res, next) => {
       const apiRes = res as ApiV3Response;
-      apiRes.apiv3 = (data) => res.json(data);
-      apiRes.apiv3Err = (error, statusCode = 500) =>
-        res.status(statusCode).json({ error });
+      apiRes.apiv3 = data => res.json(data);
+      apiRes.apiv3Err = (error, statusCode = 500) => res.status(statusCode).json({ error });
       next();
     });
 
     // Import and mount the actual router using dynamic import
     const fileUploadSettingModule = await import('./file-upload-setting');
-    const fileUploadSettingRouterFactory =
-      (fileUploadSettingModule as any).default || fileUploadSettingModule;
+    const fileUploadSettingRouterFactory = (fileUploadSettingModule as any).default || fileUploadSettingModule;
     const fileUploadSettingRouter = fileUploadSettingRouterFactory(crowiMock);
     app.use('/', fileUploadSettingRouter);
   });
@@ -86,7 +75,7 @@ describe('file-upload-setting route', () => {
     mockRequire.stopAll();
   });
 
-  it('should update file upload type to local', async () => {
+  it('should update file upload type to local', async() => {
     const response = await request(app)
       .put('/')
       .send({
@@ -100,7 +89,7 @@ describe('file-upload-setting route', () => {
   });
 
   describe('AWS settings', () => {
-    const setupAwsSecret = async (secret: string) => {
+    const setupAwsSecret = async(secret: string) => {
       await configManager.updateConfigs({
         'app:fileUploadType': 'aws',
         'aws:s3SecretAccessKey': toNonBlankString(secret),
@@ -110,13 +99,11 @@ describe('file-upload-setting route', () => {
       await configManager.loadConfigs();
     };
 
-    it('should preserve existing s3SecretAccessKey when not included in request', async () => {
+    it('should preserve existing s3SecretAccessKey when not included in request', async() => {
       const existingSecret = 'existing-secret-key-12345';
       await setupAwsSecret(existingSecret);
 
-      expect(configManager.getConfig('aws:s3SecretAccessKey')).toBe(
-        existingSecret,
-      );
+      expect(configManager.getConfig('aws:s3SecretAccessKey')).toBe(existingSecret);
 
       const response = await request(app)
         .put('/')
@@ -130,19 +117,15 @@ describe('file-upload-setting route', () => {
 
       await configManager.loadConfigs();
 
-      expect(configManager.getConfig('aws:s3SecretAccessKey')).toBe(
-        existingSecret,
-      );
+      expect(configManager.getConfig('aws:s3SecretAccessKey')).toBe(existingSecret);
       expect(response.body.responseParams.fileUploadType).toBe('aws');
     });
 
-    it('should update s3SecretAccessKey when new value is provided in request', async () => {
+    it('should update s3SecretAccessKey when new value is provided in request', async() => {
       const existingSecret = 'existing-secret-key-12345';
       await setupAwsSecret(existingSecret);
 
-      expect(configManager.getConfig('aws:s3SecretAccessKey')).toBe(
-        existingSecret,
-      );
+      expect(configManager.getConfig('aws:s3SecretAccessKey')).toBe(existingSecret);
 
       const newSecret = 'new-secret-key-67890';
       const response = await request(app)
@@ -162,13 +145,11 @@ describe('file-upload-setting route', () => {
       expect(response.body.responseParams.fileUploadType).toBe('aws');
     });
 
-    it('should remove s3SecretAccessKey when empty string is provided in request', async () => {
+    it('should remove s3SecretAccessKey when empty string is provided in request', async() => {
       const existingSecret = 'existing-secret-key-12345';
       await setupAwsSecret(existingSecret);
 
-      expect(configManager.getConfig('aws:s3SecretAccessKey')).toBe(
-        existingSecret,
-      );
+      expect(configManager.getConfig('aws:s3SecretAccessKey')).toBe(existingSecret);
 
       const response = await request(app)
         .put('/')
@@ -189,7 +170,7 @@ describe('file-upload-setting route', () => {
   });
 
   describe('GCS settings', () => {
-    const setupGcsSecret = async (apiKeyPath: string) => {
+    const setupGcsSecret = async(apiKeyPath: string) => {
       await configManager.updateConfigs({
         'app:fileUploadType': 'gcs',
         'gcs:apiKeyJsonPath': toNonBlankString(apiKeyPath),
@@ -198,13 +179,11 @@ describe('file-upload-setting route', () => {
       await configManager.loadConfigs();
     };
 
-    it('should preserve existing gcsApiKeyJsonPath when not included in request', async () => {
+    it('should preserve existing gcsApiKeyJsonPath when not included in request', async() => {
       const existingApiKeyPath = '/path/to/existing-api-key.json';
       await setupGcsSecret(existingApiKeyPath);
 
-      expect(configManager.getConfig('gcs:apiKeyJsonPath')).toBe(
-        existingApiKeyPath,
-      );
+      expect(configManager.getConfig('gcs:apiKeyJsonPath')).toBe(existingApiKeyPath);
 
       const response = await request(app)
         .put('/')
@@ -217,19 +196,15 @@ describe('file-upload-setting route', () => {
 
       await configManager.loadConfigs();
 
-      expect(configManager.getConfig('gcs:apiKeyJsonPath')).toBe(
-        existingApiKeyPath,
-      );
+      expect(configManager.getConfig('gcs:apiKeyJsonPath')).toBe(existingApiKeyPath);
       expect(response.body.responseParams.fileUploadType).toBe('gcs');
     });
 
-    it('should update gcsApiKeyJsonPath when new value is provided in request', async () => {
+    it('should update gcsApiKeyJsonPath when new value is provided in request', async() => {
       const existingApiKeyPath = '/path/to/existing-api-key.json';
       await setupGcsSecret(existingApiKeyPath);
 
-      expect(configManager.getConfig('gcs:apiKeyJsonPath')).toBe(
-        existingApiKeyPath,
-      );
+      expect(configManager.getConfig('gcs:apiKeyJsonPath')).toBe(existingApiKeyPath);
 
       const newApiKeyPath = '/path/to/new-api-key.json';
       const response = await request(app)
@@ -248,13 +223,11 @@ describe('file-upload-setting route', () => {
       expect(response.body.responseParams.fileUploadType).toBe('gcs');
     });
 
-    it('should remove gcsApiKeyJsonPath when empty string is provided in request', async () => {
+    it('should remove gcsApiKeyJsonPath when empty string is provided in request', async() => {
       const existingApiKeyPath = '/path/to/existing-api-key.json';
       await setupGcsSecret(existingApiKeyPath);
 
-      expect(configManager.getConfig('gcs:apiKeyJsonPath')).toBe(
-        existingApiKeyPath,
-      );
+      expect(configManager.getConfig('gcs:apiKeyJsonPath')).toBe(existingApiKeyPath);
 
       const response = await request(app)
         .put('/')
@@ -274,7 +247,7 @@ describe('file-upload-setting route', () => {
   });
 
   describe('Azure settings', () => {
-    const setupAzureSecret = async (secret: string) => {
+    const setupAzureSecret = async(secret: string) => {
       await configManager.updateConfigs({
         'app:fileUploadType': 'azure',
         'azure:clientSecret': toNonBlankString(secret),
@@ -286,13 +259,11 @@ describe('file-upload-setting route', () => {
       await configManager.loadConfigs();
     };
 
-    it('should preserve existing azureClientSecret when not included in request', async () => {
+    it('should preserve existing azureClientSecret when not included in request', async() => {
       const existingSecret = 'existing-azure-secret-12345';
       await setupAzureSecret(existingSecret);
 
-      expect(configManager.getConfig('azure:clientSecret')).toBe(
-        existingSecret,
-      );
+      expect(configManager.getConfig('azure:clientSecret')).toBe(existingSecret);
 
       const response = await request(app)
         .put('/')
@@ -308,19 +279,15 @@ describe('file-upload-setting route', () => {
 
       await configManager.loadConfigs();
 
-      expect(configManager.getConfig('azure:clientSecret')).toBe(
-        existingSecret,
-      );
+      expect(configManager.getConfig('azure:clientSecret')).toBe(existingSecret);
       expect(response.body.responseParams.fileUploadType).toBe('azure');
     });
 
-    it('should update azureClientSecret when new value is provided in request', async () => {
+    it('should update azureClientSecret when new value is provided in request', async() => {
       const existingSecret = 'existing-azure-secret-12345';
       await setupAzureSecret(existingSecret);
 
-      expect(configManager.getConfig('azure:clientSecret')).toBe(
-        existingSecret,
-      );
+      expect(configManager.getConfig('azure:clientSecret')).toBe(existingSecret);
 
       const newSecret = 'new-azure-secret-67890';
       const response = await request(app)
@@ -342,13 +309,11 @@ describe('file-upload-setting route', () => {
       expect(response.body.responseParams.fileUploadType).toBe('azure');
     });
 
-    it('should remove azureClientSecret when empty string is provided in request', async () => {
+    it('should remove azureClientSecret when empty string is provided in request', async() => {
       const existingSecret = 'existing-azure-secret-12345';
       await setupAzureSecret(existingSecret);
 
-      expect(configManager.getConfig('azure:clientSecret')).toBe(
-        existingSecret,
-      );
+      expect(configManager.getConfig('azure:clientSecret')).toBe(existingSecret);
 
       const response = await request(app)
         .put('/')

+ 94 - 173
apps/app/src/server/routes/apiv3/app-settings/file-upload-setting.ts

@@ -1,7 +1,5 @@
 import {
-  SCOPE,
-  toNonBlankString,
-  toNonBlankStringOrUndefined,
+  toNonBlankString, toNonBlankStringOrUndefined, SCOPE,
 } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import express from 'express';
@@ -16,9 +14,7 @@ import loggerFactory from '~/utils/logger';
 import { generateAddActivityMiddleware } from '../../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 
-const logger = loggerFactory(
-  'growi:routes:apiv3:app-settings:file-upload-setting',
-);
+const logger = loggerFactory('growi:routes:apiv3:app-settings:file-upload-setting');
 
 const router = express.Router();
 
@@ -50,11 +46,7 @@ type AzureResponseParams = BaseResponseParams & {
   azureReferenceFileWithRelayMode?: boolean;
 };
 
-type ResponseParams =
-  | BaseResponseParams
-  | GcsResponseParams
-  | AwsResponseParams
-  | AzureResponseParams;
+type ResponseParams = BaseResponseParams | GcsResponseParams | AwsResponseParams | AzureResponseParams;
 
 const validator = {
   fileUploadSetting: [
@@ -62,14 +54,12 @@ const validator = {
     body('gcsApiKeyJsonPath').optional(),
     body('gcsBucket').optional(),
     body('gcsUploadNamespace').optional(),
-    body('gcsReferenceFileWithRelayMode')
-      .if((value) => value != null)
-      .isBoolean(),
+    body('gcsReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
     body('s3Bucket').optional(),
     body('s3Region')
       .optional()
-      .if((value) => value !== '' && value != null)
-      .custom(async (value) => {
+      .if(value => value !== '' && value != null)
+      .custom(async(value) => {
         const { t } = await getTranslation();
         if (!/^[a-z]+-[a-z]+-\d+$/.test(value)) {
           throw new Error(t('validation.aws_region'));
@@ -78,30 +68,23 @@ const validator = {
       }),
     body('s3CustomEndpoint')
       .optional()
-      .if((value) => value !== '' && value != null)
-      .custom(async (value) => {
+      .if(value => value !== '' && value != null)
+      .custom(async(value) => {
         const { t } = await getTranslation();
         if (!/^(https?:\/\/[^/]+|)$/.test(value)) {
           throw new Error(t('validation.aws_custom_endpoint'));
         }
         return true;
       }),
-    body('s3AccessKeyId')
-      .optional()
-      .if((value) => value !== '' && value != null)
-      .matches(/^[\da-zA-Z]+$/),
+    body('s3AccessKeyId').optional().if(value => value !== '' && value != null).matches(/^[\da-zA-Z]+$/),
     body('s3SecretAccessKey').optional(),
-    body('s3ReferenceFileWithRelayMode')
-      .if((value) => value != null)
-      .isBoolean(),
+    body('s3ReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
     body('azureTenantId').optional(),
     body('azureClientId').optional(),
     body('azureClientSecret').optional(),
     body('azureStorageAccountName').optional(),
     body('azureStorageStorageName').optional(),
-    body('azureReferenceFileWithRelayMode')
-      .if((value) => value != null)
-      .isBoolean(),
+    body('azureReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
   ],
 };
 
@@ -135,35 +118,24 @@ const validator = {
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../../middlewares/login-required')(
-    crowi,
-  );
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
   const adminRequired = require('../../../middlewares/admin-required')(crowi);
   const addActivity = generateAddActivityMiddleware();
 
   const activityEvent = crowi.event('activity');
 
   //  eslint-disable-next-line max-len
-  router.put(
-    '/',
-    accessTokenParser([SCOPE.WRITE.ADMIN.APP]),
-    loginRequiredStrictly,
-    adminRequired,
-    addActivity,
-    validator.fileUploadSetting,
-    apiV3FormValidator,
-    async (req, res) => {
+  router.put('/', accessTokenParser([SCOPE.WRITE.ADMIN.APP]),
+    loginRequiredStrictly, adminRequired, addActivity, validator.fileUploadSetting, apiV3FormValidator, async(req, res) => {
       const { fileUploadType } = req.body;
 
       if (fileUploadType === 'local' || fileUploadType === 'gridfs') {
         try {
-          await configManager.updateConfigs(
-            {
-              'app:fileUploadType': fileUploadType,
-            },
-            { skipPubsub: true },
-          );
-        } catch (err) {
+          await configManager.updateConfigs({
+            'app:fileUploadType': fileUploadType,
+          }, { skipPubsub: true });
+        }
+        catch (err) {
           const msg = `Error occurred in updating ${fileUploadType} settings: ${err.message}`;
           logger.error('Error', err);
           return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
@@ -174,67 +146,55 @@ module.exports = (crowi) => {
         try {
           try {
             toNonBlankString(req.body.s3Bucket);
-          } catch (err) {
+          }
+          catch (err) {
             throw new Error('S3 Bucket name is required');
           }
           try {
             toNonBlankString(req.body.s3Region);
-          } catch (err) {
+          }
+          catch (err) {
             throw new Error('S3 Region is required');
           }
-          await configManager.updateConfigs(
-            {
-              'app:fileUploadType': fileUploadType,
-              'aws:s3Region': toNonBlankString(req.body.s3Region),
-              'aws:s3Bucket': toNonBlankString(req.body.s3Bucket),
-              'aws:referenceFileWithRelayMode':
-                req.body.s3ReferenceFileWithRelayMode,
-            },
-            { skipPubsub: true },
-          );
+          await configManager.updateConfigs({
+            'app:fileUploadType': fileUploadType,
+            'aws:s3Region': toNonBlankString(req.body.s3Region),
+            'aws:s3Bucket': toNonBlankString(req.body.s3Bucket),
+            'aws:referenceFileWithRelayMode': req.body.s3ReferenceFileWithRelayMode,
+          },
+          { skipPubsub: true });
 
           // Update optional non-secret fields (can be removed if undefined)
-          await configManager.updateConfigs(
-            {
-              'aws:s3CustomEndpoint': toNonBlankStringOrUndefined(
-                req.body.s3CustomEndpoint,
-              ),
+          await configManager.updateConfigs({
+            'aws:s3CustomEndpoint': toNonBlankStringOrUndefined(req.body.s3CustomEndpoint),
+          },
+          {
+            skipPubsub: true,
+            removeIfUndefined: true,
+          });
+
+          // Update secret fields only if explicitly provided in request
+          if (req.body.s3AccessKeyId !== undefined) {
+            await configManager.updateConfigs({
+              'aws:s3AccessKeyId': toNonBlankStringOrUndefined(req.body.s3AccessKeyId),
             },
             {
               skipPubsub: true,
               removeIfUndefined: true,
-            },
-          );
-
-          // Update secret fields only if explicitly provided in request
-          if (req.body.s3AccessKeyId !== undefined) {
-            await configManager.updateConfigs(
-              {
-                'aws:s3AccessKeyId': toNonBlankStringOrUndefined(
-                  req.body.s3AccessKeyId,
-                ),
-              },
-              {
-                skipPubsub: true,
-                removeIfUndefined: true,
-              },
-            );
+            });
           }
 
           if (req.body.s3SecretAccessKey !== undefined) {
-            await configManager.updateConfigs(
-              {
-                'aws:s3SecretAccessKey': toNonBlankStringOrUndefined(
-                  req.body.s3SecretAccessKey,
-                ),
-              },
-              {
-                skipPubsub: true,
-                removeIfUndefined: true,
-              },
-            );
+            await configManager.updateConfigs({
+              'aws:s3SecretAccessKey': toNonBlankStringOrUndefined(req.body.s3SecretAccessKey),
+            },
+            {
+              skipPubsub: true,
+              removeIfUndefined: true,
+            });
           }
-        } catch (err) {
+        }
+        catch (err) {
           const msg = `Error occurred in updating AWS S3 settings: ${err.message}`;
           logger.error('Error', err);
           return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
@@ -243,38 +203,28 @@ module.exports = (crowi) => {
 
       if (fileUploadType === 'gcs') {
         try {
-          await configManager.updateConfigs(
-            {
-              'app:fileUploadType': fileUploadType,
-              'gcs:referenceFileWithRelayMode':
-                req.body.gcsReferenceFileWithRelayMode,
-            },
-            { skipPubsub: true },
-          );
+          await configManager.updateConfigs({
+            'app:fileUploadType': fileUploadType,
+            'gcs:referenceFileWithRelayMode': req.body.gcsReferenceFileWithRelayMode,
+          },
+          { skipPubsub: true });
 
           // Update optional non-secret fields (can be removed if undefined)
-          await configManager.updateConfigs(
-            {
-              'gcs:bucket': toNonBlankStringOrUndefined(req.body.gcsBucket),
-              'gcs:uploadNamespace': toNonBlankStringOrUndefined(
-                req.body.gcsUploadNamespace,
-              ),
-            },
-            { skipPubsub: true, removeIfUndefined: true },
-          );
+          await configManager.updateConfigs({
+            'gcs:bucket': toNonBlankStringOrUndefined(req.body.gcsBucket),
+            'gcs:uploadNamespace': toNonBlankStringOrUndefined(req.body.gcsUploadNamespace),
+          },
+          { skipPubsub: true, removeIfUndefined: true });
 
           // Update secret fields only if explicitly provided in request
           if (req.body.gcsApiKeyJsonPath !== undefined) {
-            await configManager.updateConfigs(
-              {
-                'gcs:apiKeyJsonPath': toNonBlankStringOrUndefined(
-                  req.body.gcsApiKeyJsonPath,
-                ),
-              },
-              { skipPubsub: true, removeIfUndefined: true },
-            );
+            await configManager.updateConfigs({
+              'gcs:apiKeyJsonPath': toNonBlankStringOrUndefined(req.body.gcsApiKeyJsonPath),
+            },
+            { skipPubsub: true, removeIfUndefined: true });
           }
-        } catch (err) {
+        }
+        catch (err) {
           const msg = `Error occurred in updating GCS settings: ${err.message}`;
           logger.error('Error', err);
           return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
@@ -283,46 +233,28 @@ module.exports = (crowi) => {
 
       if (fileUploadType === 'azure') {
         try {
-          await configManager.updateConfigs(
-            {
-              'app:fileUploadType': fileUploadType,
-              'azure:referenceFileWithRelayMode':
-                req.body.azureReferenceFileWithRelayMode,
-            },
-            { skipPubsub: true },
-          );
+          await configManager.updateConfigs({
+            'app:fileUploadType': fileUploadType,
+            'azure:referenceFileWithRelayMode': req.body.azureReferenceFileWithRelayMode,
+          },
+          { skipPubsub: true });
 
           // Update optional non-secret fields (can be removed if undefined)
-          await configManager.updateConfigs(
-            {
-              'azure:tenantId': toNonBlankStringOrUndefined(
-                req.body.azureTenantId,
-              ),
-              'azure:clientId': toNonBlankStringOrUndefined(
-                req.body.azureClientId,
-              ),
-              'azure:storageAccountName': toNonBlankStringOrUndefined(
-                req.body.azureStorageAccountName,
-              ),
-              'azure:storageContainerName': toNonBlankStringOrUndefined(
-                req.body.azureStorageContainerName,
-              ),
-            },
-            { skipPubsub: true, removeIfUndefined: true },
-          );
+          await configManager.updateConfigs({
+            'azure:tenantId': toNonBlankStringOrUndefined(req.body.azureTenantId),
+            'azure:clientId': toNonBlankStringOrUndefined(req.body.azureClientId),
+            'azure:storageAccountName': toNonBlankStringOrUndefined(req.body.azureStorageAccountName),
+            'azure:storageContainerName': toNonBlankStringOrUndefined(req.body.azureStorageContainerName),
+          }, { skipPubsub: true, removeIfUndefined: true });
 
           // Update secret fields only if explicitly provided in request
           if (req.body.azureClientSecret !== undefined) {
-            await configManager.updateConfigs(
-              {
-                'azure:clientSecret': toNonBlankStringOrUndefined(
-                  req.body.azureClientSecret,
-                ),
-              },
-              { skipPubsub: true, removeIfUndefined: true },
-            );
+            await configManager.updateConfigs({
+              'azure:clientSecret': toNonBlankStringOrUndefined(req.body.azureClientSecret),
+            }, { skipPubsub: true, removeIfUndefined: true });
           }
-        } catch (err) {
+        }
+        catch (err) {
           const msg = `Error occurred in updating Azure settings: ${err.message}`;
           logger.error('Error', err);
           return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
@@ -343,9 +275,7 @@ module.exports = (crowi) => {
             gcsApiKeyJsonPath: configManager.getConfig('gcs:apiKeyJsonPath'),
             gcsBucket: configManager.getConfig('gcs:bucket'),
             gcsUploadNamespace: configManager.getConfig('gcs:uploadNamespace'),
-            gcsReferenceFileWithRelayMode: configManager.getConfig(
-              'gcs:referenceFileWithRelayMode',
-            ),
+            gcsReferenceFileWithRelayMode: configManager.getConfig('gcs:referenceFileWithRelayMode'),
           };
         }
 
@@ -356,9 +286,7 @@ module.exports = (crowi) => {
             s3CustomEndpoint: configManager.getConfig('aws:s3CustomEndpoint'),
             s3Bucket: configManager.getConfig('aws:s3Bucket'),
             s3AccessKeyId: configManager.getConfig('aws:s3AccessKeyId'),
-            s3ReferenceFileWithRelayMode: configManager.getConfig(
-              'aws:referenceFileWithRelayMode',
-            ),
+            s3ReferenceFileWithRelayMode: configManager.getConfig('aws:referenceFileWithRelayMode'),
           };
         }
 
@@ -368,30 +296,23 @@ module.exports = (crowi) => {
             azureTenantId: configManager.getConfig('azure:tenantId'),
             azureClientId: configManager.getConfig('azure:clientId'),
             azureClientSecret: configManager.getConfig('azure:clientSecret'),
-            azureStorageAccountName: configManager.getConfig(
-              'azure:storageAccountName',
-            ),
-            azureStorageContainerName: configManager.getConfig(
-              'azure:storageContainerName',
-            ),
-            azureReferenceFileWithRelayMode: configManager.getConfig(
-              'azure:referenceFileWithRelayMode',
-            ),
+            azureStorageAccountName: configManager.getConfig('azure:storageAccountName'),
+            azureStorageContainerName: configManager.getConfig('azure:storageContainerName'),
+            azureReferenceFileWithRelayMode: configManager.getConfig('azure:referenceFileWithRelayMode'),
           };
         }
 
-        const parameters = {
-          action: SupportedAction.ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
-        };
+        const parameters = { action: SupportedAction.ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE };
         activityEvent.emit('update', res.locals.activity._id, parameters);
         return res.apiv3({ responseParams });
-      } catch (err) {
+      }
+      catch (err) {
         const msg = 'Error occurred in retrieving file upload configurations';
         logger.error('Error', err);
         return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
       }
-    },
-  );
+
+    });
 
   return router;
 };

+ 175 - 342
apps/app/src/server/routes/apiv3/app-settings/index.ts

@@ -1,4 +1,6 @@
-import { ConfigSource, SCOPE } from '@growi/core/dist/interfaces';
+import {
+  ConfigSource, SCOPE,
+} from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { body } from 'express-validator';
 
@@ -13,6 +15,7 @@ import loggerFactory from '~/utils/logger';
 import { generateAddActivityMiddleware } from '../../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 
+
 const logger = loggerFactory('growi:routes:apiv3:app-settings');
 
 const { pathUtils } = require('@growi/core/dist/utils');
@@ -20,6 +23,7 @@ const express = require('express');
 
 const router = express.Router();
 
+
 /**
  * @swagger
  *
@@ -313,9 +317,7 @@ const router = express.Router();
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../../middlewares/login-required')(
-    crowi,
-  );
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
   const adminRequired = require('../../../middlewares/admin-required')(crowi);
   const addActivity = generateAddActivityMiddleware();
 
@@ -331,39 +333,29 @@ module.exports = (crowi) => {
     ],
     siteUrlSetting: [
       // https://regex101.com/r/5Xef8V/1
-      body('siteUrl')
-        .trim()
-        .matches(/^(https?:\/\/)/)
-        .isURL({ require_tld: false }),
+      body('siteUrl').trim().matches(/^(https?:\/\/)/).isURL({ require_tld: false }),
     ],
     mailSetting: [
-      body('fromAddress')
-        .trim()
-        .if((value) => value !== '')
-        .isEmail(),
+      body('fromAddress').trim().if(value => value !== '').isEmail(),
       body('transmissionMethod').isIn(['smtp', 'ses']),
     ],
     smtpSetting: [
       body('smtpHost').trim(),
-      body('smtpPort')
-        .trim()
-        .if((value) => value !== '')
-        .isPort(),
+      body('smtpPort').trim().if(value => value !== '').isPort(),
       body('smtpUser').trim(),
       body('smtpPassword').trim(),
     ],
     sesSetting: [
-      body('sesAccessKeyId')
-        .trim()
-        .if((value) => value !== '')
-        .matches(/^[\da-zA-Z]+$/),
+      body('sesAccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
       body('sesSecretAccessKey').trim(),
     ],
     pageBulkExportSettings: [
       body('isBulkExportPagesEnabled').isBoolean(),
       body('bulkExportDownloadExpirationSeconds').isInt(),
     ],
-    maintenanceMode: [body('flag').isBoolean()],
+    maintenanceMode: [
+      body('flag').isBoolean(),
+    ],
   };
 
   /**
@@ -388,141 +380,74 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/AppSettingParams'
    */
-  router.get(
-    '/',
-    accessTokenParser([SCOPE.READ.ADMIN.APP], { acceptLegacy: true }),
-    loginRequiredStrictly,
-    adminRequired,
-    async (req, res) => {
-      const appSettingsParams = {
-        title: configManager.getConfig('app:title'),
-        confidential: configManager.getConfig('app:confidential'),
-        globalLang: configManager.getConfig('app:globalLang'),
-        isEmailPublishedForNewUser: configManager.getConfig(
-          'customize:isEmailPublishedForNewUser',
-        ),
-        fileUpload: configManager.getConfig('app:fileUpload'),
-        useOnlyEnvVarsForIsBulkExportPagesEnabled: configManager.getConfig(
-          'env:useOnlyEnvVars:app:isBulkExportPagesEnabled',
-        ),
-        isV5Compatible: configManager.getConfig('app:isV5Compatible'),
-        siteUrl: configManager.getConfig('app:siteUrl'),
-        siteUrlUseOnlyEnvVars: configManager.getConfig(
-          'env:useOnlyEnvVars:app:siteUrl',
-        ),
-        envSiteUrl: configManager.getConfig('app:siteUrl', ConfigSource.env),
-        isMailerSetup: crowi.mailService.isMailerSetup,
-        fromAddress: configManager.getConfig('mail:from'),
-
-        transmissionMethod: configManager.getConfig('mail:transmissionMethod'),
-        smtpHost: configManager.getConfig('mail:smtpHost'),
-        smtpPort: configManager.getConfig('mail:smtpPort'),
-        smtpUser: configManager.getConfig('mail:smtpUser'),
-        smtpPassword: configManager.getConfig('mail:smtpPassword'),
-        sesAccessKeyId: configManager.getConfig('mail:sesAccessKeyId'),
-        sesSecretAccessKey: configManager.getConfig('mail:sesSecretAccessKey'),
-
-        fileUploadType: configManager.getConfig('app:fileUploadType'),
-        envFileUploadType: configManager.getConfig(
-          'app:fileUploadType',
-          ConfigSource.env,
-        ),
-        useOnlyEnvVarForFileUploadType: configManager.getConfig(
-          'env:useOnlyEnvVars:app:fileUploadType',
-        ),
-
-        s3Region: configManager.getConfig('aws:s3Region'),
-        s3CustomEndpoint: configManager.getConfig('aws:s3CustomEndpoint'),
-        s3Bucket: configManager.getConfig('aws:s3Bucket'),
-        s3AccessKeyId: configManager.getConfig('aws:s3AccessKeyId'),
-        s3ReferenceFileWithRelayMode: configManager.getConfig(
-          'aws:referenceFileWithRelayMode',
-        ),
-
-        gcsUseOnlyEnvVars: configManager.getConfig('env:useOnlyEnvVars:gcs'),
-        gcsApiKeyJsonPath: configManager.getConfig('gcs:apiKeyJsonPath'),
-        gcsBucket: configManager.getConfig('gcs:bucket'),
-        gcsUploadNamespace: configManager.getConfig('gcs:uploadNamespace'),
-        gcsReferenceFileWithRelayMode: configManager.getConfig(
-          'gcs:referenceFileWithRelayMode',
-        ),
-
-        envGcsApiKeyJsonPath: configManager.getConfig(
-          'gcs:apiKeyJsonPath',
-          ConfigSource.env,
-        ),
-        envGcsBucket: configManager.getConfig('gcs:bucket', ConfigSource.env),
-        envGcsUploadNamespace: configManager.getConfig(
-          'gcs:uploadNamespace',
-          ConfigSource.env,
-        ),
-
-        azureUseOnlyEnvVars: configManager.getConfig(
-          'env:useOnlyEnvVars:azure',
-        ),
-        azureTenantId: configManager.getConfig(
-          'azure:tenantId',
-          ConfigSource.db,
-        ),
-        azureClientId: configManager.getConfig(
-          'azure:clientId',
-          ConfigSource.db,
-        ),
-        azureClientSecret: configManager.getConfig(
-          'azure:clientSecret',
-          ConfigSource.db,
-        ),
-        azureStorageAccountName: configManager.getConfig(
-          'azure:storageAccountName',
-          ConfigSource.db,
-        ),
-        azureStorageContainerName: configManager.getConfig(
-          'azure:storageContainerName',
-          ConfigSource.db,
-        ),
-        azureReferenceFileWithRelayMode: configManager.getConfig(
-          'azure:referenceFileWithRelayMode',
-        ),
-
-        envAzureTenantId: configManager.getConfig(
-          'azure:tenantId',
-          ConfigSource.env,
-        ),
-        envAzureClientId: configManager.getConfig(
-          'azure:clientId',
-          ConfigSource.env,
-        ),
-        envAzureClientSecret: configManager.getConfig(
-          'azure:clientSecret',
-          ConfigSource.env,
-        ),
-        envAzureStorageAccountName: configManager.getConfig(
-          'azure:storageAccountName',
-          ConfigSource.env,
-        ),
-        envAzureStorageContainerName: configManager.getConfig(
-          'azure:storageContainerName',
-          ConfigSource.env,
-        ),
-
-        isMaintenanceMode: configManager.getConfig('app:isMaintenanceMode'),
-
-        isBulkExportPagesEnabled: configManager.getConfig(
-          'app:isBulkExportPagesEnabled',
-        ),
-        envIsBulkExportPagesEnabled: configManager.getConfig(
-          'app:isBulkExportPagesEnabled',
-        ),
-        bulkExportDownloadExpirationSeconds: configManager.getConfig(
-          'app:bulkExportDownloadExpirationSeconds',
-        ),
-        // TODO: remove this property when bulk export can be relased for cloud (https://redmine.weseek.co.jp/issues/163220)
-        isBulkExportDisabledForCloud:
-          configManager.getConfig('app:growiCloudUri') != null,
-      };
-      return res.apiv3({ appSettingsParams });
-    },
-  );
+  router.get('/', accessTokenParser([SCOPE.READ.ADMIN.APP], { acceptLegacy: true }), loginRequiredStrictly, adminRequired, async(req, res) => {
+    const appSettingsParams = {
+      title: configManager.getConfig('app:title'),
+      confidential: configManager.getConfig('app:confidential'),
+      globalLang: configManager.getConfig('app:globalLang'),
+      isEmailPublishedForNewUser: configManager.getConfig('customize:isEmailPublishedForNewUser'),
+      fileUpload: configManager.getConfig('app:fileUpload'),
+      useOnlyEnvVarsForIsBulkExportPagesEnabled: configManager.getConfig('env:useOnlyEnvVars:app:isBulkExportPagesEnabled'),
+      isV5Compatible: configManager.getConfig('app:isV5Compatible'),
+      siteUrl: configManager.getConfig('app:siteUrl'),
+      siteUrlUseOnlyEnvVars: configManager.getConfig('env:useOnlyEnvVars:app:siteUrl'),
+      envSiteUrl: configManager.getConfig('app:siteUrl', ConfigSource.env),
+      isMailerSetup: crowi.mailService.isMailerSetup,
+      fromAddress: configManager.getConfig('mail:from'),
+
+      transmissionMethod: configManager.getConfig('mail:transmissionMethod'),
+      smtpHost: configManager.getConfig('mail:smtpHost'),
+      smtpPort: configManager.getConfig('mail:smtpPort'),
+      smtpUser: configManager.getConfig('mail:smtpUser'),
+      smtpPassword: configManager.getConfig('mail:smtpPassword'),
+      sesAccessKeyId: configManager.getConfig('mail:sesAccessKeyId'),
+      sesSecretAccessKey: configManager.getConfig('mail:sesSecretAccessKey'),
+
+      fileUploadType: configManager.getConfig('app:fileUploadType'),
+      envFileUploadType: configManager.getConfig('app:fileUploadType', ConfigSource.env),
+      useOnlyEnvVarForFileUploadType: configManager.getConfig('env:useOnlyEnvVars:app:fileUploadType'),
+
+      s3Region: configManager.getConfig('aws:s3Region'),
+      s3CustomEndpoint: configManager.getConfig('aws:s3CustomEndpoint'),
+      s3Bucket: configManager.getConfig('aws:s3Bucket'),
+      s3AccessKeyId: configManager.getConfig('aws:s3AccessKeyId'),
+      s3ReferenceFileWithRelayMode: configManager.getConfig('aws:referenceFileWithRelayMode'),
+
+      gcsUseOnlyEnvVars: configManager.getConfig('env:useOnlyEnvVars:gcs'),
+      gcsApiKeyJsonPath: configManager.getConfig('gcs:apiKeyJsonPath'),
+      gcsBucket: configManager.getConfig('gcs:bucket'),
+      gcsUploadNamespace: configManager.getConfig('gcs:uploadNamespace'),
+      gcsReferenceFileWithRelayMode: configManager.getConfig('gcs:referenceFileWithRelayMode'),
+
+      envGcsApiKeyJsonPath: configManager.getConfig('gcs:apiKeyJsonPath', ConfigSource.env),
+      envGcsBucket: configManager.getConfig('gcs:bucket', ConfigSource.env),
+      envGcsUploadNamespace: configManager.getConfig('gcs:uploadNamespace', ConfigSource.env),
+
+      azureUseOnlyEnvVars: configManager.getConfig('env:useOnlyEnvVars:azure'),
+      azureTenantId: configManager.getConfig('azure:tenantId', ConfigSource.db),
+      azureClientId: configManager.getConfig('azure:clientId', ConfigSource.db),
+      azureClientSecret: configManager.getConfig('azure:clientSecret', ConfigSource.db),
+      azureStorageAccountName: configManager.getConfig('azure:storageAccountName', ConfigSource.db),
+      azureStorageContainerName: configManager.getConfig('azure:storageContainerName', ConfigSource.db),
+      azureReferenceFileWithRelayMode: configManager.getConfig('azure:referenceFileWithRelayMode'),
+
+      envAzureTenantId: configManager.getConfig('azure:tenantId', ConfigSource.env),
+      envAzureClientId: configManager.getConfig('azure:clientId', ConfigSource.env),
+      envAzureClientSecret: configManager.getConfig('azure:clientSecret', ConfigSource.env),
+      envAzureStorageAccountName: configManager.getConfig('azure:storageAccountName', ConfigSource.env),
+      envAzureStorageContainerName: configManager.getConfig('azure:storageContainerName', ConfigSource.env),
+
+      isMaintenanceMode: configManager.getConfig('app:isMaintenanceMode'),
+
+      isBulkExportPagesEnabled: configManager.getConfig('app:isBulkExportPagesEnabled'),
+      envIsBulkExportPagesEnabled: configManager.getConfig('app:isBulkExportPagesEnabled'),
+      bulkExportDownloadExpirationSeconds: configManager.getConfig('app:bulkExportDownloadExpirationSeconds'),
+      // TODO: remove this property when bulk export can be relased for cloud (https://redmine.weseek.co.jp/issues/163220)
+      isBulkExportDisabledForCloud: configManager.getConfig('app:growiCloudUri') != null,
+    };
+    return res.apiv3({ appSettingsParams });
+
+  });
 
   /**
    * @swagger
@@ -552,21 +477,14 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/AppSettingPutParams'
    */
-  router.put(
-    '/app-setting',
-    accessTokenParser([SCOPE.WRITE.ADMIN.APP]),
-    loginRequiredStrictly,
-    adminRequired,
-    addActivity,
-    validator.appSetting,
-    apiV3FormValidator,
-    async (req, res) => {
+  router.put('/app-setting', accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity,
+    validator.appSetting, apiV3FormValidator,
+    async(req, res) => {
       const requestAppSettingParams = {
         'app:title': req.body.title,
         'app:confidential': req.body.confidential,
         'app:globalLang': req.body.globalLang,
-        'customize:isEmailPublishedForNewUser':
-          req.body.isEmailPublishedForNewUser,
+        'customize:isEmailPublishedForNewUser': req.body.isEmailPublishedForNewUser,
         'app:fileUpload': req.body.fileUpload,
       };
 
@@ -576,25 +494,22 @@ module.exports = (crowi) => {
           title: configManager.getConfig('app:title'),
           confidential: configManager.getConfig('app:confidential'),
           globalLang: configManager.getConfig('app:globalLang'),
-          isEmailPublishedForNewUser: configManager.getConfig(
-            'customize:isEmailPublishedForNewUser',
-          ),
+          isEmailPublishedForNewUser: configManager.getConfig('customize:isEmailPublishedForNewUser'),
           fileUpload: configManager.getConfig('app:fileUpload'),
         };
 
-        const parameters = {
-          action: SupportedAction.ACTION_ADMIN_APP_SETTINGS_UPDATE,
-        };
+        const parameters = { action: SupportedAction.ACTION_ADMIN_APP_SETTINGS_UPDATE };
         activityEvent.emit('update', res.locals.activity._id, parameters);
 
         return res.apiv3({ appSettingParams });
-      } catch (err) {
+      }
+      catch (err) {
         const msg = 'Error occurred in updating app setting';
         logger.error('Error', err);
         return res.apiv3Err(new ErrorV3(msg, 'update-appSetting-failed'));
       }
-    },
-  );
+
+    });
 
   /**
    * @swagger
@@ -628,24 +543,14 @@ module.exports = (crowi) => {
    *                          description: Site URL. e.g. https://example.com, https://example.com:3000
    *                          example: 'http://localhost:3000'
    */
-  router.put(
-    '/site-url-setting',
-    accessTokenParser([SCOPE.WRITE.ADMIN.APP]),
-    loginRequiredStrictly,
-    adminRequired,
-    addActivity,
-    validator.siteUrlSetting,
-    apiV3FormValidator,
-    async (req, res) => {
-      const useOnlyEnvVars = configManager.getConfig(
-        'env:useOnlyEnvVars:app:siteUrl',
-      );
+  router.put('/site-url-setting', accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity,
+    validator.siteUrlSetting, apiV3FormValidator,
+    async(req, res) => {
+      const useOnlyEnvVars = configManager.getConfig('env:useOnlyEnvVars:app:siteUrl');
 
       if (useOnlyEnvVars) {
         const msg = 'Updating the Site URL is prohibited on this system.';
-        return res.apiv3Err(
-          new ErrorV3(msg, 'update-siteUrlSetting-prohibited'),
-        );
+        return res.apiv3Err(new ErrorV3(msg, 'update-siteUrlSetting-prohibited'));
       }
 
       const requestSiteUrlSettingParams = {
@@ -658,18 +563,17 @@ module.exports = (crowi) => {
           siteUrl: configManager.getConfig('app:siteUrl'),
         };
 
-        const parameters = {
-          action: SupportedAction.ACTION_ADMIN_SITE_URL_UPDATE,
-        };
+        const parameters = { action: SupportedAction.ACTION_ADMIN_SITE_URL_UPDATE };
         activityEvent.emit('update', res.locals.activity._id, parameters);
         return res.apiv3({ siteUrlSettingParams });
-      } catch (err) {
+      }
+      catch (err) {
         const msg = 'Error occurred in updating site url setting';
         logger.error('Error', err);
         return res.apiv3Err(new ErrorV3(msg, 'update-siteUrlSetting-failed'));
       }
-    },
-  );
+
+    });
 
   /**
    * send mail (Promise wrapper)
@@ -679,7 +583,8 @@ module.exports = (crowi) => {
       smtpClient.sendMail(options, (err, res) => {
         if (err) {
           reject(err);
-        } else {
+        }
+        else {
           resolve(res);
         }
       });
@@ -690,6 +595,7 @@ module.exports = (crowi) => {
    * validate mail setting send test mail
    */
   async function sendTestEmail(destinationAddress) {
+
     const { mailService } = crowi;
 
     if (!mailService.isMailerSetup) {
@@ -739,13 +645,13 @@ module.exports = (crowi) => {
     await sendMailPromiseWrapper(smtpClient, mailOptions);
   }
 
-  const updateMailSettinConfig = async (requestMailSettingParams) => {
-    const { mailService } = crowi;
+  const updateMailSettinConfig = async function(requestMailSettingParams) {
+    const {
+      mailService,
+    } = crowi;
 
     // update config without publishing S2sMessage
-    await configManager.updateConfigs(requestMailSettingParams, {
-      skipPubsub: true,
-    });
+    await configManager.updateConfigs(requestMailSettingParams, { skipPubsub: true });
 
     await mailService.initialize();
     mailService.publishUpdatedMessage();
@@ -790,15 +696,9 @@ module.exports = (crowi) => {
    *                      type: object
    *                      $ref: '#/components/schemas/SmtpSettingResponseParams'
    */
-  router.put(
-    '/smtp-setting',
-    accessTokenParser([SCOPE.WRITE.ADMIN.APP]),
-    loginRequiredStrictly,
-    adminRequired,
-    addActivity,
-    validator.smtpSetting,
-    apiV3FormValidator,
-    async (req, res) => {
+  router.put('/smtp-setting', accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity,
+    validator.smtpSetting, apiV3FormValidator,
+    async(req, res) => {
       const requestMailSettingParams = {
         'mail:from': req.body.fromAddress,
         'mail:transmissionMethod': req.body.transmissionMethod,
@@ -809,21 +709,17 @@ module.exports = (crowi) => {
       };
 
       try {
-        const mailSettingParams = await updateMailSettinConfig(
-          requestMailSettingParams,
-        );
-        const parameters = {
-          action: SupportedAction.ACTION_ADMIN_MAIL_SMTP_UPDATE,
-        };
+        const mailSettingParams = await updateMailSettinConfig(requestMailSettingParams);
+        const parameters = { action: SupportedAction.ACTION_ADMIN_MAIL_SMTP_UPDATE };
         activityEvent.emit('update', res.locals.activity._id, parameters);
         return res.apiv3({ mailSettingParams });
-      } catch (err) {
+      }
+      catch (err) {
         const msg = 'Error occurred in updating smtp setting';
         logger.error('Error', err);
         return res.apiv3Err(new ErrorV3(msg, 'update-smtpSetting-failed'));
       }
-    },
-  );
+    });
 
   /**
    * @swagger
@@ -844,30 +740,22 @@ module.exports = (crowi) => {
    *                  type: object
    *                  description: Empty object
    */
-  router.post(
-    '/smtp-test',
-    accessTokenParser([SCOPE.WRITE.ADMIN.APP]),
-    loginRequiredStrictly,
-    adminRequired,
-    addActivity,
-    async (req, res) => {
-      const { t } = await getTranslation({ lang: req.user.lang });
+  router.post('/smtp-test', accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
+    const { t } = await getTranslation({ lang: req.user.lang });
 
-      try {
-        await sendTestEmail(req.user.email);
-        const parameters = {
-          action: SupportedAction.ACTION_ADMIN_MAIL_TEST_SUBMIT,
-        };
-        activityEvent.emit('update', res.locals.activity._id, parameters);
-        return res.apiv3({});
-      } catch (err) {
-        const msg = t('validation.failed_to_send_a_test_email');
-        logger.error('Error', err);
-        logger.debug('Error validate mail setting: ', err);
-        return res.apiv3Err(new ErrorV3(msg, 'send-email-with-smtp-failed'));
-      }
-    },
-  );
+    try {
+      await sendTestEmail(req.user.email);
+      const parameters = { action: SupportedAction.ACTION_ADMIN_MAIL_TEST_SUBMIT };
+      activityEvent.emit('update', res.locals.activity._id, parameters);
+      return res.apiv3({});
+    }
+    catch (err) {
+      const msg = t('validation.failed_to_send_a_test_email');
+      logger.error('Error', err);
+      logger.debug('Error validate mail setting: ', err);
+      return res.apiv3Err(new ErrorV3(msg, 'send-email-with-smtp-failed'));
+    }
+  });
 
   /**
    * @swagger
@@ -893,15 +781,9 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/SesSettingResponseParams'
    */
-  router.put(
-    '/ses-setting',
-    accessTokenParser([SCOPE.WRITE.ADMIN.APP]),
-    loginRequiredStrictly,
-    adminRequired,
-    addActivity,
-    validator.sesSetting,
-    apiV3FormValidator,
-    async (req, res) => {
+  router.put('/ses-setting', accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity,
+    validator.sesSetting, apiV3FormValidator,
+    async(req, res) => {
       const { mailService } = crowi;
 
       const requestSesSettingParams = {
@@ -911,12 +793,11 @@ module.exports = (crowi) => {
         'mail:sesSecretAccessKey': req.body.sesSecretAccessKey,
       };
 
-      let mailSettingParams: Awaited<ReturnType<typeof updateMailSettinConfig>>;
+      let mailSettingParams;
       try {
-        mailSettingParams = await updateMailSettinConfig(
-          requestSesSettingParams,
-        );
-      } catch (err) {
+        mailSettingParams = await updateMailSettinConfig(requestSesSettingParams);
+      }
+      catch (err) {
         const msg = 'Error occurred in updating ses setting';
         logger.error('Error', err);
         return res.apiv3Err(new ErrorV3(msg, 'update-ses-setting-failed'));
@@ -924,57 +805,41 @@ module.exports = (crowi) => {
 
       await mailService.initialize();
       mailService.publishUpdatedMessage();
-      const parameters = {
-        action: SupportedAction.ACTION_ADMIN_MAIL_SES_UPDATE,
-      };
+      const parameters = { action: SupportedAction.ACTION_ADMIN_MAIL_SES_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
       return res.apiv3({ mailSettingParams });
-    },
-  );
+    });
 
   router.use('/file-upload-setting', require('./file-upload-setting')(crowi));
 
-  router.put(
-    '/page-bulk-export-settings',
-    accessTokenParser([SCOPE.WRITE.ADMIN.APP]),
-    loginRequiredStrictly,
-    adminRequired,
-    addActivity,
-    validator.pageBulkExportSettings,
-    apiV3FormValidator,
-    async (req, res) => {
+
+  router.put('/page-bulk-export-settings',
+    accessTokenParser([SCOPE.WRITE.ADMIN.APP]), loginRequiredStrictly, adminRequired, addActivity, validator.pageBulkExportSettings, apiV3FormValidator,
+    async(req, res) => {
       const requestParams = {
         'app:isBulkExportPagesEnabled': req.body.isBulkExportPagesEnabled,
-        'app:bulkExportDownloadExpirationSeconds':
-          req.body.bulkExportDownloadExpirationSeconds,
+        'app:bulkExportDownloadExpirationSeconds': req.body.bulkExportDownloadExpirationSeconds,
       };
 
       try {
         await configManager.updateConfigs(requestParams, { skipPubsub: true });
         const responseParams = {
-          isBulkExportPagesEnabled: configManager.getConfig(
-            'app:isBulkExportPagesEnabled',
-          ),
-          bulkExportDownloadExpirationSeconds: configManager.getConfig(
-            'app:bulkExportDownloadExpirationSeconds',
-          ),
+          isBulkExportPagesEnabled: configManager.getConfig('app:isBulkExportPagesEnabled'),
+          bulkExportDownloadExpirationSeconds: configManager.getConfig('app:bulkExportDownloadExpirationSeconds'),
         };
 
-        const parameters = {
-          action: SupportedAction.ACTION_ADMIN_APP_SETTINGS_UPDATE,
-        };
+        const parameters = { action: SupportedAction.ACTION_ADMIN_APP_SETTINGS_UPDATE };
         activityEvent.emit('update', res.locals.activity._id, parameters);
 
         return res.apiv3({ responseParams });
-      } catch (err) {
+      }
+      catch (err) {
         const msg = 'Error occurred in updating page bulk export settings';
         logger.error('Error', err);
-        return res.apiv3Err(
-          new ErrorV3(msg, 'update-page-bulk-export-settings-failed'),
-        );
+        return res.apiv3Err(new ErrorV3(msg, 'update-page-bulk-export-settings-failed'));
       }
-    },
-  );
+
+    });
 
   /**
    * @swagger
@@ -1000,39 +865,27 @@ module.exports = (crowi) => {
    *                      description: is V5 compatible, or not
    *                      example: true
    */
-  router.post(
-    '/v5-schema-migration',
-    accessTokenParser([SCOPE.WRITE.ADMIN.APP], { acceptLegacy: true }),
-    loginRequiredStrictly,
-    adminRequired,
-    async (req, res) => {
+  router.post('/v5-schema-migration',
+    accessTokenParser([SCOPE.WRITE.ADMIN.APP], { acceptLegacy: true }), loginRequiredStrictly, adminRequired, async(req, res) => {
       const isMaintenanceMode = crowi.appService.isMaintenanceMode();
       if (!isMaintenanceMode) {
-        return res.apiv3Err(
-          new ErrorV3(
-            'GROWI is not maintenance mode. To import data, please activate the maintenance mode first.',
-            'not_maintenance_mode',
-          ),
-        );
+        return res.apiv3Err(new ErrorV3('GROWI is not maintenance mode. To import data, please activate the maintenance mode first.', 'not_maintenance_mode'));
       }
 
       const isV5Compatible = configManager.getConfig('app:isV5Compatible');
 
       try {
         if (!isV5Compatible) {
-          // This method throws and emit socketIo event when error occurs
+        // This method throws and emit socketIo event when error occurs
           crowi.pageService.normalizeAllPublicPages();
         }
-      } catch (err) {
-        return res.apiv3Err(
-          new ErrorV3(`Failed to migrate pages: ${err.message}`),
-          500,
-        );
+      }
+      catch (err) {
+        return res.apiv3Err(new ErrorV3(`Failed to migrate pages: ${err.message}`), 500);
       }
 
       return res.apiv3({ isV5Compatible });
-    },
-  );
+    });
 
   /**
    * @swagger
@@ -1067,47 +920,28 @@ module.exports = (crowi) => {
    *                      description: true if maintenance mode is enabled
    *                      example: true
    */
-  router.post(
-    '/maintenance-mode',
+  router.post('/maintenance-mode',
     accessTokenParser([SCOPE.WRITE.ADMIN.APP], { acceptLegacy: true }),
-    loginRequiredStrictly,
-    adminRequired,
-    addActivity,
-    validator.maintenanceMode,
-    apiV3FormValidator,
-    async (req, res) => {
+    loginRequiredStrictly, adminRequired, addActivity, validator.maintenanceMode, apiV3FormValidator, async(req, res) => {
       const { flag } = req.body;
       const parameters = {};
       try {
         if (flag) {
           await crowi.appService.startMaintenanceMode();
-          Object.assign(parameters, {
-            action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
-          });
-        } else {
+          Object.assign(parameters, { action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_ENABLED });
+        }
+        else {
           await crowi.appService.endMaintenanceMode();
-          Object.assign(parameters, {
-            action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_DISABLED,
-          });
+          Object.assign(parameters, { action: SupportedAction.ACTION_ADMIN_MAINTENANCEMODE_DISABLED });
         }
-      } catch (err) {
+      }
+      catch (err) {
         logger.error(err);
         if (flag) {
-          res.apiv3Err(
-            new ErrorV3(
-              'Failed to start maintenance mode',
-              'failed_to_start_maintenance_mode',
-            ),
-            500,
-          );
-        } else {
-          res.apiv3Err(
-            new ErrorV3(
-              'Failed to end maintenance mode',
-              'failed_to_end_maintenance_mode',
-            ),
-            500,
-          );
+          res.apiv3Err(new ErrorV3('Failed to start maintenance mode', 'failed_to_start_maintenance_mode'), 500);
+        }
+        else {
+          res.apiv3Err(new ErrorV3('Failed to end maintenance mode', 'failed_to_end_maintenance_mode'), 500);
         }
       }
 
@@ -1116,8 +950,7 @@ module.exports = (crowi) => {
       }
 
       res.apiv3({ flag });
-    },
-  );
+    });
 
   return router;
 };

+ 11 - 16
apps/app/src/server/routes/apiv3/page/check-page-existence.ts

@@ -1,5 +1,4 @@
 import type { IPage, IUserHasId } from '@growi/core';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { normalizePath } from '@growi/core/dist/utils/path-utils';
 import type { Request, RequestHandler } from 'express';
@@ -7,6 +6,7 @@ import type { ValidationChain } from 'express-validator';
 import { query } from 'express-validator';
 import mongoose from 'mongoose';
 
+import { SCOPE } from '@growi/core/dist/interfaces';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
@@ -15,27 +15,24 @@ import loggerFactory from '~/utils/logger';
 
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page:check-page-existence');
 
+
 type ReqQuery = {
-  path: string;
-};
+  path: string,
+}
 
 interface Req extends Request<ReqQuery, ApiV3Response> {
-  user: IUserHasId;
+  user: IUserHasId,
 }
 
 type CreatePageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 
-export const checkPageExistenceHandlersFactory: CreatePageHandlersFactory = (
-  crowi,
-) => {
+export const checkPageExistenceHandlersFactory: CreatePageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
 
-  const loginRequired = require('../../../middlewares/login-required')(
-    crowi,
-    true,
-  );
+  const loginRequired = require('../../../middlewares/login-required')(crowi, true);
 
   // define validators for req.body
   const validator: ValidationChain[] = [
@@ -43,11 +40,9 @@ export const checkPageExistenceHandlersFactory: CreatePageHandlersFactory = (
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
-    loginRequired,
-    validator,
-    apiV3FormValidator,
-    async (req: Req, res: ApiV3Response) => {
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequired,
+    validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
       const { path } = req.query;
 
       if (path == null || Array.isArray(path)) {

+ 65 - 154
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -1,16 +1,10 @@
 import { allOrigin } from '@growi/core';
-import type { IPage, IUser, IUserHasId } from '@growi/core/dist/interfaces';
-import { SCOPE } from '@growi/core/dist/interfaces';
+import type {
+  IPage, IUser, IUserHasId,
+} from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
-import {
-  isCreatablePage,
-  isUserPage,
-  isUsersHomepage,
-} from '@growi/core/dist/utils/page-path-utils';
-import {
-  attachTitleHeader,
-  normalizePath,
-} from '@growi/core/dist/utils/path-utils';
+import { isCreatablePage, isUserPage, isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
+import { attachTitleHeader, normalizePath } from '@growi/core/dist/utils/path-utils';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { body } from 'express-validator';
@@ -22,16 +16,14 @@ import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
 import type { IOptionsForCreate } from '~/interfaces/page';
+import { SCOPE } from '@growi/core/dist/interfaces';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 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 { serializePageSecurely, serializeRevisionSecurely } from '~/server/models/serializers';
 import { configManager } from '~/server/service/config-manager';
 import { getTranslation } from '~/server/service/i18next';
 import loggerFactory from '~/utils/logger';
@@ -40,29 +32,21 @@ import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import { excludeReadOnlyUser } from '../../../middlewares/exclude-read-only-user';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page:create-page');
 
-async function generateUntitledPath(
-  parentPath: string,
-  basePathname: string,
-  index = 1,
-): Promise<string> {
+
+async function generateUntitledPath(parentPath: string, basePathname: string, index = 1): Promise<string> {
   const Page = mongoose.model<IPage>('Page');
 
-  const path = normalizePath(
-    `${normalizePath(parentPath)}/${basePathname}-${index}`,
-  );
-  if ((await Page.exists({ path, isEmpty: false })) != null) {
+  const path = normalizePath(`${normalizePath(parentPath)}/${basePathname}-${index}`);
+  if (await Page.exists({ path, isEmpty: false }) != null) {
     return generateUntitledPath(parentPath, basePathname, index + 1);
   }
   return path;
 }
 
-async function determinePath(
-  _parentPath?: string,
-  _path?: string,
-  optionalParentPath?: string,
-): Promise<string> {
+async function determinePath(_parentPath?: string, _path?: string, optionalParentPath?: string): Promise<string> {
   const { t } = await getTranslation();
 
   const basePathname = t?.('create_page.untitled') || 'Untitled';
@@ -106,85 +90,53 @@ async function determinePath(
   return generateUntitledPath('/', basePathname);
 }
 
-type ReqBody = IApiv3PageCreateParams;
+
+type ReqBody = IApiv3PageCreateParams
 
 interface CreatePageRequest extends Request<undefined, ApiV3Response, ReqBody> {
-  user: IUserHasId;
+  user: IUserHasId,
 }
 
 type CreatePageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 
 export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
-  const User = mongoose.model<IUser, { isExistUserByUserPagePath: any }>(
-    'User',
-  );
+  const User = mongoose.model<IUser, { isExistUserByUserPagePath: any }>('User');
+
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
-  const loginRequiredStrictly = require('../../../middlewares/login-required')(
-    crowi,
-  );
 
   // define validators for req.body
   const validator: ValidationChain[] = [
-    body('path')
-      .optional()
-      .not()
-      .isEmpty({ ignore_whitespace: true })
+    body('path').optional().not().isEmpty({ ignore_whitespace: true })
       .withMessage("Empty value is not allowed for 'path'"),
-    body('parentPath')
-      .optional()
-      .not()
-      .isEmpty({ ignore_whitespace: true })
+    body('parentPath').optional().not().isEmpty({ ignore_whitespace: true })
       .withMessage("Empty value is not allowed for 'parentPath'"),
-    body('optionalParentPath')
-      .optional()
-      .not()
-      .isEmpty({ ignore_whitespace: true })
+    body('optionalParentPath').optional().not().isEmpty({ ignore_whitespace: true })
       .withMessage("Empty value is not allowed for 'optionalParentPath'"),
-    body('body')
-      .optional()
-      .isString()
+    body('body').optional().isString()
       .withMessage('body must be string or undefined'),
-    body('grant')
-      .optional()
-      .isInt({ min: 0, max: 5 })
-      .withMessage('grant must be integer from 1 to 5'),
-    body('onlyInheritUserRelatedGrantedGroups')
-      .optional()
-      .isBoolean()
-      .withMessage('onlyInheritUserRelatedGrantedGroups must be boolean'),
-    body('overwriteScopesOfDescendants')
-      .optional()
-      .isBoolean()
-      .withMessage('overwriteScopesOfDescendants must be boolean'),
+    body('grant').optional().isInt({ min: 0, max: 5 }).withMessage('grant must be integer from 1 to 5'),
+    body('onlyInheritUserRelatedGrantedGroups').optional().isBoolean().withMessage('onlyInheritUserRelatedGrantedGroups must be boolean'),
+    body('overwriteScopesOfDescendants').optional().isBoolean().withMessage('overwriteScopesOfDescendants must be boolean'),
     body('pageTags').optional().isArray().withMessage('pageTags must be array'),
-    body('isSlackEnabled')
-      .optional()
-      .isBoolean()
-      .withMessage('isSlackEnabled must be boolean'),
-    body('slackChannels')
-      .optional()
-      .isString()
-      .withMessage('slackChannels must be string'),
+    body('isSlackEnabled').optional().isBoolean().withMessage('isSlackEnabled must be boolean'),
+    body('slackChannels').optional().isString().withMessage('slackChannels must be string'),
     body('wip').optional().isBoolean().withMessage('wip must be boolean'),
-    body('origin')
-      .optional()
-      .isIn(allOrigin)
-      .withMessage('origin must be "view" or "editor"'),
+    body('origin').optional().isIn(allOrigin).withMessage('origin must be "view" or "editor"'),
   ];
 
+
   async function determineBodyAndTags(
-    path: string,
-    _body: string | null | undefined,
-    _tags: string[] | null | undefined,
-  ): Promise<{ body: string; tags: string[] }> {
+      path: string,
+      _body: string | null | undefined, _tags: string[] | null | undefined,
+  ): Promise<{ body: string, tags: string[] }> {
+
     let body: string = _body ?? '';
     let tags: string[] = _tags ?? [];
 
     if (_body == null) {
-      const isEnabledAttachTitleHeader = await configManager.getConfig(
-        'customize:isEnabledAttachTitleHeader',
-      );
+      const isEnabledAttachTitleHeader = await configManager.getConfig('customize:isEnabledAttachTitleHeader');
       if (isEnabledAttachTitleHeader) {
         body += `${attachTitleHeader(path)}\n`;
       }
@@ -201,24 +153,14 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
     return { body, tags };
   }
 
-  async function saveTags({
-    createdPage,
-    pageTags,
-  }: {
-    createdPage: PageDocument;
-    pageTags: string[];
-  }) {
+  async function saveTags({ createdPage, pageTags }: { createdPage: PageDocument, pageTags: string[] }) {
     const tagEvent = crowi.event('tag');
     await PageTagRelation.updatePageTags(createdPage.id, pageTags);
     tagEvent.emit('update', createdPage, pageTags);
     return PageTagRelation.listTagNamesByPage(createdPage.id);
   }
 
-  async function postAction(
-    req: CreatePageRequest,
-    res: ApiV3Response,
-    createdPage: HydratedDocument<PageDocument>,
-  ) {
+  async function postAction(req: CreatePageRequest, res: ApiV3Response, createdPage: HydratedDocument<PageDocument>) {
     // persist activity
     const parameters = {
       targetModel: SupportedTargetModel.MODEL_PAGE,
@@ -230,12 +172,9 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
 
     // global notification
     try {
-      await crowi.globalNotificationService.fire(
-        GlobalNotificationSettingEvent.PAGE_CREATE,
-        createdPage,
-        req.user,
-      );
-    } catch (err) {
+      await crowi.globalNotificationService.fire(GlobalNotificationSettingEvent.PAGE_CREATE, createdPage, req.user);
+    }
+    catch (err) {
       logger.error('Create grobal notification failed', err);
     }
 
@@ -243,42 +182,34 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
     const { isSlackEnabled, slackChannels } = req.body;
     if (isSlackEnabled) {
       try {
-        const results = await crowi.userNotificationService.fire(
-          createdPage,
-          req.user,
-          slackChannels,
-          'create',
-        );
+        const results = await crowi.userNotificationService.fire(createdPage, req.user, slackChannels, 'create');
         results.forEach((result) => {
           if (result.status === 'rejected') {
             logger.error('Create user notification failed', result.reason);
           }
         });
-      } catch (err) {
+      }
+      catch (err) {
         logger.error('Create user notification failed', err);
       }
     }
 
     // create subscription
     try {
-      await crowi.inAppNotificationService.createSubscription(
-        req.user._id,
-        createdPage._id,
-        subscribeRuleNames.PAGE_CREATE,
-      );
-    } catch (err) {
+      await crowi.inAppNotificationService.createSubscription(req.user._id, createdPage._id, subscribeRuleNames.PAGE_CREATE);
+    }
+    catch (err) {
       logger.error('Failed to create subscription document', err);
     }
 
     // Rebuild vector store file
     if (isAiEnabled()) {
-      const { getOpenaiService } = await import(
-        '~/features/openai/server/services/openai'
-      );
+      const { getOpenaiService } = await import('~/features/openai/server/services/openai');
       try {
         const openaiService = getOpenaiService();
         await openaiService?.createVectorStoreFileOnPageCreate([createdPage]);
-      } catch (err) {
+      }
+      catch (err) {
         logger.error('Rebuild vector store failed', err);
       }
     }
@@ -287,60 +218,39 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
   const addActivity = generateAddActivityMiddleware();
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
-    loginRequiredStrictly,
-    excludeReadOnlyUser,
-    addActivity,
-    validator,
-    apiV3FormValidator,
-    async (req: CreatePageRequest, res: ApiV3Response) => {
-      const { body: bodyByParam, pageTags: tagsByParam } = req.body;
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly, excludeReadOnlyUser, addActivity,
+    validator, apiV3FormValidator,
+    async(req: CreatePageRequest, res: ApiV3Response) => {
+      const {
+        body: bodyByParam, pageTags: tagsByParam,
+      } = req.body;
 
       let pathToCreate: string;
       try {
         const { path, parentPath, optionalParentPath } = req.body;
-        pathToCreate = await determinePath(
-          parentPath,
-          path,
-          optionalParentPath,
-        );
-      } catch (err) {
-        return res.apiv3Err(
-          new ErrorV3(err.toString(), 'could_not_create_page'),
-        );
+        pathToCreate = await determinePath(parentPath, path, optionalParentPath);
+      }
+      catch (err) {
+        return res.apiv3Err(new ErrorV3(err.toString(), 'could_not_create_page'));
       }
 
       if (isUserPage(pathToCreate)) {
         const isExistUser = await User.isExistUserByUserPagePath(pathToCreate);
         if (!isExistUser) {
-          return res.apiv3Err(
-            "Unable to create a page under a non-existent user's user page",
-          );
+          return res.apiv3Err("Unable to create a page under a non-existent user's user page");
         }
       }
 
-      const { body, tags } = await determineBodyAndTags(
-        pathToCreate,
-        bodyByParam,
-        tagsByParam,
-      );
+      const { body, tags } = await determineBodyAndTags(pathToCreate, bodyByParam, tagsByParam);
 
       let createdPage: HydratedDocument<PageDocument>;
       try {
         const {
-          grant,
-          grantUserGroupIds,
-          onlyInheritUserRelatedGrantedGroups,
-          overwriteScopesOfDescendants,
-          wip,
-          origin,
+          grant, grantUserGroupIds, onlyInheritUserRelatedGrantedGroups, overwriteScopesOfDescendants, wip, origin,
         } = req.body;
 
         const options: IOptionsForCreate = {
-          onlyInheritUserRelatedGrantedGroups,
-          overwriteScopesOfDescendants,
-          wip,
-          origin,
+          onlyInheritUserRelatedGrantedGroups, overwriteScopesOfDescendants, wip, origin,
         };
         if (grant != null) {
           options.grant = grant;
@@ -352,7 +262,8 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
           req.user,
           options,
         );
-      } catch (err) {
+      }
+      catch (err) {
         logger.error('Error occurred while creating a page.', err);
         return res.apiv3Err(err);
       }

+ 50 - 71
apps/app/src/server/routes/apiv3/page/get-page-paths-with-descendant-count.ts

@@ -1,10 +1,10 @@
 import type { IPage, IUserHasId } from '@growi/core';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { query } from 'express-validator';
 import mongoose from 'mongoose';
 
+import { SCOPE } from '@growi/core/dist/interfaces';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { PageModel } from '~/server/models/page';
@@ -13,86 +13,65 @@ import loggerFactory from '~/utils/logger';
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page:get-pages-by-page-paths');
 
-type GetPagePathsWithDescendantCountFactory = (
-  crowi: Crowi,
-) => RequestHandler[];
+type GetPagePathsWithDescendantCountFactory = (crowi: Crowi) => RequestHandler[];
 
 type ReqQuery = {
-  paths: string[];
-  userGroups?: string[];
-  isIncludeEmpty?: boolean;
-  includeAnyoneWithTheLink?: boolean;
-};
+  paths: string[],
+  userGroups?: string[],
+  isIncludeEmpty?: boolean,
+  includeAnyoneWithTheLink?: boolean,
+}
 
 interface Req extends Request<undefined, ApiV3Response, undefined, ReqQuery> {
-  user: IUserHasId;
+  user: IUserHasId,
 }
-export const getPagePathsWithDescendantCountFactory: GetPagePathsWithDescendantCountFactory =
-  (crowi) => {
-    const Page = mongoose.model<IPage, PageModel>('Page');
-    const loginRequiredStrictly =
-      require('../../../middlewares/login-required')(crowi);
+export const getPagePathsWithDescendantCountFactory: GetPagePathsWithDescendantCountFactory = (crowi) => {
+  const Page = mongoose.model<IPage, PageModel>('Page');
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
-    const validator: ValidationChain[] = [
-      query('paths').isArray().withMessage('paths must be an array of strings'),
-      query('paths').custom((paths: string[]) => {
-        if (paths.length > 300) {
-          throw new Error(
-            'paths must be an array of strings with a maximum length of 300',
-          );
-        }
-        return true;
-      }),
-      query('paths.*') // each item of paths
-        .isString()
-        .withMessage('paths must be an array of strings'),
+  const validator: ValidationChain[] = [
+    query('paths').isArray().withMessage('paths must be an array of strings'),
+    query('paths').custom((paths: string[]) => {
+      if (paths.length > 300) {
+        throw new Error('paths must be an array of strings with a maximum length of 300');
+      }
+      return true;
+    }),
+    query('paths.*') // each item of paths
+      .isString()
+      .withMessage('paths must be an array of strings'),
 
-      query('userGroups')
-        .optional()
-        .isArray()
-        .withMessage('userGroups must be an array of strings'),
-      query('userGroups.*') // each item of userGroups
-        .isMongoId()
-        .withMessage('userGroups must be an array of strings'),
+    query('userGroups').optional().isArray().withMessage('userGroups must be an array of strings'),
+    query('userGroups.*') // each item of userGroups
+      .isMongoId()
+      .withMessage('userGroups must be an array of strings'),
 
-      query('isIncludeEmpty')
-        .optional()
-        .isBoolean()
-        .withMessage('isIncludeEmpty must be a boolean'),
-      query('isIncludeEmpty').toBoolean(),
+    query('isIncludeEmpty').optional().isBoolean().withMessage('isIncludeEmpty must be a boolean'),
+    query('isIncludeEmpty').toBoolean(),
 
-      query('includeAnyoneWithTheLink')
-        .optional()
-        .isBoolean()
-        .withMessage('includeAnyoneWithTheLink must be a boolean'),
-      query('includeAnyoneWithTheLink').toBoolean(),
-    ];
+    query('includeAnyoneWithTheLink').optional().isBoolean().withMessage('includeAnyoneWithTheLink must be a boolean'),
+    query('includeAnyoneWithTheLink').toBoolean(),
+  ];
 
-    return [
-      accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
-      loginRequiredStrictly,
-      validator,
-      apiV3FormValidator,
-      async (req: Req, res: ApiV3Response) => {
-        const { paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink } =
-          req.query;
+  return [
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
+    validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
+      const {
+        paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink,
+      } = req.query;
 
-        try {
-          const pagePathsWithDescendantCount =
-            await Page.descendantCountByPaths(
-              paths,
-              req.user,
-              userGroups,
-              isIncludeEmpty,
-              includeAnyoneWithTheLink,
-            );
-          return res.apiv3({ pagePathsWithDescendantCount });
-        } catch (err) {
-          logger.error(err);
-          return res.apiv3Err(err);
-        }
-      },
-    ];
-  };
+      try {
+        const pagePathsWithDescendantCount = await Page.descendantCountByPaths(paths, req.user, userGroups, isIncludeEmpty, includeAnyoneWithTheLink);
+        return res.apiv3({ pagePathsWithDescendantCount });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err);
+      }
+    },
+  ];
+};

+ 13 - 23
apps/app/src/server/routes/apiv3/page/get-yjs-data.ts

@@ -1,11 +1,11 @@
 import type { IPage, IUserHasId } from '@growi/core';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { param } from 'express-validator';
 import mongoose from 'mongoose';
 
+import { SCOPE } from '@growi/core/dist/interfaces';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { PageModel } from '~/server/models/page';
@@ -14,52 +14,42 @@ import loggerFactory from '~/utils/logger';
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page:get-yjs-data');
 
 type GetYjsDataHandlerFactory = (crowi: Crowi) => RequestHandler[];
 
 type ReqParams = {
-  pageId: string;
-};
+  pageId: string,
+}
 interface Req extends Request<ReqParams, ApiV3Response> {
-  user: IUserHasId;
+  user: IUserHasId,
 }
 export const getYjsDataHandlerFactory: GetYjsDataHandlerFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
-  const loginRequiredStrictly = require('../../../middlewares/login-required')(
-    crowi,
-  );
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
   // define validators for req.params
   const validator: ValidationChain[] = [
-    param('pageId')
-      .isMongoId()
-      .withMessage('The param "pageId" must be specified'),
+    param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
   ];
 
   return [
-    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }),
-    loginRequiredStrictly,
-    validator,
-    apiV3FormValidator,
-    async (req: Req, res: ApiV3Response) => {
+    accessTokenParser([SCOPE.READ.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
+    validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;
 
       // check whether accessible
       if (!(await Page.isAccessiblePageByViewer(pageId, req.user))) {
-        return res.apiv3Err(
-          new ErrorV3(
-            'Current user is not accessible to this page.',
-            'forbidden-page',
-          ),
-          403,
-        );
+        return res.apiv3Err(new ErrorV3('Current user is not accessible to this page.', 'forbidden-page'), 403);
       }
 
       try {
         const yjsData = await crowi.pageService.getYjsData(pageId);
         return res.apiv3({ yjsData });
-      } catch (err) {
+      }
+      catch (err) {
         logger.error(err);
         return res.apiv3Err(err);
       }

Разница между файлами не показана из-за своего большого размера
+ 275 - 474
apps/app/src/server/routes/apiv3/page/index.ts


+ 14 - 19
apps/app/src/server/routes/apiv3/page/publish-page.ts

@@ -1,11 +1,11 @@
 import type { IPage, IUserHasId } from '@growi/core';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { param } from 'express-validator';
 import mongoose from 'mongoose';
 
+import { SCOPE } from '@growi/core/dist/interfaces';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { PageModel } from '~/server/models/page';
@@ -14,40 +14,34 @@ import loggerFactory from '~/utils/logger';
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page:unpublish-page');
 
+
 type ReqParams = {
-  pageId: string;
-};
+  pageId: string,
+}
 
 interface Req extends Request<ReqParams, ApiV3Response> {
-  user: IUserHasId;
+  user: IUserHasId,
 }
 
 type PublishPageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 
-export const publishPageHandlersFactory: PublishPageHandlersFactory = (
-  crowi,
-) => {
+export const publishPageHandlersFactory: PublishPageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
 
-  const loginRequiredStrictly = require('../../../middlewares/login-required')(
-    crowi,
-  );
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
   // define validators for req.body
   const validator: ValidationChain[] = [
-    param('pageId')
-      .isMongoId()
-      .withMessage('The param "pageId" must be specified'),
+    param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
-    loginRequiredStrictly,
-    validator,
-    apiV3FormValidator,
-    async (req: Req, res: ApiV3Response) => {
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
+    validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;
 
       try {
@@ -59,7 +53,8 @@ export const publishPageHandlersFactory: PublishPageHandlersFactory = (
         page.publish();
         const updatedPage = await page.save();
         return res.apiv3(updatedPage);
-      } catch (err) {
+      }
+      catch (err) {
         logger.error(err);
         return res.apiv3Err(err);
       }

+ 40 - 60
apps/app/src/server/routes/apiv3/page/sync-latest-revision-body-to-yjs-draft.ts

@@ -1,11 +1,11 @@
 import type { IPage, IUserHasId } from '@growi/core';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
-import { body, param } from 'express-validator';
+import { param, body } from 'express-validator';
 import mongoose from 'mongoose';
 
+import { SCOPE } from '@growi/core/dist/interfaces';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { PageModel } from '~/server/models/page';
@@ -15,71 +15,51 @@ import loggerFactory from '~/utils/logger';
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
-const logger = loggerFactory(
-  'growi:routes:apiv3:page:sync-latest-revision-body-to-yjs-draft',
-);
 
-type SyncLatestRevisionBodyToYjsDraftHandlerFactory = (
-  crowi: Crowi,
-) => RequestHandler[];
+const logger = loggerFactory('growi:routes:apiv3:page:sync-latest-revision-body-to-yjs-draft');
+
+type SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi: Crowi) => RequestHandler[];
 
 type ReqParams = {
-  pageId: string;
-};
+  pageId: string,
+}
 type ReqBody = {
-  editingMarkdownLength?: number;
-};
+  editingMarkdownLength?: number,
+}
 interface Req extends Request<ReqParams, ApiV3Response, ReqBody> {
-  user: IUserHasId;
+  user: IUserHasId,
 }
-export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionBodyToYjsDraftHandlerFactory =
-  (crowi) => {
-    const Page = mongoose.model<IPage, PageModel>('Page');
-    const loginRequiredStrictly =
-      require('../../../middlewares/login-required')(crowi);
+export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi) => {
+  const Page = mongoose.model<IPage, PageModel>('Page');
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
-    // define validators for req.params
-    const validator: ValidationChain[] = [
-      param('pageId')
-        .isMongoId()
-        .withMessage('The param "pageId" must be specified'),
-      body('editingMarkdownLength')
-        .optional()
-        .isInt()
-        .withMessage('The body "editingMarkdownLength" must be integer'),
-    ];
+  // define validators for req.params
+  const validator: ValidationChain[] = [
+    param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
+    body('editingMarkdownLength').optional().isInt().withMessage('The body "editingMarkdownLength" must be integer'),
+  ];
 
-    return [
-      accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
-      loginRequiredStrictly,
-      validator,
-      apiV3FormValidator,
-      async (req: Req, res: ApiV3Response) => {
-        const { pageId } = req.params;
-        const { editingMarkdownLength } = req.body;
+  return [
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
+    validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
+      const { pageId } = req.params;
+      const { editingMarkdownLength } = req.body;
 
-        // check whether accessible
-        if (!(await Page.isAccessiblePageByViewer(pageId, req.user))) {
-          return res.apiv3Err(
-            new ErrorV3(
-              'Current user is not accessible to this page.',
-              'forbidden-page',
-            ),
-            403,
-          );
-        }
+      // check whether accessible
+      if (!(await Page.isAccessiblePageByViewer(pageId, req.user))) {
+        return res.apiv3Err(new ErrorV3('Current user is not accessible to this page.', 'forbidden-page'), 403);
+      }
 
-        try {
-          const yjsService = getYjsService();
-          const result = await yjsService.syncWithTheLatestRevisionForce(
-            pageId,
-            editingMarkdownLength,
-          );
-          return res.apiv3(result);
-        } catch (err) {
-          logger.error(err);
-          return res.apiv3Err(err);
-        }
-      },
-    ];
-  };
+      try {
+        const yjsService = getYjsService();
+        const result = await yjsService.syncWithTheLatestRevisionForce(pageId, editingMarkdownLength);
+        return res.apiv3(result);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err);
+      }
+    },
+  ];
+};

+ 14 - 19
apps/app/src/server/routes/apiv3/page/unpublish-page.ts

@@ -1,11 +1,11 @@
 import type { IPage, IUserHasId } from '@growi/core';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { param } from 'express-validator';
 import mongoose from 'mongoose';
 
+import { SCOPE } from '@growi/core/dist/interfaces';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import type { PageModel } from '~/server/models/page';
@@ -14,40 +14,34 @@ import loggerFactory from '~/utils/logger';
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page:unpublish-page');
 
+
 type ReqParams = {
-  pageId: string;
-};
+  pageId: string,
+}
 
 interface Req extends Request<ReqParams, ApiV3Response> {
-  user: IUserHasId;
+  user: IUserHasId,
 }
 
 type UnpublishPageHandlersFactory = (crowi: Crowi) => RequestHandler[];
 
-export const unpublishPageHandlersFactory: UnpublishPageHandlersFactory = (
-  crowi,
-) => {
+export const unpublishPageHandlersFactory: UnpublishPageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
 
-  const loginRequiredStrictly = require('../../../middlewares/login-required')(
-    crowi,
-  );
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
   // define validators for req.body
   const validator: ValidationChain[] = [
-    param('pageId')
-      .isMongoId()
-      .withMessage('The param "pageId" must be specified'),
+    param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
   ];
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
-    loginRequiredStrictly,
-    validator,
-    apiV3FormValidator,
-    async (req: Req, res: ApiV3Response) => {
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly,
+    validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;
 
       try {
@@ -60,7 +54,8 @@ export const unpublishPageHandlersFactory: UnpublishPageHandlersFactory = (
         const updatedPage = await page.save();
 
         return res.apiv3(updatedPage);
-      } catch (err) {
+      }
+      catch (err) {
         logger.error(err);
         return res.apiv3Err(err);
       }

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

@@ -1,12 +1,11 @@
-import type { IPage, IRevisionHasId, IUserHasId } from '@growi/core';
-import { allOrigin, getIdForRef, Origin } from '@growi/core';
+import { Origin, allOrigin, getIdForRef } from '@growi/core';
+import type {
+  IPage, IRevisionHasId, IUserHasId,
+} from '@growi/core';
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
-import {
-  isTopPage,
-  isUsersProtectedPages,
-} from '@growi/core/dist/utils/page-path-utils';
+import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { body } from 'express-validator';
@@ -15,20 +14,14 @@ import mongoose from 'mongoose';
 
 import { isAiEnabled } from '~/features/openai/server/services';
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
-import {
-  type IApiv3PageUpdateParams,
-  PageUpdateErrorCode,
-} from '~/interfaces/apiv3';
+import { type IApiv3PageUpdateParams, PageUpdateErrorCode } from '~/interfaces/apiv3';
 import type { IOptionsForUpdate } from '~/interfaces/page';
 import type Crowi from '~/server/crowi';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import type { PageDocument, PageModel } from '~/server/models/page';
-import {
-  serializePageSecurely,
-  serializeRevisionSecurely,
-} from '~/server/models/serializers';
+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';
@@ -39,12 +32,14 @@ import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import { excludeReadOnlyUser } from '../../../middlewares/exclude-read-only-user';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page:update-page');
 
+
 type ReqBody = IApiv3PageUpdateParams;
 
 interface UpdatePageRequest extends Request<undefined, ApiV3Response, ReqBody> {
-  user: IUserHasId;
+  user: IUserHasId,
 }
 
 type UpdatePageHandlersFactory = (crowi: Crowi) => RequestHandler[];
@@ -53,63 +48,31 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
   const Page = mongoose.model<IPage, PageModel>('Page');
   const Revision = mongoose.model<IRevisionHasId>('Revision');
 
-  const loginRequiredStrictly = require('../../../middlewares/login-required')(
-    crowi,
-  );
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
 
   // define validators for req.body
   const validator: ValidationChain[] = [
-    body('pageId')
-      .isMongoId()
-      .exists()
-      .not()
+    body('pageId').isMongoId().exists().not()
       .isEmpty({ ignore_whitespace: true })
       .withMessage("'pageId' must be specified"),
-    body('revisionId')
-      .optional()
-      .exists()
-      .not()
+    body('revisionId').optional().exists().not()
       .isEmpty({ ignore_whitespace: true })
       .withMessage("'revisionId' must be specified"),
-    body('body')
-      .exists()
-      .isString()
+    body('body').exists().isString()
       .withMessage("Empty value is not allowed for 'body'"),
-    body('grant')
-      .optional()
-      .not()
-      .isString()
+    body('grant').optional().not().isString()
       .isInt({ min: 0, max: 5 })
       .withMessage('grant must be an integer from 1 to 5'),
-    body('userRelatedGrantUserGroupIds')
-      .optional()
-      .isArray()
-      .withMessage('userRelatedGrantUserGroupIds must be an array of group id'),
-    body('overwriteScopesOfDescendants')
-      .optional()
-      .isBoolean()
-      .withMessage('overwriteScopesOfDescendants must be boolean'),
-    body('isSlackEnabled')
-      .optional()
-      .isBoolean()
-      .withMessage('isSlackEnabled must be boolean'),
-    body('slackChannels')
-      .optional()
-      .isString()
-      .withMessage('slackChannels must be string'),
-    body('origin')
-      .optional()
-      .isIn(allOrigin)
-      .withMessage('origin must be "view" or "editor"'),
+    body('userRelatedGrantUserGroupIds').optional().isArray().withMessage('userRelatedGrantUserGroupIds must be an array of group id'),
+    body('overwriteScopesOfDescendants').optional().isBoolean().withMessage('overwriteScopesOfDescendants must be boolean'),
+    body('isSlackEnabled').optional().isBoolean().withMessage('isSlackEnabled must be boolean'),
+    body('slackChannels').optional().isString().withMessage('slackChannels must be string'),
+    body('origin').optional().isIn(allOrigin).withMessage('origin must be "view" or "editor"'),
     body('wip').optional().isBoolean().withMessage('wip must be boolean'),
   ];
 
-  async function postAction(
-    req: UpdatePageRequest,
-    res: ApiV3Response,
-    updatedPage: HydratedDocument<PageDocument>,
-    previousRevision: IRevisionHasId | null,
-  ) {
+
+  async function postAction(req: UpdatePageRequest, res: ApiV3Response, updatedPage: HydratedDocument<PageDocument>, previousRevision: IRevisionHasId | null) {
     // Reflect the updates in ydoc
     const origin = req.body.origin;
     if (origin === Origin.View || origin === undefined) {
@@ -118,10 +81,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     }
 
     // persist activity
-    const creator =
-      updatedPage.creator != null
-        ? getIdForRef(updatedPage.creator)
-        : undefined;
+    const creator = updatedPage.creator != null ? getIdForRef(updatedPage.creator) : undefined;
     const parameters = {
       targetModel: SupportedTargetModel.MODEL_PAGE,
       target: updatedPage,
@@ -129,21 +89,16 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     };
     const activityEvent = crowi.event('activity');
     activityEvent.emit(
-      'update',
-      res.locals.activity._id,
-      parameters,
+      'update', res.locals.activity._id, parameters,
       { path: updatedPage.path, creator },
       preNotifyService.generatePreNotify,
     );
 
     // global notification
     try {
-      await crowi.globalNotificationService.fire(
-        GlobalNotificationSettingEvent.PAGE_EDIT,
-        updatedPage,
-        req.user,
-      );
-    } catch (err) {
+      await crowi.globalNotificationService.fire(GlobalNotificationSettingEvent.PAGE_EDIT, updatedPage, req.user);
+    }
+    catch (err) {
       logger.error('Edit notification failed', err);
     }
 
@@ -151,34 +106,27 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     const { isSlackEnabled, slackChannels } = req.body;
     if (isSlackEnabled) {
       try {
-        const option =
-          previousRevision != null ? { previousRevision } : undefined;
-        const results = await crowi.userNotificationService.fire(
-          updatedPage,
-          req.user,
-          slackChannels,
-          'update',
-          option,
-        );
+        const option = previousRevision != null ? { previousRevision } : undefined;
+        const results = await crowi.userNotificationService.fire(updatedPage, req.user, slackChannels, 'update', option);
         results.forEach((result) => {
           if (result.status === 'rejected') {
             logger.error('Create user notification failed', result.reason);
           }
         });
-      } catch (err) {
+      }
+      catch (err) {
         logger.error('Create user notification failed', err);
       }
     }
 
     // Rebuild vector store file
     if (isAiEnabled()) {
-      const { getOpenaiService } = await import(
-        '~/features/openai/server/services/openai'
-      );
+      const { getOpenaiService } = await import('~/features/openai/server/services/openai');
       try {
         const openaiService = getOpenaiService();
         await openaiService?.updateVectorStoreFileOnPageUpdate(updatedPage);
-      } catch (err) {
+      }
+      catch (err) {
         logger.error('Rebuild vector store failed', err);
       }
     }
@@ -187,102 +135,62 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
   const addActivity = generateAddActivityMiddleware();
 
   return [
-    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }),
-    loginRequiredStrictly,
-    excludeReadOnlyUser,
-    addActivity,
-    validator,
-    apiV3FormValidator,
-    async (req: UpdatePageRequest, res: ApiV3Response) => {
-      const { pageId, revisionId, body, origin, grant } = req.body;
+    accessTokenParser([SCOPE.WRITE.FEATURES.PAGE], { acceptLegacy: true }), loginRequiredStrictly, excludeReadOnlyUser, addActivity,
+    validator, apiV3FormValidator,
+    async(req: UpdatePageRequest, res: ApiV3Response) => {
+      const {
+        pageId, revisionId, body, origin, grant,
+      } = req.body;
 
-      const sanitizeRevisionId =
-        revisionId == null ? undefined : generalXssFilter.process(revisionId);
+      const sanitizeRevisionId = revisionId == null ? undefined : generalXssFilter.process(revisionId);
 
       // check page existence
-      const isExist = (await Page.count({ _id: { $eq: pageId } })) > 0;
+      const isExist = await Page.count({ _id: { $eq: pageId } }) > 0;
       if (!isExist) {
-        return res.apiv3Err(
-          new ErrorV3(
-            `Page('${pageId}' is not found or forbidden`,
-            'notfound_or_forbidden',
-          ),
-          400,
-        );
+        return res.apiv3Err(new ErrorV3(`Page('${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 400);
       }
 
       // check revision
       const currentPage = await Page.findByIdAndViewer(pageId, req.user);
       // check page existence (for type safety)
       if (currentPage == null) {
-        return res.apiv3Err(
-          new ErrorV3(
-            `Page('${pageId}' is not found or forbidden`,
-            'notfound_or_forbidden',
-          ),
-          400,
-        );
+        return res.apiv3Err(new ErrorV3(`Page('${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 400);
       }
 
-      const isGrantImmutable =
-        isTopPage(currentPage.path) || isUsersProtectedPages(currentPage.path);
+      const isGrantImmutable = isTopPage(currentPage.path) || isUsersProtectedPages(currentPage.path);
 
       if (grant != null && grant !== currentPage.grant && isGrantImmutable) {
-        return res.apiv3Err(
-          new ErrorV3(
-            'The grant settings for the specified page cannot be modified.',
-            PageUpdateErrorCode.FORBIDDEN,
-          ),
-          403,
-        );
+        return res.apiv3Err(new ErrorV3('The grant settings for the specified page cannot be modified.', PageUpdateErrorCode.FORBIDDEN), 403);
       }
 
       if (currentPage != null) {
         // Normalize the latest revision which was borken by the migration script '20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js'
         try {
           await normalizeLatestRevisionIfBroken(pageId);
-        } catch (err) {
+        }
+        catch (err) {
           logger.error('Error occurred in normalizing the latest revision');
         }
       }
 
-      if (
-        currentPage != null &&
-        !(await currentPage.isUpdatable(sanitizeRevisionId, origin))
-      ) {
-        const latestRevision = await Revision.findById(
-          currentPage.revision,
-        ).populate('author');
+      if (currentPage != null && !await currentPage.isUpdatable(sanitizeRevisionId, origin)) {
+        const latestRevision = await Revision.findById(currentPage.revision).populate('author');
         const returnLatestRevision = {
           revisionId: latestRevision?._id.toString(),
           revisionBody: latestRevision?.body,
           createdAt: latestRevision?.createdAt,
           user: serializeUserSecurely(latestRevision?.author),
         };
-        return res.apiv3Err(
-          new ErrorV3(
-            'Posted param "revisionId" is outdated.',
-            PageUpdateErrorCode.CONFLICT,
-            undefined,
-            { returnLatestRevision },
-          ),
-          409,
-        );
+        return res.apiv3Err(new ErrorV3('Posted param "revisionId" is outdated.', PageUpdateErrorCode.CONFLICT, undefined, { returnLatestRevision }), 409);
       }
 
       let updatedPage: HydratedDocument<PageDocument>;
       let previousRevision: IRevisionHasId | null;
       try {
         const {
-          userRelatedGrantUserGroupIds,
-          overwriteScopesOfDescendants,
-          wip,
+          userRelatedGrantUserGroupIds, overwriteScopesOfDescendants, wip,
         } = req.body;
-        const options: IOptionsForUpdate = {
-          overwriteScopesOfDescendants,
-          origin,
-          wip,
-        };
+        const options: IOptionsForUpdate = { overwriteScopesOfDescendants, origin, wip };
         if (grant != null) {
           options.grant = grant;
           options.userRelatedGrantUserGroupIds = userRelatedGrantUserGroupIds;
@@ -291,14 +199,9 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
 
         // There are cases where "revisionId" is not required for revision updates
         // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
-        updatedPage = await crowi.pageService.updatePage(
-          currentPage,
-          body,
-          previousRevision?.body ?? null,
-          req.user,
-          options,
-        );
-      } catch (err) {
+        updatedPage = await crowi.pageService.updatePage(currentPage, body, previousRevision?.body ?? null, req.user, options);
+      }
+      catch (err) {
         logger.error('Error occurred while updating a page.', err);
         return res.apiv3Err(err);
       }

+ 2 - 0
biome.json

@@ -30,6 +30,8 @@
       "!packages/pdf-converter-client/specs",
       "!apps/app/src/client",
       "!apps/app/src/server/middlewares",
+      "!apps/app/src/server/routes/apiv3/app-settings",
+      "!apps/app/src/server/routes/apiv3/page",
       "!apps/app/src/server/routes/apiv3/*.js",
       "!apps/app/src/server/service"
     ]

Некоторые файлы не были показаны из-за большого количества измененных файлов