underpost 3.1.3 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +0 -2
- package/.github/workflows/ghpkg.ci.yml +4 -4
- package/.github/workflows/npmpkg.ci.yml +28 -11
- package/.github/workflows/publish.ci.yml +6 -0
- package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
- package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
- package/.github/workflows/release.cd.yml +13 -8
- package/CHANGELOG.md +396 -1
- package/CLI-HELP.md +53 -6
- package/Dockerfile +4 -2
- package/README.md +3 -2
- package/bin/build.js +18 -12
- package/bin/deploy.js +177 -124
- package/bin/file.js +3 -0
- package/conf.js +3 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
- package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/nodemon.json +1 -1
- package/package.json +22 -15
- package/scripts/rhel-grpc-setup.sh +56 -0
- package/src/api/file/file.ref.json +18 -0
- package/src/api/user/user.service.js +8 -7
- package/src/cli/cluster.js +7 -7
- package/src/cli/db.js +726 -825
- package/src/cli/deploy.js +151 -93
- package/src/cli/env.js +19 -0
- package/src/cli/fs.js +5 -2
- package/src/cli/index.js +45 -2
- package/src/cli/kubectl.js +211 -0
- package/src/cli/release.js +284 -0
- package/src/cli/repository.js +434 -75
- package/src/cli/run.js +189 -34
- package/src/cli/secrets.js +73 -0
- package/src/cli/test.js +3 -3
- package/src/client/Default.index.js +3 -4
- package/src/client/components/core/AppStore.js +69 -0
- package/src/client/components/core/CalendarCore.js +2 -2
- package/src/client/components/core/DropDown.js +137 -17
- package/src/client/components/core/Keyboard.js +2 -2
- package/src/client/components/core/LogIn.js +2 -2
- package/src/client/components/core/LogOut.js +2 -2
- package/src/client/components/core/Modal.js +0 -1
- package/src/client/components/core/Panel.js +0 -1
- package/src/client/components/core/PanelForm.js +19 -19
- package/src/client/components/core/SocketIo.js +82 -29
- package/src/client/components/core/SocketIoHandler.js +75 -0
- package/src/client/components/core/Stream.js +143 -95
- package/src/client/components/core/Webhook.js +40 -7
- package/src/client/components/default/AppStoreDefault.js +5 -0
- package/src/client/components/default/LogInDefault.js +3 -3
- package/src/client/components/default/LogOutDefault.js +2 -2
- package/src/client/components/default/MenuDefault.js +5 -5
- package/src/client/components/default/SocketIoDefault.js +3 -51
- package/src/client/services/core/core.service.js +20 -8
- package/src/client/services/user/user.management.js +2 -2
- package/src/index.js +24 -1
- package/src/runtime/express/Dockerfile +4 -0
- package/src/runtime/express/Express.js +18 -1
- package/src/runtime/lampp/Dockerfile +13 -2
- package/src/runtime/lampp/Lampp.js +27 -4
- package/src/runtime/wp/Dockerfile +68 -0
- package/src/runtime/wp/Wp.js +639 -0
- package/src/server/auth.js +24 -1
- package/src/server/backup.js +57 -23
- package/src/server/client-build-docs.js +9 -2
- package/src/server/client-build.js +31 -31
- package/src/server/client-formatted.js +109 -57
- package/src/server/cron.js +23 -18
- package/src/server/ipfs-client.js +24 -1
- package/src/server/peer.js +8 -0
- package/src/server/runtime.js +25 -1
- package/src/server/start.js +3 -2
- package/src/ws/IoInterface.js +1 -10
- package/src/ws/IoServer.js +14 -33
- package/src/ws/core/channels/core.ws.chat.js +65 -20
- package/src/ws/core/channels/core.ws.mailer.js +113 -32
- package/src/ws/core/channels/core.ws.stream.js +90 -31
- package/src/ws/core/core.ws.connection.js +12 -33
- package/src/ws/core/core.ws.emit.js +10 -26
- package/src/ws/core/core.ws.server.js +25 -58
- package/src/ws/default/channels/default.ws.main.js +53 -12
- package/src/ws/default/default.ws.connection.js +26 -13
- package/src/ws/default/default.ws.server.js +30 -12
- package/src/client/components/default/ElementsDefault.js +0 -38
- package/src/ws/core/management/core.ws.chat.js +0 -8
- package/src/ws/core/management/core.ws.mailer.js +0 -16
- package/src/ws/core/management/core.ws.stream.js +0 -8
- package/src/ws/default/management/default.ws.main.js +0 -8
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress runtime for the Underpost engine.
|
|
3
|
+
* Manages WordPress installations backed by LAMPP (Apache + PHP 8+) and MariaDB.
|
|
4
|
+
*
|
|
5
|
+
* Two provisioning modes:
|
|
6
|
+
* - **clone** — `conf.repository` is set: clones that GitHub repo as the wp root.
|
|
7
|
+
* - **fresh** — no `conf.repository`: downloads wordpress.org/latest.zip, creates
|
|
8
|
+
* the database if it does not exist, and writes wp-config.php.
|
|
9
|
+
*
|
|
10
|
+
* @module src/runtime/wp/Wp.js
|
|
11
|
+
* @namespace WpService
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { shellExec } from '../../server/process.js';
|
|
17
|
+
import { loggerFactory } from '../../server/logger.js';
|
|
18
|
+
import { Lampp } from '../lampp/Lampp.js';
|
|
19
|
+
import Underpost from '../../index.js';
|
|
20
|
+
|
|
21
|
+
const logger = loggerFactory(import.meta);
|
|
22
|
+
|
|
23
|
+
const WP_DOWNLOAD_URL = 'https://wordpress.org/latest.zip';
|
|
24
|
+
const WP_ZIP_PATH = '/tmp/wordpress-latest.zip';
|
|
25
|
+
const WP_BASE_DIR = '/opt/lampp/htdocs/wp';
|
|
26
|
+
// XAMPP ships its own PHP binary under /opt/lampp/bin which is not on the default
|
|
27
|
+
// PATH for non-login shells spawned by shellExec. Prepend it to every wp-cli call.
|
|
28
|
+
const LAMPP_BIN = '/opt/lampp/bin';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @class WpService
|
|
32
|
+
* @description Manages WordPress provisioning and Apache virtual-host wiring
|
|
33
|
+
* on top of the LAMPP service. Each server conf entry with `runtime: 'wp'`
|
|
34
|
+
* is handled here.
|
|
35
|
+
* @memberof WpService
|
|
36
|
+
*/
|
|
37
|
+
class WpService {
|
|
38
|
+
/**
|
|
39
|
+
* Ensures the base WordPress hosting directory exists.
|
|
40
|
+
*/
|
|
41
|
+
static ensureBaseDir() {
|
|
42
|
+
if (!fs.existsSync(WP_BASE_DIR)) fs.mkdirSync(WP_BASE_DIR, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns the on-disk root path for a WordPress site.
|
|
47
|
+
* @param {string} host - Virtual-host name (used as folder name).
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
static siteDir(host) {
|
|
51
|
+
return path.join(WP_BASE_DIR, host);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ensures WP-CLI (`wp`) is available on PATH, downloading and installing it
|
|
56
|
+
* to `/usr/local/bin/wp` if it is not already present.
|
|
57
|
+
*/
|
|
58
|
+
static ensureWpCli() {
|
|
59
|
+
const existing = shellExec(`PATH="${LAMPP_BIN}:$PATH" which wp 2>/dev/null || true`, {
|
|
60
|
+
stdout: true,
|
|
61
|
+
silent: true,
|
|
62
|
+
disableLog: true,
|
|
63
|
+
});
|
|
64
|
+
if (existing && existing.trim()) return;
|
|
65
|
+
logger.info('WP-CLI not found — installing to /usr/local/bin/wp');
|
|
66
|
+
shellExec(`curl -sL -o /tmp/wp-cli.phar https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar`);
|
|
67
|
+
shellExec(`chmod +x /tmp/wp-cli.phar`);
|
|
68
|
+
shellExec(`sudo mv /tmp/wp-cli.phar /usr/local/bin/wp`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ensures a no-op sendmail stub exists at /usr/sbin/sendmail so WP plugins
|
|
73
|
+
* that invoke sendmail directly do not crash when a real MTA is absent.
|
|
74
|
+
* Mirrors the Dockerfile `RUN printf '#!/bin/sh\ncat > /dev/null\n' > /usr/sbin/sendmail`
|
|
75
|
+
* line but guards against older container images that pre-date that layer.
|
|
76
|
+
*/
|
|
77
|
+
static ensureSendmail() {
|
|
78
|
+
const sendmailPath = '/usr/sbin/sendmail';
|
|
79
|
+
const existing = shellExec(`test -x "${sendmailPath}" && echo ok || true`, {
|
|
80
|
+
stdout: true,
|
|
81
|
+
silent: true,
|
|
82
|
+
disableLog: true,
|
|
83
|
+
});
|
|
84
|
+
if (existing && existing.trim() === 'ok') return;
|
|
85
|
+
logger.info('sendmail stub missing — creating no-op at /usr/sbin/sendmail');
|
|
86
|
+
shellExec(
|
|
87
|
+
`printf '#!/bin/sh\\ncat > /dev/null\\n' | sudo tee ${sendmailPath} > /dev/null && sudo chmod +x ${sendmailPath}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Provisions a WordPress site and registers it with the LAMPP virtual-host router.
|
|
93
|
+
*
|
|
94
|
+
* Directory layout (e.g. host='test.nexodev.org', pathRoute='/wp'):
|
|
95
|
+
* - DocumentRoot (vhostDir): /opt/lampp/htdocs/wp/test.nexodev.org
|
|
96
|
+
* - WordPress files (wpDir): /opt/lampp/htdocs/wp/test.nexodev.org/wp
|
|
97
|
+
* - Root .htaccess rewrites / → /wp/ so Apache serves WordPress transparently.
|
|
98
|
+
*
|
|
99
|
+
* When pathRoute='/' the DocumentRoot and wpDir are the same directory.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} opts
|
|
102
|
+
* @param {number} opts.port - Apache VirtualHost listen port.
|
|
103
|
+
* @param {string} opts.host - Server name / virtual host.
|
|
104
|
+
* @param {string} opts.pathRoute - URL path (e.g. '/wp' or '/').
|
|
105
|
+
* @param {string|null} [opts.repository] - GitHub HTTPS clone URL. When set the
|
|
106
|
+
* site root is cloned instead of downloading WordPress.
|
|
107
|
+
* @param {object|null} [opts.db] - MariaDB connection config:
|
|
108
|
+
* `{ host, name, user, password }`.
|
|
109
|
+
* Required for fresh installs; ignored when
|
|
110
|
+
* `repository` is set (wp-config.php is assumed
|
|
111
|
+
* to be part of the repo).
|
|
112
|
+
* @param {object|null} [opts.wp] - WordPress install config:
|
|
113
|
+
* `{ title, adminUser, adminPassword, adminEmail }`.
|
|
114
|
+
* @param {boolean} [opts.redirect] - If true, enables Apache RewriteEngine redirection.
|
|
115
|
+
* @param {string} [opts.redirectTarget] - Target URL for the redirect rule.
|
|
116
|
+
* @param {boolean} [opts.resetRouter] - Clear LAMPP router before appending.
|
|
117
|
+
* @returns {{ disabled: boolean }}
|
|
118
|
+
*/
|
|
119
|
+
static createApp({ port, host, pathRoute, repository, db, wp, redirect, redirectTarget, resetRouter }) {
|
|
120
|
+
if (!Lampp.enabled()) {
|
|
121
|
+
logger.warn(`LAMPP not installed — skipping ${host}`);
|
|
122
|
+
return { disabled: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
WpService.ensureBaseDir();
|
|
126
|
+
|
|
127
|
+
// DocumentRoot for the Apache VirtualHost — always under WP_BASE_DIR/<host>
|
|
128
|
+
const vhostDir = WpService.siteDir(host);
|
|
129
|
+
|
|
130
|
+
// When pathRoute is a subdirectory (e.g. '/wp'), WordPress files live one level deeper.
|
|
131
|
+
// When pathRoute is '/' they share the same directory as the DocumentRoot.
|
|
132
|
+
const subDir = pathRoute && pathRoute !== '/' ? pathRoute.replace(/^\/+/, '').replace(/\/+$/, '') : '';
|
|
133
|
+
const wpDir = subDir ? path.join(vhostDir, subDir) : vhostDir;
|
|
134
|
+
|
|
135
|
+
let freshInstall = false;
|
|
136
|
+
if (repository) {
|
|
137
|
+
({ freshInstall } = WpService.provisionClone({ host, siteRoot: wpDir, repository, db, wp, subDir }));
|
|
138
|
+
} else {
|
|
139
|
+
({ freshInstall } = WpService.provisionFresh({ host, siteRoot: wpDir, db, wp, subDir }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Ensure git is initialized and linked to the backup repository.
|
|
143
|
+
// Mark the directory as safe before git operations — the site root is owned
|
|
144
|
+
// by daemon:daemon (Apache) and git 2.35+ refuses to run in directories owned
|
|
145
|
+
// by a different user unless explicitly declared safe.
|
|
146
|
+
if (repository) {
|
|
147
|
+
shellExec(`git config --global --add safe.directory "${wpDir}"`);
|
|
148
|
+
Underpost.repo.initLocalRepo({ path: wpDir, origin: repository });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Write a root .htaccess that rewrites / → /subDir/ when running in subdirectory mode
|
|
152
|
+
if (subDir) {
|
|
153
|
+
WpService.ensureSubdirHtaccess({ vhostDir, subDir });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Write security rules into the WordPress root .htaccess
|
|
157
|
+
WpService.ensureSecurityHtaccess({ dir: wpDir });
|
|
158
|
+
|
|
159
|
+
// Make the site writable by the XAMPP Apache process (runs as daemon:daemon).
|
|
160
|
+
// This is required for plugins like Wordfence WAF and Sucuri that write config/upload files.
|
|
161
|
+
shellExec(`sudo chown -R daemon:daemon "${vhostDir}"`);
|
|
162
|
+
shellExec(`sudo find "${vhostDir}" -type d -exec chmod 755 {} \\;`);
|
|
163
|
+
shellExec(`sudo find "${vhostDir}" -type f -exec chmod 644 {} \\;`);
|
|
164
|
+
|
|
165
|
+
// Wire up Apache VirtualHost via Lampp — DocumentRoot is always vhostDir;
|
|
166
|
+
// Lampp.createApp uses `directory` directly as the DocumentRoot.
|
|
167
|
+
const { disabled } = Lampp.createApp({
|
|
168
|
+
port,
|
|
169
|
+
host,
|
|
170
|
+
path: pathRoute,
|
|
171
|
+
directory: vhostDir,
|
|
172
|
+
redirect,
|
|
173
|
+
redirectTarget,
|
|
174
|
+
resetRouter,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Immediately commit and push all generated files (wp-config.php, .htaccess,
|
|
178
|
+
// security rules, plugins, etc.) so that on rollout/restart the clone will
|
|
179
|
+
// have a complete working state and won't fall back to fresh install again.
|
|
180
|
+
if (repository && freshInstall) {
|
|
181
|
+
WpService.persistToRepo({ siteRoot: wpDir, repository, host });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { disabled };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Clone mode — clones `repository` into `siteRoot` if not present, then
|
|
189
|
+
* verifies `wp-config.php` exists. If it is missing the site root is wiped
|
|
190
|
+
* and a fresh WordPress install is performed (requires `db` config).
|
|
191
|
+
* @param {object} opts
|
|
192
|
+
* @param {string} opts.host - Virtual-host name (for logging).
|
|
193
|
+
* @param {string} opts.siteRoot - Absolute path where the site should live.
|
|
194
|
+
* @param {string} opts.repository - HTTPS clone URL.
|
|
195
|
+
* @param {object|null} [opts.db] - MariaDB config used as fallback for fresh install.
|
|
196
|
+
*/
|
|
197
|
+
static provisionClone({ host, siteRoot, repository, db, wp, subDir = '' }) {
|
|
198
|
+
if (!process.env.GITHUB_TOKEN && repository && repository.startsWith('https://github.com/')) {
|
|
199
|
+
logger.warn(`${host}: GITHUB_TOKEN not set — git operations will fail for private repositories`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Step 0 — verify the remote repository is reachable; fall back to fresh install if not.
|
|
203
|
+
// repository.js isRemoteRepo handles token injection internally.
|
|
204
|
+
const repoAccessible = Underpost.repo.isRemoteRepo(repository);
|
|
205
|
+
logger.info(`${host}: remote accessible = ${repoAccessible} (${repository})`);
|
|
206
|
+
if (!repoAccessible) {
|
|
207
|
+
logger.warn(`${host}: remote repository not accessible (${repository}) — running fresh install`);
|
|
208
|
+
return WpService.provisionFresh({ host, siteRoot, db, wp, subDir });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Step 1 — clone if the directory does not exist yet
|
|
212
|
+
if (!fs.existsSync(siteRoot)) {
|
|
213
|
+
logger.info(`${host}: cloning ${repository} → ${siteRoot}`);
|
|
214
|
+
const cloneUrl = Underpost.repo.resolveAuthUrl(repository);
|
|
215
|
+
const tmp = `${siteRoot}.tmp`;
|
|
216
|
+
if (fs.existsSync(tmp)) shellExec(`sudo rm -rf "${tmp}"`);
|
|
217
|
+
shellExec(`git clone "${cloneUrl}" "${tmp}"`);
|
|
218
|
+
shellExec(`sudo mv "${tmp}" "${siteRoot}"`);
|
|
219
|
+
shellExec(`sudo chmod -R 755 "${siteRoot}"`);
|
|
220
|
+
shellExec(`sudo chown -R daemon:daemon "${siteRoot}"`);
|
|
221
|
+
} else {
|
|
222
|
+
logger.info(`${host}: repo already present at ${siteRoot}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Step 2 — verify wp-config.php; if missing, wipe and do a fresh install.
|
|
226
|
+
// initLocalRepo is NOT called here — createApp always calls it after provisionClone
|
|
227
|
+
// returns (whether clone or fallback path) so we avoid a double-init and ensure
|
|
228
|
+
// safe.directory is declared before git runs.
|
|
229
|
+
if (!fs.existsSync(path.join(siteRoot, 'wp-config.php'))) {
|
|
230
|
+
logger.warn(`${host}: wp-config.php not found — wiping site root and running fresh install`);
|
|
231
|
+
shellExec(`sudo rm -rf "${siteRoot}"`);
|
|
232
|
+
return WpService.provisionFresh({ host, siteRoot, db, wp, subDir });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { freshInstall: false };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Fresh-install mode — downloads wordpress.org/latest.zip, extracts it,
|
|
240
|
+
* creates the MariaDB database, and writes wp-config.php.
|
|
241
|
+
* @param {object} opts
|
|
242
|
+
* @param {string} opts.host - Virtual-host name (for logging).
|
|
243
|
+
* @param {string} opts.siteRoot - Absolute path where WordPress should live.
|
|
244
|
+
* @param {object|null} opts.db - `{ host, name, user, password }`.
|
|
245
|
+
*/
|
|
246
|
+
static provisionFresh({ host, siteRoot, db, wp, subDir = '' }) {
|
|
247
|
+
// Validator: wp-config.php presence means installation is complete/valid
|
|
248
|
+
if (fs.existsSync(path.join(siteRoot, 'wp-config.php'))) {
|
|
249
|
+
logger.info(`${host}: wp-config.php found at ${siteRoot}, skipping fresh install`);
|
|
250
|
+
return { freshInstall: false };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
logger.info(`${host}: fresh install → ${siteRoot}`);
|
|
254
|
+
|
|
255
|
+
// Download WordPress zip if not already cached
|
|
256
|
+
if (!fs.existsSync(WP_ZIP_PATH)) {
|
|
257
|
+
shellExec(`curl -Lo "${WP_ZIP_PATH}" "${WP_DOWNLOAD_URL}"`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Extract to /tmp/wordpress then move to siteRoot
|
|
261
|
+
const extractDir = '/tmp/wp-extract';
|
|
262
|
+
if (fs.existsSync(extractDir)) shellExec(`sudo rm -rf "${extractDir}"`);
|
|
263
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
264
|
+
shellExec(`unzip -q "${WP_ZIP_PATH}" -d "${extractDir}"`);
|
|
265
|
+
// The zip always extracts to /tmp/wp-extract/wordpress/
|
|
266
|
+
const extracted = path.join(extractDir, 'wordpress');
|
|
267
|
+
if (fs.existsSync(siteRoot)) shellExec(`sudo rm -rf "${siteRoot}"`);
|
|
268
|
+
// Ensure parent directory exists (e.g. /opt/lampp/htdocs/wp/<host>/)
|
|
269
|
+
const parentDir = path.dirname(siteRoot);
|
|
270
|
+
if (!fs.existsSync(parentDir)) fs.mkdirSync(parentDir, { recursive: true });
|
|
271
|
+
shellExec(`sudo mv "${extracted}" "${siteRoot}"`);
|
|
272
|
+
shellExec(`sudo chmod -R 755 "${siteRoot}"`);
|
|
273
|
+
shellExec(`sudo chown -R daemon:daemon "${siteRoot}"`);
|
|
274
|
+
|
|
275
|
+
if (db) {
|
|
276
|
+
WpService.createDatabase(db);
|
|
277
|
+
WpService.writeWpConfig({ siteRoot, db, host, subDir, wp });
|
|
278
|
+
WpService.wpCliInstall({ siteRoot, db, host, wp, subDir });
|
|
279
|
+
} else {
|
|
280
|
+
logger.warn(`${host}: no db config provided — wp-config.php not written`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { freshInstall: true };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Runs WP-CLI to complete the WordPress installation non-interactively,
|
|
288
|
+
* then installs and activates the Wordfence security plugin.
|
|
289
|
+
* Safe to call on an already-installed site — wp-cli will detect it and skip.
|
|
290
|
+
* @param {object} opts
|
|
291
|
+
* @param {string} opts.siteRoot - Absolute path to the WordPress installation.
|
|
292
|
+
* @param {object} opts.db - MariaDB config (unused here but kept for signature consistency).
|
|
293
|
+
* @param {string} opts.host - Virtual-host name.
|
|
294
|
+
* @param {object|null} [opts.wp] - WordPress install config from server conf:
|
|
295
|
+
* `{ title, adminUser, adminPassword, adminEmail }`.
|
|
296
|
+
* @param {string} [opts.subDir] - Subdirectory name (e.g. 'wp').
|
|
297
|
+
*/
|
|
298
|
+
static wpCliInstall({ siteRoot, db, host, wp, subDir = '' }) {
|
|
299
|
+
WpService.ensureWpCli();
|
|
300
|
+
WpService.ensureSendmail();
|
|
301
|
+
const siteUrl = subDir ? `https://${host}/${subDir}` : `https://${host}`;
|
|
302
|
+
const adminUser = (wp && wp.adminUser) || process.env.WP_ADMIN_USER || 'admin';
|
|
303
|
+
const adminPassword =
|
|
304
|
+
(wp && wp.adminPassword) ||
|
|
305
|
+
process.env.WP_ADMIN_PASSWORD ||
|
|
306
|
+
'ChangeMe_' + Math.random().toString(36).slice(2, 10);
|
|
307
|
+
const adminEmail = (wp && wp.adminEmail) || process.env.WP_ADMIN_EMAIL || `admin@${host}`;
|
|
308
|
+
const siteTitle = (wp && wp.title) || process.env.WP_SITE_TITLE || host;
|
|
309
|
+
// Prepend XAMPP's bin dir so WP-CLI can find the bundled PHP binary.
|
|
310
|
+
const wpCli = (cmd) =>
|
|
311
|
+
shellExec(`PATH="${LAMPP_BIN}:$PATH" wp --allow-root --path="${siteRoot}" ${cmd}`, {
|
|
312
|
+
stdout: true,
|
|
313
|
+
silent: false,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Step 1 — install WordPress core (skipped automatically by WP-CLI if already installed)
|
|
317
|
+
logger.info(`${host}: running wp core install`);
|
|
318
|
+
wpCli(
|
|
319
|
+
`core install` +
|
|
320
|
+
` --url="${siteUrl}"` +
|
|
321
|
+
` --title="${siteTitle}"` +
|
|
322
|
+
` --admin_user="${adminUser}"` +
|
|
323
|
+
` --admin_password="${adminPassword}"` +
|
|
324
|
+
` --admin_email="${adminEmail}"` +
|
|
325
|
+
` --skip-email`,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Step 2 — install and activate Wordfence Security
|
|
329
|
+
logger.info(`${host}: installing Wordfence security plugin`);
|
|
330
|
+
wpCli(`plugin install wordfence --activate`);
|
|
331
|
+
wpCli(`plugin install all-in-one-wp-security-and-firewall --activate`);
|
|
332
|
+
wpCli(`plugin install sucuri-scanner --activate`);
|
|
333
|
+
wpCli(`plugin install cleantalk-spam-protect --activate`);
|
|
334
|
+
|
|
335
|
+
// Step 3 — enable auto-updates for the plugin
|
|
336
|
+
wpCli(`plugin auto-updates enable wordfence`);
|
|
337
|
+
wpCli(`plugin auto-updates enable all-in-one-wp-security-and-firewall`);
|
|
338
|
+
wpCli(`plugin auto-updates enable sucuri-scanner`);
|
|
339
|
+
wpCli(`plugin auto-updates enable cleantalk-spam-protect`);
|
|
340
|
+
|
|
341
|
+
// Step 4 — install and activate WP Mail SMTP when configured
|
|
342
|
+
if (wp && wp.wpMailSmtp) {
|
|
343
|
+
logger.info(`${host}: installing WP Mail SMTP plugin`);
|
|
344
|
+
wpCli(`plugin install wp-mail-smtp --activate`);
|
|
345
|
+
wpCli(`plugin auto-updates enable wp-mail-smtp`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
logger.info(`${host}: WP-CLI provisioning complete`, { siteUrl, adminUser, adminEmail });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Appends rewrite rules for a WordPress subdirectory to the root .htaccess.
|
|
353
|
+
* Each subdirectory gets its own scoped RewriteRule block so multiple
|
|
354
|
+
* WordPress installs under the same host do not overwrite each other.
|
|
355
|
+
* @param {{ vhostDir: string, subDir: string }} opts
|
|
356
|
+
*/
|
|
357
|
+
static ensureSubdirHtaccess({ vhostDir, subDir }) {
|
|
358
|
+
if (!fs.existsSync(vhostDir)) fs.mkdirSync(vhostDir, { recursive: true });
|
|
359
|
+
const htaccessPath = path.join(vhostDir, '.htaccess');
|
|
360
|
+
|
|
361
|
+
// Marker comments to identify each subDir block
|
|
362
|
+
const marker = `# -- wp-subdir: ${subDir} --`;
|
|
363
|
+
const block = `${marker}
|
|
364
|
+
RewriteCond %{REQUEST_URI} ^\\/${subDir}(\\/|$) [NC]
|
|
365
|
+
RewriteCond %{REQUEST_FILENAME} !-f
|
|
366
|
+
RewriteCond %{REQUEST_FILENAME} !-d
|
|
367
|
+
RewriteRule ^${subDir}\\/?(.*)?$ \\/${subDir}\\/index.php [L]
|
|
368
|
+
${marker} end`;
|
|
369
|
+
|
|
370
|
+
let existing = '';
|
|
371
|
+
if (fs.existsSync(htaccessPath)) {
|
|
372
|
+
existing = fs.readFileSync(htaccessPath, 'utf8');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// If this subDir block already exists, replace it; otherwise append
|
|
376
|
+
const markerRegex = new RegExp(
|
|
377
|
+
`${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} end`,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
if (markerRegex.test(existing)) {
|
|
381
|
+
existing = existing.replace(markerRegex, block);
|
|
382
|
+
} else {
|
|
383
|
+
// Ensure the RewriteEngine directive and IfModule wrapper exist
|
|
384
|
+
if (!existing.includes('RewriteEngine on')) {
|
|
385
|
+
existing = `<IfModule mod_rewrite.c>\nRewriteEngine on\n\n</IfModule>\n`;
|
|
386
|
+
}
|
|
387
|
+
// Insert the new block before the closing </IfModule>
|
|
388
|
+
existing = existing.replace('</IfModule>', `${block}\n</IfModule>`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
fs.writeFileSync(htaccessPath, existing, 'utf8');
|
|
392
|
+
logger.info(`subdirectory .htaccess updated`, { vhostDir, subDir });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Writes security rules into the WordPress site root `.htaccess`.
|
|
397
|
+
* Protects `.git` directories, sensitive config files, and SQL dumps
|
|
398
|
+
* from being served by Apache. Idempotent — uses marker comments to
|
|
399
|
+
* detect and replace existing blocks on re-runs.
|
|
400
|
+
* @param {{ dir: string }} opts
|
|
401
|
+
* @param {string} opts.dir - Absolute path to the WordPress root (where .htaccess lives).
|
|
402
|
+
*/
|
|
403
|
+
static ensureSecurityHtaccess({ dir }) {
|
|
404
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
405
|
+
const htaccessPath = path.join(dir, '.htaccess');
|
|
406
|
+
|
|
407
|
+
const marker = '# -- wp-security --';
|
|
408
|
+
const block = `${marker}
|
|
409
|
+
# Block access to .git directories and files
|
|
410
|
+
RedirectMatch 404 /\\.git
|
|
411
|
+
|
|
412
|
+
# Block access to sensitive dotfiles
|
|
413
|
+
<FilesMatch "^\\.(env|htpasswd|htaccess\\.bak|DS_Store)">
|
|
414
|
+
Require all denied
|
|
415
|
+
</FilesMatch>
|
|
416
|
+
|
|
417
|
+
# Block access to WordPress config backups and SQL dumps
|
|
418
|
+
<FilesMatch "(wp-config\\.php\\.bak|wp-config-sample\\.php|\\.sql|\\.sql\\.gz)$">
|
|
419
|
+
Require all denied
|
|
420
|
+
</FilesMatch>
|
|
421
|
+
|
|
422
|
+
# Block direct access to PHP files in uploads
|
|
423
|
+
<IfModule mod_rewrite.c>
|
|
424
|
+
RewriteEngine On
|
|
425
|
+
RewriteRule ^wp-content/uploads/.*\\.php$ - [F,L]
|
|
426
|
+
</IfModule>
|
|
427
|
+
|
|
428
|
+
# Block access to xmlrpc.php (common attack vector)
|
|
429
|
+
<Files "xmlrpc.php">
|
|
430
|
+
Require all denied
|
|
431
|
+
</Files>
|
|
432
|
+
|
|
433
|
+
# Block access to readme.html and license.txt (version disclosure)
|
|
434
|
+
<FilesMatch "^(readme\\.html|license\\.txt)$">
|
|
435
|
+
Require all denied
|
|
436
|
+
</FilesMatch>
|
|
437
|
+
${marker} end`;
|
|
438
|
+
|
|
439
|
+
let existing = '';
|
|
440
|
+
if (fs.existsSync(htaccessPath)) {
|
|
441
|
+
existing = fs.readFileSync(htaccessPath, 'utf8');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const markerRegex = new RegExp(
|
|
445
|
+
`${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} end`,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
if (markerRegex.test(existing)) {
|
|
449
|
+
existing = existing.replace(markerRegex, block);
|
|
450
|
+
} else {
|
|
451
|
+
existing = existing ? `${existing}\n${block}\n` : `${block}\n`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fs.writeFileSync(htaccessPath, existing, 'utf8');
|
|
455
|
+
logger.info(`security .htaccess updated`, { dir });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Ensures a WordPress-specific `.gitignore` exists in the site root so that
|
|
460
|
+
* large/transient files are excluded from the backup repository while
|
|
461
|
+
* wp-config.php and security .htaccess ARE tracked.
|
|
462
|
+
* Idempotent — only writes when the file is missing.
|
|
463
|
+
* @param {{ dir: string }} opts
|
|
464
|
+
*/
|
|
465
|
+
static ensureGitignore({ dir }) {
|
|
466
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
467
|
+
if (fs.existsSync(gitignorePath)) return;
|
|
468
|
+
const content = `# WordPress .gitignore
|
|
469
|
+
# Cache and temp
|
|
470
|
+
wp-content/cache/
|
|
471
|
+
wp-content/upgrade/
|
|
472
|
+
wp-content/backup-db/
|
|
473
|
+
wp-content/backups/
|
|
474
|
+
wp-content/blogs.dir/
|
|
475
|
+
wp-content/advanced-cache.php
|
|
476
|
+
wp-content/wp-cache-config.php
|
|
477
|
+
wp-content/debug.log
|
|
478
|
+
|
|
479
|
+
# OS / editor
|
|
480
|
+
.DS_Store
|
|
481
|
+
Thumbs.db
|
|
482
|
+
*.swp
|
|
483
|
+
*.swo
|
|
484
|
+
*~
|
|
485
|
+
`;
|
|
486
|
+
fs.writeFileSync(gitignorePath, content, 'utf8');
|
|
487
|
+
logger.info(`.gitignore written`, { dir });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Commits all files in the WordPress site root and pushes to the remote
|
|
492
|
+
* repository. This persists wp-config.php, .htaccess security rules,
|
|
493
|
+
* installed plugins, and theme files so that on pod rollout/restart a
|
|
494
|
+
* `git clone` yields a fully working site without needing a fresh install.
|
|
495
|
+
*
|
|
496
|
+
* Safe to call repeatedly — `git commit` is a no-op when the working tree
|
|
497
|
+
* is clean (`|| true` prevents non-zero exit).
|
|
498
|
+
*
|
|
499
|
+
* @param {object} opts
|
|
500
|
+
* @param {string} opts.siteRoot - Absolute path to the WordPress root.
|
|
501
|
+
* @param {string} opts.repository - Git remote URL.
|
|
502
|
+
* @param {string} opts.host - Virtual-host name (for logging/commit msg).
|
|
503
|
+
*/
|
|
504
|
+
static persistToRepo({ siteRoot, repository, host }) {
|
|
505
|
+
if (!fs.existsSync(path.join(siteRoot, '.git'))) {
|
|
506
|
+
logger.warn(`persistToRepo: .git missing at ${siteRoot} — skipping`);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
WpService.ensureGitignore({ dir: siteRoot });
|
|
511
|
+
|
|
512
|
+
const githubOrg = process.env.GITHUB_USERNAME || 'underpostnet';
|
|
513
|
+
const repoName = repository.split('/').pop().split('.')[0];
|
|
514
|
+
|
|
515
|
+
logger.info(`${host}: persisting site to repository`);
|
|
516
|
+
shellExec(
|
|
517
|
+
`cd "${siteRoot}" && git add -A && git commit -m "wp provision ${host} $(date -u +%Y-%m-%dT%H:%M:%SZ)" || true`,
|
|
518
|
+
);
|
|
519
|
+
shellExec(`cd "${siteRoot}" && underpost push . ${githubOrg}/${repoName} -f`);
|
|
520
|
+
logger.info(`${host}: initial commit pushed to ${githubOrg}/${repoName}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Drops and recreates a MariaDB database to ensure a clean state for fresh installs.
|
|
525
|
+
* @param {{ host: string, name: string, user: string, password: string }} db
|
|
526
|
+
*/
|
|
527
|
+
static createDatabase({ host, name, user, password }) {
|
|
528
|
+
logger.info(`Dropping and recreating database "${name}" on ${host}`);
|
|
529
|
+
const mysql = `/opt/lampp/bin/mysql`;
|
|
530
|
+
const q = (s) => s.replace(/'/g, "\\'");
|
|
531
|
+
const exec = (sql) => shellExec(`${mysql} -h '${host}' -u '${q(user)}' -p'${q(password)}' -e "${sql}"`);
|
|
532
|
+
exec(`DROP DATABASE IF EXISTS \\\`${q(name)}\\\``);
|
|
533
|
+
exec(`CREATE DATABASE \\\`${q(name)}\\\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Writes a minimal `wp-config.php` from `wp-config-sample.php`.
|
|
538
|
+
* When `wp.wpMailSmtp` is provided, injects WP Mail SMTP plugin constants
|
|
539
|
+
* (WPMS_ON, WPMS_SMTP_HOST, etc.) so the plugin is pre-configured on first boot.
|
|
540
|
+
* @param {{ siteRoot: string, db: { host: string, name: string, user: string, password: string }, host?: string, subDir?: string, wp?: object }} opts
|
|
541
|
+
*/
|
|
542
|
+
static writeWpConfig({ siteRoot, db, host = '', subDir = '', wp }) {
|
|
543
|
+
const sample = path.join(siteRoot, 'wp-config-sample.php');
|
|
544
|
+
const target = path.join(siteRoot, 'wp-config.php');
|
|
545
|
+
if (!fs.existsSync(sample)) {
|
|
546
|
+
logger.warn(`wp-config-sample.php not found at ${siteRoot}`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
let cfg = fs.readFileSync(sample, 'utf8');
|
|
550
|
+
cfg = cfg
|
|
551
|
+
.replace("define( 'DB_NAME', 'database_name_here' );", `define( 'DB_NAME', '${db.name}' );`)
|
|
552
|
+
.replace("define( 'DB_USER', 'username_here' );", `define( 'DB_USER', '${db.user}' );`)
|
|
553
|
+
.replace("define( 'DB_PASSWORD', 'password_here' );", `define( 'DB_PASSWORD', '${db.password}' );`)
|
|
554
|
+
.replace("define( 'DB_HOST', 'localhost' );", `define( 'DB_HOST', '${db.host}' );`)
|
|
555
|
+
.replace("define( 'DB_CHARSET', 'utf8' );", `define( 'DB_CHARSET', 'utf8mb4' );`);
|
|
556
|
+
// When WordPress is installed in a subdirectory, WP_HOME and WP_SITEURL must be set
|
|
557
|
+
if (host && subDir) {
|
|
558
|
+
const wpSiteUrl = `https://${host}/${subDir}`;
|
|
559
|
+
cfg = cfg.replace(
|
|
560
|
+
'/** Absolute path to the WordPress directory. */',
|
|
561
|
+
`define( 'WP_HOME', '${wpSiteUrl}' );\ndefine( 'WP_SITEURL', '${wpSiteUrl}' );\n\n/** Absolute path to the WordPress directory. */`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
// Inject reverse-proxy HTTPS detection (needed behind Contour/envoy)
|
|
565
|
+
const httpsSnippet = `
|
|
566
|
+
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
|
|
567
|
+
$_SERVER['HTTPS'] = 'on';
|
|
568
|
+
}
|
|
569
|
+
`;
|
|
570
|
+
cfg = cfg.replace('<?php', `<?php\n${httpsSnippet}`);
|
|
571
|
+
|
|
572
|
+
// Inject WP Mail SMTP constants when wpMailSmtp config is provided
|
|
573
|
+
const wpMailSmtp = wp && wp.wpMailSmtp;
|
|
574
|
+
if (wpMailSmtp) {
|
|
575
|
+
const smtp = wpMailSmtp.smtp || {};
|
|
576
|
+
const wpmsLines = [
|
|
577
|
+
`define( 'WPMS_ON', true );`,
|
|
578
|
+
wpMailSmtp.fromEmail ? `define( 'WPMS_MAIL_FROM', '${wpMailSmtp.fromEmail}' );` : null,
|
|
579
|
+
wpMailSmtp.fromName ? `define( 'WPMS_MAIL_FROM_NAME', '${wpMailSmtp.fromName}' );` : null,
|
|
580
|
+
`define( 'WPMS_MAIL_FROM_FORCE', true );`,
|
|
581
|
+
`define( 'WPMS_MAIL_FROM_NAME_FORCE', false );`,
|
|
582
|
+
wpMailSmtp.mailer ? `define( 'WPMS_MAILER', '${wpMailSmtp.mailer}' );` : null,
|
|
583
|
+
wpMailSmtp.returnPath !== undefined ? `define( 'WPMS_SET_RETURN_PATH', ${wpMailSmtp.returnPath} );` : null,
|
|
584
|
+
smtp.host ? `define( 'WPMS_SMTP_HOST', '${smtp.host}' );` : null,
|
|
585
|
+
smtp.port ? `define( 'WPMS_SMTP_PORT', ${smtp.port} );` : null,
|
|
586
|
+
smtp.encryption ? `define( 'WPMS_SSL', '${smtp.encryption}' );` : null,
|
|
587
|
+
smtp.auth !== undefined ? `define( 'WPMS_SMTP_AUTH', ${smtp.auth} );` : null,
|
|
588
|
+
`define( 'WPMS_SMTP_AUTOTLS', true );`,
|
|
589
|
+
smtp.user ? `define( 'WPMS_SMTP_USER', '${smtp.user}' );` : null,
|
|
590
|
+
smtp.pass ? `define( 'WPMS_SMTP_PASS', '${smtp.pass}' );` : null,
|
|
591
|
+
]
|
|
592
|
+
.filter(Boolean)
|
|
593
|
+
.join('\n');
|
|
594
|
+
cfg = cfg.replace(
|
|
595
|
+
'/** Absolute path to the WordPress directory. */',
|
|
596
|
+
`// WP Mail SMTP plugin constants\n${wpmsLines}\n\n/** Absolute path to the WordPress directory. */`,
|
|
597
|
+
);
|
|
598
|
+
logger.info(`${host}: WP Mail SMTP constants injected into wp-config.php`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
fs.writeFileSync(target, cfg, 'utf8');
|
|
602
|
+
logger.info(`wp-config.php written for ${db.name}`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Backs up a WordPress site: commits all changes and pushes the
|
|
607
|
+
* site directory to its git remote.
|
|
608
|
+
* If no `.git` directory exists but `repository` is provided, git is
|
|
609
|
+
* initialized first so subsequent cron-triggered backups can push.
|
|
610
|
+
* @param {object} opts
|
|
611
|
+
* @param {string} opts.host - Virtual-host name.
|
|
612
|
+
* @param {string|null} [opts.repository] - Git remote URL; used to initialize git if missing.
|
|
613
|
+
*/
|
|
614
|
+
static backup({ host, repository }) {
|
|
615
|
+
const siteRoot = WpService.siteDir(host);
|
|
616
|
+
const githubOrg = process.env.GITHUB_USERNAME || 'underpostnet';
|
|
617
|
+
if (!fs.existsSync(siteRoot)) {
|
|
618
|
+
logger.warn(`backup: site root does not exist — ${siteRoot}`);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
logger.info(`backup: ${host}`);
|
|
622
|
+
|
|
623
|
+
// Ensure git is initialized when a repository is configured
|
|
624
|
+
if (repository && !fs.existsSync(path.join(siteRoot, '.git'))) {
|
|
625
|
+
Underpost.repo.initLocalRepo({ path: siteRoot, origin: repository });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// MariaDB export is handled by the shared db.js backup flow — no duplicate dump here.
|
|
629
|
+
if (fs.existsSync(path.join(siteRoot, '.git'))) {
|
|
630
|
+
shellExec(`cd "${siteRoot}" && git add -A && git commit -m "wp backup $(date -u +%Y-%m-%dT%H:%M:%SZ)" || true`);
|
|
631
|
+
shellExec(`cd "${siteRoot}" && underpost push . ${githubOrg}/${repository.split('/').pop().split('.')[0]}`);
|
|
632
|
+
logger.info(`backup: git push done for ${siteRoot}`);
|
|
633
|
+
} else {
|
|
634
|
+
logger.warn(`backup: no .git and no repository configured for ${host} — skipping git push`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export { WpService };
|
package/src/server/auth.js
CHANGED
|
@@ -9,7 +9,12 @@ import { loggerFactory } from './logger.js';
|
|
|
9
9
|
import crypto from 'crypto';
|
|
10
10
|
import { promisify } from 'util';
|
|
11
11
|
import { UserDto } from '../api/user/user.model.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
commonAdminGuard,
|
|
14
|
+
commonModeratorGuard,
|
|
15
|
+
commonUserGuard,
|
|
16
|
+
validatePassword,
|
|
17
|
+
} from '../client/components/core/CommonJs.js';
|
|
13
18
|
import helmet from 'helmet';
|
|
14
19
|
import rateLimit from 'express-rate-limit';
|
|
15
20
|
import slowDown from 'express-slow-down';
|
|
@@ -303,6 +308,23 @@ const moderatorGuard = (req, res, next) => {
|
|
|
303
308
|
return res.status(400).json({ status: 'error', message: 'bad request' });
|
|
304
309
|
}
|
|
305
310
|
};
|
|
311
|
+
/**
|
|
312
|
+
* Express middleware to guard routes for authenticated users (any non-guest role).
|
|
313
|
+
* @param {import('express').Request} req The Express request object.
|
|
314
|
+
* @param {import('express').Response} res The Express response object.
|
|
315
|
+
* @param {import('express').NextFunction} next The next middleware function.
|
|
316
|
+
* @memberof Auth
|
|
317
|
+
*/
|
|
318
|
+
const userGuard = (req, res, next) => {
|
|
319
|
+
try {
|
|
320
|
+
if (!req.auth || !commonUserGuard(req.auth.user.role))
|
|
321
|
+
return res.status(403).json({ status: 'error', message: 'Insufficient permission' });
|
|
322
|
+
return next();
|
|
323
|
+
} catch (err) {
|
|
324
|
+
logger.error(err);
|
|
325
|
+
return res.status(400).json({ status: 'error', message: 'bad request' });
|
|
326
|
+
}
|
|
327
|
+
};
|
|
306
328
|
|
|
307
329
|
// ---------- Password validation middleware (server-side) ----------
|
|
308
330
|
/**
|
|
@@ -678,6 +700,7 @@ export {
|
|
|
678
700
|
jwtVerify as verifyJWT,
|
|
679
701
|
adminGuard,
|
|
680
702
|
moderatorGuard,
|
|
703
|
+
userGuard,
|
|
681
704
|
validatePasswordMiddleware,
|
|
682
705
|
getBearerToken,
|
|
683
706
|
createSessionAndUserToken,
|