ソースを参照

configure biome for attachment routes

Futa Arai 4 ヶ月 前
コミット
948edb42da

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

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

+ 3 - 5
apps/app/src/server/routes/admin.js

@@ -75,11 +75,9 @@ module.exports = (crowi, app) => {
     const { validationResult } = require('express-validator');
     const errors = validationResult(req);
     if (!errors.isEmpty()) {
-      return res
-        .status(422)
-        .json({
-          errors: `${fileName} is invalid. Do not use path like '../'.`,
-        });
+      return res.status(422).json({
+        errors: `${fileName} is invalid. Do not use path like '../'.`,
+      });
     }
 
     try {

+ 34 - 19
apps/app/src/server/routes/attachment/api.js

@@ -3,10 +3,9 @@ import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
 
 import { Attachment } from '../../models/attachment';
-
 import { validateImageContentType } from './image-content-type-validator';
-/* eslint-disable no-use-before-define */
 
+/* eslint-disable no-use-before-define */
 
 const logger = loggerFactory('growi:routes:attachment');
 
@@ -154,7 +153,8 @@ export const routesFactory = (crowi) => {
     }
 
     const ownerId = attachment.creator._id || attachment.creator;
-    if (attachment.page == null) { // when profile image
+    if (attachment.page == null) {
+      // when profile image
       return user.id === ownerId.toString();
     }
 
@@ -162,7 +162,6 @@ export const routesFactory = (crowi) => {
     return await Page.isAccessiblePageByViewer(attachment.page, user);
   }
 
-
   const actions = {};
   const api = {};
 
@@ -238,7 +237,7 @@ export const routesFactory = (crowi) => {
    *
    * @apiParam {File} file
    */
-  api.uploadProfileImage = async function(req, res) {
+  api.uploadProfileImage = async (req, res) => {
     // check params
     if (req.file == null) {
       return res.json(ApiResponse.error('File error.'));
@@ -260,10 +259,14 @@ export const routesFactory = (crowi) => {
     try {
       const user = await User.findById(req.user._id);
       await user.deleteImage();
-      attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.PROFILE_IMAGE);
+      attachment = await attachmentService.createAttachment(
+        file,
+        req.user,
+        null,
+        AttachmentType.PROFILE_IMAGE,
+      );
       await user.updateImage(attachment);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       return res.json(ApiResponse.error(err.message));
     }
@@ -312,7 +315,7 @@ export const routesFactory = (crowi) => {
    *
    * @apiParam {String} attachment_id
    */
-  api.remove = async function(req, res) {
+  api.remove = async (req, res) => {
     const id = req.body.attachment_id;
 
     const attachment = await Attachment.findById(id);
@@ -323,18 +326,25 @@ export const routesFactory = (crowi) => {
 
     const isDeletable = await isDeletableByUser(req.user, attachment);
     if (!isDeletable) {
-      return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
+      return res.json(
+        ApiResponse.error(
+          `Forbidden to remove the attachment '${attachment.id}'`,
+        ),
+      );
     }
 
     try {
       await attachmentService.removeAttachment(attachment);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
-      return res.status(500).json(ApiResponse.error('Error while deleting file'));
+      return res
+        .status(500)
+        .json(ApiResponse.error('Error while deleting file'));
     }
 
-    activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ATTACHMENT_REMOVE });
+    activityEvent.emit('update', res.locals.activity._id, {
+      action: SupportedAction.ACTION_ATTACHMENT_REMOVE,
+    });
 
     return res.json(ApiResponse.success({}));
   };
@@ -373,7 +383,7 @@ export const routesFactory = (crowi) => {
    * @apiGroup Attachment
    * @apiParam {String} attachment_id
    */
-  api.removeProfileImage = async function(req, res) {
+  api.removeProfileImage = async (req, res) => {
     const user = req.user;
     const attachment = await Attachment.findById(user.imageAttachment);
 
@@ -383,15 +393,20 @@ export const routesFactory = (crowi) => {
 
     const isDeletable = await isDeletableByUser(user, attachment);
     if (!isDeletable) {
-      return res.json(ApiResponse.error(`Forbidden to remove the attachment '${attachment.id}'`));
+      return res.json(
+        ApiResponse.error(
+          `Forbidden to remove the attachment '${attachment.id}'`,
+        ),
+      );
     }
 
     try {
       await user.deleteImage();
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
-      return res.status(500).json(ApiResponse.error('Error while deleting image'));
+      return res
+        .status(500)
+        .json(ApiResponse.error('Error while deleting image'));
     }
 
     return res.json(ApiResponse.success({}));

+ 14 - 12
apps/app/src/server/routes/attachment/download.ts

@@ -1,5 +1,5 @@
-import express from 'express';
 import type { Router } from 'express';
+import express from 'express';
 
 import { SupportedAction } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -7,17 +7,14 @@ import loggerFactory from '~/utils/logger';
 
 import type Crowi from '../../crowi';
 import { certifySharedPageAttachmentMiddleware } from '../../middlewares/certify-shared-page-attachment';
-
 import type { GetRequest, GetResponse } from './get';
 import { getActionFactory, retrieveAttachmentFromIdParam } from './get';
 
-
 const logger = loggerFactory('growi:routes:attachment:download');
 
-
 const generateActivityParameters = (req: CrowiRequest) => {
   return {
-    ip:  req.ip,
+    ip: req.ip,
     endpoint: req.originalUrl,
     action: SupportedAction.ACTION_ATTACHMENT_DOWNLOAD,
     user: req.user?._id,
@@ -28,21 +25,25 @@ const generateActivityParameters = (req: CrowiRequest) => {
 };
 
 export const downloadRouterFactory = (crowi: Crowi): Router => {
-
-  const loginRequired = require('../../middlewares/login-required')(crowi, true);
+  const loginRequired = require('../../middlewares/login-required')(
+    crowi,
+    true,
+  );
 
   const router = express.Router();
 
   // note: retrieveAttachmentFromIdParam requires `req.params.id`
-  router.get<{ id: string }>('/:id([0-9a-z]{24})',
-    certifySharedPageAttachmentMiddleware, loginRequired,
+  router.get<{ id: string }>(
+    '/:id([0-9a-z]{24})',
+    certifySharedPageAttachmentMiddleware,
+    loginRequired,
     retrieveAttachmentFromIdParam,
 
-    async(req: GetRequest, res: GetResponse) => {
+    async (req: GetRequest, res: GetResponse) => {
       const { attachment } = res.locals;
 
       const activityParameters = generateActivityParameters(req);
-      const createActivity = async() => {
+      const createActivity = async () => {
         await crowi.activityService.createActivity(activityParameters);
       };
 
@@ -50,7 +51,8 @@ export const downloadRouterFactory = (crowi: Crowi): Router => {
       await getAction(req, res, { download: true });
 
       createActivity();
-    });
+    },
+  );
 
   return router;
 };

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

@@ -1,10 +1,8 @@
+import { SCOPE } from '@growi/core/dist/interfaces';
+import type { Response, Router } from 'express';
 import express from 'express';
-import type {
-  Response, Router,
-} from 'express';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import loggerFactory from '~/utils/logger';
 
@@ -13,30 +11,39 @@ import { AttachmentType } from '../../interfaces/attachment';
 import { generateCertifyBrandLogoMiddleware } from '../../middlewares/certify-brand-logo';
 import { Attachment } from '../../models/attachment';
 import ApiResponse from '../../util/apiResponse';
-
 import { getActionFactory } from './get';
 
-
 const logger = loggerFactory('growi:routes:attachment:get-brand-logo');
 
-
 export const getBrandLogoRouterFactory = (crowi: Crowi): Router => {
-
   const certifyBrandLogo = generateCertifyBrandLogoMiddleware(crowi);
-  const loginRequired = require('../../middlewares/login-required')(crowi, true);
+  const loginRequired = require('../../middlewares/login-required')(
+    crowi,
+    true,
+  );
 
   const router = express.Router();
 
-  router.get('/brand-logo', certifyBrandLogo, accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]), loginRequired, async(req: CrowiRequest, res: Response) => {
-    const brandLogoAttachment = await Attachment.findOne({ attachmentType: AttachmentType.BRAND_LOGO });
-
-    if (brandLogoAttachment == null) {
-      return res.status(404).json(ApiResponse.error('Brand logo does not exist'));
-    }
-
-    const getAction = getActionFactory(crowi, brandLogoAttachment);
-    getAction(req, res);
-  });
+  router.get(
+    '/brand-logo',
+    certifyBrandLogo,
+    accessTokenParser([SCOPE.READ.FEATURES.ATTACHMENT]),
+    loginRequired,
+    async (req: CrowiRequest, res: Response) => {
+      const brandLogoAttachment = await Attachment.findOne({
+        attachmentType: AttachmentType.BRAND_LOGO,
+      });
+
+      if (brandLogoAttachment == null) {
+        return res
+          .status(404)
+          .json(ApiResponse.error('Brand logo does not exist'));
+      }
+
+      const getAction = getActionFactory(crowi, brandLogoAttachment);
+      getAction(req, res);
+    },
+  );
 
   return router;
 };

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

@@ -1,17 +1,19 @@
-import {
-  getIdStringForRef, type IPage, type IUser,
-} from '@growi/core';
+import { getIdStringForRef, type IPage, type IUser } from '@growi/core';
+import type { NextFunction, Request, Response, Router } from 'express';
 import express from 'express';
-import type {
-  NextFunction, Request, Response, Router,
-} from 'express';
 import mongoose from 'mongoose';
 
 import type { CrowiProperties, CrowiRequest } from '~/interfaces/crowi-request';
-import { ResponseMode, type ExpressHttpHeader, type RespondOptions } from '~/server/interfaces/attachment';
 import {
+  type ExpressHttpHeader,
+  type RespondOptions,
+  ResponseMode,
+} from '~/server/interfaces/attachment';
+import {
+  applyHeaders,
+  createContentHeaders,
   type FileUploader,
-  toExpressHttpHeaders, applyHeaders, createContentHeaders,
+  toExpressHttpHeaders,
 } from '~/server/service/file-uploader';
 import loggerFactory from '~/utils/logger';
 
@@ -20,32 +22,31 @@ import { certifySharedPageAttachmentMiddleware } from '../../middlewares/certify
 import { Attachment, type IAttachmentDocument } from '../../models/attachment';
 import ApiResponse from '../../util/apiResponse';
 
-
 const logger = loggerFactory('growi:routes:attachment:get');
 
-
 // TODO: remove this local interface when models/page has typescriptized
 interface PageModel {
-  isAccessiblePageByViewer: (pageId: string, user: IUser | undefined) => Promise<boolean>
+  isAccessiblePageByViewer: (
+    pageId: string,
+    user: IUser | undefined,
+  ) => Promise<boolean>;
 }
 
 type LocalsAfterDataInjection = { attachment: IAttachmentDocument };
 
-type RetrieveAttachmentFromIdParamRequest = CrowiProperties & Request<
-  { id: string },
-  any, any, any,
-  LocalsAfterDataInjection
->;
+type RetrieveAttachmentFromIdParamRequest = CrowiProperties &
+  Request<{ id: string }, any, any, any, LocalsAfterDataInjection>;
 
 type RetrieveAttachmentFromIdParamResponse = Response<
   any,
   LocalsAfterDataInjection
 >;
 
-export const retrieveAttachmentFromIdParam = async(
-    req: RetrieveAttachmentFromIdParamRequest, res: RetrieveAttachmentFromIdParamResponse, next: NextFunction,
+export const retrieveAttachmentFromIdParam = async (
+  req: RetrieveAttachmentFromIdParamRequest,
+  res: RetrieveAttachmentFromIdParamResponse,
+  next: NextFunction,
 ): Promise<void> => {
-
   const id = req.params.id;
   const attachment = await Attachment.findById(id);
 
@@ -59,9 +60,16 @@ export const retrieveAttachmentFromIdParam = async(
   // check viewer has permission
   if (user != null && attachment.page != null) {
     const Page = mongoose.model<IPage, PageModel>('Page');
-    const isAccessible = await Page.isAccessiblePageByViewer(getIdStringForRef(attachment.page), user);
+    const isAccessible = await Page.isAccessiblePageByViewer(
+      getIdStringForRef(attachment.page),
+      user,
+    );
     if (!isAccessible) {
-      res.json(ApiResponse.error(`Forbidden to access to the attachment '${attachment.id}'. This attachment might belong to other pages.`));
+      res.json(
+        ApiResponse.error(
+          `Forbidden to access to the attachment '${attachment.id}'. This attachment might belong to other pages.`,
+        ),
+      );
       return;
     }
   }
@@ -71,16 +79,21 @@ export const retrieveAttachmentFromIdParam = async(
   return next();
 };
 
-
-export const generateHeadersForFresh = (attachment: IAttachmentDocument): ExpressHttpHeader[] => {
+export const generateHeadersForFresh = (
+  attachment: IAttachmentDocument,
+): ExpressHttpHeader[] => {
   return toExpressHttpHeaders({
     ETag: `Attachment-${attachment._id}`,
     'Last-Modified': attachment.createdAt.toUTCString(),
   });
 };
 
-
-const respondForRedirectMode = async(res: Response, fileUploadService: FileUploader, attachment: IAttachmentDocument, opts?: RespondOptions): Promise<void> => {
+const respondForRedirectMode = async (
+  res: Response,
+  fileUploadService: FileUploader,
+  attachment: IAttachmentDocument,
+  opts?: RespondOptions,
+): Promise<void> => {
   const isDownload = opts?.download ?? false;
 
   if (!isDownload) {
@@ -91,42 +104,59 @@ const respondForRedirectMode = async(res: Response, fileUploadService: FileUploa
     }
   }
 
-  const temporaryUrl = await fileUploadService.generateTemporaryUrl(attachment, opts);
+  const temporaryUrl = await fileUploadService.generateTemporaryUrl(
+    attachment,
+    opts,
+  );
 
   res.redirect(temporaryUrl.url);
 
   // persist temporaryUrl
   if (!isDownload) {
     try {
-      attachment.cashTemporaryUrlByProvideSec(temporaryUrl.url, temporaryUrl.lifetimeSec);
+      attachment.cashTemporaryUrlByProvideSec(
+        temporaryUrl.url,
+        temporaryUrl.lifetimeSec,
+      );
       return;
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
     }
   }
 };
 
-const respondForRelayMode = async(res: Response, fileUploadService: FileUploader, attachment: IAttachmentDocument, opts?: RespondOptions): Promise<void> => {
+const respondForRelayMode = async (
+  res: Response,
+  fileUploadService: FileUploader,
+  attachment: IAttachmentDocument,
+  opts?: RespondOptions,
+): Promise<void> => {
   // apply content-* headers before response
   const isDownload = opts?.download ?? false;
-  const contentHeaders = createContentHeaders(attachment, { inline: !isDownload });
+  const contentHeaders = createContentHeaders(attachment, {
+    inline: !isDownload,
+  });
   applyHeaders(res, contentHeaders);
 
   try {
     const readable = await fileUploadService.findDeliveryFile(attachment);
     readable.pipe(res);
-  }
-  catch (e) {
+  } catch (e) {
     logger.error(e);
     res.json(ApiResponse.error(e.message));
     return;
   }
 };
 
-export const getActionFactory = (crowi: Crowi, attachment: IAttachmentDocument) => {
-  return async(req: CrowiRequest, res: Response, opts?: RespondOptions): Promise<void> => {
-
+export const getActionFactory = (
+  crowi: Crowi,
+  attachment: IAttachmentDocument,
+) => {
+  return async (
+    req: CrowiRequest,
+    res: Response,
+    opts?: RespondOptions,
+  ): Promise<void> => {
     // add headers before evaluating 'req.fresh'
     applyHeaders(res, generateHeadersForFresh(attachment));
 
@@ -154,26 +184,22 @@ export const getActionFactory = (crowi: Crowi, attachment: IAttachmentDocument)
   };
 };
 
+export type GetRequest = CrowiProperties &
+  Request<{ id: string }, any, any, any, LocalsAfterDataInjection>;
 
-export type GetRequest = CrowiProperties & Request<
-  { id: string },
-  any, any, any,
-  LocalsAfterDataInjection
->;
-
-export type GetResponse = Response<
-  any,
-  LocalsAfterDataInjection
->
+export type GetResponse = Response<any, LocalsAfterDataInjection>;
 
 export const getRouterFactory = (crowi: Crowi): Router => {
-
-  const loginRequired = require('../../middlewares/login-required')(crowi, true);
+  const loginRequired = require('../../middlewares/login-required')(
+    crowi,
+    true,
+  );
 
   const router = express.Router();
 
   // note: retrieveAttachmentFromIdParam requires `req.params.id`
-  router.get<{ id: string }>('/:id([0-9a-z]{24})',
+  router.get<{ id: string }>(
+    '/:id([0-9a-z]{24})',
     certifySharedPageAttachmentMiddleware,
     loginRequired,
     retrieveAttachmentFromIdParam,
@@ -182,7 +208,8 @@ export const getRouterFactory = (crowi: Crowi): Router => {
       const { attachment } = res.locals;
       const getAction = getActionFactory(crowi, attachment);
       getAction(req, res);
-    });
+    },
+  );
 
   return router;
 };

+ 5 - 2
apps/app/src/server/routes/attachment/image-content-type-validator.spec.ts

@@ -1,6 +1,9 @@
-import { describe, test, expect } from 'vitest';
+import { describe, expect, test } from 'vitest';
 
-import { validateImageContentType, type SupportedImageMimeType } from './image-content-type-validator';
+import {
+  type SupportedImageMimeType,
+  validateImageContentType,
+} from './image-content-type-validator';
 
 describe('validateImageContentType', () => {
   describe('valid cases', () => {

+ 8 - 3
apps/app/src/server/routes/attachment/image-content-type-validator.ts

@@ -15,7 +15,8 @@ export const SUPPORTED_IMAGE_MIME_TYPES = [
 ] as const;
 
 // Create a type for supported MIME types
-export type SupportedImageMimeType = typeof SUPPORTED_IMAGE_MIME_TYPES[number];
+export type SupportedImageMimeType =
+  (typeof SUPPORTED_IMAGE_MIME_TYPES)[number];
 
 export interface ImageContentTypeValidatorResult {
   isValid: boolean;
@@ -28,7 +29,9 @@ export interface ImageContentTypeValidatorResult {
  * @param mimeType MIME type string
  * @returns Validation result containing isValid flag and extracted content type
  */
-export const validateImageContentType = (mimeType: string): ImageContentTypeValidatorResult => {
+export const validateImageContentType = (
+  mimeType: string,
+): ImageContentTypeValidatorResult => {
   if (typeof mimeType !== 'string') {
     return {
       isValid: false,
@@ -38,7 +41,9 @@ export const validateImageContentType = (mimeType: string): ImageContentTypeVali
   }
 
   const trimmedType = mimeType.trim();
-  const isValid = SUPPORTED_IMAGE_MIME_TYPES.includes(trimmedType as SupportedImageMimeType);
+  const isValid = SUPPORTED_IMAGE_MIME_TYPES.includes(
+    trimmedType as SupportedImageMimeType,
+  );
 
   if (!isValid) {
     const supportedFormats = 'PNG, JPEG, GIF, WebP, AVIF, HEIC/HEIF, TIFF, SVG';

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

@@ -1,3 +1,3 @@
+export * from './download';
 export * from './get';
 export * from './get-brand-logo';
-export * from './download';

+ 0 - 1
biome.json

@@ -30,7 +30,6 @@
       "!apps/app/src/client",
       "!apps/app/src/server/middlewares",
       "!apps/app/src/server/routes/apiv3",
-      "!apps/app/src/server/routes/attachment",
       "!apps/app/src/server/service"
     ]
   },