V0.3
This commit is contained in:
+463
-302
@@ -1,302 +1,463 @@
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Schéma de base de données (Supabase / PostgreSQL)
|
||||
-- À exécuter UNE SEULE FOIS dans : Supabase > SQL Editor > New query
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------- Types énumérés -------------------------------------------
|
||||
do $$ begin
|
||||
create type member_role as enum
|
||||
('lead','programmer','artist','game_designer','sound','writer','qa','other');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type task_status as enum ('todo','in_progress','review','done');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type priority_level as enum ('low','medium','high','urgent');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type bug_severity as enum ('low','medium','high','critical');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type bug_status as enum ('open','in_progress','resolved','closed');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
-- ---------- Profils (étend auth.users) -------------------------------
|
||||
create table if not exists public.profiles (
|
||||
id uuid primary key references auth.users(id) on delete cascade,
|
||||
full_name text not null default 'Membre',
|
||||
role member_role not null default 'other', -- rôle principal (= roles[0])
|
||||
roles member_role[] not null default '{}', -- tous les pôles du membre
|
||||
avatar_color text not null default '#7c5cff',
|
||||
avatar_url text, -- photo de profil (optionnelle)
|
||||
onboarded boolean not null default false, -- a terminé l'accueil
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Projets --------------------------------------------------
|
||||
create table if not exists public.projects (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
description text,
|
||||
created_by uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Tâches ---------------------------------------------------
|
||||
-- Un « pôle » (= member_role) reçoit la tâche ; une ou plusieurs
|
||||
-- personnes de ce pôle peuvent y être assignées (assignee_ids).
|
||||
create table if not exists public.tasks (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
project_id uuid not null references public.projects(id) on delete cascade,
|
||||
title text not null,
|
||||
description text,
|
||||
status task_status not null default 'todo',
|
||||
priority priority_level not null default 'medium',
|
||||
pole member_role, -- pôle principal (= poles[0], compat)
|
||||
poles member_role[] not null default '{}', -- tous les pôles concernés par la tâche
|
||||
assignee_ids uuid[] not null default '{}',
|
||||
due_date date,
|
||||
position double precision not null default 0,
|
||||
created_by uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Bugs -----------------------------------------------------
|
||||
create table if not exists public.bugs (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
project_id uuid not null references public.projects(id) on delete cascade,
|
||||
title text not null,
|
||||
description text,
|
||||
severity bug_severity not null default 'medium',
|
||||
status bug_status not null default 'open',
|
||||
priority priority_level not null default 'medium',
|
||||
pole member_role,
|
||||
assignee_id uuid references public.profiles(id) on delete set null,
|
||||
reporter_id uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_tasks_project on public.tasks(project_id);
|
||||
create index if not exists idx_bugs_project on public.bugs(project_id);
|
||||
|
||||
-- ---------- Maj automatique de updated_at ----------------------------
|
||||
create or replace function public.touch_updated_at()
|
||||
returns trigger language plpgsql as $$
|
||||
begin new.updated_at = now(); return new; end $$;
|
||||
|
||||
drop trigger if exists trg_tasks_touch on public.tasks;
|
||||
create trigger trg_tasks_touch before update on public.tasks
|
||||
for each row execute function public.touch_updated_at();
|
||||
|
||||
drop trigger if exists trg_bugs_touch on public.bugs;
|
||||
create trigger trg_bugs_touch before update on public.bugs
|
||||
for each row execute function public.touch_updated_at();
|
||||
|
||||
-- ---------- Création auto du profil à l'inscription ------------------
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger language plpgsql security definer set search_path = public as $$
|
||||
declare r member_role;
|
||||
begin
|
||||
r := coalesce((new.raw_user_meta_data->>'role')::member_role, 'other');
|
||||
insert into public.profiles (id, full_name, role, roles)
|
||||
values (
|
||||
new.id,
|
||||
coalesce(new.raw_user_meta_data->>'full_name', split_part(new.email,'@',1)),
|
||||
r,
|
||||
array[r]
|
||||
)
|
||||
on conflict (id) do nothing;
|
||||
return new;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists on_auth_user_created on auth.users;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute function public.handle_new_user();
|
||||
|
||||
-- ---------- Sécurité (RLS) -------------------------------------------
|
||||
-- Équipe privée et de confiance : tout membre connecté a accès complet.
|
||||
alter table public.profiles enable row level security;
|
||||
alter table public.projects enable row level security;
|
||||
alter table public.tasks enable row level security;
|
||||
alter table public.bugs enable row level security;
|
||||
|
||||
do $$
|
||||
declare t text;
|
||||
begin
|
||||
foreach t in array array['profiles','projects','tasks','bugs'] loop
|
||||
execute format('drop policy if exists %I_all on public.%I', t, t);
|
||||
execute format(
|
||||
'create policy %I_all on public.%I for all to authenticated using (true) with check (true)',
|
||||
t, t);
|
||||
end loop;
|
||||
end $$;
|
||||
|
||||
-- ---------- Realtime (synchro live entre membres) --------------------
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.tasks;
|
||||
exception when duplicate_object then null; end $$;
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.bugs;
|
||||
exception when duplicate_object then null; end $$;
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.profiles;
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
-- =====================================================================
|
||||
-- ADMINISTRATION : comptes & rôles gérés par un panel admin in-app
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------- Champ "administrateur" sur les profils -------------------
|
||||
alter table public.profiles
|
||||
add column if not exists is_admin boolean not null default false;
|
||||
|
||||
-- ---------- Sécurité fine sur les profils ----------------------------
|
||||
-- On remplace la policy permissive "profiles_all" par des règles précises :
|
||||
-- * tout le monde (connecté) PEUT LIRE les profils (affichage de l'équipe)
|
||||
-- * chacun ne peut MODIFIER que SON propre profil
|
||||
-- * la création/suppression de comptes passe uniquement par l'Edge
|
||||
-- Function "admin-users" (clé service_role, côté serveur).
|
||||
drop policy if exists profiles_all on public.profiles;
|
||||
drop policy if exists profiles_select on public.profiles;
|
||||
drop policy if exists profiles_update_self on public.profiles;
|
||||
|
||||
create policy profiles_select on public.profiles
|
||||
for select to authenticated using (true);
|
||||
|
||||
create policy profiles_update_self on public.profiles
|
||||
for update to authenticated
|
||||
using (id = auth.uid()) with check (id = auth.uid());
|
||||
|
||||
-- ---------- Garde-fou : on ne se promeut pas admin soi-même ----------
|
||||
-- Empêche un membre connecté (rôle "authenticated") de changer le champ
|
||||
-- is_admin. Les opérations légitimes passent par l'Edge Function
|
||||
-- (rôle "service_role") ou par le SQL Editor (rôle "postgres"), tous deux
|
||||
-- autorisés ici. Un admin déjà en place peut aussi modifier le statut.
|
||||
create or replace function public.guard_is_admin()
|
||||
returns trigger language plpgsql security definer set search_path = public as $$
|
||||
begin
|
||||
if new.is_admin is distinct from old.is_admin
|
||||
and current_user = 'authenticated'
|
||||
and not exists (
|
||||
select 1 from public.profiles p where p.id = auth.uid() and p.is_admin
|
||||
) then
|
||||
raise exception 'Seul un administrateur peut modifier le statut admin.';
|
||||
end if;
|
||||
return new;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists trg_profiles_guard_admin on public.profiles;
|
||||
create trigger trg_profiles_guard_admin before update on public.profiles
|
||||
for each row execute function public.guard_is_admin();
|
||||
|
||||
-- ---------- Amorçage du PREMIER admin (à faire UNE fois) -------------
|
||||
-- Après ta première inscription dans l'appli, exécute cette ligne en
|
||||
-- remplaçant l'email par le tien pour devenir administrateur :
|
||||
--
|
||||
-- update public.profiles set is_admin = true
|
||||
-- where id = (select id from auth.users where email = 'TON_EMAIL@ exemple.com');
|
||||
--
|
||||
-- Ensuite, tu pourras créer/gérer tous les autres comptes depuis l'appli.
|
||||
|
||||
-- =====================================================================
|
||||
-- TAGS (« jobs ») & SUIVI DU TEMPS
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------- Tags sur les tâches (ex: Programmation, Art, Audio) ------
|
||||
alter table public.tasks
|
||||
add column if not exists tags text[] not null default '{}';
|
||||
|
||||
-- ---------- Journal du temps passé (par tâche OU activité libre) -----
|
||||
-- task_id null + label => temps « libre » (réunion, etc.).
|
||||
create table if not exists public.time_logs (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
project_id uuid references public.projects(id) on delete cascade,
|
||||
task_id uuid references public.tasks(id) on delete cascade,
|
||||
label text,
|
||||
user_id uuid references public.profiles(id) on delete set null,
|
||||
minutes integer not null check (minutes > 0),
|
||||
note text,
|
||||
logged_at date not null default current_date,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_time_logs_task on public.time_logs(task_id);
|
||||
create index if not exists idx_time_logs_project on public.time_logs(project_id);
|
||||
create index if not exists idx_time_logs_user on public.time_logs(user_id);
|
||||
|
||||
alter table public.time_logs enable row level security;
|
||||
drop policy if exists time_logs_all on public.time_logs;
|
||||
create policy time_logs_all on public.time_logs
|
||||
for all to authenticated using (true) with check (true);
|
||||
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.time_logs;
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
-- =====================================================================
|
||||
-- NOTIFICATIONS (assignation d'une tâche / d'un bug à un pôle)
|
||||
-- =====================================================================
|
||||
create table if not exists public.notifications (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid not null references public.profiles(id) on delete cascade,
|
||||
project_id uuid references public.projects(id) on delete cascade,
|
||||
kind text not null, -- 'task' | 'bug'
|
||||
title text not null, -- ex : « Nouvelle tâche »
|
||||
body text, -- titre de la tâche / du bug
|
||||
entity_id uuid, -- id de la tâche / du bug
|
||||
pole member_role, -- pôle destinataire
|
||||
read boolean not null default false,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_notifications_user on public.notifications(user_id);
|
||||
|
||||
alter table public.notifications enable row level security;
|
||||
|
||||
-- Chacun lit/modifie SES notifications ; l'insertion est ouverte à tout
|
||||
-- membre connecté (afin de notifier les autres membres d'un pôle).
|
||||
drop policy if exists notifications_select on public.notifications;
|
||||
drop policy if exists notifications_insert on public.notifications;
|
||||
drop policy if exists notifications_update on public.notifications;
|
||||
drop policy if exists notifications_delete on public.notifications;
|
||||
|
||||
create policy notifications_select on public.notifications
|
||||
for select to authenticated using (user_id = auth.uid());
|
||||
create policy notifications_insert on public.notifications
|
||||
for insert to authenticated with check (true);
|
||||
create policy notifications_update on public.notifications
|
||||
for update to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid());
|
||||
create policy notifications_delete on public.notifications
|
||||
for delete to authenticated using (user_id = auth.uid());
|
||||
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.notifications;
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
-- =====================================================================
|
||||
-- STOCKAGE : photos de profil (bucket "avatars")
|
||||
-- =====================================================================
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('avatars', 'avatars', true)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
drop policy if exists avatars_read on storage.objects;
|
||||
drop policy if exists avatars_insert on storage.objects;
|
||||
drop policy if exists avatars_update on storage.objects;
|
||||
drop policy if exists avatars_delete on storage.objects;
|
||||
|
||||
create policy avatars_read on storage.objects
|
||||
for select using (bucket_id = 'avatars');
|
||||
create policy avatars_insert on storage.objects
|
||||
for insert to authenticated with check (bucket_id = 'avatars');
|
||||
create policy avatars_update on storage.objects
|
||||
for update to authenticated using (bucket_id = 'avatars');
|
||||
create policy avatars_delete on storage.objects
|
||||
for delete to authenticated using (bucket_id = 'avatars');
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Schéma complet (Supabase / PostgreSQL)
|
||||
--
|
||||
-- NOUVELLE base : exécute CE fichier, une seule fois.
|
||||
-- Base EXISTANTE : exécute plutôt « update_all.sql » (ne perd rien).
|
||||
--
|
||||
-- Où ? Supabase > SQL Editor > New query > coller > Run.
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------- Types énumérés -------------------------------------------
|
||||
do $$ begin
|
||||
create type member_role as enum
|
||||
('lead','programmer','artist','game_designer','sound','writer','qa','other');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type task_status as enum ('todo','in_progress','review','done');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type priority_level as enum ('low','medium','high','urgent');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type bug_severity as enum ('low','medium','high','critical');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type bug_status as enum ('open','in_progress','resolved','closed');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
-- =====================================================================
|
||||
-- TABLES
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------- Profils (étend auth.users) -------------------------------
|
||||
-- `roles` est la SEULE source de vérité pour les pôles d'un membre.
|
||||
create table if not exists public.profiles (
|
||||
id uuid primary key references auth.users(id) on delete cascade,
|
||||
full_name text not null default 'Membre',
|
||||
roles member_role[] not null default '{}',
|
||||
avatar_color text not null default '#7c5cff',
|
||||
avatar_url text,
|
||||
is_admin boolean not null default false,
|
||||
onboarded boolean not null default false,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Projets --------------------------------------------------
|
||||
create table if not exists public.projects (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
description text,
|
||||
created_by uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Tâches ---------------------------------------------------
|
||||
-- Une tâche est confiée à un ou plusieurs PÔLES (poles) et assignée
|
||||
-- nommément à une ou plusieurs PERSONNES (assignee_ids).
|
||||
create table if not exists public.tasks (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
project_id uuid not null references public.projects(id) on delete cascade,
|
||||
title text not null,
|
||||
description text,
|
||||
status task_status not null default 'todo',
|
||||
priority priority_level not null default 'medium',
|
||||
poles member_role[] not null default '{}',
|
||||
assignee_ids uuid[] not null default '{}',
|
||||
checklist jsonb not null default '[]'::jsonb, -- [{id, text, done}]
|
||||
tags text[] not null default '{}',
|
||||
due_date date,
|
||||
position double precision not null default 0,
|
||||
created_by uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Bugs -----------------------------------------------------
|
||||
-- Même modèle que les tâches (pôles + assignés multiples). La SÉVÉRITÉ
|
||||
-- est le seul axe d'importance : la colonne `priority` faisait doublon
|
||||
-- et n'est plus exposée dans l'interface.
|
||||
create table if not exists public.bugs (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
project_id uuid not null references public.projects(id) on delete cascade,
|
||||
title text not null,
|
||||
description text,
|
||||
severity bug_severity not null default 'medium',
|
||||
status bug_status not null default 'open',
|
||||
priority priority_level not null default 'medium',
|
||||
poles member_role[] not null default '{}',
|
||||
assignee_ids uuid[] not null default '{}',
|
||||
reporter_id uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Journal du temps (tâche OU activité libre) ---------------
|
||||
create table if not exists public.time_logs (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
project_id uuid references public.projects(id) on delete cascade,
|
||||
task_id uuid references public.tasks(id) on delete cascade,
|
||||
bug_id uuid references public.bugs(id) on delete cascade,
|
||||
label text, -- si ni tâche ni bug
|
||||
user_id uuid references public.profiles(id) on delete set null,
|
||||
minutes integer not null check (minutes > 0),
|
||||
note text,
|
||||
logged_at date not null default current_date,
|
||||
created_at timestamptz not null default now(),
|
||||
-- Une entrée porte sur une tâche OU un bug OU rien : jamais les deux.
|
||||
constraint time_logs_one_target check (not (task_id is not null and bug_id is not null))
|
||||
);
|
||||
|
||||
-- ---------- Commentaires (tâches et bugs) ----------------------------
|
||||
create table if not exists public.comments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
project_id uuid not null references public.projects(id) on delete cascade,
|
||||
entity_kind text not null check (entity_kind in ('task','bug')),
|
||||
entity_id uuid not null,
|
||||
author_id uuid references public.profiles(id) on delete set null,
|
||||
body text not null check (length(btrim(body)) > 0),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Pièces jointes (captures d'écran, logs) ------------------
|
||||
create table if not exists public.attachments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
project_id uuid not null references public.projects(id) on delete cascade,
|
||||
entity_kind text not null check (entity_kind in ('task','bug')),
|
||||
entity_id uuid not null,
|
||||
uploader_id uuid references public.profiles(id) on delete set null,
|
||||
name text not null,
|
||||
path text not null,
|
||||
size bigint not null default 0,
|
||||
mime text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Documentation (fiches créées ou importées) ----------------
|
||||
-- Le contenu est du Markdown : format pivot, lisible et cherchable. Un
|
||||
-- .docx importé est converti en Markdown au moment de l'import.
|
||||
create table if not exists public.documents (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
project_id uuid not null references public.projects(id) on delete cascade,
|
||||
title text not null,
|
||||
content text not null default '',
|
||||
tags text[] not null default '{}',
|
||||
source_name text,
|
||||
created_by uuid references public.profiles(id) on delete set null,
|
||||
updated_by uuid references public.profiles(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- Notifications --------------------------------------------
|
||||
create table if not exists public.notifications (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid not null references public.profiles(id) on delete cascade,
|
||||
project_id uuid references public.projects(id) on delete cascade,
|
||||
kind text not null, -- 'task' | 'bug' | 'comment'
|
||||
entity_kind text not null default 'task', -- ce qu'on ouvre au clic
|
||||
entity_id uuid,
|
||||
title text not null,
|
||||
body text,
|
||||
pole member_role,
|
||||
read boolean not null default false,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- =====================================================================
|
||||
-- INDEX
|
||||
-- =====================================================================
|
||||
create index if not exists idx_tasks_project on public.tasks(project_id);
|
||||
create index if not exists idx_tasks_project_status on public.tasks(project_id, status, position);
|
||||
create index if not exists idx_bugs_project on public.bugs(project_id);
|
||||
create index if not exists idx_bugs_project_status on public.bugs(project_id, status);
|
||||
create index if not exists idx_time_logs_task on public.time_logs(task_id);
|
||||
create index if not exists idx_time_logs_bug on public.time_logs(bug_id);
|
||||
create index if not exists idx_time_logs_project on public.time_logs(project_id);
|
||||
create index if not exists idx_time_logs_user on public.time_logs(user_id);
|
||||
create index if not exists idx_time_logs_date on public.time_logs(project_id, logged_at);
|
||||
create index if not exists idx_comments_entity on public.comments(entity_kind, entity_id);
|
||||
create index if not exists idx_comments_project on public.comments(project_id);
|
||||
create index if not exists idx_attachments_entity on public.attachments(entity_kind, entity_id);
|
||||
create index if not exists idx_attachments_project on public.attachments(project_id);
|
||||
create index if not exists idx_documents_project on public.documents(project_id, updated_at desc);
|
||||
create index if not exists idx_notifications_user on public.notifications(user_id);
|
||||
create index if not exists idx_notifications_user_unread
|
||||
on public.notifications(user_id, read, created_at desc);
|
||||
|
||||
-- =====================================================================
|
||||
-- FONCTIONS & TRIGGERS
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------- Maj automatique de updated_at ----------------------------
|
||||
create or replace function public.touch_updated_at()
|
||||
returns trigger language plpgsql as $$
|
||||
begin new.updated_at = now(); return new; end $$;
|
||||
|
||||
drop trigger if exists trg_tasks_touch on public.tasks;
|
||||
create trigger trg_tasks_touch before update on public.tasks
|
||||
for each row execute function public.touch_updated_at();
|
||||
|
||||
drop trigger if exists trg_bugs_touch on public.bugs;
|
||||
create trigger trg_bugs_touch before update on public.bugs
|
||||
for each row execute function public.touch_updated_at();
|
||||
|
||||
drop trigger if exists trg_documents_touch on public.documents;
|
||||
create trigger trg_documents_touch before update on public.documents
|
||||
for each row execute function public.touch_updated_at();
|
||||
|
||||
drop trigger if exists trg_comments_touch on public.comments;
|
||||
create trigger trg_comments_touch before update on public.comments
|
||||
for each row execute function public.touch_updated_at();
|
||||
|
||||
-- ---------- Création auto du profil à l'inscription ------------------
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger language plpgsql security definer set search_path = public as $$
|
||||
declare
|
||||
palette text[] := array['#7c5cff','#00d2b8','#ff5c8a','#ffa53c','#3ca8ff','#9bdb4d','#e15cff','#ff6b6b'];
|
||||
r member_role;
|
||||
begin
|
||||
r := coalesce((new.raw_user_meta_data->>'role')::member_role, 'other');
|
||||
insert into public.profiles (id, full_name, roles, avatar_color)
|
||||
values (
|
||||
new.id,
|
||||
coalesce(new.raw_user_meta_data->>'full_name', split_part(new.email, '@', 1)),
|
||||
array[r],
|
||||
palette[1 + (abs(hashtext(new.id::text)) % array_length(palette, 1))]
|
||||
)
|
||||
on conflict (id) do nothing;
|
||||
return new;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists on_auth_user_created on auth.users;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute function public.handle_new_user();
|
||||
|
||||
-- ---------- Réordonnancement du Kanban en une transaction ------------
|
||||
-- L'appli envoie la liste ordonnée des tâches d'une colonne ; une seule
|
||||
-- requête réécrit statut + position pour toute la colonne.
|
||||
create or replace function public.reorder_tasks(p_status task_status, p_ids uuid[])
|
||||
returns void
|
||||
language plpgsql
|
||||
security invoker
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
update public.tasks t
|
||||
set status = p_status,
|
||||
position = ordered.idx
|
||||
from (
|
||||
select id, (ord - 1)::double precision as idx
|
||||
from unnest(p_ids) with ordinality as u(id, ord)
|
||||
) ordered
|
||||
where t.id = ordered.id;
|
||||
end $$;
|
||||
|
||||
grant execute on function public.reorder_tasks(task_status, uuid[]) to authenticated;
|
||||
|
||||
-- ---------- Garde-fou : on ne se promeut pas admin soi-même ----------
|
||||
create or replace function public.guard_is_admin()
|
||||
returns trigger language plpgsql security definer set search_path = public as $$
|
||||
begin
|
||||
if new.is_admin is distinct from old.is_admin
|
||||
and current_user = 'authenticated'
|
||||
and not exists (
|
||||
select 1 from public.profiles p where p.id = auth.uid() and p.is_admin
|
||||
) then
|
||||
raise exception 'Seul un administrateur peut modifier le statut admin.';
|
||||
end if;
|
||||
return new;
|
||||
end $$;
|
||||
|
||||
drop trigger if exists trg_profiles_guard_admin on public.profiles;
|
||||
create trigger trg_profiles_guard_admin before update on public.profiles
|
||||
for each row execute function public.guard_is_admin();
|
||||
|
||||
-- =====================================================================
|
||||
-- SÉCURITÉ (RLS)
|
||||
-- Équipe privée et de confiance : tout membre connecté lit et écrit le
|
||||
-- contenu des projets. Les exceptions sont explicites ci-dessous :
|
||||
-- · un profil ne se modifie que par son propriétaire
|
||||
-- · un projet ne se supprime que par son créateur ou un admin
|
||||
-- · un commentaire ne s'édite que par son auteur
|
||||
-- =====================================================================
|
||||
alter table public.profiles enable row level security;
|
||||
alter table public.projects enable row level security;
|
||||
alter table public.tasks enable row level security;
|
||||
alter table public.bugs enable row level security;
|
||||
alter table public.time_logs enable row level security;
|
||||
alter table public.comments enable row level security;
|
||||
alter table public.attachments enable row level security;
|
||||
alter table public.documents enable row level security;
|
||||
alter table public.notifications enable row level security;
|
||||
|
||||
-- ---------- Contenu partagé (tâches, bugs, temps) --------------------
|
||||
do $$
|
||||
declare t text;
|
||||
begin
|
||||
foreach t in array array['tasks','bugs','time_logs','documents'] loop
|
||||
execute format('drop policy if exists %I_all on public.%I', t, t);
|
||||
execute format(
|
||||
'create policy %I_all on public.%I for all to authenticated using (true) with check (true)',
|
||||
t, t);
|
||||
end loop;
|
||||
end $$;
|
||||
|
||||
-- ---------- Profils ---------------------------------------------------
|
||||
drop policy if exists profiles_all on public.profiles;
|
||||
drop policy if exists profiles_select on public.profiles;
|
||||
drop policy if exists profiles_update_self on public.profiles;
|
||||
|
||||
create policy profiles_select on public.profiles
|
||||
for select to authenticated using (true);
|
||||
create policy profiles_update_self on public.profiles
|
||||
for update to authenticated
|
||||
using (id = auth.uid()) with check (id = auth.uid());
|
||||
-- La création / suppression de comptes passe par l'Edge Function
|
||||
-- « admin-users » (clé service_role, côté serveur).
|
||||
|
||||
-- ---------- Projets ---------------------------------------------------
|
||||
drop policy if exists projects_all on public.projects;
|
||||
drop policy if exists projects_select on public.projects;
|
||||
drop policy if exists projects_insert on public.projects;
|
||||
drop policy if exists projects_update on public.projects;
|
||||
drop policy if exists projects_delete on public.projects;
|
||||
|
||||
create policy projects_select on public.projects
|
||||
for select to authenticated using (true);
|
||||
create policy projects_insert on public.projects
|
||||
for insert to authenticated with check (true);
|
||||
create policy projects_update on public.projects
|
||||
for update to authenticated using (true) with check (true);
|
||||
create policy projects_delete on public.projects
|
||||
for delete to authenticated
|
||||
using (
|
||||
created_by = auth.uid()
|
||||
or exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
-- ---------- Commentaires ----------------------------------------------
|
||||
drop policy if exists comments_select on public.comments;
|
||||
drop policy if exists comments_insert on public.comments;
|
||||
drop policy if exists comments_update on public.comments;
|
||||
drop policy if exists comments_delete on public.comments;
|
||||
|
||||
create policy comments_select on public.comments
|
||||
for select to authenticated using (true);
|
||||
create policy comments_insert on public.comments
|
||||
for insert to authenticated with check (author_id = auth.uid());
|
||||
create policy comments_update on public.comments
|
||||
for update to authenticated
|
||||
using (author_id = auth.uid()) with check (author_id = auth.uid());
|
||||
create policy comments_delete on public.comments
|
||||
for delete to authenticated
|
||||
using (
|
||||
author_id = auth.uid()
|
||||
or exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
-- ---------- Pièces jointes --------------------------------------------
|
||||
drop policy if exists attachments_select on public.attachments;
|
||||
drop policy if exists attachments_insert on public.attachments;
|
||||
drop policy if exists attachments_delete on public.attachments;
|
||||
|
||||
create policy attachments_select on public.attachments
|
||||
for select to authenticated using (true);
|
||||
create policy attachments_insert on public.attachments
|
||||
for insert to authenticated with check (uploader_id = auth.uid());
|
||||
create policy attachments_delete on public.attachments
|
||||
for delete to authenticated
|
||||
using (
|
||||
uploader_id = auth.uid()
|
||||
or exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
-- ---------- Notifications ---------------------------------------------
|
||||
drop policy if exists notifications_select on public.notifications;
|
||||
drop policy if exists notifications_insert on public.notifications;
|
||||
drop policy if exists notifications_update on public.notifications;
|
||||
drop policy if exists notifications_delete on public.notifications;
|
||||
|
||||
create policy notifications_select on public.notifications
|
||||
for select to authenticated using (user_id = auth.uid());
|
||||
-- Insertion ouverte : un membre doit pouvoir notifier ses coéquipiers.
|
||||
create policy notifications_insert on public.notifications
|
||||
for insert to authenticated with check (true);
|
||||
create policy notifications_update on public.notifications
|
||||
for update to authenticated
|
||||
using (user_id = auth.uid()) with check (user_id = auth.uid());
|
||||
create policy notifications_delete on public.notifications
|
||||
for delete to authenticated using (user_id = auth.uid());
|
||||
|
||||
-- =====================================================================
|
||||
-- TEMPS RÉEL (synchro live entre membres)
|
||||
-- =====================================================================
|
||||
do $$
|
||||
declare t text;
|
||||
begin
|
||||
foreach t in array array['tasks','bugs','profiles','projects','time_logs',
|
||||
'notifications','comments','attachments','documents'] loop
|
||||
begin
|
||||
execute format('alter publication supabase_realtime add table public.%I', t);
|
||||
exception when duplicate_object then null;
|
||||
end;
|
||||
end loop;
|
||||
end $$;
|
||||
|
||||
-- =====================================================================
|
||||
-- STOCKAGE
|
||||
-- · avatars : photos de profil
|
||||
-- · attachments : captures d'écran et fichiers joints aux bugs/tâches
|
||||
-- =====================================================================
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('avatars', 'avatars', true), ('attachments', 'attachments', true)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
drop policy if exists avatars_read on storage.objects;
|
||||
drop policy if exists avatars_insert on storage.objects;
|
||||
drop policy if exists avatars_update on storage.objects;
|
||||
drop policy if exists avatars_delete on storage.objects;
|
||||
|
||||
create policy avatars_read on storage.objects
|
||||
for select using (bucket_id = 'avatars');
|
||||
create policy avatars_insert on storage.objects
|
||||
for insert to authenticated with check (bucket_id = 'avatars');
|
||||
create policy avatars_update on storage.objects
|
||||
for update to authenticated using (bucket_id = 'avatars');
|
||||
create policy avatars_delete on storage.objects
|
||||
for delete to authenticated using (bucket_id = 'avatars');
|
||||
|
||||
drop policy if exists attachments_read on storage.objects;
|
||||
drop policy if exists attachments_write on storage.objects;
|
||||
drop policy if exists attachments_remove on storage.objects;
|
||||
|
||||
create policy attachments_read on storage.objects
|
||||
for select using (bucket_id = 'attachments');
|
||||
create policy attachments_write on storage.objects
|
||||
for insert to authenticated with check (bucket_id = 'attachments');
|
||||
create policy attachments_remove on storage.objects
|
||||
for delete to authenticated using (bucket_id = 'attachments');
|
||||
|
||||
-- =====================================================================
|
||||
-- DERNIÈRE ÉTAPE — devenir administrateur (à faire UNE fois)
|
||||
-- Après avoir créé ton compte dans l'application, exécute :
|
||||
--
|
||||
-- update public.profiles set is_admin = true
|
||||
-- where id = (select id from auth.users where email = 'TON_EMAIL@exemple.com');
|
||||
--
|
||||
-- Tu pourras ensuite gérer tous les comptes depuis l'onglet Administration.
|
||||
-- =====================================================================
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Dernière ligne : forcer l'API à relire le schéma.
|
||||
-- Sans ça, PostgREST peut continuer à servir l'ancienne structure et
|
||||
-- renvoyer « Could not find the 'xxx' column ... in the schema cache »
|
||||
-- pendant plusieurs minutes après la migration.
|
||||
-- ---------------------------------------------------------------------
|
||||
notify pgrst, 'reload schema';
|
||||
|
||||
Reference in New Issue
Block a user