V5.0.0 //
-> Refonte du système de login -> Récupération du MDP -> Refonte imports d'horaires + Modale de gestion d'horaires
This commit is contained in:
1 parent
d89eaaa17f
commit
36b0c00cbc
23 files changed
+5557
-1399
No files matched your search
@@ -0,0 +1,152 @@
|
||||
# Guide de Configuration : Authentification & Envoi d'Emails Supabase Self-Hosted
|
||||
|
||||
Ce document détaille la configuration complète pour :
|
||||
1. **Gérer les profils et permissions sans table dédiée** via Supabase Auth (`user_metadata`).
|
||||
2. **Configurer l'envoi d'emails SMTP** sur une instance Supabase Self-Hosted.
|
||||
3. **Appliquer le template HTML de réinitialisation de mot de passe**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Stockage des Profils et Permissions sans Table Dédiée
|
||||
|
||||
Supabase Auth intègre nativement un champ JSONB nommé `raw_user_meta_data` dans la table système `auth.users`.
|
||||
L'application Ferrovia Panel utilise désormais ce champ pour stocker les profils utilisateurs et leurs permissions RBAC sans nécessiter de table `public.utilisateurs`.
|
||||
|
||||
### Structure des Métadonnées Utilisateur (`user_metadata`)
|
||||
```json
|
||||
{
|
||||
"nom": "Dupont",
|
||||
"prenom": "Jean",
|
||||
"name": "Jean Dupont",
|
||||
"role": "admin",
|
||||
"permissions": {
|
||||
"dashboard": { "read": true, "create": true, "update": true, "delete": true },
|
||||
"gares": { "read": true, "create": true, "update": true, "delete": true },
|
||||
"horaires": { "read": true, "create": true, "update": true, "delete": true }
|
||||
},
|
||||
"region_admin": false,
|
||||
"region_id": null,
|
||||
"region_nom": null,
|
||||
"actif": true
|
||||
}
|
||||
```
|
||||
|
||||
### Consultation et Modification directe depuis Supabase (SQL)
|
||||
Si vous souhaitez attribuer le rôle administrateur ou des permissions personnalisées à un utilisateur directement en base de données :
|
||||
|
||||
```sql
|
||||
-- Mettre à jour les métadonnées d'un utilisateur existant :
|
||||
UPDATE auth.users
|
||||
SET raw_user_meta_data = jsonb_build_object(
|
||||
'nom', 'Administrateur',
|
||||
'prenom', 'Principal',
|
||||
'name', 'Principal Administrateur',
|
||||
'role', 'admin',
|
||||
'actif', true,
|
||||
'permissions', '{
|
||||
"dashboard": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"gares": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"materiel_roulant": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"horaires": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"fiches_horaires": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"lignes": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"services": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"quais": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"informations": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"perturbations": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"travaux": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"menu": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"pages": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"tarifs": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"messagerie": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"parametres": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"utilisateurs_global": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"annonces_sonores": {"read": true, "create": true, "update": true, "delete": true},
|
||||
"systeme_maj": {"read": true, "create": true, "update": true, "delete": true}
|
||||
}'::jsonb
|
||||
)
|
||||
WHERE email = 'votre-email@domaine.com';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Configuration SMTP sur Supabase Self-Hosted (Docker)
|
||||
|
||||
Sur votre serveur hébergeant Supabase (ex: avec Docker Compose), éditez le fichier `.env` du dossier Supabase.
|
||||
|
||||
### Variables d'environnement pour l'envoi de mail (GoTrue / Auth)
|
||||
|
||||
```env
|
||||
## Configuration Générale du Site
|
||||
SITE_URL=https://votre-domaine.fr
|
||||
ADDITIONAL_REDIRECT_URLS=https://votre-domaine.fr/reinitialiser-mot-de-passe,http://localhost:3000/reinitialiser-mot-de-passe
|
||||
|
||||
## Activation du Serveur SMTP
|
||||
GOTRUE_SMTP_HOST=smtp.votre-fournisseur.com
|
||||
GOTRUE_SMTP_PORT=587
|
||||
GOTRUE_SMTP_USER=votre-utilisateur-smtp
|
||||
GOTRUE_SMTP_PASS=votre-mot-de-passe-smtp
|
||||
GOTRUE_SMTP_ADMIN_EMAIL=noreply@votre-domaine.fr
|
||||
GOTRUE_SMTP_SENDER_NAME="Ferrovia Panel"
|
||||
|
||||
## Paramètres des liens d'authentification
|
||||
GOTRUE_MAILER_AUTOCONFIRM=false
|
||||
GOTRUE_MAILER_URLPATHS_RECOVERY=/reinitialiser-mot-de-passe
|
||||
```
|
||||
|
||||
> **Note Fournisseurs SMTP recommandés :** Brevo (ex-Sendinblue), Mailjet, Resend, Amazon SES ou un serveur Postfix dédié.
|
||||
|
||||
---
|
||||
|
||||
## 3. Installation du Template Email de Réinitialisation
|
||||
|
||||
Le template email responsive Ferrovia est situé dans :
|
||||
`supabase/templates/recovery.html`
|
||||
|
||||
### Option A : Via Supabase Studio (Interface Web)
|
||||
1. Rendez-vous sur votre interface Supabase Studio (`https://votre-supabase/project/default/auth/templates`).
|
||||
2. Allez dans **Authentication** > **Email Templates**.
|
||||
3. Sélectionnez l'onglet **Reset Password**.
|
||||
4. Dans le champ **Subject**, indiquez :
|
||||
`Réinitialisation de votre mot de passe - Ferrovia Panel`
|
||||
5. Copiez-collez l'intégralité du code HTML du fichier `supabase/templates/recovery.html` dans le champ **Body**.
|
||||
6. Cliquez sur **Save changes**.
|
||||
|
||||
### Option B : Montage Docker (GoTrue)
|
||||
Si votre déploiement Docker utilise des fichiers de templates montés :
|
||||
1. Dans `docker-compose.yml`, montez le template dans le conteneur `auth` :
|
||||
```yaml
|
||||
auth:
|
||||
image: supabase/gotrue:...
|
||||
volumes:
|
||||
- ./templates/recovery.html:/etc/gotrue/templates/recovery.html:ro
|
||||
environment:
|
||||
GOTRUE_MAILER_TEMPLATES_RECOVERY: /etc/gotrue/templates/recovery.html
|
||||
```
|
||||
2. Redémarrez le conteneur auth :
|
||||
```bash
|
||||
docker compose restart auth
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Parcours Utilisateur dans l'Application
|
||||
|
||||
1. **Page de Connexion (`/`)** :
|
||||
- L'utilisateur clique sur **"Mot de passe oublié ?"**.
|
||||
- Si un email était déjà tapé, il est automatiquement pré-rempli sur la page de réinitialisation.
|
||||
|
||||
2. **Page de Réinitialisation (`/reinitialiser-mot-de-passe`) - Étape 1** :
|
||||
- L'utilisateur saisit son adresse email professionnelle.
|
||||
- L'application appelle `supabase.auth.resetPasswordForEmail(email, { redirectTo })`.
|
||||
- Supabase génère un token sécurisé et expédie l'email avec le template personnalisé.
|
||||
|
||||
3. **Email Reçu** :
|
||||
- L'utilisateur clique sur **"Définir mon nouveau mot de passe"**.
|
||||
- Le lien redirige vers `https://votre-domaine.fr/reinitialiser-mot-de-passe#access_token=...&type=recovery`.
|
||||
|
||||
4. **Page de Réinitialisation - Étape 2** :
|
||||
- L'application détecte le mode récupération (`type=recovery`).
|
||||
- L'utilisateur saisit son nouveau mot de passe (min. 8 caractères) et confirme.
|
||||
- L'application appelle `supabase.auth.updateUser({ password })`.
|
||||
- Message de confirmation et redirection automatique vers la connexion.
|
||||
@@ -248,6 +248,10 @@ otp_expiry = 3600
|
||||
# subject = "You have been invited"
|
||||
# content_path = "./supabase/templates/invite.html"
|
||||
|
||||
[auth.email.template.recovery]
|
||||
subject = "Réinitialisation de votre mot de passe - Ferrovia Panel"
|
||||
content_path = "./supabase/templates/recovery.html"
|
||||
|
||||
# Uncomment to customize notification email template
|
||||
# [auth.email.notification.password_changed]
|
||||
# enabled = true
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
-- ============================================================================
|
||||
-- MIGRATION : OPTIMISATION DE LA TABLE UNIQUE "horaires"
|
||||
-- Date : 2026-10-01
|
||||
-- Objectif : Consolider 100% des données relatives aux horaires (sillons, dessertes,
|
||||
-- quais, arrêts intermédiaires, substitutions, calendrier, matériel,
|
||||
-- temps réel) dans une seule et unique table "horaires" ultra-performante.
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. Ajout de toutes les colonnes nécessaires à la table horaires
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Identifiants et informations train
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'nom_train') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "nom_train" varchar(255);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'code_mission') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "code_mission" varchar(50);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'sens') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "sens" varchar(20) DEFAULT 'aller';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'transporteur') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "transporteur" varchar(100) DEFAULT 'SNCF Voyageurs';
|
||||
END IF;
|
||||
|
||||
-- Dessertes et attribution des quais
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'gares_desservies') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "gares_desservies" jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'attributions_quais') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "attributions_quais" jsonb NOT NULL DEFAULT '{}'::jsonb;
|
||||
END IF;
|
||||
|
||||
-- Calendrier étendu et dates de validité
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'date_debut') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "date_debut" timestamptz;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'date_fin') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "date_fin" timestamptz;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'calendrier_circulation') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "calendrier_circulation" jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'periode_validite') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "periode_validite" jsonb NOT NULL DEFAULT '{}'::jsonb;
|
||||
END IF;
|
||||
|
||||
-- Matériel et composition
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'composition_train') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "composition_train" jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||
END IF;
|
||||
|
||||
-- Région
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'region') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "region" varchar(255);
|
||||
END IF;
|
||||
|
||||
-- Service annuel et type de service
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'service_type') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "service_type" varchar(50) DEFAULT 'TER';
|
||||
END IF;
|
||||
|
||||
-- Substitutions et autocars
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'est_substitution') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "est_substitution" boolean NOT NULL DEFAULT false;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'motif_substitution') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "motif_substitution" varchar(255);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'substitution_disponible') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "substitution_disponible" boolean NOT NULL DEFAULT false;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'horaire_id') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "horaire_id" uuid;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'substitutions_associees') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "substitutions_associees" jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||
END IF;
|
||||
|
||||
-- Sillons & infrastructure
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'sillon_id') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "sillon_id" text;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'tranche') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "tranche" varchar(50);
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'gestionnaire_infrastructure') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "gestionnaire_infrastructure" varchar(100) DEFAULT 'SNCF Réseau';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'vitesse_max_kmh') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "vitesse_max_kmh" integer;
|
||||
END IF;
|
||||
|
||||
-- Exploitation temps réel et retards
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'statut') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "statut" varchar(50) NOT NULL DEFAULT 'a_l_heure';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'retard_minutes') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "retard_minutes" integer NOT NULL DEFAULT 0;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'retard_motif') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "retard_motif" text;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'informations_voyageurs') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "informations_voyageurs" text;
|
||||
END IF;
|
||||
|
||||
-- Métadonnées et notes
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'metadata') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "metadata" jsonb NOT NULL DEFAULT '{}'::jsonb;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'notes') THEN
|
||||
ALTER TABLE "public"."horaires" ADD COLUMN "notes" text;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 2. Migration des données relationnelles de horaires_gares_desservies vers horaires.gares_desservies (si la table existe)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'horaires_gares_desservies') THEN
|
||||
-- Mettre à jour horaires.gares_desservies avec les lignes de la table relationnelle si gares_desservies est vide
|
||||
UPDATE "public"."horaires" h
|
||||
SET gares_desservies = sub.json_dessertes,
|
||||
updated_at = NOW()
|
||||
FROM (
|
||||
SELECT
|
||||
horaire_id,
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'gare_id', gare_id,
|
||||
'gare_nom', gare_nom,
|
||||
'heure_arrivee', heure_arrivee,
|
||||
'heure_depart', heure_depart,
|
||||
'ordre', ordre,
|
||||
'quai', quai
|
||||
) ORDER BY ordre ASC
|
||||
) AS json_dessertes
|
||||
FROM "public"."horaires_gares_desservies"
|
||||
GROUP BY horaire_id
|
||||
) sub
|
||||
WHERE h.id = sub.horaire_id
|
||||
AND (h.gares_desservies IS NULL OR h.gares_desservies = '[]'::jsonb);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 3. Migration des substitutions de horaires_substitutions vers horaires (si la table existe)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'horaires_substitutions') THEN
|
||||
INSERT INTO "public"."horaires" (
|
||||
"id",
|
||||
"numero_train",
|
||||
"type_train",
|
||||
"type_train_id",
|
||||
"gare_depart_id",
|
||||
"gare_depart_nom",
|
||||
"heure_depart",
|
||||
"gare_arrivee_id",
|
||||
"gare_arrivee_nom",
|
||||
"heure_arrivee",
|
||||
"date_debut",
|
||||
"date_fin",
|
||||
"circule_lundi",
|
||||
"circule_mardi",
|
||||
"circule_mercredi",
|
||||
"circule_jeudi",
|
||||
"circule_vendredi",
|
||||
"circule_samedi",
|
||||
"circule_dimanche",
|
||||
"circule_jours_feries",
|
||||
"jours_personnalises",
|
||||
"jours_non_circulation",
|
||||
"materiel_roulant_id",
|
||||
"ligne_id",
|
||||
"ligne_nom",
|
||||
"service_annuel_id",
|
||||
"service_annuel_nom",
|
||||
"est_substitution",
|
||||
"motif_substitution",
|
||||
"gares_desservies",
|
||||
"composition_train",
|
||||
"substitution_disponible",
|
||||
"actif",
|
||||
"region_id",
|
||||
"region",
|
||||
"horaire_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at"
|
||||
)
|
||||
SELECT
|
||||
s."id",
|
||||
COALESCE(s."numero_train", 'SUBST-' || substring(s."id"::text, 1, 6)),
|
||||
s."type_train",
|
||||
s."type_train_id",
|
||||
s."gare_depart_id",
|
||||
s."gare_depart_nom",
|
||||
s."heure_depart",
|
||||
s."gare_arrivee_id",
|
||||
s."gare_arrivee_nom",
|
||||
s."heure_arrivee",
|
||||
s."date_debut",
|
||||
s."date_fin",
|
||||
COALESCE(s."circule_lundi", true),
|
||||
COALESCE(s."circule_mardi", true),
|
||||
COALESCE(s."circule_mercredi", true),
|
||||
COALESCE(s."circule_jeudi", true),
|
||||
COALESCE(s."circule_vendredi", true),
|
||||
COALESCE(s."circule_samedi", true),
|
||||
COALESCE(s."circule_dimanche", true),
|
||||
COALESCE(s."circule_jours_feries", false),
|
||||
COALESCE(s."jours_personnalises", '[]'::jsonb),
|
||||
COALESCE(s."jours_non_circulation", '[]'::jsonb),
|
||||
s."materiel_roulant_id",
|
||||
s."ligne_id",
|
||||
s."ligne_nom",
|
||||
s."service_annuel_id",
|
||||
s."service_annuel_nom",
|
||||
true AS est_substitution,
|
||||
s."motif_substitution",
|
||||
COALESCE(s."gares_desservies", '[]'::jsonb),
|
||||
COALESCE(s."composition_train", '[]'::jsonb),
|
||||
false,
|
||||
COALESCE(s."actif", true),
|
||||
s."region_id",
|
||||
s."region",
|
||||
s."horaire_id",
|
||||
COALESCE(s."created_at", NOW()),
|
||||
COALESCE(s."updated_at", NOW()),
|
||||
s."deleted_at"
|
||||
FROM "public"."horaires_substitutions" s
|
||||
ON CONFLICT ("id") DO UPDATE
|
||||
SET est_substitution = true,
|
||||
motif_substitution = EXCLUDED.motif_substitution,
|
||||
date_debut = EXCLUDED.date_debut,
|
||||
date_fin = EXCLUDED.date_fin,
|
||||
updated_at = NOW();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 4. Initialisation du dictionnaire attributions_quais depuis gares_desservies
|
||||
UPDATE "public"."horaires"
|
||||
SET attributions_quais = (
|
||||
SELECT COALESCE(
|
||||
jsonb_object_agg(
|
||||
COALESCE(elem->>'gare_id', elem->>'gare_nom'),
|
||||
jsonb_build_object(
|
||||
'quai', elem->>'quai',
|
||||
'quai_reel', elem->>'quai_reel',
|
||||
'ordre', elem->'ordre'
|
||||
)
|
||||
),
|
||||
'{}'::jsonb
|
||||
)
|
||||
FROM jsonb_array_elements(gares_desservies) elem
|
||||
WHERE elem->>'quai' IS NOT NULL OR elem->>'quai_reel' IS NOT NULL
|
||||
)
|
||||
WHERE attributions_quais = '{}'::jsonb
|
||||
AND gares_desservies IS NOT NULL
|
||||
AND jsonb_array_length(gares_desservies) > 0;
|
||||
|
||||
-- 5. Création des index haute performance (B-Tree et GIN)
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_numero_train" ON "public"."horaires" ("numero_train");
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_ligne_id" ON "public"."horaires" ("ligne_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_region_id" ON "public"."horaires" ("region_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_departs" ON "public"."horaires" ("gare_depart_id", "heure_depart");
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_arrivees" ON "public"."horaires" ("gare_arrivee_id", "heure_arrivee");
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_actif" ON "public"."horaires" ("actif") WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_substitution" ON "public"."horaires" ("est_substitution");
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_dates" ON "public"."horaires" ("date_debut", "date_fin");
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_statut" ON "public"."horaires" ("statut");
|
||||
|
||||
-- Index GIN JSONB pour recherche et filtrage ultra-rapide sans jointure
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_gares_desservies_gin"
|
||||
ON "public"."horaires" USING gin ("gares_desservies" jsonb_path_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_attributions_quais_gin"
|
||||
ON "public"."horaires" USING gin ("attributions_quais");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_calendrier_gin"
|
||||
ON "public"."horaires" USING gin ("calendrier_circulation" jsonb_path_ops);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "idx_horaires_metadata_gin"
|
||||
ON "public"."horaires" USING gin ("metadata");
|
||||
|
||||
-- 6. Trigger de mise à jour automatique de updated_at
|
||||
CREATE OR REPLACE TRIGGER "tr_horaires_set_updated_at"
|
||||
BEFORE UPDATE ON "public"."horaires"
|
||||
FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"();
|
||||
|
||||
-- 7. Politiques de sécurité RLS
|
||||
ALTER TABLE "public"."horaires" ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "Public read access" ON "public"."horaires";
|
||||
CREATE POLICY "Public read access" ON "public"."horaires" FOR SELECT USING (true);
|
||||
|
||||
DROP POLICY IF EXISTS "Public insert access" ON "public"."horaires";
|
||||
CREATE POLICY "Public insert access" ON "public"."horaires" FOR INSERT WITH CHECK (true);
|
||||
|
||||
DROP POLICY IF EXISTS "Public update access" ON "public"."horaires";
|
||||
CREATE POLICY "Public update access" ON "public"."horaires" FOR UPDATE USING (true);
|
||||
|
||||
DROP POLICY IF EXISTS "Public delete access" ON "public"."horaires";
|
||||
CREATE POLICY "Public delete access" ON "public"."horaires" FOR DELETE USING (true);
|
||||
|
||||
-- Commentaires d'optimisation
|
||||
COMMENT ON TABLE "public"."horaires" IS 'Table UNIQUE et centrale pour stocker l''intégralité des données horaires, dessertes, quais, substitutions, calendrier et temps réel.';
|
||||
COMMENT ON COLUMN "public"."horaires"."gares_desservies" IS 'Liste JSONB complète et ordonnée de tous les arrêts, gares, horaires et quais.';
|
||||
COMMENT ON COLUMN "public"."horaires"."attributions_quais" IS 'Dictionnaire JSONB rapide O(1) { [gare_id]: { quai, quai_reel } } sans jointure.';
|
||||
COMMENT ON COLUMN "public"."horaires"."est_substitution" IS 'Vrai si ce sillon représente un autocar ou train de substitution.';
|
||||
@@ -0,0 +1,252 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Réinitialisation de votre mot de passe - Ferrovia Panel</title>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<style>
|
||||
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||
img { -ms-interpolation-mode: bicubic; border: 0; outline: none; text-decoration: none; }
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
background-color: #f1f5f9;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
color: #1e293b;
|
||||
}
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
background-color: #f1f5f9;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.main {
|
||||
background-color: #ffffff;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border-spacing: 0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #062743 0%, #0e3b62 60%, #1a4e7a 100%);
|
||||
padding: 36px 40px;
|
||||
text-align: left;
|
||||
}
|
||||
.kicker {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
color: #93c5fd;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.brand-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.brand-subtitle {
|
||||
font-size: 13px;
|
||||
color: #cbd5e1;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
.content {
|
||||
padding: 40px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.greeting {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.text {
|
||||
font-size: 15px;
|
||||
color: #334155;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn-container {
|
||||
text-align: center;
|
||||
padding: 24px 0 32px 0;
|
||||
}
|
||||
.button {
|
||||
background-color: #0070f3;
|
||||
background-image: linear-gradient(135deg, #0066cc 0%, #004c99 100%);
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
display: inline-block;
|
||||
box-shadow: 0 4px 12px rgba(0, 102, 204, 0.25);
|
||||
}
|
||||
.token-box {
|
||||
background-color: #f8fafc;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.token-label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #64748b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.token-code {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 4px;
|
||||
color: #0369a1;
|
||||
}
|
||||
.alert-box {
|
||||
background-color: #eff6ff;
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding: 14px 16px;
|
||||
border-radius: 0 6px 6px 0;
|
||||
margin: 24px 0;
|
||||
font-size: 13.5px;
|
||||
color: #1e40af;
|
||||
}
|
||||
.fallback-url {
|
||||
background-color: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
color: #64748b;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.footer a {
|
||||
color: #64748b;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 30px; padding-bottom: 20px;">
|
||||
<!-- CONTENEUR PRINCIPAL -->
|
||||
<table class="main" role="presentation" cellspacing="0" cellpadding="0" border="0">
|
||||
<!-- EN-TÊTE BRANDING -->
|
||||
<tr>
|
||||
<td class="header">
|
||||
<div class="kicker">Console d'exploitation</div>
|
||||
<h1 class="brand-title">FERROVIA PANEL</h1>
|
||||
<p class="brand-subtitle">Gestion régionale et système d'information ferroviaire</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- CONTENU DU MESSAGE -->
|
||||
<tr>
|
||||
<td class="content">
|
||||
<h2 class="greeting">Réinitialisation de votre mot de passe</h2>
|
||||
|
||||
<p class="text">
|
||||
Bonjour,
|
||||
</p>
|
||||
|
||||
<p class="text">
|
||||
Une demande de réinitialisation de mot de passe a été effectuée pour le compte administrateur associé à l'adresse <strong>{{ .Email }}</strong>.
|
||||
</p>
|
||||
|
||||
<p class="text">
|
||||
Cliquez sur le bouton ci-dessous pour choisir votre nouveau mot de passe sécurisé :
|
||||
</p>
|
||||
|
||||
<!-- BOUTON D'ACTION PRINCIPAL -->
|
||||
<div class="btn-container">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ .ConfirmationURL }}" style="height:48px;v-text-anchor:middle;width:260px;" arcsize="15%" stroke="f" fillcolor="#0066cc">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:sans-serif;font-size:15px;font-weight:bold;">
|
||||
Définir mon mot de passe →
|
||||
</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-- -->
|
||||
<a href="{{ .ConfirmationURL }}" class="button" target="_blank">
|
||||
Définir mon nouveau mot de passe →
|
||||
</a>
|
||||
<!--<![endif]-->
|
||||
</div>
|
||||
|
||||
{{ if .Token }}
|
||||
<div class="token-box">
|
||||
<div class="token-label">Code de sécurité à usage unique</div>
|
||||
<div class="token-code">{{ .Token }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="alert-box">
|
||||
<strong>Important :</strong> Ce lien de sécurité est temporaire et expire automatiquement après <strong>1 heure</strong>.
|
||||
</div>
|
||||
|
||||
<p class="text" style="font-size: 13px; color: #64748b; margin-bottom: 6px;">
|
||||
Si le bouton ne fonctionne pas, copiez et collez l'URL suivante dans votre navigateur :
|
||||
</p>
|
||||
<div class="fallback-url">
|
||||
{{ .ConfirmationURL }}
|
||||
</div>
|
||||
|
||||
<hr style="border: none; border-top: 1px solid #e2e8f0; margin: 30px 0 20px 0;" />
|
||||
|
||||
<p class="text" style="font-size: 13px; color: #94a3b8; margin-bottom: 0;">
|
||||
Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet email en toute sécurité. Votre mot de passe actuel restera inchangé et votre compte demeure protégé.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- PIED DE PAGE -->
|
||||
<table role="presentation" width="100%" style="max-width: 600px;" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td class="footer">
|
||||
<p style="margin: 8px 0;">
|
||||
<strong>Ferrovia Panel</strong> • Système d'Exploitation Ferroviaire
|
||||
</p>
|
||||
<p style="margin: 4px 0;">
|
||||
Cet email a été envoyé automatiquement par le serveur d'authentification Supabase. Merci de ne pas y répondre directement.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user