v1.0.0.1
StocketBase
- <?php
-
- declare(strict_types=1);
-
- require_once __DIR__ . '/database.php';
-
- final class Auth
- {
- public const ROLE_SUPER_ADMIN = 'super_admin';
- public const ROLE_STORE_OWNER = 'store_owner';
- public const ROLE_STORE_MANAGER = 'store_manager';
- public const ROLE_CATALOG_EDITOR = 'catalog_editor';
- public const ROLE_PRODUCT_EDITOR = 'product_editor';
- public const ROLE_CATALOG_ASSISTANT = 'catalog_assistant';
-
- private const ROLE_LEVELS = [
- self::ROLE_SUPER_ADMIN => 60,
- self::ROLE_STORE_OWNER => 50,
- self::ROLE_STORE_MANAGER => 40,
- self::ROLE_CATALOG_EDITOR => 30,
- self::ROLE_PRODUCT_EDITOR => 20,
- self::ROLE_CATALOG_ASSISTANT => 10,
- ];
-
- private static ?array $current = null;
-
- public static function boot(): void
- {
- if (session_status() !== PHP_SESSION_ACTIVE) {
- session_set_cookie_params([
- 'lifetime' => 0,
- 'path' => '/',
- 'secure' => Config::get('SESSION_SECURE', '1') === '1',
- 'httponly' => true,
- 'samesite' => 'Lax',
- ]);
- session_name(Config::get('SESSION_COOKIE_NAME', 'stocketbase_session'));
- session_start();
- }
- }
-
- public static function verifyCredentials(string $username, string $password): ?array
- {
- $statement = Database::site()->prepare(
- 'SELECT * FROM team_accounts WHERE (username = :username OR email = :email) AND status = :status LIMIT 1'
- );
- $statement->execute(['username' => $username, 'email' => $username, 'status' => 'active']);
- $account = $statement->fetch();
-
- if (!$account || !password_verify($password, $account['password_hash'])) {
- return null;
- }
-
- return $account;
- }
-
- public static function completeLogin(array $account): void
- {
- session_regenerate_id(true);
- unset($_SESSION['team_2fa']);
- $_SESSION['team_account_id'] = (int) $account['id'];
- $_SESSION['team_role'] = $account['role'];
- $_SESSION['team_pw_fp'] = hash('sha256', (string) $account['password_hash']);
- self::$current = null;
-
- $update = Database::site()->prepare('UPDATE team_accounts SET last_login_at = NOW() WHERE id = :id');
- $update->execute(['id' => $account['id']]);
- }
-
- public static function startTwoFactor(array $account): void
- {
- session_regenerate_id(true);
- $_SESSION['team_2fa'] = ['id' => (int) $account['id'], 'expires' => time() + 300];
- }
-
- public static function pendingTwoFactorAccount(): ?array
- {
- $pending = $_SESSION['team_2fa'] ?? null;
- if (!is_array($pending) || (int) ($pending['expires'] ?? 0) < time()) {
- unset($_SESSION['team_2fa']);
- return null;
- }
-
- $statement = Database::site()->prepare('SELECT * FROM team_accounts WHERE id = :id AND status = :status LIMIT 1');
- $statement->execute(['id' => (int) $pending['id'], 'status' => 'active']);
- $account = $statement->fetch();
-
- if (!$account) {
- unset($_SESSION['team_2fa']);
- return null;
- }
-
- return $account;
- }
-
- public static function cancelTwoFactor(): void
- {
- unset($_SESSION['team_2fa']);
- }
-
- public static function logout(): void
- {
- self::$current = null;
- $_SESSION = [];
- if (session_status() === PHP_SESSION_ACTIVE) {
- session_destroy();
- }
- }
-
- public static function check(): bool
- {
- return self::user() !== null;
- }
-
- public static function refreshPasswordFingerprint(int $accountId): void
- {
- $statement = Database::site()->prepare('SELECT password_hash FROM team_accounts WHERE id = :id LIMIT 1');
- $statement->execute(['id' => $accountId]);
- $_SESSION['team_pw_fp'] = hash('sha256', (string) $statement->fetchColumn());
- self::$current = null;
- }
-
- public static function user(): ?array
- {
- if (!isset($_SESSION['team_account_id'])) {
- return null;
- }
-
- if (self::$current !== null) {
- return self::$current;
- }
-
- $statement = Database::site()->prepare('SELECT * FROM team_accounts WHERE id = :id LIMIT 1');
- $statement->execute(['id' => $_SESSION['team_account_id']]);
- $account = $statement->fetch();
-
- $fingerprintMatches = $account
- && isset($_SESSION['team_pw_fp'])
- && hash_equals((string) $_SESSION['team_pw_fp'], hash('sha256', (string) $account['password_hash']));
-
- if (!$account || ($account['status'] ?? 'active') !== 'active' || !$fingerprintMatches) {
- self::logout();
- return null;
- }
-
- $_SESSION['team_role'] = $account['role'];
- self::$current = $account;
- return self::$current;
- }
-
- public static function role(): ?string
- {
- return self::user() !== null ? ($_SESSION['team_role'] ?? null) : null;
- }
-
- public static function hasRoleAtLeast(string $role): bool
- {
- $current = self::role();
- if ($current === null || !isset(self::ROLE_LEVELS[$current]) || !isset(self::ROLE_LEVELS[$role])) {
- return false;
- }
-
- return self::ROLE_LEVELS[$current] >= self::ROLE_LEVELS[$role];
- }
-
- public static function isAdministrative(): bool
- {
- return self::hasRoleAtLeast(self::ROLE_STORE_MANAGER);
- }
-
- public static function canPublishDirectly(): bool
- {
- return self::hasRoleAtLeast(self::ROLE_CATALOG_EDITOR);
- }
-
- public static function canModerate(): bool
- {
- return self::hasRoleAtLeast(self::ROLE_STORE_MANAGER);
- }
-
- public static function requireLogin(): void
- {
- if (!self::check()) {
- header('Location: team-login.php');
- exit;
- }
- }
-
- public static function requireRoleAtLeast(string $role): void
- {
- self::requireLogin();
- if (!self::hasRoleAtLeast($role)) {
- http_response_code(403);
- echo 'Forbidden — insufficient role.';
- exit;
- }
- }
-
- public static function roleLabel(string $role): string
- {
- return match ($role) {
- self::ROLE_SUPER_ADMIN => 'Super Admin',
- self::ROLE_STORE_OWNER => 'Store Owner',
- self::ROLE_STORE_MANAGER => 'Store Manager',
- self::ROLE_CATALOG_EDITOR => 'Catalog Editor',
- self::ROLE_PRODUCT_EDITOR => 'Product Editor',
- self::ROLE_CATALOG_ASSISTANT => 'Catalog Assistant',
- default => 'Unknown',
- };
- }
- }
-