fix(freelancer-edit): save sends the hash and uses PATCH

The save was authorised by the record id alone — ids are small
sequential integers, publicly visible on every directory card, so
a PATCH carrying only an id let one visitor overwrite another's
listing. The save now:

- uses PATCH (C.saveMethod), matching n8n
- carries the hash in the body under the same "has" key the
  delete already sends, so save and delete share one credential
  path, alongside the record id
- refuses to fire at all when the page holds no hash, and says why
  in the status line

Everything else is unchanged: the 15-point gate, receipt only on
2xx, the honest failure line that keeps the edits, the save.disabled
handling and the DELETE shape.
This commit is contained in:
webmaster committed 2026-11-03 18:40:29 +00:00
1 parent 768dc1151a
commit 7dfd5aec9b
1 file changed
+18 -6
+18 -6
View File
@@ -164,9 +164,11 @@
hashKeys: ["has", "hash"],
// Wire name used OUTWARD on the GET query string.
hashWireKey: "has",
// SAVE: POST with the record id — the admin path's convention (POST with an id =
// update an existing record). Match this method in the n8n workflow.
saveMethod: "POST",
// SAVE: PATCH the existing record, carrying BOTH the hash (the credential, same key
// the delete already sends) and the record id. An id alone authorises nothing: record
// ids are small sequential integers, publicly visible on every directory card, so a
// request without the hash is refused before it is sent. Match this method in n8n.
saveMethod: "PATCH",
// DELETE: unverified. A route that refuses it (404/405/501) is a clear failure and
// this page never reports success on a non-2xx. Match this method in the workflow.
deleteMethod: "DELETE",
@@ -566,12 +568,22 @@
focusTotalLine.setAttribute("data-bad", "1");
return;
}
// A save is authorised by the HASH, not by the id. If this page holds no hash there
// is nothing to prove ownership, so the request is not fired at all — a PATCH
// carrying only an id is exactly the hole this closes. Say why, do not fail silently.
if (!hash) {
line.className = "status state-err";
line.textContent = "This link arrived without its code, so we cannot save your " +
"changes. Open it again from the email that brought you here.";
return;
}
save.disabled = true;
line.className = "status";
line.textContent = "Saving…";
// The body shape: the record's id + every field this form owns. SAVE_METHOD is
// C.saveMethod ("POST", the admin path's convention).
var payload = { id: rec && rec.id !== undefined ? rec.id : undefined };
// The body shape: the credential (the SAME "has" key the delete sends, one
// credential path for both) + the record's id + every field this form owns.
// Method is C.saveMethod ("PATCH").
var payload = { has: hash, id: rec && rec.id !== undefined ? rec.id : undefined };
payload.Name = document.getElementById("edName").value.trim();
payload.Focus = document.getElementById("edFocus").value.trim();
payload.description = document.getElementById("edDescription").value.trim();