fix #29 : envoyer le lien magique par email (envoyer_mail_smtp)

This commit is contained in:
Cedric Abonnel
2026-05-13 23:41:58 +02:00
commit 8a85c15372
129 changed files with 22818 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
-- Réactions visiteurs (cookie anti-doublon)
CREATE TABLE article_reactions (
id SERIAL PRIMARY KEY,
article_uuid TEXT NOT NULL,
reaction_type TEXT NOT NULL CHECK (reaction_type IN ('useful', 'important', 'interesting')),
visitor_hash TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (article_uuid, reaction_type, visitor_hash)
);
CREATE INDEX ON article_reactions (article_uuid);
-- Commentaires avec vérification par email
CREATE TABLE comments (
id SERIAL PRIMARY KEY,
article_uuid TEXT NOT NULL,
author_name TEXT NOT NULL,
author_email TEXT NOT NULL,
content TEXT NOT NULL CHECK (LENGTH(content) <= 2000),
verify_token TEXT,
verification_code TEXT,
verify_attempts INTEGER NOT NULL DEFAULT 0,
verified BOOLEAN NOT NULL DEFAULT FALSE,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
ip_address TEXT,
user_agent TEXT
);
CREATE INDEX ON comments (article_uuid, verified, published);
CREATE INDEX ON comments (verify_token)
WHERE verified = FALSE AND verify_token IS NOT NULL;
+47
View File
@@ -0,0 +1,47 @@
<?php
// À exécuter UNE SEULE FOIS pour enregistrer les migrations déjà appliquées.
// php database/migrate-init.php
declare(strict_types=1);
$baseDir = dirname(__DIR__);
$envFile = $baseDir . '/.env';
$env = [];
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
[$k, $v] = array_pad(explode('=', $line, 2), 2, '');
$env[trim($k)] = trim($v, '"\'');
}
$pdo = new PDO($env['DB_DSN'], $env['DB_USER'] ?? '', $env['DB_PASS'] ?? '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->exec('
CREATE TABLE IF NOT EXISTS schema_migrations (
name TEXT NOT NULL PRIMARY KEY,
applied_at TIMESTAMP NOT NULL DEFAULT NOW()
)
');
// Migrations déjà appliquées avant la mise en place de ce système
$alreadyApplied = [
'migration_001_roles_ratings.sql',
'migration_002_profile_url.sql',
'migration_003_profile_slug.sql',
'migration_004_profile_bio.sql',
'migration_005_rss_feeds.sql',
'migration_006_profile_links.sql',
];
$stmt = $pdo->prepare('INSERT INTO schema_migrations (name) VALUES (?) ON CONFLICT DO NOTHING');
foreach ($alreadyApplied as $name) {
$stmt->execute([$name]);
echo " ✓ marquée : $name\n";
}
echo "\nInitialisation terminée. Vous pouvez supprimer ce fichier.\n";
+83
View File
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
// Runner de migrations SQL.
// Appelé sur le serveur après chaque rsync : php database/migrate.php
$baseDir = dirname(__DIR__);
$envFile = $baseDir . '/.env';
if (!file_exists($envFile)) {
fwrite(STDERR, "Fichier .env introuvable : $envFile\n");
exit(1);
}
$env = [];
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) {
continue;
}
[$k, $v] = array_pad(explode('=', $line, 2), 2, '');
$env[trim($k)] = trim($v, '"\'');
}
$dsn = $env['DB_DSN'] ?? '';
$user = $env['DB_USER'] ?? '';
$pass = $env['DB_PASS'] ?? '';
if (!$dsn) {
fwrite(STDERR, "DB_DSN manquant dans .env\n");
exit(1);
}
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// Crée la table de suivi si elle n'existe pas encore
$pdo->exec('
CREATE TABLE IF NOT EXISTS schema_migrations (
name TEXT NOT NULL PRIMARY KEY,
applied_at TIMESTAMP NOT NULL DEFAULT NOW()
)
');
// Migrations déjà appliquées
$applied = array_flip(
$pdo->query('SELECT name FROM schema_migrations ORDER BY name')
->fetchAll(PDO::FETCH_COLUMN)
);
// Fichiers de migration dans le même dossier, triés par nom
$files = glob(__DIR__ . '/migration_*.sql') ?: [];
sort($files);
$count = 0;
foreach ($files as $file) {
$name = basename($file);
if (isset($applied[$name])) {
continue;
}
$sql = file_get_contents($file);
if ($sql === false || trim($sql) === '') {
echo "$name : fichier vide ou illisible, ignoré\n";
continue;
}
echo "$name ... ";
$pdo->exec($sql);
$pdo->prepare('INSERT INTO schema_migrations (name) VALUES (?)')
->execute([$name]);
echo "\n";
$count++;
}
if ($count === 0) {
echo " (aucune migration en attente)\n";
} else {
echo " $count migration(s) appliquée(s).\n";
}
+39
View File
@@ -0,0 +1,39 @@
-- Migration 001 : Système de rôles et notes d'articles
-- À exécuter une seule fois sur le serveur PostgreSQL
-- Rôles disponibles
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
label TEXT NOT NULL
);
INSERT INTO roles (name, label) VALUES
('admin', 'Administrateur'),
('editor', 'Rédacteur'),
('reader', 'Lecteur')
ON CONFLICT (name) DO NOTHING;
-- Association utilisateur ↔ rôle (clé : email, pour compatibilité OIDC sans FK)
CREATE TABLE IF NOT EXISTS user_roles (
user_email TEXT NOT NULL,
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
granted_at TIMESTAMP DEFAULT NOW(),
granted_by TEXT,
PRIMARY KEY (user_email, role_id)
);
-- Seed : cedric@abonnel.fr → admin
INSERT INTO user_roles (user_email, role_id, granted_by)
SELECT 'cedric@abonnel.fr', id, 'migration'
FROM roles WHERE name = 'admin'
ON CONFLICT DO NOTHING;
-- Notes d'articles (1-5 étoiles, une note par utilisateur par article)
CREATE TABLE IF NOT EXISTS article_ratings (
article_uuid VARCHAR(36) NOT NULL,
user_email TEXT NOT NULL,
rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
rated_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (article_uuid, user_email)
);
+1
View File
@@ -0,0 +1 @@
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS profile_url TEXT NOT NULL DEFAULT '';
+2
View File
@@ -0,0 +1,2 @@
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS profile_slug TEXT NOT NULL DEFAULT '';
CREATE UNIQUE INDEX IF NOT EXISTS user_profiles_profile_slug_idx ON user_profiles (profile_slug) WHERE profile_slug <> '';
+1
View File
@@ -0,0 +1 @@
ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS bio TEXT NOT NULL DEFAULT '';
+8
View File
@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS rss_feeds (
id SERIAL PRIMARY KEY,
user_email TEXT NOT NULL,
feed_url TEXT NOT NULL,
label TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP DEFAULT now(),
UNIQUE (user_email, feed_url)
);
+9
View File
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS profile_links (
id SERIAL PRIMARY KEY,
user_email TEXT NOT NULL,
url TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
position INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT now()
);
@@ -0,0 +1,6 @@
-- Ajout du token UUID dans l'URL de vérification et du compteur de tentatives
ALTER TABLE comments ADD COLUMN IF NOT EXISTS verify_token TEXT;
ALTER TABLE comments ADD COLUMN IF NOT EXISTS verify_attempts INTEGER NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_comments_verify_token ON comments (verify_token)
WHERE verified = FALSE AND verify_token IS NOT NULL;
+17
View File
@@ -0,0 +1,17 @@
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP,
is_published BOOLEAN DEFAULT FALSE
);
CREATE TABLE post_files (
id SERIAL PRIMARY KEY,
post_id INTEGER REFERENCES posts(id) ON DELETE CASCADE,
file_type TEXT,
file_path TEXT,
original_name TEXT,
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);