This commit is contained in:
Ty Clifford
2026-06-14 15:25:31 -04:00
parent 2f67ae718f
commit b7eaa81501
34 changed files with 3814 additions and 2 deletions
+114
View File
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Budget;
use PDO;
final class Auth
{
public function __construct(private PDO $pdo, private Settings $settings)
{
}
public function needsSetup(): bool
{
return (int) $this->pdo->query('SELECT COUNT(*) FROM users')->fetchColumn() === 0;
}
public function setup(string $email, string $password, bool $enableTwoFactor): ?string
{
if (!$this->needsSetup()) {
throw new \RuntimeException('The administrator account already exists.');
}
$secret = $enableTwoFactor ? Totp::generateSecret() : null;
$statement = $this->pdo->prepare(
'INSERT INTO users (email, password_hash, totp_secret) VALUES (:email, :password_hash, :secret)'
);
$statement->execute([
'email' => strtolower(trim($email)),
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'secret' => $secret,
]);
$this->settings->set('alerts.email', strtolower(trim($email)));
$this->settings->set('security.2fa_required', $enableTwoFactor);
$_SESSION['user_id'] = (int) $this->pdo->lastInsertId();
session_regenerate_id(true);
return $secret;
}
public function attempt(string $email, string $password): string
{
$statement = $this->pdo->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
$statement->execute(['email' => strtolower(trim($email))]);
$user = $statement->fetch();
if (!$user || !password_verify($password, (string) $user['password_hash'])) {
return 'invalid';
}
if ((bool) $this->settings->get('security.2fa_required', false)) {
if (empty($user['totp_secret'])) {
return 'unconfigured_2fa';
}
$_SESSION['pending_2fa_user'] = (int) $user['id'];
return '2fa';
}
$this->completeLogin((int) $user['id']);
return 'ok';
}
public function verifyTwoFactor(string $code): bool
{
$userId = (int) ($_SESSION['pending_2fa_user'] ?? 0);
if ($userId === 0) {
return false;
}
$statement = $this->pdo->prepare('SELECT totp_secret FROM users WHERE id = :id');
$statement->execute(['id' => $userId]);
$secret = (string) $statement->fetchColumn();
if ($secret === '' || !Totp::verify($secret, $code)) {
return false;
}
unset($_SESSION['pending_2fa_user']);
$this->completeLogin($userId);
return true;
}
public function check(): bool
{
return isset($_SESSION['user_id']);
}
public function id(): ?int
{
return $this->check() ? (int) $_SESSION['user_id'] : null;
}
public function user(): ?array
{
if (!$this->check()) {
return null;
}
$statement = $this->pdo->prepare('SELECT id, email, totp_secret, created_at FROM users WHERE id = :id');
$statement->execute(['id' => $this->id()]);
return $statement->fetch() ?: null;
}
public function logout(): void
{
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$parameters = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $parameters['path'], $parameters['domain'], $parameters['secure'], $parameters['httponly']);
}
session_destroy();
}
private function completeLogin(int $userId): void
{
$_SESSION['user_id'] = $userId;
session_regenerate_id(true);
}
}