fix(gallery): implement voting JS + fix change-vote server path

Voting was completely broken: gallery.el referenced d8251f5e5aa1.js
which was never written. Buttons rendered disabled with no JS to
enable them, load vote state, or call /api/vote.

Fix 1 — client: inline the voting script directly in gallery.el.
Initializes Supabase client from window.NEURON_CFG, calls
/api/vote-state/<id> on load (with JWT if signed in) to populate
scores and active states, wires vote buttons with toggle logic
(same direction = undo/none), handles sign-in modal with magic-link
flow, re-loads all vote states on auth state change.

Fix 2 — server: replace supabase_upsert_user (upsert via user JWT)
with delete-then-insert. Upsert requires both INSERT + UPDATE RLS
policies; the UPDATE policy is typically absent on share_votes.
Delete (user JWT, RLS-safe) + insert (service key, user already
auth-validated) is reliable for both new votes and vote changes.
This commit is contained in:
Will Anderson
2026-05-03 11:19:09 -05:00
parent 254afd2fb2
commit 102343c8fe
2 changed files with 117 additions and 5 deletions
+10 -4
View File
@@ -1448,11 +1448,17 @@ fn handle_request(method: String, path: String, body: String) -> String {
let del_url: String = v_sb_url + "/rest/v1/share_votes?share_id=eq." + v_id + "&user_id=eq." + v_uid
let _del_resp: String = http_delete_auth(del_url, v_jwt, v_anon)
} else {
// up/down - upsert. PostgREST resolves on the (share_id, user_id) PK
// when on_conflict matches. The trigger fires on insert and update.
// up/down delete any existing vote first (idempotent), then insert fresh.
// Upsert via user JWT requires both INSERT + UPDATE RLS policies; the
// UPDATE policy is often absent. Delete-then-insert is more reliable:
// user JWT covers the DELETE (auth.uid()=user_id RLS passes),
// service key covers the INSERT (bypasses RLS; user identity already
// validated above via supabase_auth_user). The recalc trigger fires on
// both operations and keeps share_cards.score accurate.
let del_url: String = v_sb_url + "/rest/v1/share_votes?share_id=eq." + v_id + "&user_id=eq." + v_uid
let _del_r: String = http_delete_auth(del_url, v_jwt, v_anon)
let row: String = "{\"share_id\":\"" + v_id + "\",\"user_id\":\"" + v_uid + "\",\"direction\":\"" + v_dir + "\"}"
let up_path: String = "share_votes?on_conflict=share_id,user_id"
let _up_resp: String = supabase_upsert_user(v_sb_url, v_anon, v_jwt, up_path, row)
let _ins_r: String = supabase_insert(v_sb_url, v_service, "share_votes", row)
}
// Re-fetch fresh aggregate from share_cards (service key - public read).
// PostgREST returns a JSON array; use json_array_get(0) then json_get.