From 7dfd5aec9b847a8265b730385daa67565f785243 Mon Sep 17 00:00:00 2001 From: webmaster Date: Tue, 3 Nov 2026 18:40:29 +0000 Subject: [PATCH] fix(freelancer-edit): save sends the hash and uses PATCH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- freelancer-edit/index.html | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/freelancer-edit/index.html b/freelancer-edit/index.html index 01be887..2a3a69c 100644 --- a/freelancer-edit/index.html +++ b/freelancer-edit/index.html @@ -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();