add structured logging for admin/partage form submissions + migration system

- AppLogger: JSON-line logger in storage/logs/form-submissions.log
- Logs submissions (admin + partage) with IP, UA, thesis ID, author
- Logs errors with context (post keys, share slug)
- Migration runner (app/migrations/run.php) handles schema drift
- 001_add_objet_column.sql fixes production DB missing 'objet' column
- ThesisCreateController::getIdentifier() helper for logging
This commit is contained in:
Pontoporeia
2026-04-24 16:55:11 +02:00
parent decb9e2907
commit 4986fa74f4
9 changed files with 344 additions and 10 deletions

View File

@@ -0,0 +1,7 @@
-- Add 'objet' column to theses table if it doesn't already exist.
-- Required: the admin form sends 'objet' in POST since commit ~Apr 2024
-- but older production databases may lack the column.
-- SQLite 3.35+ supports ALTER TABLE ADD COLUMN (bundled with PHP 8+).
ALTER TABLE theses ADD COLUMN objet TEXT NOT NULL DEFAULT 'tfe'
CHECK (objet IN ('tfe', 'thèse', 'frart'));

84
app/migrations/run.php Normal file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env php
<?php
/**
* Run pending migrations on the production database.
*
* Usage: php app/migrations/run.php [DB_PATH]
*
* If no DB_PATH is given, defaults to storage/posterg.db.
*
* Each migration in migrations/pending/ is applied in alphabetical order.
* After success, the file is moved to migrations/applied/.
*/
$root = dirname(__DIR__);
$dbPath = $argv[1] ?? ($root . '/storage/posterg.db');
if (!file_exists($dbPath)) {
die("Database not found: $dbPath\n");
}
$pdo = new PDO('sqlite:' . $dbPath);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Create migrations tracking table
$pdo->exec("
CREATE TABLE IF NOT EXISTS _migrations (
name TEXT PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
$applied = $pdo->query("SELECT name FROM _migrations")->fetchAll(PDO::FETCH_COLUMN);
$pendingDir = $root . '/migrations/pending';
$appliedDir = $root . '/migrations/applied';
if (!is_dir($pendingDir)) {
echo "No pending migrations directory.\n";
exit(0);
}
if (!is_dir($appliedDir)) {
mkdir($appliedDir, 0755, true);
}
$files = glob($pendingDir . '/*.sql');
sort($files);
if (empty($files)) {
echo "No pending migration files.\n";
exit(0);
}
$count = 0;
foreach ($files as $file) {
$name = basename($file);
if (in_array($name, $applied, true)) {
echo "Skip (already applied): $name\n";
continue;
}
echo "Applying: $name\n";
$sql = file_get_contents($file);
try {
$pdo->exec($sql);
$pdo->prepare("INSERT INTO _migrations (name) VALUES (?)")->execute([$name]);
rename($file, $appliedDir . '/' . $name);
$count++;
} catch (PDOException $e) {
$msg = $e->getMessage();
// SQLite: skip if column already exists
if (stripos($msg, 'duplicate column name') !== false) {
echo " Already exists (skipping): $name\n";
$pdo->prepare("INSERT OR REPLACE INTO _migrations (name) VALUES (?)")->execute([$name]);
rename($file, $appliedDir . '/' . $name);
continue;
}
throw $e;
}
}
echo "$count migration(s) applied.\n";