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:
67
server/src/attachment-types.ts
Normal file
67
server/src/attachment-types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Shared attachment content-type configuration.
|
||||
*
|
||||
* By default only image types are allowed. Set the
|
||||
* `PAPERCLIP_ALLOWED_ATTACHMENT_TYPES` environment variable to a
|
||||
* comma-separated list of MIME types or wildcard patterns to expand the
|
||||
* allowed set.
|
||||
*
|
||||
* Examples:
|
||||
* PAPERCLIP_ALLOWED_ATTACHMENT_TYPES=image/*,application/pdf
|
||||
* PAPERCLIP_ALLOWED_ATTACHMENT_TYPES=image/*,application/pdf,text/*
|
||||
*
|
||||
* Supported pattern syntax:
|
||||
* - Exact types: "application/pdf"
|
||||
* - Wildcards: "image/*" or "application/vnd.openxmlformats-officedocument.*"
|
||||
*/
|
||||
|
||||
export const DEFAULT_ALLOWED_TYPES: readonly string[] = [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/webp",
|
||||
"image/gif",
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse a comma-separated list of MIME type patterns into a normalised array.
|
||||
* Returns the default image-only list when the input is empty or undefined.
|
||||
*/
|
||||
export function parseAllowedTypes(raw: string | undefined): string[] {
|
||||
if (!raw) return [...DEFAULT_ALLOWED_TYPES];
|
||||
const parsed = raw
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter((s) => s.length > 0);
|
||||
return parsed.length > 0 ? parsed : [...DEFAULT_ALLOWED_TYPES];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether `contentType` matches any entry in `allowedPatterns`.
|
||||
*
|
||||
* Supports exact matches ("application/pdf") and wildcard / prefix
|
||||
* patterns ("image/*", "application/vnd.openxmlformats-officedocument.*").
|
||||
*/
|
||||
export function matchesContentType(contentType: string, allowedPatterns: string[]): boolean {
|
||||
const ct = contentType.toLowerCase();
|
||||
return allowedPatterns.some((pattern) => {
|
||||
if (pattern.endsWith("/*") || pattern.endsWith(".*")) {
|
||||
return ct.startsWith(pattern.slice(0, -1));
|
||||
}
|
||||
return ct === pattern;
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Module-level singletons read once at startup ----------
|
||||
|
||||
const allowedPatterns: string[] = parseAllowedTypes(
|
||||
process.env.PAPERCLIP_ALLOWED_ATTACHMENT_TYPES,
|
||||
);
|
||||
|
||||
/** Convenience wrapper using the process-level allowed list. */
|
||||
export function isAllowedContentType(contentType: string): boolean {
|
||||
return matchesContentType(contentType, allowedPatterns);
|
||||
}
|
||||
|
||||
export const MAX_ATTACHMENT_BYTES =
|
||||
Number(process.env.PAPERCLIP_ATTACHMENT_MAX_BYTES) || 10 * 1024 * 1024;
|
||||
Reference in New Issue
Block a user