diff --git a/src/gallery.el b/src/gallery.el index 01528df..3ad5b4b 100644 --- a/src/gallery.el +++ b/src/gallery.el @@ -271,7 +271,113 @@ window.NEURON_CFG=window.NEURON_CFG||{}; window.NEURON_CFG.supabase_url=\"" + supabase_url + "\"; window.NEURON_CFG.supabase_anon_key=\"" + supabase_anon_key + "\"; - + " diff --git a/src/main.el b/src/main.el index ac0e8d0..2a4c0c4 100644 --- a/src/main.el +++ b/src/main.el @@ -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.