Version initiale — carte des établissements scolaires
Carte Leaflet (annuaire Éducation nationale) + API PHP/MySQL : fiche établissement (effectifs, IPS, population INSEE, DPE ADEME), GED documents/photos, commentaires par groupe, gestion utilisateurs et politique de mots de passe. Imports opendata (effectifs, IPS, population, DPE, IRIS). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
/**
|
||||
* Initialisation de la base « ecoles » :
|
||||
* - crée la base et les tables (etablissements, users, user_scopes, user_groups,
|
||||
* group_members, group_scopes, comments, folders, items) ;
|
||||
* - importe les établissements depuis data/etablissements.json ;
|
||||
* - crée un super-admin par défaut et une arborescence d'exemple ;
|
||||
* - crée les dossiers d'upload.
|
||||
*
|
||||
* Usage :
|
||||
* php setup.php (ligne de commande)
|
||||
* php setup.php --reset (réimporte les établissements)
|
||||
* http://.../setup.php (navigateur ; ?reset=1 pour réimporter)
|
||||
*
|
||||
* Idempotent : relançable sans risque.
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . '/api/db.php';
|
||||
|
||||
$cli = PHP_SAPI === 'cli';
|
||||
$reset = isset($_GET['reset']) || in_array('--reset', $argv ?? [], true);
|
||||
|
||||
if (!$cli) {
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
}
|
||||
function say(string $m): void
|
||||
{
|
||||
echo $m . "\n";
|
||||
flush();
|
||||
}
|
||||
|
||||
/** Ajoute une colonne si elle n'existe pas déjà (migration idempotente). */
|
||||
function add_col_if_missing(PDO $pdo, string $table, string $col, string $ddl): void
|
||||
{
|
||||
$st = $pdo->prepare(
|
||||
'SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?'
|
||||
);
|
||||
$st->execute([$table, $col]);
|
||||
if (!$st->fetchColumn()) {
|
||||
$pdo->exec("ALTER TABLE `$table` ADD COLUMN $ddl");
|
||||
say(" + colonne $table.$col ajoutée.");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Création de la base (connexion sans schéma).
|
||||
$root = new PDO(
|
||||
'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';charset=utf8mb4',
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
$root->exec('CREATE DATABASE IF NOT EXISTS `' . DB_NAME . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
|
||||
say('✔ Base « ' . DB_NAME . ' » prête.');
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// 2. Établissements.
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS etablissements (
|
||||
id VARCHAR(12) NOT NULL PRIMARY KEY,
|
||||
nom VARCHAR(255),
|
||||
type VARCHAR(80),
|
||||
stat VARCHAR(40),
|
||||
reg VARCHAR(120),
|
||||
aca VARCHAR(120),
|
||||
dep VARCHAR(120),
|
||||
depc VARCHAR(10),
|
||||
com VARCHAR(160),
|
||||
cp VARCHAR(10),
|
||||
adr VARCHAR(255),
|
||||
tel VARCHAR(40),
|
||||
mail VARCHAR(160),
|
||||
web VARCHAR(255),
|
||||
etat VARCHAR(20),
|
||||
circ VARCHAR(255),
|
||||
lat DECIMAL(9,5),
|
||||
lon DECIMAL(9,5),
|
||||
rest TINYINT NOT NULL DEFAULT 0,
|
||||
heb TINYINT NOT NULL DEFAULT 0,
|
||||
ulis TINYINT NOT NULL DEFAULT 0,
|
||||
segp TINYINT NOT NULL DEFAULT 0,
|
||||
appr TINYINT NOT NULL DEFAULT 0,
|
||||
INDEX idx_type (type),
|
||||
INDEX idx_reg (reg),
|
||||
INDEX idx_aca (aca),
|
||||
INDEX idx_dep (dep),
|
||||
INDEX idx_comdep (com, depc),
|
||||
INDEX idx_etat (etat)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
|
||||
// Anciennes tables de la v1 (remplacées par « items »).
|
||||
$pdo->exec('DROP TABLE IF EXISTS documents');
|
||||
$pdo->exec('DROP TABLE IF EXISTS photos');
|
||||
|
||||
// 3. Utilisateurs (admins) et périmètres.
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS users (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(80) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM('super','perimeter','member') NOT NULL DEFAULT 'perimeter',
|
||||
email VARCHAR(160) NULL,
|
||||
must_change TINYINT NOT NULL DEFAULT 0,
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
// Migration des bases existantes : rôle « member » + colonnes de politique de mot de passe.
|
||||
$pdo->exec("ALTER TABLE users MODIFY role ENUM('super','perimeter','member') NOT NULL DEFAULT 'perimeter'");
|
||||
add_col_if_missing($pdo, 'users', 'email', 'email VARCHAR(160) NULL');
|
||||
add_col_if_missing($pdo, 'users', 'must_change', 'must_change TINYINT NOT NULL DEFAULT 0');
|
||||
add_col_if_missing($pdo, 'users', 'failed_attempts', 'failed_attempts INT NOT NULL DEFAULT 0');
|
||||
add_col_if_missing($pdo, 'users', 'locked_until', 'locked_until DATETIME NULL');
|
||||
|
||||
// Jetons de réinitialisation de mot de passe (lien à usage unique).
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS password_resets (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
used TINYINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_token (token_hash),
|
||||
CONSTRAINT fk_reset_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS user_scopes (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
com VARCHAR(160) NOT NULL,
|
||||
depc VARCHAR(10) NOT NULL,
|
||||
UNIQUE KEY uniq_scope (user_id, com, depc),
|
||||
INDEX idx_comdep (com, depc),
|
||||
CONSTRAINT fk_scope_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
|
||||
// 3 bis. Groupes d'utilisateurs (équipes). can_validate = le groupe peut valider
|
||||
// les documents ; group_scopes porte alors son périmètre de communes.
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS user_groups (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(120) NOT NULL UNIQUE,
|
||||
can_validate TINYINT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS group_members (
|
||||
group_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
PRIMARY KEY (group_id, user_id),
|
||||
INDEX idx_user (user_id),
|
||||
CONSTRAINT fk_gm_group FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_gm_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS group_scopes (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
group_id INT NOT NULL,
|
||||
com VARCHAR(160) NOT NULL,
|
||||
depc VARCHAR(10) NOT NULL,
|
||||
UNIQUE KEY uniq_gscope (group_id, com, depc),
|
||||
INDEX idx_comdep (com, depc),
|
||||
CONSTRAINT fk_gs_group FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
|
||||
// 3 ter. Commentaires de fiche : horodatés, visibles entre membres d'un même groupe.
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS comments (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
uai VARCHAR(12) NOT NULL,
|
||||
item_id INT NULL, -- commentaire sur un document précis (sinon NULL)
|
||||
user_id INT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_uai (uai),
|
||||
INDEX idx_item (item_id),
|
||||
CONSTRAINT fk_com_uai FOREIGN KEY (uai) REFERENCES etablissements(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_com_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
|
||||
// 4. Arborescence globale de dossiers.
|
||||
$pdo->exec(
|
||||
'CREATE TABLE IF NOT EXISTS folders (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
parent_id INT NULL,
|
||||
name VARCHAR(160) NOT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_parent (parent_id),
|
||||
CONSTRAINT fk_folder_parent FOREIGN KEY (parent_id) REFERENCES folders(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||
);
|
||||
|
||||
// 5. Éléments : fichiers, photos, liens — soumis puis validés.
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS items (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
uai VARCHAR(12) NOT NULL,
|
||||
folder_id INT NULL,
|
||||
kind ENUM('file','photo','link') NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
filename VARCHAR(255) NULL,
|
||||
url VARCHAR(2048) NULL,
|
||||
size BIGINT NULL,
|
||||
mime VARCHAR(160) NULL,
|
||||
status ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending',
|
||||
submitter_name VARCHAR(160) NULL,
|
||||
submitter_email VARCHAR(160) NULL,
|
||||
reject_reason VARCHAR(500) NULL,
|
||||
submitted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
validated_by INT NULL,
|
||||
validated_at DATETIME NULL,
|
||||
INDEX idx_uai (uai),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_folder (folder_id),
|
||||
CONSTRAINT fk_item_uai FOREIGN KEY (uai) REFERENCES etablissements(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_item_folder FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_item_validator FOREIGN KEY (validated_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
|
||||
// Migrations des bases existantes : description des éléments + portée document des commentaires.
|
||||
add_col_if_missing($pdo, 'items', 'description', 'description TEXT NULL AFTER title');
|
||||
add_col_if_missing($pdo, 'comments', 'item_id', 'item_id INT NULL AFTER uai');
|
||||
// Index sur comments.item_id si absent.
|
||||
$hasIdx = $pdo->query("SHOW INDEX FROM comments WHERE Key_name = 'idx_item'")->fetchColumn();
|
||||
if (!$hasIdx) {
|
||||
$pdo->exec('ALTER TABLE comments ADD INDEX idx_item (item_id)');
|
||||
}
|
||||
say('✔ Tables prêtes.');
|
||||
|
||||
// 6. Dossiers d'upload.
|
||||
foreach (['files', 'photos'] as $d) {
|
||||
$p = __DIR__ . '/uploads/' . $d;
|
||||
if (!is_dir($p)) {
|
||||
mkdir($p, 0777, true);
|
||||
}
|
||||
}
|
||||
say('✔ Dossiers uploads/ prêts.');
|
||||
|
||||
// 7. Super-admin par défaut (changement de mot de passe forcé à la 1re connexion).
|
||||
if ((int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn() === 0) {
|
||||
$pdo->prepare('INSERT INTO users (username, password_hash, role, must_change) VALUES (?, ?, ?, 1)')
|
||||
->execute(['admin', password_hash('admin', PASSWORD_DEFAULT), 'super']);
|
||||
say('✔ Super-admin créé : « admin » / « admin » → changement de mot de passe demandé à la 1re connexion.');
|
||||
}
|
||||
|
||||
// 7 bis. Dossier privé (boîte d'envoi du mailer en mode « log »), non servi par Apache.
|
||||
$priv = __DIR__ . '/private';
|
||||
if (!is_dir($priv)) {
|
||||
mkdir($priv, 0777, true);
|
||||
}
|
||||
if (!is_file($priv . '/.htaccess')) {
|
||||
file_put_contents($priv . '/.htaccess', "Require all denied\n");
|
||||
}
|
||||
say('✔ Dossier private/ prêt.');
|
||||
|
||||
// 8. Arborescence d'exemple (modifiable ensuite dans l'admin).
|
||||
if ((int) $pdo->query('SELECT COUNT(*) FROM folders')->fetchColumn() === 0) {
|
||||
$tree = [
|
||||
'Administratif' => ['Budgets', 'Comptes-rendus de conseil'],
|
||||
'Pédagogie' => ['Projet d’établissement', 'Règlement intérieur'],
|
||||
'Travaux & bâtiments' => [],
|
||||
'Photos' => ['Façade', 'Intérieur'],
|
||||
'Liens utiles' => [],
|
||||
];
|
||||
$insTop = $pdo->prepare('INSERT INTO folders (parent_id, name, position) VALUES (NULL, ?, ?)');
|
||||
$insSub = $pdo->prepare('INSERT INTO folders (parent_id, name, position) VALUES (?, ?, ?)');
|
||||
$pos = 0;
|
||||
foreach ($tree as $top => $subs) {
|
||||
$insTop->execute([$top, $pos++]);
|
||||
$pid = (int) $pdo->lastInsertId();
|
||||
$sp = 0;
|
||||
foreach ($subs as $sub) {
|
||||
$insSub->execute([$pid, $sub, $sp++]);
|
||||
}
|
||||
}
|
||||
say('✔ Arborescence d’exemple créée.');
|
||||
}
|
||||
|
||||
// 9. Import des établissements.
|
||||
$count = (int) $pdo->query('SELECT COUNT(*) FROM etablissements')->fetchColumn();
|
||||
if ($count > 0 && !$reset) {
|
||||
say("ℹ $count établissements déjà en base — import ignoré (utilisez --reset / ?reset=1 pour réimporter).");
|
||||
} else {
|
||||
$jsonPath = __DIR__ . '/data/etablissements.json';
|
||||
if (!is_file($jsonPath)) {
|
||||
throw new RuntimeException("Fichier introuvable : $jsonPath");
|
||||
}
|
||||
say('… Lecture de ' . basename($jsonPath) . ' …');
|
||||
$rows = json_decode(file_get_contents($jsonPath), true);
|
||||
if (!is_array($rows)) {
|
||||
throw new RuntimeException('JSON invalide.');
|
||||
}
|
||||
$n = import_etablissements($pdo, $rows);
|
||||
say("✔ Import terminé : $n établissements.");
|
||||
}
|
||||
|
||||
say('');
|
||||
say('🎉 Initialisation terminée.');
|
||||
} catch (Throwable $e) {
|
||||
if (isset($pdo) && $pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
http_response_code(500);
|
||||
say('!! ERREUR : ' . $e->getMessage());
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user