Compare commits

...
3 Commits
Author SHA1 Message Date
voxan be12cadea6 Maj SQL 2026-10-21 22:18:01 +02:00
voxan 5c43bf9e7c Correction SQL 2026-10-21 22:17:26 +02:00
voxan 0c8d9cba6d Corrige un soucis de Overflow 32 bits 2026-10-21 22:16:42 +02:00
3 changed files with 136 additions and 101 deletions

No files matched your search

+14 -11
View File
@@ -10,24 +10,24 @@
* une tache cron/systemd timer sur le serveur.
*
* Regle de securite appliquee ici :
* - Pas de shell_exec()/exec()/proc_open() sur une entree utilisateur.
* - Pas de mot de passe DB affiche : on reference une variable
* d'environnement ou un fichier .my.cnf externe.
* - Seule action en base : enregistrement de metadonnees de sauvegarde
* (date, taille, note) dans une table de suivi, jamais le contenu.
* - Pas de shell_exec()/exec()/proc_open() sur une entree utilisateur.
* - Pas de mot de passe DB affiche : on reference une variable
* d'environnement ou un fichier .my.cnf externe.
* - Seule action en base : enregistrement de metadonnees de sauvegarde
* (date, taille, note) dans une table de suivi, jamais le contenu.
*
* CORRIGE (20/09/2026, suite a erreur 500 en usage reel) :
* - require_once _nav.php manquait, provoquant une erreur fatale
* "Call to undefined function render_admin_nav()".
* - La table backup_log necessaire a cette page n'existait pas :
* voir admin/sql/migration_add_missing_tables.sql.
* - require_once _nav.php manquait, provoquant une erreur fatale
* "Call to undefined function render_admin_nav()".
* - La table backup_log necessaire a cette page n'existait pas :
* voir admin/sql/migration_add_missing_tables.sql.
*
* 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
* error_log().
*
* Hypothese de schema : backup_log(id, created_at, admin_username,
* label, note, size_bytes_reported)
* label, note, size_bytes_reported)
*/
declare(strict_types=1);
@@ -54,7 +54,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else {
$label = trim((string)($_POST['label'] ?? ''));
$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 === '') {
$feedback = 'Le libelle de la sauvegarde est obligatoire.';
+60 -59
View File
@@ -9,8 +9,8 @@
* suppression d'une couche.
*
* Hypothese de schema :
* map_layers(id, name, layer_key, color, is_visible, sort_order,
* description)
* map_layers(id, name, layer_key, color, is_visible, sort_order,
* description)
*/
declare(strict_types=1);
@@ -42,21 +42,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$description = trim((string)($_POST['description'] ?? ''));
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';
} elseif ($color === null) {
$feedback = 'Couleur invalide (format attendu : #rrggbb).';
$feedbackType = 'error';
} else {
try {
// Correction de l'erreur MySQL 1093 avec une table dérivée (sous-requête AS m2)
$stmt = $db->prepare(
'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]);
$feedback = 'Couche créé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';
}
}
@@ -65,7 +66,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($layerId) {
$stmt = $db->prepare('UPDATE map_layers SET is_visible = 1 - is_visible WHERE id = ?');
$stmt->execute([$layerId]);
$feedback = 'Visibilité·· mise à jour.';
$feedback = 'Visibilité mise à jour.';
}
} elseif ($action === 'update_color') {
$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);
$confirmed = isset($_POST['confirm']) && $_POST['confirm'] === '1';
if (!$confirmed || !$layerId) {
$feedback = 'Suppression refusé··e : confirmation manquante.';
$feedback = 'Suppression refusée : confirmation manquante.';
$feedbackType = 'error';
} else {
$stmt = $db->prepare('DELETE FROM map_layers WHERE id = ? LIMIT 1');
$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">
<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): ?>
<div class="alert alert-<?= $feedbackType === 'error' ? 'error' : 'success' ?>">
@@ -120,10 +121,10 @@ $user = current_admin_username();
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="create_layer">
<label>Nom affiché··e
<label>Nom affichée
<input type="text" name="name" required maxlength="100">
</label>
<label>Clé·· technique
<label>Clé technique
<input type="text" name="layer_key" required pattern="[a-z0-9_\-]+" placeholder="ex: sites_4g">
</label>
<label>Couleur
@@ -132,7 +133,7 @@ $user = current_admin_username();
<label>Description
<input type="text" name="description" maxlength="255">
</label>
<button type="submit" class="btn btn-primary">Cré··er</button>
<button type="submit" class="btn btn-primary">Créer</button>
</form>
</section>
@@ -140,55 +141,55 @@ $user = current_admin_username();
<h2>Couches existantes (<?= count($layers) ?>)</h2>
<table class="table">
<thead>
<tr>
<th>Ordre</th>
<th>Nom</th>
<th>Clé··</th>
<th>Couleur</th>
<th>Visible</th>
<th>Description</th>
<th>Actions</th>
</tr>
<tr>
<th>Ordre</th>
<th>Nom</th>
<th>Clé</th>
<th>Couleur</th>
<th>Visible</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($layers as $layer): ?>
<tr>
<td><?= (int)$layer['sort_order'] ?></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>
<form method="post" style="display:inline-flex;gap:.5rem;align-items:center">
<?= csrf_field() ?>
<input type="hidden" name="action" value="update_color">
<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()">
</form>
</td>
<td>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="toggle_visibility">
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
<button type="submit" class="btn btn-secondary">
<?= ((int)$layer['is_visible'] === 1) ? 'Visible' : 'Masqué··e' ?>
</button>
</form>
</td>
<td><?= htmlspecialchars((string)$layer['description'], ENT_QUOTES, 'UTF-8') ?></td>
<td>
<form method="post" onsubmit="return confirm('Confirmer la suppression de cette couche ?');">
<?= csrf_field() ?>
<input type="hidden" name="action" value="delete_layer">
<input type="hidden" name="confirm" value="1">
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
<button type="submit" class="btn btn-danger">Supprimer</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($layers)): ?>
<tr><td colspan="7">Aucune couche configuré··e.</td></tr>
<?php endif; ?>
<?php foreach ($layers as $layer): ?>
<tr>
<td><?= (int)$layer['sort_order'] ?></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>
<form method="post" style="display:inline-flex;gap:.5rem;align-items:center">
<?= csrf_field() ?>
<input type="hidden" name="action" value="update_color">
<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()">
</form>
</td>
<td>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="toggle_visibility">
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
<button type="submit" class="btn btn-secondary">
<?= ((int)$layer['is_visible'] === 1) ? 'Visible' : 'Masquée' ?>
</button>
</form>
</td>
<td><?= htmlspecialchars((string)$layer['description'], ENT_QUOTES, 'UTF-8') ?></td>
<td>
<form method="post" onsubmit="return confirm('Confirmer la suppression de cette couche ?');">
<?= csrf_field() ?>
<input type="hidden" name="action" value="delete_layer">
<input type="hidden" name="confirm" value="1">
<input type="hidden" name="layer_id" value="<?= (int)$layer['id'] ?>">
<button type="submit" class="btn btn-danger">Supprimer</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($layers)): ?>
<tr><td colspan="7">Aucune couche configurée.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
+62 -31
View File
@@ -4,20 +4,20 @@
-- 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)
-- 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
-- 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)
-- - 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.
@@ -219,18 +219,6 @@ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
-- ---------------------------------------------------------------------
-- 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,
@@ -244,18 +232,61 @@ 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;
-- ---------------------------------------------------------------------
-- 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".
-- - 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".
-- =====================================================================