0, 'checkin' => 1, 'manager' => 2, 'admin' => 3]; const PERMISSIONS = [ 'checkin' => 'checkin', 'events' => 'manager', 'venues' => 'manager', 'orders' => 'manager', 'approvals' => 'manager', 'discounts' => 'manager', 'reports' => 'manager', 'users' => 'admin', 'settings' => 'admin', 'audit' => 'admin', ]; /** The WordPress capabilities that grant each level. A higher capability includes the lower levels. */ const LEVEL_CAPS = [ 'checkin' => ['gatecraft_checkin', 'gatecraft_manage', 'gatecraft_admin'], 'manager' => ['gatecraft_manage', 'gatecraft_admin'], 'admin' => ['gatecraft_admin'], ]; /** True when the current WordPress user may do $perm, checked with current_user_can(). */ function current_user_can_perm(string $perm): bool { $need = PERMISSIONS[$perm] ?? 'admin'; foreach (LEVEL_CAPS[$need] as $cap) { if (current_user_can($cap)) return true; } return false; } function gatecraft_role_of(WP_User $u): string { if (!$u->exists()) return 'customer'; if ($u->has_cap('gatecraft_admin')) return 'admin'; if ($u->has_cap('gatecraft_manage')) return 'manager'; if ($u->has_cap('gatecraft_checkin')) return 'checkin'; return 'customer'; } function account_row(int $userId): array { $a = q1('SELECT * FROM {accounts} WHERE user_id = ?', [$userId]); return $a ?: ['user_id' => $userId, 'google_sub' => null, 'avatar_url' => null, 'status' => 'active', 'totp_secret_enc' => null, 'totp_enabled' => 0, 'totp_last_step' => null, 'last_login_at' => null]; } function account_touch(int $userId): void { q('INSERT IGNORE INTO {accounts} (user_id, status, created_at) VALUES (?, ?, ?)', [$userId, 'active', now()]); } /** Array view of a WordPress user, in the shape the rest of the plugin expects. */ function user_view(WP_User $u): array { $a = account_row($u->ID); return [ 'id' => $u->ID, 'email' => strtolower($u->user_email), 'name' => $u->display_name !== $u->user_login ? $u->display_name : ($u->first_name ?: ''), 'avatar_url' => $a['avatar_url'] ?: null, 'role' => gatecraft_role_of($u), 'status' => $a['status'], 'totp_enabled' => (int)$a['totp_enabled'], 'has_password' => true, 'google_sub' => $a['google_sub'], ]; } function current_user(): ?array { static $cache = []; $id = get_current_user_id(); if (!$id) return null; if (!isset($cache[$id])) { $u = user_view(wp_get_current_user()); $cache[$id] = $u['status'] === 'active' ? $u : null; } return $cache[$id]; } function is_staff(?array $u = null): bool { $u ??= current_user(); return $u !== null && ROLE_RANK[$u['role']] >= ROLE_RANK['checkin']; } function can(string $perm, ?array $u = null): bool { if ($u === null) { // The signed-in user: an active ticketing account, and the capability checked with current_user_can(). return current_user() !== null && current_user_can_perm($perm); } $need = PERMISSIONS[$perm] ?? 'admin'; return ROLE_RANK[$u['role']] >= ROLE_RANK[$need]; } function require_login(): array { $u = current_user(); if (!$u) redirect(url('/login', ['next' => gatecraft_current_path()])); return $u; } function require_perm(string $perm): array { $u = current_user(); if (!$u) { wp_safe_redirect(wp_login_url(gatecraft_current_url())); exit; } if (!current_user_can_perm($perm)) abort(403, 'Your account does not have access to this page.'); return $u; } /** Sign a user in to WordPress (used by Google sign-in and the 2FA step). */ function login_user(int $userId, string $method, bool $remember = false): void { gatecraft_session_regenerate(); sess_del('csrf', 'pending_2fa', 'enroll_2fa', 'oauth'); $GLOBALS['gatecraft_login_method'] = $method; // tells our wp_login hook not to ask for 2FA again // Only mark the session as 2FA-verified when this login really included the authenticator step. A session without the // mark stays usable while the account needs no 2FA, and is refused the moment it does (a promotion to staff, say). if (str_contains($method, 'totp')) $GLOBALS['gatecraft_2fa_passed'] = $userId; wp_clear_auth_cookie(); wp_set_current_user($userId); sess_set('_uid', $userId); wp_set_auth_cookie($userId, $remember, is_ssl()); $u = get_userdata($userId); do_action('wp_login', $u->user_login, $u); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core hook, fired so other plugins see the sign-in account_touch($userId); q('UPDATE {accounts} SET last_login_at = ? WHERE user_id = ?', [now(), $userId]); // Attach earlier guest orders placed with the same verified email. q('UPDATE {orders} SET user_id = ? WHERE user_id IS NULL AND buyer_email = ?', [$userId, strtolower($u->user_email)]); q('INSERT INTO {audit_log} (user_id, action, details, ip, created_at) VALUES (?,?,?,?,?)', [$userId, 'login', wp_json_encode(['method' => $method]), client_ip(), now()]); } function logout_user(): void { wp_logout(); gatecraft_session_destroy(); } /** End every WordPress session of a user (password change, 2FA reset, role change, disable). */ function revoke_sessions(int $userId, bool $keepCurrent = false): void { trust_forget_user($userId); $m = WP_Session_Tokens::get_instance($userId); if ($keepCurrent && $userId === get_current_user_id()) $m->destroy_others(wp_get_session_token()); else $m->destroy_all(); } function staff_users(): array { $ids = get_users(['capability__in' => ['gatecraft_manage', 'gatecraft_admin'], 'fields' => 'ID', 'number' => 500]); if (!$ids) return []; $ids = array_map('intval', $ids); $disabled = q("SELECT user_id FROM {accounts} WHERE status = 'disabled' AND user_id IN (" . placeholders($ids) . ')', $ids)->fetchAll(Result::COLUMN); return array_values(array_diff(array_map('intval', $ids), array_map('intval', $disabled))); } function gatecraft_current_path(): string { return '/' . ltrim((string)($GLOBALS['gatecraft_path'] ?? '/'), '/'); } function gatecraft_current_url(): string { return (is_ssl() ? 'https://' : 'http://') . server('HTTP_HOST') . (server('REQUEST_URI') ?: '/'); }