Aller au contenu principal

📐 Base de données — Schéma complet + RLS

PostgreSQL via Supabase. Migration : supabase/migrations/20260406145132_*.sql


Tables

email_analyses

Historique des analyses email par utilisateur.

ColonneTypeDéfautDescription
idUUIDgen_random_uuid()Clé primaire
user_idUUIDFK → auth.users (CASCADE DELETE)
legacy_idBIGINTnullID migration (Date.now())
timestampTIMESTAMPTZnow()Date/heure de l'analyse
email_contentTEXTnullDébut des headers (max 500 chars)
spfTEXT'unknown'Résultat SPF
dkimTEXT'unknown'Résultat DKIM
dmarcTEXT'unknown'Résultat DMARC
ip_reputationINTEGER50Score réputation IP (0–100)
sender_scoreINTEGER50Score expéditeur (0–100)
threat_levelTEXT'low'low / medium / high
domainTEXTnullDomaine expéditeur
ip_addressTEXTnullIP source principale
from_addressTEXTnullAdresse complète expéditeur
subjectTEXTnullSujet de l'email
delivered_toTEXTnullDestinataire
message_idTEXTnullMessage-ID (sans chevrons)
return_pathTEXTnullReturn-Path (sans chevrons)
formatted_dateTEXTnullDate formatée (Europe/Zurich)
ip_reputationsJSONB'[]'Résultats IPQS/AbuseIPDB/VirusTotal
received_chainJSONB'[]'Chaîne SMTP [{from, by}]
marked_spamBOOLEANfalseMarquage manuel spam
created_atTIMESTAMPTZnow()Date d'insertion

Structure ip_reputations (JSONB) :

[{
"ip": "203.0.113.1",
"ipQualityScore": { "fraud_score": 45, "country": "FR", "isp": "Orange SA", "is_vpn": false },
"abuseIPDB": { "abuse_confidence_score": 10, "country": "FR", "total_reports": 2 },
"virusTotal": { "malicious": 0, "suspicious": 0, "harmless": 65, "asn": 3215 }
}]

phone_checks

Historique des vérifications téléphoniques par utilisateur.

ColonneTypeDéfautDescription
idUUIDgen_random_uuid()Clé primaire
user_idUUIDFK → auth.users (CASCADE DELETE)
phone_numberTEXTNuméro saisi (format original)
normalizedTEXTNuméro normalisé (33XXXXXXXXX)
is_blockedBOOLEANfalseBloqué selon ARCEP
match_countINTEGER0Nombre de patterns correspondants
matchesJSONB'[]'Patterns ARCEP déclenchés
timestampTIMESTAMPTZnow()Date de vérification
marked_spamBOOLEANfalseMarquage spam (auto ou manuel)
manually_blockedBOOLEANfalseBlocage manuel par l'utilisateur
community_reportedBOOLEANfalseSignalé à la communauté
check_typeTEXT'phone'phone ou sms
database_infoJSONB''Version et taille base ARCEP
created_atTIMESTAMPTZnow()Date d'insertion

Structure matches (JSONB) :

[{ "name": "Démarchage commercial", "action": "block", "pattern": "336123#####" }]

Structure database_info (JSONB) :

{ "version": "v2026.02.26.17.26", "total_blocked": 14000000 }

community_reports

Signalements communautaires partagés entre tous les utilisateurs.

ColonneTypeDéfautDescription
idUUIDgen_random_uuid()Clé primaire
reporter_idUUIDFK → auth.users (CASCADE DELETE)
report_typeTEXTemail ou phone (CHECK constraint)
identifierTEXTEmail ou numéro signalé
reasonTEXTnullRaison du signalement
report_countINTEGER1Nombre de signalements cumulés
confirmed_spamBOOLEANfalseConfirmé comme spam
created_atTIMESTAMPTZnow()Date création
updated_atTIMESTAMPTZnow()Date mise à jour

Index unique : (report_type, identifier) → upsert avec ON CONFLICT pour incrémenter report_count.


Row Level Security (RLS)

email_analyses et phone_checks

-- Lecture : uniquement ses propres données
CREATE POLICY "Users can view own data"
ON public.email_analyses FOR SELECT TO authenticated
USING (auth.uid() = user_id);

-- Insertion : uniquement pour soi-même
CREATE POLICY "Users can insert own data"
ON public.email_analyses FOR INSERT TO authenticated
WITH CHECK (auth.uid() = user_id);

-- Modification : uniquement ses propres données
CREATE POLICY "Users can update own data"
ON public.email_analyses FOR UPDATE TO authenticated
USING (auth.uid() = user_id);

-- Suppression : uniquement ses propres données
CREATE POLICY "Users can delete own data"
ON public.email_analyses FOR DELETE TO authenticated
USING (auth.uid() = user_id);

community_reports

-- Lecture : accessible à tous les authentifiés
CREATE POLICY "Anyone can view community reports"
ON public.community_reports FOR SELECT TO authenticated
USING (true);

-- Écriture : uniquement par le reporter
CREATE POLICY "Users can insert own reports"
ON public.community_reports FOR INSERT TO authenticated
WITH CHECK (auth.uid() = reporter_id);

CREATE POLICY "Users can update own reports"
ON public.community_reports FOR UPDATE TO authenticated
USING (auth.uid() = reporter_id);

CREATE POLICY "Users can delete own reports"
ON public.community_reports FOR DELETE TO authenticated
USING (auth.uid() = reporter_id);

Realtime

-- Publication Supabase Realtime
ALTER PUBLICATION supabase_realtime ADD TABLE public.community_reports;

Queries fréquentes

// Récupérer analyses email (tri par date, max 200)
const { data } = await supabase
.from('email_analyses')
.select('*')
.order('timestamp', { ascending: false })
.limit(200);

// Marquer comme spam
await supabase
.from('email_analyses')
.update({ marked_spam: true })
.eq('id', id);

// Signalement communautaire (upsert)
await supabase
.from('community_reports')
.upsert({
reporter_id: userId,
report_type: 'phone',
identifier: normalizedNumber,
reason: 'Signalement communautaire',
}, { onConflict: 'report_type,identifier' });

// Suppression de toutes ses analyses
// RLS garantit que seules les lignes user_id = auth.uid() sont affectées
await supabase
.from('email_analyses')
.delete()
.neq('id', '00000000-0000-0000-0000-000000000000');

Suppression en cascade

Toutes les tables ont ON DELETE CASCADE sur user_id → auth.users.
La suppression d'un compte supprime automatiquement toutes ses données associées.


Alias camelCase (compatibilité)

Les hooks useEmailAnalyses.ts et usePhoneChecks.ts définissent une fonction toCompat() qui ajoute des alias camelCase sur les colonnes snake_case de la DB :

DB (snake_case)Alias frontend (camelCase)
email_contentemailContent
ip_reputationipReputation
threat_levelthreatLevel
from_addressfrom
delivered_todeliveredTo
message_idmessageId
ip_reputationsipReputations
received_chainreceivedChain
marked_spammarkedSpam
phone_numberphoneNumber
is_blockedisBlocked
manually_blockedmanuallyBlocked