fix #29 : envoyer le lien magique par email (envoyer_mail_smtp)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 d’obtenir 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]);
|
||||
}
|
||||
}
|
||||
@@ -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 l’id (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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user