Ajoute images et réactions sur les notes du Mur de la Honte
Build and deploy / deploy (push) Successful in 36s
Build and deploy / deploy (push) Successful in 36s
Chaque note peut désormais porter une image facultative : compressée et redimensionnée côté client (resizeImageToBlob, sans forcer un carré comme pour les avatars) avant l'upload, avec une limite dure côté bucket Supabase (3 Mo, webp/jpeg/png uniquement) en filet de sécurité. Le chemin de stockage ne contient jamais le user_id, contrairement aux avatars, pour ne pas faire fuiter l'auteur via l'URL publique de l'image. Ajoute aussi des réactions emoji (👍😂😱❤️🔥) sur chaque note, avec le même principe d'anonymat que le reste du Mur : le total par emoji est public (wall_note_reaction_counts), mais personne ne voit qui a réagi. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+145
-8
@@ -1034,6 +1034,9 @@ grant execute on function public.admin_list_members() to authenticated;
|
||||
-- donner une raison de rouvrir l'app tous les jours de la semaine, pas
|
||||
-- seulement pour le classement. L'anonymat n'est qu'à moitié réel : les
|
||||
-- Archontes voient l'historique complet avec l'auteur de chaque note.
|
||||
-- Chaque note peut porter une image facultative (bucket wall-images,
|
||||
-- compressée côté client, limite de taille côté serveur) et des réactions
|
||||
-- emoji, elles aussi anonymes (voir wall_note_reactions plus bas).
|
||||
|
||||
create table if not exists public.wall_notes (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
@@ -1042,6 +1045,9 @@ create table if not exists public.wall_notes (
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- URL publique dans le bucket wall-images (voir plus bas), facultative.
|
||||
alter table public.wall_notes add column if not exists image_url text;
|
||||
|
||||
alter table public.wall_notes enable row level security;
|
||||
revoke insert, update, delete on public.wall_notes from authenticated, anon;
|
||||
grant select on public.wall_notes to authenticated;
|
||||
@@ -1064,14 +1070,18 @@ end $$;
|
||||
-- Seul moyen pour un Citoyen de voir les notes des autres sans jamais
|
||||
-- exposer qui les a écrites côté client : la RLS ne peut pas masquer une
|
||||
-- colonne pour certaines lignes, donc cette RPC (SECURITY DEFINER) fait le
|
||||
-- filtre elle-même et ne renvoie jamais user_id.
|
||||
-- filtre elle-même et ne renvoie jamais user_id. Le type de retour change
|
||||
-- (ajout d'image_url) : drop explicite requis, create or replace ne peut
|
||||
-- pas changer le type de retour d'une fonction existante.
|
||||
drop function if exists public.wall_notes_today();
|
||||
|
||||
create or replace function public.wall_notes_today()
|
||||
returns table (id uuid, text text, created_at timestamptz)
|
||||
returns table (id uuid, text text, image_url text, created_at timestamptz)
|
||||
language sql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select w.id, w.text, w.created_at
|
||||
select w.id, w.text, w.image_url, w.created_at
|
||||
from public.wall_notes w
|
||||
where (w.created_at at time zone 'Europe/Paris')::date = (now() at time zone 'Europe/Paris')::date
|
||||
order by w.created_at asc;
|
||||
@@ -1084,8 +1094,12 @@ grant execute on function public.wall_notes_today() to authenticated;
|
||||
-- mécanique de toute la semaine. Petite fenêtre de course possible si deux
|
||||
-- personnes postent la même seconde alors qu'il reste 1 place (double
|
||||
-- insertion à 11) — acceptée sciemment, comme d'autres arbitrages similaires
|
||||
-- déjà faits dans le projet pour un groupe de ~12 amis.
|
||||
create or replace function public.post_wall_note(p_text text)
|
||||
-- déjà faits dans le projet pour un groupe de ~12 amis. La signature change
|
||||
-- (ajout de p_image_url) : l'ancienne (text) est explicitement supprimée,
|
||||
-- sinon create or replace créerait une 2e surcharge au lieu de remplacer.
|
||||
drop function if exists public.post_wall_note(text);
|
||||
|
||||
create or replace function public.post_wall_note(p_text text, p_image_url text default null)
|
||||
returns public.wall_notes
|
||||
language plpgsql
|
||||
security definer
|
||||
@@ -1096,6 +1110,7 @@ declare
|
||||
v_today date := (now() at time zone 'Europe/Paris')::date;
|
||||
v_already_posted boolean;
|
||||
v_count_today int;
|
||||
v_image_url text := nullif(trim(coalesce(p_image_url, '')), '');
|
||||
v_row public.wall_notes;
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
@@ -1114,6 +1129,13 @@ begin
|
||||
raise exception 'note trop longue';
|
||||
end if;
|
||||
|
||||
-- Doit venir du bucket wall-images géré par l'app, jamais une URL
|
||||
-- arbitraire (le bucket a lui-même une limite de taille/mime, voir plus
|
||||
-- bas — cette vérification empêche seulement d'y stocker n'importe quoi).
|
||||
if v_image_url is not null and v_image_url not like '%/wall-images/%' then
|
||||
raise exception 'image invalide';
|
||||
end if;
|
||||
|
||||
select exists (
|
||||
select 1 from public.wall_notes
|
||||
where user_id = auth.uid()
|
||||
@@ -1130,15 +1152,130 @@ begin
|
||||
raise exception 'wall full today';
|
||||
end if;
|
||||
|
||||
insert into public.wall_notes (user_id, text)
|
||||
values (auth.uid(), trim(p_text))
|
||||
insert into public.wall_notes (user_id, text, image_url)
|
||||
values (auth.uid(), trim(p_text), v_image_url)
|
||||
returning * into v_row;
|
||||
|
||||
return v_row;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.post_wall_note(text) to authenticated;
|
||||
grant execute on function public.post_wall_note(text, text) to authenticated;
|
||||
|
||||
-- Bucket dédié aux images du Mur (pas "avatars") : chemin d'objet
|
||||
-- volontairement SANS le user_id (contrairement aux avatars) pour ne
|
||||
-- jamais faire fuiter l'auteur via l'URL publique de l'image — cohérent
|
||||
-- avec l'anonymat de wall_notes_today. file_size_limit + allowed_mime_types
|
||||
-- bornent le stockage côté Supabase (coût) ; le client compresse déjà
|
||||
-- l'image bien en dessous de cette limite avant l'upload
|
||||
-- (resizeImageToBlob), qui n'est qu'un filet de sécurité, pas la seule
|
||||
-- garantie.
|
||||
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
values ('wall-images', 'wall-images', true, 3145728, array['image/webp', 'image/jpeg', 'image/png'])
|
||||
on conflict (id) do update set
|
||||
public = excluded.public,
|
||||
file_size_limit = excluded.file_size_limit,
|
||||
allowed_mime_types = excluded.allowed_mime_types;
|
||||
|
||||
drop policy if exists "Wall images are publicly viewable" on storage.objects;
|
||||
create policy "Wall images are publicly viewable"
|
||||
on storage.objects for select
|
||||
using (bucket_id = 'wall-images');
|
||||
|
||||
drop policy if exists "Citizens can upload wall images" on storage.objects;
|
||||
create policy "Citizens can upload wall images"
|
||||
on storage.objects for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'wall-images'
|
||||
and exists (select 1 from public.profiles p where p.id = auth.uid() and p.role <> 'judge')
|
||||
);
|
||||
|
||||
-- Réactions sur les notes -------------------------------------------------
|
||||
-- Même principe d'anonymat que les notes elles-mêmes : chacun ne voit que
|
||||
-- ses propres réactions (pour savoir lesquelles sont déjà activées), le
|
||||
-- compte agrégé par note et par emoji est exposé séparément par
|
||||
-- wall_note_reaction_counts(), sans jamais révéler qui a réagi.
|
||||
create table if not exists public.wall_note_reactions (
|
||||
id bigint generated always as identity primary key,
|
||||
note_id uuid not null references public.wall_notes (id) on delete cascade,
|
||||
user_id uuid not null references public.profiles (id) on delete cascade,
|
||||
emoji text not null check (emoji in ('👍', '😂', '😱', '❤️', '🔥')),
|
||||
created_at timestamptz not null default now(),
|
||||
unique (note_id, user_id, emoji)
|
||||
);
|
||||
|
||||
alter table public.wall_note_reactions enable row level security;
|
||||
revoke insert, update, delete on public.wall_note_reactions from authenticated, anon;
|
||||
grant select on public.wall_note_reactions to authenticated;
|
||||
|
||||
drop policy if exists "wall_note_reactions readable by reactor only" on public.wall_note_reactions;
|
||||
create policy "wall_note_reactions readable by reactor only"
|
||||
on public.wall_note_reactions for select to authenticated
|
||||
using (user_id = auth.uid());
|
||||
|
||||
do $$
|
||||
begin
|
||||
alter publication supabase_realtime add table public.wall_note_reactions;
|
||||
exception
|
||||
when duplicate_object then null;
|
||||
end $$;
|
||||
|
||||
create or replace function public.wall_note_reaction_counts()
|
||||
returns table (note_id uuid, emoji text, count bigint)
|
||||
language sql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select r.note_id, r.emoji, count(*) as count
|
||||
from public.wall_note_reactions r
|
||||
join public.wall_notes w on w.id = r.note_id
|
||||
where (w.created_at at time zone 'Europe/Paris')::date = (now() at time zone 'Europe/Paris')::date
|
||||
group by r.note_id, r.emoji;
|
||||
$$;
|
||||
|
||||
grant execute on function public.wall_note_reaction_counts() to authenticated;
|
||||
|
||||
-- Bascule une réaction (ajoute si absente, retire si déjà posée) en un seul
|
||||
-- aller-retour : un delete puis, si rien n'a été supprimé, un insert —
|
||||
-- évite une lecture préalable puis un choix côté client entre 2 RPC.
|
||||
create or replace function public.toggle_wall_note_reaction(p_note_id uuid, p_emoji text)
|
||||
returns boolean
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
v_allowed constant text[] := array['👍', '😂', '😱', '❤️', '🔥'];
|
||||
v_deleted int;
|
||||
begin
|
||||
if auth.uid() is null then
|
||||
raise exception 'authentication required';
|
||||
end if;
|
||||
|
||||
if not (p_emoji = any(v_allowed)) then
|
||||
raise exception 'emoji non autorisé';
|
||||
end if;
|
||||
|
||||
if not exists (select 1 from public.wall_notes where id = p_note_id) then
|
||||
raise exception 'note introuvable';
|
||||
end if;
|
||||
|
||||
delete from public.wall_note_reactions
|
||||
where note_id = p_note_id and user_id = auth.uid() and emoji = p_emoji;
|
||||
get diagnostics v_deleted = row_count;
|
||||
|
||||
if v_deleted > 0 then
|
||||
return false;
|
||||
end if;
|
||||
|
||||
insert into public.wall_note_reactions (note_id, user_id, emoji)
|
||||
values (p_note_id, auth.uid(), p_emoji);
|
||||
return true;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant execute on function public.toggle_wall_note_reaction(uuid, text) to authenticated;
|
||||
|
||||
-- 15. L'Urne de l'Agora -------------------------------------------------------
|
||||
-- Vote quotidien inspiré du vote à l'urne de l'Athènes antique : chaque jour
|
||||
|
||||
Reference in New Issue
Block a user