API ISsues fixes pluigna dn app, plugin v udapte 1.4.0
This commit is contained in:
@@ -128,8 +128,7 @@ class IntegrationViewSet(SiteSectorModelViewSet):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@extend_schema(tags=['Integration'])
|
@extend_schema(tags=['Integration'])
|
||||||
@action(detail=False, methods=['post'], url_path='test-connection',
|
@action(detail=False, methods=['post'], url_path='test-connection')
|
||||||
permission_classes=[AllowAny], throttle_classes=[NoThrottle])
|
|
||||||
def test_connection_collection(self, request):
|
def test_connection_collection(self, request):
|
||||||
"""
|
"""
|
||||||
Test WordPress connection using Site.wp_api_key (single source of truth).
|
Test WordPress connection using Site.wp_api_key (single source of truth).
|
||||||
@@ -161,14 +160,14 @@ class IntegrationViewSet(SiteSectorModelViewSet):
|
|||||||
return error_response('Site not found or invalid', None, status.HTTP_404_NOT_FOUND, request)
|
return error_response('Site not found or invalid', None, status.HTTP_404_NOT_FOUND, request)
|
||||||
|
|
||||||
# Authentication: user must be authenticated and belong to same account
|
# Authentication: user must be authenticated and belong to same account
|
||||||
if not hasattr(request, 'user') or not getattr(request.user, 'is_authenticated', False):
|
if not request.user or not request.user.is_authenticated:
|
||||||
return error_response('Authentication required', None, status.HTTP_403_FORBIDDEN, request)
|
return error_response('Authentication required', None, status.HTTP_401_UNAUTHORIZED, request)
|
||||||
|
|
||||||
try:
|
if not hasattr(request.user, 'account') or not request.user.account:
|
||||||
if site.account != request.user.account:
|
return error_response('User account not found', None, status.HTTP_403_FORBIDDEN, request)
|
||||||
return error_response('Site does not belong to your account', None, status.HTTP_403_FORBIDDEN, request)
|
|
||||||
except Exception:
|
if site.account_id != request.user.account_id:
|
||||||
return error_response('Authentication failed', None, status.HTTP_403_FORBIDDEN, request)
|
return error_response('Site does not belong to your account', None, status.HTTP_403_FORBIDDEN, request)
|
||||||
|
|
||||||
# Get stored API key from Site model (single source of truth)
|
# Get stored API key from Site model (single source of truth)
|
||||||
stored_api_key = site.wp_api_key
|
stored_api_key = site.wp_api_key
|
||||||
|
|||||||
259
docs/90-REFERENCE/AUTHENTICATION-SECURITY-FIXES.md
Normal file
259
docs/90-REFERENCE/AUTHENTICATION-SECURITY-FIXES.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# Authentication Security Fixes - January 13, 2026
|
||||||
|
|
||||||
|
## Critical Security Issues Fixed
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
Fixed multiple critical security vulnerabilities where API connections could appear successful without proper authentication, and status endpoints were publicly accessible without API key validation.
|
||||||
|
|
||||||
|
## Issues Fixed
|
||||||
|
|
||||||
|
### 1. Backend `/test-connection/` Endpoint Security
|
||||||
|
**Issue:** Backend endpoint allowed unauthenticated requests via `AllowAny` permission class
|
||||||
|
|
||||||
|
**Location:** `/data/app/igny8/backend/igny8_core/modules/integration/views.py`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```python
|
||||||
|
@action(detail=False, methods=['post'], url_path='test-connection',
|
||||||
|
permission_classes=[AllowAny], throttle_classes=[NoThrottle])
|
||||||
|
def test_connection_collection(self, request):
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```python
|
||||||
|
@action(detail=False, methods=['post'], url_path='test-connection')
|
||||||
|
def test_connection_collection(self, request):
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Now requires proper Django REST Framework authentication (JWT token)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Backend Authentication Check Logic
|
||||||
|
**Issue:** Weak authentication validation logic that could be bypassed
|
||||||
|
|
||||||
|
**Location:** `/data/app/igny8/backend/igny8_core/modules/integration/views.py`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```python
|
||||||
|
# Weak check with fallbacks
|
||||||
|
if not hasattr(request, 'user') or not getattr(request.user, 'is_authenticated', False):
|
||||||
|
return error_response('Authentication required', None, status.HTTP_403_FORBIDDEN, request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if site.account != request.user.account:
|
||||||
|
return error_response('Site does not belong to your account', None, status.HTTP_403_FORBIDDEN, request)
|
||||||
|
except Exception:
|
||||||
|
return error_response('Authentication failed', None, status.HTTP_403_FORBIDDEN, request)
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```python
|
||||||
|
# Strong authentication check
|
||||||
|
if not request.user or not request.user.is_authenticated:
|
||||||
|
return error_response('Authentication required', None, status.HTTP_401_UNAUTHORIZED, request)
|
||||||
|
|
||||||
|
if not hasattr(request.user, 'account') or not request.user.account:
|
||||||
|
return error_response('User account not found', None, status.HTTP_403_FORBIDDEN, request)
|
||||||
|
|
||||||
|
if site.account_id != request.user.account_id:
|
||||||
|
return error_response('Site does not belong to your account', None, status.HTTP_403_FORBIDDEN, request)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Proper 401 for missing auth, clear account validation, uses account_id directly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. WordPress Plugin `/status` Endpoint Security
|
||||||
|
**Issue:** Status endpoint was public, returning connection info without authentication
|
||||||
|
|
||||||
|
**Location:** `/data/app/igny8/plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```php
|
||||||
|
register_rest_route('igny8/v1', '/status', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_status'),
|
||||||
|
'permission_callback' => '__return_true', // Public endpoint
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```php
|
||||||
|
register_rest_route('igny8/v1', '/status', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_status'),
|
||||||
|
'permission_callback' => array($this, 'check_permission'), // Requires API key
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Status endpoint now requires valid API key in header
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. WordPress Plugin Status Response Enhancement
|
||||||
|
**Issue:** Status response didn't track API key validation timestamp
|
||||||
|
|
||||||
|
**Location:** `/data/app/igny8/plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```php
|
||||||
|
public function get_status($request) {
|
||||||
|
// No validation tracking
|
||||||
|
$data = array(
|
||||||
|
'connected' => !empty($api_key) && $api->is_authenticated(),
|
||||||
|
'has_api_key' => !empty($api_key),
|
||||||
|
'last_health_check' => get_option('igny8_last_api_health_check', 0),
|
||||||
|
// ...
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```php
|
||||||
|
public function get_status($request) {
|
||||||
|
// Update validation timestamp when status is checked with valid API key
|
||||||
|
update_option('igny8_last_api_health_check', time());
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'connected' => !empty($api_key) && $api->is_authenticated(),
|
||||||
|
'has_api_key' => !empty($api_key),
|
||||||
|
'api_key_validated' => true, // Since check_permission passed
|
||||||
|
'last_health_check' => time(),
|
||||||
|
// ...
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Tracks when API key was last successfully validated
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. WordPress Plugin Connection State Logic
|
||||||
|
**Issue:** Plugin showed "connected" status without validating API key with backend
|
||||||
|
|
||||||
|
**Location:** `/data/app/igny8/plugins/wordpress/source/igny8-wp-bridge/includes/functions.php`
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```php
|
||||||
|
function igny8_get_connection_state() {
|
||||||
|
$api_key = igny8_get_secure_option('igny8_api_key');
|
||||||
|
$integration_id = get_option('igny8_integration_id');
|
||||||
|
$last_structure_sync = get_option('igny8_last_structure_sync');
|
||||||
|
|
||||||
|
if (empty($api_key)) {
|
||||||
|
return 'not_connected';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows "connected" if API key exists, even without validation!
|
||||||
|
if (!empty($api_key) && !empty($integration_id) && !empty($last_structure_sync)) {
|
||||||
|
return 'connected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'configured';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```php
|
||||||
|
function igny8_get_connection_state() {
|
||||||
|
$api_key = igny8_get_secure_option('igny8_api_key');
|
||||||
|
$integration_id = get_option('igny8_integration_id');
|
||||||
|
$last_structure_sync = get_option('igny8_last_structure_sync');
|
||||||
|
|
||||||
|
// SECURITY: Must have API key to show ANY connection state
|
||||||
|
if (empty($api_key)) {
|
||||||
|
return 'not_connected';
|
||||||
|
}
|
||||||
|
|
||||||
|
// SECURITY: Only show 'connected' if API key was recently validated
|
||||||
|
// Check if we have a recent successful API health check (within last hour)
|
||||||
|
$last_health_check = get_option('igny8_last_api_health_check', 0);
|
||||||
|
$one_hour_ago = time() - 3600;
|
||||||
|
|
||||||
|
if (!empty($api_key) && !empty($integration_id) && !empty($last_structure_sync) && $last_health_check > $one_hour_ago) {
|
||||||
|
return 'connected';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have API key but no recent validation, show as 'configured' not 'connected'
|
||||||
|
return 'configured';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- Only shows "connected" if API key was validated within last hour
|
||||||
|
- Shows "configured" if key exists but hasn't been validated recently
|
||||||
|
- Prevents false "connected" status on fresh installations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Flow Summary
|
||||||
|
|
||||||
|
### Before Fixes
|
||||||
|
1. ❌ Plugin could show "connected" without API key validation
|
||||||
|
2. ❌ Backend `/test-connection/` accepted unauthenticated requests
|
||||||
|
3. ❌ Plugin `/status` endpoint was public
|
||||||
|
4. ❌ No tracking of when API key was last validated
|
||||||
|
|
||||||
|
### After Fixes
|
||||||
|
1. ✅ Backend `/test-connection/` requires JWT authentication
|
||||||
|
2. ✅ Strong authentication checks with proper 401/403 responses
|
||||||
|
3. ✅ Plugin `/status` endpoint requires valid API key
|
||||||
|
4. ✅ Plugin tracks last successful API key validation
|
||||||
|
5. ✅ Connection state only shows "connected" with recent validation
|
||||||
|
6. ✅ Clear distinction between "configured" (key exists) and "connected" (key validated)
|
||||||
|
|
||||||
|
## Authentication Flow
|
||||||
|
|
||||||
|
### Correct Flow (After Fixes)
|
||||||
|
```
|
||||||
|
1. User adds API key in WordPress plugin settings
|
||||||
|
→ Status: "configured"
|
||||||
|
|
||||||
|
2. IGNY8 backend calls /wp-json/igny8/v1/verify-key
|
||||||
|
→ Plugin validates API key from X-IGNY8-API-KEY header
|
||||||
|
→ Updates igny8_last_api_health_check timestamp
|
||||||
|
→ Returns 200 OK
|
||||||
|
|
||||||
|
3. Plugin connection state check
|
||||||
|
→ Sees igny8_last_api_health_check < 1 hour old
|
||||||
|
→ Status: "connected"
|
||||||
|
|
||||||
|
4. After 1 hour without health check
|
||||||
|
→ Status: returns to "configured"
|
||||||
|
→ Next test connection will re-validate and update timestamp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Fresh plugin installation shows "not_connected"
|
||||||
|
- [ ] Adding API key shows "configured", NOT "connected"
|
||||||
|
- [ ] Test connection with valid API key shows "connected"
|
||||||
|
- [ ] Test connection with invalid API key shows error
|
||||||
|
- [ ] Backend `/test-connection/` requires authentication (401 without token)
|
||||||
|
- [ ] Plugin `/status` endpoint requires API key (401/403 without key)
|
||||||
|
- [ ] Connection state returns to "configured" after 1 hour
|
||||||
|
- [ ] All API communication properly authenticated
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
1. Backend changes applied: ✅ (container restarted)
|
||||||
|
2. Plugin changes ready: ✅ (needs deployment to WordPress site)
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
- Deploy updated plugin to WordPress site (massagersmart.com)
|
||||||
|
- Test complete authentication flow
|
||||||
|
- Verify no false "connected" states
|
||||||
|
- Verify proper error messages for missing/invalid API keys
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
- `backend/igny8_core/modules/integration/views.py`
|
||||||
|
- `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php`
|
||||||
|
- `plugins/wordpress/source/igny8-wp-bridge/includes/functions.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Date:** January 13, 2026
|
||||||
|
**Status:** Implemented, Backend Deployed, Plugin Ready for Deployment
|
||||||
|
**Priority:** CRITICAL - Security Vulnerability Fixes
|
||||||
290
docs/90-REFERENCE/PLUGIN-UPDATE-BUG-FIX.md
Normal file
290
docs/90-REFERENCE/PLUGIN-UPDATE-BUG-FIX.md
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
# Plugin Update Double-Notification Bug - Fixed
|
||||||
|
|
||||||
|
## Issue Description
|
||||||
|
**Bug:** After updating the IGNY8 WordPress plugin, the update notification appears again even though the plugin is already on the latest version. Users had to click "Update" twice, and the notification persisted even when current version matched the update notification version.
|
||||||
|
|
||||||
|
**Affected Areas:**
|
||||||
|
- Plugin update page (`/wp-admin/update-core.php`)
|
||||||
|
- Plugins list page (`/wp-admin/plugins.php`)
|
||||||
|
|
||||||
|
## Root Causes
|
||||||
|
|
||||||
|
### 1. Improper Transient State Management
|
||||||
|
The `check_for_updates()` method was using `unset()` to remove the plugin from the update response when no update was available. However, WordPress requires explicit indication that a plugin has been checked and no update is available.
|
||||||
|
|
||||||
|
**Problem Code:**
|
||||||
|
```php
|
||||||
|
} else {
|
||||||
|
// Remove from response if version is current or newer
|
||||||
|
unset($transient->response[$plugin_basename]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Issue:** Simply unsetting from the response doesn't persist properly in WordPress transient cache.
|
||||||
|
|
||||||
|
### 2. Missing Post-Update Cache Clear
|
||||||
|
After a plugin update completes, WordPress continues to use cached update information. There was no hook to clear this cache, causing stale update notifications to persist.
|
||||||
|
|
||||||
|
**Missing:** No `upgrader_process_complete` hook handler to clear transients after update.
|
||||||
|
|
||||||
|
### 3. No Manual Cache Clear Option
|
||||||
|
Administrators had no way to manually clear the update cache when it got stuck, requiring manual database manipulation or complex workarounds.
|
||||||
|
|
||||||
|
## Solutions Implemented
|
||||||
|
|
||||||
|
### Fix 1: Proper Transient State Management
|
||||||
|
|
||||||
|
**File:** `includes/class-igny8-updater.php`
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
1. When no update is available, explicitly add plugin to `$transient->no_update` array
|
||||||
|
2. Remove from `$transient->response` if present
|
||||||
|
3. Mark plugin as "checked with no update available"
|
||||||
|
|
||||||
|
**New Code:**
|
||||||
|
```php
|
||||||
|
} else {
|
||||||
|
// CRITICAL FIX: Explicitly mark as no update by removing from response
|
||||||
|
// and adding to no_update to prevent false update notifications
|
||||||
|
if (isset($transient->response[$plugin_basename])) {
|
||||||
|
unset($transient->response[$plugin_basename]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark plugin as checked with no update available
|
||||||
|
if (!isset($transient->no_update)) {
|
||||||
|
$transient->no_update = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$transient->no_update[$plugin_basename] = (object) array(
|
||||||
|
'slug' => $this->plugin_slug,
|
||||||
|
'plugin' => $plugin_basename,
|
||||||
|
'new_version' => $this->version,
|
||||||
|
'url' => $update_info['info_url'] ?? '',
|
||||||
|
'package' => '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** WordPress now correctly recognizes when plugin is up-to-date and doesn't show false update notifications.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix 2: Automatic Cache Clear After Update
|
||||||
|
|
||||||
|
**File:** `includes/class-igny8-updater.php`
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
1. Added `upgrader_process_complete` action hook
|
||||||
|
2. Detects when IGNY8 plugin is updated
|
||||||
|
3. Automatically clears WordPress update transients
|
||||||
|
4. Forces fresh update check on next page load
|
||||||
|
|
||||||
|
**New Code:**
|
||||||
|
```php
|
||||||
|
// In constructor:
|
||||||
|
add_action('upgrader_process_complete', array($this, 'clear_update_cache'), 10, 2);
|
||||||
|
|
||||||
|
// New method:
|
||||||
|
public function clear_update_cache($upgrader, $hook_extra) {
|
||||||
|
// Check if this is a plugin update
|
||||||
|
if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if our plugin was updated
|
||||||
|
$plugin_basename = plugin_basename($this->plugin_file);
|
||||||
|
$updated_plugins = array();
|
||||||
|
|
||||||
|
if (isset($hook_extra['plugin'])) {
|
||||||
|
$updated_plugins[] = $hook_extra['plugin'];
|
||||||
|
} elseif (isset($hook_extra['plugins'])) {
|
||||||
|
$updated_plugins = $hook_extra['plugins'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our plugin was updated, force WordPress to recheck updates
|
||||||
|
if (in_array($plugin_basename, $updated_plugins, true)) {
|
||||||
|
// Delete the update_plugins transient to force a fresh check
|
||||||
|
delete_site_transient('update_plugins');
|
||||||
|
|
||||||
|
// Also clear any cached update info
|
||||||
|
wp_cache_delete('igny8_update_check', 'igny8');
|
||||||
|
|
||||||
|
// Log the cache clear for debugging
|
||||||
|
error_log('IGNY8: Cleared update cache after plugin update to version ' . $this->version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** After plugin update, WordPress immediately recognizes new version and removes update notification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fix 3: Manual Cache Clear Button
|
||||||
|
|
||||||
|
**File:** `admin/pages/settings.php`
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
1. Added "Clear Update Cache" button in Settings page
|
||||||
|
2. AJAX handler for instant cache clearing
|
||||||
|
3. Visual feedback (loading, success, error states)
|
||||||
|
|
||||||
|
**UI Addition:**
|
||||||
|
```php
|
||||||
|
<button type="button" id="igny8-clear-update-cache" class="igny8-btn">
|
||||||
|
<svg>...</svg>
|
||||||
|
Clear Update Cache
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript Handler:**
|
||||||
|
- Shows loading state while processing
|
||||||
|
- Success: Green background, checkmark icon, "Cache Cleared!"
|
||||||
|
- Error: Red background, X icon, "Failed" or "Error"
|
||||||
|
- Returns to normal state after 2 seconds
|
||||||
|
|
||||||
|
**File:** `admin/class-admin.php`
|
||||||
|
|
||||||
|
**AJAX Handler:**
|
||||||
|
```php
|
||||||
|
public static function clear_update_cache_ajax() {
|
||||||
|
// Verify nonce
|
||||||
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'igny8_clear_cache')) {
|
||||||
|
wp_send_json_error(array('message' => 'Invalid security token'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user permissions
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error(array('message' => 'Insufficient permissions'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the update_plugins transient to force a fresh check
|
||||||
|
delete_site_transient('update_plugins');
|
||||||
|
|
||||||
|
// Also clear any cached update info
|
||||||
|
wp_cache_delete('igny8_update_check', 'igny8');
|
||||||
|
|
||||||
|
// Clear WordPress object cache
|
||||||
|
wp_cache_flush();
|
||||||
|
|
||||||
|
error_log('IGNY8: Update cache cleared manually by admin');
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => 'Update cache cleared successfully.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Registered AJAX Action:**
|
||||||
|
```php
|
||||||
|
add_action('wp_ajax_igny8_clear_update_cache', array('Igny8Admin', 'clear_update_cache_ajax'));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:** Administrators can instantly clear stuck update notifications without technical knowledge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### WordPress Update Check Flow (Before Fix)
|
||||||
|
```
|
||||||
|
1. WordPress checks for updates
|
||||||
|
2. Plugin returns new_version if available
|
||||||
|
3. Plugin does unset() if no update
|
||||||
|
4. Transient caches partial state ❌
|
||||||
|
5. Next check sees partial cache
|
||||||
|
6. Shows update notification again ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
### WordPress Update Check Flow (After Fix)
|
||||||
|
```
|
||||||
|
1. WordPress checks for updates
|
||||||
|
2. Plugin returns new_version if available
|
||||||
|
3. Plugin adds to no_update if current ✅
|
||||||
|
4. Transient properly caches state ✅
|
||||||
|
5. After update: clear_update_cache() runs ✅
|
||||||
|
6. Next check sees fresh state ✅
|
||||||
|
7. No false update notification ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. **`includes/class-igny8-updater.php`**
|
||||||
|
- Added `clear_update_cache()` method
|
||||||
|
- Fixed `check_for_updates()` transient logic
|
||||||
|
- Added `upgrader_process_complete` hook
|
||||||
|
|
||||||
|
2. **`admin/pages/settings.php`**
|
||||||
|
- Added "Clear Update Cache" button
|
||||||
|
- Added JavaScript AJAX handler
|
||||||
|
- Added loading/success/error states
|
||||||
|
|
||||||
|
3. **`admin/class-admin.php`**
|
||||||
|
- Added `clear_update_cache_ajax()` static method
|
||||||
|
- Registered AJAX action `igny8_clear_update_cache`
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### Automatic Clearing (Fix 1 & 2)
|
||||||
|
- [ ] Update plugin to new version
|
||||||
|
- [ ] Verify update notification disappears immediately
|
||||||
|
- [ ] Verify no second "Update" prompt
|
||||||
|
- [ ] Check plugins list page shows correct version
|
||||||
|
- [ ] Check update-core page shows no IGNY8 update
|
||||||
|
|
||||||
|
### Manual Clearing (Fix 3)
|
||||||
|
- [ ] Navigate to IGNY8 → Settings
|
||||||
|
- [ ] Click "Clear Update Cache" button
|
||||||
|
- [ ] Verify button shows loading state
|
||||||
|
- [ ] Verify button shows success (green, checkmark)
|
||||||
|
- [ ] Refresh plugins page
|
||||||
|
- [ ] Verify update notifications are current
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- [ ] Clear cache with no updates available
|
||||||
|
- [ ] Clear cache with update available (should still show update)
|
||||||
|
- [ ] Update plugin while on settings page
|
||||||
|
- [ ] Update multiple plugins simultaneously
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Backend Changes
|
||||||
|
✅ No backend changes required (WordPress plugin only)
|
||||||
|
|
||||||
|
### Plugin Changes
|
||||||
|
✅ Ready for deployment:
|
||||||
|
- `includes/class-igny8-updater.php` - Core fix
|
||||||
|
- `admin/pages/settings.php` - UI enhancement
|
||||||
|
- `admin/class-admin.php` - AJAX handler
|
||||||
|
|
||||||
|
### Version Bump
|
||||||
|
**Recommended:** Bump plugin version to test the fix itself
|
||||||
|
- Current version: (check `igny8-bridge.php`)
|
||||||
|
- New version: Current + 0.0.1
|
||||||
|
- Update `IGNY8_BRIDGE_VERSION` constant
|
||||||
|
- Update changelog
|
||||||
|
|
||||||
|
## Rollout Plan
|
||||||
|
|
||||||
|
1. **Test in Development**
|
||||||
|
- Deploy updated plugin to test WordPress site
|
||||||
|
- Trigger update process
|
||||||
|
- Verify no double-notification
|
||||||
|
|
||||||
|
2. **Deploy to Production**
|
||||||
|
- Update plugin files on IGNY8 update server
|
||||||
|
- WordPress sites will auto-update
|
||||||
|
- Monitor for any issues
|
||||||
|
|
||||||
|
3. **User Communication**
|
||||||
|
- Fixed: Double-update notification bug
|
||||||
|
- Added: Manual cache clear button in Settings
|
||||||
|
- Improved: Update check reliability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Date:** January 13, 2026
|
||||||
|
**Status:** Implemented, Ready for Testing
|
||||||
|
**Priority:** MEDIUM - Bug Fix, User Experience Improvement
|
||||||
|
**Related:** [AUTHENTICATION-SECURITY-FIXES.md](AUTHENTICATION-SECURITY-FIXES.md)
|
||||||
@@ -756,8 +756,42 @@ class Igny8Admin {
|
|||||||
public function sanitize_post_status($value) {
|
public function sanitize_post_status($value) {
|
||||||
$allowed = array('draft', 'publish');
|
$allowed = array('draft', 'publish');
|
||||||
return in_array($value, $allowed, true) ? $value : 'draft';
|
return in_array($value, $allowed, true) ? $value : 'draft';
|
||||||
}
|
}
|
||||||
}
|
/**
|
||||||
|
* Clear update cache via AJAX
|
||||||
|
*
|
||||||
|
* Fixes the double-update notification bug by clearing WordPress update transients
|
||||||
|
* and forcing a fresh update check.
|
||||||
|
*/
|
||||||
|
public static function clear_update_cache_ajax() {
|
||||||
|
// Verify nonce
|
||||||
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'igny8_clear_cache')) {
|
||||||
|
wp_send_json_error(array('message' => 'Invalid security token'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user permissions
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error(array('message' => 'Insufficient permissions'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the update_plugins transient to force a fresh check
|
||||||
|
delete_site_transient('update_plugins');
|
||||||
|
|
||||||
|
// Also clear any cached update info
|
||||||
|
wp_cache_delete('igny8_update_check', 'igny8');
|
||||||
|
|
||||||
|
// Clear WordPress object cache
|
||||||
|
wp_cache_flush();
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
error_log('IGNY8: Update cache cleared manually by admin');
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => 'Update cache cleared successfully. WordPress will check for updates on next page load.'
|
||||||
|
));
|
||||||
|
}}
|
||||||
|
|
||||||
// Register AJAX handlers
|
// Register AJAX handlers
|
||||||
add_action('wp_ajax_igny8_test_connection', array('Igny8Admin', 'test_connection'));
|
add_action('wp_ajax_igny8_test_connection', array('Igny8Admin', 'test_connection'));
|
||||||
@@ -766,4 +800,4 @@ add_action('wp_ajax_igny8_sync_taxonomies', array('Igny8Admin', 'sync_taxonomies
|
|||||||
add_action('wp_ajax_igny8_sync_from_igny8', array('Igny8Admin', 'sync_from_igny8'));
|
add_action('wp_ajax_igny8_sync_from_igny8', array('Igny8Admin', 'sync_from_igny8'));
|
||||||
add_action('wp_ajax_igny8_collect_site_data', array('Igny8Admin', 'collect_site_data'));
|
add_action('wp_ajax_igny8_collect_site_data', array('Igny8Admin', 'collect_site_data'));
|
||||||
add_action('wp_ajax_igny8_get_stats', array('Igny8Admin', 'get_stats'));
|
add_action('wp_ajax_igny8_get_stats', array('Igny8Admin', 'get_stats'));
|
||||||
|
add_action('wp_ajax_igny8_clear_update_cache', array('Igny8Admin', 'clear_update_cache_ajax'));
|
||||||
|
|||||||
@@ -236,12 +236,21 @@ foreach ($all_post_types as $pt) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="igny8-btn igny8-btn-primary">
|
<div style="display: flex; gap: 12px; align-items: center;">
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width: 16px; height: 16px;">
|
<button type="submit" class="igny8-btn igny8-btn-primary">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width: 16px; height: 16px;">
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||||
<?php _e('Save Settings', 'igny8-bridge'); ?>
|
</svg>
|
||||||
</button>
|
<?php _e('Save Settings', 'igny8-bridge'); ?>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" id="igny8-clear-update-cache" class="igny8-btn" style="background: var(--igny8-surface); border: 1px solid var(--igny8-stroke);">
|
||||||
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width: 16px; height: 16px;">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||||
|
</svg>
|
||||||
|
<?php _e('Clear Update Cache', 'igny8-bridge'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -283,6 +292,46 @@ jQuery(document).ready(function($) {
|
|||||||
card.slideUp(200);
|
card.slideUp(200);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle clear update cache button
|
||||||
|
$('#igny8-clear-update-cache').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var $btn = $(this);
|
||||||
|
var originalText = $btn.html();
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).html('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width: 16px; height: 16px; animation: spin 1s linear infinite;"><circle cx="12" cy="12" r="10" stroke-width="4" stroke="currentColor" stroke-opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Clearing...');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'igny8_clear_update_cache',
|
||||||
|
nonce: '<?php echo wp_create_nonce('igny8_clear_cache'); ?>'
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
$btn.html('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width: 16px; height: 16px;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> Cache Cleared!').css('background', '#10b981').css('color', 'white').css('border-color', '#10b981');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
$btn.prop('disabled', false).html(originalText).css('background', '').css('color', '').css('border-color', '');
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
$btn.html('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width: 16px; height: 16px;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg> Failed').css('background', '#ef4444').css('color', 'white');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
$btn.prop('disabled', false).html(originalText).css('background', '').css('color', '');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$btn.html('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width: 16px; height: 16px;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg> Error').css('background', '#ef4444').css('color', 'white');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
$btn.prop('disabled', false).html(originalText).css('background', '').css('color', '');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: IGNY8 WordPress Bridge
|
* Plugin Name: IGNY8 WordPress Bridge
|
||||||
* Plugin URI: https://igny8.com/igny8-wp-bridge
|
* Plugin URI: https://igny8.com/igny8-wp-bridge
|
||||||
* Description: Lightweight bridge plugin that connects WordPress to IGNY8 API for one-way content publishing.
|
* Description: Lightweight bridge plugin that connects WordPress to IGNY8 API for one-way content publishing.
|
||||||
* Version: 1.3.9
|
* Version: 1.4.0
|
||||||
* Author: IGNY8
|
* Author: IGNY8
|
||||||
* Author URI: https://igny8.com/
|
* Author URI: https://igny8.com/
|
||||||
* License: GPL v2 or later
|
* License: GPL v2 or later
|
||||||
@@ -22,7 +22,7 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define plugin constants
|
// Define plugin constants
|
||||||
define('IGNY8_BRIDGE_VERSION', '1.3.9');
|
define('IGNY8_BRIDGE_VERSION', '1.4.0');
|
||||||
define('IGNY8_BRIDGE_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('IGNY8_BRIDGE_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('IGNY8_BRIDGE_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('IGNY8_BRIDGE_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
define('IGNY8_BRIDGE_PLUGIN_FILE', __FILE__);
|
define('IGNY8_BRIDGE_PLUGIN_FILE', __FILE__);
|
||||||
|
|||||||
@@ -79,10 +79,11 @@ class Igny8RestAPI {
|
|||||||
));
|
));
|
||||||
|
|
||||||
// Plugin status endpoint - returns connection status and API key info
|
// Plugin status endpoint - returns connection status and API key info
|
||||||
|
// SECURITY: Requires API key authentication
|
||||||
register_rest_route('igny8/v1', '/status', array(
|
register_rest_route('igny8/v1', '/status', array(
|
||||||
'methods' => 'GET',
|
'methods' => 'GET',
|
||||||
'callback' => array($this, 'get_status'),
|
'callback' => array($this, 'get_status'),
|
||||||
'permission_callback' => '__return_true', // Public endpoint for health checks
|
'permission_callback' => array($this, 'check_permission'), // Requires API key
|
||||||
));
|
));
|
||||||
|
|
||||||
// API key verification endpoint - requires valid API key in header
|
// API key verification endpoint - requires valid API key in header
|
||||||
@@ -397,6 +398,10 @@ class Igny8RestAPI {
|
|||||||
* @return WP_REST_Response
|
* @return WP_REST_Response
|
||||||
*/
|
*/
|
||||||
public function get_status($request) {
|
public function get_status($request) {
|
||||||
|
// If we reach here, API key was validated by check_permission
|
||||||
|
// Update last health check timestamp
|
||||||
|
update_option('igny8_last_api_health_check', time());
|
||||||
|
|
||||||
$api = new Igny8API();
|
$api = new Igny8API();
|
||||||
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||||
$connection_enabled = igny8_is_connection_enabled();
|
$connection_enabled = igny8_is_connection_enabled();
|
||||||
@@ -404,10 +409,11 @@ class Igny8RestAPI {
|
|||||||
$data = array(
|
$data = array(
|
||||||
'connected' => !empty($api_key) && $api->is_authenticated(),
|
'connected' => !empty($api_key) && $api->is_authenticated(),
|
||||||
'has_api_key' => !empty($api_key),
|
'has_api_key' => !empty($api_key),
|
||||||
|
'api_key_validated' => true, // Since check_permission passed
|
||||||
'communication_enabled' => $connection_enabled,
|
'communication_enabled' => $connection_enabled,
|
||||||
'plugin_version' => defined('IGNY8_BRIDGE_VERSION') ? IGNY8_BRIDGE_VERSION : '1.0.0',
|
'plugin_version' => defined('IGNY8_BRIDGE_VERSION') ? IGNY8_BRIDGE_VERSION : '1.0.0',
|
||||||
'wordpress_version' => get_bloginfo('version'),
|
'wordpress_version' => get_bloginfo('version'),
|
||||||
'last_health_check' => get_option('igny8_last_api_health_check', 0),
|
'last_health_check' => time(),
|
||||||
'health' => (!empty($api_key) && $connection_enabled) ? 'healthy' : 'not_configured'
|
'health' => (!empty($api_key) && $connection_enabled) ? 'healthy' : 'not_configured'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ class Igny8_Updater {
|
|||||||
add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_updates'));
|
add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_updates'));
|
||||||
add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);
|
add_filter('plugins_api', array($this, 'plugin_info'), 10, 3);
|
||||||
|
|
||||||
|
// Clear update cache after plugin is updated
|
||||||
|
add_action('upgrader_process_complete', array($this, 'clear_update_cache'), 10, 2);
|
||||||
|
|
||||||
// Add custom styling for plugin details popup
|
// Add custom styling for plugin details popup
|
||||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_plugin_details_styles'));
|
add_action('admin_enqueue_scripts', array($this, 'enqueue_plugin_details_styles'));
|
||||||
}
|
}
|
||||||
@@ -94,8 +97,24 @@ class Igny8_Updater {
|
|||||||
'requires_php' => $update_info['requires_php'] ?? '',
|
'requires_php' => $update_info['requires_php'] ?? '',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Remove from response if version is current or newer
|
// CRITICAL FIX: Explicitly mark as no update by removing from response
|
||||||
unset($transient->response[$plugin_basename]);
|
// and adding to no_update to prevent false update notifications
|
||||||
|
if (isset($transient->response[$plugin_basename])) {
|
||||||
|
unset($transient->response[$plugin_basename]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark plugin as checked with no update available
|
||||||
|
if (!isset($transient->no_update)) {
|
||||||
|
$transient->no_update = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$transient->no_update[$plugin_basename] = (object) array(
|
||||||
|
'slug' => $this->plugin_slug,
|
||||||
|
'plugin' => $plugin_basename,
|
||||||
|
'new_version' => $this->version,
|
||||||
|
'url' => $update_info['info_url'] ?? '',
|
||||||
|
'package' => '',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $transient;
|
return $transient;
|
||||||
@@ -142,6 +161,44 @@ class Igny8_Updater {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear update cache after plugin update completes
|
||||||
|
*
|
||||||
|
* Fixes the double-update notification bug by forcing WordPress
|
||||||
|
* to re-check for updates with the new version number
|
||||||
|
*
|
||||||
|
* @param WP_Upgrader $upgrader WP_Upgrader instance
|
||||||
|
* @param array $hook_extra Extra arguments passed to hooked filters
|
||||||
|
*/
|
||||||
|
public function clear_update_cache($upgrader, $hook_extra) {
|
||||||
|
// Check if this is a plugin update
|
||||||
|
if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if our plugin was updated
|
||||||
|
$plugin_basename = plugin_basename($this->plugin_file);
|
||||||
|
$updated_plugins = array();
|
||||||
|
|
||||||
|
if (isset($hook_extra['plugin'])) {
|
||||||
|
$updated_plugins[] = $hook_extra['plugin'];
|
||||||
|
} elseif (isset($hook_extra['plugins'])) {
|
||||||
|
$updated_plugins = $hook_extra['plugins'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our plugin was updated, force WordPress to recheck updates
|
||||||
|
if (in_array($plugin_basename, $updated_plugins, true)) {
|
||||||
|
// Delete the update_plugins transient to force a fresh check
|
||||||
|
delete_site_transient('update_plugins');
|
||||||
|
|
||||||
|
// Also clear any cached update info
|
||||||
|
wp_cache_delete('igny8_update_check', 'igny8');
|
||||||
|
|
||||||
|
// Log the cache clear for debugging
|
||||||
|
error_log('IGNY8: Cleared update cache after plugin update to version ' . $this->version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue custom styles for plugin details popup
|
* Enqueue custom styles for plugin details popup
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -363,17 +363,24 @@ function igny8_get_connection_state() {
|
|||||||
$integration_id = get_option('igny8_integration_id');
|
$integration_id = get_option('igny8_integration_id');
|
||||||
$last_structure_sync = get_option('igny8_last_structure_sync');
|
$last_structure_sync = get_option('igny8_last_structure_sync');
|
||||||
|
|
||||||
|
// SECURITY: Must have API key to show ANY connection state
|
||||||
if (empty($api_key)) {
|
if (empty($api_key)) {
|
||||||
igny8_log_connection_state('not_connected', 'No API key found');
|
igny8_log_connection_state('not_connected', 'No API key found');
|
||||||
return 'not_connected';
|
return 'not_connected';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($api_key) && !empty($integration_id) && !empty($last_structure_sync)) {
|
// SECURITY: Only show 'connected' if API key is validated
|
||||||
igny8_log_connection_state('connected', 'Fully connected and synced');
|
// Check if we have a recent successful API health check (within last hour)
|
||||||
|
$last_health_check = get_option('igny8_last_api_health_check', 0);
|
||||||
|
$one_hour_ago = time() - 3600;
|
||||||
|
|
||||||
|
if (!empty($api_key) && !empty($integration_id) && !empty($last_structure_sync) && $last_health_check > $one_hour_ago) {
|
||||||
|
igny8_log_connection_state('connected', 'Fully connected and synced with validated API key');
|
||||||
return 'connected';
|
return 'connected';
|
||||||
}
|
}
|
||||||
|
|
||||||
igny8_log_connection_state('configured', 'API key set, pending structure sync');
|
// If we have API key but no recent validation, show as 'configured' not 'connected'
|
||||||
|
igny8_log_connection_state('configured', 'API key set, validation pending');
|
||||||
return 'configured';
|
return 'configured';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user