feat: ShootTracker SQLite+JWT+YOLOv8
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
-- ============================================================
|
||||
-- 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]);
|
||||
Reference in New Issue
Block a user