logger = Qixit_Logger::get_instance(); } /** * Login and get JWT token * * @param string $username Vendor username * @param string $password Vendor password * @return array Result array with success, token, vendorId, and error */ public function login($username, $password) { $this->logger->log('Attempting login for vendor: ' . $username); if (empty($username) || empty($password)) { return array( 'success' => false, 'error' => __('Username and password are required', '2waymicropay') ); } $api_base_url = qixit_get_api_url(); $url = trailingslashit($api_base_url) . 'api/auth/login'; $body = json_encode(array( 'username' => $username, 'password' => $password )); $args = array( 'method' => 'POST', 'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json' ), 'body' => $body, // Auth login — admin/publisher initiated, not on visitor page load 'timeout' => get_option('qixit_api_timeout', 8), 'sslverify' => true ); $response = wp_remote_post($url, $args); if (is_wp_error($response)) { $error_message = $response->get_error_message(); $this->logger->log('Login error: ' . $error_message, 'error'); return array( 'success' => false, // translators: %s: HTTP/client error message 'error' => sprintf(__('Connection error: %s', '2waymicropay'), $error_message) ); } $status_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ($status_code === 200 && isset($data['token'])) { // Store token and related data $expires = time() + (QIXIT_TOKEN_EXPIRY_HOURS * 3600); update_option('qixit_jwt_token', $data['token']); update_option('qixit_jwt_expires', $expires); update_option('qixit_vendor_id', isset($data['vendorId']) ? $data['vendorId'] : 0); update_option('qixit_vendor_username', $username); $this->logger->log('Login successful for vendor: ' . $username . ', Token expires: ' . gmdate('Y-m-d H:i:s', $expires)); return array( 'success' => true, 'token' => $data['token'], 'vendorId' => isset($data['vendorId']) ? $data['vendorId'] : 0, 'username' => isset($data['username']) ? $data['username'] : $username, 'description' => isset($data['description']) ? $data['description'] : '', 'expires' => $expires ); } else { // Products API can return 200 with a step-up challenge instead of JWT. // WordPress plugin auth is service-to-service and cannot complete interactive 2FA/email verification. if ($status_code === 200 && is_array($data)) { if (!empty($data['requiresTwoFactor'])) { $error_message = __('Vendor account requires 2FA step-up. WordPress plugin login cannot complete interactive verification.', '2waymicropay'); $this->logger->log('Login blocked by 2FA requirement for vendor: ' . $username, 'error'); return array( 'success' => false, 'error' => $error_message, 'status_code' => $status_code ); } if (!empty($data['requiresEmailVerification'])) { $error_message = __('Vendor account requires email/IP verification step-up. WordPress plugin login cannot complete this challenge.', '2waymicropay'); $this->logger->log('Login blocked by email/IP step-up for vendor: ' . $username, 'error'); return array( 'success' => false, 'error' => $error_message, 'status_code' => $status_code ); } } $error_message = isset($data['message']) ? $data['message'] : __('Authentication failed', '2waymicropay'); $this->logger->log('Login failed: ' . $error_message . ' (Status: ' . $status_code . ')', 'error'); return array( 'success' => false, 'error' => $error_message, 'status_code' => $status_code ); } } /** * Get valid JWT token (auto-refresh if needed) * * @return string|false JWT token or false on failure */ public function get_valid_token() { $token = get_option('qixit_jwt_token'); $expires = get_option('qixit_jwt_expires', 0); $refresh_buffer = QIXIT_TOKEN_REFRESH_BUFFER_HOURS * 3600; // Check if token exists and is not expired or about to expire if (!empty($token) && (time() < ($expires - $refresh_buffer))) { return $token; } // Token expired or about to expire, refresh it $this->logger->log('Token expired or about to expire, refreshing...'); $username = get_option('qixit_vendor_username'); $stored_password = get_option('qixit_vendor_password'); if (empty($username) || empty($stored_password)) { $this->logger->log('Cannot refresh token: credentials not configured', 'error'); return false; } $last_error = __('Authentication failed', '2waymicropay'); $candidates = $this->get_password_candidates($stored_password); foreach ($candidates as $candidate) { $result = $this->login($username, $candidate); if ($result['success']) { return $result['token']; } $last_error = isset($result['error']) ? $result['error'] : $last_error; } $this->logger->log('Token refresh failed: ' . $last_error, 'error'); return false; } /** * Build fallback password candidates from stored format. * Handles encrypted, base64, and raw legacy values. * * @param string $stored_password * @return array */ public function get_password_candidates($stored_password) { $candidates = array(); if (empty($stored_password)) { return $candidates; } $decoded = base64_decode($stored_password, true); if ($decoded !== false && strpos($decoded, '::') !== false) { $decrypted = $this->decrypt_password($stored_password); if (!empty($decrypted)) { $candidates[] = $decrypted; } // If decrypt fails (e.g. keys changed), still attempt raw. $candidates[] = $stored_password; } elseif ($decoded !== false && base64_encode($decoded) === $stored_password) { if (!empty($decoded)) { $candidates[] = $decoded; } $candidates[] = $stored_password; } else { $candidates[] = $stored_password; } return array_values(array_filter(array_unique($candidates), function($v) { return is_string($v) && $v !== ''; })); } /** * Check if authenticated * * @return bool True if authenticated */ public function is_authenticated() { $site_key = get_option('qixit_site_api_key', ''); if (empty($site_key)) { $site_key = get_option('qixit_developer_api_key', ''); } if (empty($site_key)) { return false; } $check = $this->validate_site_key_binding(); return !empty($check['success']); } /** * Get vendor ID * * @return int Vendor ID or 0 */ public function get_vendor_id() { return intval(get_option('qixit_vendor_id', 0)); } /** * Get vendor username * * @return string Vendor username */ public function get_vendor_username() { return get_option('qixit_vendor_username', ''); } /** * Logout and clear stored credentials */ public function logout() { delete_option('qixit_jwt_token'); delete_option('qixit_jwt_expires'); delete_option('qixit_vendor_id'); $this->logger->log('User logged out, token cleared'); } /** * Test API connection * * @return array Result with success status and data */ public function test_connection() { $api_base_url = qixit_get_api_url(); $url = trailingslashit($api_base_url) . 'api/auth/test'; $args = array( // Test connection — admin-initiated, 8s is acceptable 'timeout' => get_option('qixit_api_timeout', 8), 'sslverify' => true ); $response = wp_remote_get($url, $args); if (is_wp_error($response)) { return array( 'success' => false, 'error' => $response->get_error_message() ); } $status_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ($status_code === 200) { return array( 'success' => true, 'data' => $data, 'message' => isset($data['message']) ? $data['message'] : 'API is reachable' ); } else { return array( 'success' => false, 'error' => isset($data['message']) ? $data['message'] : 'Connection failed', 'status_code' => $status_code ); } } /** * Get token expiration time * * @return int Unix timestamp or 0 */ public function get_token_expiry() { return intval(get_option('qixit_jwt_expires', 0)); } /** * Check if token is about to expire (within refresh buffer) * * @return bool True if token needs refresh */ public function token_needs_refresh() { $expires = $this->get_token_expiry(); $refresh_buffer = QIXIT_TOKEN_REFRESH_BUFFER_HOURS * 3600; return (time() >= ($expires - $refresh_buffer)); } /** * Get token expiry status * * @return array Status information */ public function get_token_status() { $site_key = get_option('qixit_site_api_key', ''); if (empty($site_key)) { $site_key = get_option('qixit_developer_api_key', ''); } if (!empty($site_key)) { $connection_check = $this->validate_site_key_binding(); if (!empty($connection_check['success'])) { return array( 'status' => 'valid', 'message' => __('Connected', '2waymicropay'), 'expires' => 0, 'expires_in' => 0 ); } return array( 'status' => 'not_authenticated', // translators: %s is API validation error from Products API. 'message' => sprintf( /* translators: %s is API validation error from Products API. */ __('Site API Key is present, but could not be validated (%s).', '2waymicropay'), isset($connection_check['error']) ? $connection_check['error'] : __('unknown error', '2waymicropay') ), 'expires' => 0, 'expires_in' => 0 ); } return array( 'status' => 'not_authenticated', 'message' => __('Not connected. Add a valid Site API Key in settings.', '2waymicropay'), 'expires' => 0, 'expires_in' => 0 ); } /** * Validates that the configured Site API Key and identity binding are usable. * * This catches cases where a Site API Key exists but is not valid/usable. * Uses a short-lived transient cache to avoid repeated checks * on every settings page load. * * @return array{success:bool,error?:string} */ private function validate_site_key_binding() { $site_key = get_option('qixit_site_api_key', ''); if (empty($site_key)) { $site_key = get_option('qixit_developer_api_key', ''); } // Validate that the configured Site API key is usable for this plugin. $vendor_probe = $this->probe_site_key_binding($site_key); if (empty($vendor_probe['success'])) { return array( 'success' => false, // translators: %s is API validation error from Products API. 'error' => sprintf( /* translators: %s is API validation error from Products API. */ __('Site API Key validation failed: %s', '2waymicropay'), isset($vendor_probe['error']) ? $vendor_probe['error'] : __('unknown error', '2waymicropay') ) ); } return array('success' => true); } /** * Perform a direct API probe with the supplied Site API key. * * @param string $site_key * @return array{success:bool,error?:string} */ private function probe_site_key_binding($site_key) { $api_base_url = qixit_get_products_api_url(); $url = trailingslashit($api_base_url) . 'api/products'; $app_id = defined('QIXIT_APP_ID') ? QIXIT_APP_ID : 'wp-2waymicropay-plugin-v2'; $args = array( 'method' => 'GET', 'headers' => array( 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'X-API-Key' => $site_key, 'X-App-ID' => $app_id, ), 'timeout' => get_option('qixit_api_timeout', 8), 'sslverify' => true, ); $response = wp_remote_get($url, $args); if (is_wp_error($response)) { return array( 'success' => false, 'error' => $response->get_error_message(), ); } $status_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ($status_code >= 200 && $status_code < 300) { return array('success' => true); } $error = isset($data['message']) ? $data['message'] : ('HTTP ' . $status_code); return array( 'success' => false, 'error' => (string) $error, ); } /** * Encrypt password for storage (basic encryption) * Note: In production, use proper encryption methods * * @param string $password Plain text password * @return string Encrypted password */ public function encrypt_password($password) { if (!defined('AUTH_KEY') || !defined('AUTH_SALT')) { return base64_encode($password); // Fallback } $key = AUTH_KEY . AUTH_SALT; $iv_length = openssl_cipher_iv_length('aes-256-cbc'); $iv = openssl_random_pseudo_bytes($iv_length); $encrypted = openssl_encrypt($password, 'aes-256-cbc', $key, 0, $iv); return base64_encode($iv . '::' . $encrypted); } /** * Decrypt password from storage * * @param string $encrypted Encrypted password * @return string Plain text password */ public function decrypt_password($encrypted) { if (!defined('AUTH_KEY') || !defined('AUTH_SALT')) { return base64_decode($encrypted); // Fallback } $decoded = base64_decode($encrypted); if (strpos($decoded, '::') === false) { return $decoded; // Already decrypted or using fallback } list($iv, $encrypted_data) = explode('::', $decoded, 2); $key = AUTH_KEY . AUTH_SALT; return openssl_decrypt($encrypted_data, 'aes-256-cbc', $key, 0, $iv); } } // Initialize singleton Qixit_Auth::get_instance();