fixing issues of integration with wordpress plugin

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-12 23:25:47 +00:00
parent ad828a9fcd
commit 5c3aa90e91
18 changed files with 1414 additions and 427 deletions

View File

@@ -78,6 +78,7 @@ class SiteSerializer(serializers.ModelSerializer):
'industry', 'industry_name', 'industry_slug',
'is_active', 'status',
'site_type', 'hosting_type', 'seo_metadata',
'wp_api_key', # WordPress API key (single source of truth for integration)
'sectors_count', 'active_sectors_count', 'selected_sectors',
'can_add_sectors', 'keywords_count', 'has_integration',
'created_at', 'updated_at'
@@ -86,6 +87,7 @@ class SiteSerializer(serializers.ModelSerializer):
# Explicitly specify required fields for clarity
extra_kwargs = {
'industry': {'required': True, 'error_messages': {'required': 'Industry is required when creating a site.'}},
'wp_api_key': {'read_only': True}, # Only set via generate-api-key endpoint
}
def __init__(self, *args, **kwargs):

View File

@@ -217,6 +217,7 @@ class IntegrationService:
dict: Connection test result with detailed health status
"""
import requests
from django.utils import timezone
config = integration.config_json
@@ -324,13 +325,6 @@ class IntegrationService:
health_checks['plugin_has_api_key']
)
# Save site_url to config if successful and not already set
if is_healthy and not config.get('site_url'):
config['site_url'] = site_url
integration.config_json = config
integration.save(update_fields=['config_json'])
logger.info(f"[IntegrationService] Saved site_url to integration {integration.id} config: {site_url}")
# Build response message
if is_healthy:
message = "✅ WordPress integration is connected and authenticated via API key"
@@ -347,6 +341,28 @@ class IntegrationService:
else:
message = "❌ WordPress connection failed"
# Update integration status based on connection test result
if is_healthy:
integration.sync_status = 'success'
integration.sync_error = None
integration.last_sync_at = timezone.now()
logger.info(f"[IntegrationService] Connection test passed, set sync_status to 'success' for integration {integration.id}")
else:
integration.sync_status = 'failed'
integration.sync_error = message
logger.warning(f"[IntegrationService] Connection test failed, set sync_status to 'failed' for integration {integration.id}")
# Save site_url to config if successful and not already set
if is_healthy and not config.get('site_url'):
config['site_url'] = site_url
integration.config_json = config
# Save all changes to integration
integration.save()
if is_healthy and not config.get('site_url'):
logger.info(f"[IntegrationService] Saved site_url to integration {integration.id} config: {site_url}")
return {
'success': is_healthy,
'fully_functional': is_healthy,

View File

@@ -127,33 +127,22 @@ class PublisherService:
# Get destination config
destination_config = {}
# If content has site, try to get integration config
# Get WordPress config directly from Site model (no SiteIntegration needed)
if hasattr(content, 'site') and content.site:
from igny8_core.business.integration.models import SiteIntegration
logger.info(f"[PublisherService._publish_to_destination] 🔍 Looking for integration: site={content.site.name}, platform={destination}")
integration = SiteIntegration.objects.filter(
site=content.site,
platform=destination,
is_active=True
).first()
site = content.site
logger.info(f"[PublisherService._publish_to_destination] 🔍 Getting config from site: {site.name}")
if integration:
logger.info(f"[PublisherService._publish_to_destination] ✅ Integration found: id={integration.id}")
# Merge config_json (site_url, etc.)
destination_config.update(integration.config_json or {})
# API key is stored in Site.wp_api_key (SINGLE source of truth)
if integration.site.wp_api_key:
destination_config['api_key'] = integration.site.wp_api_key
logger.info(f"[PublisherService._publish_to_destination] 🔑 Config merged: has_api_key={bool(destination_config.get('api_key'))}, has_site_url={bool(destination_config.get('site_url'))}")
# Ensure site_url is set (from config or from site model)
if not destination_config.get('site_url'):
destination_config['site_url'] = content.site.url
logger.info(f"[PublisherService._publish_to_destination] 🌐 Using site.url: {content.site.url}")
# API key is stored in Site.wp_api_key (SINGLE source of truth)
if site.wp_api_key:
destination_config['api_key'] = site.wp_api_key
logger.info(f"[PublisherService._publish_to_destination] 🔑 API key found on site")
else:
logger.warning(f"[PublisherService._publish_to_destination] ⚠️ No integration found for site={content.site.name}, platform={destination}")
logger.error(f"[PublisherService._publish_to_destination] No API key found on site {site.name}")
raise ValueError(f"WordPress API key not configured for site {site.name}. Please generate an API key in Site Settings.")
# Use Site.domain as site_url
destination_config['site_url'] = site.domain or site.url
logger.info(f"[PublisherService._publish_to_destination] 🌐 Using site URL: {destination_config['site_url']}")
# Publish via adapter
logger.info(f"[PublisherService._publish_to_destination] 🚀 Calling adapter.publish() with config keys: {list(destination_config.keys())}")

View File

@@ -12,6 +12,7 @@ from igny8_core.api.base import SiteSectorModelViewSet
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.auth.models import Site
from igny8_core.business.integration.models import SiteIntegration
from igny8_core.business.integration.services.integration_service import IntegrationService
from igny8_core.business.integration.services.sync_service import SyncService
@@ -131,19 +132,23 @@ class IntegrationViewSet(SiteSectorModelViewSet):
permission_classes=[AllowAny], throttle_classes=[NoThrottle])
def test_connection_collection(self, request):
"""
Collection-level test connection endpoint for frontend convenience.
Test WordPress connection using Site.wp_api_key (single source of truth).
POST /api/v1/integration/integrations/test-connection/
Body:
{
"site_id": 123,
"api_key": "...",
"site_url": "https://example.com"
"site_id": 123
}
Tests:
1. WordPress site is reachable
2. IGNY8 plugin is installed
3. Plugin has API key configured (matching Site.wp_api_key)
"""
import requests as http_requests
site_id = request.data.get('site_id')
api_key = request.data.get('api_key')
site_url = request.data.get('site_url')
if not site_id:
return error_response('site_id is required', None, status.HTTP_400_BAD_REQUEST, request)
@@ -155,80 +160,146 @@ class IntegrationViewSet(SiteSectorModelViewSet):
except (Site.DoesNotExist, ValueError, TypeError):
return error_response('Site not found or invalid', None, status.HTTP_404_NOT_FOUND, request)
# Authentication: accept either authenticated user OR matching API key in body
api_key = request.data.get('api_key') or api_key
authenticated = False
# If request has a valid user and belongs to same account, allow
if hasattr(request, 'user') and getattr(request.user, 'is_authenticated', False):
try:
# If user has account, ensure site belongs to user's account
if site.account == request.user.account:
authenticated = True
except Exception:
# Ignore and fallback to api_key check
pass
# Authentication: user must be authenticated and belong to same account
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)
# If not authenticated via session, allow if provided api_key matches site's stored wp_api_key
if not authenticated:
stored_key = getattr(site, 'wp_api_key', None)
if stored_key and api_key and str(api_key) == str(stored_key):
authenticated = True
elif not stored_key:
# API key not set on site - provide helpful error message
return error_response(
'API key not configured for this site. Please generate an API key in the IGNY8 app and ensure it is saved to the site.',
None,
status.HTTP_403_FORBIDDEN,
request
)
elif api_key and stored_key and str(api_key) != str(stored_key):
# API key provided but doesn't match
return error_response(
'Invalid API key. The provided API key does not match the one stored for this site.',
None,
status.HTTP_403_FORBIDDEN,
request
)
if not authenticated:
return error_response('Authentication credentials were not provided.', None, status.HTTP_403_FORBIDDEN, request)
# Try to find an existing integration for this site+platform
integration = SiteIntegration.objects.filter(site=site, platform='wordpress').first()
# If not found, create and save the integration to database (for status tracking, not credentials)
integration_created = False
if not integration:
integration = SiteIntegration.objects.create(
account=site.account,
site=site,
platform='wordpress',
platform_type='cms',
config_json={'site_url': site_url} if site_url else {},
credentials_json={}, # API key is stored in Site.wp_api_key, not here
is_active=True,
sync_enabled=True
# Get stored API key from Site model (single source of truth)
stored_api_key = site.wp_api_key
if not stored_api_key:
return error_response(
'API key not configured. Please generate an API key first.',
None,
status.HTTP_400_BAD_REQUEST,
request
)
integration_created = True
logger.info(f"[IntegrationViewSet] Created WordPress integration {integration.id} for site {site.id}")
service = IntegrationService()
# Mark this as initial connection test since API key was provided in request body
# This allows the test to pass even if WordPress plugin hasn't stored the key yet
is_initial_connection = bool(api_key and request.data.get('api_key'))
result = service._test_wordpress_connection(integration, is_initial_connection=is_initial_connection)
# Get site URL
site_url = site.domain or site.url
if not site_url:
return error_response(
'Site URL not configured',
None,
status.HTTP_400_BAD_REQUEST,
request
)
if result.get('success'):
# Include integration_id in response so plugin can store it
result['integration_id'] = integration.id
result['integration_created'] = integration_created
return success_response(result, request=request)
else:
# If test failed and we just created integration, delete it
if integration_created:
integration.delete()
logger.info(f"[IntegrationViewSet] Deleted integration {integration.id} due to failed connection test")
return error_response(result.get('message', 'Connection test failed'), None, status.HTTP_400_BAD_REQUEST, request)
# Health check results
health_checks = {
'site_url_configured': True,
'api_key_configured': True,
'wp_rest_api_reachable': False,
'plugin_installed': False,
'plugin_has_api_key': False,
'api_key_verified': False, # NEW: Verifies IGNY8 and WordPress have SAME key
}
issues = []
try:
# Check 1: WordPress REST API reachable
try:
rest_response = http_requests.get(
f"{site_url.rstrip('/')}/wp-json/",
timeout=10
)
if rest_response.status_code == 200:
health_checks['wp_rest_api_reachable'] = True
else:
issues.append(f"WordPress REST API not reachable: HTTP {rest_response.status_code}")
except Exception as e:
issues.append(f"WordPress REST API unreachable: {str(e)}")
# Check 2: IGNY8 Plugin installed (public status endpoint)
try:
status_response = http_requests.get(
f"{site_url.rstrip('/')}/wp-json/igny8/v1/status",
timeout=10
)
if status_response.status_code == 200:
health_checks['plugin_installed'] = True
status_data = status_response.json()
plugin_data = status_data.get('data', status_data)
if plugin_data.get('connected') or plugin_data.get('has_api_key'):
health_checks['plugin_has_api_key'] = True
else:
issues.append("Plugin installed but no API key configured in WordPress")
else:
issues.append(f"IGNY8 plugin not found: HTTP {status_response.status_code}")
except Exception as e:
issues.append(f"Cannot detect IGNY8 plugin: {str(e)}")
# Check 3: Verify API keys MATCH by making authenticated request
# This is the CRITICAL check - WordPress must accept our API key
if health_checks['plugin_installed'] and health_checks['plugin_has_api_key']:
try:
# Make authenticated request using Site.wp_api_key to dedicated verify endpoint
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['api_key_verified'] = True
elif verify_response.status_code in [401, 403]:
issues.append("API key mismatch - WordPress has different key than IGNY8. Please copy the API key from IGNY8 to WordPress plugin settings.")
else:
issues.append(f"API key verification failed: HTTP {verify_response.status_code}")
except Exception as e:
issues.append(f"API key verification request failed: {str(e)}")
# Determine overall status - MUST include api_key_verified for true connection
is_healthy = (
health_checks['api_key_configured'] and
health_checks['wp_rest_api_reachable'] and
health_checks['plugin_installed'] and
health_checks['plugin_has_api_key'] and
health_checks['api_key_verified'] # CRITICAL: keys must match
)
# Build message with clear guidance
if is_healthy:
message = "✅ WordPress integration is fully connected and verified"
elif not health_checks['wp_rest_api_reachable']:
message = "❌ Cannot reach WordPress site"
elif not health_checks['plugin_installed']:
message = "⚠️ WordPress is reachable but IGNY8 plugin not installed"
elif not health_checks['plugin_has_api_key']:
message = "⚠️ Plugin installed but no API key configured in WordPress"
elif not health_checks['api_key_verified']:
message = "⚠️ API key mismatch - copy the API key from IGNY8 to WordPress plugin"
else:
message = "❌ WordPress connection failed"
return success_response({
'success': is_healthy,
'message': message,
'site_id': site.id,
'site_name': site.name,
'site_url': site_url,
'api_key_configured': bool(stored_api_key),
'health_checks': health_checks,
'issues': issues if issues else None,
}, request=request)
except Exception as e:
logger.error(f"WordPress connection test failed: {e}")
return error_response(
f'Connection test failed: {str(e)}',
None,
status.HTTP_500_INTERNAL_SERVER_ERROR,
request
)
@extend_schema(tags=['Integration'])
@action(detail=True, methods=['post'])
@@ -808,42 +879,71 @@ class IntegrationViewSet(SiteSectorModelViewSet):
site.wp_api_key = api_key
site.save(update_fields=['wp_api_key'])
# Get or create SiteIntegration (for integration status/config, NOT credentials)
integration, created = SiteIntegration.objects.get_or_create(
site=site,
platform='wordpress',
defaults={
'account': site.account,
'platform': 'wordpress',
'platform_type': 'cms',
'is_active': True,
'sync_enabled': True,
'credentials_json': {}, # Empty - API key is on Site model
'config_json': {}
}
)
# If integration already exists, just ensure it's active
if not created:
integration.is_active = True
integration.sync_enabled = True
# Clear any old credentials_json API key (migrate to Site.wp_api_key)
if integration.credentials_json.get('api_key'):
integration.credentials_json = {}
integration.save()
logger.info(
f"Generated new API key for site {site.name} (ID: {site_id}), "
f"stored in Site.wp_api_key (single source of truth)"
)
# Serialize the integration with the new key
serializer = self.get_serializer(integration)
return success_response({
'api_key': api_key,
'site_id': site.id,
'site_name': site.name,
'site_url': site.domain or site.url,
'message': 'API key generated successfully. WordPress integration is ready.',
}, request=request)
@action(detail=False, methods=['post'], url_path='revoke-api-key')
def revoke_api_key(self, request):
"""
Revoke (delete) the API key for a site's WordPress integration.
POST /api/v1/integration/integrations/revoke-api-key/
Body:
{
"site_id": 5
}
"""
site_id = request.data.get('site_id')
if not site_id:
return error_response(
'Site ID is required',
None,
status.HTTP_400_BAD_REQUEST,
request
)
try:
site = Site.objects.get(id=site_id)
except Site.DoesNotExist:
return error_response(
f'Site with ID {site_id} not found',
None,
status.HTTP_404_NOT_FOUND,
request
)
# Verify user has access to this site
if site.account != request.user.account:
return error_response(
'You do not have permission to modify this site',
None,
status.HTTP_403_FORBIDDEN,
request
)
# SINGLE SOURCE OF TRUTH: Remove API key from Site.wp_api_key
site.wp_api_key = None
site.save(update_fields=['wp_api_key'])
logger.info(
f"Revoked API key for site {site.name} (ID: {site_id})"
)
return success_response({
'integration': serializer.data,
'api_key': api_key,
'message': f"API key {'generated' if created else 'regenerated'} successfully",
'site_id': site.id,
'site_name': site.name,
'message': 'API key revoked successfully. WordPress integration is now disconnected.',
}, request=request)

View File

@@ -429,11 +429,11 @@ class ContentResource(resources.ModelResource):
@admin.register(Content)
class ContentAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
resource_class = ContentResource
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'word_count', 'get_taxonomy_count', 'created_at']
list_filter = ['status', 'content_type', 'content_structure', 'source', 'site', 'sector', 'cluster', 'word_count', 'created_at']
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'site_status', 'scheduled_publish_at', 'word_count', 'get_taxonomy_count', 'created_at']
list_filter = ['status', 'site_status', 'content_type', 'content_structure', 'source', 'site', 'sector', 'cluster', 'word_count', 'created_at']
search_fields = ['title', 'content_html', 'external_url', 'meta_title', 'primary_keyword']
ordering = ['-created_at']
readonly_fields = ['created_at', 'updated_at', 'word_count', 'get_tags_display', 'get_categories_display']
readonly_fields = ['created_at', 'updated_at', 'word_count', 'site_status_updated_at', 'get_tags_display', 'get_categories_display']
autocomplete_fields = ['cluster', 'site', 'sector']
inlines = [ContentTaxonomyInline]
actions = [
@@ -449,6 +449,10 @@ class ContentAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
('Basic Info', {
'fields': ('title', 'site', 'sector', 'cluster', 'status')
}),
('Publishing Status', {
'fields': ('site_status', 'scheduled_publish_at', 'site_status_updated_at'),
'description': 'WordPress/external site publishing status. Managed by automated publishing scheduler.'
}),
('Content Classification', {
'fields': ('content_type', 'content_structure', 'source')
}),

View File

@@ -0,0 +1,97 @@
# Generated manually on 2026-01-12
from django.db import migrations
def add_wordpress_plugin(apps, schema_editor):
"""
Add the IGNY8 WordPress Bridge plugin to the database.
"""
Plugin = apps.get_model('plugins', 'Plugin')
PluginVersion = apps.get_model('plugins', 'PluginVersion')
# Create or update the WordPress plugin
plugin, created = Plugin.objects.get_or_create(
slug='igny8-wp-bridge',
defaults={
'name': 'IGNY8 WordPress Bridge',
'platform': 'wordpress',
'description': 'Connect your WordPress site to IGNY8 for AI-powered content publishing, SEO optimization, and seamless automation. Features API key authentication, automated updates, advanced template rendering, and webhook sync.',
'homepage_url': 'https://igny8.com/docs/wordpress-integration',
'is_active': True,
}
)
if not created:
# Update existing plugin with latest information
plugin.name = 'IGNY8 WordPress Bridge'
plugin.description = 'Connect your WordPress site to IGNY8 for AI-powered content publishing, SEO optimization, and seamless automation. Features API key authentication, automated updates, advanced template rendering, and webhook sync.'
plugin.homepage_url = 'https://igny8.com/docs/wordpress-integration'
plugin.is_active = True
plugin.save()
# Add current version (1.3.4) if it doesn't exist
version, created = PluginVersion.objects.get_or_create(
plugin=plugin,
version='1.3.4',
defaults={
'version_code': 10304, # 1.03.04
'status': 'released',
'changelog': '''## Version 1.3.4 (January 12, 2026)
### Major Changes
- **API Key Authentication Only**: Removed username/password authentication
- **Simplified Integration**: Single API key for all communication
- **Bearer Token Auth**: Uses `Authorization: Bearer {api_key}` header
- **Webhooks Deprecated**: Removed webhook signature validation
### Authentication
- API key stored in WordPress options table: `igny8_api_key`
- Accepts both `X-IGNY8-API-KEY` header and `Authorization: Bearer` header
- Single source of truth: Site.wp_api_key in IGNY8 backend
### Technical Improvements
- Streamlined connection test endpoint
- Improved error handling and validation
- Better health check reporting
- Enhanced auto-update mechanism
### Backward Compatibility
- Legacy username/password fields removed
- No migration needed for existing installations
- API key authentication works with all IGNY8 API versions 1.0+
''',
'min_api_version': '1.0',
'min_platform_version': '5.6',
'min_php_version': '7.4',
'file_path': 'plugins/wordpress/dist/igny8-wp-bridge-1.3.4.zip',
'file_size': 0, # Will be updated when file is generated
'checksum': '', # Will be updated when file is generated
'force_update': False,
}
)
if created:
print(f"✅ Created WordPress plugin version 1.3.4")
else:
print(f" WordPress plugin version 1.3.4 already exists")
def remove_wordpress_plugin(apps, schema_editor):
"""
Reverse migration - remove the WordPress plugin.
This is optional and can be commented out if you want to keep the data.
"""
Plugin = apps.get_model('plugins', 'Plugin')
Plugin.objects.filter(slug='igny8-wp-bridge').delete()
class Migration(migrations.Migration):
dependencies = [
('plugins', '0003_simplify_status_choices'),
]
operations = [
migrations.RunPython(add_wordpress_plugin, remove_wordpress_plugin),
]

View File

@@ -255,7 +255,7 @@ def process_scheduled_publications() -> Dict[str, Any]:
due_content = Content.objects.filter(
site_status='scheduled',
scheduled_publish_at__lte=now
).select_related('site', 'task')
).select_related('site', 'sector', 'cluster')
for content in due_content:
results['processed'] += 1
@@ -284,11 +284,9 @@ def process_scheduled_publications() -> Dict[str, Any]:
continue
# Queue the WordPress publishing task
task_id = content.task_id if hasattr(content, 'task') and content.task else None
publish_content_to_wordpress.delay(
content_id=content.id,
site_integration_id=site_integration.id,
task_id=task_id
site_integration_id=site_integration.id
)
logger.info(f"Queued content {content.id} for WordPress publishing")