-- ============================================================================ -- FERROVIA PANEL — SCHÉMA SQL COMPLET -- Généré le 2026-08-25 par analyse exhaustive de l'application -- ============================================================================ -- Ce fichier contient TOUT le SQL nécessaire pour recréer la base de données. -- Il est conçu pour être exécuté sur une instance Supabase (self-hosted ou cloud). -- Ordre d'exécution : Extensions → Types → Fonctions → Tables → Séquences → -- Contraintes → Index → Triggers → RLS → Grants -- ============================================================================ -- ============================================================================ -- 1. EXTENSIONS -- ============================================================================ CREATE EXTENSION IF NOT EXISTS "pgcrypto"; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ============================================================================ -- 2. TYPES ENUM -- ============================================================================ DO $$ BEGIN CREATE TYPE "public"."service_type" AS ENUM ('TER','TGV','Intercités','Fret'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE "public"."station_type" AS ENUM ('interurbaine','ville'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE "public"."transport_type" AS ENUM ('bus','tram','metro','tram-train','train'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ============================================================================ -- 3. FONCTIONS UTILITAIRES (triggers) -- ============================================================================ CREATE OR REPLACE FUNCTION "public"."handle_updated_at"() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION "public"."set_updated_at"() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION "public"."update_updated_at_column"() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION "public"."update_travaux_updated_at"() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$; CREATE OR REPLACE FUNCTION "public"."update_utilisateurs_updated_at"() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$; -- ============================================================================ -- 4. FONCTION D'AUDIT -- ============================================================================ CREATE OR REPLACE FUNCTION "public"."log_audit"() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE v_changed_by_text text; v_changed_by uuid; BEGIN v_changed_by_text := current_setting('jwt.claims.user_id', true); IF v_changed_by_text IS NOT NULL AND v_changed_by_text <> '' THEN BEGIN v_changed_by := v_changed_by_text::uuid; EXCEPTION WHEN others THEN v_changed_by := NULL; END; ELSE v_changed_by := NULL; END IF; IF (TG_OP = 'INSERT') THEN INSERT INTO public.audit_logs(table_name, operation, record_id, changed_by, new_data, changed_at) VALUES (TG_TABLE_NAME, 'INSERT', NEW.id, v_changed_by, row_to_json(NEW)::jsonb, now()); RETURN NEW; ELSIF (TG_OP = 'UPDATE') THEN INSERT INTO public.audit_logs(table_name, operation, record_id, changed_by, old_data, new_data, changed_at) VALUES (TG_TABLE_NAME, 'UPDATE', NEW.id, v_changed_by, row_to_json(OLD)::jsonb, row_to_json(NEW)::jsonb, now()); RETURN NEW; ELSIF (TG_OP = 'DELETE') THEN INSERT INTO public.audit_logs(table_name, operation, record_id, changed_by, old_data, changed_at) VALUES (TG_TABLE_NAME, 'DELETE', OLD.id, v_changed_by, row_to_json(OLD)::jsonb, now()); RETURN OLD; END IF; RETURN NULL; END; $$; -- ============================================================================ -- 5. FONCTION STUB GTFS -- ============================================================================ CREATE OR REPLACE FUNCTION "public"."import_gtfs_rpc"(payload json) RETURNS json LANGUAGE plpgsql AS $$ BEGIN RETURN json_build_object('status', 'ok', 'created', 0, 'warnings', json_build_array()); EXCEPTION WHEN OTHERS THEN RETURN json_build_object('status', 'error', 'message', SQLERRM); END; $$; -- ============================================================================ -- 6. TABLES (ordonnées par dépendances) -- ============================================================================ -- ----------------------------------------------- -- 6.01 audit_logs -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."audit_logs" ( "id" bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "table_name" text NOT NULL, "operation" text NOT NULL, "record_id" uuid, "changed_by" uuid, "old_data" jsonb, "new_data" jsonb, "changed_at" timestamptz NOT NULL DEFAULT now() ); -- ----------------------------------------------- -- 6.02 utilisateurs -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."utilisateurs" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "email" varchar(255) NOT NULL UNIQUE, "nom" varchar(100) NOT NULL, "prenom" varchar(100) NOT NULL, "role" varchar(50) NOT NULL DEFAULT 'utilisateur', "actif" boolean NOT NULL DEFAULT true, "password_hash" varchar(255), "photo_url" text, "region_admin" boolean DEFAULT false, "region_id" varchar(100), "region_nom" varchar(255), "permissions" jsonb NOT NULL DEFAULT '{}'::jsonb, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now() ); CREATE UNIQUE INDEX IF NOT EXISTS "utilisateurs_email_lower_idx" ON "public"."utilisateurs" (lower(email::text)); CREATE INDEX IF NOT EXISTS "idx_utilisateurs_email" ON "public"."utilisateurs" ("email"); CREATE INDEX IF NOT EXISTS "idx_utilisateurs_role" ON "public"."utilisateurs" ("role"); CREATE INDEX IF NOT EXISTS "idx_utilisateurs_actif" ON "public"."utilisateurs" ("actif"); CREATE INDEX IF NOT EXISTS "idx_utilisateurs_region_id" ON "public"."utilisateurs" ("region_id"); -- ----------------------------------------------- -- 6.03 gares -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."gares" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "nom" text NOT NULL, "type" varchar(50) NOT NULL DEFAULT 'ville', "services" "public"."service_type"[] NOT NULL DEFAULT '{}'::service_type[], "informations" jsonb DEFAULT '{}'::jsonb, "quais_hall" jsonb DEFAULT '[]'::jsonb, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "deleted_at" timestamptz ); -- ----------------------------------------------- -- 6.04 gare_quais -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."gare_quais" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "gare_id" uuid NOT NULL REFERENCES "public"."gares"("id") ON DELETE CASCADE, "nom" text NOT NULL, "distance_metres" integer NOT NULL DEFAULT 0, "created_at" timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_gare_quais_gare_id" ON "public"."gare_quais" ("gare_id"); -- ----------------------------------------------- -- 6.05 gare_transports -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."gare_transports" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "gare_id" uuid NOT NULL REFERENCES "public"."gares"("id") ON DELETE CASCADE, "type" "public"."transport_type" NOT NULL, "ligne" text, "created_at" timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_gare_transports_gare_id" ON "public"."gare_transports" ("gare_id"); -- ----------------------------------------------- -- 6.06 types_train -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."types_train" ( "id" bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "nom" varchar(255) NOT NULL, "code" varchar(50), "description" text, "logo_url" text, "logo_color_url" text, "couleur" varchar(20) DEFAULT '#0074d9', "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_types_train_code" ON "public"."types_train" ("code"); CREATE INDEX IF NOT EXISTS "idx_types_train_deleted_at" ON "public"."types_train" ("deleted_at"); -- ----------------------------------------------- -- 6.07 materiel-roulant -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."materiel-roulant" ( "id" bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "nom" text, "nom_technique" text, "capacite" integer, "image_url" text, "type_train" text, "numero_serie" text UNIQUE, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz ); -- ----------------------------------------------- -- 6.08 services_annuels -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."services_annuels" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "nom" varchar(255) NOT NULL, "code" varchar(50), "date_debut" date NOT NULL, "date_fin" date NOT NULL, "description" text, "actif" boolean DEFAULT true, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_services_annuels_actif" ON "public"."services_annuels" ("actif") WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS "idx_services_annuels_dates" ON "public"."services_annuels" ("date_debut","date_fin"); -- ----------------------------------------------- -- 6.09 lignes -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."lignes" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "nom" varchar(255) NOT NULL, "code" varchar(50), "gare_depart_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "gare_arrivee_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "actif" boolean DEFAULT true, "fh_url" text, "region" varchar(100), "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_lignes_actif" ON "public"."lignes" ("actif") WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS "idx_lignes_fh_url" ON "public"."lignes" ("fh_url"); -- ----------------------------------------------- -- 6.10 lignes_gares -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."lignes_gares" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE CASCADE, "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE CASCADE, "ordre" integer NOT NULL, "desserte" varchar(20) NOT NULL CHECK (desserte IN ('tous','partielle')), "created_at" timestamptz DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_lignes_gares_ligne" ON "public"."lignes_gares" ("ligne_id"); CREATE INDEX IF NOT EXISTS "idx_lignes_gares_ordre" ON "public"."lignes_gares" ("ligne_id","ordre"); -- ----------------------------------------------- -- 6.11 horaires -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."horaires" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "numero_train" varchar(50) NOT NULL, "type_train" varchar(255), "type_train_id" bigint REFERENCES "public"."types_train"("id") ON DELETE SET NULL, "gare_depart_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "gare_depart_nom" varchar(255), "heure_depart" time, "gare_arrivee_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "gare_arrivee_nom" varchar(255), "heure_arrivee" time, "circule_lundi" boolean DEFAULT true, "circule_mardi" boolean DEFAULT true, "circule_mercredi" boolean DEFAULT true, "circule_jeudi" boolean DEFAULT true, "circule_vendredi" boolean DEFAULT true, "circule_samedi" boolean DEFAULT true, "circule_dimanche" boolean DEFAULT true, "circule_jours_feries" boolean DEFAULT false, "circule_dimanches_feries" boolean DEFAULT false, "jours_personnalises" jsonb DEFAULT '[]'::jsonb, "jours_non_circulation" jsonb DEFAULT '[]'::jsonb, "materiel_roulant_id" bigint REFERENCES "public"."materiel-roulant"("id") ON DELETE SET NULL, "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, "ligne_nom" varchar(255), "service_annuel_id" uuid REFERENCES "public"."services_annuels"("id") ON DELETE SET NULL, "service_annuel_nom" varchar(255), "est_substitution" boolean DEFAULT false, "motif_substitution" varchar(255), "gares_desservies" jsonb DEFAULT '[]'::jsonb, "composition_train" jsonb DEFAULT '[]'::jsonb, "substitution_disponible" boolean DEFAULT false, "actif" boolean DEFAULT true, "region_id" varchar(100), "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_horaires_numero_train" ON "public"."horaires" ("numero_train"); CREATE INDEX IF NOT EXISTS "idx_horaires_gare_depart" ON "public"."horaires" ("gare_depart_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_gare_arrivee" ON "public"."horaires" ("gare_arrivee_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_actif" ON "public"."horaires" ("actif") WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS "idx_horaires_type_train" ON "public"."horaires" ("type_train_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_materiel" ON "public"."horaires" ("materiel_roulant_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_ligne" ON "public"."horaires" ("ligne_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_service_annuel" ON "public"."horaires" ("service_annuel_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_substitution" ON "public"."horaires" ("est_substitution"); CREATE INDEX IF NOT EXISTS "idx_horaires_region_id" ON "public"."horaires" ("region_id"); -- ----------------------------------------------- -- 6.12 horaires_gares_desservies -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."horaires_gares_desservies" ( "id" bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "horaire_id" uuid REFERENCES "public"."horaires"("id") ON DELETE CASCADE, "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "gare_nom" varchar(255) NOT NULL, "heure_arrivee" time, "heure_depart" time, "ordre" integer NOT NULL DEFAULT 0, "quai" varchar(50), "quai_id" uuid REFERENCES "public"."gare_quais"("id"), "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_gares_desservies_horaire" ON "public"."horaires_gares_desservies" ("horaire_id"); CREATE INDEX IF NOT EXISTS "idx_gares_desservies_ordre" ON "public"."horaires_gares_desservies" ("horaire_id","ordre"); CREATE INDEX IF NOT EXISTS "idx_hgd_gare_id" ON "public"."horaires_gares_desservies" ("gare_id"); -- ----------------------------------------------- -- 6.13 horaires_substitutions (utilisé par l'API mais absent du dump) -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."horaires_substitutions" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "horaire_id" uuid REFERENCES "public"."horaires"("id") ON DELETE SET NULL, "region_id" varchar(100), "region" varchar(255), "numero_train" varchar(50), "type_train" varchar(100), "type_train_id" bigint, "gare_depart_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "gare_depart_nom" varchar(255), "heure_depart" time, "gare_arrivee_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "gare_arrivee_nom" varchar(255), "heure_arrivee" time, "date_debut" timestamptz, "date_fin" timestamptz, "circule_lundi" boolean DEFAULT true, "circule_mardi" boolean DEFAULT true, "circule_mercredi" boolean DEFAULT true, "circule_jeudi" boolean DEFAULT true, "circule_vendredi" boolean DEFAULT true, "circule_samedi" boolean DEFAULT true, "circule_dimanche" boolean DEFAULT true, "circule_jours_feries" boolean DEFAULT false, "jours_personnalises" jsonb DEFAULT '[]'::jsonb, "jours_non_circulation" jsonb DEFAULT '[]'::jsonb, "materiel_roulant_id" bigint, "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, "ligne_nom" varchar(255), "service_annuel_id" uuid REFERENCES "public"."services_annuels"("id") ON DELETE SET NULL, "service_annuel_nom" varchar(255), "est_substitution" boolean DEFAULT true, "motif_substitution" varchar(255), "gares_desservies" jsonb DEFAULT '[]'::jsonb, "composition_train" jsonb DEFAULT '[]'::jsonb, "actif" boolean DEFAULT true, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_horaires_substitutions_region" ON "public"."horaires_substitutions" ("region_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_substitutions_horaire" ON "public"."horaires_substitutions" ("horaire_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_substitutions_actif" ON "public"."horaires_substitutions" ("actif") WHERE deleted_at IS NULL; -- ----------------------------------------------- -- 6.14 perturbations -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."perturbations" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "horaire_id" uuid NOT NULL REFERENCES "public"."horaires"("id") ON DELETE CASCADE, "type_perturbation" varchar(50) NOT NULL CHECK (type_perturbation IN ('retard','suppression','modification_parcours')), "duree_retard_minutes" integer, "cause" varchar(100) NOT NULL CHECK (cause IN ('incident_technique','accident_personne','conditions_meteorologiques','travaux','mouvement_social','intervention_police','affluence','malveillance','autre')), "cause_details" text, "sillon_substitution_id" uuid REFERENCES "public"."horaires"("id") ON DELETE SET NULL, "date_debut" timestamptz NOT NULL DEFAULT now(), "date_fin" timestamptz, "actif" boolean DEFAULT true, "notes" text, "region_id" varchar(100), "created_by" uuid, "perturbations_arrets" jsonb, "jours_perturbation" jsonb, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_perturbations_horaire" ON "public"."perturbations" ("horaire_id"); CREATE INDEX IF NOT EXISTS "idx_perturbations_type" ON "public"."perturbations" ("type_perturbation"); CREATE INDEX IF NOT EXISTS "idx_perturbations_actif" ON "public"."perturbations" ("actif") WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS "idx_perturbations_dates" ON "public"."perturbations" ("date_debut","date_fin"); CREATE INDEX IF NOT EXISTS "idx_perturbations_sillon_substitution" ON "public"."perturbations" ("sillon_substitution_id"); CREATE INDEX IF NOT EXISTS "idx_perturbations_region_id" ON "public"."perturbations" ("region_id"); CREATE INDEX IF NOT EXISTS "idx_perturbations_created_by" ON "public"."perturbations" ("created_by"); -- ----------------------------------------------- -- 6.15 perturbations_modifications_parcours -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."perturbations_modifications_parcours" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "perturbation_id" uuid NOT NULL REFERENCES "public"."perturbations"("id") ON DELETE CASCADE, "type_modification" varchar(20) NOT NULL CHECK (type_modification IN ('gare_supprimee','gare_ajoutee','horaire_modifie')), "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "gare_nom" varchar(255) NOT NULL, "nouvelle_heure_arrivee" time, "nouvelle_heure_depart" time, "ordre" integer, "created_at" timestamptz DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_modif_parcours_perturbation" ON "public"."perturbations_modifications_parcours" ("perturbation_id"); CREATE INDEX IF NOT EXISTS "idx_modif_parcours_gare" ON "public"."perturbations_modifications_parcours" ("gare_id"); -- ----------------------------------------------- -- 6.16 info_trafic -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."info_trafic" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "titre" varchar(255) NOT NULL, "description" text, "type" varchar(50) NOT NULL CHECK (type IN ('perturbation','travaux','greve','meteo','autre')), "severite" varchar(20) NOT NULL CHECK (severite IN ('info','warning','critical')), "ligne" varchar(100), "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "date_debut" timestamptz NOT NULL DEFAULT now(), "date_fin" timestamptz, "actif" boolean DEFAULT true, "region_id" varchar(100), "created_by" uuid, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_info_trafic_actif" ON "public"."info_trafic" ("actif") WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS "idx_info_trafic_dates" ON "public"."info_trafic" ("date_debut","date_fin"); CREATE INDEX IF NOT EXISTS "idx_info_trafic_region_id" ON "public"."info_trafic" ("region_id"); CREATE INDEX IF NOT EXISTS "idx_info_trafic_created_by" ON "public"."info_trafic" ("created_by"); -- ----------------------------------------------- -- 6.17 evenements -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."evenements" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "titre" varchar(255) NOT NULL, "description" text, "lieu" varchar(255), "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "date_debut" timestamptz NOT NULL, "date_fin" timestamptz, "image_url" varchar(500), "lien_externe" varchar(500), "actif" boolean DEFAULT true, "visible_site" boolean DEFAULT true, "region_id" varchar(100), "created_by" uuid, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_evenements_actif" ON "public"."evenements" ("actif") WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS "idx_evenements_dates" ON "public"."evenements" ("date_debut","date_fin"); CREATE INDEX IF NOT EXISTS "idx_evenements_region_id" ON "public"."evenements" ("region_id"); CREATE INDEX IF NOT EXISTS "idx_evenements_created_by" ON "public"."evenements" ("created_by"); -- ----------------------------------------------- -- 6.18 actualites -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."actualites" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "titre" varchar(255) NOT NULL, "contenu" text NOT NULL, "resume" varchar(500), "categorie" varchar(50) CHECK (categorie IN ('general','service','promotion','partenariat','autre')), "image_url" varchar(500), "image_couverture" text, "date_publication" timestamptz NOT NULL DEFAULT now(), "auteur" varchar(100), "actif" boolean DEFAULT true, "mise_en_avant" boolean DEFAULT false, "region_id" varchar(100), "created_by" uuid, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_actualites_actif" ON "public"."actualites" ("actif") WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS "idx_actualites_publication" ON "public"."actualites" ("date_publication"); CREATE INDEX IF NOT EXISTS "idx_actualites_region_id" ON "public"."actualites" ("region_id"); CREATE INDEX IF NOT EXISTS "idx_actualites_created_by" ON "public"."actualites" ("created_by"); -- ----------------------------------------------- -- 6.19 travaux -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."travaux" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "titre" text NOT NULL, "description" text, "date_debut" date NOT NULL, "date_fin" date NOT NULL, "jours_travaux" text[] NOT NULL, "heure_debut" time NOT NULL, "heure_fin" time NOT NULL, "actif" boolean DEFAULT true, "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, "ligne_nom" text, "region_id" varchar(100), "created_by" uuid REFERENCES "public"."utilisateurs"("id"), "substitutions_attribuees" jsonb DEFAULT '[]'::jsonb, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz, CONSTRAINT "valid_dates" CHECK (date_fin >= date_debut), CONSTRAINT "valid_hours" CHECK (heure_fin > heure_debut) ); CREATE INDEX IF NOT EXISTS "idx_travaux_dates" ON "public"."travaux" ("date_debut","date_fin"); CREATE INDEX IF NOT EXISTS "idx_travaux_actif" ON "public"."travaux" ("actif"); CREATE INDEX IF NOT EXISTS "idx_travaux_deleted" ON "public"."travaux" ("deleted_at"); CREATE INDEX IF NOT EXISTS "idx_travaux_ligne_id" ON "public"."travaux" ("ligne_id"); CREATE INDEX IF NOT EXISTS "idx_travaux_region_id" ON "public"."travaux" ("region_id"); CREATE INDEX IF NOT EXISTS "idx_travaux_created_by" ON "public"."travaux" ("created_by"); CREATE INDEX IF NOT EXISTS "idx_travaux_substitutions_attribuees_gin" ON "public"."travaux" USING gin ("substitutions_attribuees"); -- ----------------------------------------------- -- 6.20 travaux_substitutions -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."travaux_substitutions" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "travaux_id" uuid NOT NULL REFERENCES "public"."travaux"("id") ON DELETE CASCADE, "horaire_id" uuid NOT NULL REFERENCES "public"."horaires"("id") ON DELETE CASCADE, "ordre" integer DEFAULT 0, "created_at" timestamptz DEFAULT now(), UNIQUE ("travaux_id","horaire_id") ); CREATE INDEX IF NOT EXISTS "idx_travaux_substitutions_travaux" ON "public"."travaux_substitutions" ("travaux_id"); CREATE INDEX IF NOT EXISTS "idx_travaux_substitutions_horaire" ON "public"."travaux_substitutions" ("horaire_id"); -- ----------------------------------------------- -- 6.21 tarifs -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."tarifs" ( "id" integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "nom" varchar(255) NOT NULL, "type" varchar(50) NOT NULL, "description" text, "prix_minimum" numeric(10,2), "prix_par_km" numeric(10,3), "age_maximum" integer, "regions" text[] NOT NULL, "date_debut" date, "date_fin" date, "ordre" integer NOT NULL DEFAULT 0, "page_dediee" boolean DEFAULT false, "infos" jsonb DEFAULT '{}'::jsonb, "details" jsonb, "created_at" timestamp DEFAULT now(), "updated_at" timestamp DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_tarifs_ordre" ON "public"."tarifs" ("ordre"); CREATE INDEX IF NOT EXISTS "idx_tarifs_regions" ON "public"."tarifs" USING gin ("regions"); -- ----------------------------------------------- -- 6.22 parametres -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."parametres" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "cle" varchar(100) NOT NULL UNIQUE, "valeur" jsonb, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now() ); -- ----------------------------------------------- -- 6.23 menus_regionaux -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."menus_regionaux" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "nom" varchar(200) NOT NULL, "description" text, "actif" boolean DEFAULT false, "structure" jsonb DEFAULT '[]'::jsonb, "region_id" varchar(64), "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_menus_regionaux_region_id" ON "public"."menus_regionaux" ("region_id"); -- ----------------------------------------------- -- 6.24 pages (CMS) -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."pages" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "titre" text NOT NULL, "slug" text, "contenu" jsonb DEFAULT '[]'::jsonb, "statut" text DEFAULT 'brouillon', "region_id" varchar(255), "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); -- ----------------------------------------------- -- 6.25 page_personnalisation -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."page_personnalisation" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "page" text NOT NULL UNIQUE, "config" jsonb DEFAULT '{}'::jsonb, "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now() ); -- ----------------------------------------------- -- 6.26 info_gares -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."info_gares" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "titre" text, "description" text, "debut_affichage" timestamptz, "fin_affichage" timestamptz, "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, "gare_ids" uuid[] DEFAULT '{}'::uuid[], "ligne_ids" uuid[] DEFAULT '{}'::uuid[], "type" text DEFAULT 'Information', "created_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); -- ----------------------------------------------- -- 6.27 regles_quais -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."regles_quais" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "gare_id" uuid NOT NULL REFERENCES "public"."gares"("id") ON DELETE CASCADE, "ligne_id" uuid REFERENCES "public"."lignes"("id") ON DELETE SET NULL, "type_regle" text, "quais_concernes" jsonb DEFAULT '[]'::jsonb, "created_at" timestamptz DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_regles_quais_gare_id" ON "public"."regles_quais" ("gare_id"); -- ----------------------------------------------- -- 6.28 messagerie_threads -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."messagerie_threads" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now() ); -- ----------------------------------------------- -- 6.29 messagerie_participants -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."messagerie_participants" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "thread_id" uuid NOT NULL REFERENCES "public"."messagerie_threads"("id") ON DELETE CASCADE, "user_id" uuid NOT NULL REFERENCES "public"."utilisateurs"("id") ON DELETE CASCADE, "created_at" timestamptz NOT NULL DEFAULT now(), UNIQUE ("thread_id","user_id") ); CREATE INDEX IF NOT EXISTS "idx_messagerie_participants_user_id" ON "public"."messagerie_participants" ("user_id"); -- ----------------------------------------------- -- 6.30 messagerie_messages -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."messagerie_messages" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "thread_id" uuid NOT NULL REFERENCES "public"."messagerie_threads"("id") ON DELETE CASCADE, "sender_id" uuid NOT NULL REFERENCES "public"."utilisateurs"("id") ON DELETE CASCADE, "contenu" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_messagerie_messages_thread_id" ON "public"."messagerie_messages" ("thread_id"); CREATE INDEX IF NOT EXISTS "idx_messagerie_messages_created_at" ON "public"."messagerie_messages" ("created_at" DESC); -- ----------------------------------------------- -- 6.31 profils_utilisateurs (app mobile / client) -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."profils_utilisateurs" ( "id" uuid NOT NULL PRIMARY KEY, "email" text NOT NULL, "prenom" text, "nom" text, "date_naissance" date, "telephone" text, "carte_reduction" text, "gares_favorites" text[] DEFAULT '{}'::text[], "lignes_favorites" text[] DEFAULT '{}'::text[], "cartes_bancaires" jsonb DEFAULT '[]'::jsonb, "mode_paiement_favori" text, "commandes" jsonb DEFAULT '[]'::jsonb, "panier" jsonb DEFAULT '[]'::jsonb, "trajets_favoris" text[] DEFAULT '{}'::text[], "created_at" timestamptz DEFAULT now(), "updated_at" timestamptz DEFAULT now() ); CREATE INDEX IF NOT EXISTS "idx_profils_email" ON "public"."profils_utilisateurs" ("email"); -- ----------------------------------------------- -- 6.32 banque_mots (annonces sonores) -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."banque_mots" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "label" text, "nom_fichier" text, "storage_path" text, "categorie" text DEFAULT 'general', "format" text DEFAULT 'mp3', "taille_bytes" bigint, "description" text, "duree_ms" integer, "actif" boolean NOT NULL DEFAULT true, "created_by" uuid, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_banque_mots_categorie" ON "public"."banque_mots" ("categorie"); CREATE INDEX IF NOT EXISTS "idx_banque_mots_label" ON "public"."banque_mots" ("label"); CREATE INDEX IF NOT EXISTS "idx_banque_mots_actif" ON "public"."banque_mots" ("actif") WHERE deleted_at IS NULL; -- ----------------------------------------------- -- 6.33 annonces_templates -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."annonces_templates" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "nom" text NOT NULL, "type_annonce" text NOT NULL, "description" text, "sequence" jsonb NOT NULL DEFAULT '[]'::jsonb, "variables" jsonb NOT NULL DEFAULT '[]'::jsonb, "tts_texte" text, "actif" boolean NOT NULL DEFAULT true, "created_by" uuid, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); CREATE INDEX IF NOT EXISTS "idx_templates_type" ON "public"."annonces_templates" ("type_annonce"); CREATE INDEX IF NOT EXISTS "idx_templates_actif" ON "public"."annonces_templates" ("actif") WHERE deleted_at IS NULL; -- ----------------------------------------------- -- 6.34 annonces_enregistrees -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."annonces_enregistrees" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "nom" text, "type_annonce" text, "template_id" uuid REFERENCES "public"."annonces_templates"("id"), "storage_path" text, "nom_fichier" text, "variables_values" jsonb DEFAULT '{}'::jsonb, "mode_generation" text DEFAULT 'tts', "duree_ms" integer, "taille_bytes" bigint, "gare_id" uuid REFERENCES "public"."gares"("id"), "diffuse" boolean DEFAULT false, "date_diffusion" timestamptz, "notes" text, "created_by" uuid, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz DEFAULT now(), "deleted_at" timestamptz ); -- ----------------------------------------------- -- 6.35 annonces_diffusions -- ----------------------------------------------- CREATE TABLE IF NOT EXISTS "public"."annonces_diffusions" ( "id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "annonce_id" uuid REFERENCES "public"."annonces_enregistrees"("id") ON DELETE SET NULL, "gare_id" uuid REFERENCES "public"."gares"("id") ON DELETE SET NULL, "audio_path" text, "tts_texte" text, "created_at" timestamptz NOT NULL DEFAULT now() ); -- ============================================================================ -- 7. TRIGGERS -- ============================================================================ DROP TRIGGER IF EXISTS "tr_utilisateurs_set_updated_at" ON "public"."utilisateurs"; CREATE TRIGGER "tr_utilisateurs_set_updated_at" BEFORE UPDATE ON "public"."utilisateurs" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); DROP TRIGGER IF EXISTS "tr_utilisateurs_audit" ON "public"."utilisateurs"; CREATE TRIGGER "tr_utilisateurs_audit" AFTER INSERT OR DELETE OR UPDATE ON "public"."utilisateurs" FOR EACH ROW EXECUTE FUNCTION "public"."log_audit"(); DROP TRIGGER IF EXISTS "tr_gares_set_updated_at" ON "public"."gares"; CREATE TRIGGER "tr_gares_set_updated_at" BEFORE UPDATE ON "public"."gares" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); DROP TRIGGER IF EXISTS "tr_gares_audit" ON "public"."gares"; CREATE TRIGGER "tr_gares_audit" AFTER INSERT OR DELETE OR UPDATE ON "public"."gares" FOR EACH ROW EXECUTE FUNCTION "public"."log_audit"(); DROP TRIGGER IF EXISTS "update_horaires_updated_at" ON "public"."horaires"; CREATE TRIGGER "update_horaires_updated_at" BEFORE UPDATE ON "public"."horaires" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); DROP TRIGGER IF EXISTS "update_horaires_gares_desservies_updated_at" ON "public"."horaires_gares_desservies"; CREATE TRIGGER "update_horaires_gares_desservies_updated_at" BEFORE UPDATE ON "public"."horaires_gares_desservies" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); DROP TRIGGER IF EXISTS "update_perturbations_updated_at" ON "public"."perturbations"; CREATE TRIGGER "update_perturbations_updated_at" BEFORE UPDATE ON "public"."perturbations" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); DROP TRIGGER IF EXISTS "update_info_trafic_updated_at" ON "public"."info_trafic"; CREATE TRIGGER "update_info_trafic_updated_at" BEFORE UPDATE ON "public"."info_trafic" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); DROP TRIGGER IF EXISTS "update_evenements_updated_at" ON "public"."evenements"; CREATE TRIGGER "update_evenements_updated_at" BEFORE UPDATE ON "public"."evenements" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); DROP TRIGGER IF EXISTS "update_actualites_updated_at" ON "public"."actualites"; CREATE TRIGGER "update_actualites_updated_at" BEFORE UPDATE ON "public"."actualites" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); DROP TRIGGER IF EXISTS "update_lignes_updated_at" ON "public"."lignes"; CREATE TRIGGER "update_lignes_updated_at" BEFORE UPDATE ON "public"."lignes" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); DROP TRIGGER IF EXISTS "update_services_annuels_updated_at" ON "public"."services_annuels"; CREATE TRIGGER "update_services_annuels_updated_at" BEFORE UPDATE ON "public"."services_annuels" FOR EACH ROW EXECUTE FUNCTION "public"."update_updated_at_column"(); DROP TRIGGER IF EXISTS "trigger_update_travaux_updated_at" ON "public"."travaux"; CREATE TRIGGER "trigger_update_travaux_updated_at" BEFORE UPDATE ON "public"."travaux" FOR EACH ROW EXECUTE FUNCTION "public"."update_travaux_updated_at"(); DROP TRIGGER IF EXISTS "set_profils_updated_at" ON "public"."profils_utilisateurs"; CREATE TRIGGER "set_profils_updated_at" BEFORE UPDATE ON "public"."profils_utilisateurs" FOR EACH ROW EXECUTE FUNCTION "public"."handle_updated_at"(); -- ============================================================================ -- 8. FONCTIONS RPC MÉTIER -- ============================================================================ -- Authentification CREATE OR REPLACE FUNCTION "public"."authenticate_user"(p_email text, p_password text) RETURNS TABLE(id uuid, email text, nom text, prenom text, name text, role text, region_admin boolean, region_id text) LANGUAGE plpgsql STABLE SECURITY DEFINER AS $$ DECLARE has_deleted_at boolean; sql text; BEGIN SELECT EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema='public' AND table_name='utilisateurs' AND column_name='deleted_at') INTO has_deleted_at; sql := 'SELECT u.id::uuid, u.email::text, u.nom::text, u.prenom::text, ' || '(COALESCE(u.nom,'''') || '' '' || COALESCE(u.prenom,''''))::text as name, ' || 'u.role::text, ' || 'COALESCE(u.region_admin, false)::boolean as region_admin, ' || 'u.region_id::text ' || 'FROM public.utilisateurs u ' || 'WHERE lower(u.email) = lower($1) ' || 'AND u.password_hash IS NOT NULL ' || 'AND u.password_hash = crypt($2, u.password_hash) ' || 'AND coalesce(u.actif, true) = true'; IF has_deleted_at THEN sql := sql || ' AND u.deleted_at IS NULL'; END IF; RETURN QUERY EXECUTE sql USING p_email, p_password; END; $$; -- Création utilisateur avec mot de passe CREATE OR REPLACE FUNCTION "public"."create_user_with_password"( p_email text, p_nom text, p_prenom text, p_role text DEFAULT 'utilisateur', p_actif boolean DEFAULT true, p_password text DEFAULT NULL, p_photo_url text DEFAULT NULL, p_region_admin boolean DEFAULT false, p_region_id text DEFAULT NULL ) RETURNS TABLE(id uuid, email text, nom text, prenom text, role text, actif boolean, photo_url text, region_admin boolean, region_id text) LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE new_id uuid; BEGIN INSERT INTO public.utilisateurs (email, nom, prenom, role, actif, password_hash, photo_url, region_admin, region_id, created_at) VALUES (lower(p_email), p_nom, p_prenom, p_role, p_actif, CASE WHEN p_password IS NOT NULL AND p_password != '' THEN crypt(p_password, gen_salt('bf')) ELSE NULL END, p_photo_url, p_region_admin, p_region_id, NOW()) RETURNING utilisateurs.id INTO new_id; RETURN QUERY SELECT u.id, u.email::text, u.nom::text, u.prenom::text, u.role::text, u.actif, u.photo_url::text, u.region_admin, u.region_id::text FROM public.utilisateurs u WHERE u.id = new_id; END; $$; -- Mise à jour mot de passe CREATE OR REPLACE FUNCTION "public"."update_user_password"(p_user_id uuid, p_password text) RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN IF p_password IS NULL OR p_password = '' THEN RAISE EXCEPTION 'Le mot de passe ne peut pas être vide'; END IF; UPDATE public.utilisateurs SET password_hash = crypt(p_password, gen_salt('bf')), updated_at = NOW() WHERE id = p_user_id; IF NOT FOUND THEN RAISE EXCEPTION 'Utilisateur non trouvé'; END IF; END; $$; -- Détails d'une gare CREATE OR REPLACE FUNCTION "public"."get_gare_details"(p_gare_id uuid) RETURNS TABLE(id uuid, nom text, type varchar(50), services "public"."service_type"[], quais jsonb, transports jsonb, created_at timestamptz, updated_at timestamptz) LANGUAGE plpgsql STABLE SECURITY DEFINER AS $$ BEGIN RETURN QUERY SELECT g.id, g.nom, g.type, g.services, COALESCE((SELECT jsonb_agg(jsonb_build_object('id',q.id,'nom',q.nom,'distance_metres',q.distance_metres)) FROM public.gare_quais q WHERE q.gare_id = g.id), '[]'::jsonb) as quais, COALESCE((SELECT jsonb_agg(jsonb_build_object('id',t.id,'type',t.type,'ligne',t.ligne)) FROM public.gare_transports t WHERE t.gare_id = g.id), '[]'::jsonb) as transports, g.created_at, g.updated_at FROM public.gares g WHERE g.id = p_gare_id AND g.deleted_at IS NULL; END; $$; -- ============================================================================ -- 9. ROW LEVEL SECURITY (RLS) + POLICIES -- ============================================================================ -- menus_regionaux ALTER TABLE "public"."menus_regionaux" ENABLE ROW LEVEL SECURITY; CREATE POLICY "menus_select" ON "public"."menus_regionaux" FOR SELECT USING (true); CREATE POLICY "menus_insert" ON "public"."menus_regionaux" FOR INSERT WITH CHECK (true); CREATE POLICY "menus_update" ON "public"."menus_regionaux" FOR UPDATE USING (true); CREATE POLICY "menus_delete" ON "public"."menus_regionaux" FOR DELETE USING (true); -- parametres ALTER TABLE "public"."parametres" ENABLE ROW LEVEL SECURITY; CREATE POLICY "parametres_select_all" ON "public"."parametres" FOR SELECT USING (true); CREATE POLICY "parametres_insert_all" ON "public"."parametres" FOR INSERT WITH CHECK (true); CREATE POLICY "parametres_update_all" ON "public"."parametres" FOR UPDATE USING (true); CREATE POLICY "parametres_delete_all" ON "public"."parametres" FOR DELETE USING (true); -- messagerie_threads ALTER TABLE "public"."messagerie_threads" ENABLE ROW LEVEL SECURITY; CREATE POLICY "messagerie_threads_insert" ON "public"."messagerie_threads" FOR INSERT TO "anon" WITH CHECK (true); CREATE POLICY "messagerie_threads_select" ON "public"."messagerie_threads" FOR SELECT TO "anon" USING (EXISTS (SELECT 1 FROM "public"."messagerie_participants" p WHERE p.thread_id = messagerie_threads.id)); CREATE POLICY "messagerie_threads_update" ON "public"."messagerie_threads" FOR UPDATE TO "anon" USING (EXISTS (SELECT 1 FROM "public"."messagerie_participants" p WHERE p.thread_id = messagerie_threads.id)) WITH CHECK (true); -- messagerie_participants ALTER TABLE "public"."messagerie_participants" ENABLE ROW LEVEL SECURITY; CREATE POLICY "messagerie_participants_insert" ON "public"."messagerie_participants" FOR INSERT TO "anon" WITH CHECK (true); CREATE POLICY "messagerie_participants_select" ON "public"."messagerie_participants" FOR SELECT TO "anon" USING (true); -- messagerie_messages ALTER TABLE "public"."messagerie_messages" ENABLE ROW LEVEL SECURITY; CREATE POLICY "messagerie_messages_insert" ON "public"."messagerie_messages" FOR INSERT TO "anon" WITH CHECK (EXISTS (SELECT 1 FROM "public"."messagerie_participants" p WHERE p.thread_id = messagerie_messages.thread_id AND p.user_id = messagerie_messages.sender_id)); CREATE POLICY "messagerie_messages_select" ON "public"."messagerie_messages" FOR SELECT TO "anon" USING (EXISTS (SELECT 1 FROM "public"."messagerie_participants" p WHERE p.thread_id = messagerie_messages.thread_id)); -- profils_utilisateurs ALTER TABLE "public"."profils_utilisateurs" ENABLE ROW LEVEL SECURITY; CREATE POLICY "Lecture profil propre" ON "public"."profils_utilisateurs" FOR SELECT USING (auth.uid() = id); CREATE POLICY "Insertion profil propre" ON "public"."profils_utilisateurs" FOR INSERT WITH CHECK (auth.uid() = id); CREATE POLICY "Mise à jour profil propre" ON "public"."profils_utilisateurs" FOR UPDATE USING (auth.uid() = id) WITH CHECK (auth.uid() = id); CREATE POLICY "Suppression profil propre" ON "public"."profils_utilisateurs" FOR DELETE USING (auth.uid() = id); -- ============================================================================ -- 10. GRANTS — Accès complet pour anon / authenticated / service_role -- ============================================================================ GRANT USAGE ON SCHEMA "public" TO "postgres", "anon", "authenticated", "service_role"; -- Tables GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA "public" TO "anon", "authenticated", "service_role"; -- Séquences GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "public" TO "anon", "authenticated", "service_role"; -- Fonctions GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA "public" TO "anon", "authenticated", "service_role"; -- Droits par défaut pour les futurs objets ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "anon", "authenticated", "service_role"; ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT USAGE, SELECT ON SEQUENCES TO "anon", "authenticated", "service_role"; ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT EXECUTE ON FUNCTIONS TO "anon", "authenticated", "service_role"; -- ============================================================================ -- 11. DONNÉES INITIALES — Utilisateur administrateur -- ============================================================================ INSERT INTO "public"."utilisateurs" (email, nom, prenom, password_hash, role, actif, region_admin) VALUES ( 'admin@ferrovia.fr', 'Admin', 'Ferrovia', crypt('password', gen_salt('bf')), 'admin', true, true ) ON CONFLICT (email) DO UPDATE SET password_hash = EXCLUDED.password_hash, role = EXCLUDED.role, actif = EXCLUDED.actif, region_admin = EXCLUDED.region_admin, updated_at = NOW(); -- ============================================================================ -- FIN DU SCHÉMA — 35 tables, 3 enums, 12 fonctions, 14 triggers, 1 admin -- ============================================================================