WebOrbiton
v1.0.0.0

StocketBase

178 lines · 5.9 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/database.php';
  6. require_once __DIR__ . '/language.php';
  7. ​
  8. final class UserAuth
  9. {
  10. private static ?array $current = null;
  11. ​
  12. public static function boot(): void
  13. {
  14. if (session_status() !== PHP_SESSION_ACTIVE) {
  15. session_set_cookie_params([
  16. 'lifetime' => 0,
  17. 'path' => '/',
  18. 'secure' => Config::get('SESSION_SECURE', '1') === '1',
  19. 'httponly' => true,
  20. 'samesite' => 'Lax',
  21. ]);
  22. session_start();
  23. }
  24. }
  25. ​
  26. public static function registrationEnabled(): bool
  27. {
  28. require_once __DIR__ . '/site-front.php';
  29. ​
  30. return SiteFront::settings()['registration_enabled'] === '1';
  31. }
  32. ​
  33. public static function register(string $email, string $password, string $displayName): array
  34. {
  35. if (!self::registrationEnabled()) {
  36. return [false, Language::get('auth_registration_closed', 'Registration is currently closed.')];
  37. }
  38. ​
  39. $email = trim(strtolower($email));
  40. ​
  41. if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
  42. return [false, Language::get('auth_invalid_email', 'Please provide a valid email address.')];
  43. }
  44. ​
  45. if (strlen($password) < 8) {
  46. return [false, Language::get('auth_password_too_short', 'Password must be at least 8 characters.')];
  47. }
  48. ​
  49. $db = Database::users();
  50. ​
  51. $existingStatement = $db->prepare('SELECT id FROM user_accounts WHERE email = :email LIMIT 1');
  52. $existingStatement->execute(['email' => $email]);
  53. if ($existingStatement->fetch()) {
  54. return [false, Language::get('auth_email_taken', 'An account with this email already exists.')];
  55. }
  56. ​
  57. $insert = $db->prepare(
  58. 'INSERT INTO user_accounts (email, password_hash, display_name, status) VALUES (:email, :hash, :display_name, :status)'
  59. );
  60. $passwordHash = password_hash($password, PASSWORD_DEFAULT);
  61. $insert->execute([
  62. 'email' => $email,
  63. 'hash' => $passwordHash,
  64. 'display_name' => mb_substr($displayName !== '' ? $displayName : explode('@', $email)[0], 0, 100),
  65. 'status' => 'active',
  66. ]);
  67. ​
  68. $newId = (int) $db->lastInsertId();
  69. self::startSession($newId, $passwordHash);
  70. ​
  71. return [true, null];
  72. }
  73. ​
  74. public static function attemptLogin(string $email, string $password): bool
  75. {
  76. $email = trim(strtolower($email));
  77. $db = Database::users();
  78. ​
  79. $statement = $db->prepare('SELECT * FROM user_accounts WHERE email = :email AND status = :status LIMIT 1');
  80. $statement->execute(['email' => $email, 'status' => 'active']);
  81. $account = $statement->fetch();
  82. ​
  83. if (!$account || !password_verify($password, $account['password_hash'])) {
  84. return false;
  85. }
  86. ​
  87. self::startSession((int) $account['id'], (string) $account['password_hash']);
  88. ​
  89. $update = $db->prepare('UPDATE user_accounts SET last_login_at = NOW() WHERE id = :id');
  90. $update->execute(['id' => $account['id']]);
  91. ​
  92. return true;
  93. }
  94. ​
  95. private static function startSession(int $userId, string $passwordHash): void
  96. {
  97. session_regenerate_id(true);
  98. $_SESSION['user_account_id'] = $userId;
  99. $_SESSION['user_pw_fp'] = hash('sha256', $passwordHash);
  100. self::$current = null;
  101. }
  102. ​
  103. public static function refreshPasswordFingerprint(int $userId): void
  104. {
  105. $statement = Database::users()->prepare('SELECT password_hash FROM user_accounts WHERE id = :id LIMIT 1');
  106. $statement->execute(['id' => $userId]);
  107. $_SESSION['user_pw_fp'] = hash('sha256', (string) $statement->fetchColumn());
  108. self::$current = null;
  109. }
  110. ​
  111. public static function verifyPassword(int $userId, string $password): bool
  112. {
  113. $statement = Database::users()->prepare('SELECT password_hash FROM user_accounts WHERE id = :id LIMIT 1');
  114. $statement->execute(['id' => $userId]);
  115. ​
  116. return password_verify($password, (string) $statement->fetchColumn());
  117. }
  118. ​
  119. public static function logout(): void
  120. {
  121. unset($_SESSION['user_account_id'], $_SESSION['user_pw_fp']);
  122. self::$current = null;
  123. if (session_status() === PHP_SESSION_ACTIVE && !headers_sent()) {
  124. session_regenerate_id(true);
  125. }
  126. }
  127. ​
  128. public static function check(): bool
  129. {
  130. return self::user() !== null;
  131. }
  132. ​
  133. public static function user(): ?array
  134. {
  135. if (!isset($_SESSION['user_account_id'])) {
  136. return null;
  137. }
  138. ​
  139. if (self::$current !== null) {
  140. return self::$current;
  141. }
  142. ​
  143. $statement = Database::users()->prepare('SELECT * FROM user_accounts WHERE id = :id LIMIT 1');
  144. $statement->execute(['id' => $_SESSION['user_account_id']]);
  145. $account = $statement->fetch();
  146. ​
  147. $fingerprintMatches = isset($_SESSION['user_pw_fp'])
  148. && $account
  149. && hash_equals((string) $_SESSION['user_pw_fp'], hash('sha256', (string) $account['password_hash']));
  150. ​
  151. if (!$account || $account['status'] !== 'active' || !$fingerprintMatches) {
  152. self::logout();
  153. return null;
  154. }
  155. ​
  156. self::$current = $account;
  157. return self::$current;
  158. }
  159. ​
  160. public static function hasActiveSubscription(int $userAccountId): bool
  161. {
  162. $statement = Database::users()->prepare(
  163. "SELECT id FROM subscriptions WHERE user_account_id = :id AND status IN ('active', 'trialing') AND (current_period_end IS NULL OR current_period_end > NOW()) LIMIT 1"
  164. );
  165. $statement->execute(['id' => $userAccountId]);
  166. ​
  167. return (bool) $statement->fetch();
  168. }
  169. ​
  170. public static function requireLogin(): void
  171. {
  172. if (!self::check()) {
  173. header('Location: user-login.php');
  174. exit;
  175. }
  176. }
  177. }
  178. ​