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); } }