Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions frontend/src/components/upload/DropZone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Accessible upload drop zone (#1513, #1512).
*
* The drop zone was not keyboard accessible and its title label was not
* associated with its input. This wires the label to the input via
* `htmlFor`/`id` and supports keyboard activation, using a neutral title
* placeholder rather than the finance example (#1512).
*/
import * as React from "react";

export interface DropZoneProps {
id?: string;
label?: string;
accept?: string;
onFile: (file: File) => void;
}

export function DropZone({
id = "upload-file",
label = "Choose a document to upload",
accept,
onFile,
}: DropZoneProps): React.ReactElement {
const inputRef = React.useRef<HTMLInputElement>(null);
const labelId = `${id}-label`;

return (
<div>
<label htmlFor={id} id={labelId}>
{label}
</label>
<div
role="button"
tabIndex={0}
aria-labelledby={labelId}
onClick={() => inputRef.current?.click()}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
inputRef.current?.click();
}
}}
onDrop={(event) => {
event.preventDefault();
const file = event.dataTransfer.files?.[0];
if (file) {
onFile(file);
}
}}
onDragOver={(event) => event.preventDefault()}
>
Drag and drop, or press Enter to browse
</div>
<input
ref={inputRef}
id={id}
type="file"
accept={accept}
className="sr-only"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) {
onFile(file);
}
}}
/>
</div>
);
}

export default DropZone;
35 changes: 35 additions & 0 deletions frontend/src/lib/security-headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Content-Security-Policy builder (#1515).
*
* The previous CSP allowed `unsafe-inline`, `unsafe-eval` and connected to any
* `http(s):` origin. This builder produces a strict policy and lets callers add
* only the explicit sources they need.
*/
export interface CspOptions {
connectSources?: string[];
scriptSources?: string[];
styleSources?: string[];
imageSources?: string[];
allowInlineStyles?: boolean;
}

export function buildContentSecurityPolicy(options: CspOptions = {}): string {
const directives: Record<string, string[]> = {
"default-src": ["'self'"],
"script-src": ["'self'", ...(options.scriptSources ?? [])],
"style-src": [
"'self'",
...(options.allowInlineStyles ? ["'unsafe-inline'"] : []),
...(options.styleSources ?? []),
],
"img-src": ["'self'", "data:", ...(options.imageSources ?? [])],
"connect-src": ["'self'", ...(options.connectSources ?? [])],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"frame-ancestors": ["'none'"],
};

return Object.entries(directives)
.map(([directive, values]) => `${directive} ${values.join(" ")}`)
.join("; ");
}
Loading