4 * @module routes/wordpress
5 * @description WordPress site management and DigitalOcean App Platform deployment endpoints.
6 * Handles automated WordPress provisioning with database, Spaces storage, and credential management.
7 * Integrates with DigitalOcean API for app creation and project organization.
9 * @requires middleware/auth
10 * @requires middleware/tenant
11 * @requires services/db
14 * @author RMM-PSA Development Team
15 * @copyright 2026 RMM-PSA Platform
16 * @license Proprietary
20 * @apiDefine WordPress WordPress
21 * WordPress site deployment and management for DigitalOcean App Platform
24const express = require('express');
25const router = express.Router();
26const authenticateToken = require('../middleware/auth');
27const { setTenantContext } = require('../middleware/tenant');
28const pool = require('../db');
29const axios = require('axios');
30const crypto = require('crypto');
31// const { Client } = require('ssh2');
32// const fs = require('fs');
34// Monitoring POST endpoint (no auth required - uses API key instead)
35router.post('/monitoring', async (req, res) => {
37 const monitorKey = req.headers['x-monitor-key'] || req.headers['x-monitor-api-key'];
40 return res.status(401).json({ error: 'Missing monitor key' });
43 const { server_ip, server_hostname, timestamp, last_scan, sites } = req.body;
45 if (!server_ip || !sites) {
46 return res.status(400).json({ error: 'Missing required fields' });
49 // Store raw monitoring report
51 INSERT INTO wordpress_monitoring_reports (server_ip, server_hostname, report_data)
53 `, [server_ip, server_hostname, JSON.stringify(req.body)]);
55 console.log(`[WordPress Monitoring] Received report from ${server_ip} with ${sites.length} sites`);
59 message: 'Monitoring data received',
60 sites_updated: sites.length
63 console.error('[WordPress Monitoring] Error processing data:', error);
64 res.status(500).json({ error: 'Failed to process monitoring data' });
68// Apply authentication and tenant context to all other routes
69router.use(authenticateToken, setTenantContext);
72 * @api {get} /api/wordpress/deployments/:id/progress Get deployment progress
73 * @apiName GetWordPressDeploymentProgress
75 * @apiDescription Check the progress of a WordPress site deployment. Returns current status,
76 * step, and any error messages. Used for real-time deployment monitoring in the UI.
77 * @apiParam {number} id Deployment ID
78 * @apiSuccess {object} progress Deployment progress object
79 * @apiSuccess {number} progress.id Deployment ID
80 * @apiSuccess {string} progress.site_name Site name being deployed
81 * @apiSuccess {string} progress.status Status (pending, in_progress, success, error)
82 * @apiSuccess {string} progress.step Current deployment step
83 * @apiSuccess {string} progress.message Progress message
84 * @apiSuccess {string} [progress.error] Error message if status=error
85 * @apiSuccess {DateTime} progress.created_at Deployment start time
86 * @apiSuccess {DateTime} progress.updated_at Last update time
87 * @apiError {string} error="Deployment not found" (404) Deployment ID not found
88 * @apiError {string} error="Failed to fetch deployment progress" (500) Database query failed
89 * @apiExample {curl} Example:
90 * curl -X GET http://localhost:3000/api/wordpress/deployments/123/progress \
91 * -H "Authorization: Bearer YOUR_TOKEN"
92 * @apiSuccessExample {json} Success-Response:
97 * "site_name": "example-site",
98 * "status": "in_progress",
100 * "message": "Creating database wp_example_site",
101 * "created_at": "2026-03-12T10:00:00.000Z",
102 * "updated_at": "2026-03-12T10:02:30.000Z"
106router.get('/deployments/:id/progress', async (req, res) => {
107 const { id } = req.params;
109 const result = await pool.query(
110 `SELECT id, site_name, status, step, message, error, created_at, updated_at
111 FROM wordpress_deployments WHERE id = $1`,
114 if (result.rows.length === 0) {
115 return res.status(404).json({ error: 'Deployment not found' });
117 res.json({ progress: result.rows[0] });
119 res.status(500).json({ error: 'Failed to fetch deployment progress', details: error.message });
123// Helper function to generate secure password
128function generatePassword(length = 16) {
129 const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
131 const randomBytes = crypto.randomBytes(length);
132 for (let i = 0; i < length; i++) {
133 password += charset[randomBytes[i] % charset.length];
139 * Execute command on WordPress server via SSH
140 * @param {string} serverIp - Server IP address
141 * @param {string} command - Command to execute
142 * @returns {Promise<string>} Command output
144/* TEMPORARILY DISABLED - Moving to API bridge approach for security
145async function sshExec(serverIp, command) {
146 return new Promise((resolve, reject) => {
147 const conn = new Client();
150 const sshKeyPath = process.env.WORDPRESS_SSH_KEY_PATH || '/root/.ssh/wordpress_hosting_rsa';
152 if (!fs.existsSync(sshKeyPath)) {
153 return reject(new Error('SSH key not found'));
156 const privateKey = fs.readFileSync(sshKeyPath);
158 conn.on('ready', () => {
159 conn.exec(command, (err, stream) => {
165 stream.on('close', (code) => {
168 return reject(new Error(`Command exited with code ${code}`));
171 }).on('data', (data) => {
172 output += data.toString();
173 }).stderr.on('data', (data) => {
174 console.error('[SSH] stderr:', data.toString());
177 }).on('error', (err) => {
183 privateKey: privateKey
190 * @api {get} /api/wordpress/sites List WordPress sites
191 * @apiName ListWordPressSites
192 * @apiGroup WordPress
193 * @apiDescription Get all WordPress sites for the current tenant with optional customer details.
194 * Sites are ordered by creation date (newest first).
195 * @apiSuccess {Array} sites Array of WordPress site objects
196 * @apiSuccess {UUID} sites.site_id Site ID
197 * @apiSuccess {UUID} sites.tenant_id Tenant ID
198 * @apiSuccess {UUID} [sites.customer_id] Customer ID
199 * @apiSuccess {string} sites.name Site name (slug)
200 * @apiSuccess {string} sites.app_id DigitalOcean App ID
201 * @apiSuccess {string} sites.region Region (syd, nyc3, etc)
202 * @apiSuccess {string} sites.status App status (deploying, active, error)
203 * @apiSuccess {string} sites.url App URL
204 * @apiSuccess {string} [sites.domain] Custom domain if configured
205 * @apiSuccess {string} sites.db_name MySQL database name
206 * @apiSuccess {string} sites.spaces_path Spaces storage folder
207 * @apiSuccess {string} sites.wp_admin_user WordPress admin username
208 * @apiSuccess {string} sites.wp_admin_password WordPress admin password
209 * @apiSuccess {string} sites.wp_admin_email WordPress admin email
210 * @apiSuccess {string} [sites.customer_name] Customer name (joined)
211 * @apiSuccess {DateTime} sites.created_at Creation timestamp
212 * @apiError {string} error="Missing tenant context" (400) No tenant in request
213 * @apiError {string} error="Failed to fetch WordPress sites" (500) Database query failed
214 * @apiExample {curl} Example:
215 * curl -X GET http://localhost:3000/api/wordpress/sites \
216 * -H "Authorization: Bearer YOUR_TOKEN"
217 * @apiSuccessExample {json} Success-Response:
222 * "site_id": "123e4567-e89b-12d3-a456-426614174000",
223 * "tenant_id": "456e7890-ab12-34cd-5678-901234567890",
224 * "customer_id": "789e0123-ab45-67cd-89ef-012345678901",
225 * "name": "example-site",
226 * "app_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
228 * "status": "active",
229 * "url": "https://wordpress-example-site-abc123.ondigitalocean.app",
230 * "domain": "www.example.com",
231 * "db_name": "wp_example_site",
232 * "spaces_path": "example-site",
233 * "wp_admin_user": "admin",
234 * "wp_admin_password": "SecurePass123!@#$%",
235 * "wp_admin_email": "admin@example-site.everydaytech.au",
236 * "customer_name": "Acme Corp",
237 * "created_at": "2026-03-12T08:00:00.000Z"
242router.get('/sites', async (req, res) => {
244 // Validate tenant context
245 if (!req.tenant || !req.tenant.id) {
246 console.error('[WordPress] Missing tenant context:', { tenant: req.tenant, user: req.user?.email });
247 return res.status(400).json({
248 error: 'Missing tenant context. Please ensure you are logged in and have a valid tenant.'
252 const result = await pool.query(
255 c.name as customer_name
256 FROM wordpress_sites w
257 LEFT JOIN customers c ON w.customer_id = c.customer_id
258 WHERE w.tenant_id = $1
259 ORDER BY w.created_at DESC`,
263 res.json({ sites: result.rows });
265 console.error('[WordPress] Error fetching sites:', error);
266 res.status(500).json({ error: 'Failed to fetch WordPress sites', details: error.message });
271 * @api {post} /api/wordpress/create Create WordPress site
272 * @apiName CreateWordPressSite
273 * @apiGroup WordPress
274 * @apiDescription Fully automated WordPress site provisioning on DigitalOcean App Platform.
275 * Creates MySQL database, configures Spaces storage, deploys from GitHub template with PHP buildpack,
276 * generates credentials, and assigns to WordPress project. Tracks progress via deployment ID.
277 * @apiParam {string} siteName Site name slug (lowercase alphanumeric with hyphens)
278 * @apiParam {number} [customerId] Customer ID to link site to
279 * @apiParam {string} [domain] Custom domain (optional, PRIMARY domain for app)
280 * @apiSuccess {boolean} success=true Site created successfully
281 * @apiSuccess {string} message="WordPress site created successfully"
282 * @apiSuccess {string} appId DigitalOcean App ID
283 * @apiSuccess {string} url Default app URL (.ondigitalocean.app)
284 * @apiSuccess {string} status App deployment status
285 * @apiSuccess {number} deploymentId Deployment progress tracker ID
286 * @apiSuccess {Object} credentials WordPress and database credentials
287 * @apiSuccess {string} credentials.adminUser WordPress admin username (always 'admin')
288 * @apiSuccess {string} credentials.adminPassword Generated WordPress admin password
289 * @apiSuccess {string} credentials.adminEmail WordPress admin email
290 * @apiSuccess {string} credentials.database MySQL database name
291 * @apiSuccess {string} credentials.spacesPath Spaces storage folder
292 * @apiError {string} error="Missing tenant context" (400) No tenant in request
293 * @apiError {string} error="Missing required field: siteName" (400) siteName not provided
294 * @apiError {string} error="Site name must contain only lowercase letters, numbers, and hyphens" (400) Invalid siteName format
295 * @apiError {String} error="WordPress credentials not configured" (400) Global WordPress config missing
296 * @apiError {String} error="DigitalOcean configuration not found" (404) Tenant DO config missing
297 * @apiError {String} error (500) Deployment failed
299 * @apiExample {curl} Example:
300 * curl -X POST http://localhost:3000/api/wordpress/create \
301 * -H "Authorization: Bearer YOUR_TOKEN" \
302 * -H "Content-Type: application/json" \
304 * "siteName": "example-site",
306 * "domain": "www.example.com"
309 * @apiSuccessExample {json} Success-Response:
313 * "message": "WordPress site created successfully",
314 * "appId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
315 * "url": "https://wordpress-example-site-abc123.ondigitalocean.app",
316 * "status": "deploying",
317 * "deploymentId": 456,
319 * "adminUser": "admin",
320 * "adminPassword": "Xy9!kL2#mN5$pQ8@rT1%",
321 * "adminEmail": "admin@example-site.everydaytech.au",
322 * "database": "wp_example_site",
323 * "spacesPath": "example-site"
327router.post('/create', async (req, res) => {
328 const { siteName: siteNameRaw, customerId: customerIdRaw, domain: domainRaw } = req.body;
330 // Ensure values are properly typed
331 const siteName = siteNameRaw ? String(siteNameRaw).trim() : null;
332 const domain = domainRaw ? String(domainRaw).trim() : null;
333 const customerId = customerIdRaw ? parseInt(customerIdRaw, 10) : null;
335 // Validate tenant context
336 if (!req.tenant || !req.tenant.id) {
337 console.error('[WordPress] Missing tenant context:', { tenant: req.tenant, user: req.user?.email });
338 return res.status(400).json({
339 error: 'Missing tenant context. Please ensure you are logged in and have a valid tenant.'
345 return res.status(400).json({
346 error: 'Missing required field: siteName'
350 // Validate siteName format (lowercase, alphanumeric, hyphens only)
351 if (!/^[a-z0-9-]+$/.test(siteName)) {
352 return res.status(400).json({
353 error: 'Site name must contain only lowercase letters, numbers, and hyphens'
357 // Validate customerId if provided
358 if (customerId !== null && (isNaN(customerId) || customerId < 1)) {
359 return res.status(400).json({
360 error: 'Invalid customerId: must be a positive integer'
364 let deploymentId = null;
366 // Insert deployment progress record
367 const depResult = await pool.query(
368 `INSERT INTO wordpress_deployments (site_name, status, step, message)
369 VALUES ($1, 'pending', 'starting', 'Starting deployment') RETURNING id`,
372 deploymentId = depResult.rows[0].id;
373 const updateProgress = async (step, status, message, error = null) => {
375 `UPDATE wordpress_deployments SET step=$1, status=$2, message=$3, error=$4, updated_at=NOW() WHERE id=$5`,
376 [step, status, message, error, deploymentId]
379 // Get WordPress credentials from global settings
380 const settingsResult = await pool.query('SELECT config FROM app_settings ORDER BY id DESC LIMIT 1');
382 if (settingsResult.rows.length === 0 || !settingsResult.rows[0].config || !settingsResult.rows[0].config.wordpress) {
383 if (deploymentId) await updateProgress('settings', 'error', 'WordPress credentials not configured', 'Missing config');
384 return res.status(400).json({
385 error: 'WordPress credentials not configured. Please configure in Settings (Root Tenant only).'
389 const wordpressConfig = settingsResult.rows[0].config.wordpress;
390 const dbHost = wordpressConfig.database_host;
391 const dbPort = wordpressConfig.database_port;
392 const dbUser = wordpressConfig.database_user;
393 const dbPassword = wordpressConfig.database_password;
394 const dbDefaultName = wordpressConfig.database_name;
395 const dbSslMode = wordpressConfig.database_sslmode || 'REQUIRED';
396 const mysqlClusterId = wordpressConfig.mysql_cluster_id;
397 const spacesKey = wordpressConfig.spaces_access_key;
398 const spacesSecret = wordpressConfig.spaces_secret_key;
400 if (!dbHost || !dbPort || !dbUser || !dbPassword || !dbDefaultName ||
401 !mysqlClusterId || !spacesKey || !spacesSecret) {
402 if (deploymentId) await updateProgress('settings', 'error', 'Incomplete WordPress credentials in settings', 'Missing fields');
403 return res.status(400).json({
404 error: 'Incomplete WordPress credentials in settings. Please configure all fields in Settings (database_host, database_port, database_user, database_password, database_name, mysql_cluster_id, spaces_access_key, spaces_secret_key).'
408 // Get DigitalOcean API token
409 const configResult = await pool.query(
410 `SELECT api_token_encrypted FROM digitalocean_config WHERE tenant_id = $1 AND is_active = true`,
414 if (configResult.rows.length === 0) {
415 if (deploymentId) await updateProgress('settings', 'error', 'DigitalOcean configuration not found', 'No config');
416 return res.status(404).json({
417 error: 'DigitalOcean configuration not found. Please configure in Settings.'
421 const doToken = configResult.rows[0].api_token_encrypted;
423 // Generate WordPress admin credentials
424 const wpAdminUser = 'admin';
425 const wpAdminPassword = generatePassword(20);
426 const wpAdminEmail = `admin@${siteName}.everydaytech.au`;
427 const dbName = `wp_${siteName.replace(/-/g, '_')}`;
428 const spacesPath = siteName;
430 if (deploymentId) await updateProgress('credentials', 'in_progress', 'Generated WordPress credentials');
432 // Create MySQL database on DigitalOcean (using cluster ID from settings)
434 if (deploymentId) await updateProgress('database', 'in_progress', `Creating database ${dbName}`);
436 `https://api.digitalocean.com/v2/databases/${mysqlClusterId}/dbs`,
440 'Authorization': `Bearer ${doToken}`,
441 'Content-Type': 'application/json'
445 if (deploymentId) await updateProgress('database', 'success', `Database ${dbName} created successfully`);
447 if (dbError.response?.status === 422) {
448 if (deploymentId) await updateProgress('database', 'success', `Database ${dbName} already exists, continuing...`);
450 if (deploymentId) await updateProgress('database', 'error', 'Failed to create MySQL database', dbError.response?.data?.message || dbError.message);
451 throw new Error(`Failed to create MySQL database: ${dbError.response?.data?.message || dbError.message}`);
455 // Note: Spaces folder will be auto-created by WordPress when it uploads first file
456 // The BUCKET_SITE_PATH env var tells WordPress which folder to use
457 if (deploymentId) await updateProgress('spaces', 'in_progress', 'Preparing Spaces bucket/folder');
459 // Get WordPress project from DigitalOcean
460 let projectId = null;
462 const projectsResponse = await axios.get(
463 'https://api.digitalocean.com/v2/projects',
466 'Authorization': `Bearer ${doToken}`,
467 'Content-Type': 'application/json'
471 const projects = projectsResponse.data.projects || [];
472 // Look for WordPress projects folder
473 const wordpressProject = projects.find(p =>
474 p.name.toLowerCase().includes('wordpress')
476 if (wordpressProject) {
477 projectId = wordpressProject.id;
478 if (deploymentId) await updateProgress('project', 'success', `Using project: ${wordpressProject.name}`);
480 if (deploymentId) await updateProgress('project', 'success', 'No WordPress project found, using default project');
483 if (deploymentId) await updateProgress('project', 'error', 'Could not fetch projects', error.message);
484 // Continue without project - app will be created in default project
487 // Create App Spec for WordPress using PHP buildpack (proven working method)
488 if (deploymentId) await updateProgress('app', 'in_progress', 'Creating App Platform app');
490 name: `wordpress-${siteName}`,
497 deploy_on_push: false,
498 repo: 'Independent-Business-Group/wordpress-template'
500 build_command: 'chmod +x .do/deploy.sh && ./.do/deploy.sh',
501 run_command: 'heroku-php-apache2',
502 environment_slug: 'php',
504 instance_size_slug: 'apps-s-1vcpu-0.5gb',
507 http_path: '/health.php',
509 initial_delay_seconds: 60,
512 success_threshold: 1,
523 scope: 'RUN_AND_BUILD_TIME',
529 scope: 'RUN_AND_BUILD_TIME',
535 scope: 'RUN_AND_BUILD_TIME',
541 scope: 'RUN_AND_BUILD_TIME',
547 scope: 'RUN_AND_BUILD_TIME',
548 value: String(dbPort)
552 scope: 'RUN_AND_BUILD_TIME',
556 key: 'SPACES_BUCKET',
557 scope: 'RUN_AND_BUILD_TIME',
558 value: 'everydaytech-wordpress'
561 key: 'SPACES_FOLDER',
562 scope: 'RUN_AND_BUILD_TIME',
567 scope: 'RUN_AND_BUILD_TIME',
572 key: 'SPACES_SECRET',
573 scope: 'RUN_AND_BUILD_TIME',
582 // Add custom domain if provided
592 console.log('[WordPress] Creating app with spec:', JSON.stringify(appSpec, null, 2));
594 // Create app on DigitalOcean
595 const doResponse = await axios.post(
596 'https://api.digitalocean.com/v2/apps',
600 'Authorization': `Bearer ${doToken}`,
601 'Content-Type': 'application/json'
606 const app = doResponse.data.app;
607 if (deploymentId) await updateProgress('app', 'success', `App created: ${app.id}`);
608 console.log(`[WordPress] App created: ${app.id}`);
610 // Assign app to WordPress project if we found one
614 `https://api.digitalocean.com/v2/projects/${projectId}/resources`,
616 resources: [`do:app:${app.id}`]
620 'Authorization': `Bearer ${doToken}`,
621 'Content-Type': 'application/json'
625 if (deploymentId) await updateProgress('project', 'success', `App assigned to project ${projectId}`);
626 console.log(`[WordPress] App assigned to project ${projectId}`);
628 if (deploymentId) await updateProgress('project', 'error', 'Could not assign app to project', error.message);
629 console.warn(`[WordPress] Could not assign app to project:`, error.message);
630 // Continue even if assignment fails
636 `INSERT INTO wordpress_sites (
650 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
657 app.phase || 'deploying',
658 app.default_ingress || null,
667 if (deploymentId) await updateProgress('completed', 'success', 'Deployment completed', null);
671 message: 'WordPress site created successfully',
673 url: app.default_ingress,
677 adminUser: wpAdminUser,
678 adminPassword: wpAdminPassword,
679 adminEmail: wpAdminEmail,
681 spacesPath: spacesPath
685 console.error('[WordPress] Error creating site:', error);
687 // More detailed error message
688 if (error.response) {
689 const doError = error.response.data;
690 console.error('[WordPress] DO API Error:', JSON.stringify(doError, null, 2));
691 return res.status(error.response.status).json({
692 error: doError.message || 'Failed to create WordPress site on DigitalOcean',
697 res.status(500).json({
698 error: error.message || 'Failed to create WordPress site'
704 * @api {get} /api/wordpress/site/:appId Get WordPress site details
705 * @apiName GetWordPressSite
706 * @apiGroup WordPress
707 * @apiDescription Get detailed information about a specific WordPress site by App ID.
708 * Optionally fetches live status from DigitalOcean API and updates database cache.
709 * @apiParam {string} appId DigitalOcean App ID
710 * @apiSuccess {object} site WordPress site object
711 * @apiSuccess {UUID} site.site_id Site ID
712 * @apiSuccess {UUID} site.tenant_id Tenant ID
713 * @apiSuccess {UUID} [site.customer_id] Customer ID
714 * @apiSuccess {string} site.name Site name
715 * @apiSuccess {string} site.app_id DigitalOcean App ID
716 * @apiSuccess {string} site.status Current status (deploying, active, error)
717 * @apiSuccess {string} site.url App URL
718 * @apiSuccess {string} [site.domain] Custom domain
719 * @apiSuccess {string} site.db_name MySQL database name
720 * @apiSuccess {string} site.spaces_path Spaces storage folder
721 * @apiSuccess {string} site.wp_admin_user WordPress admin username
722 * @apiSuccess {string} site.wp_admin_password WordPress admin password
723 * @apiSuccess {string} site.wp_admin_email WordPress admin email
724 * @apiSuccess {string} [site.customer_name] Customer name (joined)
725 * @apiError {string} error="Missing tenant context" (400) No tenant in request
726 * @apiError {string} error="WordPress site not found" (404) Site not found for tenant
727 * @apiError {string} error="Failed to fetch WordPress site" (500) Database query failed
728 * @apiExample {curl} Example:
729 * curl -X GET http://localhost:3000/api/wordpress/site/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
730 * -H "Authorization: Bearer YOUR_TOKEN"
732 * @apiSuccessExample {json} Success-Response:
736 * "site_id": "123e4567-e89b-12d3-a456-426614174000",
737 * "tenant_id": "456e7890-ab12-34cd-5678-901234567890",
738 * "customer_id": "789e0123-ab45-67cd-89ef-012345678901",
739 * "name": "example-site",
740 * "app_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
742 * "status": "active",
743 * "url": "https://wordpress-example-site-abc123.ondigitalocean.app",
744 * "domain": "www.example.com",
745 * "db_name": "wp_example_site",
746 * "spaces_path": "example-site",
747 * "wp_admin_user": "admin",
748 * "wp_admin_password": "SecurePass123!@#$%",
749 * "wp_admin_email": "admin@example-site.everydaytech.au",
750 * "customer_name": "Acme Corp"
754router.get('/site/:appId', async (req, res) => {
756 const { appId } = req.params;
758 // Validate tenant context
759 if (!req.tenant || !req.tenant.id) {
760 console.error('[WordPress] Missing tenant context:', { tenant: req.tenant, user: req.user?.email });
761 return res.status(400).json({
762 error: 'Missing tenant context. Please ensure you are logged in and have a valid tenant.'
767 const result = await pool.query(
770 c.name as customer_name
771 FROM wordpress_sites w
772 LEFT JOIN customers c ON w.customer_id = c.customer_id
773 WHERE w.app_id = $1 AND w.tenant_id = $2`,
774 [appId, req.tenant.id]
777 if (result.rows.length === 0) {
778 return res.status(404).json({ error: 'WordPress site not found' });
781 // Optionally fetch live status from DigitalOcean
782 const configResult = await pool.query(
783 `SELECT api_token_encrypted FROM digitalocean_config WHERE tenant_id = $1 AND is_active = true`,
787 if (configResult.rows.length > 0) {
788 const doToken = configResult.rows[0].api_token_encrypted;
791 const doResponse = await axios.get(
792 `https://api.digitalocean.com/v2/apps/${appId}`,
795 'Authorization': `Bearer ${doToken}`
800 const app = doResponse.data.app;
802 // Update database with latest info
804 `UPDATE wordpress_sites
805 SET status = $1, url = $2, updated_at = CURRENT_TIMESTAMP
807 [app.phase, app.default_ingress, appId]
810 result.rows[0].status = app.phase;
811 result.rows[0].url = app.default_ingress;
813 console.error('[WordPress] Error fetching app from DO:', doError.message);
814 // Continue with database data if DO API fails
818 res.json({ site: result.rows[0] });
820 console.error('[WordPress] Error fetching site:', error);
821 res.status(500).json({ error: 'Failed to fetch WordPress site' });
826 * @api {get} /api/wordpress/monitoring/latest Get latest monitoring data
827 * @apiName GetLatestMonitoring
828 * @apiGroup WordPress
829 * @apiDescription Get the most recent monitoring report from all WordPress servers
830 * @apiSuccess {object[]} reports Array of monitoring reports
832router.get('/monitoring/latest', async (req, res) => {
834 const result = await pool.query(`
841 FROM wordpress_monitoring_reports
842 WHERE received_at > NOW() - INTERVAL '24 hours'
843 ORDER BY received_at DESC
847 const reports = result.rows.map(row => ({
849 report_id: row.report_id,
850 received_at: row.received_at
853 res.json({ reports });
855 console.error('[WordPress Monitoring] Error fetching reports:', error);
856 res.status(500).json({ error: 'Failed to fetch monitoring data' });
861 * @api {post} /api/wordpress/sites/:domain/assign Assign site to customer
862 * @apiName AssignWordPressSiteToCustomer
863 * @apiGroup WordPress
864 * @apiDescription Assign a WordPress site to a specific customer and add notes
865 * @apiParam {string} domain WordPress site domain
866 * @apiBody {number} customerId Customer ID to assign to
867 * @apiBody {string} [notes] Optional notes about the site
868 * @apiSuccess {boolean} success Operation success
869 * @apiSuccess {string} message Success message
871router.post('/sites/:domain/assign', async (req, res) => {
873 const { domain } = req.params;
874 const { customerId, notes } = req.body;
875 const tenantId = req.user.tenantId;
877 // Get or create the WordPress site record
878 const siteResult = await pool.query(`
879 INSERT INTO wordpress_sites (tenant_id, domain, notes, updated_at)
880 VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
881 ON CONFLICT (tenant_id, domain) DO UPDATE SET
883 updated_at = CURRENT_TIMESTAMP
885 `, [tenantId, domain, notes || null]);
887 const siteId = siteResult.rows[0].site_id;
889 // If customer_id provided, create or update customer association
891 // Update customer assignment (assuming we add a customer_id column)
893 UPDATE wordpress_sites
895 updated_at = CURRENT_TIMESTAMP
897 `, [notes || null, siteId]);
899 // You may want to create a separate wordpress_site_customers table for many-to-many
900 // For now, we'll store it in notes or add a customer_id column
905 message: 'Site assignment updated successfully'
908 console.error('[WordPress] Error assigning site:', error);
909 res.status(500).json({ error: 'Failed to update site assignment' });
914 * @api {get} /api/wordpress/sites/:domain Get site details
915 * @apiName GetWordPressSiteDetails
916 * @apiGroup WordPress
917 * @apiDescription Get detailed information about a WordPress site including plugins, themes, and security issues
918 * @apiParam {string} domain Site domain
919 * @apiSuccess {object} site Site details with plugins, themes, security data
921router.get('/sites/:domain', async (req, res) => {
923 const { domain } = req.params;
925 // Get latest monitoring data for this site
926 const result = await pool.query(`
933 FROM wordpress_monitoring_reports
934 WHERE received_at > NOW() - INTERVAL '24 hours'
935 ORDER BY received_at DESC
939 if (result.rows.length === 0) {
940 return res.status(404).json({ error: 'No recent monitoring data found' });
943 const report = result.rows[0];
944 const reportData = report.report_data;
946 // Find the specific site
947 const site = reportData.sites?.find(s => s.domain === domain);
950 return res.status(404).json({ error: 'Site not found in monitoring data' });
953 // Get security scan data
954 const securityResult = await pool.query(`
964 FROM wordpress_security_scans
966 SELECT site_id FROM wordpress_sites WHERE domain = $1
968 ORDER BY scan_date DESC
973 const siteDetails = {
975 server_ip: report.server_ip,
976 server_hostname: report.server_hostname,
977 last_updated: report.received_at,
978 last_scan: reportData.last_scan,
979 security: securityResult.rows.length > 0 ? securityResult.rows[0] : null,
986 // Fetch detailed information via WordPress API Bridge
988 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
989 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
991 if (!wordpressApiKey) {
992 console.warn('[WordPress] WORDPRESS_API_KEY not configured, skipping detailed fetch');
994 console.log(`[WordPress] Fetching detailed info for ${domain} from API bridge`);
996 const apiUrl = `http://${report.server_ip}:${wordpressApiPort}/api/site/${domain}/details`;
997 const apiResponse = await axios.get(apiUrl, {
999 'X-API-Key': wordpressApiKey
1004 const details = apiResponse.data;
1005 siteDetails.plugins = details.plugins || [];
1006 siteDetails.themes = details.themes || [];
1007 siteDetails.core = details.core || null;
1008 siteDetails.admin_users = details.admin_users || [];
1009 siteDetails.site_url = details.site_url || `https://${domain}`;
1010 siteDetails.database = details.database || null;
1012 } catch (apiError) {
1013 console.error('[WordPress] API fetch error:', apiError.message);
1014 // Continue with basic data even if API fetch fails
1015 siteDetails.api_error = apiError.message;
1018 res.json({ site: siteDetails });
1020 console.error('[WordPress] Error fetching site details:', error);
1021 res.status(500).json({ error: 'Failed to fetch site details' });
1026 * @api {post} /api/wordpress/sites/:domain/login Generate auto-login URL
1027 * @apiName GenerateWordPressAutoLogin
1028 * @apiGroup WordPress
1029 * @apiDescription Generate a one-time auto-login URL for WordPress admin access
1030 * @apiParam {string} domain Site domain
1031 * @apiSuccess {string} loginUrl One-time login URL
1033router.post('/sites/:domain/login', async (req, res) => {
1035 const { domain } = req.params;
1037 // Get site info from latest monitoring data
1038 const result = await pool.query(`
1041 FROM wordpress_monitoring_reports
1042 WHERE received_at > NOW() - INTERVAL '24 hours'
1043 ORDER BY received_at DESC
1047 if (result.rows.length === 0) {
1048 return res.status(404).json({ error: 'Site not found' });
1051 const reportData = result.rows[0].report_data;
1052 const site = reportData.sites?.find(s => s.domain === domain);
1055 return res.status(404).json({ error: 'Site not found in monitoring data' });
1058 // Generate one-time login token (expires in 5 minutes)
1059 const loginToken = crypto.randomBytes(32).toString('hex');
1060 const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes
1064 INSERT INTO wordpress_sites (tenant_id, domain, sso_token, sso_token_expires, updated_at)
1065 VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
1066 ON CONFLICT (tenant_id, domain) DO UPDATE SET
1068 sso_token_expires = $4,
1069 updated_at = CURRENT_TIMESTAMP
1070 `, [req.user.tenantId, domain, loginToken, expiresAt]);
1072 // Return the login URL (will be handled by a separate auto-login endpoint)
1073 const loginUrl = `https://${domain}/wp-admin/?auto_login=${loginToken}`;
1079 message: 'Auto-login URL generated (expires in 5 minutes)'
1082 console.error('[WordPress] Error generating login URL:', error);
1083 res.status(500).json({ error: 'Failed to generate login URL' });
1088 * @api {get} /api/wordpress/sites/:domain/users Get site users
1089 * @apiName GetWordPressSiteUsers
1090 * @apiGroup WordPress
1091 * @apiDescription Get all users and their roles for a WordPress site
1092 * @apiParam {string} domain Site domain
1093 * @apiSuccess {array} users List of users with roles
1095router.get('/sites/:domain/users', async (req, res) => {
1097 const { domain } = req.params;
1098 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1099 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1101 if (!wordpressApiKey) {
1102 return res.status(503).json({ error: 'WordPress API not configured' });
1105 // Get server IP from monitoring data
1106 const result = await pool.query(`
1107 SELECT server_ip FROM wordpress_monitoring_reports
1108 WHERE received_at > NOW() - INTERVAL '24 hours'
1109 AND report_data->'sites' @> jsonb_build_array(jsonb_build_object('domain', $1::text))
1110 ORDER BY received_at DESC LIMIT 1
1113 if (result.rows.length === 0) {
1114 return res.status(404).json({ error: 'Site server not found' });
1117 const serverIp = result.rows[0].server_ip;
1118 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/users`;
1120 const apiResponse = await axios.get(apiUrl, {
1121 headers: { 'X-API-Key': wordpressApiKey },
1125 res.json(apiResponse.data);
1127 console.error('[WordPress] Users fetch error:', error.message);
1128 res.status(500).json({ error: 'Failed to fetch users', message: error.message });
1133 * @api {post} /api/wordpress/sites/:domain/plugin/:plugin/update Update plugin
1134 * @apiName UpdateWordPressPlugin
1135 * @apiGroup WordPress
1136 * @apiDescription Update a specific plugin on a WordPress site
1137 * @apiParam {string} domain Site domain
1138 * @apiParam {string} plugin Plugin slug
1139 * @apiSuccess {boolean} success Operation success
1141router.post('/sites/:domain/plugin/:plugin/update', async (req, res) => {
1143 const { domain, plugin } = req.params;
1144 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1145 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1147 if (!wordpressApiKey) {
1148 return res.status(503).json({ error: 'WordPress API not configured' });
1151 // Get server IP from monitoring data
1152 const result = await pool.query(`
1153 SELECT server_ip FROM wordpress_monitoring_reports
1154 WHERE received_at > NOW() - INTERVAL '24 hours'
1155 AND report_data->'sites' @> jsonb_build_array(jsonb_build_object('domain', $1::text))
1156 ORDER BY received_at DESC LIMIT 1
1159 if (result.rows.length === 0) {
1160 return res.status(404).json({ error: 'Site server not found' });
1163 const serverIp = result.rows[0].server_ip;
1164 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/plugin/${plugin}/update`;
1166 const apiResponse = await axios.post(apiUrl, {}, {
1167 headers: { 'X-API-Key': wordpressApiKey },
1171 res.json(apiResponse.data);
1173 console.error('[WordPress] Plugin update error:', error.message);
1174 res.status(500).json({ error: 'Failed to update plugin', message: error.message });
1179 * @api {post} /api/wordpress/sites/:domain/theme/:theme/update Update theme
1180 * @apiName UpdateWordPressTheme
1181 * @apiGroup WordPress
1182 * @apiDescription Update a specific theme on a WordPress site
1183 * @apiParam {string} domain Site domain
1184 * @apiParam {string} theme Theme slug
1185 * @apiSuccess {boolean} success Operation success
1187router.post('/sites/:domain/theme/:theme/update', async (req, res) => {
1189 const { domain, theme } = req.params;
1190 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1191 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1193 if (!wordpressApiKey) {
1194 return res.status(503).json({ error: 'WordPress API not configured' });
1197 // Get server IP from monitoring data
1198 const result = await pool.query(`
1199 SELECT server_ip FROM wordpress_monitoring_reports
1200 WHERE received_at > NOW() - INTERVAL '24 hours'
1201 AND report_data->'sites' @> jsonb_build_array(jsonb_build_object('domain', $1::text))
1202 ORDER BY received_at DESC LIMIT 1
1205 if (result.rows.length === 0) {
1206 return res.status(404).json({ error: 'Site server not found' });
1209 const serverIp = result.rows[0].server_ip;
1210 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/theme/${theme}/update`;
1212 const apiResponse = await axios.post(apiUrl, {}, {
1213 headers: { 'X-API-Key': wordpressApiKey },
1217 res.json(apiResponse.data);
1219 console.error('[WordPress] Theme update error:', error.message);
1220 res.status(500).json({ error: 'Failed to update theme', message: error.message });
1225 * @api {post} /api/wordpress/sites/:domain/core/update Update WordPress core
1226 * @apiName UpdateWordPressCore
1227 * @apiGroup WordPress
1228 * @apiDescription Update WordPress core on a site
1229 * @apiParam {string} domain Site domain
1230 * @apiBody {string} [version] Target version (optional)
1231 * @apiSuccess {boolean} success Operation success
1233router.post('/sites/:domain/core/update', async (req, res) => {
1235 const { domain } = req.params;
1236 const { version } = req.body;
1237 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1238 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1240 if (!wordpressApiKey) {
1241 return res.status(503).json({ error: 'WordPress API not configured' });
1244 // Get server IP from monitoring data
1245 const result = await pool.query(`
1246 SELECT server_ip FROM wordpress_monitoring_reports
1247 WHERE received_at > NOW() - INTERVAL '24 hours'
1248 AND report_data->'sites' @> jsonb_build_array(jsonb_build_object('domain', $1::text))
1249 ORDER BY received_at DESC LIMIT 1
1252 if (result.rows.length === 0) {
1253 return res.status(404).json({ error: 'Site server not found' });
1256 const serverIp = result.rows[0].server_ip;
1257 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/core/update`;
1259 const apiResponse = await axios.post(apiUrl, { version }, {
1260 headers: { 'X-API-Key': wordpressApiKey },
1261 timeout: 300000 // 5 minutes for core updates
1264 res.json(apiResponse.data);
1266 console.error('[WordPress] Core update error:', error.message);
1267 res.status(500).json({ error: 'Failed to update WordPress core', message: error.message });
1272 * @api {post} /api/wordpress/sites/:domain/clean Clean malware
1273 * @apiName CleanWordPressSiteThreats
1274 * @apiGroup WordPress
1275 * @apiDescription Remove malware and threats from a WordPress site
1276 * @apiParam {string} domain Site domain
1277 * @apiSuccess {boolean} success Operation success
1279router.post('/sites/:domain/clean', async (req, res) => {
1281 const { domain } = req.params;
1282 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1283 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1285 if (!wordpressApiKey) {
1286 return res.status(503).json({ error: 'WordPress API not configured' });
1289 // Get server IP from monitoring data
1290 const result = await pool.query(`
1291 SELECT server_ip FROM wordpress_monitoring_reports
1292 WHERE received_at > NOW() - INTERVAL '24 hours'
1293 AND report_data->'sites' @> jsonb_build_array(jsonb_build_object('domain', $1::text))
1294 ORDER BY received_at DESC LIMIT 1
1297 if (result.rows.length === 0) {
1298 return res.status(404).json({ error: 'Site server not found' });
1301 const serverIp = result.rows[0].server_ip;
1302 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/clean`;
1304 const apiResponse = await axios.post(apiUrl, {}, {
1305 headers: { 'X-API-Key': wordpressApiKey },
1309 res.json(apiResponse.data);
1311 console.error('[WordPress] Cleanup error:', error.message);
1312 res.status(500).json({ error: 'Failed to clean threats', message: error.message });
1317 * @api {get} /api/wordpress/sites/:domain/details Get detailed site information
1318 * @apiName GetWordPressSiteDetails
1319 * @apiGroup WordPress
1320 * @apiDescription Get detailed information about WordPress site including plugins, themes, and core version
1321 * @apiParam {string} domain Site domain
1322 * @apiSuccess {object} details Site details with plugins, themes, core info
1324router.get('/sites/:domain/details', async (req, res) => {
1326 const { domain } = req.params;
1327 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1328 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1330 if (!wordpressApiKey) {
1331 return res.status(503).json({ error: 'WordPress API not configured' });
1334 // Get server IP from monitoring data
1335 const result = await pool.query(`
1336 SELECT server_ip FROM wordpress_monitoring_reports
1337 WHERE received_at > NOW() - INTERVAL '24 hours'
1338 AND report_data->'sites' @> jsonb_build_array(jsonb_build_object('domain', $1::text))
1339 ORDER BY received_at DESC LIMIT 1
1342 if (result.rows.length === 0) {
1343 return res.status(404).json({ error: 'Site server not found' });
1346 const serverIp = result.rows[0].server_ip;
1347 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/details`;
1349 const apiResponse = await axios.get(apiUrl, {
1350 headers: { 'X-API-Key': wordpressApiKey },
1354 res.json(apiResponse.data);
1356 console.error('[WordPress] Site details error:', error.message);
1357 res.status(500).json({ error: 'Failed to fetch site details', message: error.message });
1362 * @api {post} /api/wordpress/sites/:domain/autologin Generate auto-login link
1363 * @apiName GenerateAutoLogin
1364 * @apiGroup WordPress
1365 * @apiDescription Generate a magic login link for WordPress admin access
1366 * @apiParam {string} domain Site domain
1367 * @apiBody {string} [username] Optional username (defaults to first admin)
1368 * @apiSuccess {string} login_url Magic login URL
1370router.post('/sites/:domain/autologin', async (req, res) => {
1372 const { domain } = req.params;
1373 const { username } = req.body;
1374 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1375 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1377 if (!wordpressApiKey) {
1378 return res.status(503).json({ error: 'WordPress API not configured' });
1381 // Get server IP from monitoring data
1382 const result = await pool.query(`
1383 SELECT server_ip FROM wordpress_monitoring_reports
1384 WHERE received_at > NOW() - INTERVAL '24 hours'
1385 AND report_data->'sites' @> jsonb_build_array(jsonb_build_object('domain', $1::text))
1386 ORDER BY received_at DESC LIMIT 1
1389 if (result.rows.length === 0) {
1390 return res.status(404).json({ error: 'Site server not found' });
1393 const serverIp = result.rows[0].server_ip;
1394 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/autologin`;
1396 const apiResponse = await axios.post(apiUrl, { username }, {
1397 headers: { 'X-API-Key': wordpressApiKey },
1401 res.json(apiResponse.data);
1403 console.error('[WordPress] Auto-login error:', error.message);
1404 res.status(500).json({ error: 'Failed to generate auto-login link', message: error.message });
1408module.exports = router;