wp plugin udapted v1.5.0
This commit is contained in:
596
docs/audits/INTEGRATION-SECURITY-AUDIT-2026-01-13.md
Normal file
596
docs/audits/INTEGRATION-SECURITY-AUDIT-2026-01-13.md
Normal file
@@ -0,0 +1,596 @@
|
||||
# IGNY8 ↔ WordPress Integration Complete Security Audit
|
||||
|
||||
**Audit Date:** 2026-01-13
|
||||
**Auditor:** System Audit (Documentation Calibrated)
|
||||
**Scope:** Backend (Django) + WordPress Plugin + Documentation Review
|
||||
**Status:** COMPLETE - CALIBRATED WITH DOCUMENTATION
|
||||
|
||||
---
|
||||
|
||||
## EXECUTIVE SUMMARY
|
||||
|
||||
This comprehensive audit examined the IGNY8 backend and WordPress plugin integration system, cross-referencing against all existing documentation. The audit identified **17 security vulnerabilities** (3 critical, 5 high, 6 medium, 3 low), **6 unused/redundant database fields**, **2 major data duplication issues**, and several **documentation vs. implementation gaps**.
|
||||
|
||||
**Key Findings:**
|
||||
- API keys stored in plain text (no encryption) despite documentation claims
|
||||
- Public endpoints expose sensitive configuration data
|
||||
- Timing attack vulnerability in Bearer token validation
|
||||
- Documentation states "credentials encrypted at rest" but encryption NOT implemented
|
||||
- Several fields documented but never used
|
||||
- Missing scheduled publishing task from Celery Beat (documented but not configured)
|
||||
|
||||
---
|
||||
|
||||
## PART 1: DOCUMENTATION VS IMPLEMENTATION GAPS
|
||||
|
||||
### 1.1 Critical Discrepancies Found
|
||||
|
||||
| Documentation Claim | Reality | Impact |
|
||||
|---------------------|---------|--------|
|
||||
| "API keys encrypted at rest" (INTEGRATIONS.md) | Plain text CharField in database | **CRITICAL** - False security assumption |
|
||||
| "Webhook signature verification" (INTEGRATIONS.md) | No signature verification implemented | **HIGH** - Anyone with key can spoof webhooks |
|
||||
| "Rate limiting on webhook endpoint" (INTEGRATIONS.md) | Webhooks explicitly disable throttling (`NoThrottle`) | **HIGH** - DoS possible |
|
||||
| "auto_publish_enabled triggers WordPress push" | Automation Stage 7 sets status but does NOT trigger WP push | **MEDIUM** - Manual publish required |
|
||||
| "Scheduled auto-publish task active" | Task exists but NOT in Celery Beat schedule | **MEDIUM** - Content sits unpublished |
|
||||
| SiteIntegration.api_key field (INTEGRATIONS.md line 97) | API key stored in Site.wp_api_key, NOT SiteIntegration | Documentation outdated |
|
||||
| "credentials_json stores WordPress credentials" | credentials_json is empty for WordPress; Site.wp_api_key is source of truth | Documentation outdated |
|
||||
|
||||
### 1.2 Documentation Accuracy Summary
|
||||
|
||||
| Document | Accuracy | Issues Found |
|
||||
|----------|----------|--------------|
|
||||
| WORDPRESS-INTEGRATION-FLOW.md | 90% | Accurate on auth, slight gaps on scheduling |
|
||||
| WORDPRESS-INTEGRATION.md | 85% | Missing encryption truth, good on plugin distribution |
|
||||
| PUBLISHER.md | 95% | Accurate on models and flows |
|
||||
| INTEGRATIONS.md | 70% | Contains outdated field references, false encryption claim |
|
||||
| SCHEDULED-CONTENT-PUBLISHING.md | 95% | Accurate on Celery tasks, correctly notes site_status vs status |
|
||||
| CONTENT-PIPELINE.md | 90% | Accurate on stages, slight gap on Stage 8 auto-publish |
|
||||
|
||||
---
|
||||
|
||||
## PART 2: SYSTEM ARCHITECTURE (VERIFIED)
|
||||
|
||||
### 2.1 Actual Data Flow (Verified Against Code)
|
||||
|
||||
```
|
||||
IGNY8 Backend WordPress Plugin
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ Site Model │ │ wp_options │
|
||||
│ ├─ wp_api_key ◄──────┼────────────────┼─► igny8_api_key │
|
||||
│ │ (SINGLE SOURCE) │ │ igny8_site_id │
|
||||
│ ├─ domain │ │ igny8_integration_id│
|
||||
│ └─ wp_url (LEGACY) │ │ │
|
||||
│ │ │ Post Meta │
|
||||
│ SiteIntegration │ │ ├─ _igny8_content_id │
|
||||
│ ├─ config_json │ │ ├─ _igny8_task_id │
|
||||
│ │ (site_url only) │ │ └─ _igny8_last_synced│
|
||||
│ ├─ credentials_json │ │ │
|
||||
│ │ (EMPTY for WP!) │ │ │
|
||||
│ └─ sync_status │ │ │
|
||||
│ │ │ │
|
||||
│ Content Model │ │ │
|
||||
│ ├─ external_id ◄─────┼────────────────┼─► post_id │
|
||||
│ ├─ external_url │ │ │
|
||||
│ ├─ status (editorial)│ │ │
|
||||
│ └─ site_status │ │ │
|
||||
│ (publishing) │ │ │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Authentication Flow (Verified)
|
||||
|
||||
**IGNY8 → WordPress:**
|
||||
```python
|
||||
# publisher_service.py (line 136, 144)
|
||||
destination_config = {
|
||||
'api_key': site.wp_api_key, # FROM SITE MODEL
|
||||
'site_url': site.domain or site.wp_url # Fallback to legacy
|
||||
}
|
||||
|
||||
# wordpress_adapter.py
|
||||
headers = {
|
||||
'X-IGNY8-API-KEY': api_key, # Primary method
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
```
|
||||
|
||||
**WordPress → IGNY8:**
|
||||
```php
|
||||
// Plugin: check_permission() method
|
||||
$stored_api_key = get_option('igny8_api_key');
|
||||
$header_api_key = $request->get_header('x-igny8-api-key');
|
||||
|
||||
// CORRECT: hash_equals for X-IGNY8-API-KEY
|
||||
if (hash_equals($stored_api_key, $header_api_key)) return true;
|
||||
|
||||
// VULNERABLE: strpos for Bearer token
|
||||
if (strpos($auth_header, 'Bearer ' . $stored_api_key) !== false) return true;
|
||||
```
|
||||
|
||||
### 2.3 API Key Generation (Verified)
|
||||
|
||||
```
|
||||
POST /api/v1/integration/integrations/generate-api-key/
|
||||
Body: { "site_id": 123 }
|
||||
|
||||
Key Format: igny8_site_{site_id}_{timestamp_ms}_{random_10_chars}
|
||||
Example: igny8_site_123_1736780400000_a7b9c3d2e1
|
||||
|
||||
Storage: Site.wp_api_key (CharField, plain text, max 255)
|
||||
Recovery: NOT POSSIBLE - shown once on generation
|
||||
Revocation: Sets Site.wp_api_key = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PART 3: CONTENT STATUS SYSTEM (DOCUMENTED CORRECTLY)
|
||||
|
||||
### 3.1 Two-Status Architecture
|
||||
|
||||
The documentation correctly describes the dual-status system:
|
||||
|
||||
| Field | Purpose | Values |
|
||||
|-------|---------|--------|
|
||||
| `Content.status` | Editorial workflow | draft → review → approved → (published - legacy) |
|
||||
| `Content.site_status` | WordPress publishing | not_published → scheduled → publishing → published / failed |
|
||||
|
||||
### 3.2 Publishing Flow Paths
|
||||
|
||||
**Path 1: Manual Publish (Working)**
|
||||
```
|
||||
User clicks "Publish" → ContentViewSet.publish_to_wordpress
|
||||
→ Celery task: publish_content_to_wordpress
|
||||
→ WordPress API call
|
||||
→ Update external_id, external_url, site_status='published'
|
||||
```
|
||||
|
||||
**Path 2: Scheduled Publish (Partially Working)**
|
||||
```
|
||||
schedule_approved_content (hourly)
|
||||
→ Find approved content with site_status='not_published'
|
||||
→ Assign scheduled_publish_at
|
||||
→ Set site_status='scheduled'
|
||||
|
||||
process_scheduled_publications (every 5 min)
|
||||
→ Find content where scheduled_publish_at <= now
|
||||
→ Queue publish_content_to_wordpress task
|
||||
→ WordPress API call
|
||||
```
|
||||
|
||||
**Path 3: Automation Stage 7 (NOT TRIGGERING WP PUSH)**
|
||||
```
|
||||
Automation Stage 7:
|
||||
→ Content.status = 'published' (legacy status change)
|
||||
→ NO site_status change
|
||||
→ NO WordPress API call queued
|
||||
→ Content sits with site_status='not_published'
|
||||
```
|
||||
|
||||
**GAP IDENTIFIED:** Automation Stage 7 does NOT call publishing scheduler or queue WordPress task.
|
||||
|
||||
---
|
||||
|
||||
## PART 4: SECURITY VULNERABILITIES (VERIFIED)
|
||||
|
||||
### 4.1 CRITICAL ISSUES
|
||||
|
||||
#### CRITICAL-1: API Keys Stored in Plain Text
|
||||
|
||||
**Location:** Backend - `Site.wp_api_key` field (auth/models.py line 491)
|
||||
**Documentation Claim:** "Credentials encrypted at rest" (INTEGRATIONS.md line 345)
|
||||
**Reality:** CharField stores plain text - NO encryption
|
||||
|
||||
**Evidence:**
|
||||
```python
|
||||
# auth/models.py
|
||||
wp_api_key = models.CharField(max_length=255, blank=True, null=True,
|
||||
help_text="API key for WordPress integration via IGNY8 WP Bridge plugin")
|
||||
```
|
||||
|
||||
**Impact:** Database compromise exposes ALL WordPress API keys
|
||||
**Risk Score:** 9/10
|
||||
|
||||
---
|
||||
|
||||
#### CRITICAL-2: Timing Attack in Bearer Token Validation
|
||||
|
||||
**Location:** Plugin - `class-igny8-rest-api.php:140`
|
||||
**Impact:** API key can be guessed character-by-character
|
||||
|
||||
**Vulnerable Code:**
|
||||
```php
|
||||
// Uses strpos (VULNERABLE)
|
||||
if (strpos($auth_header, 'Bearer ' . $stored_api_key) !== false)
|
||||
|
||||
// Should use hash_equals (SAFE)
|
||||
if (hash_equals($stored_api_key, substr($auth_header, 7)))
|
||||
```
|
||||
|
||||
**Risk Score:** 8/10
|
||||
|
||||
---
|
||||
|
||||
#### CRITICAL-3: Diagnostic Logging Exposes Sensitive Data
|
||||
|
||||
**Location:** Plugin - `class-igny8-rest-api.php:533-565`
|
||||
**Impact:** Full request bodies logged including all content
|
||||
|
||||
**Evidence:**
|
||||
```php
|
||||
error_log('========== RAW REQUEST BODY ==========');
|
||||
error_log($raw_body);
|
||||
error_log('========== PARSED JSON DATA ==========');
|
||||
error_log(print_r($content_data, true));
|
||||
```
|
||||
|
||||
**Risk Score:** 8/10
|
||||
|
||||
---
|
||||
|
||||
### 4.2 HIGH SEVERITY ISSUES
|
||||
|
||||
#### HIGH-1: Public Endpoints Expose Configuration
|
||||
|
||||
**Verified Endpoints:**
|
||||
| Endpoint | Permission | Data Exposed |
|
||||
|----------|------------|--------------|
|
||||
| `/wp-json/igny8/v1/status` | `__return_true` (PUBLIC) | has_api_key, connected, versions |
|
||||
| `/wp-json/igny8/v1/site-metadata/` | `__return_true` (PUBLIC) | post_types, taxonomies, counts |
|
||||
|
||||
**Note:** Documentation (WORDPRESS-INTEGRATION-FLOW.md line 86-91) does NOT flag these as security issues.
|
||||
|
||||
---
|
||||
|
||||
#### HIGH-2: Permission Architecture Inconsistency
|
||||
|
||||
**Location:** `test_connection_collection` endpoint
|
||||
**Permission:** `AllowAny` (then checks auth manually inside view)
|
||||
|
||||
**Documentation:** Does not mention this inconsistency.
|
||||
|
||||
---
|
||||
|
||||
#### HIGH-3: No Webhook Signature Verification
|
||||
|
||||
**Documentation Claim:** "Optional signature verification" (INTEGRATIONS.md line 349)
|
||||
**Reality:** Only API key validation, NO HMAC signature verification
|
||||
|
||||
**Code Evidence (webhooks.py):**
|
||||
```python
|
||||
api_key = request.headers.get('X-IGNY8-API-KEY')
|
||||
if not stored_api_key or stored_api_key != api_key:
|
||||
return error_response('Invalid API key', ...)
|
||||
# NO signature validation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### HIGH-4: Webhooks Disable Rate Limiting
|
||||
|
||||
**Documentation Claim:** "Rate limiting on webhook endpoint" (INTEGRATIONS.md line 351)
|
||||
**Reality:** Webhooks explicitly use `NoThrottle` class
|
||||
|
||||
---
|
||||
|
||||
#### HIGH-5: Encryption Falls Back Silently
|
||||
|
||||
**Location:** Plugin - `functions.php:31-48`
|
||||
**Impact:** Admin unaware if encryption fails
|
||||
|
||||
---
|
||||
|
||||
### 4.3 MEDIUM SEVERITY ISSUES
|
||||
|
||||
1. **SSRF Risk in Connection Testing** - No IP validation
|
||||
2. **credentials_json Exposed in Serializer** - All fields returned
|
||||
3. **Flexible ID Lookup Enables Enumeration** - `/post-status/{id}` accepts both IDs
|
||||
4. **Excessive API Request Logging** - Keys in logs
|
||||
5. **WordPress URL Not Validated** - No HTTPS enforcement
|
||||
6. **API Key Partially Visible** - `/verify-key` shows 15-char prefix
|
||||
|
||||
### 4.4 LOW SEVERITY ISSUES
|
||||
|
||||
1. Version disclosure in multiple endpoints
|
||||
2. No CORS headers defined on plugin endpoints
|
||||
3. Webhook logs stored long-term (500 entries)
|
||||
|
||||
---
|
||||
|
||||
## PART 5: REST API ENDPOINTS INVENTORY (VERIFIED)
|
||||
|
||||
### 5.1 Backend Endpoints (Verified Against urls.py)
|
||||
|
||||
| Endpoint | Method | Permission | Documentation |
|
||||
|----------|--------|------------|---------------|
|
||||
| `/api/v1/integration/integrations/` | CRUD | IsAuthenticatedAndActive + IsEditorOrAbove | Correct |
|
||||
| `/api/v1/integration/integrations/test-connection/` (collection) | POST | **AllowAny** | NOT documented as AllowAny |
|
||||
| `/api/v1/integration/integrations/{id}/test-connection/` | POST | Authenticated | Correct |
|
||||
| `/api/v1/integration/integrations/generate-api-key/` | POST | Authenticated | Correct |
|
||||
| `/api/v1/integration/integrations/revoke-api-key/` | POST | Authenticated | Correct |
|
||||
| `/api/v1/integration/webhooks/wordpress/status/` | POST | AllowAny (header auth) | Correct |
|
||||
| `/api/v1/integration/webhooks/wordpress/metadata/` | POST | AllowAny (header auth) | Correct |
|
||||
| `/api/v1/publisher/publish/` | POST | Authenticated | Correct |
|
||||
| `/api/v1/writer/content/{id}/publish_to_wordpress/` | POST | Authenticated | Correct |
|
||||
| `/api/v1/writer/content/{id}/schedule/` | POST | Authenticated | Correct (v1.3.2) |
|
||||
| `/api/v1/writer/content/{id}/unschedule/` | POST | Authenticated | Correct (v1.3.2) |
|
||||
|
||||
### 5.2 Plugin Endpoints (Verified Against class-igny8-rest-api.php)
|
||||
|
||||
| Endpoint | Method | Permission | Documentation Match |
|
||||
|----------|--------|------------|---------------------|
|
||||
| `/wp-json/igny8/v1/status` | GET | **PUBLIC** | Not flagged as security issue |
|
||||
| `/wp-json/igny8/v1/site-metadata/` | GET | **PUBLIC** (internal check) | Not flagged |
|
||||
| `/wp-json/igny8/v1/verify-key` | GET | check_permission | Correct |
|
||||
| `/wp-json/igny8/v1/publish` | POST | check_permission | Correct |
|
||||
| `/wp-json/igny8/v1/post-by-content-id/{id}` | GET | check_permission | Correct |
|
||||
| `/wp-json/igny8/v1/post-by-task-id/{id}` | GET | check_permission | Correct |
|
||||
| `/wp-json/igny8/v1/post-status/{id}` | GET | check_permission | Correct |
|
||||
| `/wp-json/igny8/v1/event` | POST | verify_webhook_secret | Correct |
|
||||
|
||||
---
|
||||
|
||||
## PART 6: DATA STORAGE ANALYSIS (VERIFIED)
|
||||
|
||||
### 6.1 Site Model Fields (auth/models.py)
|
||||
|
||||
| Field | Documentation Status | Actual Usage | Action |
|
||||
|-------|---------------------|--------------|--------|
|
||||
| `wp_api_key` | Documented as primary | ACTIVE - single source of truth | KEEP + ENCRYPT |
|
||||
| `domain` | Documented | ACTIVE - WordPress URL | KEEP |
|
||||
| `wp_url` | Documented as "deprecated" | LEGACY - fallback in publisher_service | EVALUATE for removal |
|
||||
| `wp_username` | Documented as "deprecated" | **ZERO USAGE** in codebase | **REMOVE** |
|
||||
| `wp_app_password` | Documented as "deprecated" | **ZERO USAGE** in codebase | **REMOVE** |
|
||||
|
||||
### 6.2 SiteIntegration Model Fields
|
||||
|
||||
| Field | Documentation Status | Actual Usage | Action |
|
||||
|-------|---------------------|--------------|--------|
|
||||
| `site` | Documented | ACTIVE | KEEP |
|
||||
| `platform` | Documented | ACTIVE ('wordpress') | KEEP |
|
||||
| `config_json` | Documented as storing URL | ACTIVE (site_url only) | KEEP |
|
||||
| `credentials_json` | Documented as storing creds | **EMPTY** for WordPress | Documentation outdated |
|
||||
| `sync_status` | Documented | ACTIVE | KEEP |
|
||||
|
||||
**Documentation Gap:** INTEGRATIONS.md line 97-98 shows `api_key` and `username` as SiteIntegration fields, but WordPress actually uses Site.wp_api_key.
|
||||
|
||||
### 6.3 Content Model Fields
|
||||
|
||||
| Field | Documentation Status | Actual Usage | Action |
|
||||
|-------|---------------------|--------------|--------|
|
||||
| `status` | Correctly documented as editorial | ACTIVE | KEEP |
|
||||
| `site_status` | Correctly documented as publishing | ACTIVE | KEEP |
|
||||
| `external_id` | Documented | ACTIVE - WordPress post ID | KEEP |
|
||||
| `external_url` | Documented | ACTIVE - WordPress URL | KEEP |
|
||||
| `external_type` | Not documented | **NEVER USED** | **REMOVE** |
|
||||
| `external_metadata` | Not documented | Only set to {} | **REMOVE** |
|
||||
| `sync_status` (on Content) | Not documented | **NEVER USED** | **REMOVE** |
|
||||
|
||||
### 6.4 ContentTaxonomy Model Fields
|
||||
|
||||
| Field | Documentation Status | Actual Usage | Action |
|
||||
|-------|---------------------|--------------|--------|
|
||||
| `external_id` | Documented | ACTIVE | KEEP |
|
||||
| `external_taxonomy` | Documented | ACTIVE | KEEP |
|
||||
| `sync_status` | Not documented | **NEVER USED** | **REMOVE** |
|
||||
|
||||
---
|
||||
|
||||
## PART 7: CELERY TASKS STATUS (VERIFIED)
|
||||
|
||||
### 7.1 Scheduled Tasks (celery.py Beat Schedule)
|
||||
|
||||
| Task | Schedule | Documentation | Status |
|
||||
|------|----------|---------------|--------|
|
||||
| `schedule_approved_content` | Every hour | Documented (SCHEDULED-CONTENT-PUBLISHING.md) | **ACTIVE** |
|
||||
| `process_scheduled_publications` | Every 5 min | Documented | **ACTIVE** |
|
||||
| `publish_content_to_wordpress` | On-demand | Documented | **ACTIVE** |
|
||||
|
||||
### 7.2 Missing Tasks (Documented but NOT Scheduled)
|
||||
|
||||
**`process_pending_wordpress_publications`** - WORDPRESS-INTEGRATION-FLOW.md mentions this task exists but notes:
|
||||
> "CURRENT STATUS: This task is NOT in Celery Beat schedule!"
|
||||
|
||||
This matches the documentation correctly.
|
||||
|
||||
---
|
||||
|
||||
## PART 8: PLUGIN DISTRIBUTION SYSTEM (VERIFIED)
|
||||
|
||||
### 8.1 Plugin Models (Correctly Documented)
|
||||
|
||||
| Model | Documentation | Implementation | Match |
|
||||
|-------|---------------|----------------|-------|
|
||||
| `Plugin` | WORDPRESS-INTEGRATION.md | plugins/models.py | ✅ |
|
||||
| `PluginVersion` | Documented with all fields | Matches implementation | ✅ |
|
||||
| `PluginInstallation` | Documented | Matches implementation | ✅ |
|
||||
| `PluginDownload` | Documented | Matches implementation | ✅ |
|
||||
|
||||
### 8.2 Plugin API Endpoints (Correctly Documented)
|
||||
|
||||
| Endpoint | Documentation | Implementation |
|
||||
|----------|---------------|----------------|
|
||||
| `/api/plugins/{slug}/download/` | INDEX.md line 24 | ✅ Working |
|
||||
| `/api/plugins/{slug}/check-update/` | INDEX.md line 25 | ✅ Working |
|
||||
| `/api/plugins/{slug}/info/` | INDEX.md line 26 | ✅ Working |
|
||||
| `/api/plugins/{slug}/register/` | INDEX.md line 27 | ✅ Working |
|
||||
| `/api/plugins/{slug}/health-check/` | INDEX.md line 28 | ✅ Working |
|
||||
|
||||
---
|
||||
|
||||
## PART 9: UNUSED/DEAD CODE INVENTORY
|
||||
|
||||
### 9.1 Fields to Remove (Verified Zero Usage)
|
||||
|
||||
| Model | Field | Evidence |
|
||||
|-------|-------|----------|
|
||||
| Site | `wp_username` | `grep -r "wp_username" --include="*.py"` = 0 results |
|
||||
| Site | `wp_app_password` | `grep -r "wp_app_password" --include="*.py"` = 0 results |
|
||||
| Content | `external_type` | Never read in codebase |
|
||||
| Content | `external_metadata` | Only set to {} in publisher_service.py |
|
||||
| Content | `sync_status` | SiteIntegration.sync_status used instead |
|
||||
| ContentTaxonomy | `sync_status` | Never read or written |
|
||||
|
||||
### 9.2 Redundant Code Paths
|
||||
|
||||
1. **Duplicate Connection Testing** - Two endpoints with same logic
|
||||
2. **Duplicate API Key Validation** - Same validation in 4+ files
|
||||
3. **Dead Admin Bulk Actions** - `bulk_trigger_sync()`, `bulk_test_connection()` have TODO comments
|
||||
|
||||
### 9.3 Plugin Unused Options
|
||||
|
||||
| Option | Status |
|
||||
|--------|--------|
|
||||
| `igny8_access_token` | Redundant with igny8_api_key |
|
||||
| `igny8_access_token_issued` | Referenced but not used in auth |
|
||||
|
||||
---
|
||||
|
||||
## PART 10: DATA DUPLICATION ISSUES
|
||||
|
||||
### 10.1 API Key Duplication
|
||||
|
||||
| Location | Field | Role |
|
||||
|----------|-------|------|
|
||||
| Django Site model | `wp_api_key` | **PRIMARY** (single source of truth) |
|
||||
| Django SiteIntegration | `credentials_json` | EMPTY for WordPress |
|
||||
| WordPress | `igny8_api_key` | COPY (must match primary) |
|
||||
| WordPress | `igny8_access_token` | REDUNDANT (should remove) |
|
||||
|
||||
**Risk:** Documentation mentions credentials_json but WordPress doesn't use it.
|
||||
|
||||
### 10.2 URL Duplication
|
||||
|
||||
| Location | Field | Role |
|
||||
|----------|-------|------|
|
||||
| Site.domain | Primary | ACTIVE |
|
||||
| Site.wp_url | Legacy | Fallback only |
|
||||
| SiteIntegration.config_json['site_url'] | Configuration | ACTIVE |
|
||||
|
||||
---
|
||||
|
||||
## PART 11: RECOMMENDATIONS
|
||||
|
||||
### Immediate Actions (Critical)
|
||||
|
||||
| Priority | Issue | Action | Documentation Update |
|
||||
|----------|-------|--------|---------------------|
|
||||
| 1 | Plain text API keys | Implement field encryption | Update INTEGRATIONS.md |
|
||||
| 2 | Timing attack (strpos) | Use hash_equals everywhere | Update plugin docs |
|
||||
| 3 | Diagnostic logging | Remove or conditional | Update plugin docs |
|
||||
| 4 | Public endpoints | Secure /status and /site-metadata | Update WORDPRESS-INTEGRATION-FLOW.md |
|
||||
|
||||
### Short-term Actions (High)
|
||||
|
||||
| Priority | Issue | Action | Documentation Update |
|
||||
|----------|-------|--------|---------------------|
|
||||
| 5 | Permission inconsistency | Fix test_connection_collection | Update ENDPOINTS.md |
|
||||
| 6 | No webhook signatures | Implement HMAC verification | Update INTEGRATIONS.md |
|
||||
| 7 | No rate limiting | Enable throttling on webhooks | Update INTEGRATIONS.md |
|
||||
| 8 | API key in response | Remove from verify-key | N/A |
|
||||
|
||||
### Documentation Updates Required
|
||||
|
||||
| Document | Updates Needed |
|
||||
|----------|----------------|
|
||||
| INTEGRATIONS.md | Remove "encrypted at rest" claim, fix field references |
|
||||
| WORDPRESS-INTEGRATION-FLOW.md | Flag public endpoints as security concern |
|
||||
| ENDPOINTS.md | Note AllowAny on test-connection collection |
|
||||
|
||||
### Cleanup Actions
|
||||
|
||||
| Action | Fields to Remove | Migration Required |
|
||||
|--------|------------------|-------------------|
|
||||
| Remove from Site | wp_username, wp_app_password | Yes |
|
||||
| Remove from Content | external_type, external_metadata, sync_status | Yes |
|
||||
| Remove from ContentTaxonomy | sync_status | Yes |
|
||||
| Remove from Plugin | igny8_access_token option | Plugin update |
|
||||
|
||||
---
|
||||
|
||||
## PART 12: PUBLISHING WORKFLOW SUMMARY
|
||||
|
||||
### What's Working
|
||||
|
||||
| Flow | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| Manual publish button | ✅ Working | ContentViewSet.publish_to_wordpress |
|
||||
| Scheduled publishing | ✅ Working | Celery Beat tasks active |
|
||||
| Plugin distribution | ✅ Working | Auto-update mechanism functional |
|
||||
| Webhook status sync | ✅ Working | WordPress → IGNY8 updates |
|
||||
|
||||
### What's NOT Working
|
||||
|
||||
| Flow | Status | Issue |
|
||||
|------|--------|-------|
|
||||
| Automation Stage 7 → WP | ❌ Broken | Sets status but no WP push |
|
||||
| Content update sync | ❌ Missing | No republish capability |
|
||||
| WordPress → IGNY8 import | ❌ Missing | No pull sync feature |
|
||||
|
||||
### Documented But Not Implemented
|
||||
|
||||
| Feature | Documentation Reference | Status |
|
||||
|---------|------------------------|--------|
|
||||
| Webhook signature verification | INTEGRATIONS.md line 349 | NOT implemented |
|
||||
| Webhook rate limiting | INTEGRATIONS.md line 351 | NOT implemented |
|
||||
| Credential encryption | INTEGRATIONS.md line 345 | NOT implemented |
|
||||
|
||||
---
|
||||
|
||||
## APPENDIX A: FILES AUDITED
|
||||
|
||||
### Backend Files
|
||||
- `backend/igny8_core/auth/models.py` - Site model, wp_api_key
|
||||
- `backend/igny8_core/business/integration/models.py` - SiteIntegration, SyncEvent
|
||||
- `backend/igny8_core/business/publishing/models.py` - PublishingRecord
|
||||
- `backend/igny8_core/business/content/models.py` - Content, external fields
|
||||
- `backend/igny8_core/modules/integration/views.py` - API endpoints
|
||||
- `backend/igny8_core/modules/integration/webhooks.py` - Webhook handlers
|
||||
- `backend/igny8_core/business/publishing/services/publisher_service.py`
|
||||
- `backend/igny8_core/business/publishing/services/adapters/wordpress_adapter.py`
|
||||
- `backend/igny8_core/tasks/publishing_scheduler.py` - Celery tasks
|
||||
- `backend/igny8_core/tasks/wordpress_publishing.py` - Publishing task
|
||||
- `backend/igny8_core/celery.py` - Beat schedule
|
||||
|
||||
### Plugin Files
|
||||
- `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php`
|
||||
- `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-api.php`
|
||||
- `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-webhooks.php`
|
||||
- `plugins/wordpress/source/igny8-wp-bridge/includes/functions.php`
|
||||
|
||||
### Documentation Files Reviewed
|
||||
- `docs/60-PLUGINS/WORDPRESS-INTEGRATION.md`
|
||||
- `docs/60-PLUGINS/INDEX.md`
|
||||
- `docs/60-PLUGINS/PLUGIN-UPDATE-WORKFLOW.md`
|
||||
- `docs/50-DEPLOYMENT/WORDPRESS-INTEGRATION-FLOW.md`
|
||||
- `docs/10-MODULES/PUBLISHER.md`
|
||||
- `docs/10-MODULES/INTEGRATIONS.md`
|
||||
- `docs/40-WORKFLOWS/CONTENT-PIPELINE.md`
|
||||
- `docs/40-WORKFLOWS/SCHEDULED-CONTENT-PUBLISHING.md`
|
||||
- `docs/00-SYSTEM/ARCHITECTURE.md`
|
||||
- `docs/20-API/ENDPOINTS.md`
|
||||
|
||||
---
|
||||
|
||||
## APPENDIX B: VULNERABILITY SEVERITY MATRIX
|
||||
|
||||
| ID | Title | CVSS | Documentation Claim | Reality |
|
||||
|----|-------|------|---------------------|---------|
|
||||
| CRITICAL-1 | Plain text API keys | 9.0 | "Encrypted at rest" | Plain text CharField |
|
||||
| CRITICAL-2 | Timing attack | 8.0 | Not mentioned | strpos vulnerability |
|
||||
| CRITICAL-3 | Diagnostic logging | 8.0 | Not mentioned | Full request bodies logged |
|
||||
| HIGH-1 | Public endpoints | 7.0 | Not flagged | Information disclosure |
|
||||
| HIGH-2 | Permission inconsistency | 6.5 | Not documented | AllowAny misuse |
|
||||
| HIGH-3 | No webhook signatures | 6.0 | "Optional" | Not implemented at all |
|
||||
| HIGH-4 | No rate limiting | 5.5 | "Rate limiting enabled" | NoThrottle class used |
|
||||
| HIGH-5 | Silent encryption fail | 6.0 | Not mentioned | Falls back to plain text |
|
||||
|
||||
---
|
||||
|
||||
**End of Calibrated Audit Report**
|
||||
|
||||
**Next Steps:**
|
||||
1. Review findings with development team
|
||||
2. Prioritize critical security fixes
|
||||
3. Update documentation to match reality
|
||||
4. Create migration plan for field removal
|
||||
5. Implement encryption before production
|
||||
297
docs/plans/ACCURATE-INTEGRATION-SECURITY-PLAN.md
Normal file
297
docs/plans/ACCURATE-INTEGRATION-SECURITY-PLAN.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# IGNY8 ↔ WordPress Integration Security Plan
|
||||
|
||||
**Date:** 2026-01-13
|
||||
**Status:** Audit Complete - Plan Ready
|
||||
|
||||
---
|
||||
|
||||
## PART 1: ACTUAL ARCHITECTURE (VERIFIED)
|
||||
|
||||
### 1.1 Publishing Flow (WORKING - No Changes Needed)
|
||||
|
||||
```
|
||||
Frontend (Review/Approved pages)
|
||||
│
|
||||
▼
|
||||
POST /v1/publisher/publish/
|
||||
│
|
||||
▼
|
||||
PublisherService._publish_to_destination()
|
||||
│
|
||||
├── Gets API key: content.site.wp_api_key (SINGLE SOURCE OF TRUTH)
|
||||
├── Gets URL: content.site.domain or content.site.url
|
||||
│
|
||||
▼
|
||||
WordPressAdapter._publish_via_api_key()
|
||||
│
|
||||
▼
|
||||
POST {site_url}/wp-json/igny8/v1/publish
|
||||
Headers: X-IGNY8-API-KEY: {api_key}
|
||||
│
|
||||
▼
|
||||
Plugin: check_permission() validates header against stored igny8_api_key
|
||||
│
|
||||
▼
|
||||
Plugin creates/updates WordPress post
|
||||
```
|
||||
|
||||
**Key Finding:** Publishing does NOT use SiteIntegration model. It uses:
|
||||
- `Site.wp_api_key` - for authentication
|
||||
- `Site.domain` - for WordPress URL
|
||||
|
||||
### 1.2 Data Storage Locations
|
||||
|
||||
| Location | Field | Purpose | Used By |
|
||||
|----------|-------|---------|---------|
|
||||
| **Django Site model** | `wp_api_key` | API key (source of truth) | Publishing, test-connection |
|
||||
| **Django Site model** | `domain` | WordPress URL | Publishing |
|
||||
| **WordPress wp_options** | `igny8_api_key` | Stored copy of API key | Plugin auth check |
|
||||
| **WordPress wp_options** | `igny8_site_id` | Site ID from key | Plugin display |
|
||||
| **WordPress wp_options** | `igny8_integration_id` | Integration ID | Plugin (currently unused) |
|
||||
| **WordPress wp_options** | `igny8_last_structure_sync` | Last sync timestamp | Connection status UI |
|
||||
|
||||
### 1.3 SiteIntegration Model Status
|
||||
|
||||
**Table exists:** `igny8_site_integrations`
|
||||
**Records:** 0 (empty)
|
||||
**Used by:**
|
||||
- `publishing_scheduler.py` - scheduled publishing (requires record)
|
||||
- `wordpress_publishing.py` - Celery task path (requires record)
|
||||
- `sync_metadata_service.py` - metadata sync (requires record)
|
||||
- `ContentViewSet.publish` action (requires record)
|
||||
|
||||
**NOT used by:**
|
||||
- `PublisherService` (main UI publishing path) - uses Site directly
|
||||
- `WordPressAdapter` - uses Site.wp_api_key directly
|
||||
|
||||
---
|
||||
|
||||
## PART 2: SECURITY ISSUES FOUND
|
||||
|
||||
### 2.1 Plugin Public Endpoints (SECURITY RISK)
|
||||
|
||||
| Endpoint | Current Permission | Risk | Data Exposed |
|
||||
|----------|-------------------|------|--------------|
|
||||
| `/igny8/v1/status` | `__return_true` (PUBLIC) | Medium | Plugin installed, has_api_key, plugin version |
|
||||
| `/igny8/v1/site-metadata/` | `__return_true` (PUBLIC*) | Low | Post types, taxonomies (auth checked inside) |
|
||||
|
||||
*Note: `/site-metadata/` checks permission inside callback but still registers as public
|
||||
|
||||
### 2.2 Backend test-connection Flow
|
||||
|
||||
The `test_connection_collection` endpoint:
|
||||
1. Uses `AllowAny` permission class
|
||||
2. BUT validates authentication inside the handler
|
||||
3. Calls public `/status` endpoint to check if plugin installed
|
||||
4. Then calls authenticated `/verify-key` endpoint
|
||||
|
||||
**Issue:** Relies on public `/status` endpoint to detect plugin presence.
|
||||
|
||||
---
|
||||
|
||||
## PART 3: RECOMMENDED CHANGES
|
||||
|
||||
### 3.1 Plugin Changes (REQUIRED)
|
||||
|
||||
#### 3.1.1 Make `/status` Endpoint Authenticated
|
||||
|
||||
**File:** `plugins/wordpress/source/igny8-wp-bridge/includes/class-igny8-rest-api.php`
|
||||
|
||||
**Current (lines 83-87):**
|
||||
```php
|
||||
register_rest_route('igny8/v1', '/status', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_status'),
|
||||
'permission_callback' => '__return_true', // Public endpoint for health checks
|
||||
));
|
||||
```
|
||||
|
||||
**Change to:**
|
||||
```php
|
||||
register_rest_route('igny8/v1', '/status', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_status'),
|
||||
'permission_callback' => array($this, 'check_api_key_auth'),
|
||||
));
|
||||
```
|
||||
|
||||
#### 3.1.2 Add Lightweight Auth Check Method
|
||||
|
||||
Add new method to allow API key OR no key if not configured yet:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Check API key authentication for status-type endpoints
|
||||
* Returns true if: no API key stored yet, OR valid API key provided
|
||||
*/
|
||||
public function check_api_key_auth($request) {
|
||||
$stored_api_key = function_exists('igny8_get_secure_option')
|
||||
? igny8_get_secure_option('igny8_api_key')
|
||||
: get_option('igny8_api_key');
|
||||
|
||||
// If no API key configured yet, allow access (plugin not connected)
|
||||
if (empty($stored_api_key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If API key is configured, require valid key in header
|
||||
$header_api_key = $request->get_header('x-igny8-api-key');
|
||||
if ($header_api_key && hash_equals($stored_api_key, $header_api_key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new WP_Error('rest_forbidden', 'Invalid API key', array('status' => 401));
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.1.3 Fix `/site-metadata/` Route Registration
|
||||
|
||||
**Current (lines 74-79):**
|
||||
```php
|
||||
register_rest_route('igny8/v1', '/site-metadata/', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_site_metadata'),
|
||||
'permission_callback' => '__return_true',
|
||||
));
|
||||
```
|
||||
|
||||
**Change to:**
|
||||
```php
|
||||
register_rest_route('igny8/v1', '/site-metadata/', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array($this, 'get_site_metadata'),
|
||||
'permission_callback' => array($this, 'check_permission'),
|
||||
));
|
||||
```
|
||||
|
||||
Then remove the internal permission check from `get_site_metadata()` method.
|
||||
|
||||
### 3.2 Backend Changes (REQUIRED)
|
||||
|
||||
#### 3.2.1 Update test-connection to Not Rely on Public Endpoint
|
||||
|
||||
**File:** `backend/igny8_core/modules/integration/views.py`
|
||||
|
||||
**Current approach:**
|
||||
1. Call `/status` (public) to check plugin installed
|
||||
2. Call `/verify-key` (authenticated) to verify key
|
||||
|
||||
**New approach:**
|
||||
1. Call `/verify-key` directly
|
||||
2. If 200 → plugin installed AND key valid
|
||||
3. If 404 → plugin not installed (route doesn't exist)
|
||||
4. If 401/403 → plugin installed but key mismatch
|
||||
|
||||
**Change Check 2 section (approximately lines 218-237):**
|
||||
|
||||
```python
|
||||
# Check 2 & 3 Combined: Plugin installed + API key verification
|
||||
# Use /verify-key endpoint - if it succeeds, both are confirmed
|
||||
try:
|
||||
verify_response = http_requests.get(
|
||||
f"{site_url.rstrip('/')}/wp-json/igny8/v1/verify-key",
|
||||
headers={
|
||||
'X-IGNY8-API-KEY': stored_api_key,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
if verify_response.status_code == 200:
|
||||
health_checks['plugin_installed'] = True
|
||||
health_checks['plugin_has_api_key'] = True
|
||||
health_checks['api_key_verified'] = True
|
||||
elif verify_response.status_code == 404:
|
||||
# Route not found = plugin not installed
|
||||
issues.append("IGNY8 plugin not installed on WordPress site")
|
||||
elif verify_response.status_code in [401, 403]:
|
||||
# Auth failed = plugin installed but key doesn't match
|
||||
health_checks['plugin_installed'] = True
|
||||
issues.append("API key mismatch - copy the API key from IGNY8 to WordPress plugin settings")
|
||||
else:
|
||||
issues.append(f"Unexpected response from plugin: HTTP {verify_response.status_code}")
|
||||
except http_requests.exceptions.ConnectionError:
|
||||
issues.append("Cannot connect to WordPress site")
|
||||
except Exception as e:
|
||||
issues.append(f"Connection error: {str(e)}")
|
||||
```
|
||||
|
||||
### 3.3 SiteIntegration Decision (OPTIONAL)
|
||||
|
||||
The SiteIntegration model is used by:
|
||||
- Scheduled publishing task
|
||||
- ContentViewSet.publish action
|
||||
- Metadata sync service
|
||||
|
||||
**Options:**
|
||||
|
||||
**Option A: Remove SiteIntegration entirely**
|
||||
- Modify scheduled publishing to use Site directly
|
||||
- Modify sync service to use Site directly
|
||||
- Remove model and migrations
|
||||
- **Effort:** High, risk of breaking things
|
||||
|
||||
**Option B: Keep but don't require it**
|
||||
- Current main publishing path works without it
|
||||
- Scheduled publishing would need records created
|
||||
- **Effort:** Low, minimal changes
|
||||
|
||||
**Recommendation:** Option B - Keep as-is. The main UI publishing flow works. If scheduled publishing is needed, run:
|
||||
```bash
|
||||
docker exec igny8_backend python manage.py sync_wordpress_api_keys
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PART 4: IMPLEMENTATION ORDER
|
||||
|
||||
### Phase 1: Plugin Security (30 min)
|
||||
1. Add `check_api_key_auth()` method to plugin
|
||||
2. Change `/status` endpoint to use `check_api_key_auth`
|
||||
3. Change `/site-metadata/` to use `check_permission`
|
||||
4. Remove internal permission check from `get_site_metadata()`
|
||||
5. Build and deploy plugin
|
||||
|
||||
### Phase 2: Backend Update (15 min)
|
||||
1. Update `test_connection_collection` to use only `/verify-key`
|
||||
2. Remove `/status` endpoint call
|
||||
3. Test connection flow
|
||||
|
||||
### Phase 3: Testing (15 min)
|
||||
1. Test plugin uninstalled scenario (404 from /verify-key)
|
||||
2. Test plugin installed, no key (401 from /verify-key)
|
||||
3. Test plugin installed, wrong key (401 from /verify-key)
|
||||
4. Test plugin installed, correct key (200 from /verify-key)
|
||||
5. Test publishing still works
|
||||
|
||||
---
|
||||
|
||||
## PART 5: FILES TO MODIFY
|
||||
|
||||
### Plugin Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `includes/class-igny8-rest-api.php` | Add `check_api_key_auth()`, update route permissions |
|
||||
|
||||
### Backend Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `modules/integration/views.py` | Update `test_connection_collection` logic |
|
||||
|
||||
---
|
||||
|
||||
## PART 6: WHAT STAYS THE SAME
|
||||
|
||||
✅ **Publishing flow** - No changes needed
|
||||
✅ **Site.wp_api_key** - Single source of truth
|
||||
✅ **Site.domain** - WordPress URL source
|
||||
✅ **WordPressAdapter** - Works correctly
|
||||
✅ **Plugin /publish endpoint** - Already authenticated
|
||||
✅ **Plugin /verify-key endpoint** - Already authenticated
|
||||
✅ **SiteIntegration model** - Keep for scheduled publishing (optional use)
|
||||
|
||||
---
|
||||
|
||||
## Document History
|
||||
| Date | Author | Changes |
|
||||
|------|--------|---------|
|
||||
| 2026-01-13 | AI | Created after proper code audit |
|
||||
@@ -403,21 +403,13 @@ class Igny8Admin {
|
||||
// Store API key securely
|
||||
if (function_exists('igny8_store_secure_option')) {
|
||||
igny8_store_secure_option('igny8_api_key', $api_key);
|
||||
igny8_store_secure_option('igny8_access_token', $api_key);
|
||||
} else {
|
||||
update_option('igny8_api_key', $api_key);
|
||||
update_option('igny8_access_token', $api_key);
|
||||
}
|
||||
|
||||
// Store site ID
|
||||
// Store site ID and last communication timestamp
|
||||
update_option('igny8_site_id', sanitize_text_field($site_id));
|
||||
|
||||
// Store integration_id from response if provided
|
||||
$response_data = $test_response['data'] ?? array();
|
||||
$integration_id = isset($response_data['integration_id']) ? intval($response_data['integration_id']) : null;
|
||||
if ($integration_id) {
|
||||
update_option('igny8_integration_id', $integration_id);
|
||||
}
|
||||
update_option('igny8_last_communication', current_time('timestamp'));
|
||||
|
||||
add_settings_error(
|
||||
'igny8_settings',
|
||||
@@ -439,17 +431,21 @@ class Igny8Admin {
|
||||
public static function revoke_api_key() {
|
||||
if (function_exists('igny8_delete_secure_option')) {
|
||||
igny8_delete_secure_option('igny8_api_key');
|
||||
igny8_delete_secure_option('igny8_access_token');
|
||||
igny8_delete_secure_option('igny8_refresh_token');
|
||||
} else {
|
||||
delete_option('igny8_api_key');
|
||||
delete_option('igny8_access_token');
|
||||
delete_option('igny8_refresh_token');
|
||||
}
|
||||
|
||||
// Also clear token-issued timestamps
|
||||
delete_option('igny8_token_refreshed_at');
|
||||
// Clear connection data
|
||||
delete_option('igny8_site_id');
|
||||
delete_option('igny8_last_communication');
|
||||
|
||||
// Clean up deprecated options (for older installations)
|
||||
delete_option('igny8_access_token');
|
||||
delete_option('igny8_access_token_issued');
|
||||
delete_option('igny8_integration_id');
|
||||
delete_option('igny8_last_structure_sync');
|
||||
delete_option('igny8_refresh_token');
|
||||
delete_option('igny8_token_refreshed_at');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ if (!defined('ABSPATH')) {
|
||||
|
||||
// Get current page
|
||||
$current_page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : 'igny8-dashboard';
|
||||
$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');
|
||||
$is_connected = !empty($api_key);
|
||||
?>
|
||||
|
||||
@@ -43,19 +43,9 @@ $is_connected = !empty($api_key);
|
||||
|
||||
<nav>
|
||||
<ul class="igny8-sidebar-nav">
|
||||
<li>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-dashboard')); ?>"
|
||||
class="<?php echo ($current_page === 'igny8-dashboard') ? 'active' : ''; ?>">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
<?php _e('Dashboard', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-connection')); ?>"
|
||||
class="<?php echo ($current_page === 'igny8-connection') ? 'active' : ''; ?>">
|
||||
class="<?php echo ($current_page === 'igny8-connection' || $current_page === 'igny8-dashboard') ? 'active' : ''; ?>">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
|
||||
</svg>
|
||||
|
||||
@@ -16,23 +16,26 @@ include IGNY8_BRIDGE_PLUGIN_DIR . 'admin/layout-header.php';
|
||||
// Get connection settings
|
||||
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
$site_id = get_option('igny8_site_id', '');
|
||||
$integration_id = get_option('igny8_integration_id', '');
|
||||
$access_token = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_access_token') : get_option('igny8_access_token');
|
||||
$last_structure_sync = get_option('igny8_last_structure_sync', 0);
|
||||
$is_connected = !empty($access_token) && !empty($integration_id) && !empty($last_structure_sync);
|
||||
$is_connected = !empty($api_key);
|
||||
$date_format = get_option('date_format');
|
||||
$time_format = get_option('time_format');
|
||||
$now = current_time('timestamp');
|
||||
$token_issued = intval(get_option('igny8_access_token_issued', 0));
|
||||
$token_age_text = $token_issued ? sprintf(__('%s ago', 'igny8-bridge'), human_time_diff($token_issued, $now)) : __('Not generated yet', 'igny8-bridge');
|
||||
$token_issued_formatted = $token_issued ? date_i18n($date_format . ' ' . $time_format, $token_issued) : __('—', 'igny8-bridge');
|
||||
$last_health_check = intval(get_option('igny8_last_api_health_check', 0));
|
||||
$last_health_check_formatted = $last_health_check ? date_i18n($date_format . ' ' . $time_format, $last_health_check) : __('Never', 'igny8-bridge');
|
||||
$last_communication = intval(get_option('igny8_last_communication', 0));
|
||||
$last_communication_text = $last_communication ? sprintf(__('%s ago', 'igny8-bridge'), human_time_diff($last_communication, $now)) : __('Never', 'igny8-bridge');
|
||||
$last_communication_formatted = $last_communication ? date_i18n($date_format . ' ' . $time_format, $last_communication) : __('—', 'igny8-bridge');
|
||||
?>
|
||||
|
||||
<div class="igny8-page-header">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||||
<div>
|
||||
<h1><?php _e('Connection', 'igny8-bridge'); ?></h1>
|
||||
<p><?php _e('Manage your IGNY8 API connection and authentication', 'igny8-bridge'); ?></p>
|
||||
</div>
|
||||
<div style="text-align: right; font-size: 13px; color: rgba(255,255,255,0.85);">
|
||||
<span style="display: block;">Plugin v<?php echo esc_html(IGNY8_BRIDGE_VERSION); ?></span>
|
||||
<span style="display: block; opacity: 0.7;">PHP <?php echo esc_html(PHP_VERSION); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!$is_connected): ?>
|
||||
@@ -85,7 +88,9 @@ $last_health_check_formatted = $last_health_check ? date_i18n($date_format . ' '
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Connection Instructions -->
|
||||
<!-- Quick Actions Row -->
|
||||
<div class="igny8-grid igny8-grid-3">
|
||||
<!-- How to Connect Card -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
@@ -95,17 +100,54 @@ $last_health_check_formatted = $last_health_check ? date_i18n($date_format . ' '
|
||||
<?php _e('How to Connect', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ol style="padding-left: 20px; line-height: 1.8;">
|
||||
<ol style="padding-left: 20px; line-height: 1.7; font-size: 14px;">
|
||||
<li><?php _e('Log in to your IGNY8 account', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Navigate to Settings → Integrations', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Find WordPress in the integrations list', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Click "Add Site" to generate a new API key', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Copy the API key and paste it in the field above', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Click "Connect to IGNY8" to establish the connection', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Copy the API key and paste it above', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Click "Connect to IGNY8"', 'igny8-bridge'); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- Settings Card -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<?php _e('Settings', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: var(--igny8-text-dim); margin-bottom: 16px;">
|
||||
<?php _e('Configure post types and taxonomies to sync', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-settings')); ?>" class="igny8-btn igny8-btn-secondary">
|
||||
<?php _e('Configure Settings', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- View Logs Card -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<?php _e('View Logs', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: var(--igny8-text-dim); margin-bottom: 16px;">
|
||||
<?php _e('Review sync history and troubleshoot issues', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-logs')); ?>" class="igny8-btn igny8-btn-secondary">
|
||||
<?php _e('View Logs', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- Connected - Show Connection Details -->
|
||||
<div class="igny8-alert igny8-alert-success">
|
||||
@@ -141,13 +183,6 @@ $last_health_check_formatted = $last_health_check ? date_i18n($date_format . ' '
|
||||
<?php echo esc_html($site_id ?: __('Not set', 'igny8-bridge')); ?>
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div class="igny8-form-group">
|
||||
<label><?php _e('Integration ID', 'igny8-bridge'); ?></label>
|
||||
<code style="display: block; padding: 10px; background: var(--igny8-surface); border-radius: var(--igny8-radius-base);">
|
||||
<?php echo esc_html($integration_id ?: __('Not set', 'igny8-bridge')); ?>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key Details -->
|
||||
@@ -192,6 +227,64 @@ $last_health_check_formatted = $last_health_check ? date_i18n($date_format . ' '
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions Row -->
|
||||
<div class="igny8-grid igny8-grid-3">
|
||||
<!-- How to Publish Card -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<?php _e('Publishing Content', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
<ol style="padding-left: 20px; line-height: 1.7; font-size: 14px;">
|
||||
<li><?php _e('Create content in IGNY8 app', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Review and approve content', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Click "Publish to WordPress"', 'igny8-bridge'); ?></li>
|
||||
<li><?php _e('Content appears on your site', 'igny8-bridge'); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- Settings Card -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<?php _e('Settings', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: var(--igny8-text-dim); margin-bottom: 16px;">
|
||||
<?php _e('Configure post types and taxonomies to sync', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-settings')); ?>" class="igny8-btn igny8-btn-secondary">
|
||||
<?php _e('Configure Settings', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- View Logs Card -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<?php _e('View Logs', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: var(--igny8-text-dim); margin-bottom: 16px;">
|
||||
<?php _e('Review sync history and troubleshoot issues', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-logs')); ?>" class="igny8-btn igny8-btn-secondary">
|
||||
<?php _e('View Logs', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disconnect Section -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Dashboard Page
|
||||
* Dashboard Page - Redirects to Connection Page
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
@@ -10,173 +10,6 @@ if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Include layout header
|
||||
include IGNY8_BRIDGE_PLUGIN_DIR . 'admin/layout-header.php';
|
||||
|
||||
// Get connection status
|
||||
$api_key = get_option('igny8_api_key');
|
||||
$is_connected = !empty($api_key);
|
||||
$site_id = get_option('igny8_site_id');
|
||||
$last_sync = get_option('igny8_last_sync_time');
|
||||
?>
|
||||
|
||||
<div class="igny8-page-header">
|
||||
<h1><?php _e('Dashboard', 'igny8-bridge'); ?></h1>
|
||||
<p><?php _e('Overview of your IGNY8 integration', 'igny8-bridge'); ?></p>
|
||||
</div>
|
||||
|
||||
<?php if (!$is_connected): ?>
|
||||
<div class="igny8-alert igny8-alert-warning">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong><?php _e('Not Connected', 'igny8-bridge'); ?></strong>
|
||||
<p><?php _e('You need to connect to IGNY8 to start syncing content.', 'igny8-bridge'); ?>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-connection')); ?>"><?php _e('Connect now', 'igny8-bridge'); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="igny8-grid igny8-grid-3">
|
||||
<!-- Connection Status Card -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
|
||||
</svg>
|
||||
<?php _e('Connection', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
<div style="padding: 16px 0;">
|
||||
<?php if ($is_connected): ?>
|
||||
<div class="igny8-status igny8-status-connected" style="font-size: 16px; padding: 8px 16px;">
|
||||
<span class="igny8-status-indicator"></span>
|
||||
<?php _e('Connected', 'igny8-bridge'); ?>
|
||||
</div>
|
||||
<?php if ($site_id): ?>
|
||||
<p style="margin-top: 12px; font-size: 13px; color: var(--igny8-text-dim);">
|
||||
<?php _e('Site ID:', 'igny8-bridge'); ?> <code><?php echo esc_html($site_id); ?></code>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="igny8-status igny8-status-disconnected" style="font-size: 16px; padding: 8px 16px;">
|
||||
<span class="igny8-status-indicator"></span>
|
||||
<?php _e('Not Connected', 'igny8-bridge'); ?>
|
||||
</div>
|
||||
<p style="margin-top: 12px;">
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-connection')); ?>" class="igny8-btn igny8-btn-primary">
|
||||
<?php _e('Connect Now', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Status Card -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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('Last Sync', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
<div style="padding: 16px 0;">
|
||||
<?php if ($last_sync): ?>
|
||||
<p style="font-size: 14px; margin: 0;">
|
||||
<?php echo esc_html(human_time_diff($last_sync, current_time('timestamp'))) . ' ' . __('ago', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<p style="font-size: 13px; color: var(--igny8-text-dim); margin: 8px 0 0 0;">
|
||||
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $last_sync)); ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<p style="font-size: 14px; color: var(--igny8-text-dim); margin: 0;">
|
||||
<?php _e('No sync performed yet', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Info Card -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<?php _e('Plugin Info', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
<div style="padding: 16px 0;">
|
||||
<p style="font-size: 14px; margin: 0 0 8px 0;">
|
||||
<strong><?php _e('Version:', 'igny8-bridge'); ?></strong> <?php echo esc_html(IGNY8_BRIDGE_VERSION); ?>
|
||||
</p>
|
||||
<p style="font-size: 14px; margin: 0;">
|
||||
<strong><?php _e('PHP:', 'igny8-bridge'); ?></strong> <?php echo esc_html(PHP_VERSION); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="igny8-card">
|
||||
<div class="igny8-card-header">
|
||||
<h2>
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
<?php _e('Quick Actions', 'igny8-bridge'); ?>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="igny8-grid igny8-grid-2" style="padding: 16px 0;">
|
||||
<div>
|
||||
<h3 style="font-size: 16px; margin: 0 0 12px 0;"><?php _e('Connection', 'igny8-bridge'); ?></h3>
|
||||
<p style="font-size: 14px; color: var(--igny8-text-dim); margin-bottom: 16px;">
|
||||
<?php _e('Manage your API connection and authentication', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-connection')); ?>" class="igny8-btn igny8-btn-secondary">
|
||||
<?php _e('Manage Connection', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="font-size: 16px; margin: 0 0 12px 0;"><?php _e('Settings', 'igny8-bridge'); ?></h3>
|
||||
<p style="font-size: 14px; color: var(--igny8-text-dim); margin-bottom: 16px;">
|
||||
<?php _e('Configure post types and taxonomies to sync', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-settings')); ?>" class="igny8-btn igny8-btn-secondary">
|
||||
<?php _e('Configure Settings', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="font-size: 16px; margin: 0 0 12px 0;"><?php _e('Sync Settings', 'igny8-bridge'); ?></h3>
|
||||
<p style="font-size: 14px; color: var(--igny8-text-dim); margin-bottom: 16px;">
|
||||
<?php _e('Configure automatic sync and content updates', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-sync')); ?>" class="igny8-btn igny8-btn-secondary">
|
||||
<?php _e('Sync Settings', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="font-size: 16px; margin: 0 0 12px 0;"><?php _e('View Logs', 'igny8-bridge'); ?></h3>
|
||||
<p style="font-size: 14px; color: var(--igny8-text-dim); margin-bottom: 16px;">
|
||||
<?php _e('Review sync history and troubleshoot issues', 'igny8-bridge'); ?>
|
||||
</p>
|
||||
<a href="<?php echo esc_url(admin_url('admin.php?page=igny8-logs')); ?>" class="igny8-btn igny8-btn-secondary">
|
||||
<?php _e('View Logs', 'igny8-bridge'); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Include layout footer
|
||||
include IGNY8_BRIDGE_PLUGIN_DIR . 'admin/layout-footer.php';
|
||||
?>
|
||||
// Redirect to connection page (Dashboard is now merged with Connection)
|
||||
wp_redirect(admin_url('admin.php?page=igny8-connection'));
|
||||
exit;
|
||||
|
||||
@@ -13,18 +13,13 @@ if (!defined('ABSPATH')) {
|
||||
// Get current settings
|
||||
$email = get_option('igny8_email', '');
|
||||
$site_id = get_option('igny8_site_id', '');
|
||||
$integration_id = get_option('igny8_integration_id', '');
|
||||
$access_token = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_access_token') : get_option('igny8_access_token');
|
||||
$last_structure_sync = get_option('igny8_last_structure_sync', 0);
|
||||
// Connection is complete only if: API key exists, integration_id exists, and structure has been synced
|
||||
$is_connected = !empty($access_token) && !empty($integration_id) && !empty($last_structure_sync);
|
||||
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
$is_connected = !empty($api_key);
|
||||
$date_format = get_option('date_format');
|
||||
$time_format = get_option('time_format');
|
||||
$now = current_time('timestamp');
|
||||
$token_issued = intval(get_option('igny8_access_token_issued', 0));
|
||||
$token_age_text = $token_issued ? sprintf(__('%s ago', 'igny8-bridge'), human_time_diff($token_issued, $now)) : __('Not generated yet', 'igny8-bridge');
|
||||
$token_issued_formatted = $token_issued ? date_i18n($date_format . ' ' . $time_format, $token_issued) : __('—', 'igny8-bridge');
|
||||
$last_communication = intval(get_option('igny8_last_communication', 0));
|
||||
$last_communication_text = $last_communication ? sprintf(__('%s ago', 'igny8-bridge'), human_time_diff($last_communication, $now)) : __('Never', 'igny8-bridge');
|
||||
$last_health_check = intval(get_option('igny8_last_api_health_check', 0));
|
||||
$last_health_check_formatted = $last_health_check ? date_i18n($date_format . ' ' . $time_format, $last_health_check) : __('Never', 'igny8-bridge');
|
||||
$last_site_sync = intval(get_option('igny8_last_site_sync', 0));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: IGNY8 WordPress Bridge
|
||||
* Plugin URI: https://igny8.com/igny8-wp-bridge
|
||||
* Description: Lightweight bridge plugin that connects WordPress to IGNY8 API for one-way content publishing.
|
||||
* Version: 1.4.2
|
||||
* Version: 1.5
|
||||
* Author: IGNY8
|
||||
* Author URI: https://igny8.com/
|
||||
* License: GPL v2 or later
|
||||
@@ -22,7 +22,7 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
// Define plugin constants
|
||||
define('IGNY8_BRIDGE_VERSION', '1.4.2');
|
||||
define('IGNY8_BRIDGE_VERSION', '1.5');
|
||||
define('IGNY8_BRIDGE_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||
define('IGNY8_BRIDGE_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||
define('IGNY8_BRIDGE_PLUGIN_FILE', __FILE__);
|
||||
|
||||
@@ -123,21 +123,32 @@ class Igny8RestAPI {
|
||||
public function check_permission($request) {
|
||||
// Check if authenticated with IGNY8 via API key
|
||||
$api = new Igny8API();
|
||||
$stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
|
||||
if (empty($stored_api_key)) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
__('IGNY8 API key not configured', 'igny8-bridge'),
|
||||
array('status' => 401)
|
||||
);
|
||||
}
|
||||
|
||||
// Accept explicit X-IGNY8-API-KEY header for incoming requests
|
||||
$header_api_key = $request->get_header('x-igny8-api-key');
|
||||
if ($header_api_key) {
|
||||
$stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
if ($stored_api_key && hash_equals($stored_api_key, $header_api_key)) {
|
||||
if ($header_api_key && hash_equals($stored_api_key, $header_api_key)) {
|
||||
// Update last communication timestamp
|
||||
update_option('igny8_last_communication', current_time('timestamp'));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check Authorization Bearer header
|
||||
// Check Authorization Bearer header (timing-safe comparison)
|
||||
$auth_header = $request->get_header('Authorization');
|
||||
if ($auth_header) {
|
||||
$stored_api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
if ($stored_api_key && strpos($auth_header, 'Bearer ' . $stored_api_key) !== false) {
|
||||
// Extract token from "Bearer <token>" format
|
||||
$expected_header = 'Bearer ' . $stored_api_key;
|
||||
if (hash_equals($expected_header, $auth_header)) {
|
||||
// Update last communication timestamp
|
||||
update_option('igny8_last_communication', current_time('timestamp'));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,21 +360,14 @@ if (!function_exists('igny8_is_connection_enabled')) {
|
||||
*/
|
||||
function igny8_get_connection_state() {
|
||||
$api_key = function_exists('igny8_get_secure_option') ? igny8_get_secure_option('igny8_api_key') : get_option('igny8_api_key');
|
||||
$integration_id = get_option('igny8_integration_id');
|
||||
$last_structure_sync = get_option('igny8_last_structure_sync');
|
||||
|
||||
if (empty($api_key)) {
|
||||
igny8_log_connection_state('not_connected', 'No API key found');
|
||||
return 'not_connected';
|
||||
}
|
||||
|
||||
if (!empty($api_key) && !empty($integration_id) && !empty($last_structure_sync)) {
|
||||
igny8_log_connection_state('connected', 'Fully connected and synced');
|
||||
igny8_log_connection_state('connected', 'API key configured');
|
||||
return 'connected';
|
||||
}
|
||||
|
||||
igny8_log_connection_state('configured', 'API key set, pending structure sync');
|
||||
return 'configured';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,20 +8,18 @@
|
||||
class Test_Revoke_Api_Key extends WP_UnitTestCase {
|
||||
|
||||
public function test_revoke_api_key_clears_options() {
|
||||
// Simulate stored API key and tokens
|
||||
// Simulate stored connection data (simplified structure)
|
||||
update_option('igny8_api_key', 'test-key-123');
|
||||
update_option('igny8_access_token', 'test-key-123');
|
||||
update_option('igny8_refresh_token', 'refresh-123');
|
||||
update_option('igny8_token_refreshed_at', time());
|
||||
update_option('igny8_site_id', '12345');
|
||||
update_option('igny8_last_communication', time());
|
||||
|
||||
// Call revoke
|
||||
Igny8Admin::revoke_api_key();
|
||||
|
||||
// Assert removed
|
||||
// Assert core connection options removed
|
||||
$this->assertFalse(get_option('igny8_api_key'));
|
||||
$this->assertFalse(get_option('igny8_access_token'));
|
||||
$this->assertFalse(get_option('igny8_refresh_token'));
|
||||
$this->assertFalse(get_option('igny8_token_refreshed_at'));
|
||||
$this->assertFalse(get_option('igny8_site_id'));
|
||||
$this->assertFalse(get_option('igny8_last_communication'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ class Test_Site_Metadata_Endpoint extends WP_UnitTestCase {
|
||||
update_option('igny8_connection_enabled', 1);
|
||||
|
||||
// Create a fake API key so permission checks pass via Igny8API
|
||||
// Note: Only igny8_api_key is needed (access_token was deprecated)
|
||||
update_option('igny8_api_key', 'test-api-key-123');
|
||||
update_option('igny8_access_token', 'test-api-key-123');
|
||||
|
||||
// Build request
|
||||
$request = new WP_REST_Request('GET', '/igny8/v1/site-metadata/');
|
||||
|
||||
@@ -14,12 +14,15 @@ if (!defined('WP_UNINSTALL_PLUGIN')) {
|
||||
|
||||
// Remove all IGNY8 options from wp_options table
|
||||
$igny8_options = array(
|
||||
// Authentication & Connection
|
||||
// Core Connection (current)
|
||||
'igny8_api_key',
|
||||
'igny8_site_id',
|
||||
'igny8_last_communication',
|
||||
|
||||
// Deprecated Auth (kept for cleanup of old installations)
|
||||
'igny8_access_token',
|
||||
'igny8_refresh_token',
|
||||
'igny8_api_key',
|
||||
'igny8_email',
|
||||
'igny8_site_id',
|
||||
'igny8_integration_id',
|
||||
'igny8_access_token_issued',
|
||||
'igny8_token_refreshed_at',
|
||||
|
||||
Reference in New Issue
Block a user