All checks were successful
CD / Build and push images (push) Successful in 4m2s
CI / Lint, typecheck, test (push) Successful in 2m46s
CI / Auth e2e pack (push) Successful in 3m45s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 12s
Extend uploads (#27, ADR 0011) beyond images to a configurable general attachment allowlist, plus the page attachments section and the Pond Admin file manager. Backend: - Two instance settings: `upload.allowedExtensions` (lowercase, dot-stripped, images always allowed regardless) and `upload.svgPolicy` (reject | sanitize). - FilesService.resolveUpload: raster images still decided by magic bytes; SVG is sanitized with DOMPurify (scripts, event handlers, foreignObject stripped) or rejected per policy; everything else is admitted only if its extension is on the allowlist. A sanitized SVG's stored bytes are re-accounted so pond_usage matches disk. - Downloads set `Content-Disposition: attachment` for every non-raster type (office files, PDFs, SVG) with `nosniff`, so they can never execute inline; raster images stay inline for page embeds. - New endpoints: `GET /ponds/:id/files` (pond_admin: all files + usage + orphan flag), `POST /pages/:id/files` and `GET /pages/:id/files` (page-write/read: the attachments section). New error code `upload_type_not_allowed` (de+en). Frontend: - Page attachments section (AttachmentsPanel): upload, list with type glyph, size, and uploader, insert-as-link into the document (an internal media link that downloads, never renders inline), and delete. Toggled in the editor. - Pond file manager (PondFileManager) in pond settings for Pond Admins: every file with its referencing page (or an orphan flag) and storage usage. - Admin uploads settings form (allowlist + SVG policy). New `files` i18n namespace (de+en). Tests: - files.e2e.db.test.ts: allowlisted non-image accepted and served as a download; disallowed extension rejected; renamed-.html-as-.png still fails; SVG sanitized (scripts/handlers stripped) and reject-mode rejects; page attachment listing; pond file manager usage/orphan; non-admin denied. - New e2e pack apps/web/e2e/attachments.spec.ts (+ CI step): upload → list → insert link (verified attachment disposition + nosniff), disallowed-type error, pond file manager usage/orphan. Local: typecheck, lint, i18n:check, build all green; api-db 184, shared 121, web 50; attachments pack 3/3, members 3/3, content 5/5. Adds dompurify + jsdom to the api for server-side SVG sanitization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
34 lines
1.3 KiB
TypeScript
34 lines
1.3 KiB
TypeScript
import { fileExtension, isImageMimeType } from '@dorfteich/shared';
|
|
|
|
/** Human-readable byte size (binary units) for the file listings (#61). */
|
|
export function formatBytes(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
const units = ['KiB', 'MiB', 'GiB', 'TiB'];
|
|
let value = bytes / 1024;
|
|
let unit = 0;
|
|
while (value >= 1024 && unit < units.length - 1) {
|
|
value /= 1024;
|
|
unit += 1;
|
|
}
|
|
return `${value.toFixed(value < 10 ? 1 : 0)} ${units[unit]}`;
|
|
}
|
|
|
|
/**
|
|
* A tiny emoji glyph standing in for a file-type icon (no icon asset pipeline
|
|
* yet) — enough to tell an image, a document, a spreadsheet, and an archive
|
|
* apart at a glance in the attachment lists.
|
|
*/
|
|
export function fileGlyph(fileName: string, mimeType: string): string {
|
|
if (isImageMimeType(mimeType) || mimeType === 'image/svg+xml') return '🖼️';
|
|
const ext = fileExtension(fileName);
|
|
if (ext === 'pdf') return '📕';
|
|
if (['doc', 'docx', 'odt', 'rtf', 'txt', 'md'].includes(ext)) return '📄';
|
|
if (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) return '📊';
|
|
if (['ppt', 'pptx', 'odp'].includes(ext)) return '📽️';
|
|
if (['zip'].includes(ext)) return '🗜️';
|
|
return '📎';
|
|
}
|
|
|
|
/** The permission-checked download URL for an attachment (ADR 0011). */
|
|
export const mediaUrl = (fileId: string): string => `/api/v1/media/${fileId}`;
|