docs: add plugin distribution system plan for WP/Shopify/Custom integrations
Comprehensive plan covering:
- Directory structure: /plugins/{platform}/source/ and /dist/
- Database models: Plugin, PluginVersion, PluginInstallation
- API endpoints for download, check-update, register, health-check
- WordPress auto-update mechanism via pre_set_site_transient_update_plugins hook
- Build scripts for ZIP generation with versioning
- Security: signed URLs, checksums, rate limiting
- Future-ready for Shopify and custom site integrations
- Monitoring and analytics dashboard widgets
- 5-phase implementation roadmap
This commit is contained in:
692
docs/plans/PLUGIN-DISTRIBUTION-SYSTEM.md
Normal file
692
docs/plans/PLUGIN-DISTRIBUTION-SYSTEM.md
Normal file
@@ -0,0 +1,692 @@
|
|||||||
|
# IGNY8 Plugin Distribution System
|
||||||
|
|
||||||
|
**Created:** January 9, 2026
|
||||||
|
**Version:** 1.0
|
||||||
|
**Status:** Planning
|
||||||
|
**Scope:** WordPress, Shopify, Custom Site Integration Plugins
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the architecture for distributing and auto-updating IGNY8 integration plugins across multiple platforms (WordPress, Shopify, Custom Sites).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Directory Structure
|
||||||
|
|
||||||
|
### 1.1 - Server Location
|
||||||
|
|
||||||
|
```
|
||||||
|
/data/app/igny8/
|
||||||
|
├── backend/
|
||||||
|
│ └── igny8_core/
|
||||||
|
│ └── plugins/ # NEW: Plugin management
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── models.py # Plugin version tracking
|
||||||
|
│ ├── views.py # Download & update API
|
||||||
|
│ ├── serializers.py
|
||||||
|
│ ├── urls.py
|
||||||
|
│ └── utils.py # ZIP compression, versioning
|
||||||
|
├── plugins/ # NEW: Plugin source & distributions
|
||||||
|
│ ├── wordpress/
|
||||||
|
│ │ ├── source/ # Development source files
|
||||||
|
│ │ │ └── igny8-wp-bridge/
|
||||||
|
│ │ │ ├── igny8-wp-bridge.php
|
||||||
|
│ │ │ ├── includes/
|
||||||
|
│ │ │ ├── assets/
|
||||||
|
│ │ │ └── readme.txt
|
||||||
|
│ │ └── dist/ # Compiled/zipped releases
|
||||||
|
│ │ ├── igny8-wp-bridge-v1.0.0.zip
|
||||||
|
│ │ ├── igny8-wp-bridge-v1.0.1.zip
|
||||||
|
│ │ └── igny8-wp-bridge-latest.zip # Symlink to latest
|
||||||
|
│ ├── shopify/ # FUTURE
|
||||||
|
│ │ ├── source/
|
||||||
|
│ │ └── dist/
|
||||||
|
│ └── custom-site/ # FUTURE
|
||||||
|
│ ├── source/
|
||||||
|
│ └── dist/
|
||||||
|
└── docs/
|
||||||
|
└── plans/
|
||||||
|
└── PLUGIN-DISTRIBUTION-SYSTEM.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 - Why This Structure?
|
||||||
|
|
||||||
|
| Location | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `/plugins/` | Separate from app code, easy to manage |
|
||||||
|
| `/plugins/{platform}/source/` | Development files, version controlled |
|
||||||
|
| `/plugins/{platform}/dist/` | Ready-to-download ZIP files |
|
||||||
|
| `/backend/igny8_core/plugins/` | Django app for API & version management |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Database Models
|
||||||
|
|
||||||
|
### 2.1 - Plugin Model
|
||||||
|
|
||||||
|
```python
|
||||||
|
# backend/igny8_core/plugins/models.py
|
||||||
|
|
||||||
|
class Plugin(models.Model):
|
||||||
|
"""Represents a plugin type (WordPress, Shopify, etc.)"""
|
||||||
|
|
||||||
|
PLATFORM_CHOICES = [
|
||||||
|
('wordpress', 'WordPress'),
|
||||||
|
('shopify', 'Shopify'),
|
||||||
|
('custom', 'Custom Site'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = models.CharField(max_length=100) # "IGNY8 WP Bridge"
|
||||||
|
slug = models.SlugField(unique=True) # "igny8-wp-bridge"
|
||||||
|
platform = models.CharField(max_length=20, choices=PLATFORM_CHOICES)
|
||||||
|
description = models.TextField()
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'plugins'
|
||||||
|
|
||||||
|
|
||||||
|
class PluginVersion(models.Model):
|
||||||
|
"""Tracks each version of a plugin"""
|
||||||
|
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('draft', 'Draft'), # In development
|
||||||
|
('testing', 'Testing'), # Internal testing
|
||||||
|
('staged', 'Staged'), # Ready, not yet pushed
|
||||||
|
('released', 'Released'), # Available for download
|
||||||
|
('update_ready', 'Update Ready'), # Push to installed sites
|
||||||
|
('deprecated', 'Deprecated'), # Old version
|
||||||
|
]
|
||||||
|
|
||||||
|
plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE, related_name='versions')
|
||||||
|
version = models.CharField(max_length=20) # "1.0.0", "1.0.1"
|
||||||
|
version_code = models.IntegerField() # 100, 101 (for comparison)
|
||||||
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
|
||||||
|
|
||||||
|
# File info
|
||||||
|
file_path = models.CharField(max_length=500) # Relative path in dist/
|
||||||
|
file_size = models.IntegerField(default=0) # Bytes
|
||||||
|
checksum = models.CharField(max_length=64) # SHA256 for integrity
|
||||||
|
|
||||||
|
# Release info
|
||||||
|
changelog = models.TextField(blank=True)
|
||||||
|
min_api_version = models.CharField(max_length=20, default='1.0')
|
||||||
|
|
||||||
|
# Timestamps
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
released_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
|
# Auto-update control
|
||||||
|
force_update = models.BooleanField(default=False) # Critical security fix
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'plugin_versions'
|
||||||
|
unique_together = ['plugin', 'version']
|
||||||
|
ordering = ['-version_code']
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInstallation(models.Model):
|
||||||
|
"""Tracks where plugins are installed (per site)"""
|
||||||
|
|
||||||
|
site = models.ForeignKey('Site', on_delete=models.CASCADE, related_name='plugin_installations')
|
||||||
|
plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE)
|
||||||
|
current_version = models.ForeignKey(PluginVersion, on_delete=models.SET_NULL, null=True)
|
||||||
|
|
||||||
|
# Installation status
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
last_health_check = models.DateTimeField(null=True)
|
||||||
|
|
||||||
|
# Update tracking
|
||||||
|
pending_update = models.ForeignKey(
|
||||||
|
PluginVersion,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
related_name='pending_installations'
|
||||||
|
)
|
||||||
|
update_notified_at = models.DateTimeField(null=True)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'plugin_installations'
|
||||||
|
unique_together = ['site', 'plugin']
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API Endpoints
|
||||||
|
|
||||||
|
### 3.1 - Public Endpoints (No Auth Required)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Download latest plugin (for initial installation)
|
||||||
|
GET /api/plugins/{slug}/download/
|
||||||
|
# Returns: ZIP file download
|
||||||
|
|
||||||
|
# Check for updates (called by installed plugins)
|
||||||
|
GET /api/plugins/{slug}/check-update/?current_version=1.0.0
|
||||||
|
# Returns: { "update_available": true, "latest_version": "1.0.1", "download_url": "...", "changelog": "..." }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 - Authenticated Endpoints
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Get download URL for user (tracks downloads)
|
||||||
|
GET /api/plugins/{slug}/download-url/
|
||||||
|
# Returns: { "url": "...", "expires_at": "..." }
|
||||||
|
|
||||||
|
# Report installation (called after WP plugin is activated)
|
||||||
|
POST /api/plugins/{slug}/register-installation/
|
||||||
|
# Body: { "site_id": 123, "version": "1.0.0" }
|
||||||
|
|
||||||
|
# Update installation status (health check)
|
||||||
|
POST /api/plugins/{slug}/health-check/
|
||||||
|
# Body: { "site_id": 123, "version": "1.0.0", "status": "active" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 - Admin Endpoints
|
||||||
|
|
||||||
|
```python
|
||||||
|
# List all plugin versions
|
||||||
|
GET /api/admin/plugins/
|
||||||
|
|
||||||
|
# Create new version
|
||||||
|
POST /api/admin/plugins/{slug}/versions/
|
||||||
|
# Body: { "version": "1.0.1", "changelog": "...", "status": "draft" }
|
||||||
|
|
||||||
|
# Upload plugin ZIP
|
||||||
|
POST /api/admin/plugins/{slug}/versions/{version}/upload/
|
||||||
|
# Body: multipart/form-data with ZIP file
|
||||||
|
|
||||||
|
# Release version (make available for download)
|
||||||
|
POST /api/admin/plugins/{slug}/versions/{version}/release/
|
||||||
|
|
||||||
|
# Push update to installed sites
|
||||||
|
POST /api/admin/plugins/{slug}/versions/{version}/push-update/
|
||||||
|
# This sets status to 'update_ready' and notifies all installations
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. WordPress Plugin Update Mechanism
|
||||||
|
|
||||||
|
### 4.1 - How WordPress Auto-Updates Work
|
||||||
|
|
||||||
|
WordPress checks for plugin updates by calling a filter. Our plugin hooks into this:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In igny8-wp-bridge.php
|
||||||
|
|
||||||
|
class IGNY8_WP_Bridge {
|
||||||
|
|
||||||
|
private $plugin_slug = 'igny8-wp-bridge';
|
||||||
|
private $version = '1.0.0';
|
||||||
|
private $api_url = 'https://app.igny8.com/api/plugins/';
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
// Check for updates
|
||||||
|
add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_updates']);
|
||||||
|
add_filter('plugins_api', [$this, 'plugin_info'], 10, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check_for_updates($transient) {
|
||||||
|
if (empty($transient->checked)) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call IGNY8 API to check for updates
|
||||||
|
$response = wp_remote_get($this->api_url . $this->plugin_slug . '/check-update/', [
|
||||||
|
'timeout' => 10,
|
||||||
|
'headers' => [
|
||||||
|
'X-IGNY8-Site-ID' => get_option('igny8_site_id'),
|
||||||
|
'X-IGNY8-API-Key' => get_option('igny8_api_key'),
|
||||||
|
],
|
||||||
|
'body' => [
|
||||||
|
'current_version' => $this->version,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(wp_remote_retrieve_body($response), true);
|
||||||
|
|
||||||
|
if (!empty($data['update_available'])) {
|
||||||
|
$plugin_file = $this->plugin_slug . '/' . $this->plugin_slug . '.php';
|
||||||
|
|
||||||
|
$transient->response[$plugin_file] = (object) [
|
||||||
|
'slug' => $this->plugin_slug,
|
||||||
|
'new_version' => $data['latest_version'],
|
||||||
|
'package' => $data['download_url'],
|
||||||
|
'url' => $data['info_url'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function plugin_info($result, $action, $args) {
|
||||||
|
if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch plugin info from IGNY8 API
|
||||||
|
$response = wp_remote_get($this->api_url . $this->plugin_slug . '/info/');
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode(wp_remote_retrieve_body($response), true);
|
||||||
|
|
||||||
|
return (object) [
|
||||||
|
'name' => $data['name'],
|
||||||
|
'slug' => $this->plugin_slug,
|
||||||
|
'version' => $data['version'],
|
||||||
|
'author' => 'IGNY8',
|
||||||
|
'homepage' => 'https://igny8.com',
|
||||||
|
'sections' => [
|
||||||
|
'description' => $data['description'],
|
||||||
|
'changelog' => $data['changelog'],
|
||||||
|
],
|
||||||
|
'download_link' => $data['download_url'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 - Update Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Plugin Update Flow │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. Admin marks version as "Update Ready" in IGNY8 Dashboard │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 2. IGNY8 API returns update_available=true for check-update calls │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 3. WordPress cron checks for updates (or user clicks "Check Now") │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 4. WP Plugin calls IGNY8 API → Gets new version info │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 5. WordPress shows "Update Available" in Plugins page │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 6. User clicks Update (or auto-update if enabled) │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 7. WordPress downloads ZIP from IGNY8 and installs │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ 8. Plugin activation hook reports new version to IGNY8 │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Initial Download (ZIP Generation)
|
||||||
|
|
||||||
|
### 5.1 - Build Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# scripts/build-wp-plugin.sh
|
||||||
|
|
||||||
|
VERSION=$1
|
||||||
|
PLUGIN_NAME="igny8-wp-bridge"
|
||||||
|
SOURCE_DIR="/data/app/igny8/plugins/wordpress/source/${PLUGIN_NAME}"
|
||||||
|
DIST_DIR="/data/app/igny8/plugins/wordpress/dist"
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "Usage: ./build-wp-plugin.sh <version>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create dist directory if not exists
|
||||||
|
mkdir -p "$DIST_DIR"
|
||||||
|
|
||||||
|
# Create temp directory for packaging
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
PACKAGE_DIR="${TEMP_DIR}/${PLUGIN_NAME}"
|
||||||
|
|
||||||
|
# Copy source files
|
||||||
|
cp -r "$SOURCE_DIR" "$PACKAGE_DIR"
|
||||||
|
|
||||||
|
# Update version in main plugin file
|
||||||
|
sed -i "s/Version: .*/Version: ${VERSION}/" "${PACKAGE_DIR}/${PLUGIN_NAME}.php"
|
||||||
|
sed -i "s/\$version = '.*'/\$version = '${VERSION}'/" "${PACKAGE_DIR}/${PLUGIN_NAME}.php"
|
||||||
|
|
||||||
|
# Create ZIP
|
||||||
|
cd "$TEMP_DIR"
|
||||||
|
zip -r "${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.zip" "$PLUGIN_NAME"
|
||||||
|
|
||||||
|
# Update latest symlink
|
||||||
|
ln -sf "${PLUGIN_NAME}-v${VERSION}.zip" "${DIST_DIR}/${PLUGIN_NAME}-latest.zip"
|
||||||
|
|
||||||
|
# Calculate checksum
|
||||||
|
sha256sum "${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.zip" > "${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.sha256"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
|
||||||
|
echo "Built: ${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.zip"
|
||||||
|
echo "Checksum: $(cat ${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.sha256)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 - Django Management Command
|
||||||
|
|
||||||
|
```python
|
||||||
|
# backend/igny8_core/plugins/management/commands/build_plugin.py
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
import subprocess
|
||||||
|
import hashlib
|
||||||
|
from plugins.models import Plugin, PluginVersion
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Build and register a new plugin version'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--plugin', type=str, required=True)
|
||||||
|
parser.add_argument('--version', type=str, required=True)
|
||||||
|
parser.add_argument('--changelog', type=str, default='')
|
||||||
|
parser.add_argument('--release', action='store_true')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
plugin = Plugin.objects.get(slug=options['plugin'])
|
||||||
|
version = options['version']
|
||||||
|
|
||||||
|
# Run build script
|
||||||
|
result = subprocess.run(
|
||||||
|
['./scripts/build-wp-plugin.sh', version],
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
self.stderr.write(f"Build failed: {result.stderr}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calculate version code (1.0.1 -> 101)
|
||||||
|
parts = version.split('.')
|
||||||
|
version_code = int(parts[0]) * 10000 + int(parts[1]) * 100 + int(parts[2])
|
||||||
|
|
||||||
|
# Get file info
|
||||||
|
file_path = f"{plugin.slug}-v{version}.zip"
|
||||||
|
full_path = f"/data/app/igny8/plugins/wordpress/dist/{file_path}"
|
||||||
|
|
||||||
|
with open(full_path, 'rb') as f:
|
||||||
|
checksum = hashlib.sha256(f.read()).hexdigest()
|
||||||
|
|
||||||
|
import os
|
||||||
|
file_size = os.path.getsize(full_path)
|
||||||
|
|
||||||
|
# Create version record
|
||||||
|
plugin_version, created = PluginVersion.objects.update_or_create(
|
||||||
|
plugin=plugin,
|
||||||
|
version=version,
|
||||||
|
defaults={
|
||||||
|
'version_code': version_code,
|
||||||
|
'file_path': file_path,
|
||||||
|
'file_size': file_size,
|
||||||
|
'checksum': checksum,
|
||||||
|
'changelog': options['changelog'],
|
||||||
|
'status': 'released' if options['release'] else 'draft',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
action = 'Created' if created else 'Updated'
|
||||||
|
self.stdout.write(f"{action} version {version} for {plugin.name}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Frontend Integration
|
||||||
|
|
||||||
|
### 6.1 - Download Button Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// frontend/src/components/sites/PluginDownloadSection.tsx
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { api } from '@/services/api';
|
||||||
|
|
||||||
|
interface PluginInfo {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
version: string;
|
||||||
|
download_url: string;
|
||||||
|
file_size: number;
|
||||||
|
platform: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PluginDownloadSection({ platform = 'wordpress' }: { platform: string }) {
|
||||||
|
const { data: plugin, isLoading } = useQuery<PluginInfo>({
|
||||||
|
queryKey: ['plugin', platform],
|
||||||
|
queryFn: () => api.get(`/plugins/${platform}/latest/`).then(r => r.data),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (!plugin) return;
|
||||||
|
|
||||||
|
// Track download
|
||||||
|
await api.post(`/plugins/${plugin.slug}/track-download/`);
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
window.location.href = plugin.download_url;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (!plugin) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="p-3 bg-green-50 rounded-lg">
|
||||||
|
<DownloadIcon className="w-6 h-6 text-green-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-gray-900">{plugin.name}</h3>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
Download and install the plugin on your {platform === 'wordpress' ? 'WordPress' : platform} site
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 mt-2 text-xs text-gray-400">
|
||||||
|
<span>Version {plugin.version}</span>
|
||||||
|
<span>{formatFileSize(plugin.file_size)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleDownload} variant="outline">
|
||||||
|
<DownloadIcon className="w-4 h-4 mr-2" />
|
||||||
|
Download Plugin
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 - Admin Plugin Management Page (Future)
|
||||||
|
|
||||||
|
```
|
||||||
|
/admin/plugins # List all plugins
|
||||||
|
/admin/plugins/{slug} # Plugin details + versions
|
||||||
|
/admin/plugins/{slug}/versions # Version history
|
||||||
|
/admin/plugins/{slug}/upload # Upload new version
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Security Considerations
|
||||||
|
|
||||||
|
### 7.1 - Download Security
|
||||||
|
|
||||||
|
| Measure | Implementation |
|
||||||
|
|---------|----------------|
|
||||||
|
| Signed URLs | Time-limited download URLs (15 min expiry) |
|
||||||
|
| Checksum | SHA256 verification in WP plugin |
|
||||||
|
| API Key Required | Downloads require valid site API key |
|
||||||
|
| Rate Limiting | Max 10 downloads/hour per site |
|
||||||
|
|
||||||
|
### 7.2 - Update Security
|
||||||
|
|
||||||
|
| Measure | Implementation |
|
||||||
|
|---------|----------------|
|
||||||
|
| HTTPS Only | All API calls over TLS |
|
||||||
|
| Version Signing | Checksum comparison before install |
|
||||||
|
| Rollback | Keep last 3 versions for emergency rollback |
|
||||||
|
| Staged Rollout | Option to release to % of installs first |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Basic Infrastructure (Week 1)
|
||||||
|
- [ ] Create `/plugins/` directory structure
|
||||||
|
- [ ] Create Django `plugins` app with models
|
||||||
|
- [ ] Migrate existing WP plugin to `/plugins/wordpress/source/`
|
||||||
|
- [ ] Create build script for ZIP generation
|
||||||
|
- [ ] Implement download API endpoint
|
||||||
|
|
||||||
|
### Phase 2: Frontend Integration (Week 1)
|
||||||
|
- [ ] Update Site Settings > Integrations download button
|
||||||
|
- [ ] Create plugin info API endpoint
|
||||||
|
- [ ] Add version display to download section
|
||||||
|
|
||||||
|
### Phase 3: Update System (Week 2)
|
||||||
|
- [ ] Implement check-update API
|
||||||
|
- [ ] Add update hooks to WP plugin
|
||||||
|
- [ ] Create PluginInstallation tracking
|
||||||
|
- [ ] Build admin version management UI
|
||||||
|
|
||||||
|
### Phase 4: Advanced Features (Week 3)
|
||||||
|
- [ ] Signed download URLs
|
||||||
|
- [ ] Version rollback support
|
||||||
|
- [ ] Update notifications in IGNY8 dashboard
|
||||||
|
- [ ] Staged rollout percentage
|
||||||
|
|
||||||
|
### Phase 5: Additional Platforms (Future)
|
||||||
|
- [ ] Shopify plugin structure
|
||||||
|
- [ ] Custom site SDK/package
|
||||||
|
- [ ] Platform-specific update mechanisms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. File Serving Configuration
|
||||||
|
|
||||||
|
### 9.1 - Nginx Configuration
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Add to nginx config for igny8
|
||||||
|
|
||||||
|
# Serve plugin downloads
|
||||||
|
location /plugins/download/ {
|
||||||
|
alias /data/app/igny8/plugins/;
|
||||||
|
|
||||||
|
# Only allow ZIP files
|
||||||
|
location ~ \.zip$ {
|
||||||
|
add_header Content-Disposition 'attachment';
|
||||||
|
add_header X-Content-Type-Options 'nosniff';
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deny access to source files
|
||||||
|
location ~ /source/ {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 - Django URL (Proxied Downloads)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Better approach: Django handles download with auth
|
||||||
|
|
||||||
|
# backend/igny8_core/plugins/views.py
|
||||||
|
|
||||||
|
from django.http import FileResponse
|
||||||
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
@permission_classes([AllowAny])
|
||||||
|
def download_plugin(request, slug):
|
||||||
|
plugin = get_object_or_404(Plugin, slug=slug, is_active=True)
|
||||||
|
latest = plugin.versions.filter(status__in=['released', 'update_ready']).first()
|
||||||
|
|
||||||
|
if not latest:
|
||||||
|
return Response({'error': 'No version available'}, status=404)
|
||||||
|
|
||||||
|
file_path = f'/data/app/igny8/plugins/{plugin.platform}/dist/{latest.file_path}'
|
||||||
|
|
||||||
|
response = FileResponse(
|
||||||
|
open(file_path, 'rb'),
|
||||||
|
content_type='application/zip'
|
||||||
|
)
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="{latest.file_path}"'
|
||||||
|
return response
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Monitoring & Analytics
|
||||||
|
|
||||||
|
### 10.1 - Metrics to Track
|
||||||
|
|
||||||
|
| Metric | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| Downloads per version | Adoption rate |
|
||||||
|
| Active installations | Current reach |
|
||||||
|
| Update success rate | Quality indicator |
|
||||||
|
| Version distribution | Upgrade urgency |
|
||||||
|
| Health check failures | Support needs |
|
||||||
|
|
||||||
|
### 10.2 - Admin Dashboard Widgets
|
||||||
|
|
||||||
|
- **Plugin Downloads** - Daily/weekly download counts
|
||||||
|
- **Version Distribution** - Pie chart of installed versions
|
||||||
|
- **Update Adoption** - Time to 80% adoption after release
|
||||||
|
- **Installation Health** - Sites with outdated/failing plugins
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build new version
|
||||||
|
python manage.py build_plugin --plugin=igny8-wp-bridge --version=1.0.1 --changelog="Bug fixes"
|
||||||
|
|
||||||
|
# Release version (make downloadable)
|
||||||
|
python manage.py release_plugin --plugin=igny8-wp-bridge --version=1.0.1
|
||||||
|
|
||||||
|
# Push update to sites
|
||||||
|
python manage.py push_plugin_update --plugin=igny8-wp-bridge --version=1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Quick Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/plugins/{slug}/download/ # Download latest ZIP
|
||||||
|
GET /api/plugins/{slug}/check-update/ # Check for updates
|
||||||
|
GET /api/plugins/{slug}/info/ # Plugin metadata
|
||||||
|
POST /api/plugins/{slug}/register/ # Register installation
|
||||||
|
POST /api/plugins/{slug}/health-check/ # Report status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Owner:** IGNY8 Team
|
||||||
|
**Next Review:** After Phase 1 implementation
|
||||||
Reference in New Issue
Block a user