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
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use PDO;
final class DictionaryRepository
{
public function __construct(private PDO $pdo)
{
}
public function getEntityByCode(string $code): ?array
{
$st = $this->pdo->prepare('SELECT * FROM dd_entities WHERE code = :c AND is_active IS TRUE');
$st->execute([':c' => $code]);
$e = $st->fetch(PDO::FETCH_ASSOC);
if (!$e) {
return null;
}
$e['fields'] = $this->getFields((int)$e['id']);
$e['rules'] = $this->getRules((int)$e['id']);
return $e;
}
public function getFields(int $entityId): array
{
$st = $this->pdo->prepare('SELECT * FROM dd_fields WHERE entity_id = :id ORDER BY ui_order NULLS LAST, id');
$st->execute([':id' => $entityId]);
return $st->fetchAll(PDO::FETCH_ASSOC);
}
public function getRules(int $entityId): array
{
$st = $this->pdo->prepare('SELECT * FROM dd_rules WHERE entity_id = :id AND active IS TRUE');
$st->execute([':id' => $entityId]);
return $st->fetchAll(PDO::FETCH_ASSOC);
}
public function getEnum(string $name): array
{
$st = $this->pdo->prepare('
SELECT ev.code, ev.label
FROM dd_enums e JOIN dd_enum_values ev ON ev.enum_id = e.id
WHERE e.name = :n AND ev.active IS TRUE
ORDER BY ev.sort_order, ev.id
');
$st->execute([':n' => $name]);
return $st->fetchAll(PDO::FETCH_ASSOC);
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use PDO;
use App\Infrastructure\Database;
final class ProfileRepository
{
private PDO $pdo;
public function __construct(?PDO $pdo = null)
{
// 0) DI directe
if ($pdo instanceof PDO) {
$this->pdo = $pdo;
return;
}
// 1) App\Infrastructure\Database (si elle expose quelque chose)
if (class_exists(Database::class)) {
if (method_exists(Database::class, 'pdo')) {
$try = Database::pdo();
if ($try instanceof PDO) {
$this->pdo = $try;
return;
}
}
if (method_exists(Database::class, 'getPdo')) {
$try = Database::getPdo();
if ($try instanceof PDO) {
$this->pdo = $try;
return;
}
}
if (method_exists(Database::class, 'getInstance')) {
$db = Database::getInstance();
if ($db instanceof PDO) {
$this->pdo = $db;
return;
}
if (is_object($db) && method_exists($db, 'pdo')) {
$try = $db->pdo();
if ($try instanceof PDO) {
$this->pdo = $try;
return;
}
}
}
}
// 2) Fonction globale éventuelle
if (function_exists('db')) {
$try = db();
if ($try instanceof PDO) {
$this->pdo = $try;
return;
}
}
// 3) Variable globale éventuelle
if (!empty($GLOBALS['pdo']) && $GLOBALS['pdo'] instanceof PDO) {
$this->pdo = $GLOBALS['pdo'];
return;
}
// 4) Fallback env/const : compose un DSN pgsql si nécessaire
$dsn = getenv('DB_DSN') ?: ($_ENV['DB_DSN'] ?? null);
$user = getenv('DB_USER') ?: ($_ENV['DB_USER'] ?? null);
$pass = getenv('DB_PASS') ?: ($_ENV['DB_PASS'] ?? null);
if (!$dsn) {
$host = getenv('DB_HOST') ?: ($_ENV['DB_HOST'] ?? 'localhost');
$port = getenv('DB_PORT') ?: ($_ENV['DB_PORT'] ?? '5432');
$name = getenv('DB_NAME') ?: ($_ENV['DB_NAME'] ?? null);
if ($name) {
$dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $name);
}
}
if ($dsn) {
$pdo = new PDO($dsn, (string)$user, (string)$pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$this->pdo = $pdo;
return;
}
throw new \RuntimeException('Impossible dobtenir un PDO (aucune source valide trouvée).');
}
public function all(?bool $onlyActive = null): array
{
$sql = 'SELECT * FROM profiles';
if ($onlyActive !== null) {
$sql .= ' WHERE is_active = :act';
}
$sql .= ' ORDER BY slug';
$stmt = $this->pdo->prepare($sql);
if ($onlyActive !== null) {
$stmt->bindValue(':act', $onlyActive, PDO::PARAM_BOOL);
}
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
}
public function findById(int $id): ?array
{
$stmt = $this->pdo->prepare('SELECT * FROM profiles WHERE id = :id');
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
public function findBySlug(string $slug): ?array
{
$stmt = $this->pdo->prepare('SELECT * FROM profiles WHERE slug = :slug');
$stmt->execute([':slug' => $slug]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
public function create(string $slug, string $label, ?string $description, array $permissions, bool $isSystem, bool $isActive): int
{
$stmt = $this->pdo->prepare('INSERT INTO profiles(slug,label,description,permissions,is_system,is_active) VALUES(:slug,:label,:desc,CAST(:perms AS jsonb),:sys,:act) RETURNING id');
$stmt->execute([
':slug' => $slug,
':label' => $label,
':desc' => $description,
':perms' => json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
':sys' => $isSystem,
':act' => $isActive,
]);
return (int)$stmt->fetchColumn();
}
public function update(int $id, string $slug, string $label, ?string $description, array $permissions, bool $isSystem, bool $isActive): void
{
$stmt = $this->pdo->prepare('UPDATE profiles SET slug=:slug,label=:label,description=:desc,permissions=CAST(:perms AS jsonb),is_system=:sys,is_active=:act WHERE id=:id');
$stmt->execute([
':id' => $id,
':slug' => $slug,
':label' => $label,
':desc' => $description,
':perms' => json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
':sys' => $isSystem,
':act' => $isActive,
]);
}
public function delete(int $id): void
{
$stmt = $this->pdo->prepare('DELETE FROM profiles WHERE id=:id AND is_system = FALSE');
$stmt->execute([':id' => $id]);
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Domain\User;
use PDO;
final class UserRepository
{
public function __construct(private PDO $pdo)
{
}
/**
* Crée (si besoin) un utilisateur OIDC.
* - Idempotent par email : si existe, retourne l'id existant.
* - Génère un password_hash aléatoire inutilisable (compte OIDC).
*
* @return string ID (uuid) sous forme de chaîne
*/
public function createFromOidc(string $email): string
{
$email = strtolower(trim($email));
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Email OIDC invalide.');
}
// 1) Existe déjà ?
$st = $this->pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1');
$st->execute([':email' => $email]);
$id = $st->fetchColumn();
if ($id !== false && $id !== null) {
return (string)$id;
}
// 2) Création
// Génère un hash robuste sur une valeur aléatoire (aucune chance de connexion par mot de passe).
$randomSecret = bin2hex(random_bytes(32));
$randomHash = password_hash($randomSecret, PASSWORD_DEFAULT);
$sql = <<<SQL
INSERT INTO users (email, password_hash)
VALUES (:email, :hash)
RETURNING id
SQL;
try {
$st = $this->pdo->prepare($sql);
$st->execute([
':email' => $email,
':hash' => $randomHash,
]);
return (string)$st->fetchColumn();
} catch (\PDOException $e) {
// Unique violation sur email (23505) → on relit lid (race condition)
if ($e->getCode() === '23505') {
$st = $this->pdo->prepare('SELECT id FROM users WHERE email = :email LIMIT 1');
$st->execute([':email' => $email]);
$id = $st->fetchColumn();
if ($id !== false && $id !== null) {
return (string)$id;
}
}
throw $e;
}
}
public function findByEmail(string $email): ?User
{
$sql = 'SELECT id, email, password_hash, is_active FROM users WHERE email = :email LIMIT 1';
$st = $this->pdo->prepare($sql);
$st->execute([':email' => $email]);
$row = $st->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
$isActive = $this->toBool($row['is_active']);
return new User(
(string)$row['id'],
(string)$row['email'],
(string)$row['password_hash'],
$isActive
);
}
public function create(string $email, string $passwordHash): string
{
// PostgreSQL
$sql = 'INSERT INTO users (email, password_hash) VALUES (:email, :hash) RETURNING id';
$st = $this->pdo->prepare($sql);
$st->execute([':email' => $email, ':hash' => $passwordHash]);
return (string)$st->fetchColumn();
}
public function updatePassword(string $userId, string $newHash): void
{
$sql = <<<SQL
UPDATE users
SET password_hash = :h,
updated_at = NOW(),
password_changed_at = NOW()
WHERE id = :id
SQL;
$st = $this->pdo->prepare($sql);
$st->execute([':h' => $newHash, ':id' => $userId]);
}
/**
* Normalise un bool venant de PDO/pgsql ('t','f',1,0,true,false,'1','0','true','false')
*/
private function toBool(mixed $v): bool
{
if (is_bool($v)) {
return $v;
}
if (is_int($v)) {
return $v === 1;
}
if (is_string($v)) {
$v = strtolower($v);
return in_array($v, ['t', '1', 'true', 'on', 'yes'], true);
}
return (bool)$v;
}
}