Files
desk-moloni/modules/desk_moloni/controllers/OAuthController.php
Emanuel Almeida c19f6fd9ee fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module
- Normalize hooks to after_client_* and instantiate PerfexHooks safely
- Fix OAuthController view path and API client class name
- Add missing admin views for webhook config/logs; adjust view loading
- Harden client portal routes and admin routes mapping
- Make Dashboard/Logs/Queue tolerant to optional model methods
- Align log details query with existing schema; avoid broken joins

This makes the module operational in Perfex (admin + client), reduces 404s,
and avoids fatal errors due to inconsistent tables/methods.
2025-09-11 17:38:45 +01:00

425 lines
14 KiB
PHP

<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* OAuth Controller for Moloni Integration
*
* Handles OAuth 2.0 authentication flow for Perfex CRM integration
*
* @package DeskMoloni
* @author Descomplicar®
* @copyright 2025 Descomplicar
* @version 3.0.0
*/
class OAuthController extends AdminController
{
public function __construct()
{
parent::__construct();
// Check if user has permission to access Moloni settings
if (!has_permission('desk_moloni', '', 'view')) {
access_denied('Desk-Moloni');
}
// Load required libraries
$this->load->library('desk_moloni/moloni_oauth');
$this->load->library('desk_moloni/token_manager');
$this->load->helper('url');
// Set page title
$this->app_menu->add_breadcrumb(_l('desk_moloni'), admin_url('desk_moloni'));
$this->app_menu->add_breadcrumb(_l('oauth_settings'), '');
}
/**
* OAuth settings and initiation page
*/
public function index()
{
// Handle form submission
if ($this->input->post()) {
$this->handle_oauth_configuration();
}
$data = [];
// Get current OAuth status
$data['oauth_status'] = $this->moloni_oauth->get_status();
$data['token_status'] = $this->token_manager->get_token_status();
// Get configuration test results
$data['config_test'] = $this->moloni_oauth->test_configuration();
// Get current settings
$data['client_id'] = get_option('desk_moloni_client_id');
$data['client_secret'] = get_option('desk_moloni_client_secret');
$data['use_pkce'] = (bool)get_option('desk_moloni_use_pkce', true);
// Generate CSRF token for forms
$data['csrf_token'] = $this->security->get_csrf_hash();
// Load view
$data['title'] = _l('desk_moloni_oauth_settings');
$this->load->view('admin/includes/header', $data);
$this->load->view('admin/modules/desk_moloni/oauth_setup', $data);
$this->load->view('admin/includes/footer');
}
/**
* Initiate OAuth authorization flow
*/
public function authorize()
{
try {
// Verify CSRF token
if (!$this->security->get_csrf_hash()) {
throw new Exception('Invalid CSRF token');
}
// Check if OAuth is configured
if (!$this->moloni_oauth->is_configured()) {
set_alert('danger', _l('oauth_not_configured'));
redirect(admin_url('desk_moloni/oauth'));
}
// Generate state for CSRF protection
$state = bin2hex(random_bytes(16));
$this->session->set_userdata('oauth_state', $state);
// Get authorization URL
$auth_url = $this->moloni_oauth->get_authorization_url($state);
// Log authorization initiation
log_activity('Desk-Moloni: OAuth authorization initiated by ' . get_staff_full_name());
// Redirect to Moloni OAuth page
redirect($auth_url);
} catch (Exception $e) {
log_activity('Desk-Moloni: OAuth authorization failed - ' . $e->getMessage());
set_alert('danger', _l('oauth_authorization_failed') . ': ' . $e->getMessage());
redirect(admin_url('desk_moloni/oauth'));
}
}
/**
* Handle OAuth callback from Moloni
*/
public function callback()
{
try {
// Get callback parameters
$code = $this->input->get('code');
$state = $this->input->get('state');
$error = $this->input->get('error');
$error_description = $this->input->get('error_description');
// Handle OAuth errors
if ($error) {
throw new Exception("OAuth Error: {$error} - {$error_description}");
}
// Validate required parameters
if (empty($code)) {
throw new Exception('Authorization code not received');
}
// Verify state parameter (CSRF protection)
$stored_state = $this->session->userdata('oauth_state');
if (empty($stored_state) || $state !== $stored_state) {
throw new Exception('Invalid state parameter - possible CSRF attack');
}
// Clear stored state
$this->session->unset_userdata('oauth_state');
// Exchange code for tokens
$success = $this->moloni_oauth->handle_callback($code, $state);
if ($success) {
// Log successful authentication
log_activity('Desk-Moloni: OAuth authentication successful for ' . get_staff_full_name());
// Set success message
set_alert('success', _l('oauth_connected_successfully'));
// Test API connection
$this->test_api_connection();
} else {
throw new Exception('Token exchange failed');
}
} catch (Exception $e) {
log_activity('Desk-Moloni: OAuth callback failed - ' . $e->getMessage());
set_alert('danger', _l('oauth_callback_failed') . ': ' . $e->getMessage());
}
// Redirect back to OAuth settings
redirect(admin_url('desk_moloni/oauth'));
}
/**
* Disconnect OAuth (revoke tokens)
*/
public function disconnect()
{
try {
// Verify CSRF token
if (!$this->input->post() || !$this->security->get_csrf_hash()) {
throw new Exception('Invalid request');
}
// Revoke OAuth access
$success = $this->moloni_oauth->revoke_access();
if ($success) {
log_activity('Desk-Moloni: OAuth disconnected by ' . get_staff_full_name());
set_alert('success', _l('oauth_disconnected_successfully'));
} else {
set_alert('warning', _l('oauth_disconnect_partial'));
}
} catch (Exception $e) {
log_activity('Desk-Moloni: OAuth disconnect failed - ' . $e->getMessage());
set_alert('danger', _l('oauth_disconnect_failed') . ': ' . $e->getMessage());
}
redirect(admin_url('desk_moloni/oauth'));
}
/**
* Refresh OAuth tokens manually
*/
public function refresh_token()
{
try {
// Verify CSRF token
if (!$this->input->post() || !$this->security->get_csrf_hash()) {
throw new Exception('Invalid request');
}
// Attempt token refresh
$success = $this->moloni_oauth->refresh_access_token();
if ($success) {
log_activity('Desk-Moloni: OAuth tokens refreshed by ' . get_staff_full_name());
set_alert('success', _l('oauth_tokens_refreshed'));
} else {
set_alert('danger', _l('oauth_token_refresh_failed'));
}
} catch (Exception $e) {
log_activity('Desk-Moloni: Token refresh failed - ' . $e->getMessage());
set_alert('danger', _l('oauth_token_refresh_failed') . ': ' . $e->getMessage());
}
redirect(admin_url('desk_moloni/oauth'));
}
/**
* Test OAuth configuration
*/
public function test_config()
{
try {
// Run configuration test
$test_results = $this->moloni_oauth->test_configuration();
if ($test_results['is_valid']) {
set_alert('success', _l('oauth_config_test_passed'));
} else {
$issues = implode('<br>', $test_results['issues']);
set_alert('danger', _l('oauth_config_test_failed') . ':<br>' . $issues);
}
} catch (Exception $e) {
set_alert('danger', _l('oauth_config_test_error') . ': ' . $e->getMessage());
}
redirect(admin_url('desk_moloni/oauth'));
}
/**
* Get OAuth status via AJAX
*/
public function get_status()
{
// Verify AJAX request
if (!$this->input->is_ajax_request()) {
show_404();
}
try {
$status = [
'oauth' => $this->moloni_oauth->get_status(),
'tokens' => $this->token_manager->get_token_status(),
'timestamp' => date('Y-m-d H:i:s')
];
$this->output
->set_content_type('application/json')
->set_output(json_encode($status));
} catch (Exception $e) {
$this->output
->set_status_header(500)
->set_content_type('application/json')
->set_output(json_encode(['error' => $e->getMessage()]));
}
}
/**
* Export OAuth logs
*/
public function export_logs()
{
try {
// Check permissions
if (!has_permission('desk_moloni', '', 'view')) {
throw new Exception('Insufficient permissions');
}
// Load API log model
$this->load->model('desk_moloni_api_log_model');
// Get logs from last 30 days
$logs = $this->desk_moloni_api_log_model->get_logs([
'start_date' => date('Y-m-d', strtotime('-30 days')),
'end_date' => date('Y-m-d'),
'limit' => 1000
]);
// Generate CSV
$csv_data = $this->generate_logs_csv($logs);
// Set headers for file download
$filename = 'moloni_oauth_logs_' . date('Y-m-d') . '.csv';
$this->output
->set_content_type('application/csv')
->set_header('Content-Disposition: attachment; filename="' . $filename . '"')
->set_output($csv_data);
} catch (Exception $e) {
set_alert('danger', _l('export_logs_failed') . ': ' . $e->getMessage());
redirect(admin_url('desk_moloni/oauth'));
}
}
/**
* Handle OAuth configuration form submission
*/
private function handle_oauth_configuration()
{
try {
// Validate CSRF token
if (!$this->security->get_csrf_hash()) {
throw new Exception('Invalid CSRF token');
}
// Get form data
$client_id = $this->input->post('client_id', true);
$client_secret = $this->input->post('client_secret', true);
$use_pkce = (bool)$this->input->post('use_pkce');
// Validate required fields
if (empty($client_id) || empty($client_secret)) {
throw new Exception('Client ID and Client Secret are required');
}
// Configure OAuth
$options = [
'use_pkce' => $use_pkce,
'timeout' => (int)$this->input->post('timeout', true) ?: 30
];
$success = $this->moloni_oauth->configure($client_id, $client_secret, $options);
if ($success) {
log_activity('Desk-Moloni: OAuth configuration updated by ' . get_staff_full_name());
set_alert('success', _l('oauth_configuration_saved'));
} else {
throw new Exception('Configuration save failed');
}
} catch (Exception $e) {
log_activity('Desk-Moloni: OAuth configuration failed - ' . $e->getMessage());
set_alert('danger', _l('oauth_configuration_failed') . ': ' . $e->getMessage());
}
}
/**
* Test API connection after OAuth setup
*/
private function test_api_connection()
{
try {
$this->load->library('desk_moloni/moloni_api_client');
// Try to get companies list (basic API test)
$companies = $this->moloni_api_client->make_request('companies/getAll');
if (!empty($companies)) {
log_activity('Desk-Moloni: API connection test successful');
set_alert('info', _l('api_connection_test_passed'));
}
} catch (Exception $e) {
log_activity('Desk-Moloni: API connection test failed - ' . $e->getMessage());
set_alert('warning', _l('api_connection_test_failed') . ': ' . $e->getMessage());
}
}
/**
* Generate CSV data from logs
*
* @param array $logs Log entries
* @return string CSV data
*/
private function generate_logs_csv($logs)
{
$csv_lines = [];
// Add header
$csv_lines[] = 'Timestamp,Endpoint,Method,Status,Error,User';
// Add log entries
foreach ($logs as $log) {
$csv_lines[] = sprintf(
'"%s","%s","%s","%s","%s","%s"',
$log['timestamp'],
$log['endpoint'] ?? '',
$log['method'] ?? 'POST',
$log['error'] ? 'ERROR' : 'SUCCESS',
str_replace('"', '""', $log['error'] ?? ''),
$log['user_name'] ?? 'System'
);
}
return implode("\n", $csv_lines);
}
/**
* Security check for sensitive operations
*
* @param string $action Action being performed
* @return bool Security check passed
*/
private function security_check($action)
{
// Rate limiting for sensitive operations
$rate_limit_key = 'oauth_action_' . get_staff_user_id() . '_' . $action;
$attempts = $this->session->userdata($rate_limit_key) ?? 0;
if ($attempts >= 5) {
throw new Exception('Too many attempts. Please wait before trying again.');
}
$this->session->set_userdata($rate_limit_key, $attempts + 1);
return true;
}
}