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.
This commit is contained in:
392
modules/desk_moloni/libraries/TokenManager.php
Normal file
392
modules/desk_moloni/libraries/TokenManager.php
Normal file
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Token Manager Library
|
||||
*
|
||||
* Handles secure token storage and management with AES-256 encryption
|
||||
*
|
||||
* @package DeskMoloni
|
||||
* @author Descomplicar®
|
||||
* @copyright 2025 Descomplicar
|
||||
* @version 3.0.0
|
||||
*/
|
||||
class TokenManager
|
||||
{
|
||||
private $CI;
|
||||
|
||||
// Encryption configuration
|
||||
private $cipher = 'AES-256-CBC';
|
||||
private $key_size = 32; // 256 bits
|
||||
private $iv_size = 16; // 128 bits
|
||||
|
||||
// Token storage keys
|
||||
private $access_token_key = 'desk_moloni_access_token_encrypted';
|
||||
private $refresh_token_key = 'desk_moloni_refresh_token_encrypted';
|
||||
private $token_expires_key = 'desk_moloni_token_expires';
|
||||
private $token_scope_key = 'desk_moloni_token_scope';
|
||||
private $encryption_key_option = 'desk_moloni_encryption_key';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
|
||||
// Ensure encryption key exists
|
||||
$this->ensure_encryption_key();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save OAuth tokens securely
|
||||
*
|
||||
* @param array $token_data Token response from OAuth
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function save_tokens($token_data)
|
||||
{
|
||||
try {
|
||||
// Validate required fields
|
||||
if (!isset($token_data['access_token'])) {
|
||||
throw new Exception('Access token is required');
|
||||
}
|
||||
|
||||
// Calculate expiration time with 60-second buffer
|
||||
$expires_in = isset($token_data['expires_in']) ? (int)$token_data['expires_in'] : 3600;
|
||||
$expires_at = time() + $expires_in - 60;
|
||||
|
||||
// Encrypt and save access token
|
||||
$encrypted_access = $this->encrypt($token_data['access_token']);
|
||||
update_option($this->access_token_key, $encrypted_access);
|
||||
|
||||
// Encrypt and save refresh token if provided
|
||||
if (isset($token_data['refresh_token'])) {
|
||||
$encrypted_refresh = $this->encrypt($token_data['refresh_token']);
|
||||
update_option($this->refresh_token_key, $encrypted_refresh);
|
||||
}
|
||||
|
||||
// Save expiration and scope
|
||||
update_option($this->token_expires_key, $expires_at);
|
||||
update_option($this->token_scope_key, $token_data['scope'] ?? '');
|
||||
|
||||
// Log successful token save
|
||||
log_activity('Desk-Moloni: OAuth tokens saved securely');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_activity('Desk-Moloni: Token save failed - ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get decrypted access token
|
||||
*
|
||||
* @return string|null Access token or null if not available
|
||||
*/
|
||||
public function get_access_token()
|
||||
{
|
||||
try {
|
||||
$encrypted_token = get_option($this->access_token_key);
|
||||
|
||||
if (empty($encrypted_token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->decrypt($encrypted_token);
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_activity('Desk-Moloni: Access token decryption failed - ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get decrypted refresh token
|
||||
*
|
||||
* @return string|null Refresh token or null if not available
|
||||
*/
|
||||
public function get_refresh_token()
|
||||
{
|
||||
try {
|
||||
$encrypted_token = get_option($this->refresh_token_key);
|
||||
|
||||
if (empty($encrypted_token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->decrypt($encrypted_token);
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_activity('Desk-Moloni: Refresh token decryption failed - ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tokens are valid and not expired
|
||||
*
|
||||
* @return bool Token validity status
|
||||
*/
|
||||
public function are_tokens_valid()
|
||||
{
|
||||
// Check if access token exists
|
||||
if (empty($this->get_access_token())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
$expires_at = get_option($this->token_expires_key);
|
||||
if ($expires_at && time() >= $expires_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tokens are close to expiring (within 5 minutes)
|
||||
*
|
||||
* @return bool True if tokens expire soon
|
||||
*/
|
||||
public function tokens_expire_soon()
|
||||
{
|
||||
$expires_at = get_option($this->token_expires_key);
|
||||
|
||||
if (!$expires_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (time() + 300) >= $expires_at; // 5 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token expiration timestamp
|
||||
*
|
||||
* @return int|null Expiration timestamp or null
|
||||
*/
|
||||
public function get_token_expiration()
|
||||
{
|
||||
return get_option($this->token_expires_key) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token scope
|
||||
*
|
||||
* @return string Token scope
|
||||
*/
|
||||
public function get_token_scope()
|
||||
{
|
||||
return get_option($this->token_scope_key) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all stored tokens
|
||||
*
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function clear_tokens()
|
||||
{
|
||||
try {
|
||||
update_option($this->access_token_key, '');
|
||||
update_option($this->refresh_token_key, '');
|
||||
update_option($this->token_expires_key, '');
|
||||
update_option($this->token_scope_key, '');
|
||||
|
||||
log_activity('Desk-Moloni: OAuth tokens cleared');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_activity('Desk-Moloni: Token clear failed - ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive token status
|
||||
*
|
||||
* @return array Token status information
|
||||
*/
|
||||
public function get_token_status()
|
||||
{
|
||||
$expires_at = $this->get_token_expiration();
|
||||
|
||||
return [
|
||||
'has_access_token' => !empty($this->get_access_token()),
|
||||
'has_refresh_token' => !empty($this->get_refresh_token()),
|
||||
'is_valid' => $this->are_tokens_valid(),
|
||||
'expires_soon' => $this->tokens_expire_soon(),
|
||||
'expires_at' => $expires_at,
|
||||
'expires_in' => $expires_at ? max(0, $expires_at - time()) : 0,
|
||||
'scope' => $this->get_token_scope(),
|
||||
'formatted_expiry' => $expires_at ? date('Y-m-d H:i:s', $expires_at) : null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt data using AES-256-CBC
|
||||
*
|
||||
* @param string $data Data to encrypt
|
||||
* @return string Base64 encoded encrypted data with IV
|
||||
*/
|
||||
private function encrypt($data)
|
||||
{
|
||||
if (empty($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$key = $this->get_encryption_key();
|
||||
$iv = random_bytes($this->iv_size);
|
||||
|
||||
$encrypted = openssl_encrypt($data, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
if ($encrypted === false) {
|
||||
throw new Exception('Encryption failed');
|
||||
}
|
||||
|
||||
// Prepend IV to encrypted data and encode
|
||||
return base64_encode($iv . $encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt data using AES-256-CBC
|
||||
*
|
||||
* @param string $encrypted_data Base64 encoded encrypted data with IV
|
||||
* @return string Decrypted data
|
||||
*/
|
||||
private function decrypt($encrypted_data)
|
||||
{
|
||||
if (empty($encrypted_data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = base64_decode($encrypted_data);
|
||||
|
||||
if ($data === false || strlen($data) < $this->iv_size) {
|
||||
throw new Exception('Invalid encrypted data');
|
||||
}
|
||||
|
||||
$key = $this->get_encryption_key();
|
||||
$iv = substr($data, 0, $this->iv_size);
|
||||
$encrypted = substr($data, $this->iv_size);
|
||||
|
||||
$decrypted = openssl_decrypt($encrypted, $this->cipher, $key, OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
if ($decrypted === false) {
|
||||
throw new Exception('Decryption failed');
|
||||
}
|
||||
|
||||
return $decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or generate encryption key
|
||||
*
|
||||
* @return string Encryption key
|
||||
*/
|
||||
private function get_encryption_key()
|
||||
{
|
||||
$key = get_option($this->encryption_key_option);
|
||||
|
||||
if (empty($key)) {
|
||||
throw new Exception('Encryption key not found');
|
||||
}
|
||||
|
||||
return base64_decode($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure encryption key exists
|
||||
*/
|
||||
private function ensure_encryption_key()
|
||||
{
|
||||
$existing_key = get_option($this->encryption_key_option);
|
||||
|
||||
if (empty($existing_key)) {
|
||||
// Generate new random key
|
||||
$key = random_bytes($this->key_size);
|
||||
$encoded_key = base64_encode($key);
|
||||
|
||||
update_option($this->encryption_key_option, $encoded_key);
|
||||
|
||||
log_activity('Desk-Moloni: New encryption key generated');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate encryption key (for security maintenance)
|
||||
* WARNING: This will invalidate all existing tokens
|
||||
*
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function rotate_encryption_key()
|
||||
{
|
||||
try {
|
||||
// Clear existing tokens first
|
||||
$this->clear_tokens();
|
||||
|
||||
// Generate new key
|
||||
$new_key = random_bytes($this->key_size);
|
||||
$encoded_key = base64_encode($new_key);
|
||||
|
||||
update_option($this->encryption_key_option, $encoded_key);
|
||||
|
||||
log_activity('Desk-Moloni: Encryption key rotated - all tokens cleared');
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_activity('Desk-Moloni: Key rotation failed - ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate encryption setup
|
||||
*
|
||||
* @return array Validation results
|
||||
*/
|
||||
public function validate_encryption()
|
||||
{
|
||||
$issues = [];
|
||||
|
||||
// Check if OpenSSL is available
|
||||
if (!extension_loaded('openssl')) {
|
||||
$issues[] = 'OpenSSL extension not loaded';
|
||||
}
|
||||
|
||||
// Check if cipher is supported
|
||||
if (!in_array($this->cipher, openssl_get_cipher_methods())) {
|
||||
$issues[] = 'AES-256-CBC cipher not supported';
|
||||
}
|
||||
|
||||
// Check if encryption key exists
|
||||
try {
|
||||
$this->get_encryption_key();
|
||||
} catch (Exception $e) {
|
||||
$issues[] = 'Encryption key not available: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
// Test encryption/decryption
|
||||
try {
|
||||
$test_data = 'test_token_' . time();
|
||||
$encrypted = $this->encrypt($test_data);
|
||||
$decrypted = $this->decrypt($encrypted);
|
||||
|
||||
if ($decrypted !== $test_data) {
|
||||
$issues[] = 'Encryption/decryption test failed';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$issues[] = 'Encryption test failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return [
|
||||
'is_valid' => empty($issues),
|
||||
'issues' => $issues,
|
||||
'cipher' => $this->cipher,
|
||||
'openssl_loaded' => extension_loaded('openssl'),
|
||||
'supported_ciphers' => openssl_get_cipher_methods()
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user