V0.3
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
// =====================================================================
|
||||
// Edge Function : admin-users
|
||||
// Gère les comptes (lister / créer / supprimer / modifier rôle & admin).
|
||||
// Gère les comptes (lister / créer / supprimer / modifier).
|
||||
// Tourne sur les serveurs Supabase et garde la clé service_role en
|
||||
// sécurité — elle n'est JAMAIS exposée dans l'appli.
|
||||
// sécurité — elle n'est JAMAIS exposée dans l'application.
|
||||
//
|
||||
// Déploiement (voir le guide) :
|
||||
// Déploiement :
|
||||
// supabase functions deploy admin-users --no-verify-jwt
|
||||
// (la fonction vérifie elle-même l'identité de l'appelant ci-dessous.)
|
||||
// (la fonction vérifie elle-même l'identité de l'appelant ci-dessous)
|
||||
//
|
||||
// v10 : la colonne `profiles.role` a disparu ; seul `roles[]` fait foi.
|
||||
// =====================================================================
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
@@ -20,6 +22,17 @@ const SUPABASE_URL = Deno.env.get('SUPABASE_URL')!
|
||||
const ANON_KEY = Deno.env.get('SUPABASE_ANON_KEY')!
|
||||
const SERVICE_ROLE = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
|
||||
const VALID_ROLES = [
|
||||
'lead',
|
||||
'programmer',
|
||||
'artist',
|
||||
'game_designer',
|
||||
'sound',
|
||||
'writer',
|
||||
'qa',
|
||||
'other'
|
||||
]
|
||||
|
||||
function json(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
@@ -27,8 +40,13 @@ function json(body: unknown, status = 200): Response {
|
||||
})
|
||||
}
|
||||
|
||||
/** Ne garde que des pôles valides (l'enum SQL rejetterait le reste). */
|
||||
function cleanRoles(input: unknown): string[] {
|
||||
if (!Array.isArray(input)) return []
|
||||
return [...new Set(input.filter((r) => typeof r === 'string' && VALID_ROLES.includes(r)))]
|
||||
}
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
// Pré-vol CORS (envoyé automatiquement par le navigateur/webview).
|
||||
if (req.method === 'OPTIONS') return new Response('ok', { headers: cors })
|
||||
|
||||
try {
|
||||
@@ -43,7 +61,7 @@ Deno.serve(async (req) => {
|
||||
} = await caller.auth.getUser()
|
||||
if (userErr || !user) return json({ error: 'Non authentifié.' }, 401)
|
||||
|
||||
// --- 2) Client "admin" (service_role) pour les opérations sensibles ---
|
||||
// --- 2) Client « admin » (service_role) pour les opérations sensibles ---
|
||||
const admin = createClient(SUPABASE_URL, SERVICE_ROLE, {
|
||||
auth: { autoRefreshToken: false, persistSession: false }
|
||||
})
|
||||
@@ -61,63 +79,61 @@ Deno.serve(async (req) => {
|
||||
|
||||
switch (action) {
|
||||
case 'list': {
|
||||
// Emails dans auth.users + métadonnées dans profiles → on fusionne.
|
||||
// Les emails sont dans auth.users, le reste dans profiles → fusion.
|
||||
const { data: list, error } = await admin.auth.admin.listUsers({ perPage: 1000 })
|
||||
if (error) throw error
|
||||
const { data: profiles } = await admin.from('profiles').select('*')
|
||||
const byId = new Map((profiles ?? []).map((pr: any) => [pr.id, pr]))
|
||||
const users = list.users.map((u) => {
|
||||
const pr: any = byId.get(u.id) ?? {}
|
||||
const roles =
|
||||
Array.isArray(pr.roles) && pr.roles.length
|
||||
? pr.roles
|
||||
: pr.role
|
||||
? [pr.role]
|
||||
: []
|
||||
return {
|
||||
id: u.id,
|
||||
email: u.email ?? '',
|
||||
full_name: pr.full_name ?? '',
|
||||
role: pr.role ?? 'other',
|
||||
roles,
|
||||
is_admin: pr.is_admin ?? false,
|
||||
avatar_color: pr.avatar_color ?? '#7c5cff',
|
||||
avatar_url: pr.avatar_url ?? null,
|
||||
created_at: pr.created_at ?? u.created_at
|
||||
}
|
||||
})
|
||||
const byId = new Map((profiles ?? []).map((pr: Record<string, unknown>) => [pr.id, pr]))
|
||||
|
||||
const users = list.users
|
||||
.map((u) => {
|
||||
const pr = (byId.get(u.id) ?? {}) as Record<string, unknown>
|
||||
return {
|
||||
id: u.id,
|
||||
email: u.email ?? '',
|
||||
full_name: (pr.full_name as string) ?? '',
|
||||
roles: Array.isArray(pr.roles) ? pr.roles : [],
|
||||
is_admin: Boolean(pr.is_admin),
|
||||
avatar_color: (pr.avatar_color as string) ?? '#7c5cff',
|
||||
avatar_url: (pr.avatar_url as string) ?? null,
|
||||
created_at: (pr.created_at as string) ?? u.created_at
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.full_name.localeCompare(b.full_name, 'fr'))
|
||||
|
||||
return json(users)
|
||||
}
|
||||
|
||||
case 'create': {
|
||||
const { email, password, full_name, role, roles, is_admin } = p
|
||||
const { email, password, full_name, roles, is_admin } = p
|
||||
if (!email || !password) return json({ error: 'Email et mot de passe requis.' }, 400)
|
||||
if (String(password).length < 6)
|
||||
if (String(password).length < 6) {
|
||||
return json({ error: 'Le mot de passe doit faire au moins 6 caractères.' }, 400)
|
||||
}
|
||||
|
||||
// Liste de pôles (rôles) ; le premier fait office de rôle principal.
|
||||
const roleList: string[] = Array.isArray(roles) && roles.length ? roles : role ? [role] : []
|
||||
const roleList = cleanRoles(roles)
|
||||
const primary = roleList[0] ?? 'other'
|
||||
const name = full_name || String(email).split('@')[0]
|
||||
|
||||
// Crée le compte (email déjà confirmé : connexion immédiate possible).
|
||||
// email_confirm : le membre peut se connecter immédiatement.
|
||||
const { data: created, error } = await admin.auth.admin.createUser({
|
||||
email,
|
||||
password,
|
||||
email_confirm: true,
|
||||
user_metadata: { full_name: full_name || email.split('@')[0], role: primary }
|
||||
user_metadata: { full_name: name, role: primary }
|
||||
})
|
||||
if (error) return json({ error: error.message }, 400)
|
||||
|
||||
// Le trigger handle_new_user a créé le profil ; on cale les champs.
|
||||
await admin
|
||||
const { error: profErr } = await admin
|
||||
.from('profiles')
|
||||
.update({
|
||||
full_name: full_name || email.split('@')[0],
|
||||
role: primary,
|
||||
full_name: name,
|
||||
roles: roleList.length ? roleList : [primary],
|
||||
is_admin: !!is_admin
|
||||
is_admin: Boolean(is_admin)
|
||||
})
|
||||
.eq('id', created.user.id)
|
||||
if (profErr) return json({ error: profErr.message }, 400)
|
||||
|
||||
return json({ ok: true, id: created.user.id })
|
||||
}
|
||||
@@ -125,58 +141,61 @@ Deno.serve(async (req) => {
|
||||
case 'delete': {
|
||||
const { id } = p
|
||||
if (!id) return json({ error: 'Identifiant manquant.' }, 400)
|
||||
if (id === user.id)
|
||||
if (id === user.id) {
|
||||
return json({ error: 'Tu ne peux pas supprimer ton propre compte.' }, 400)
|
||||
}
|
||||
const { error } = await admin.auth.admin.deleteUser(id) // profil supprimé en cascade
|
||||
if (error) return json({ error: error.message }, 400)
|
||||
return json({ ok: true })
|
||||
}
|
||||
|
||||
case 'update': {
|
||||
const { id, full_name, email, password, role, roles, is_admin, avatar_url } = p
|
||||
const { id, full_name, email, password, roles, is_admin, avatar_url } = p
|
||||
if (!id) return json({ error: 'Identifiant manquant.' }, 400)
|
||||
if (id === user.id && is_admin === false)
|
||||
if (id === user.id && is_admin === false) {
|
||||
return json({ error: 'Tu ne peux pas retirer ton propre statut admin.' }, 400)
|
||||
}
|
||||
|
||||
// --- a) Champs du compte auth (email / mot de passe / métadonnées) ---
|
||||
// --- a) Compte auth (email / mot de passe / métadonnées) ---
|
||||
const authPatch: Record<string, unknown> = {}
|
||||
if (email !== undefined && String(email).trim()) {
|
||||
authPatch.email = String(email).trim()
|
||||
authPatch.email_confirm = true // évite le mail de confirmation
|
||||
}
|
||||
if (password !== undefined && String(password) !== '') {
|
||||
if (String(password).length < 6)
|
||||
if (String(password).length < 6) {
|
||||
return json({ error: 'Le mot de passe doit faire au moins 6 caractères.' }, 400)
|
||||
}
|
||||
authPatch.password = String(password)
|
||||
}
|
||||
if (full_name !== undefined)
|
||||
authPatch.user_metadata = { full_name, role: Array.isArray(roles) ? roles[0] : role }
|
||||
if (full_name !== undefined) {
|
||||
// On fusionne avec les métadonnées existantes plutôt que de les écraser.
|
||||
const { data: existing } = await admin.auth.admin.getUserById(id)
|
||||
authPatch.user_metadata = {
|
||||
...(existing?.user?.user_metadata ?? {}),
|
||||
full_name
|
||||
}
|
||||
}
|
||||
if (Object.keys(authPatch).length > 0) {
|
||||
const { error: authErr } = await admin.auth.admin.updateUserById(id, authPatch)
|
||||
if (authErr) return json({ error: authErr.message }, 400)
|
||||
}
|
||||
|
||||
// --- b) Champs du profil (nom, pôles, admin) ---
|
||||
// --- b) Profil (nom, pôles, admin, avatar) ---
|
||||
const patch: Record<string, unknown> = {}
|
||||
if (full_name !== undefined) patch.full_name = full_name
|
||||
if (is_admin !== undefined) patch.is_admin = !!is_admin
|
||||
if (is_admin !== undefined) patch.is_admin = Boolean(is_admin)
|
||||
if (avatar_url !== undefined) patch.avatar_url = avatar_url || null
|
||||
// Gestion des pôles : roles = liste complète, role = principal (roles[0]).
|
||||
if (Array.isArray(roles)) {
|
||||
const primary = roles[0] ?? 'other'
|
||||
patch.roles = roles.length ? roles : [primary]
|
||||
patch.role = primary
|
||||
} else if (role !== undefined) {
|
||||
patch.role = role
|
||||
patch.roles = [role]
|
||||
}
|
||||
if (roles !== undefined) patch.roles = cleanRoles(roles)
|
||||
|
||||
if (Object.keys(patch).length > 0) {
|
||||
const { error } = await admin.from('profiles').update(patch).eq('id', id)
|
||||
if (error) return json({ error: error.message }, 400)
|
||||
}
|
||||
|
||||
if (Object.keys(authPatch).length === 0 && Object.keys(patch).length === 0)
|
||||
if (Object.keys(authPatch).length === 0 && Object.keys(patch).length === 0) {
|
||||
return json({ error: 'Rien à modifier.' }, 400)
|
||||
}
|
||||
return json({ ok: true })
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Migration v10 « Refonte »
|
||||
-- À coller dans : Supabase > SQL Editor > New query > Run
|
||||
-- Idempotent : sans danger si déjà appliquée en tout ou partie.
|
||||
--
|
||||
-- Ce que fait cette migration :
|
||||
-- 0. Colonnes « tableau » manquantes (dont tasks.tags) et non-NULL
|
||||
-- 1. Commentaires sur les tâches et les bugs
|
||||
-- 2. Pièces jointes (captures d'écran) sur les tâches et les bugs
|
||||
-- 3. Parité tâches/bugs : plusieurs pôles par bug
|
||||
-- 4. Suppression des colonnes « de compatibilité » devenues des pièges
|
||||
-- (profiles.role, tasks.pole, bugs.assignee_id)
|
||||
-- 5. Réordonnancement du Kanban en UNE requête transactionnelle
|
||||
-- 6. Suppression d'un projet réservée à son créateur ou à un admin
|
||||
-- 7. Temps réel sur les projets
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 0) Colonnes « tableau » garanties — À EXÉCUTER EN PREMIER
|
||||
--
|
||||
-- `tasks.tags` n'existait que dans schema.sql (installations neuves) et
|
||||
-- n'a jamais été ajoutée par les migrations : sur une base mise à jour
|
||||
-- pas à pas, la colonne manquait et l'application plantait en tentant
|
||||
-- d'afficher les étiquettes d'une tâche.
|
||||
--
|
||||
-- Tant qu'on y est, on ré-affirme l'invariant sur TOUTES les colonnes
|
||||
-- tableau : jamais NULL, toujours un tableau vide par défaut.
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.tasks add column if not exists tags text[] not null default '{}';
|
||||
alter table public.tasks add column if not exists poles member_role[] not null default '{}';
|
||||
alter table public.tasks add column if not exists assignee_ids uuid[] not null default '{}';
|
||||
alter table public.tasks add column if not exists checklist jsonb not null default '[]'::jsonb;
|
||||
alter table public.bugs add column if not exists assignee_ids uuid[] not null default '{}';
|
||||
alter table public.profiles add column if not exists roles member_role[] not null default '{}';
|
||||
|
||||
-- Remet à vide les valeurs NULL héritées, puis verrouille la contrainte.
|
||||
do $$
|
||||
declare
|
||||
r record;
|
||||
begin
|
||||
for r in
|
||||
select * from (values
|
||||
('tasks', 'tags', '''{}''::text[]'),
|
||||
('tasks', 'poles', '''{}''::member_role[]'),
|
||||
('tasks', 'assignee_ids', '''{}''::uuid[]'),
|
||||
('tasks', 'checklist', '''[]''::jsonb'),
|
||||
('bugs', 'poles', '''{}''::member_role[]'),
|
||||
('bugs', 'assignee_ids', '''{}''::uuid[]'),
|
||||
('profiles', 'roles', '''{}''::member_role[]')
|
||||
) as t(tbl, col, empty)
|
||||
loop
|
||||
if exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = r.tbl and column_name = r.col
|
||||
) then
|
||||
execute format('update public.%I set %I = %s where %I is null', r.tbl, r.col, r.empty, r.col);
|
||||
execute format('alter table public.%I alter column %I set default %s', r.tbl, r.col, r.empty);
|
||||
execute format('alter table public.%I alter column %I set not null', r.tbl, r.col);
|
||||
end if;
|
||||
end loop;
|
||||
end $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 1) Parité bugs / tâches : plusieurs pôles par bug
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.bugs
|
||||
add column if not exists poles member_role[] not null default '{}';
|
||||
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'bugs' and column_name = 'pole') then
|
||||
update public.bugs set poles = array[pole] where pole is not null and poles = '{}';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 2) Commentaires (fil de discussion sur une tâche ou un bug)
|
||||
-- ---------------------------------------------------------------------
|
||||
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()
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
alter table public.comments enable row level security;
|
||||
|
||||
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;
|
||||
|
||||
-- Toute l'équipe lit la discussion ; on n'écrit qu'en son propre nom ;
|
||||
-- on ne modifie/supprime que ses propres messages (ou n'importe lequel si admin).
|
||||
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)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 3) Pièces jointes (captures d'écran, logs, fichiers)
|
||||
-- ---------------------------------------------------------------------
|
||||
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, -- chemin dans le bucket "attachments"
|
||||
size bigint not null default 0,
|
||||
mime text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
alter table public.attachments enable row level security;
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
-- Bucket de stockage des fichiers joints.
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('attachments', 'attachments', true)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
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');
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 4) Notifications : sur QUOI pointe la notification
|
||||
-- (une notif de commentaire ouvre la tâche ou le bug commenté)
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.notifications
|
||||
add column if not exists entity_kind text not null default 'task';
|
||||
|
||||
update public.notifications
|
||||
set entity_kind = kind
|
||||
where kind in ('task', 'bug') and entity_kind is distinct from kind;
|
||||
|
||||
create index if not exists idx_notifications_user_unread
|
||||
on public.notifications(user_id, read, created_at desc);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 5) Réordonnancement du Kanban en UNE requête
|
||||
-- Avant : une requête UPDATE par carte de la colonne (jusqu'à 30
|
||||
-- allers-retours non transactionnels — un échec corrompait l'ordre).
|
||||
-- ---------------------------------------------------------------------
|
||||
create or replace function public.reorder_tasks(p_status task_status, p_ids uuid[])
|
||||
returns void
|
||||
language plpgsql
|
||||
security invoker -- la RLS de l'appelant s'applique normalement
|
||||
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;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 6) Suppression d'un projet : créateur ou administrateur seulement
|
||||
-- (avant : n'importe quel membre pouvait effacer un projet entier
|
||||
-- et tout son contenu en cascade)
|
||||
-- ---------------------------------------------------------------------
|
||||
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)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 7) Temps réel : projets, commentaires, pièces jointes
|
||||
-- ---------------------------------------------------------------------
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.projects;
|
||||
exception when duplicate_object then null; end $$;
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.comments;
|
||||
exception when duplicate_object then null; end $$;
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.attachments;
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 8) Création de profil à l'inscription : couleur d'avatar variée
|
||||
-- (avant, tout le monde héritait du même violet #7c5cff)
|
||||
-- ---------------------------------------------------------------------
|
||||
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 $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 9) Suppression des colonnes « de compatibilité »
|
||||
-- Elles doublonnaient les tableaux et devaient être re-synchronisées
|
||||
-- à la main dans six endroits du code. On sauvegarde d'abord, puis
|
||||
-- on supprime. À exécuter EN DERNIER (la fonction du point 8 ne les
|
||||
-- utilise plus).
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
-- a) profiles.role → profiles.roles[1]
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'profiles' and column_name = 'role') then
|
||||
update public.profiles set roles = array[role] where roles = '{}' and role is not null;
|
||||
alter table public.profiles drop column role;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- b) tasks.pole → tasks.poles[1]
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'tasks' and column_name = 'pole') then
|
||||
update public.tasks set poles = array[pole] where pole is not null and poles = '{}';
|
||||
alter table public.tasks drop column pole;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- c) bugs.assignee_id → bugs.assignee_ids[1] · bugs.pole → bugs.poles[1]
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'bugs' and column_name = 'assignee_id') then
|
||||
update public.bugs set assignee_ids = array[assignee_id]
|
||||
where assignee_id is not null and assignee_ids = '{}';
|
||||
alter table public.bugs drop column assignee_id;
|
||||
end if;
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'bugs' and column_name = 'pole') then
|
||||
alter table public.bugs drop column pole;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- d) bugs.priority : plus exposée dans l'interface (la sévérité suffit).
|
||||
-- La colonne reste en base, avec une valeur par défaut, pour ne rien
|
||||
-- perdre — mais plus personne ne la remplit.
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'bugs' and column_name = 'priority') then
|
||||
alter table public.bugs alter column priority set default 'medium';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 10) Index manquants sur les colonnes les plus filtrées
|
||||
-- ---------------------------------------------------------------------
|
||||
create index if not exists idx_tasks_project_status on public.tasks(project_id, status, position);
|
||||
create index if not exists idx_bugs_project_status on public.bugs(project_id, status);
|
||||
create index if not exists idx_time_logs_date on public.time_logs(project_id, logged_at);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 11) Temps passé sur un BUG (parité avec les tâches)
|
||||
-- Une entrée de temps porte sur une tâche, OU sur un bug, OU sur
|
||||
-- rien (activité libre : réunion, veille…). Jamais les deux.
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.time_logs
|
||||
add column if not exists bug_id uuid references public.bugs(id) on delete cascade;
|
||||
|
||||
create index if not exists idx_time_logs_bug on public.time_logs(bug_id);
|
||||
|
||||
do $$ begin
|
||||
alter table public.time_logs
|
||||
add constraint time_logs_one_target
|
||||
check (not (task_id is not null and bug_id is not null));
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 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';
|
||||
@@ -0,0 +1,46 @@
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Migration v11 « Documentation »
|
||||
-- À coller dans : Supabase > SQL Editor > New query > Run
|
||||
-- Idempotent : sans danger si déjà appliquée.
|
||||
--
|
||||
-- Ajoute les fiches de documentation : GDD, conventions de code, notes
|
||||
-- de réunion, procédures… Créées dans l'app ou importées depuis un
|
||||
-- fichier Markdown / Word.
|
||||
-- =====================================================================
|
||||
|
||||
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,
|
||||
-- Contenu en Markdown : c'est le format pivot. Un .docx importé est
|
||||
-- converti en Markdown à l'import, ce qui garde les fiches lisibles,
|
||||
-- cherchables et modifiables dans l'application.
|
||||
content text not null default '',
|
||||
tags text[] not null default '{}',
|
||||
-- Fichier d'origine, quand la fiche vient d'un import (informatif).
|
||||
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()
|
||||
);
|
||||
|
||||
create index if not exists idx_documents_project on public.documents(project_id, updated_at desc);
|
||||
|
||||
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();
|
||||
|
||||
alter table public.documents enable row level security;
|
||||
|
||||
drop policy if exists documents_all on public.documents;
|
||||
|
||||
-- Équipe de confiance : tout membre connecté lit et écrit la documentation.
|
||||
create policy documents_all on public.documents
|
||||
for all to authenticated using (true) with check (true);
|
||||
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.documents;
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
notify pgrst, 'reload schema';
|
||||
@@ -1,70 +0,0 @@
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Migration v2
|
||||
-- À exécuter UNE FOIS dans : Supabase > SQL Editor > New query
|
||||
-- (pour une base déjà créée avec schema.sql v1)
|
||||
--
|
||||
-- Contenu :
|
||||
-- • Suppression des JALONS (milestones)
|
||||
-- • Notion de PÔLE sur les tâches et les bugs
|
||||
-- • Plusieurs personnes assignables sur une tâche (assignee_ids)
|
||||
-- • Système de NOTIFICATIONS (assignation à un pôle)
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------- Pôle sur tâches & bugs (réutilise l'enum member_role) -----
|
||||
alter table public.tasks add column if not exists pole member_role;
|
||||
alter table public.bugs add column if not exists pole member_role;
|
||||
|
||||
-- ---------- Plusieurs assignés par tâche -----------------------------
|
||||
alter table public.tasks
|
||||
add column if not exists assignee_ids uuid[] not null default '{}';
|
||||
|
||||
-- Reprend l'assigné unique existant dans le nouveau tableau.
|
||||
update public.tasks
|
||||
set assignee_ids = array[assignee_id]
|
||||
where assignee_id is not null
|
||||
and (assignee_ids is null or assignee_ids = '{}');
|
||||
|
||||
alter table public.tasks drop column if exists assignee_id;
|
||||
|
||||
-- ---------- Suppression des jalons -----------------------------------
|
||||
alter table public.tasks drop column if exists milestone_id;
|
||||
drop table if exists public.milestones cascade;
|
||||
drop type if exists milestone_status;
|
||||
|
||||
-- ---------- 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'
|
||||
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 $$;
|
||||
@@ -1,25 +0,0 @@
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Migration v3
|
||||
-- À exécuter UNE FOIS dans : Supabase > SQL Editor > New query
|
||||
-- (après migration_v2.sql)
|
||||
--
|
||||
-- Contenu :
|
||||
-- • Temps « libre » dans la feuille de temps (réunion, etc.) :
|
||||
-- une entrée peut ne pas être liée à une tâche du Kanban.
|
||||
-- =====================================================================
|
||||
|
||||
-- La tâche devient optionnelle ; un libellé libre la remplace si absente.
|
||||
alter table public.time_logs alter column task_id drop not null;
|
||||
alter table public.time_logs add column if not exists label text;
|
||||
|
||||
-- Rattachement direct au projet (pour retrouver aussi les entrées sans tâche).
|
||||
alter table public.time_logs
|
||||
add column if not exists project_id uuid references public.projects(id) on delete cascade;
|
||||
|
||||
-- Backfill : les entrées existantes héritent du projet de leur tâche.
|
||||
update public.time_logs tl
|
||||
set project_id = t.project_id
|
||||
from public.tasks t
|
||||
where tl.task_id = t.id and tl.project_id is null;
|
||||
|
||||
create index if not exists idx_time_logs_project on public.time_logs(project_id);
|
||||
@@ -1,18 +0,0 @@
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Migration v4
|
||||
-- À exécuter UNE FOIS dans : Supabase > SQL Editor > New query
|
||||
-- (après migration_v3.sql)
|
||||
--
|
||||
-- Contenu :
|
||||
-- • Un membre peut appartenir à PLUSIEURS pôles (rôles).
|
||||
-- profiles.role reste le rôle « principal » ; profiles.roles
|
||||
-- contient l'ensemble des pôles.
|
||||
-- =====================================================================
|
||||
|
||||
alter table public.profiles
|
||||
add column if not exists roles member_role[] not null default '{}';
|
||||
|
||||
-- Backfill : les profils existants héritent de leur rôle unique actuel.
|
||||
update public.profiles
|
||||
set roles = array[role]
|
||||
where roles = '{}';
|
||||
@@ -0,0 +1,29 @@
|
||||
-- =====================================================================
|
||||
-- Bucket « releases » — hébergement des mises à jour de l'application
|
||||
-- À exécuter UNE SEULE FOIS : Supabase > SQL Editor > New query > Run
|
||||
--
|
||||
-- Ce bucket contient l'installeur signé et le fichier « latest.json »
|
||||
-- que chaque application interroge au lancement.
|
||||
--
|
||||
-- Lecture PUBLIQUE : les collègues doivent pouvoir télécharger la mise
|
||||
-- à jour avant même d'être connectés. Aucun risque : le paquet est
|
||||
-- signé, et une mise à jour non signée par la clé privée du projet est
|
||||
-- purement et simplement refusée par Tauri.
|
||||
--
|
||||
-- Écriture : réservée au script « npm run release », qui s'authentifie
|
||||
-- avec la clé service_role depuis la machine de l'administrateur.
|
||||
-- =====================================================================
|
||||
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('releases', 'releases', true)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
drop policy if exists releases_read on storage.objects;
|
||||
|
||||
create policy releases_read on storage.objects
|
||||
for select using (bucket_id = 'releases');
|
||||
|
||||
-- Personne d'autre que service_role n'écrit ici : aucune policy d'insert
|
||||
-- n'est créée volontairement (service_role contourne la RLS).
|
||||
|
||||
notify pgrst, 'reload schema';
|
||||
+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';
|
||||
|
||||
+563
-156
@@ -1,156 +1,563 @@
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Mise à jour complète (v2 + v3 + v4)
|
||||
-- À coller dans : Supabase > SQL Editor > New query > Run
|
||||
-- Sans danger si une partie a déjà été appliquée (tout est idempotent).
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v2 — Pôles, assignation multiple, notifications, suppression jalons
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.tasks add column if not exists pole member_role;
|
||||
alter table public.bugs add column if not exists pole member_role;
|
||||
|
||||
alter table public.tasks
|
||||
add column if not exists assignee_ids uuid[] not null default '{}';
|
||||
|
||||
-- Reprend l'assigné unique existant dans le tableau (si la colonne existe encore).
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='tasks' and column_name='assignee_id') then
|
||||
update public.tasks
|
||||
set assignee_ids = array[assignee_id]
|
||||
where assignee_id is not null and (assignee_ids is null or assignee_ids = '{}');
|
||||
alter table public.tasks drop column assignee_id;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
alter table public.tasks drop column if exists milestone_id;
|
||||
drop table if exists public.milestones cascade;
|
||||
drop type if exists milestone_status;
|
||||
|
||||
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,
|
||||
title text not null,
|
||||
body text,
|
||||
entity_id uuid,
|
||||
pole member_role,
|
||||
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;
|
||||
|
||||
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 $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v3 — Feuille de temps : entrées libres (réunion, etc.)
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Crée la table si elle n'a jamais existé (ancienne base sans suivi du temps).
|
||||
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()
|
||||
);
|
||||
|
||||
-- Si la table existait déjà (ancienne version), on l'aligne sur le nouveau format.
|
||||
alter table public.time_logs alter column task_id drop not null;
|
||||
alter table public.time_logs add column if not exists label text;
|
||||
alter table public.time_logs
|
||||
add column if not exists project_id uuid references public.projects(id) on delete cascade;
|
||||
|
||||
update public.time_logs tl
|
||||
set project_id = t.project_id
|
||||
from public.tasks t
|
||||
where tl.task_id = t.id and tl.project_id is null;
|
||||
|
||||
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 $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v4 — Plusieurs pôles (rôles) par membre
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.profiles
|
||||
add column if not exists roles member_role[] not null default '{}';
|
||||
|
||||
update public.profiles
|
||||
set roles = array[role]
|
||||
where roles = '{}';
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v5 — Accueil (onboarding) des nouveaux membres
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.profiles
|
||||
add column if not exists onboarded boolean not null default false;
|
||||
|
||||
-- Les membres déjà présents ne sont pas ré-embarqués : on les marque « fait ».
|
||||
update public.profiles set onboarded = true where onboarded = false;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v6 — Photo de profil (avatar image) : colonne + bucket de stockage
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.profiles add column if not exists avatar_url text;
|
||||
|
||||
-- Bucket public "avatars" pour héberger les photos de profil.
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('avatars', 'avatars', true)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- Lecture publique ; écriture réservée aux membres connectés.
|
||||
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');
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v7 — Plusieurs pôles par tâche
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.tasks
|
||||
add column if not exists poles member_role[] not null default '{}';
|
||||
|
||||
-- Reprend le pôle unique existant dans le tableau (si présent).
|
||||
update public.tasks
|
||||
set poles = array[pole]
|
||||
where pole is not null and poles = '{}';
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Mise à jour complète (v2 + v3 + v4)
|
||||
-- À coller dans : Supabase > SQL Editor > New query > Run
|
||||
-- Sans danger si une partie a déjà été appliquée (tout est idempotent).
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v2 — Pôles, assignation multiple, notifications, suppression jalons
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.tasks add column if not exists pole member_role;
|
||||
alter table public.bugs add column if not exists pole member_role;
|
||||
|
||||
alter table public.tasks
|
||||
add column if not exists assignee_ids uuid[] not null default '{}';
|
||||
|
||||
-- Reprend l'assigné unique existant dans le tableau (si la colonne existe encore).
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='tasks' and column_name='assignee_id') then
|
||||
update public.tasks
|
||||
set assignee_ids = array[assignee_id]
|
||||
where assignee_id is not null and (assignee_ids is null or assignee_ids = '{}');
|
||||
alter table public.tasks drop column assignee_id;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
alter table public.tasks drop column if exists milestone_id;
|
||||
drop table if exists public.milestones cascade;
|
||||
drop type if exists milestone_status;
|
||||
|
||||
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,
|
||||
title text not null,
|
||||
body text,
|
||||
entity_id uuid,
|
||||
pole member_role,
|
||||
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;
|
||||
|
||||
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 $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v3 — Feuille de temps : entrées libres (réunion, etc.)
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Crée la table si elle n'a jamais existé (ancienne base sans suivi du temps).
|
||||
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()
|
||||
);
|
||||
|
||||
-- Si la table existait déjà (ancienne version), on l'aligne sur le nouveau format.
|
||||
alter table public.time_logs alter column task_id drop not null;
|
||||
alter table public.time_logs add column if not exists label text;
|
||||
alter table public.time_logs
|
||||
add column if not exists project_id uuid references public.projects(id) on delete cascade;
|
||||
|
||||
update public.time_logs tl
|
||||
set project_id = t.project_id
|
||||
from public.tasks t
|
||||
where tl.task_id = t.id and tl.project_id is null;
|
||||
|
||||
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 $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v4 — Plusieurs pôles (rôles) par membre
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.profiles
|
||||
add column if not exists roles member_role[] not null default '{}';
|
||||
|
||||
update public.profiles
|
||||
set roles = array[role]
|
||||
where roles = '{}';
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v5 — Accueil (onboarding) des nouveaux membres
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.profiles
|
||||
add column if not exists onboarded boolean not null default false;
|
||||
|
||||
-- Les membres déjà présents ne sont pas ré-embarqués : on les marque « fait ».
|
||||
update public.profiles set onboarded = true where onboarded = false;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v6 — Photo de profil (avatar image) : colonne + bucket de stockage
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.profiles add column if not exists avatar_url text;
|
||||
|
||||
-- Bucket public "avatars" pour héberger les photos de profil.
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('avatars', 'avatars', true)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
-- Lecture publique ; écriture réservée aux membres connectés.
|
||||
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');
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v7 — Plusieurs pôles par tâche
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.tasks
|
||||
add column if not exists poles member_role[] not null default '{}';
|
||||
|
||||
-- Reprend le pôle unique existant dans le tableau (si présent).
|
||||
update public.tasks
|
||||
set poles = array[pole]
|
||||
where pole is not null and poles = '{}';
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v8 — Checklists (cases à cocher) sur les tâches
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Liste d'éléments [{ "id": "...", "text": "...", "done": false }, ...]
|
||||
alter table public.tasks
|
||||
add column if not exists checklist jsonb not null default '[]'::jsonb;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- v9 — Assignation MULTIPLE des bugs (parité avec les tâches)
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.bugs
|
||||
add column if not exists assignee_ids uuid[] not null default '{}';
|
||||
|
||||
-- Reprend l'assigné unique existant dans le tableau (si présent).
|
||||
update public.bugs
|
||||
set assignee_ids = array[assignee_id]
|
||||
where assignee_id is not null and assignee_ids = '{}';
|
||||
|
||||
|
||||
-- =====================================================================
|
||||
-- GameDev Tracker — Migration v10 « Refonte »
|
||||
-- À coller dans : Supabase > SQL Editor > New query > Run
|
||||
-- Idempotent : sans danger si déjà appliquée en tout ou partie.
|
||||
--
|
||||
-- Ce que fait cette migration :
|
||||
-- 0. Colonnes « tableau » manquantes (dont tasks.tags) et non-NULL
|
||||
-- 1. Commentaires sur les tâches et les bugs
|
||||
-- 2. Pièces jointes (captures d'écran) sur les tâches et les bugs
|
||||
-- 3. Parité tâches/bugs : plusieurs pôles par bug
|
||||
-- 4. Suppression des colonnes « de compatibilité » devenues des pièges
|
||||
-- (profiles.role, tasks.pole, bugs.assignee_id)
|
||||
-- 5. Réordonnancement du Kanban en UNE requête transactionnelle
|
||||
-- 6. Suppression d'un projet réservée à son créateur ou à un admin
|
||||
-- 7. Temps réel sur les projets
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 0) Colonnes « tableau » garanties — À EXÉCUTER EN PREMIER
|
||||
--
|
||||
-- `tasks.tags` n'existait que dans schema.sql (installations neuves) et
|
||||
-- n'a jamais été ajoutée par les migrations : sur une base mise à jour
|
||||
-- pas à pas, la colonne manquait et l'application plantait en tentant
|
||||
-- d'afficher les étiquettes d'une tâche.
|
||||
--
|
||||
-- Tant qu'on y est, on ré-affirme l'invariant sur TOUTES les colonnes
|
||||
-- tableau : jamais NULL, toujours un tableau vide par défaut.
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.tasks add column if not exists tags text[] not null default '{}';
|
||||
alter table public.tasks add column if not exists poles member_role[] not null default '{}';
|
||||
alter table public.tasks add column if not exists assignee_ids uuid[] not null default '{}';
|
||||
alter table public.tasks add column if not exists checklist jsonb not null default '[]'::jsonb;
|
||||
alter table public.bugs add column if not exists assignee_ids uuid[] not null default '{}';
|
||||
alter table public.profiles add column if not exists roles member_role[] not null default '{}';
|
||||
|
||||
-- Remet à vide les valeurs NULL héritées, puis verrouille la contrainte.
|
||||
do $$
|
||||
declare
|
||||
r record;
|
||||
begin
|
||||
for r in
|
||||
select * from (values
|
||||
('tasks', 'tags', '''{}''::text[]'),
|
||||
('tasks', 'poles', '''{}''::member_role[]'),
|
||||
('tasks', 'assignee_ids', '''{}''::uuid[]'),
|
||||
('tasks', 'checklist', '''[]''::jsonb'),
|
||||
('bugs', 'poles', '''{}''::member_role[]'),
|
||||
('bugs', 'assignee_ids', '''{}''::uuid[]'),
|
||||
('profiles', 'roles', '''{}''::member_role[]')
|
||||
) as t(tbl, col, empty)
|
||||
loop
|
||||
if exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = r.tbl and column_name = r.col
|
||||
) then
|
||||
execute format('update public.%I set %I = %s where %I is null', r.tbl, r.col, r.empty, r.col);
|
||||
execute format('alter table public.%I alter column %I set default %s', r.tbl, r.col, r.empty);
|
||||
execute format('alter table public.%I alter column %I set not null', r.tbl, r.col);
|
||||
end if;
|
||||
end loop;
|
||||
end $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 1) Parité bugs / tâches : plusieurs pôles par bug
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.bugs
|
||||
add column if not exists poles member_role[] not null default '{}';
|
||||
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'bugs' and column_name = 'pole') then
|
||||
update public.bugs set poles = array[pole] where pole is not null and poles = '{}';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 2) Commentaires (fil de discussion sur une tâche ou un bug)
|
||||
-- ---------------------------------------------------------------------
|
||||
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()
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
alter table public.comments enable row level security;
|
||||
|
||||
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;
|
||||
|
||||
-- Toute l'équipe lit la discussion ; on n'écrit qu'en son propre nom ;
|
||||
-- on ne modifie/supprime que ses propres messages (ou n'importe lequel si admin).
|
||||
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)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 3) Pièces jointes (captures d'écran, logs, fichiers)
|
||||
-- ---------------------------------------------------------------------
|
||||
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, -- chemin dans le bucket "attachments"
|
||||
size bigint not null default 0,
|
||||
mime text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
alter table public.attachments enable row level security;
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
-- Bucket de stockage des fichiers joints.
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('attachments', 'attachments', true)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
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');
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 4) Notifications : sur QUOI pointe la notification
|
||||
-- (une notif de commentaire ouvre la tâche ou le bug commenté)
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.notifications
|
||||
add column if not exists entity_kind text not null default 'task';
|
||||
|
||||
update public.notifications
|
||||
set entity_kind = kind
|
||||
where kind in ('task', 'bug') and entity_kind is distinct from kind;
|
||||
|
||||
create index if not exists idx_notifications_user_unread
|
||||
on public.notifications(user_id, read, created_at desc);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 5) Réordonnancement du Kanban en UNE requête
|
||||
-- Avant : une requête UPDATE par carte de la colonne (jusqu'à 30
|
||||
-- allers-retours non transactionnels — un échec corrompait l'ordre).
|
||||
-- ---------------------------------------------------------------------
|
||||
create or replace function public.reorder_tasks(p_status task_status, p_ids uuid[])
|
||||
returns void
|
||||
language plpgsql
|
||||
security invoker -- la RLS de l'appelant s'applique normalement
|
||||
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;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 6) Suppression d'un projet : créateur ou administrateur seulement
|
||||
-- (avant : n'importe quel membre pouvait effacer un projet entier
|
||||
-- et tout son contenu en cascade)
|
||||
-- ---------------------------------------------------------------------
|
||||
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)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 7) Temps réel : projets, commentaires, pièces jointes
|
||||
-- ---------------------------------------------------------------------
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.projects;
|
||||
exception when duplicate_object then null; end $$;
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.comments;
|
||||
exception when duplicate_object then null; end $$;
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.attachments;
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 8) Création de profil à l'inscription : couleur d'avatar variée
|
||||
-- (avant, tout le monde héritait du même violet #7c5cff)
|
||||
-- ---------------------------------------------------------------------
|
||||
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 $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 9) Suppression des colonnes « de compatibilité »
|
||||
-- Elles doublonnaient les tableaux et devaient être re-synchronisées
|
||||
-- à la main dans six endroits du code. On sauvegarde d'abord, puis
|
||||
-- on supprime. À exécuter EN DERNIER (la fonction du point 8 ne les
|
||||
-- utilise plus).
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
-- a) profiles.role → profiles.roles[1]
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'profiles' and column_name = 'role') then
|
||||
update public.profiles set roles = array[role] where roles = '{}' and role is not null;
|
||||
alter table public.profiles drop column role;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- b) tasks.pole → tasks.poles[1]
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'tasks' and column_name = 'pole') then
|
||||
update public.tasks set poles = array[pole] where pole is not null and poles = '{}';
|
||||
alter table public.tasks drop column pole;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- c) bugs.assignee_id → bugs.assignee_ids[1] · bugs.pole → bugs.poles[1]
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'bugs' and column_name = 'assignee_id') then
|
||||
update public.bugs set assignee_ids = array[assignee_id]
|
||||
where assignee_id is not null and assignee_ids = '{}';
|
||||
alter table public.bugs drop column assignee_id;
|
||||
end if;
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'bugs' and column_name = 'pole') then
|
||||
alter table public.bugs drop column pole;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- d) bugs.priority : plus exposée dans l'interface (la sévérité suffit).
|
||||
-- La colonne reste en base, avec une valeur par défaut, pour ne rien
|
||||
-- perdre — mais plus personne ne la remplit.
|
||||
do $$ begin
|
||||
if exists (select 1 from information_schema.columns
|
||||
where table_schema = 'public' and table_name = 'bugs' and column_name = 'priority') then
|
||||
alter table public.bugs alter column priority set default 'medium';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 10) Index manquants sur les colonnes les plus filtrées
|
||||
-- ---------------------------------------------------------------------
|
||||
create index if not exists idx_tasks_project_status on public.tasks(project_id, status, position);
|
||||
create index if not exists idx_bugs_project_status on public.bugs(project_id, status);
|
||||
create index if not exists idx_time_logs_date on public.time_logs(project_id, logged_at);
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 11) Temps passé sur un BUG (parité avec les tâches)
|
||||
-- Une entrée de temps porte sur une tâche, OU sur un bug, OU sur
|
||||
-- rien (activité libre : réunion, veille…). Jamais les deux.
|
||||
-- ---------------------------------------------------------------------
|
||||
alter table public.time_logs
|
||||
add column if not exists bug_id uuid references public.bugs(id) on delete cascade;
|
||||
|
||||
create index if not exists idx_time_logs_bug on public.time_logs(bug_id);
|
||||
|
||||
do $$ begin
|
||||
alter table public.time_logs
|
||||
add constraint time_logs_one_target
|
||||
check (not (task_id is not null and bug_id is not null));
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 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';
|
||||
|
||||
|
||||
-- =====================================================================
|
||||
-- v11 — Documentation (fiches créées ou importées)
|
||||
-- =====================================================================
|
||||
|
||||
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,
|
||||
-- Contenu en Markdown : c'est le format pivot. Un .docx importé est
|
||||
-- converti en Markdown à l'import, ce qui garde les fiches lisibles,
|
||||
-- cherchables et modifiables dans l'application.
|
||||
content text not null default '',
|
||||
tags text[] not null default '{}',
|
||||
-- Fichier d'origine, quand la fiche vient d'un import (informatif).
|
||||
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()
|
||||
);
|
||||
|
||||
create index if not exists idx_documents_project on public.documents(project_id, updated_at desc);
|
||||
|
||||
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();
|
||||
|
||||
alter table public.documents enable row level security;
|
||||
|
||||
drop policy if exists documents_all on public.documents;
|
||||
|
||||
-- Équipe de confiance : tout membre connecté lit et écrit la documentation.
|
||||
create policy documents_all on public.documents
|
||||
for all to authenticated using (true) with check (true);
|
||||
|
||||
do $$ begin
|
||||
alter publication supabase_realtime add table public.documents;
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
notify pgrst, 'reload schema';
|
||||
|
||||
Reference in New Issue
Block a user