Skip to content
Draft
Changes from 1 commit
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
18 changes: 18 additions & 0 deletions web/src/routes/users/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@
$fetching = false;
};

const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
Comment thread
MattyTheHacker marked this conversation as resolved.
Comment thread
MattyTheHacker marked this conversation as resolved.
Comment on lines +51 to +60

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same CSV escaping/injection concerns apply here as in copyCSV: u.studentID is not escaped and formula-like prefixes (=, +, -, @) should be neutralized to avoid CSV injection when opened in spreadsheet applications.

Suggested change
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const escapeCSVField = (value: string) => {
const neutralizedValue = /^[=+\-@]/.test(value) ? `'${value}` : value;
return `"${neutralizedValue.replace(/"/g, '""')}"`;
};
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`);

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +60

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSV generation currently only escapes quotes in u.name, but not u.studentID, and it does not mitigate CSV formula injection (values starting with =, +, -, @) which can execute formulas when opened in spreadsheet tools. Consider a shared CSV-escaping helper applied to both fields, including formula-injection hardening.

Suggested change
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const escapeCSVField = (value: string) => {
const hardenedValue = /^[=+\-@]/.test(value) ? `'${value}` : value;
return `"${hardenedValue.replace(/"/g, '""')}"`;
};
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map(
(u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`,
);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map(
(u) => `${escapeCSVField(u.studentID)},${escapeCSVField(u.name)}`,
);

Copilot uses AI. Check for mistakes.
const csvContent = [headers.join(","), ...rows].join("\n");
Comment on lines +51 to +61

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downloadCSV duplicates the header/row building logic from copyCSV. Extracting a single helper to build the CSV string will avoid the two paths drifting (especially once escaping/sanitization is improved).

Suggested change
const copyCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
const csvContent = [headers.join(","), ...rows].join("\n");
const buildUsersCSV = () => {
const headers = ["Student ID", "Name"];
const rows = data.users.map((u) => `"${u.studentID}","${u.name.replace(/"/g, '""')}"`);
return [headers.join(","), ...rows].join("\n");
};
const copyCSV = () => {
const csvContent = buildUsersCSV();
navigator.clipboard.writeText(csvContent);
};
const downloadCSV = () => {
const csvContent = buildUsersCSV();

Copilot uses AI. Check for mistakes.

const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", "registered_users.csv");
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL.revokeObjectURL(url) is called immediately after link.click(). In some browsers this can revoke the blob URL before the download has started, causing intermittent download failures. Revoke the URL asynchronously (e.g., in a setTimeout/next tick) after the click.

Suggested change
URL.revokeObjectURL(url);
setTimeout(() => URL.revokeObjectURL(url), 0);

Copilot uses AI. Check for mistakes.
};

let restrictUserDialog: HTMLDialogElement;
const confirmRestrictUser = (user: User) => {
if (user.isRestricted) return toggleUserRestriction(user.studentID, user.isRestricted);
Expand Down Expand Up @@ -86,6 +103,7 @@
<Panel title="Manage users" headerIcon="admin_panel_settings">
<div slot="header-action" class="header-group">
<p>Currently {data.users.length} users</p>
<Button icon="download" text="Download CSV" on:click={downloadCSV} />
<Button icon="search" kind="emphasis" text="Search users" />
<Button
icon="person_remove"
Expand Down