-- ===================================================================== -- NetMonster Admin — Schema COMPLET pour INSTALLATION NEUVE -- Fichier unique : contient tout ce qu'il faut executer une seule fois -- sur une base vide pour obtenir un environnement pret a l'emploi. -- -- Regroupe et remplace, dans l'ordre : -- 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) -- 3. migration_security_hardening.sql / schema_v3_addendum_login_attempts.sql -- (table login_attempts pour le rate limiting de connexion) -- -- Usage (installation neuve uniquement) : -- mysql -u netmonster_user -p netmonster < install_full_schema.sql -- -- Si vous avez DEJA une base existante avec un schema anterieur, -- N'UTILISEZ PAS ce fichier : utilisez plutot, selon votre situation : -- - migration_fix_uniq_cell.sql (si votre contrainte uniq_cell -- ne contient pas encore sub_id2) -- - migration_security_hardening.sql (si vous n'avez pas encore la -- table login_attempts) -- Ces deux fichiers de migration restent disponibles independamment -- dans ce dossier pour les mises a jour d'une base deja en production, -- sans risque de perte de donnees. -- -- Toutes les instructions CREATE TABLE utilisent IF NOT EXISTS et les -- INSERT de reference utilisent ON DUPLICATE KEY UPDATE : ce fichier -- est idempotent, l'executer une seconde fois par erreur ne casse rien -- 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; -- --------------------------------------------------------------------- -- 9. TABLES ADDITIONNELLES (Couches, Backups, Historique d'imports) -- --------------------------------------------------------------------- -- Pour admin/map_layers.php CREATE TABLE IF NOT EXISTS map_layers ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, layer_key VARCHAR(50) NOT NULL, color VARCHAR(7) NOT NULL, is_visible TINYINT(1) DEFAULT 1, sort_order INT DEFAULT 0, description TEXT ) ENGINE=InnoDB; -- Pour admin/backup.php CREATE TABLE IF NOT EXISTS backup_log ( id INT AUTO_INCREMENT PRIMARY KEY, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, admin_username VARCHAR(100) NOT NULL, label VARCHAR(255) NOT NULL, note TEXT, size_bytes_reported BIGINT ) ENGINE=InnoDB; -- Pour admin/import_history.php CREATE TABLE IF NOT EXISTS import_batches ( id INT AUTO_INCREMENT PRIMARY KEY, filename VARCHAR(255) NOT NULL, imported_at DATETIME DEFAULT CURRENT_TIMESTAMP, imported_by VARCHAR(100), status VARCHAR(50), rows_total INT DEFAULT 0, rows_imported INT DEFAULT 0, rows_rejected INT DEFAULT 0, country_id INT, FOREIGN KEY (country_id) REFERENCES countries(id) ) ENGINE=InnoDB; CREATE TABLE IF NOT EXISTS import_errors ( id INT AUTO_INCREMENT PRIMARY KEY, batch_id INT NOT NULL, line_number INT, message TEXT, raw_line TEXT, FOREIGN KEY (batch_id) REFERENCES import_batches(id) ON DELETE CASCADE ) ENGINE=InnoDB; -- ===================================================================== -- 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". -- =====================================================================