Files
2026-04-30 22:44:27 +02:00

247 lines
12 KiB
PL/PgSQL

-- ============================================================
-- ShootTracker — Schéma Supabase complet
-- Migration 001 — Schéma initial
-- ============================================================
-- Extension UUID
create extension if not exists "uuid-ossp";
-- ─────────────────────────────────────────────────────────────
-- TABLES
-- ─────────────────────────────────────────────────────────────
-- Profils utilisateurs (étend auth.users)
create table if not exists public.profiles (
id uuid references auth.users on delete cascade primary key,
first_name text,
last_name text,
club text,
disciplines text[] default '{}',
avatar_url text,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Armes (coffre virtuel)
create table if not exists public.weapons (
id uuid default gen_random_uuid() primary key,
user_id uuid references auth.users on delete cascade not null,
name text not null,
nickname text,
type text not null check (type in ('pistolet','carabine','fusil','arc','arbalète','autre')),
caliber text,
brand text,
model text,
serial_number text,
photo_url text,
notes text,
is_archived boolean default false,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Séances d'entraînement
create table if not exists public.training_sessions (
id uuid default gen_random_uuid() primary key,
user_id uuid references auth.users on delete cascade not null,
weapon_id uuid references public.weapons on delete set null,
session_date timestamptz not null default now(),
location text,
target_type text not null check (target_type in ('issf','silhouette','libre')),
distance_m integer,
notes_start text,
notes_end text,
total_shots_declared integer default 0,
total_shots_detected integer default 0,
total_score integer default 0,
avg_score real default 0,
best_series_score integer default 0,
ai_detection_rate real default 0,
avg_dispersion real,
is_completed boolean default false,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Séries de tirs (dans une séance)
create table if not exists public.series (
id uuid default gen_random_uuid() primary key,
session_id uuid references public.training_sessions on delete cascade not null,
user_id uuid references auth.users on delete cascade not null,
series_number integer not null,
shots_declared integer not null default 0,
shots_detected integer default 0,
shots_manual integer default 0,
photo_url text,
annotated_photo_url text,
score_zone integer default 0,
score_groupement integer default 0,
score_total integer default 0,
dispersion_radius real,
center_x real,
center_y real,
ai_data jsonb,
created_at timestamptz default now()
);
-- Impacts individuels
create table if not exists public.impacts (
id uuid default gen_random_uuid() primary key,
series_id uuid references public.series on delete cascade not null,
user_id uuid references auth.users on delete cascade not null,
x real not null,
y real not null,
zone integer,
points integer default 0,
is_manual boolean default false,
created_at timestamptz default now()
);
-- Invitations
create table if not exists public.invitations (
id uuid default gen_random_uuid() primary key,
inviter_id uuid references auth.users on delete cascade not null,
email text,
token text unique not null default encode(gen_random_bytes(32), 'hex'),
is_used boolean default false,
used_by uuid references auth.users on delete set null,
created_at timestamptz default now(),
expires_at timestamptz default (now() + interval '7 days')
);
-- Lieux mémorisés
create table if not exists public.user_locations (
id uuid default gen_random_uuid() primary key,
user_id uuid references auth.users on delete cascade not null,
name text not null,
used_count integer default 1,
last_used_at timestamptz default now(),
unique (user_id, name)
);
-- ─────────────────────────────────────────────────────────────
-- INDEXES
-- ─────────────────────────────────────────────────────────────
create index if not exists weapons_user_id_idx on public.weapons(user_id);
create index if not exists weapons_type_idx on public.weapons(type);
create index if not exists weapons_archived_idx on public.weapons(is_archived);
create index if not exists sessions_user_id_idx on public.training_sessions(user_id);
create index if not exists sessions_weapon_id_idx on public.training_sessions(weapon_id);
create index if not exists sessions_date_idx on public.training_sessions(session_date desc);
create index if not exists sessions_completed_idx on public.training_sessions(is_completed);
create index if not exists series_session_id_idx on public.series(session_id);
create index if not exists series_user_id_idx on public.series(user_id);
create index if not exists impacts_series_id_idx on public.impacts(series_id);
create index if not exists impacts_user_id_idx on public.impacts(user_id);
create index if not exists invitations_token_idx on public.invitations(token);
create index if not exists invitations_inviter_idx on public.invitations(inviter_id);
create index if not exists locations_user_id_idx on public.user_locations(user_id);
-- ─────────────────────────────────────────────────────────────
-- ROW LEVEL SECURITY
-- ─────────────────────────────────────────────────────────────
alter table public.profiles enable row level security;
alter table public.weapons enable row level security;
alter table public.training_sessions enable row level security;
alter table public.series enable row level security;
alter table public.impacts enable row level security;
alter table public.invitations enable row level security;
alter table public.user_locations enable row level security;
-- Profiles
create policy "profiles_own" on public.profiles
for all using (auth.uid() = id);
-- Weapons
create policy "weapons_own" on public.weapons
for all using (auth.uid() = user_id);
-- Training sessions
create policy "sessions_own" on public.training_sessions
for all using (auth.uid() = user_id);
-- Series
create policy "series_own" on public.series
for all using (auth.uid() = user_id);
-- Impacts
create policy "impacts_own" on public.impacts
for all using (auth.uid() = user_id);
-- Invitations — le créateur voit ses invitations ; les non-connectés peuvent vérifier un token
create policy "invitations_own_select" on public.invitations
for select using (auth.uid() = inviter_id or not is_used);
create policy "invitations_own_insert" on public.invitations
for insert with check (auth.uid() = inviter_id);
create policy "invitations_own_update" on public.invitations
for update using (auth.uid() = inviter_id or (auth.uid() is not null and not is_used));
-- User locations
create policy "locations_own" on public.user_locations
for all using (auth.uid() = user_id);
-- ─────────────────────────────────────────────────────────────
-- FUNCTIONS & TRIGGERS
-- ─────────────────────────────────────────────────────────────
-- Crée automatiquement un profil à l'inscription
create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path = public as $$
begin
insert into public.profiles (id, first_name, last_name)
values (
new.id,
coalesce(new.raw_user_meta_data->>'first_name', ''),
coalesce(new.raw_user_meta_data->>'last_name', '')
);
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 procedure public.handle_new_user();
-- Met à jour updated_at automatiquement
create or replace function public.set_updated_at()
returns trigger language plpgsql as $$
begin
new.updated_at = now();
return new;
end;
$$;
create trigger weapons_updated_at before update on public.weapons for each row execute procedure public.set_updated_at();
create trigger sessions_updated_at before update on public.training_sessions for each row execute procedure public.set_updated_at();
create trigger profiles_updated_at before update on public.profiles for each row execute procedure public.set_updated_at();
-- ─────────────────────────────────────────────────────────────
-- STORAGE BUCKETS (à exécuter dans Supabase Dashboard → Storage)
-- ─────────────────────────────────────────────────────────────
-- NB : ces commandes SQL ne fonctionnent pas directement depuis le SQL editor
-- Créer manuellement dans Dashboard → Storage :
-- bucket "avatars" → privé, taille max 5 MB
-- bucket "weapon-photos" → privé, taille max 10 MB
-- bucket "target-photos" → privé, taille max 20 MB (photos haute res)
--
-- Puis appliquer ces policies de storage :
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
values
('avatars', 'avatars', false, 5242880, array['image/jpeg','image/png','image/webp']),
('weapon-photos', 'weapon-photos', false, 10485760, array['image/jpeg','image/png','image/webp']),
('target-photos', 'target-photos', false, 20971520, array['image/jpeg','image/png','image/webp'])
on conflict (id) do nothing;
-- Policies storage : chaque user n'accède qu'à son propre dossier
create policy "avatars_own" on storage.objects
for all using (bucket_id = 'avatars' and auth.uid()::text = (storage.foldername(name))[1]);
create policy "weapon_photos_own" on storage.objects
for all using (bucket_id = 'weapon-photos' and auth.uid()::text = (storage.foldername(name))[1]);
create policy "target_photos_own" on storage.objects
for all using (bucket_id = 'target-photos' and auth.uid()::text = (storage.foldername(name))[1]);