Refactor WordPress integration service to use API key for connection testing

- Updated the `IntegrationService` to perform connection tests using only the API key, removing reliance on username and app password.
- Simplified health check logic and improved error messaging for better clarity.
- Added functionality to revoke API keys in the `WordPressIntegrationForm` component.
- Enhanced site settings page with a site selector and improved integration status display.
- Cleaned up unused code and improved overall structure for better maintainability.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-22 09:31:07 +00:00
parent 1a1214d93f
commit 029c66a0f1
12 changed files with 1337 additions and 456 deletions

View File

@@ -16,7 +16,8 @@ import {
AlertIcon,
DownloadIcon,
PlusIcon,
CopyIcon
CopyIcon,
TrashBinIcon
} from '../../icons';
import { Globe, Key, RefreshCw } from 'lucide-react';
@@ -40,15 +41,6 @@ export default function WordPressIntegrationForm({
const [generatingKey, setGeneratingKey] = useState(false);
const [apiKey, setApiKey] = useState<string>('');
const [apiKeyVisible, setApiKeyVisible] = useState(false);
const [isActive, setIsActive] = useState(integration?.is_active ?? true);
const [syncEnabled, setSyncEnabled] = useState(integration?.sync_enabled ?? true);
useEffect(() => {
if (integration) {
setIsActive(integration.is_active ?? true);
setSyncEnabled(integration.sync_enabled ?? true);
}
}, [integration]);
// Load API key from site settings on mount
useEffect(() => {
@@ -121,6 +113,40 @@ export default function WordPressIntegrationForm({
}
};
const handleRevokeApiKey = async () => {
if (!confirm('Are you sure you want to revoke the API key? Your WordPress plugin will stop working until you generate a new key.')) {
return;
}
try {
setGeneratingKey(true);
// Clear API key from site settings by setting it to empty string
await fetchAPI(`/v1/auth/sites/${siteId}/`, {
method: 'PATCH',
body: JSON.stringify({ wp_api_key: '' }),
});
setApiKey('');
setApiKeyVisible(false);
// Trigger integration update to reload the integration state
if (onIntegrationUpdate && integration) {
await loadApiKeyFromSite();
// Reload integration to reflect changes
const integrations = await integrationApi.getSiteIntegrations(siteId);
const wp = integrations.find(i => i.platform === 'wordpress');
if (wp) {
onIntegrationUpdate(wp);
}
}
toast.success('API key revoked successfully');
} catch (error: any) {
toast.error(`Failed to revoke API key: ${error.message}`);
} finally {
setGeneratingKey(false);
}
};
const saveApiKeyToSite = async (key: string) => {
try {
await fetchAPI(`/v1/auth/sites/${siteId}/`, {
@@ -146,108 +172,103 @@ export default function WordPressIntegrationForm({
toast.success('Plugin download started');
};
const handleSaveSettings = async () => {
try {
setLoading(true);
// Update integration with active/sync settings
if (integration) {
await integrationApi.updateIntegration(integration.id, {
is_active: isActive,
sync_enabled: syncEnabled,
} as any);
} else {
// Create integration if it doesn't exist
await integrationApi.saveWordPressIntegration(siteId, {
url: siteUrl || '',
username: '',
app_password: '',
api_key: apiKey,
is_active: isActive,
sync_enabled: syncEnabled,
});
}
// Reload integration
const updated = await integrationApi.getWordPressIntegration(siteId);
if (onIntegrationUpdate && updated) {
onIntegrationUpdate(updated);
}
toast.success('Integration settings saved successfully');
} catch (error: any) {
toast.error(`Failed to save settings: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleTestConnection = async () => {
if (!apiKey || !siteUrl) {
toast.error('Please ensure API key and site URL are configured');
return;
}
try {
setLoading(true);
// Test connection using API key and site URL
const result = await fetchAPI(`/v1/integration/integrations/test-connection/`, {
method: 'POST',
body: JSON.stringify({
site_id: siteId,
api_key: apiKey,
site_url: siteUrl,
}),
});
// Check for fully functional connection (includes bidirectional communication)
if (result?.fully_functional) {
toast.success('✅ Connection is fully functional! Plugin is connected and can communicate with IGNY8.');
} else if (result?.success) {
// Basic connection works but not fully functional
const issues = result?.issues || [];
const healthChecks = result?.health_checks || {};
// Show specific warning based on what's missing
if (!healthChecks.plugin_connected) {
toast.warning('⚠️ WordPress is reachable but the plugin is not configured with an API key. Please add the API key in your WordPress plugin settings.');
} else if (!healthChecks.bidirectional_communication) {
toast.warning('⚠️ Plugin is configured but cannot reach IGNY8 backend. Please check your WordPress site\'s outbound connections and firewall settings.');
} else {
toast.warning(`⚠️ ${result?.message || 'Connection partially working. Some features may not function correctly.'}`);
}
} else {
// Connection completely failed
toast.error(`❌ Connection test failed: ${result?.message || 'Unknown error'}`);
}
} catch (error: any) {
toast.error(`Connection test failed: ${error.message}`);
} finally {
setLoading(false);
}
};
const maskApiKey = (key: string) => {
if (!key) return '';
if (key.length <= 12) return key;
return key.substring(0, 8) + '**********' + key.substring(key.length - 4);
};
// Toggle integration enabled status
const [integrationEnabled, setIntegrationEnabled] = useState(integration?.is_active ?? true);
const handleToggleIntegration = async (enabled: boolean) => {
try {
setIntegrationEnabled(enabled);
if (integration) {
// Update existing integration
await integrationApi.updateIntegration(integration.id, {
is_active: enabled,
} as any);
toast.success(enabled ? 'Integration enabled' : 'Integration disabled');
// Reload integration
const updated = await integrationApi.getWordPressIntegration(siteId);
if (onIntegrationUpdate && updated) {
onIntegrationUpdate(updated);
}
} else if (enabled && apiKey) {
// Create integration when enabling for first time
await integrationApi.saveWordPressIntegration(siteId, {
url: siteUrl || '',
username: '',
app_password: '',
api_key: apiKey,
is_active: enabled,
sync_enabled: true,
});
toast.success('Integration created and enabled');
// Reload integration
const updated = await integrationApi.getWordPressIntegration(siteId);
if (onIntegrationUpdate && updated) {
onIntegrationUpdate(updated);
}
}
} catch (error: any) {
toast.error(`Failed to update integration: ${error.message}`);
// Revert on error
setIntegrationEnabled(!enabled);
}
};
useEffect(() => {
if (integration) {
setIntegrationEnabled(integration.is_active ?? true);
}
}, [integration]);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<div className="p-3 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg">
<Globe className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
WordPress Integration
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Connect your WordPress site using the IGNY8 WP Bridge plugin
</p>
{/* Header with Toggle */}
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="p-3 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg">
<Globe className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
WordPress Integration
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Connect your WordPress site using the IGNY8 WP Bridge plugin
</p>
</div>
</div>
{/* Toggle Switch */}
{apiKey && (
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{integrationEnabled ? 'Enabled' : 'Disabled'}
</span>
<button
type="button"
onClick={() => handleToggleIntegration(!integrationEnabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 ${
integrationEnabled ? 'bg-brand-600' : 'bg-gray-300 dark:bg-gray-600'
}`}
role="switch"
aria-checked={integrationEnabled}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
integrationEnabled ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
)}
</div>
{/* API Keys Table */}
@@ -366,11 +387,19 @@ export default function WordPressIntegrationForm({
<button
onClick={handleRegenerateApiKey}
disabled={generatingKey}
className="text-gray-500 hover:text-error-500 dark:text-gray-400 dark:hover:text-error-500 disabled:opacity-50"
title="Regenerate"
className="text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-400 disabled:opacity-50 transition-colors"
title="Regenerate API key"
>
<RefreshCw className={`w-5 h-5 ${generatingKey ? 'animate-spin' : ''}`} />
</button>
<button
onClick={handleRevokeApiKey}
disabled={generatingKey}
className="text-gray-500 hover:text-error-500 dark:text-gray-400 dark:hover:text-error-400 disabled:opacity-50 transition-colors"
title="Revoke API key"
>
<TrashBinIcon className="w-5 h-5" />
</button>
</div>
</td>
</tr>
@@ -407,104 +436,6 @@ export default function WordPressIntegrationForm({
</div>
</Card>
)}
{/* Integration Settings */}
<Card className="p-6">
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Integration Settings
</h3>
</div>
{/* Checkboxes at the top */}
<div className="space-y-4 pb-4 border-b border-gray-200 dark:border-gray-700">
<Checkbox
id="is_active"
checked={isActive}
onChange={(checked) => setIsActive(checked)}
label="Enable Integration"
/>
<Checkbox
id="sync_enabled"
checked={syncEnabled}
onChange={(checked) => setSyncEnabled(checked)}
label="Enable Two-Way Sync"
/>
</div>
<div className="flex items-center justify-end gap-3 pt-4">
{apiKey && siteUrl && (
<Button
type="button"
variant="outline"
onClick={handleTestConnection}
disabled={loading}
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Test Connection
</Button>
)}
<Button
type="button"
variant="solid"
onClick={handleSaveSettings}
disabled={loading || !apiKey}
>
{loading ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
</Card>
{/* Integration Status */}
{integration && (
<Card className="p-6">
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Integration Status
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Status</p>
<div className="flex items-center gap-2">
{integration.is_active ? (
<CheckCircleIcon className="w-4 h-4 text-green-500" />
) : (
<AlertIcon className="w-4 h-4 text-gray-400" />
)}
<span className="text-sm font-medium text-gray-900 dark:text-white">
{integration.is_active ? 'Active' : 'Inactive'}
</span>
</div>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Sync Status</p>
<div className="flex items-center gap-2">
{integration.sync_status === 'success' ? (
<CheckCircleIcon className="w-4 h-4 text-green-500" />
) : integration.sync_status === 'failed' ? (
<AlertIcon className="w-4 h-4 text-red-500" />
) : (
<RefreshCw className="w-4 h-4 text-yellow-500 animate-spin" />
)}
<span className="text-sm font-medium text-gray-900 dark:text-white capitalize">
{integration.sync_status || 'Pending'}
</span>
</div>
</div>
</div>
{integration.last_sync_at && (
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Last Sync</p>
<p className="text-sm text-gray-900 dark:text-white">
{new Date(integration.last_sync_at).toLocaleString()}
</p>
</div>
)}
</div>
</Card>
)}
</div>
);
}