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
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
import type { PondView } from '@dorfteich/shared';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useParams } from 'react-router-dom';
|
|
|
|
import { useAuth } from '../auth/auth-context';
|
|
import { FormError } from '../components/forms';
|
|
import { LabelManager } from '../labels/LabelManager';
|
|
import { PhantomPagesView } from '../links/PhantomPagesView';
|
|
import { AccessRulesManager } from '../access/AccessRulesManager';
|
|
import { EffectivePermissionsInspector } from '../access/EffectivePermissionsInspector';
|
|
import { PondFileManager } from '../files/PondFileManager';
|
|
import { apiGet } from '../lib/api';
|
|
import { MemberManager } from '../members/MemberManager';
|
|
|
|
/**
|
|
* Pond settings (issues #44/#54). Hosts the 'Members' and 'Labels' sections;
|
|
* future pond-level configuration (fonts, etc.) joins it here. Member
|
|
* management is Pond-Admin-gated in the api (the MemberManager shows the list
|
|
* to any member and hides the controls otherwise); label management needs
|
|
* modify rights. The sidebar links here for the pond owner (Site Admins and
|
|
* other members can still navigate directly).
|
|
*/
|
|
export function PondSettingsPage(): React.JSX.Element {
|
|
const { t } = useTranslation('labels');
|
|
const { t: tLinks } = useTranslation('links');
|
|
const { t: tMembers } = useTranslation('members');
|
|
const { t: tErrors } = useTranslation('errors');
|
|
const { t: tFiles } = useTranslation('files');
|
|
const { pondSlug = '' } = useParams<{ pondSlug: string }>();
|
|
const { user } = useAuth();
|
|
|
|
const pond = useQuery({
|
|
queryKey: ['pond', pondSlug],
|
|
queryFn: () => apiGet<PondView>(`/ponds/${pondSlug}`),
|
|
});
|
|
|
|
if (pond.error) return <FormError error={pond.error} />;
|
|
if (!pond.data) return <></>;
|
|
|
|
const canModify = Boolean(user && (user.isSiteAdmin || user.id === pond.data.ownerId));
|
|
|
|
return (
|
|
<div className="pond-settings-page">
|
|
<h1>{pond.data.name}</h1>
|
|
<section>
|
|
<h2>{tMembers('title')}</h2>
|
|
<MemberManager pondId={pond.data.id} />
|
|
</section>
|
|
<AccessRulesManager pondId={pond.data.id} />
|
|
<EffectivePermissionsInspector pondId={pond.data.id} />
|
|
<section>
|
|
<h2>{t('settings.title')}</h2>
|
|
{canModify ? (
|
|
<LabelManager pondId={pond.data.id} />
|
|
) : (
|
|
<p className="form-banner form-banner--error" role="alert">
|
|
{tErrors('forbidden')}
|
|
</p>
|
|
)}
|
|
</section>
|
|
{canModify && (
|
|
<section>
|
|
<h2>{tLinks('missing.title')}</h2>
|
|
<PhantomPagesView pondId={pond.data.id} pondSlug={pondSlug} />
|
|
</section>
|
|
)}
|
|
{canModify && (
|
|
<section>
|
|
<h2>{tFiles('manager.title')}</h2>
|
|
<PondFileManager pondId={pond.data.id} />
|
|
</section>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|