130 lines
3.7 KiB
PHP
130 lines
3.7 KiB
PHP
<?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;
|
||
}
|
||
}
|