This commit is contained in:
voxan committed 2026-10-21 22:18:01 +02:00
1 parent 5c43bf9e7c
commit be12cadea6
1 file changed
+280 -249
+280 -249
View File
@@ -1,261 +1,292 @@
-- ===================================================================== -- =====================================================================
-- NetMonster Admin — Schema COMPLET pour INSTALLATION NEUVE -- NetMonster Admin — Schema COMPLET pour INSTALLATION NEUVE
-- Fichier unique : contient tout ce qu'il faut executer une seule fois -- Fichier unique : contient tout ce qu'il faut executer une seule fois
-- sur une base vide pour obtenir un environnement pret a l'emploi. -- sur une base vide pour obtenir un environnement pret a l'emploi.
-- --
-- Regroupe et remplace, dans l'ordre : -- Regroupe et remplace, dans l'ordre :
-- 1. schema_v3_with_radio_params.sql (tables de base + donnees Luxembourg) -- 1. schema_v3_with_radio_params.sql (tables de base + donnees Luxembourg)
-- 2. migration_fix_uniq_cell.sql (deja integre : uniq_cell inclut sub_id2 des la creation) -- 2. migration_fix_uniq_cell.sql (deja integre : uniq_cell inclut sub_id2 des la creation)
-- 3. migration_security_hardening.sql / schema_v3_addendum_login_attempts.sql -- 3. migration_security_hardening.sql / schema_v3_addendum_login_attempts.sql
-- (table login_attempts pour le rate limiting de connexion) -- (table login_attempts pour le rate limiting de connexion)
-- --
-- Usage (installation neuve uniquement) : -- Usage (installation neuve uniquement) :
-- mysql -u netmonster_user -p netmonster < install_full_schema.sql -- mysql -u netmonster_user -p netmonster < install_full_schema.sql
-- --
-- Si vous avez DEJA une base existante avec un schema anterieur, -- Si vous avez DEJA une base existante avec un schema anterieur,
-- N'UTILISEZ PAS ce fichier : utilisez plutot, selon votre situation : -- N'UTILISEZ PAS ce fichier : utilisez plutot, selon votre situation :
-- - migration_fix_uniq_cell.sql (si votre contrainte uniq_cell -- - migration_fix_uniq_cell.sql (si votre contrainte uniq_cell
-- ne contient pas encore sub_id2) -- ne contient pas encore sub_id2)
-- - migration_security_hardening.sql (si vous n'avez pas encore la -- - migration_security_hardening.sql (si vous n'avez pas encore la
-- table login_attempts) -- table login_attempts)
-- Ces deux fichiers de migration restent disponibles independamment -- Ces deux fichiers de migration restent disponibles independamment
-- dans ce dossier pour les mises a jour d'une base deja en production, -- dans ce dossier pour les mises a jour d'une base deja en production,
-- sans risque de perte de donnees. -- sans risque de perte de donnees.
-- --
-- Toutes les instructions CREATE TABLE utilisent IF NOT EXISTS et les -- Toutes les instructions CREATE TABLE utilisent IF NOT EXISTS et les
-- INSERT de reference utilisent ON DUPLICATE KEY UPDATE : ce fichier -- INSERT de reference utilisent ON DUPLICATE KEY UPDATE : ce fichier
-- est idempotent, l'executer une seconde fois par erreur ne casse rien -- est idempotent, l'executer une seconde fois par erreur ne casse rien
-- et ne duplique aucune donnee. -- et ne duplique aucune donnee.
-- ===================================================================== -- =====================================================================
-- ---------------------------------------------------------------------
-- 1. PAYS
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS countries (
id INT AUTO_INCREMENT PRIMARY KEY,
mcc VARCHAR(3) NOT NULL UNIQUE,
iso2 VARCHAR(2) NOT NULL,
name VARCHAR(100) NOT NULL,
default_lat DOUBLE NULL,
default_lon DOUBLE NULL,
default_zoom TINYINT NULL DEFAULT 9,
cadastre_wms_url VARCHAR(255) NULL,
cadastre_wms_layer VARCHAR(100) NULL,
cadastre_attribution_html VARCHAR(500) NULL,
cadastre_srs VARCHAR(20) NULL DEFAULT 'EPSG:3857'
) ENGINE=InnoDB;
INSERT INTO countries (mcc, iso2, name, default_lat, default_lon, default_zoom,
cadastre_wms_url, cadastre_wms_layer, cadastre_attribution_html, cadastre_srs)
VALUES (
'270','LU','Luxembourg', 49.8153, 6.1296, 9,
'https://wms.geoportail.lu/public_map_layers/service',
'359',
'Cadastre : © Administration du Cadastre et de la Topographie (ACT)',
'EPSG:3857'
)
ON DUPLICATE KEY UPDATE name = VALUES(name);
-- ---------------------------------------------------------------------
-- 2. OPERATEURS
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS operators (
id INT AUTO_INCREMENT PRIMARY KEY,
country_id INT NOT NULL,
mcc VARCHAR(3) NOT NULL,
mnc VARCHAR(2) NOT NULL,
brand VARCHAR(50) NOT NULL,
full_name VARCHAR(100),
UNIQUE KEY plmn (mcc, mnc),
FOREIGN KEY (country_id) REFERENCES countries(id)
) ENGINE=InnoDB;
INSERT INTO operators (country_id, mcc, mnc, brand, full_name)
SELECT c.id, '270','01','POST', 'POST Luxembourg' FROM countries c WHERE c.mcc='270'
UNION ALL SELECT c.id, '270','05','LoL', 'Luxembourg Online S.A.' FROM countries c WHERE c.mcc='270'
UNION ALL SELECT c.id, '270','77','Tango', 'Tango SA' FROM countries c WHERE c.mcc='270'
UNION ALL SELECT c.id, '270','99','Orange', 'Orange Communications Luxembourg S.A.' FROM countries c WHERE c.mcc='270'
ON DUPLICATE KEY UPDATE brand = VALUES(brand);
-- ---------------------------------------------------------------------
-- 3. SITES = lieu physique (pylone, toit...). Regroupe plusieurs cellules.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS sites (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
country_id INT NOT NULL,
operator_id INT NOT NULL,
lat DOUBLE NOT NULL,
lon DOUBLE NOT NULL,
location_desc VARCHAR(255),
address_desc VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100),
FOREIGN KEY (country_id) REFERENCES countries(id),
FOREIGN KEY (operator_id) REFERENCES operators(id),
INDEX idx_geo (lat, lon)
) ENGINE=InnoDB;
-- ---------------------------------------------------------------------
-- 4. CELLS = table finale VALIDEE. Une ligne = une cellule/secteur radio.
-- Inclut les parametres radio detailles (bande, frequence, azimut,
-- largeur de bande), stockes de facon structuree plutot que noyes
-- dans un texte libre.
--
-- uniq_cell inclut DEJA sub_id2 (eNB/RNC) des la creation : deux eNB
-- distincts peuvent legitimement partager le meme cell_id + area_code
-- (le CID local est souvent un petit compteur par eNB), ce n'est donc
-- pas un doublon. Le vrai doublon est : meme eNB + meme Cell ID + meme
-- area_code. (Correctif anciennement livre a part dans
-- migration_fix_uniq_cell.sql, integre ici directement.)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS cells (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
site_id BIGINT NOT NULL,
tech ENUM('2G','3G','4G','5G','CDMA') NOT NULL,
mcc VARCHAR(3) NOT NULL,
mnc VARCHAR(2) NOT NULL,
cell_id BIGINT NOT NULL,
area_code INT NOT NULL,
sub_id INT, -- BSIC / PSC / PCI
sub_id2 INT, -- RNC / eNB
band VARCHAR(20), -- ex: "B7", "N78"
frequency VARCHAR(20), -- ex: "700MHz"
azimuth SMALLINT, -- 0-359 degres
bandwidth VARCHAR(20), -- ex: "10MHz"
arfcn BIGINT,
validated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
validated_by VARCHAR(100),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
UNIQUE KEY uniq_cell (tech, mcc, mnc, sub_id2, cell_id, area_code),
INDEX idx_operator_tech (mcc, mnc, tech),
INDEX idx_band (band)
) ENGINE=InnoDB;
-- ---------------------------------------------------------------------
-- 5. STAGING = zone de quarantaine. Rien n'entre dans sites/cells sans
-- passer par ici et etre valide manuellement dans l'admin.
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS staging_batches (
id INT AUTO_INCREMENT PRIMARY KEY,
label VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS staging_cells (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
import_id INT,
batch_id INT NULL,
tech ENUM('2G','3G','4G','5G','CDMA') NOT NULL,
mcc VARCHAR(3) NOT NULL,
mnc VARCHAR(2) NOT NULL,
operator_guess INT,
cell_id BIGINT NOT NULL,
area_code INT NOT NULL,
sub_id INT,
sub_id2 INT,
band VARCHAR(20),
frequency VARCHAR(20),
azimuth SMALLINT,
bandwidth VARCHAR(20),
address_desc VARCHAR(255),
lat DOUBLE,
lon DOUBLE,
location_desc VARCHAR(255),
arfcn BIGINT,
raw_line TEXT,
source ENUM('ntm_import','manual_form','cellmapper_import') NOT NULL DEFAULT 'ntm_import',
possible_dup_of BIGINT NULL,
status ENUM('pending','validated','rejected','edited') DEFAULT 'pending',
submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
reviewed_at DATETIME NULL,
reviewed_by VARCHAR(100) NULL,
reject_reason VARCHAR(255) NULL,
FOREIGN KEY (operator_guess) REFERENCES operators(id),
FOREIGN KEY (possible_dup_of) REFERENCES cells(id),
FOREIGN KEY (batch_id) REFERENCES staging_batches(id),
INDEX idx_status (status),
INDEX idx_source (source)
) ENGINE=InnoDB;
-- Note : la valeur 'cellmapper_import' a ete ajoutee a l'ENUM `source`
-- pour tracer les cellules completees/creees via admin/cellmapper_import.php
-- (voir aussi lib none dedie : la logique vit directement dans la page).
-- ---------------------------------------------------------------------
-- 6. IMPORTS = historique des depots de fichiers .ntm
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS imports (
id INT AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) NOT NULL,
country_id INT,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
processed_at DATETIME NULL,
total_lines INT DEFAULT 0,
staged_count INT DEFAULT 0,
error_count INT DEFAULT 0,
status ENUM('uploaded','processing','done','error') DEFAULT 'uploaded',
FOREIGN KEY (country_id) REFERENCES countries(id)
) ENGINE=InnoDB;
-- ---------------------------------------------------------------------
-- 7. COMPTES ADMIN (separation stricte admin / public)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS admin_users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin','reviewer') DEFAULT 'reviewer',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- ---------------------------------------------------------------------
-- 8. RATE LIMITING DE CONNEXION (durcissement securite du 20/09/2026)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS login_attempts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username_normalized VARCHAR(190) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
attempted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
success TINYINT(1) NOT NULL DEFAULT 0,
turnstile_verified TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY idx_username_ip_time (username_normalized, ip_address, attempted_at),
KEY idx_ip_time (ip_address, attempted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
-- 1. PAYS -- 9. TABLES ADDITIONNELLES (Couches, Backups, Historique d'imports)
-- --------------------------------------------------------------------- -- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS countries (
id INT AUTO_INCREMENT PRIMARY KEY, -- Pour admin/map_layers.php
mcc VARCHAR(3) NOT NULL UNIQUE, CREATE TABLE IF NOT EXISTS map_layers (
iso2 VARCHAR(2) NOT NULL, id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
default_lat DOUBLE NULL, layer_key VARCHAR(50) NOT NULL,
default_lon DOUBLE NULL, color VARCHAR(7) NOT NULL,
default_zoom TINYINT NULL DEFAULT 9, is_visible TINYINT(1) DEFAULT 1,
cadastre_wms_url VARCHAR(255) NULL, sort_order INT DEFAULT 0,
cadastre_wms_layer VARCHAR(100) NULL, description TEXT
cadastre_attribution_html VARCHAR(500) NULL,
cadastre_srs VARCHAR(20) NULL DEFAULT 'EPSG:3857'
) ENGINE=InnoDB; ) ENGINE=InnoDB;
INSERT INTO countries (mcc, iso2, name, default_lat, default_lon, default_zoom, -- Pour admin/backup.php
cadastre_wms_url, cadastre_wms_layer, cadastre_attribution_html, cadastre_srs) CREATE TABLE IF NOT EXISTS backup_log (
VALUES ( id INT AUTO_INCREMENT PRIMARY KEY,
'270','LU','Luxembourg', 49.8153, 6.1296, 9, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
'https://wms.geoportail.lu/public_map_layers/service', admin_username VARCHAR(100) NOT NULL,
'359', label VARCHAR(255) NOT NULL,
'Cadastre : © Administration du Cadastre et de la Topographie (ACT)', note TEXT,
'EPSG:3857' size_bytes_reported BIGINT
)
ON DUPLICATE KEY UPDATE name = VALUES(name);
-- ---------------------------------------------------------------------
-- 2. OPERATEURS
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS operators (
id INT AUTO_INCREMENT PRIMARY KEY,
country_id INT NOT NULL,
mcc VARCHAR(3) NOT NULL,
mnc VARCHAR(2) NOT NULL,
brand VARCHAR(50) NOT NULL,
full_name VARCHAR(100),
UNIQUE KEY plmn (mcc, mnc),
FOREIGN KEY (country_id) REFERENCES countries(id)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
INSERT INTO operators (country_id, mcc, mnc, brand, full_name) -- Pour admin/import_history.php
SELECT c.id, '270','01','POST', 'POST Luxembourg' FROM countries c WHERE c.mcc='270' CREATE TABLE IF NOT EXISTS import_batches (
UNION ALL SELECT c.id, '270','05','LoL', 'Luxembourg Online S.A.' FROM countries c WHERE c.mcc='270' id INT AUTO_INCREMENT PRIMARY KEY,
UNION ALL SELECT c.id, '270','77','Tango', 'Tango SA' FROM countries c WHERE c.mcc='270' filename VARCHAR(255) NOT NULL,
UNION ALL SELECT c.id, '270','99','Orange', 'Orange Communications Luxembourg S.A.' FROM countries c WHERE c.mcc='270' imported_at DATETIME DEFAULT CURRENT_TIMESTAMP,
ON DUPLICATE KEY UPDATE brand = VALUES(brand); imported_by VARCHAR(100),
status VARCHAR(50),
rows_total INT DEFAULT 0,
-- --------------------------------------------------------------------- rows_imported INT DEFAULT 0,
-- 3. SITES = lieu physique (pylone, toit...). Regroupe plusieurs cellules. rows_rejected INT DEFAULT 0,
-- --------------------------------------------------------------------- country_id INT,
CREATE TABLE IF NOT EXISTS sites ( FOREIGN KEY (country_id) REFERENCES countries(id)
id BIGINT AUTO_INCREMENT PRIMARY KEY,
country_id INT NOT NULL,
operator_id INT NOT NULL,
lat DOUBLE NOT NULL,
lon DOUBLE NOT NULL,
location_desc VARCHAR(255),
address_desc VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100),
FOREIGN KEY (country_id) REFERENCES countries(id),
FOREIGN KEY (operator_id) REFERENCES operators(id),
INDEX idx_geo (lat, lon)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS import_errors (
-- --------------------------------------------------------------------- id INT AUTO_INCREMENT PRIMARY KEY,
-- 4. CELLS = table finale VALIDEE. Une ligne = une cellule/secteur radio. batch_id INT NOT NULL,
-- Inclut les parametres radio detailles (bande, frequence, azimut, line_number INT,
-- largeur de bande), stockes de facon structuree plutot que noyes message TEXT,
-- dans un texte libre. raw_line TEXT,
-- FOREIGN KEY (batch_id) REFERENCES import_batches(id) ON DELETE CASCADE
-- uniq_cell inclut DEJA sub_id2 (eNB/RNC) des la creation : deux eNB
-- distincts peuvent legitimement partager le meme cell_id + area_code
-- (le CID local est souvent un petit compteur par eNB), ce n'est donc
-- pas un doublon. Le vrai doublon est : meme eNB + meme Cell ID + meme
-- area_code. (Correctif anciennement livre a part dans
-- migration_fix_uniq_cell.sql, integre ici directement.)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS cells (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
site_id BIGINT NOT NULL,
tech ENUM('2G','3G','4G','5G','CDMA') NOT NULL,
mcc VARCHAR(3) NOT NULL,
mnc VARCHAR(2) NOT NULL,
cell_id BIGINT NOT NULL,
area_code INT NOT NULL,
sub_id INT, -- BSIC / PSC / PCI
sub_id2 INT, -- RNC / eNB
band VARCHAR(20), -- ex: "B7", "N78"
frequency VARCHAR(20), -- ex: "700MHz"
azimuth SMALLINT, -- 0-359 degres
bandwidth VARCHAR(20), -- ex: "10MHz"
arfcn BIGINT,
validated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
validated_by VARCHAR(100),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
UNIQUE KEY uniq_cell (tech, mcc, mnc, sub_id2, cell_id, area_code),
INDEX idx_operator_tech (mcc, mnc, tech),
INDEX idx_band (band)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
-- =====================================================================
-- --------------------------------------------------------------------- -- Fin du schema. A ce stade, la base contient :
-- 5. STAGING = zone de quarantaine. Rien n'entre dans sites/cells sans -- - Luxembourg (MCC 270) + POST/LoL/Tango/Orange pre-configures
-- passer par ici et etre valide manuellement dans l'admin. -- - Toutes les tables necessaires au fonctionnement complet de
-- --------------------------------------------------------------------- -- l'admin (import .ntm, import CellMapper, validation staging,
CREATE TABLE IF NOT EXISTS staging_batches ( -- edition de sites, export, rate limiting login)
id INT AUTO_INCREMENT PRIMARY KEY, -- - AUCUN compte administrateur : creez-en un manuellement, voir
label VARCHAR(255), -- INSTALL.md ou INSTALL_TEST_VM.md, section "Creation du compte
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- administrateur".
created_by VARCHAR(100)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS staging_cells (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
import_id INT,
batch_id INT NULL,
tech ENUM('2G','3G','4G','5G','CDMA') NOT NULL,
mcc VARCHAR(3) NOT NULL,
mnc VARCHAR(2) NOT NULL,
operator_guess INT,
cell_id BIGINT NOT NULL,
area_code INT NOT NULL,
sub_id INT,
sub_id2 INT,
band VARCHAR(20),
frequency VARCHAR(20),
azimuth SMALLINT,
bandwidth VARCHAR(20),
address_desc VARCHAR(255),
lat DOUBLE,
lon DOUBLE,
location_desc VARCHAR(255),
arfcn BIGINT,
raw_line TEXT,
source ENUM('ntm_import','manual_form','cellmapper_import') NOT NULL DEFAULT 'ntm_import',
possible_dup_of BIGINT NULL,
status ENUM('pending','validated','rejected','edited') DEFAULT 'pending',
submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
reviewed_at DATETIME NULL,
reviewed_by VARCHAR(100) NULL,
reject_reason VARCHAR(255) NULL,
FOREIGN KEY (operator_guess) REFERENCES operators(id),
FOREIGN KEY (possible_dup_of) REFERENCES cells(id),
FOREIGN KEY (batch_id) REFERENCES staging_batches(id),
INDEX idx_status (status),
INDEX idx_source (source)
) ENGINE=InnoDB;
-- Note : la valeur 'cellmapper_import' a ete ajoutee a l'ENUM `source`
-- pour tracer les cellules completees/creees via admin/cellmapper_import.php
-- (voir aussi lib none dedie : la logique vit directement dans la page).
-- ---------------------------------------------------------------------
-- 6. IMPORTS = historique des depots de fichiers .ntm
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS imports (
id INT AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) NOT NULL,
country_id INT,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
processed_at DATETIME NULL,
total_lines INT DEFAULT 0,
staged_count INT DEFAULT 0,
error_count INT DEFAULT 0,
status ENUM('uploaded','processing','done','error') DEFAULT 'uploaded',
FOREIGN KEY (country_id) REFERENCES countries(id)
) ENGINE=InnoDB;
-- ---------------------------------------------------------------------
-- 7. COMPTES ADMIN (separation stricte admin / public)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS admin_users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin','reviewer') DEFAULT 'reviewer',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- ---------------------------------------------------------------------
-- 8. RATE LIMITING DE CONNEXION (durcissement securite du 20/09/2026)
--
-- Compteur d'echecs de connexion stocke en base (et non en session
-- PHP), afin qu'un attaquant ne puisse pas remettre son compteur a
-- zero en vidant ses cookies ou en ouvrant une navigation privee.
-- Utilise par admin/admin_auth.php (is_login_locked_out,
-- record_login_attempt) et admin/login.php.
--
-- Deux seuils appliques cote PHP a partir de cette meme table :
-- - 5 echecs sur le couple (identifiant, IP) en 15 min -> verrouillage cible
-- - 30 echecs sur l'IP seule (tous identifiants confondus) en 15 min
-- -> verrouillage large, plus permissif pour ne pas bloquer tout
-- le monde depuis une IP partagee (ex: sortie Tailscale commune)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS login_attempts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username_normalized VARCHAR(190) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
attempted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
success TINYINT(1) NOT NULL DEFAULT 0,
turnstile_verified TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY idx_username_ip_time (username_normalized, ip_address, attempted_at),
KEY idx_ip_time (ip_address, attempted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Purge recommandee via cron (pas dans ce fichier), pour eviter que
-- la table ne grossisse indefiniment :
-- DELETE FROM login_attempts WHERE attempted_at < NOW() - INTERVAL 30 DAY;
-- =====================================================================
-- Fin du schema. A ce stade, la base contient :
-- - Luxembourg (MCC 270) + POST/LoL/Tango/Orange pre-configures
-- - Toutes les tables necessaires au fonctionnement complet de
-- l'admin (import .ntm, import CellMapper, validation staging,
-- edition de sites, export, rate limiting login)
-- - AUCUN compte administrateur : creez-en un manuellement, voir
-- INSTALL.md ou INSTALL_TEST_VM.md, section "Creation du compte
-- administrateur".
-- ===================================================================== -- =====================================================================