load->library('desk_moloni/moloni_api_client'); $this->load->library('desk_moloni/error_handler'); // Set JSON content type $this->output->set_content_type('application/json'); } /** * Main webhook endpoint for Moloni events * * Accepts POST requests from Moloni webhook system * URL: admin/desk_moloni/webhook/receive */ public function receive() { try { // Only accept POST requests if ($this->input->method() !== 'post') { throw new Exception('Only POST requests allowed', 405); } // Get raw POST data $raw_input = file_get_contents('php://input'); if (empty($raw_input)) { throw new Exception('Empty webhook payload', 400); } // Decode JSON payload $payload = json_decode($raw_input, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('Invalid JSON payload: ' . json_last_error_msg(), 400); } // Get webhook signature for verification $signature = $this->input->get_request_header('X-Moloni-Signature') ?? $this->input->get_request_header('X-Webhook-Signature'); // Log webhook received log_activity('Desk-Moloni: Webhook received - Event: ' . ($payload['event'] ?? 'unknown')); // Rate limiting check for webhooks if (!$this->check_webhook_rate_limit()) { throw new Exception('Webhook rate limit exceeded', 429); } // Process webhook through API client $success = $this->moloni_api_client->process_webhook($payload, $signature); if ($success) { // Return success response $this->output ->set_status_header(200) ->set_output(json_encode([ 'status' => 'success', 'message' => 'Webhook processed successfully', 'event' => $payload['event'] ?? null, 'timestamp' => date('Y-m-d H:i:s') ])); } else { throw new Exception('Webhook processing failed', 500); } } catch (Exception $e) { // Log error $this->error_handler->log_error( 'webhook_processing', $e->getMessage(), [ 'payload' => $payload ?? null, 'signature' => $signature ?? null, 'ip_address' => $this->input->ip_address(), 'user_agent' => $this->input->user_agent() ], 'medium' ); // Return error response $http_code = is_numeric($e->getCode()) ? $e->getCode() : 500; $this->output ->set_status_header($http_code) ->set_output(json_encode([ 'status' => 'error', 'message' => $e->getMessage(), 'timestamp' => date('Y-m-d H:i:s') ])); } } /** * Webhook configuration endpoint for administrators * * Allows admins to configure webhook settings * URL: admin/desk_moloni/webhook/configure */ public function configure() { // Check if user has admin permissions if (!has_permission('desk_moloni', '', 'edit')) { access_denied('Desk-Moloni Webhook Configuration'); } if ($this->input->post()) { $this->handle_webhook_configuration(); } $data = []; // Get current webhook settings $data['webhook_url'] = site_url('admin/desk_moloni/webhook/receive'); $data['webhook_secret'] = get_option('desk_moloni_webhook_secret'); $data['webhook_enabled'] = (bool)get_option('desk_moloni_webhook_enabled', false); $data['webhook_events'] = explode(',', get_option('desk_moloni_webhook_events', 'customer.created,customer.updated,product.created,product.updated,invoice.created,invoice.updated')); // Available webhook events $data['available_events'] = [ 'customer.created' => 'Customer Created', 'customer.updated' => 'Customer Updated', 'customer.deleted' => 'Customer Deleted', 'product.created' => 'Product Created', 'product.updated' => 'Product Updated', 'product.deleted' => 'Product Deleted', 'invoice.created' => 'Invoice Created', 'invoice.updated' => 'Invoice Updated', 'invoice.paid' => 'Invoice Paid', 'estimate.created' => 'Estimate Created', 'estimate.updated' => 'Estimate Updated' ]; // Get webhook statistics $data['webhook_stats'] = $this->get_webhook_statistics(); // Generate CSRF token $data['csrf_token'] = $this->security->get_csrf_hash(); // Load view $data['title'] = _l('desk_moloni_webhook_configuration'); $this->load->view('admin/includes/header', $data); $this->load->view('admin/modules/desk_moloni/webhook_configuration', $data); $this->load->view('admin/includes/footer'); } /** * Test webhook endpoint * * Allows testing webhook functionality * URL: admin/desk_moloni/webhook/test */ public function test() { // Check permissions if (!has_permission('desk_moloni', '', 'view')) { access_denied('Desk-Moloni Webhook Test'); } try { // Create test webhook payload $test_payload = [ 'event' => 'test.webhook', 'data' => [ 'test' => true, 'timestamp' => time(), 'message' => 'This is a test webhook from Desk-Moloni' ], 'webhook_id' => uniqid('test_'), 'created_at' => date('Y-m-d H:i:s') ]; // Process test webhook $success = $this->moloni_api_client->process_webhook($test_payload); if ($success) { set_alert('success', _l('webhook_test_successful')); } else { set_alert('danger', _l('webhook_test_failed')); } } catch (Exception $e) { set_alert('danger', _l('webhook_test_error') . ': ' . $e->getMessage()); } redirect(admin_url('desk_moloni/webhook/configure')); } /** * Webhook logs endpoint * * Displays webhook processing logs * URL: admin/desk_moloni/webhook/logs */ public function logs() { // Check permissions if (!has_permission('desk_moloni', '', 'view')) { access_denied('Desk-Moloni Webhook Logs'); } $data = []; // Load logs model // Use API client logging or fallback if API log model is unavailable if (file_exists(APPPATH . 'modules/desk_moloni/models/Desk_moloni_api_log_model.php')) { $this->load->model('desk_moloni/desk_moloni_api_log_model'); } // Get webhook logs (last 7 days) $data['logs'] = isset($this->desk_moloni_api_log_model) ? $this->desk_moloni_api_log_model->get_logs([ 'endpoint_like' => 'webhook%', 'start_date' => date('Y-m-d', strtotime('-7 days')), 'end_date' => date('Y-m-d'), 'limit' => 100, 'order_by' => 'timestamp DESC' ]) : []; // Get log statistics $data['log_stats'] = [ 'total_webhooks' => count($data['logs']), 'successful' => count(array_filter($data['logs'], function($log) { return empty($log['error']); })), 'failed' => count(array_filter($data['logs'], function($log) { return !empty($log['error']); })), 'last_24h' => count(array_filter($data['logs'], function($log) { return strtotime($log['timestamp']) > (time() - 86400); })) ]; // Load view $data['title'] = _l('desk_moloni_webhook_logs'); $this->load->view('admin/includes/header', $data); $this->load->view('admin/modules/desk_moloni/webhook_logs', $data); $this->load->view('admin/includes/footer'); } /** * Health check endpoint for webhooks * * URL: admin/desk_moloni/webhook/health */ public function health() { try { $health_data = [ 'status' => 'healthy', 'webhook_enabled' => (bool)get_option('desk_moloni_webhook_enabled', false), 'webhook_secret_configured' => !empty(get_option('desk_moloni_webhook_secret')), 'timestamp' => date('Y-m-d H:i:s'), 'checks' => [] ]; // Check webhook configuration if (!$health_data['webhook_enabled']) { $health_data['status'] = 'warning'; $health_data['checks'][] = 'Webhooks are disabled'; } if (!$health_data['webhook_secret_configured']) { $health_data['status'] = 'warning'; $health_data['checks'][] = 'Webhook secret not configured'; } // Check recent webhook activity $this->load->model('desk_moloni_api_log_model'); $recent_webhooks = $this->desk_moloni_api_log_model->get_logs([ 'endpoint_like' => 'webhook%', 'start_date' => date('Y-m-d', strtotime('-1 hour')), 'limit' => 10 ]); $health_data['recent_activity'] = [ 'webhooks_last_hour' => count($recent_webhooks), 'last_webhook' => !empty($recent_webhooks) ? $recent_webhooks[0]['timestamp'] : null ]; $this->output ->set_status_header(200) ->set_output(json_encode($health_data)); } catch (Exception $e) { $this->output ->set_status_header(500) ->set_output(json_encode([ 'status' => 'error', 'message' => $e->getMessage(), 'timestamp' => date('Y-m-d H:i:s') ])); } } /** * Handle webhook configuration form submission */ private function handle_webhook_configuration() { try { // Validate CSRF token if (!$this->security->get_csrf_hash()) { throw new Exception('Invalid CSRF token'); } // Get form data $webhook_enabled = (bool)$this->input->post('webhook_enabled'); $webhook_secret = $this->input->post('webhook_secret', true); $webhook_events = $this->input->post('webhook_events') ?? []; // Validate webhook secret if ($webhook_enabled && empty($webhook_secret)) { throw new Exception('Webhook secret is required when webhooks are enabled'); } if (!empty($webhook_secret) && strlen($webhook_secret) < 16) { throw new Exception('Webhook secret must be at least 16 characters long'); } // Save configuration update_option('desk_moloni_webhook_enabled', $webhook_enabled); update_option('desk_moloni_webhook_secret', $webhook_secret); update_option('desk_moloni_webhook_events', implode(',', $webhook_events)); // Log configuration change log_activity('Desk-Moloni: Webhook configuration updated by ' . get_staff_full_name()); set_alert('success', _l('webhook_configuration_saved')); } catch (Exception $e) { log_activity('Desk-Moloni: Webhook configuration failed - ' . $e->getMessage()); set_alert('danger', _l('webhook_configuration_failed') . ': ' . $e->getMessage()); } } /** * Check webhook rate limiting * * @return bool True if within rate limits */ private function check_webhook_rate_limit() { $rate_limit_key = 'webhook_rate_limit_' . $this->input->ip_address(); $current_count = $this->session->userdata($rate_limit_key) ?? 0; $max_webhooks_per_minute = 60; // Configurable if ($current_count >= $max_webhooks_per_minute) { return false; } // Increment counter with 1 minute expiry $this->session->set_userdata($rate_limit_key, $current_count + 1); return true; } /** * Get webhook processing statistics * * @return array Statistics data */ private function get_webhook_statistics() { $this->load->model('desk_moloni_api_log_model'); // Get statistics for last 30 days $logs = $this->desk_moloni_api_log_model->get_logs([ 'endpoint_like' => 'webhook%', 'start_date' => date('Y-m-d', strtotime('-30 days')), 'limit' => 1000 ]); $stats = [ 'total_webhooks' => count($logs), 'successful' => 0, 'failed' => 0, 'by_event' => [], 'by_day' => [] ]; foreach ($logs as $log) { // Count success/failure if (empty($log['error'])) { $stats['successful']++; } else { $stats['failed']++; } // Count by event type if (isset($log['endpoint'])) { $event = str_replace('webhook:', '', $log['endpoint']); $stats['by_event'][$event] = ($stats['by_event'][$event] ?? 0) + 1; } // Count by day $day = date('Y-m-d', strtotime($log['timestamp'])); $stats['by_day'][$day] = ($stats['by_day'][$day] ?? 0) + 1; } return $stats; } }