-- ===================================================================== -- 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 milestone_status as enum ('planned','in_progress','done'); 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', avatar_color text not null default '#7c5cff', 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() ); -- ---------- Jalons (milestones) -------------------------------------- create table if not exists public.milestones ( 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, due_date date, status milestone_status not null default 'planned', created_at timestamptz not null default now() ); -- ---------- Tâches --------------------------------------------------- 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', assignee_id uuid references public.profiles(id) on delete set null, milestone_id uuid references public.milestones(id) on delete set null, 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', 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); create index if not exists idx_milestones_project on public.milestones(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 $$ begin insert into public.profiles (id, full_name, role) values ( new.id, coalesce(new.raw_user_meta_data->>'full_name', split_part(new.email,'@',1)), coalesce((new.raw_user_meta_data->>'role')::member_role, 'other') ) 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.milestones 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','milestones','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.milestones; 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, par membre) ----------- create table if not exists public.time_logs ( id uuid primary key default gen_random_uuid(), task_id uuid not null references public.tasks(id) on delete cascade, 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_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 $$;