WebOrbiton
v1.0.0.1

StocketBase

212 lines · 6.5 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/database.php';
  6. ​
  7. final class Auth
  8. {
  9. public const ROLE_SUPER_ADMIN = 'super_admin';
  10. public const ROLE_STORE_OWNER = 'store_owner';
  11. public const ROLE_STORE_MANAGER = 'store_manager';
  12. public const ROLE_CATALOG_EDITOR = 'catalog_editor';
  13. public const ROLE_PRODUCT_EDITOR = 'product_editor';
  14. public const ROLE_CATALOG_ASSISTANT = 'catalog_assistant';
  15. ​
  16. private const ROLE_LEVELS = [
  17. self::ROLE_SUPER_ADMIN => 60,
  18. self::ROLE_STORE_OWNER => 50,
  19. self::ROLE_STORE_MANAGER => 40,
  20. self::ROLE_CATALOG_EDITOR => 30,
  21. self::ROLE_PRODUCT_EDITOR => 20,
  22. self::ROLE_CATALOG_ASSISTANT => 10,
  23. ];
  24. ​
  25. private static ?array $current = null;
  26. ​
  27. public static function boot(): void
  28. {
  29. if (session_status() !== PHP_SESSION_ACTIVE) {
  30. session_set_cookie_params([
  31. 'lifetime' => 0,
  32. 'path' => '/',
  33. 'secure' => Config::get('SESSION_SECURE', '1') === '1',
  34. 'httponly' => true,
  35. 'samesite' => 'Lax',
  36. ]);
  37. session_name(Config::get('SESSION_COOKIE_NAME', 'stocketbase_session'));
  38. session_start();
  39. }
  40. }
  41. ​
  42. public static function verifyCredentials(string $username, string $password): ?array
  43. {
  44. $statement = Database::site()->prepare(
  45. 'SELECT * FROM team_accounts WHERE (username = :username OR email = :email) AND status = :status LIMIT 1'
  46. );
  47. $statement->execute(['username' => $username, 'email' => $username, 'status' => 'active']);
  48. $account = $statement->fetch();
  49. ​
  50. if (!$account || !password_verify($password, $account['password_hash'])) {
  51. return null;
  52. }
  53. ​
  54. return $account;
  55. }
  56. ​
  57. public static function completeLogin(array $account): void
  58. {
  59. session_regenerate_id(true);
  60. unset($_SESSION['team_2fa']);
  61. $_SESSION['team_account_id'] = (int) $account['id'];
  62. $_SESSION['team_role'] = $account['role'];
  63. $_SESSION['team_pw_fp'] = hash('sha256', (string) $account['password_hash']);
  64. self::$current = null;
  65. ​
  66. $update = Database::site()->prepare('UPDATE team_accounts SET last_login_at = NOW() WHERE id = :id');
  67. $update->execute(['id' => $account['id']]);
  68. }
  69. ​
  70. public static function startTwoFactor(array $account): void
  71. {
  72. session_regenerate_id(true);
  73. $_SESSION['team_2fa'] = ['id' => (int) $account['id'], 'expires' => time() + 300];
  74. }
  75. ​
  76. public static function pendingTwoFactorAccount(): ?array
  77. {
  78. $pending = $_SESSION['team_2fa'] ?? null;
  79. if (!is_array($pending) || (int) ($pending['expires'] ?? 0) < time()) {
  80. unset($_SESSION['team_2fa']);
  81. return null;
  82. }
  83. ​
  84. $statement = Database::site()->prepare('SELECT * FROM team_accounts WHERE id = :id AND status = :status LIMIT 1');
  85. $statement->execute(['id' => (int) $pending['id'], 'status' => 'active']);
  86. $account = $statement->fetch();
  87. ​
  88. if (!$account) {
  89. unset($_SESSION['team_2fa']);
  90. return null;
  91. }
  92. ​
  93. return $account;
  94. }
  95. ​
  96. public static function cancelTwoFactor(): void
  97. {
  98. unset($_SESSION['team_2fa']);
  99. }
  100. ​
  101. public static function logout(): void
  102. {
  103. self::$current = null;
  104. $_SESSION = [];
  105. if (session_status() === PHP_SESSION_ACTIVE) {
  106. session_destroy();
  107. }
  108. }
  109. ​
  110. public static function check(): bool
  111. {
  112. return self::user() !== null;
  113. }
  114. ​
  115. public static function refreshPasswordFingerprint(int $accountId): void
  116. {
  117. $statement = Database::site()->prepare('SELECT password_hash FROM team_accounts WHERE id = :id LIMIT 1');
  118. $statement->execute(['id' => $accountId]);
  119. $_SESSION['team_pw_fp'] = hash('sha256', (string) $statement->fetchColumn());
  120. self::$current = null;
  121. }
  122. ​
  123. public static function user(): ?array
  124. {
  125. if (!isset($_SESSION['team_account_id'])) {
  126. return null;
  127. }
  128. ​
  129. if (self::$current !== null) {
  130. return self::$current;
  131. }
  132. ​
  133. $statement = Database::site()->prepare('SELECT * FROM team_accounts WHERE id = :id LIMIT 1');
  134. $statement->execute(['id' => $_SESSION['team_account_id']]);
  135. $account = $statement->fetch();
  136. ​
  137. $fingerprintMatches = $account
  138. && isset($_SESSION['team_pw_fp'])
  139. && hash_equals((string) $_SESSION['team_pw_fp'], hash('sha256', (string) $account['password_hash']));
  140. ​
  141. if (!$account || ($account['status'] ?? 'active') !== 'active' || !$fingerprintMatches) {
  142. self::logout();
  143. return null;
  144. }
  145. ​
  146. $_SESSION['team_role'] = $account['role'];
  147. self::$current = $account;
  148. return self::$current;
  149. }
  150. ​
  151. public static function role(): ?string
  152. {
  153. return self::user() !== null ? ($_SESSION['team_role'] ?? null) : null;
  154. }
  155. ​
  156. public static function hasRoleAtLeast(string $role): bool
  157. {
  158. $current = self::role();
  159. if ($current === null || !isset(self::ROLE_LEVELS[$current]) || !isset(self::ROLE_LEVELS[$role])) {
  160. return false;
  161. }
  162. ​
  163. return self::ROLE_LEVELS[$current] >= self::ROLE_LEVELS[$role];
  164. }
  165. ​
  166. public static function isAdministrative(): bool
  167. {
  168. return self::hasRoleAtLeast(self::ROLE_STORE_MANAGER);
  169. }
  170. ​
  171. public static function canPublishDirectly(): bool
  172. {
  173. return self::hasRoleAtLeast(self::ROLE_CATALOG_EDITOR);
  174. }
  175. ​
  176. public static function canModerate(): bool
  177. {
  178. return self::hasRoleAtLeast(self::ROLE_STORE_MANAGER);
  179. }
  180. ​
  181. public static function requireLogin(): void
  182. {
  183. if (!self::check()) {
  184. header('Location: team-login.php');
  185. exit;
  186. }
  187. }
  188. ​
  189. public static function requireRoleAtLeast(string $role): void
  190. {
  191. self::requireLogin();
  192. if (!self::hasRoleAtLeast($role)) {
  193. http_response_code(403);
  194. echo 'Forbidden — insufficient role.';
  195. exit;
  196. }
  197. }
  198. ​
  199. public static function roleLabel(string $role): string
  200. {
  201. return match ($role) {
  202. self::ROLE_SUPER_ADMIN => 'Super Admin',
  203. self::ROLE_STORE_OWNER => 'Store Owner',
  204. self::ROLE_STORE_MANAGER => 'Store Manager',
  205. self::ROLE_CATALOG_EDITOR => 'Catalog Editor',
  206. self::ROLE_PRODUCT_EDITOR => 'Product Editor',
  207. self::ROLE_CATALOG_ASSISTANT => 'Catalog Assistant',
  208. default => 'Unknown',
  209. };
  210. }
  211. }
  212. ​