feat: make attachment content types configurable via env var
Add PAPERCLIP_ALLOWED_ATTACHMENT_TYPES env var to configure allowed MIME types for issue attachments and asset uploads. Supports exact types (application/pdf) and wildcard patterns (image/*, text/*). Falls back to the existing image-only defaults when the env var is unset, preserving backward compatibility. - Extract shared module `attachment-types.ts` with `isAllowedContentType()` and `matchesContentType()` (pure, testable) - Update `routes/issues.ts` and `routes/assets.ts` to use shared module - Add unit tests for parsing and wildcard matching Closes #487
This commit is contained in:
@@ -5,22 +5,14 @@ import { createAssetImageMetadataSchema } from "@paperclipai/shared";
|
||||
import type { StorageService } from "../storage/types.js";
|
||||
import { assetService, logActivity } from "../services/index.js";
|
||||
import { assertCompanyAccess, getActorInfo } from "./authz.js";
|
||||
|
||||
const MAX_ASSET_IMAGE_BYTES = Number(process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES) || 10 * 1024 * 1024;
|
||||
const ALLOWED_IMAGE_CONTENT_TYPES = new Set([
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/webp",
|
||||
"image/gif",
|
||||
]);
|
||||
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
|
||||
|
||||
export function assetRoutes(db: Db, storage: StorageService) {
|
||||
const router = Router();
|
||||
const svc = assetService(db);
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: MAX_ASSET_IMAGE_BYTES, files: 1 },
|
||||
limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 },
|
||||
});
|
||||
|
||||
async function runSingleFileUpload(req: Request, res: Response) {
|
||||
@@ -41,7 +33,7 @@ export function assetRoutes(db: Db, storage: StorageService) {
|
||||
} catch (err) {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === "LIMIT_FILE_SIZE") {
|
||||
res.status(422).json({ error: `Image exceeds ${MAX_ASSET_IMAGE_BYTES} bytes` });
|
||||
res.status(422).json({ error: `Image exceeds ${MAX_ATTACHMENT_BYTES} bytes` });
|
||||
return;
|
||||
}
|
||||
res.status(400).json({ error: err.message });
|
||||
@@ -57,7 +49,7 @@ export function assetRoutes(db: Db, storage: StorageService) {
|
||||
}
|
||||
|
||||
const contentType = (file.mimetype || "").toLowerCase();
|
||||
if (!ALLOWED_IMAGE_CONTENT_TYPES.has(contentType)) {
|
||||
if (!isAllowedContentType(contentType)) {
|
||||
res.status(422).json({ error: `Unsupported image type: ${contentType || "unknown"}` });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,15 +26,7 @@ import { logger } from "../middleware/logger.js";
|
||||
import { forbidden, HttpError, unauthorized } from "../errors.js";
|
||||
import { assertCompanyAccess, getActorInfo } from "./authz.js";
|
||||
import { shouldWakeAssigneeOnCheckout } from "./issues-checkout-wakeup.js";
|
||||
|
||||
const MAX_ATTACHMENT_BYTES = Number(process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES) || 10 * 1024 * 1024;
|
||||
const ALLOWED_ATTACHMENT_CONTENT_TYPES = new Set([
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/webp",
|
||||
"image/gif",
|
||||
]);
|
||||
import { isAllowedContentType, MAX_ATTACHMENT_BYTES } from "../attachment-types.js";
|
||||
|
||||
export function issueRoutes(db: Db, storage: StorageService) {
|
||||
const router = Router();
|
||||
@@ -1067,7 +1059,7 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||
return;
|
||||
}
|
||||
const contentType = (file.mimetype || "").toLowerCase();
|
||||
if (!ALLOWED_ATTACHMENT_CONTENT_TYPES.has(contentType)) {
|
||||
if (!isAllowedContentType(contentType)) {
|
||||
res.status(422).json({ error: `Unsupported attachment type: ${contentType || "unknown"}` });
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user