EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
wordpress.js
Go to the documentation of this file.
1
2/**
3 * @file wordpress.js
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.
8 * @requires express
9 * @requires middleware/auth
10 * @requires middleware/tenant
11 * @requires services/db
12 * @requires axios
13 * @requires crypto
14 * @author RMM-PSA Development Team
15 * @copyright 2026 RMM-PSA Platform
16 * @license Proprietary
17 */
18
19/**
20 * @apiDefine WordPress WordPress
21 * WordPress site deployment and management for DigitalOcean App Platform
22 */
23
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');
33
34// Monitoring POST endpoint (no auth required - uses API key instead)
35router.post('/monitoring', async (req, res) => {
36 try {
37 const monitorKey = req.headers['x-monitor-key'] || req.headers['x-monitor-api-key'];
38
39 if (!monitorKey) {
40 return res.status(401).json({ error: 'Missing monitor key' });
41 }
42
43 const { server_ip, server_hostname, timestamp, last_scan, sites } = req.body;
44
45 if (!server_ip || !sites) {
46 return res.status(400).json({ error: 'Missing required fields' });
47 }
48
49 // Store raw monitoring report
50 await pool.query(`
51 INSERT INTO wordpress_monitoring_reports (server_ip, server_hostname, report_data)
52 VALUES ($1, $2, $3)
53 `, [server_ip, server_hostname, JSON.stringify(req.body)]);
54
55 console.log(`[WordPress Monitoring] Received report from ${server_ip} with ${sites.length} sites`);
56
57 res.json({
58 success: true,
59 message: 'Monitoring data received',
60 sites_updated: sites.length
61 });
62 } catch (error) {
63 console.error('[WordPress Monitoring] Error processing data:', error);
64 res.status(500).json({ error: 'Failed to process monitoring data' });
65 }
66});
67
68// Apply authentication and tenant context to all other routes
69router.use(authenticateToken, setTenantContext);
70
71/**
72 * @api {get} /api/wordpress/deployments/:id/progress Get deployment progress
73 * @apiName GetWordPressDeploymentProgress
74 * @apiGroup WordPress
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:
93 * HTTP/1.1 200 OK
94 * {
95 * "progress": {
96 * "id": 123,
97 * "site_name": "example-site",
98 * "status": "in_progress",
99 * "step": "database",
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"
103 * }
104 * }
105 */
106router.get('/deployments/:id/progress', async (req, res) => {
107 const { id } = req.params;
108 try {
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`,
112 [id]
113 );
114 if (result.rows.length === 0) {
115 return res.status(404).json({ error: 'Deployment not found' });
116 }
117 res.json({ progress: result.rows[0] });
118 } catch (error) {
119 res.status(500).json({ error: 'Failed to fetch deployment progress', details: error.message });
120 }
121});
122
123// Helper function to generate secure password
124/**
125 *
126 * @param length
127 */
128function generatePassword(length = 16) {
129 const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
130 let password = '';
131 const randomBytes = crypto.randomBytes(length);
132 for (let i = 0; i < length; i++) {
133 password += charset[randomBytes[i] % charset.length];
134 }
135 return password;
136}
137
138/**
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
143 */
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();
148 let output = '';
149
150 const sshKeyPath = process.env.WORDPRESS_SSH_KEY_PATH || '/root/.ssh/wordpress_hosting_rsa';
151
152 if (!fs.existsSync(sshKeyPath)) {
153 return reject(new Error('SSH key not found'));
154 }
155
156 const privateKey = fs.readFileSync(sshKeyPath);
157
158 conn.on('ready', () => {
159 conn.exec(command, (err, stream) => {
160 if (err) {
161 conn.end();
162 return reject(err);
163 }
164
165 stream.on('close', (code) => {
166 conn.end();
167 if (code !== 0) {
168 return reject(new Error(`Command exited with code ${code}`));
169 }
170 resolve(output);
171 }).on('data', (data) => {
172 output += data.toString();
173 }).stderr.on('data', (data) => {
174 console.error('[SSH] stderr:', data.toString());
175 });
176 });
177 }).on('error', (err) => {
178 reject(err);
179 }).connect({
180 host: serverIp,
181 port: 22,
182 username: 'root',
183 privateKey: privateKey
184 });
185 });
186}
187*/
188
189/**
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:
218 * HTTP/1.1 200 OK
219 * {
220 * "sites": [
221 * {
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",
227 * "region": "syd",
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"
238 * }
239 * ]
240 * }
241 */
242router.get('/sites', async (req, res) => {
243 try {
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.'
249 });
250 }
251
252 const result = await pool.query(
253 `SELECT
254 w.*,
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`,
260 [req.tenant.id]
261 );
262
263 res.json({ sites: result.rows });
264 } catch (error) {
265 console.error('[WordPress] Error fetching sites:', error);
266 res.status(500).json({ error: 'Failed to fetch WordPress sites', details: error.message });
267 }
268});
269
270/**
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
298 *
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" \
303 * -d '{
304 * "siteName": "example-site",
305 * "customerId": 123,
306 * "domain": "www.example.com"
307 * }'
308 *
309 * @apiSuccessExample {json} Success-Response:
310 * HTTP/1.1 200 OK
311 * {
312 * "success": true,
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,
318 * "credentials": {
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"
324 * }
325 * }
326 */
327router.post('/create', async (req, res) => {
328 const { siteName: siteNameRaw, customerId: customerIdRaw, domain: domainRaw } = req.body;
329
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;
334
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.'
340 });
341 }
342
343 // Validation
344 if (!siteName) {
345 return res.status(400).json({
346 error: 'Missing required field: siteName'
347 });
348 }
349
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'
354 });
355 }
356
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'
361 });
362 }
363
364 let deploymentId = null;
365 try {
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`,
370 [siteName]
371 );
372 deploymentId = depResult.rows[0].id;
373 const updateProgress = async (step, status, message, error = null) => {
374 await pool.query(
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]
377 );
378 };
379 // Get WordPress credentials from global settings
380 const settingsResult = await pool.query('SELECT config FROM app_settings ORDER BY id DESC LIMIT 1');
381
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).'
386 });
387 }
388
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;
399
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).'
405 });
406 }
407
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`,
411 [req.tenant.id]
412 );
413
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.'
418 });
419 }
420
421 const doToken = configResult.rows[0].api_token_encrypted;
422
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;
429
430 if (deploymentId) await updateProgress('credentials', 'in_progress', 'Generated WordPress credentials');
431
432 // Create MySQL database on DigitalOcean (using cluster ID from settings)
433 try {
434 if (deploymentId) await updateProgress('database', 'in_progress', `Creating database ${dbName}`);
435 await axios.post(
436 `https://api.digitalocean.com/v2/databases/${mysqlClusterId}/dbs`,
437 { name: dbName },
438 {
439 headers: {
440 'Authorization': `Bearer ${doToken}`,
441 'Content-Type': 'application/json'
442 }
443 }
444 );
445 if (deploymentId) await updateProgress('database', 'success', `Database ${dbName} created successfully`);
446 } catch (dbError) {
447 if (dbError.response?.status === 422) {
448 if (deploymentId) await updateProgress('database', 'success', `Database ${dbName} already exists, continuing...`);
449 } else {
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}`);
452 }
453 }
454
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');
458
459 // Get WordPress project from DigitalOcean
460 let projectId = null;
461 try {
462 const projectsResponse = await axios.get(
463 'https://api.digitalocean.com/v2/projects',
464 {
465 headers: {
466 'Authorization': `Bearer ${doToken}`,
467 'Content-Type': 'application/json'
468 }
469 }
470 );
471 const projects = projectsResponse.data.projects || [];
472 // Look for WordPress projects folder
473 const wordpressProject = projects.find(p =>
474 p.name.toLowerCase().includes('wordpress')
475 );
476 if (wordpressProject) {
477 projectId = wordpressProject.id;
478 if (deploymentId) await updateProgress('project', 'success', `Using project: ${wordpressProject.name}`);
479 } else {
480 if (deploymentId) await updateProgress('project', 'success', 'No WordPress project found, using default project');
481 }
482 } catch (error) {
483 if (deploymentId) await updateProgress('project', 'error', 'Could not fetch projects', error.message);
484 // Continue without project - app will be created in default project
485 }
486
487 // Create App Spec for WordPress using PHP buildpack (proven working method)
488 if (deploymentId) await updateProgress('app', 'in_progress', 'Creating App Platform app');
489 const appSpec = {
490 name: `wordpress-${siteName}`,
491 region: 'syd',
492 services: [
493 {
494 name: 'wordpress',
495 github: {
496 branch: 'main',
497 deploy_on_push: false,
498 repo: 'Independent-Business-Group/wordpress-template'
499 },
500 build_command: 'chmod +x .do/deploy.sh && ./.do/deploy.sh',
501 run_command: 'heroku-php-apache2',
502 environment_slug: 'php',
503 instance_count: 1,
504 instance_size_slug: 'apps-s-1vcpu-0.5gb',
505 http_port: 8080,
506 health_check: {
507 http_path: '/health.php',
508 port: 8080,
509 initial_delay_seconds: 60,
510 period_seconds: 10,
511 timeout_seconds: 3,
512 success_threshold: 1,
513 failure_threshold: 3
514 },
515 routes: [
516 {
517 path: '/'
518 }
519 ],
520 envs: [
521 {
522 key: 'DB_NAME',
523 scope: 'RUN_AND_BUILD_TIME',
524 type: 'SECRET',
525 value: dbName
526 },
527 {
528 key: 'DB_USER',
529 scope: 'RUN_AND_BUILD_TIME',
530 type: 'SECRET',
531 value: dbUser
532 },
533 {
534 key: 'DB_PASSWORD',
535 scope: 'RUN_AND_BUILD_TIME',
536 type: 'SECRET',
537 value: dbPassword
538 },
539 {
540 key: 'DB_HOST',
541 scope: 'RUN_AND_BUILD_TIME',
542 type: 'SECRET',
543 value: dbHost
544 },
545 {
546 key: 'DB_PORT',
547 scope: 'RUN_AND_BUILD_TIME',
548 value: String(dbPort)
549 },
550 {
551 key: 'TABLE_PREFIX',
552 scope: 'RUN_AND_BUILD_TIME',
553 value: 'wp_'
554 },
555 {
556 key: 'SPACES_BUCKET',
557 scope: 'RUN_AND_BUILD_TIME',
558 value: 'everydaytech-wordpress'
559 },
560 {
561 key: 'SPACES_FOLDER',
562 scope: 'RUN_AND_BUILD_TIME',
563 value: spacesPath
564 },
565 {
566 key: 'SPACES_KEY',
567 scope: 'RUN_AND_BUILD_TIME',
568 type: 'SECRET',
569 value: spacesKey
570 },
571 {
572 key: 'SPACES_SECRET',
573 scope: 'RUN_AND_BUILD_TIME',
574 type: 'SECRET',
575 value: spacesSecret
576 }
577 ]
578 }
579 ]
580 };
581
582 // Add custom domain if provided
583 if (domain) {
584 appSpec.domains = [
585 {
586 domain: domain,
587 type: 'PRIMARY'
588 }
589 ];
590 }
591
592 console.log('[WordPress] Creating app with spec:', JSON.stringify(appSpec, null, 2));
593
594 // Create app on DigitalOcean
595 const doResponse = await axios.post(
596 'https://api.digitalocean.com/v2/apps',
597 { spec: appSpec },
598 {
599 headers: {
600 'Authorization': `Bearer ${doToken}`,
601 'Content-Type': 'application/json'
602 }
603 }
604 );
605
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}`);
609
610 // Assign app to WordPress project if we found one
611 if (projectId) {
612 try {
613 await axios.post(
614 `https://api.digitalocean.com/v2/projects/${projectId}/resources`,
615 {
616 resources: [`do:app:${app.id}`]
617 },
618 {
619 headers: {
620 'Authorization': `Bearer ${doToken}`,
621 'Content-Type': 'application/json'
622 }
623 }
624 );
625 if (deploymentId) await updateProgress('project', 'success', `App assigned to project ${projectId}`);
626 console.log(`[WordPress] App assigned to project ${projectId}`);
627 } catch (error) {
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
631 }
632 }
633
634 // Save to database
635 await pool.query(
636 `INSERT INTO wordpress_sites (
637 tenant_id,
638 customer_id,
639 name,
640 app_id,
641 region,
642 status,
643 url,
644 domain,
645 db_name,
646 spaces_path,
647 wp_admin_user,
648 wp_admin_password,
649 wp_admin_email
650 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
651 [
652 req.tenant.id,
653 customerId || null,
654 siteName,
655 app.id,
656 'syd',
657 app.phase || 'deploying',
658 app.default_ingress || null,
659 domain || null,
660 dbName,
661 spacesPath,
662 wpAdminUser,
663 wpAdminPassword,
664 wpAdminEmail
665 ]
666 );
667 if (deploymentId) await updateProgress('completed', 'success', 'Deployment completed', null);
668
669 res.json({
670 success: true,
671 message: 'WordPress site created successfully',
672 appId: app.id,
673 url: app.default_ingress,
674 status: app.phase,
675 deploymentId,
676 credentials: {
677 adminUser: wpAdminUser,
678 adminPassword: wpAdminPassword,
679 adminEmail: wpAdminEmail,
680 database: dbName,
681 spacesPath: spacesPath
682 }
683 });
684 } catch (error) {
685 console.error('[WordPress] Error creating site:', error);
686
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',
693 details: doError
694 });
695 }
696
697 res.status(500).json({
698 error: error.message || 'Failed to create WordPress site'
699 });
700 }
701});
702
703/**
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"
731 *
732 * @apiSuccessExample {json} Success-Response:
733 * HTTP/1.1 200 OK
734 * {
735 * "site": {
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",
741 * "region": "syd",
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"
751 * }
752 * }
753 */
754router.get('/site/:appId', async (req, res) => {
755 try {
756 const { appId } = req.params;
757
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.'
763 });
764 }
765
766 // Get from database
767 const result = await pool.query(
768 `SELECT
769 w.*,
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]
775 );
776
777 if (result.rows.length === 0) {
778 return res.status(404).json({ error: 'WordPress site not found' });
779 }
780
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`,
784 [req.tenant.id]
785 );
786
787 if (configResult.rows.length > 0) {
788 const doToken = configResult.rows[0].api_token_encrypted;
789
790 try {
791 const doResponse = await axios.get(
792 `https://api.digitalocean.com/v2/apps/${appId}`,
793 {
794 headers: {
795 'Authorization': `Bearer ${doToken}`
796 }
797 }
798 );
799
800 const app = doResponse.data.app;
801
802 // Update database with latest info
803 await pool.query(
804 `UPDATE wordpress_sites
805 SET status = $1, url = $2, updated_at = CURRENT_TIMESTAMP
806 WHERE app_id = $3`,
807 [app.phase, app.default_ingress, appId]
808 );
809
810 result.rows[0].status = app.phase;
811 result.rows[0].url = app.default_ingress;
812 } catch (doError) {
813 console.error('[WordPress] Error fetching app from DO:', doError.message);
814 // Continue with database data if DO API fails
815 }
816 }
817
818 res.json({ site: result.rows[0] });
819 } catch (error) {
820 console.error('[WordPress] Error fetching site:', error);
821 res.status(500).json({ error: 'Failed to fetch WordPress site' });
822 }
823});
824
825/**
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
831 */
832router.get('/monitoring/latest', async (req, res) => {
833 try {
834 const result = await pool.query(`
835 SELECT
836 report_id,
837 server_ip,
838 server_hostname,
839 report_data,
840 received_at
841 FROM wordpress_monitoring_reports
842 WHERE received_at > NOW() - INTERVAL '24 hours'
843 ORDER BY received_at DESC
844 LIMIT 10
845 `);
846
847 const reports = result.rows.map(row => ({
848 ...row.report_data,
849 report_id: row.report_id,
850 received_at: row.received_at
851 }));
852
853 res.json({ reports });
854 } catch (error) {
855 console.error('[WordPress Monitoring] Error fetching reports:', error);
856 res.status(500).json({ error: 'Failed to fetch monitoring data' });
857 }
858});
859
860/**
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
870 */
871router.post('/sites/:domain/assign', async (req, res) => {
872 try {
873 const { domain } = req.params;
874 const { customerId, notes } = req.body;
875 const tenantId = req.user.tenantId;
876
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
882 notes = $3,
883 updated_at = CURRENT_TIMESTAMP
884 RETURNING site_id
885 `, [tenantId, domain, notes || null]);
886
887 const siteId = siteResult.rows[0].site_id;
888
889 // If customer_id provided, create or update customer association
890 if (customerId) {
891 // Update customer assignment (assuming we add a customer_id column)
892 await pool.query(`
893 UPDATE wordpress_sites
894 SET notes = $1,
895 updated_at = CURRENT_TIMESTAMP
896 WHERE site_id = $2
897 `, [notes || null, siteId]);
898
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
901 }
902
903 res.json({
904 success: true,
905 message: 'Site assignment updated successfully'
906 });
907 } catch (error) {
908 console.error('[WordPress] Error assigning site:', error);
909 res.status(500).json({ error: 'Failed to update site assignment' });
910 }
911});
912
913/**
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
920 */
921router.get('/sites/:domain', async (req, res) => {
922 try {
923 const { domain } = req.params;
924
925 // Get latest monitoring data for this site
926 const result = await pool.query(`
927 SELECT
928 report_id,
929 server_ip,
930 server_hostname,
931 report_data,
932 received_at
933 FROM wordpress_monitoring_reports
934 WHERE received_at > NOW() - INTERVAL '24 hours'
935 ORDER BY received_at DESC
936 LIMIT 1
937 `);
938
939 if (result.rows.length === 0) {
940 return res.status(404).json({ error: 'No recent monitoring data found' });
941 }
942
943 const report = result.rows[0];
944 const reportData = report.report_data;
945
946 // Find the specific site
947 const site = reportData.sites?.find(s => s.domain === domain);
948
949 if (!site) {
950 return res.status(404).json({ error: 'Site not found in monitoring data' });
951 }
952
953 // Get security scan data
954 const securityResult = await pool.query(`
955 SELECT
956 scan_id,
957 malware_issues,
958 php_in_uploads,
959 suspicious_files,
960 modified_core_files,
961 scan_output,
962 quarantined_files,
963 scan_date
964 FROM wordpress_security_scans
965 WHERE site_id IN (
966 SELECT site_id FROM wordpress_sites WHERE domain = $1
967 )
968 ORDER BY scan_date DESC
969 LIMIT 1
970 `, [domain]);
971
972 // Combine base data
973 const siteDetails = {
974 ...site,
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,
980 plugins: [],
981 themes: [],
982 core: null,
983 admin_users: []
984 };
985
986 // Fetch detailed information via WordPress API Bridge
987 try {
988 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
989 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
990
991 if (!wordpressApiKey) {
992 console.warn('[WordPress] WORDPRESS_API_KEY not configured, skipping detailed fetch');
993 } else {
994 console.log(`[WordPress] Fetching detailed info for ${domain} from API bridge`);
995
996 const apiUrl = `http://${report.server_ip}:${wordpressApiPort}/api/site/${domain}/details`;
997 const apiResponse = await axios.get(apiUrl, {
998 headers: {
999 'X-API-Key': wordpressApiKey
1000 },
1001 timeout: 15000
1002 });
1003
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;
1011 }
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;
1016 }
1017
1018 res.json({ site: siteDetails });
1019 } catch (error) {
1020 console.error('[WordPress] Error fetching site details:', error);
1021 res.status(500).json({ error: 'Failed to fetch site details' });
1022 }
1023});
1024
1025/**
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
1032 */
1033router.post('/sites/:domain/login', async (req, res) => {
1034 try {
1035 const { domain } = req.params;
1036
1037 // Get site info from latest monitoring data
1038 const result = await pool.query(`
1039 SELECT
1040 report_data
1041 FROM wordpress_monitoring_reports
1042 WHERE received_at > NOW() - INTERVAL '24 hours'
1043 ORDER BY received_at DESC
1044 LIMIT 1
1045 `);
1046
1047 if (result.rows.length === 0) {
1048 return res.status(404).json({ error: 'Site not found' });
1049 }
1050
1051 const reportData = result.rows[0].report_data;
1052 const site = reportData.sites?.find(s => s.domain === domain);
1053
1054 if (!site) {
1055 return res.status(404).json({ error: 'Site not found in monitoring data' });
1056 }
1057
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
1061
1062 // Store the token
1063 await pool.query(`
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
1067 sso_token = $3,
1068 sso_token_expires = $4,
1069 updated_at = CURRENT_TIMESTAMP
1070 `, [req.user.tenantId, domain, loginToken, expiresAt]);
1071
1072 // Return the login URL (will be handled by a separate auto-login endpoint)
1073 const loginUrl = `https://${domain}/wp-admin/?auto_login=${loginToken}`;
1074
1075 res.json({
1076 success: true,
1077 loginUrl,
1078 expiresAt,
1079 message: 'Auto-login URL generated (expires in 5 minutes)'
1080 });
1081 } catch (error) {
1082 console.error('[WordPress] Error generating login URL:', error);
1083 res.status(500).json({ error: 'Failed to generate login URL' });
1084 }
1085});
1086
1087/**
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
1094 */
1095router.get('/sites/:domain/users', async (req, res) => {
1096 try {
1097 const { domain } = req.params;
1098 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1099 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1100
1101 if (!wordpressApiKey) {
1102 return res.status(503).json({ error: 'WordPress API not configured' });
1103 }
1104
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
1111 `, [domain]);
1112
1113 if (result.rows.length === 0) {
1114 return res.status(404).json({ error: 'Site server not found' });
1115 }
1116
1117 const serverIp = result.rows[0].server_ip;
1118 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/users`;
1119
1120 const apiResponse = await axios.get(apiUrl, {
1121 headers: { 'X-API-Key': wordpressApiKey },
1122 timeout: 30000
1123 });
1124
1125 res.json(apiResponse.data);
1126 } catch (error) {
1127 console.error('[WordPress] Users fetch error:', error.message);
1128 res.status(500).json({ error: 'Failed to fetch users', message: error.message });
1129 }
1130});
1131
1132/**
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
1140 */
1141router.post('/sites/:domain/plugin/:plugin/update', async (req, res) => {
1142 try {
1143 const { domain, plugin } = req.params;
1144 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1145 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1146
1147 if (!wordpressApiKey) {
1148 return res.status(503).json({ error: 'WordPress API not configured' });
1149 }
1150
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
1157 `, [domain]);
1158
1159 if (result.rows.length === 0) {
1160 return res.status(404).json({ error: 'Site server not found' });
1161 }
1162
1163 const serverIp = result.rows[0].server_ip;
1164 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/plugin/${plugin}/update`;
1165
1166 const apiResponse = await axios.post(apiUrl, {}, {
1167 headers: { 'X-API-Key': wordpressApiKey },
1168 timeout: 30000
1169 });
1170
1171 res.json(apiResponse.data);
1172 } catch (error) {
1173 console.error('[WordPress] Plugin update error:', error.message);
1174 res.status(500).json({ error: 'Failed to update plugin', message: error.message });
1175 }
1176});
1177
1178/**
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
1186 */
1187router.post('/sites/:domain/theme/:theme/update', async (req, res) => {
1188 try {
1189 const { domain, theme } = req.params;
1190 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1191 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1192
1193 if (!wordpressApiKey) {
1194 return res.status(503).json({ error: 'WordPress API not configured' });
1195 }
1196
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
1203 `, [domain]);
1204
1205 if (result.rows.length === 0) {
1206 return res.status(404).json({ error: 'Site server not found' });
1207 }
1208
1209 const serverIp = result.rows[0].server_ip;
1210 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/theme/${theme}/update`;
1211
1212 const apiResponse = await axios.post(apiUrl, {}, {
1213 headers: { 'X-API-Key': wordpressApiKey },
1214 timeout: 30000
1215 });
1216
1217 res.json(apiResponse.data);
1218 } catch (error) {
1219 console.error('[WordPress] Theme update error:', error.message);
1220 res.status(500).json({ error: 'Failed to update theme', message: error.message });
1221 }
1222});
1223
1224/**
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
1232 */
1233router.post('/sites/:domain/core/update', async (req, res) => {
1234 try {
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';
1239
1240 if (!wordpressApiKey) {
1241 return res.status(503).json({ error: 'WordPress API not configured' });
1242 }
1243
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
1250 `, [domain]);
1251
1252 if (result.rows.length === 0) {
1253 return res.status(404).json({ error: 'Site server not found' });
1254 }
1255
1256 const serverIp = result.rows[0].server_ip;
1257 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/core/update`;
1258
1259 const apiResponse = await axios.post(apiUrl, { version }, {
1260 headers: { 'X-API-Key': wordpressApiKey },
1261 timeout: 300000 // 5 minutes for core updates
1262 });
1263
1264 res.json(apiResponse.data);
1265 } catch (error) {
1266 console.error('[WordPress] Core update error:', error.message);
1267 res.status(500).json({ error: 'Failed to update WordPress core', message: error.message });
1268 }
1269});
1270
1271/**
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
1278 */
1279router.post('/sites/:domain/clean', async (req, res) => {
1280 try {
1281 const { domain } = req.params;
1282 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1283 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1284
1285 if (!wordpressApiKey) {
1286 return res.status(503).json({ error: 'WordPress API not configured' });
1287 }
1288
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
1295 `, [domain]);
1296
1297 if (result.rows.length === 0) {
1298 return res.status(404).json({ error: 'Site server not found' });
1299 }
1300
1301 const serverIp = result.rows[0].server_ip;
1302 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/clean`;
1303
1304 const apiResponse = await axios.post(apiUrl, {}, {
1305 headers: { 'X-API-Key': wordpressApiKey },
1306 timeout: 60000
1307 });
1308
1309 res.json(apiResponse.data);
1310 } catch (error) {
1311 console.error('[WordPress] Cleanup error:', error.message);
1312 res.status(500).json({ error: 'Failed to clean threats', message: error.message });
1313 }
1314});
1315
1316/**
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
1323 */
1324router.get('/sites/:domain/details', async (req, res) => {
1325 try {
1326 const { domain } = req.params;
1327 const wordpressApiKey = process.env.WORDPRESS_API_KEY;
1328 const wordpressApiPort = process.env.WORDPRESS_API_PORT || '3100';
1329
1330 if (!wordpressApiKey) {
1331 return res.status(503).json({ error: 'WordPress API not configured' });
1332 }
1333
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
1340 `, [domain]);
1341
1342 if (result.rows.length === 0) {
1343 return res.status(404).json({ error: 'Site server not found' });
1344 }
1345
1346 const serverIp = result.rows[0].server_ip;
1347 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/details`;
1348
1349 const apiResponse = await axios.get(apiUrl, {
1350 headers: { 'X-API-Key': wordpressApiKey },
1351 timeout: 30000
1352 });
1353
1354 res.json(apiResponse.data);
1355 } catch (error) {
1356 console.error('[WordPress] Site details error:', error.message);
1357 res.status(500).json({ error: 'Failed to fetch site details', message: error.message });
1358 }
1359});
1360
1361/**
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
1369 */
1370router.post('/sites/:domain/autologin', async (req, res) => {
1371 try {
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';
1376
1377 if (!wordpressApiKey) {
1378 return res.status(503).json({ error: 'WordPress API not configured' });
1379 }
1380
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
1387 `, [domain]);
1388
1389 if (result.rows.length === 0) {
1390 return res.status(404).json({ error: 'Site server not found' });
1391 }
1392
1393 const serverIp = result.rows[0].server_ip;
1394 const apiUrl = `http://${serverIp}:${wordpressApiPort}/api/site/${domain}/autologin`;
1395
1396 const apiResponse = await axios.post(apiUrl, { username }, {
1397 headers: { 'X-API-Key': wordpressApiKey },
1398 timeout: 30000
1399 });
1400
1401 res.json(apiResponse.data);
1402 } catch (error) {
1403 console.error('[WordPress] Auto-login error:', error.message);
1404 res.status(500).json({ error: 'Failed to generate auto-login link', message: error.message });
1405 }
1406});
1407
1408module.exports = router;