Compare commits
3
Commits
3f142a5388
...
be12cadea6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be12cadea6 | ||
|
|
5c43bf9e7c | ||
|
|
0c8d9cba6d |
No files matched your search
+14
-11
@@ -10,24 +10,24 @@
|
|||||||
* une tache cron/systemd timer sur le serveur.
|
* une tache cron/systemd timer sur le serveur.
|
||||||
*
|
*
|
||||||
* Regle de securite appliquee ici :
|
* Regle de securite appliquee ici :
|
||||||
* - Pas de shell_exec()/exec()/proc_open() sur une entree utilisateur.
|
* - Pas de shell_exec()/exec()/proc_open() sur une entree utilisateur.
|
||||||
* - Pas de mot de passe DB affiche : on reference une variable
|
* - Pas de mot de passe DB affiche : on reference une variable
|
||||||
* d'environnement ou un fichier .my.cnf externe.
|
* d'environnement ou un fichier .my.cnf externe.
|
||||||
* - Seule action en base : enregistrement de metadonnees de sauvegarde
|
* - Seule action en base : enregistrement de metadonnees de sauvegarde
|
||||||
* (date, taille, note) dans une table de suivi, jamais le contenu.
|
* (date, taille, note) dans une table de suivi, jamais le contenu.
|
||||||
*
|
*
|
||||||
* CORRIGE (20/09/2026, suite a erreur 500 en usage reel) :
|
* CORRIGE (20/09/2026, suite a erreur 500 en usage reel) :
|
||||||
* - require_once _nav.php manquait, provoquant une erreur fatale
|
* - require_once _nav.php manquait, provoquant une erreur fatale
|
||||||
* "Call to undefined function render_admin_nav()".
|
* "Call to undefined function render_admin_nav()".
|
||||||
* - La table backup_log necessaire a cette page n'existait pas :
|
* - La table backup_log necessaire a cette page n'existait pas :
|
||||||
* voir admin/sql/migration_add_missing_tables.sql.
|
* voir admin/sql/migration_add_missing_tables.sql.
|
||||||
*
|
*
|
||||||
* Corrige suite a l'audit du 20/09/2026 : le detail technique des
|
* Corrige suite a l'audit du 20/09/2026 : le detail technique des
|
||||||
* exceptions n'est plus affiche a l'ecran ; il est journalise via
|
* exceptions n'est plus affiche a l'ecran ; il est journalise via
|
||||||
* error_log().
|
* error_log().
|
||||||
*
|
*
|
||||||
* Hypothese de schema : backup_log(id, created_at, admin_username,
|
* Hypothese de schema : backup_log(id, created_at, admin_username,
|
||||||
* label, note, size_bytes_reported)
|
* label, note, size_bytes_reported)
|
||||||
*/
|
*/
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
@@ -54,7 +54,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
} else {
|
} else {
|
||||||
$label = trim((string)($_POST['label'] ?? ''));
|
$label = trim((string)($_POST['label'] ?? ''));
|
||||||
$note = trim((string)($_POST['note'] ?? ''));
|
$note = trim((string)($_POST['note'] ?? ''));
|
||||||
$sizeBytes = filter_input(INPUT_POST, 'size_bytes', FILTER_VALIDATE_INT) ?: null;
|
|
||||||
|
// Correction : Accepte les grands nombres (BigInt) pour les gros backups
|
||||||
|
$rawSize = $_POST['size_bytes'] ?? '';
|
||||||
|
$sizeBytes = (ctype_digit((string)$rawSize)) ? $rawSize : null;
|
||||||
|
|
||||||
if ($label === '') {
|
if ($label === '') {
|
||||||
$feedback = 'Le libelle de la sauvegarde est obligatoire.';
|
$feedback = 'Le libelle de la sauvegarde est obligatoire.';
|
||||||
|
|||||||
+60
-59
@@ -9,8 +9,8 @@
|
|||||||
* suppression d'une couche.
|
* suppression d'une couche.
|
||||||
*
|
*
|
||||||
* Hypothese de schema :
|
* Hypothese de schema :
|
||||||
* map_layers(id, name, layer_key, color, is_visible, sort_order,
|
* map_layers(id, name, layer_key, color, is_visible, sort_order,
|
||||||
* description)
|
* description)
|
||||||
*/
|
*/
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
@@ -42,21 +42,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$description = trim((string)($_POST['description'] ?? ''));
|
$description = trim((string)($_POST['description'] ?? ''));
|
||||||
|
|
||||||
if ($name === '' || !preg_match('/^[a-z0-9_\-]+$/', $layerKey)) {
|
if ($name === '' || !preg_match('/^[a-z0-9_\-]+$/', $layerKey)) {
|
||||||
$feedback = 'Nom ou clé de couche invalide (clé··: lettres minuscules, chiffres, - et _ uniquement).';
|
$feedback = 'Nom ou clé de couche invalide (clé: lettres minuscules, chiffres, - et _ uniquement).';
|
||||||
$feedbackType = 'error';
|
$feedbackType = 'error';
|
||||||
} elseif ($color === null) {
|
} elseif ($color === null) {
|
||||||
$feedback = 'Couleur invalide (format attendu : #rrggbb).';
|
$feedback = 'Couleur invalide (format attendu : #rrggbb).';
|
||||||
$feedbackType = 'error';
|
$feedbackType = 'error';
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
// Correction de l'erreur MySQL 1093 avec une table dérivée (sous-requête AS m2)
|
||||||
$stmt = $db->prepare(
|
$stmt = $db->prepare(
|
||||||
'INSERT INTO map_layers (name, layer_key, color, is_visible, sort_order, description)
|
'INSERT INTO map_layers (name, layer_key, color, is_visible, sort_order, description)
|
||||||
VALUES (?, ?, ?, 1, (SELECT COALESCE(MAX(sort_order), 0) + 1 FROM map_layers AS m2), ?)'
|
VALUES (?, ?, ?, 1, (SELECT COALESCE(MAX(m2.sort_order), 0) + 1 FROM (SELECT sort_order FROM map_layers) AS m2), ?)'
|
||||||
);
|
);
|
||||||
$stmt->execute([$name, $layerKey, $color, $description]);
|
$stmt->execute([$name, $layerKey, $color, $description]);
|
||||||
$feedback = 'Couche créée.';
|
$feedback = 'Couche créée.';
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$feedback = 'Erreur lors de la création (clé·· déjà utilisée ?) : ' . $e->getMessage();
|
$feedback = 'Erreur lors de la création (clé déjà utilisée ?) : ' . $e->getMessage();
|
||||||
$feedbackType = 'error';
|
$feedbackType = 'error';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +66,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if ($layerId) {
|
if ($layerId) {
|
||||||
$stmt = $db->prepare('UPDATE map_layers SET is_visible = 1 - is_visible WHERE id = ?');
|
$stmt = $db->prepare('UPDATE map_layers SET is_visible = 1 - is_visible WHERE id = ?');
|
||||||
$stmt->execute([$layerId]);
|
$stmt->execute([$layerId]);
|
||||||
$feedback = 'Visibilité·· mise à jour.';
|
$feedback = 'Visibilité mise à jour.';
|
||||||
}
|
}
|
||||||
} elseif ($action === 'update_color') {
|
} elseif ($action === 'update_color') {
|
||||||
$layerId = filter_input(INPUT_POST, 'layer_id', FILTER_VALIDATE_INT);
|
$layerId = filter_input(INPUT_POST, 'layer_id', FILTER_VALIDATE_INT);
|
||||||
@@ -82,12 +83,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$layerId = filter_input(INPUT_POST, 'layer_id', FILTER_VALIDATE_INT);
|
$layerId = filter_input(INPUT_POST, 'layer_id', FILTER_VALIDATE_INT);
|
||||||
$confirmed = isset($_POST['confirm']) && $_POST['confirm'] === '1';
|
$confirmed = isset($_POST['confirm']) && $_POST['confirm'] === '1';
|
||||||
if (!$confirmed || !$layerId) {
|
if (!$confirmed || !$layerId) {
|
||||||
$feedback = 'Suppression refusé··e : confirmation manquante.';
|
$feedback = 'Suppression refusée : confirmation manquante.';
|
||||||
$feedbackType = 'error';
|
$feedbackType = 'error';
|
||||||
} else {
|
} else {
|
||||||
$stmt = $db->prepare('DELETE FROM map_layers WHERE id = ? LIMIT 1');
|
$stmt = $db->prepare('DELETE FROM map_layers WHERE id = ? LIMIT 1');
|
||||||
$stmt->execute([$layerId]);
|
$stmt->execute([$layerId]);
|
||||||
$feedback = $stmt->rowCount() > 0 ? 'Couche supprimé··e.' : 'Couche introuvable.';
|
$feedback = $stmt->rowCount() > 0 ? 'Couche supprimée.' : 'Couche introuvable.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +108,7 @@ $user = current_admin_username();
|
|||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<h1>Couches cartographiques</h1>
|
<h1>Couches cartographiques</h1>
|
||||||
<p>Gé··rez les couches affich ées sur la carte (visibilité··, couleur, ordre). Toute suppression demande une confirmation explicite.</p>
|
<p>Gérez les couches affichées sur la carte (visibilité, couleur, ordre). Toute suppression demande une confirmation explicite.</p>
|
||||||
|
|
||||||
<?php if ($feedback !== null): ?>
|
<?php if ($feedback !== null): ?>
|
||||||
<div class="alert alert-<?= $feedbackType === 'error' ? 'error' : 'success' ?>">
|
<div class="alert alert-<?= $feedbackType === 'error' ? 'error' : 'success' ?>">
|
||||||
@@ -120,10 +121,10 @@ $user = current_admin_username();
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="action" value="create_layer">
|
<input type="hidden" name="action" value="create_layer">
|
||||||
<label>Nom affiché··e
|
<label>Nom affichée
|
||||||
<input type="text" name="name" required maxlength="100">
|
<input type="text" name="name" required maxlength="100">
|
||||||
</label>
|
</label>
|
||||||
<label>Clé·· technique
|
<label>Clé technique
|
||||||
<input type="text" name="layer_key" required pattern="[a-z0-9_\-]+" placeholder="ex: sites_4g">
|
<input type="text" name="layer_key" required pattern="[a-z0-9_\-]+" placeholder="ex: sites_4g">
|
||||||
</label>
|
</label>
|
||||||
<label>Couleur
|
<label>Couleur
|
||||||
@@ -132,7 +133,7 @@ $user = current_admin_username();
|
|||||||
<label>Description
|
<label>Description
|
||||||
<input type="text" name="description" maxlength="255">
|
<input type="text" name="description" maxlength="255">
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" class="btn btn-primary">Cré··er</button>
|
<button type="submit" class="btn btn-primary">Créer</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -140,55 +141,55 @@ $user = current_admin_username();
|
|||||||
<h2>Couches existantes (<?= count($layers) ?>)</h2>
|
<h2>Couches existantes (<?= count($layers) ?>)</h2>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Ordre</th>
|
<th>Ordre</th>
|
||||||
<th>Nom</th>
|
<th>Nom</th>
|
||||||
<th>Clé··</th>
|
<th>Clé</th>
|
||||||
<th>Couleur</th>
|
<th>Couleur</th>
|
||||||
<th>Visible</th>
|
<th>Visible</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($layers as $layer): ?>
|
<?php foreach ($layers as $layer): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= (int)$layer['sort_order'] ?></td>
|
<td><?= (int)$layer['sort_order'] ?></td>
|
||||||
<td><?= htmlspecialchars((string)$layer['name'], ENT_QUOTES, 'UTF-8') ?></td>
|
<td><?= htmlspecialchars((string)$layer['name'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||||
<td><code><?= htmlspecialchars((string)$layer['layer_key'], ENT_QUOTES, 'UTF-8') ?></code></td>
|
<td><code><?= htmlspecialchars((string)$layer['layer_key'], ENT_QUOTES, 'UTF-8') ?></code></td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" style="display:inline-flex;gap:.5rem;align-items:center">
|
<form method="post" style="display:inline-flex;gap:.5rem;align-items:center">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="action" value="update_color">
|
<input type="hidden" name="action" value="update_color">
|
||||||
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
|
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
|
||||||
<input type="color" name="color" value="<?= htmlspecialchars((string)$layer['color'], ENT_QUOTES, 'UTF-8') ?>" onchange="this.form.submit()">
|
<input type="color" name="color" value="<?= htmlspecialchars((string)$layer['color'], ENT_QUOTES, 'UTF-8') ?>" onchange="this.form.submit()">
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="action" value="toggle_visibility">
|
<input type="hidden" name="action" value="toggle_visibility">
|
||||||
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
|
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
|
||||||
<button type="submit" class="btn btn-secondary">
|
<button type="submit" class="btn btn-secondary">
|
||||||
<?= ((int)$layer['is_visible'] === 1) ? 'Visible' : 'Masqué··e' ?>
|
<?= ((int)$layer['is_visible'] === 1) ? 'Visible' : 'Masquée' ?>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td><?= htmlspecialchars((string)$layer['description'], ENT_QUOTES, 'UTF-8') ?></td>
|
<td><?= htmlspecialchars((string)$layer['description'], ENT_QUOTES, 'UTF-8') ?></td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" onsubmit="return confirm('Confirmer la suppression de cette couche ?');">
|
<form method="post" onsubmit="return confirm('Confirmer la suppression de cette couche ?');">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="action" value="delete_layer">
|
<input type="hidden" name="action" value="delete_layer">
|
||||||
<input type="hidden" name="confirm" value="1">
|
<input type="hidden" name="confirm" value="1">
|
||||||
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
|
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
|
||||||
<button type="submit" class="btn btn-danger">Supprimer</button>
|
<button type="submit" class="btn btn-danger">Supprimer</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($layers)): ?>
|
<?php if (empty($layers)): ?>
|
||||||
<tr><td colspan="7">Aucune couche configuré··e.</td></tr>
|
<tr><td colspan="7">Aucune couche configurée.</td></tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+280
-249
@@ -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".
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
Reference in new issue
Block a user