diff --git a/sql/install_full_schema.sql b/sql/install_full_schema.sql new file mode 100644 index 0000000..94c01c4 --- /dev/null +++ b/sql/install_full_schema.sql @@ -0,0 +1,261 @@ +-- ===================================================================== +-- 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) +-- +-- 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". +-- ===================================================================== \ No newline at end of file diff --git a/sql/migration_add_missing_tables.sql b/sql/migration_add_missing_tables.sql new file mode 100644 index 0000000..92ea5f9 --- /dev/null +++ b/sql/migration_add_missing_tables.sql @@ -0,0 +1,108 @@ +-- admin/sql/migration_add_missing_tables.sql +-- +-- Corrige un oubli de livraison : plusieurs pages admin referencent +-- des tables qui n'ont jamais ete creees dans install_full_schema.sql +-- ni dans aucune migration precedente, causant des erreurs 500 +-- (SQLSTATE 42S02 "Base table or view not found") des l'ouverture : +-- - admin/backup.php -> backup_log +-- - admin/map_layers.php -> map_layers +-- - admin/audit_log.php -> audit_log +-- - admin/import_history.php -> import_batches, import_errors +-- +-- A executer sur TOUTE base existante (prod ou test) : +-- mysql -u netmonster_user -p netmonster < migration_add_missing_tables.sql +-- +-- Idempotent (IF NOT EXISTS) : sans risque a rejouer. + +-- --------------------------------------------------------------------- +-- Utilisee par admin/backup.php (journal des sauvegardes effectuees +-- manuellement ou par cron, aucun contenu de sauvegarde n'est stocke +-- ici, uniquement des metadonnees). +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS backup_log ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + admin_username VARCHAR(100) NOT NULL, + label VARCHAR(255) NOT NULL, + note TEXT NULL, + size_bytes_reported BIGINT UNSIGNED NULL, + PRIMARY KEY (id), + KEY idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- --------------------------------------------------------------------- +-- Utilisee par admin/map_layers.php (configuration des couches +-- cartographiques affichees sur la carte : nom, couleur, visibilite, +-- ordre d'affichage). +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS map_layers ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + layer_key VARCHAR(100) NOT NULL UNIQUE, + color CHAR(7) NOT NULL DEFAULT '#3388ff', + is_visible TINYINT(1) NOT NULL DEFAULT 1, + sort_order INT NOT NULL DEFAULT 0, + description VARCHAR(255) NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- --------------------------------------------------------------------- +-- Utilisee par admin/audit_log.php (historique en lecture seule des +-- actions administratives). Cette table n'est actuellement remplie par +-- AUCUNE page : voir la note importante en bas de fichier. +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS audit_log ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + admin_username VARCHAR(100) NOT NULL, + action VARCHAR(50) NOT NULL, + entity_type VARCHAR(50) NOT NULL, + entity_id BIGINT NULL, + details TEXT NULL, + PRIMARY KEY (id), + KEY idx_created_at (created_at), + KEY idx_entity (entity_type, entity_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- --------------------------------------------------------------------- +-- Utilisees par admin/import_history.php. Distinctes de la table +-- `imports` (deja existante, alimentee par lib/ntm_import_to_staging.php +-- et utilisee par admin/import_ntm.php) : import_batches/import_errors +-- ne sont actuellement alimentees par AUCUNE page non plus, meme +-- probleme que audit_log (voir note importante ci-dessous). +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS import_batches ( + id INT AUTO_INCREMENT PRIMARY KEY, + filename VARCHAR(255) NOT NULL, + country_id INT NULL, + imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + imported_by VARCHAR(100) NOT NULL, + status ENUM('success','partial','failed') NOT NULL DEFAULT 'success', + rows_total INT NOT NULL DEFAULT 0, + rows_imported INT NOT NULL DEFAULT 0, + rows_rejected INT NOT NULL DEFAULT 0, + FOREIGN KEY (country_id) REFERENCES countries(id), + KEY idx_imported_at (imported_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS import_errors ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + batch_id INT NOT NULL, + line_number INT NOT NULL, + message VARCHAR(500) NOT NULL, + raw_line TEXT NULL, + PRIMARY KEY (id), + FOREIGN KEY (batch_id) REFERENCES import_batches(id) ON DELETE CASCADE, + KEY idx_batch (batch_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- NOTE IMPORTANTE : ces 4 tables (backup_log, map_layers, audit_log, +-- import_batches/import_errors) resoudront les erreurs 500 (les pages +-- pourront s'afficher), mais audit_log, import_batches et import_errors +-- resteront VIDES tant qu'aucune page n'insere explicitement dedans a +-- chaque action concernee. Cette instrumentation n'a jamais ete ecrite : +-- ces pages de consultation ont ete livrees en supposant a tort que +-- d'autres pages les alimentaient deja. Une correction complementaire +-- sera necessaire si vous voulez que ces journaux soient utiles au +-- quotidien (par exemple: brancher import_history.php sur la table +-- `imports` deja existante et alimentee par import_ntm.php, plutot que +-- sur import_batches qui fait doublon et n'est jamais rempli). \ No newline at end of file diff --git a/sql/migration_fix_uniq_cell.sql b/sql/migration_fix_uniq_cell.sql new file mode 100644 index 0000000..5e16089 --- /dev/null +++ b/sql/migration_fix_uniq_cell.sql @@ -0,0 +1,31 @@ +-- migration_fix_uniq_cell.sql +-- Corrige la contrainte unique de la table `cells` pour inclure l'eNB/RNC +-- (sub_id2), evitant les faux positifs de duplication quand deux eNB +-- differents partagent par coincidence le meme Cell ID + area_code. +-- +-- Erreur corrigee : +-- SQLSTATE[23000]: Integrity constraint violation: 1062 +-- Duplicate entry 'xxx' for key 'uniq_cell' +-- +-- A executer sur la base de production avant de redeployer edit_site.php +-- si celui-ci permet desormais d'ajouter des cellules a un eNB different +-- partageant un Cell ID deja utilise ailleurs. + +-- 1) Verification prealable : recherche de vrais doublons (meme eNB, meme +-- Cell ID, meme area_code) qui bloqueraient la creation du nouvel index. +-- Si cette requete renvoie des lignes, il faut les corriger/supprimer +-- manuellement AVANT d'executer l'ALTER TABLE ci-dessous. +SELECT tech, mcc, mnc, sub_id2, cell_id, area_code, COUNT(*) AS doublons +FROM cells +GROUP BY tech, mcc, mnc, sub_id2, cell_id, area_code +HAVING COUNT(*) > 1; + +-- 2) Si la requete ci-dessus ne renvoie aucune ligne, appliquer la migration : +ALTER TABLE cells DROP INDEX uniq_cell; +ALTER TABLE cells ADD UNIQUE KEY uniq_cell (tech, mcc, mnc, sub_id2, cell_id, area_code); + +-- 3) Meme correction sur staging_cells si une contrainte equivalente y existe, +-- pour eviter le meme blocage lors de l'import initial (avant validation). +-- Decommentez si applicable a votre schema : +-- ALTER TABLE staging_cells DROP INDEX uniq_cell; +-- ALTER TABLE staging_cells ADD UNIQUE KEY uniq_cell (tech, mcc, mnc, sub_id2, cell_id, area_code); diff --git a/sql/migration_security_hardening.sql b/sql/migration_security_hardening.sql new file mode 100644 index 0000000..5eb7cce --- /dev/null +++ b/sql/migration_security_hardening.sql @@ -0,0 +1,62 @@ +-- migration_security_hardening.sql +-- +-- Migration de durcissement securite suite a l'audit du 20/09/2026, +-- POUR UNE BASE DEJA EN PRODUCTION (schema anterieur a la fusion en +-- install_full_schema.sql). Si vous demarrez une installation NEUVE, +-- n'utilisez PAS ce fichier : utilisez install_full_schema.sql qui +-- inclut deja tout ce qui suit. +-- +-- Contenu : +-- 1. Table login_attempts pour un rate limiting stocke en base, +-- independant de la session PHP du visiteur (donc non +-- contournable en vidant les cookies ou en ouvrant une navigation +-- privee). Utilisee par admin/admin_auth.php et admin/login.php. +-- 2. Ajout de la valeur 'cellmapper_import' a l'ENUM `source` de +-- staging_cells : admin/cellmapper_import.php insere des lignes +-- avec source = "cellmapper_import", ce qui echoue (ou tronque +-- silencieusement selon le mode strict SQL) si l'ENUM d'origine ne +-- contient que ('ntm_import','manual_form'). Sans ce correctif, +-- tout import CellMapper sur une base anterieure a la fusion du +-- schema echoue en base malgre un code PHP fonctionnel. +-- +-- Les roles admin_users (admin/reviewer) sont volontairement REPORTES +-- a une migration future : tant qu'un seul compte administrateur est +-- utilise, ajouter une colonne role non appliquee partout donnerait +-- une fausse impression de securite sans reel controle d'acces. +-- +-- A executer une seule fois sur la base existante : +-- mysql --defaults-extra-file=/etc/netmonster/.my.cnf netmonster < migration_security_hardening.sql + +-- --------------------------------------------------------------------- +-- 1. Rate limiting de connexion +-- --------------------------------------------------------------------- +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 cette migration), pour eviter +-- que la table ne grossisse indefiniment : +-- DELETE FROM login_attempts WHERE attempted_at < NOW() - INTERVAL 30 DAY; + + +-- --------------------------------------------------------------------- +-- 2. Ajout de 'cellmapper_import' a l'ENUM source de staging_cells +-- +-- MODIFY COLUMN reconstruit l'ENUM avec la valeur supplementaire, sans +-- toucher aux lignes existantes (leurs valeurs actuelles 'ntm_import' +-- et 'manual_form' restent valides et inchangees dans le nouvel ENUM). +-- Operation instantanee ou quasi-instantanee selon la version +-- MySQL/MariaDB (pas de reecriture de table necessaire pour un ajout +-- de valeur en fin d'ENUM sur les versions recentes). +-- --------------------------------------------------------------------- +ALTER TABLE staging_cells + MODIFY COLUMN source ENUM('ntm_import','manual_form','cellmapper_import') + NOT NULL DEFAULT 'ntm_import'; \ No newline at end of file