-
Notifications
You must be signed in to change notification settings - Fork 1
VPS-96/Allowed renaming of resources in edit view #413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
47f0e18
10b3fbd
c45bb80
121e598
bab4def
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,6 +145,34 @@ router.post("/upload", upload.array("files"), async (req, res) => { | |
| } | ||
| }); | ||
|
|
||
| /** | ||
| * @route PATCH /api/files/:fileId | ||
| * @desc Update the name of a stored file | ||
| */ | ||
| router.patch("/:fileId", async (req, res) => { | ||
| try { | ||
| const { fileId } = req.params; | ||
| const { name } = req.body; | ||
| if (!name || !name.trim()) { | ||
| return res.status(400).json({ error: "Name is required" }); | ||
| } | ||
| if (name.trim().length > 255) { | ||
| return res | ||
| .status(400) | ||
| .json({ error: "Name must be 255 characters or fewer" }); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| const meta = await StoredFile.findById(fileId); | ||
| if (!meta) return res.status(404).json({ error: "File not found" }); | ||
| meta.name = name.trim(); | ||
| await meta.save(); | ||
| const file = meta.toObject(); | ||
| delete file.gridFsId; | ||
| return res.json(file); | ||
| } catch (err) { | ||
| return res.status(500).json({ error: err.message }); | ||
| } | ||
| }); | ||
|
Comment on lines
+152
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Look for any scenario/file ownership or role checks in the files routes and middleware
rg -nP -C3 '(ownerUid|uploaderUid|scenarioId|authoriz|permission|forbidden|403)' backend/src/routes/api/files.js
fd -t f . backend/src/middleware --exec rg -nP -C3 '(role|owner|permission|scenario|403|forbidden)' {}
# Check how collections routes guard scenario access for comparison
fd -t f 'collections.js' backend/src --exec rg -nP -C3 '(owner|uid|role|permission|403)' {}Repository: UoaWDCC/VPS Length of output: 5875 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) Show the PATCH /:fileId handler and surrounding code in backend/src/routes/api/files.js
sed -n '1,260p' backend/src/routes/api/files.js | nl -ba | sed -n '120,220p'
# 2) Confirm auth middleware wiring in that file
rg -n --hidden "router\.use\(|auth|scenarioAuth|dashboardAuth|permission|role|403|forbidden|owner|uid" backend/src/routes/api/files.js
# 3) Find any authorization helper used for fileId-based routes (rename/delete) in this route file
rg -n --hidden "patch\\(\"/:fileId\"|delete\\(\"/:fileId\"|StoredFile\\.findById\\(|findById\\(fileId\\)|fileId\\)" backend/src/routes/api/files.js
# 4) Check if any middleware elsewhere enforces ownership for StoredFile routes
# (search for StoredFile access-control patterns)
fd -t f . backend/src/middleware backend/src/routes/api backend/src/db/daos backend/src/db/models --max-depth 6 2>/dev/null | head -n 2000 >/tmp/filelist.txt
rg -n --hidden "(StoredFile|gridFsId|uploaderUid|scenarioId|groupId|ownerUid|can[A-Z]|hasAccess|authorize|permission|role)" $(cat /tmp/filelist.txt) || true
# 5) Specifically locate scenario/file ownership checks in middleware and list them
rg -n --hidden "(assert.*Scenario|assert.*Group|scenarioAuth|dashboardAuth|access|ownerId)" backend/src/middleware backend/src/routes/api | head -n 200 || trueRepository: UoaWDCC/VPS Length of output: 98 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) Show backend/src/routes/api/files.js around the PATCH handler using cat -n (nl not available)
sed -n '1,260p' backend/src/routes/api/files.js | cat -n | sed -n '120,220p'
# 2) Confirm auth middleware wiring in that file
rg -n --hidden "router\.use\(|auth|scenarioAuth|dashboardAuth|permission|role|403|forbidden|owner|uid" backend/src/routes/api/files.js || true
# 3) Locate the patch/delete routes and where fileId is used
rg -n --hidden "router\.(patch|delete)\(\"/:fileId\"|StoredFile\.findById|findById\(\s*fileId|uploaderUid|scenarioId|groupId" backend/src/routes/api/files.js || true
# 4) Search for any middleware/utility used by files.js for access control
# (start with scenarioAuth/dashboardAuth and access checks; then also search for StoredFile ownership checks)
fd -t f backend/src/middleware backend/src/routes/api backend/src/db/daos backend/src/db/models --max-depth 6 2>/dev/null > /tmp/vps_files.txt
rg -n --hidden "(scenarioAuth|dashboardAuth|accessList|ownerId|uploaderUid|StoredFile|forbidden|403|permission|role|authorize|hasAccess|assert.*Scenario|assert.*Group)" $(cat /tmp/vps_files.txt) || trueRepository: UoaWDCC/VPS Length of output: 5413 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Show top of backend/src/routes/api/files.js to capture middleware wiring/order
sed -n '1,120p' backend/src/routes/api/files.js | nl -ba | head -n 120
# Inspect firebase auth middleware to see what it guarantees (e.g., req.user fields)
sed -n '1,220p' backend/src/middleware/firebaseAuth.js | cat -n
# Locate StoredFile model to see available fields (scenarioId/groupId/uploaderUid/etc.)
fd -t f 'storedfile*|StoredFile*|file*model*' backend/src/db/models backend/src/db --max-depth 6 2>/dev/null > /tmp/storedfile_models.txt || true
cat /tmp/storedfile_models.txt || true
for f in $(cat /tmp/storedfile_models.txt 2>/dev/null); do echo "---- $f"; sed -n '1,260p' "$f" | cat -n; done
# Find any ownership/permission checks around StoredFile elsewhere
rg -n --hidden "uploaderUid|ownerUid|scenarioId|groupId|access|permission|role|authorize|hasAccess|403|forbidden" backend/src/routes/api backend/src/middleware backend/src/db/daos backend/src/db/models | rg -n "StoredFile|gridFsId|files.js|/files|state-conditionals" || true
# Check if other handlers in this same files.js enforce scenario/group membership
rg -n --hidden "assertGroupInScenario|scenarioId|groupId" backend/src/routes/api/files.jsRepository: UoaWDCC/VPS Length of output: 98 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) Show full middleware wiring at top of files.js (no nl; use cat -n)
sed -n '1,120p' backend/src/routes/api/files.js | cat -n
# 2) Inspect firebase auth middleware
sed -n '1,260p' backend/src/middleware/firebaseAuth.js | cat -n
# 3) Locate and inspect StoredFile model (search by filename first)
fd -t f 'storedfile*|StoredFile*' backend/src/db/models backend/src/db | head -n 50
# If we found candidates, print them; otherwise rg for "name: " fields in StoredFile schema
for f in $(fd -t f 'storedfile*|StoredFile*' backend/src/db/models backend/src/db 2>/dev/null | head -n 20); do
echo "---- $f"
sed -n '1,260p' "$f" | cat -n
done
# 4) Find any scenario/role/ownership checks referenced from files.js (and any middleware that looks like it)
rg -n --hidden "(scenarioAuth|dashboardAuth|permission|role|ownerUid|uploaderUid|accessList|hasAccess|authorize|forbidden|403|assert.*Scenario|assert.*Group)" backend/src/routes/api/files.js backend/src/routes/api backend/src/middleware backend/src/db/daos backend/src/db/models || true
# 5) Confirm how this router is mounted (to see if additional middleware wraps it)
rg -n --hidden "routes/api/files|api/files|require\\(.*files|from '.*files|import .*files\\.js" backend/src | head -n 100 || trueRepository: UoaWDCC/VPS Length of output: 20512 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect backend/src/index.js around where files router is mounted
sed -n '1,120p' backend/src/index.js | cat -n
rg -n "app\.use\(\"/api/files\"|filesRouter" backend/src/index.js
# Inspect remaining part of backend/src/routes/api/files.js for any ownership checks in other file mutations
rg -n "router\.(patch|delete|post|put)\\(\"/:fileId\"|state-conditionals|scenarioAuth|dashboardAuth|assertGroupInScenario|accessList|getAccessList|403|forbidden" backend/src/routes/api/files.js
sed -n '140,280p' backend/src/routes/api/files.js | cat -n
# Inspect delete/state-conditionals handlers quickly for auth/ownership checks usage
sed -n '170,280p' backend/src/routes/api/files.js | cat -nRepository: UoaWDCC/VPS Length of output: 10731 Add authorization for file renames (and other file mutations) to prevent IDOR
🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * @route DELETE /api/files/:fileId | ||
| * @desc Delete a stored file and its GridFS data | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ import { | |
| UsersIcon, | ||
| PlusIcon, | ||
| XIcon, | ||
| PencilIcon, | ||
| CheckIcon, | ||
| } from "lucide-react"; | ||
| import AddGroup from "./components/AddGroup"; | ||
| import StateConditionalMenu from "../../components/StateVariables/StateConditionalMenu"; | ||
|
|
@@ -47,6 +49,8 @@ export default function ManageResourcesPage() { | |
| // Groups (each with files) | ||
| const [groups, setGroups] = useState([]); | ||
| const [selectedFile, setSelectedFile] = useState(null); | ||
| const [renamingFileId, setRenamingFileId] = useState(null); | ||
| const [renameInput, setRenameInput] = useState(""); | ||
|
|
||
| // Load groups and files | ||
| useEffect(() => { | ||
|
|
@@ -133,6 +137,55 @@ export default function ManageResourcesPage() { | |
| } | ||
| } | ||
|
|
||
| async function renameFile(fileId, newName) { | ||
| try { | ||
| const user = getAuth().currentUser; | ||
| if (!user) { | ||
| toast.error("You must be logged in."); | ||
| return; | ||
| } | ||
| const idToken = await user.getIdToken(); | ||
| const { data } = await axios.patch( | ||
| `/api/files/${fileId}`, | ||
| { name: newName }, | ||
| { headers: { Authorization: `Bearer ${idToken}` } } | ||
| ); | ||
|
|
||
| setGroups((prev) => | ||
| prev.map((g) => ({ | ||
| ...g, | ||
| files: (g.files || []).map((f) => | ||
| f.id === fileId ? { ...f, name: data.name } : f | ||
| ), | ||
| })) | ||
| ); | ||
|
|
||
| if (selectedFile?.id === fileId) { | ||
| setSelectedFile((prev) => ({ ...prev, name: data.name })); | ||
| } | ||
|
|
||
| toast.success("Renamed"); | ||
| } catch (err) { | ||
| console.error(err); | ||
| toast.error(err?.response?.data?.error || "Rename failed"); | ||
| } | ||
| } | ||
|
|
||
| async function handleRenameSubmit() { | ||
| const trimmed = renameInput.trim(); | ||
| if (!trimmed) { | ||
| setRenamingFileId(null); | ||
| return; | ||
| } | ||
| const current = groups | ||
| .flatMap((g) => g.files) | ||
| .find((f) => f.id === renamingFileId); | ||
| if (current && trimmed !== current.name) { | ||
| await renameFile(renamingFileId, trimmed); | ||
| } | ||
| setRenamingFileId(null); | ||
| } | ||
|
|
||
| async function removeFile(fileId) { | ||
| try { | ||
| const user = getAuth().currentUser; | ||
|
|
@@ -234,7 +287,7 @@ export default function ManageResourcesPage() { | |
|
|
||
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-4"> | ||
| {/* LEFT: Groups and files */} | ||
| <div className="card bg-base-100 shadow-md"> | ||
| <div className="card bg-base-100 shadow-md overflow-hidden min-w-0"> | ||
| <div className="card-body gap-4 px-0"> | ||
| <div className="flex items-center justify-between gap-2"> | ||
| <h2 className="text-m">Collections</h2> | ||
|
|
@@ -271,7 +324,7 @@ export default function ManageResourcesPage() { | |
| <ul className="menu bg-base-100 rounded-box w-full"> | ||
| {groups.map((group) => ( | ||
| <li key={group.id}> | ||
| <details> | ||
| <details style={{ overflow: "hidden" }}> | ||
| <summary className="flex items-center"> | ||
| <span className="text--1 truncate"> | ||
| {group.name} | ||
|
|
@@ -294,34 +347,93 @@ export default function ManageResourcesPage() { | |
| </div> | ||
| </summary> | ||
|
|
||
| <ul> | ||
| <ul className="overflow-hidden"> | ||
| {group.files.length === 0 && ( | ||
| <li className="opacity-60 p-2">No files yet</li> | ||
| )} | ||
|
|
||
| {group.files.map((f) => ( | ||
| <li key={f.id}> | ||
| <div className="flex items-center justify-between"> | ||
| <a | ||
| className="min-w-0 flex-1 text--1 truncate" | ||
| onClick={() => | ||
| setSelectedFile({ | ||
| ...f, | ||
| groupId: group.id, | ||
| groupName: group.name, | ||
| }) | ||
| } | ||
| > | ||
| {f.name} | ||
| </a> | ||
| <button | ||
| className="btn btn-phantom btn-xs px-0" | ||
| onClick={() => removeFile(f.id)} | ||
| title="Delete file" | ||
| <li key={f.id} className="overflow-hidden"> | ||
| {renamingFileId === f.id ? ( | ||
| <div className="flex items-center gap-1 py-1"> | ||
| <input | ||
| autoFocus | ||
| className="input input-bordered input-xs flex-1 min-w-0" | ||
| value={renameInput} | ||
| maxLength={255} | ||
| onChange={(e) => | ||
| setRenameInput(e.target.value) | ||
| } | ||
| onKeyDown={(e) => { | ||
| if (e.key === "Enter") | ||
| handleRenameSubmit(); | ||
| if (e.key === "Escape") | ||
| setRenamingFileId(null); | ||
| }} | ||
| /> | ||
| <button | ||
| className="btn btn-primary btn-xs" | ||
| onClick={handleRenameSubmit} | ||
| title="Save" | ||
| > | ||
| <CheckIcon size={14} /> | ||
| </button> | ||
| <button | ||
| className="btn btn-phantom btn-xs px-0" | ||
| onClick={() => setRenamingFileId(null)} | ||
| title="Cancel" | ||
| > | ||
| <XIcon size={14} /> | ||
| </button> | ||
| </div> | ||
| ) : ( | ||
| <div | ||
| style={{ | ||
| display: "grid", | ||
| gridTemplateColumns: | ||
| "minmax(0, 1fr) auto auto", | ||
| alignItems: "center", | ||
| gap: "4px", | ||
| overflow: "hidden", | ||
| }} | ||
| > | ||
| <XIcon size={16} /> | ||
| </button> | ||
| </div> | ||
| <a | ||
| className="text--1" | ||
| title={f.name} | ||
| style={{ | ||
| overflow: "hidden", | ||
| textOverflow: "ellipsis", | ||
| whiteSpace: "nowrap", | ||
| }} | ||
| onClick={() => | ||
| setSelectedFile({ | ||
| ...f, | ||
| groupId: group.id, | ||
| groupName: group.name, | ||
| }) | ||
| } | ||
| > | ||
| {f.name} | ||
| </a> | ||
| <button | ||
| className="btn btn-phantom btn-xs px-0" | ||
| onClick={() => { | ||
| setRenameInput(f.name); | ||
| setRenamingFileId(f.id); | ||
| }} | ||
| title="Rename file" | ||
| > | ||
| <PencilIcon size={14} /> | ||
| </button> | ||
| <button | ||
| className="btn btn-phantom btn-xs px-0" | ||
| onClick={() => removeFile(f.id)} | ||
| title="Delete file" | ||
| > | ||
| <XIcon size={16} /> | ||
| </button> | ||
| </div> | ||
| )} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
|
|
@@ -462,10 +574,14 @@ function Preview({ file }) { | |
|
|
||
| return ( | ||
| <div className="space-y-3"> | ||
| <div className="flex items-center justify-between"> | ||
| <h3 className="text-m">{file.name}</h3> | ||
| <div className="flex items-start justify-between gap-2"> | ||
| <h3 className="text-m break-all">{file.name}</h3> | ||
| {downloadUrl && ( | ||
| <a className="btn btn-phantom btn-xs" href={downloadUrl} download> | ||
| <a | ||
| className="btn btn-phantom btn-xs flex-shrink-0" | ||
| href={downloadUrl} | ||
|
Comment on lines
+580
to
+582
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In Tailwind CSS v4.0.7, you should use Citations:
🌐 Web query:
💡 Result: In Tailwind CSS v4, the documentation for the flex-shrink utilities uses shrink-* as the canonical class names. Specifically, v4 documents the utility as Citations:
Use Tailwind v4 In 🤖 Prompt for AI Agents |
||
| download | ||
| > | ||
| Download | ||
| </a> | ||
| )} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against non-string
nameto avoid a 500.A truthy non-string
name(e.g.{ "name": 123 }) passes the!namecheck, thenname.trim()throws aTypeError, which is caught and returned as a generic500instead of a400.🛡️ Proposed fix
🤖 Prompt for AI Agents