radix-cms 0.1.0 → 1.0.0
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 +17 -0
- package/CHANGELOG.md +10 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +197 -0
- package/NOTICE +3 -0
- package/README.md +126 -56
- package/SECURITY.md +22 -0
- package/dist/app.js +80 -0
- package/dist/config/db.js +66 -0
- package/dist/init/dbInit.js +132 -0
- package/dist/init/dbInit2.js +234 -0
- package/dist/middleware/auth.js +48 -0
- package/dist/plugins/my-seo-plugin/index.js +29 -0
- package/dist/routes/admin.js +808 -0
- package/dist/routes/index.js +218 -0
- package/dist/sql/radix.sql +118 -0
- package/dist/types/plugin.js +10 -0
- package/dist/utils/PluginManager.js +112 -0
- package/dist/utils/settings.js +80 -0
- package/dist/utils/themeScanner.js +33 -0
- package/dist/utils/versionCheck.js +74 -0
- package/docs/CMS_CODE_GUIDE.md +372 -0
- package/docs/CMS_USER_GUIDE.md +344 -0
- package/docs/Publishing Guidelines.txt +14 -0
- package/package.json +62 -6
- package/public/css/admin.css +178 -0
- package/public/css/fallback.css +53 -0
- package/public/hello.html +1 -0
- package/views/admin/create-page.ejs +82 -0
- package/views/admin/dashboard.ejs +44 -0
- package/views/admin/edit-page.ejs +87 -0
- package/views/admin/edit-user.ejs +45 -0
- package/views/admin/login.ejs +39 -0
- package/views/admin/media.ejs +79 -0
- package/views/admin/pages.ejs +82 -0
- package/views/admin/partials/footer.ejs +4 -0
- package/views/admin/partials/header.ejs +57 -0
- package/views/admin/settings.ejs +91 -0
- package/views/admin/setup-admin.ejs +69 -0
- package/views/admin/setup-db.ejs +112 -0
- package/views/admin/themes.ejs +24 -0
- package/views/admin/updates.ejs +79 -0
- package/views/admin/users.ejs +107 -0
- package/views/defaults/404.ejs +20 -0
- package/views/defaults/500.ejs +20 -0
- package/views/layouts/admin-layout.ejs +18 -0
- package/views/pages/gallery.ejs +5 -0
- package/views/pages/hello.ejs +4 -0
- package/views/pages/services.ejs +3 -0
- package/views/themes/default/assets/css/style.css +199 -0
- package/views/themes/default/assets/js/main.js +8 -0
- package/views/themes/default/index.ejs +55 -0
- package/views/themes/landing/assets/css/style.css +6 -0
- package/views/themes/landing/assets/js/main.js +8 -0
- package/views/themes/landing/index.ejs +26 -0
- package/views/themes/test/default.ejs +0 -0
- package/views/themes/test/full-width.ejs +0 -0
- package/views/themes/test/landing-page.ejs +0 -0
- package/index.js +0 -15
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**************************************************************************************************
|
|
3
|
+
* RadixCMS
|
|
4
|
+
*
|
|
5
|
+
* DESCRIPTION: administration routes, setup, content, users, media, and settings.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (C) 2026 Ultra Spark Software <salve@ultraspark.net>
|
|
8
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
9
|
+
**************************************************************************************************/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
const express_1 = require("express");
|
|
15
|
+
const bcrypt_1 = __importDefault(require("bcrypt"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const multer_1 = __importDefault(require("multer"));
|
|
19
|
+
const db_1 = __importDefault(require("../config/db"));
|
|
20
|
+
const themeScanner_1 = require("../utils/themeScanner");
|
|
21
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
22
|
+
const promise_1 = __importDefault(require("mysql2/promise"));
|
|
23
|
+
const dbInit2_1 = require("../init/dbInit2");
|
|
24
|
+
const nodemailer_1 = __importDefault(require("nodemailer"));
|
|
25
|
+
const versionCheck_1 = require("../utils/versionCheck");
|
|
26
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
27
|
+
const router = (0, express_1.Router)();
|
|
28
|
+
function updateEnvVersion(version) {
|
|
29
|
+
if (!version)
|
|
30
|
+
return;
|
|
31
|
+
const envPath = path_1.default.join(process.cwd(), '.env');
|
|
32
|
+
let envContent = fs_1.default.existsSync(envPath) ? fs_1.default.readFileSync(envPath, 'utf8') : '';
|
|
33
|
+
const re = /^APP_VERSION=.*$/m;
|
|
34
|
+
if (re.test(envContent)) {
|
|
35
|
+
envContent = envContent.replace(re, `APP_VERSION=${version}`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
envContent += (envContent ? '\n' : '') + `APP_VERSION=${version}`;
|
|
39
|
+
}
|
|
40
|
+
fs_1.default.writeFileSync(envPath, envContent, { encoding: 'utf8' });
|
|
41
|
+
process.env.APP_VERSION = version;
|
|
42
|
+
}
|
|
43
|
+
// Installation check: if database connection info is missing or the DB is not reachable,
|
|
44
|
+
// route the user into the installer workflow.
|
|
45
|
+
router.use(async (req, res, next) => {
|
|
46
|
+
if (req.path === '/install' || req.path.startsWith('/setup'))
|
|
47
|
+
return next();
|
|
48
|
+
const hasDbConfig = !!(process.env.DB_HOST &&
|
|
49
|
+
process.env.DB_USER &&
|
|
50
|
+
process.env.DB_PASSWORD !== undefined &&
|
|
51
|
+
process.env.DB_NAME);
|
|
52
|
+
if (!hasDbConfig) {
|
|
53
|
+
console.warn('[Radix Setup]: Missing database configuration, redirecting to installer.');
|
|
54
|
+
return res.redirect('/admin/install');
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
// quick probe against the configured pool to determine if DB is working
|
|
58
|
+
await db_1.default.execute('SELECT 1');
|
|
59
|
+
return next();
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.warn('[Radix Setup]: Database not reachable, redirecting to installer.');
|
|
63
|
+
return res.redirect('/admin/install');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// --- Install / Setup: Database Connection Form ---
|
|
67
|
+
router.get('/install', (_req, res) => {
|
|
68
|
+
res.render('admin/setup-db', { error: null, values: {} });
|
|
69
|
+
});
|
|
70
|
+
router.get('/setup/db', (_req, res) => {
|
|
71
|
+
res.render('admin/setup-db', { error: null, values: {} });
|
|
72
|
+
});
|
|
73
|
+
// POST /admin/setup/db/test - Test DB connection using provided form values without saving to .env
|
|
74
|
+
router.post('/setup/db/test', async (req, res) => {
|
|
75
|
+
const { dbName, dbUser, dbPass, dbHost, dbPort } = req.body;
|
|
76
|
+
const cfg = {
|
|
77
|
+
host: dbHost || '127.0.0.1',
|
|
78
|
+
user: dbUser || undefined,
|
|
79
|
+
password: dbPass || undefined,
|
|
80
|
+
port: Number(dbPort) || 3306,
|
|
81
|
+
connectTimeout: 3000
|
|
82
|
+
};
|
|
83
|
+
let conn;
|
|
84
|
+
try {
|
|
85
|
+
conn = await promise_1.default.createConnection(cfg);
|
|
86
|
+
// if a database name is provided, ensure it exists (querying INFORMATION_SCHEMA) otherwise run simple probe
|
|
87
|
+
if (dbName) {
|
|
88
|
+
const [rows] = await conn.query('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?', [dbName]);
|
|
89
|
+
if (!rows || (Array.isArray(rows) && rows.length === 0)) {
|
|
90
|
+
await conn.end();
|
|
91
|
+
return res.status(400).json({ ok: false, error: `Database '${dbName}' not found or inaccessible.` });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
await conn.query('SELECT 1');
|
|
96
|
+
}
|
|
97
|
+
await conn.end();
|
|
98
|
+
return res.json({ ok: true, message: 'Connection successful' });
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
try {
|
|
102
|
+
if (conn)
|
|
103
|
+
await conn.end();
|
|
104
|
+
}
|
|
105
|
+
catch (e) { /* ignore */ }
|
|
106
|
+
return res.status(400).json({ ok: false, error: err.message || String(err) });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
router.post('/setup/db', async (req, res) => {
|
|
110
|
+
const { dbName, dbUser, dbPass, dbHost, dbPort, tablePrefix } = req.body;
|
|
111
|
+
const envPath = path_1.default.join(__dirname, '../../.env');
|
|
112
|
+
try {
|
|
113
|
+
// Read existing .env (if any)
|
|
114
|
+
let envContent = '';
|
|
115
|
+
if (fs_1.default.existsSync(envPath))
|
|
116
|
+
envContent = fs_1.default.readFileSync(envPath, 'utf8');
|
|
117
|
+
const setEnv = (key, value) => {
|
|
118
|
+
const re = new RegExp(`^${key}=.*$`, 'm');
|
|
119
|
+
if (re.test(envContent)) {
|
|
120
|
+
envContent = envContent.replace(re, `${key}=${value}`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
envContent += (envContent ? '\n' : '') + `${key}=${value}`;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
setEnv('DB_NAME', dbName || 'radix');
|
|
127
|
+
setEnv('DB_USER', dbUser || '');
|
|
128
|
+
setEnv('DB_PASSWORD', dbPass || '');
|
|
129
|
+
setEnv('DB_HOST', dbHost || '127.0.0.1');
|
|
130
|
+
if (dbPort)
|
|
131
|
+
setEnv('DB_PORT', dbPort);
|
|
132
|
+
if (tablePrefix)
|
|
133
|
+
setEnv('TABLE_PREFIX', tablePrefix);
|
|
134
|
+
fs_1.default.writeFileSync(envPath, envContent, { encoding: 'utf8' });
|
|
135
|
+
// reload env and try to initialize the DB schema — explicitly override process.env with values from the new .env
|
|
136
|
+
try {
|
|
137
|
+
const parsedEnv = dotenv_1.default.parse(fs_1.default.readFileSync(envPath, 'utf8'));
|
|
138
|
+
for (const k of Object.keys(parsedEnv)) {
|
|
139
|
+
process.env[k] = parsedEnv[k];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
// fall back to regular dotenv config if parsing fails
|
|
144
|
+
dotenv_1.default.config({ path: envPath });
|
|
145
|
+
}
|
|
146
|
+
await (0, dbInit2_1.initDatabase)();
|
|
147
|
+
return res.redirect('/admin/setup/admin');
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.error('[Radix Setup DB Error]:', err);
|
|
151
|
+
return res.render('admin/setup-db', { error: String(err), values: req.body });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
// --- Setup: Admin / Site Info Form ---
|
|
155
|
+
router.get('/setup/admin', (_req, res) => {
|
|
156
|
+
res.render('admin/setup-admin', { error: null, values: {} });
|
|
157
|
+
});
|
|
158
|
+
router.post('/setup/admin', async (req, res) => {
|
|
159
|
+
const { siteTitle, username, password, email, searchEngineVisibility } = req.body;
|
|
160
|
+
try {
|
|
161
|
+
await (0, dbInit2_1.initDatabase)();
|
|
162
|
+
const passwordHash = await bcrypt_1.default.hash(password, 10);
|
|
163
|
+
// create administrator user
|
|
164
|
+
await db_1.default.execute('INSERT INTO users (username, password, password_hash, email, role, created_at) VALUES (?, ?, ?, ?, ?, NOW())', [username, null, passwordHash, email, 'Administrator']);
|
|
165
|
+
// save site settings
|
|
166
|
+
await db_1.default.execute('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)', ['site_title', siteTitle || 'Radix Site']);
|
|
167
|
+
await db_1.default.execute('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)', ['search_engine_visibility', searchEngineVisibility ? '1' : '0']);
|
|
168
|
+
return res.redirect('/admin/login');
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
console.error('[Radix Setup Admin Error]:', err);
|
|
172
|
+
return res.render('admin/setup-admin', { error: String(err), values: req.body });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
// ==========================================
|
|
176
|
+
// 1. MIDDLEWARE & HELPER CONFIGURATION
|
|
177
|
+
// ==========================================
|
|
178
|
+
const ROLE_HIERARCHY = {
|
|
179
|
+
'Administrator': 5,
|
|
180
|
+
'Manager': 4,
|
|
181
|
+
'Editor': 3,
|
|
182
|
+
'Registered': 2,
|
|
183
|
+
'Unregistered': 1
|
|
184
|
+
};
|
|
185
|
+
// GET /admin/login - Render Login View
|
|
186
|
+
router.get('/login', (req, res) => {
|
|
187
|
+
res.render('admin/login', { error: null });
|
|
188
|
+
});
|
|
189
|
+
// POST /admin/login - Process Login Form
|
|
190
|
+
router.post('/login', async (req, res) => {
|
|
191
|
+
const { username, password } = req.body;
|
|
192
|
+
try {
|
|
193
|
+
const [rows] = await db_1.default.execute('SELECT * FROM users WHERE username = ? OR email = ?', [username, username]);
|
|
194
|
+
if (rows.length === 0) {
|
|
195
|
+
res.render('admin/login', { error: 'Invalid username or password.' });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const user = rows[0];
|
|
199
|
+
const match = await bcrypt_1.default.compare(password, user.password_hash);
|
|
200
|
+
if (!match) {
|
|
201
|
+
res.render('admin/login', { error: 'Invalid username or password.' });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (user.role === 'Unregistered') {
|
|
205
|
+
res.render('admin/login', { error: 'This account is not permitted to log in.' });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
req.session.user = {
|
|
209
|
+
id: user.id,
|
|
210
|
+
username: user.username,
|
|
211
|
+
email: user.email,
|
|
212
|
+
role: user.role
|
|
213
|
+
};
|
|
214
|
+
if (user.role === 'Registered') {
|
|
215
|
+
res.redirect('/');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
res.redirect('/admin');
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
console.error('[Radix Login Error]:', err);
|
|
222
|
+
res.status(500).render('defaults/500');
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
// GET /admin/logout - Destroy Session
|
|
226
|
+
router.get('/logout', (req, res) => {
|
|
227
|
+
req.session.destroy(() => {
|
|
228
|
+
res.redirect('/admin/login');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
// RBAC Middleware Guard
|
|
232
|
+
function requireRole(minRole) {
|
|
233
|
+
return (req, res, next) => {
|
|
234
|
+
const user = req.session?.user;
|
|
235
|
+
if (!user) {
|
|
236
|
+
res.redirect('/admin/login');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const userLevel = ROLE_HIERARCHY[user.role] || 0;
|
|
240
|
+
const requiredLevel = ROLE_HIERARCHY[minRole] || 5;
|
|
241
|
+
if (userLevel < requiredLevel) {
|
|
242
|
+
res.status(403).render('defaults/403', { message: 'Access Denied: Insufficient Role Privileges.' });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
next();
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// Multer Storage Setup
|
|
249
|
+
const UPLOAD_DIR = path_1.default.join(__dirname, '../../public/uploads');
|
|
250
|
+
if (!fs_1.default.existsSync(UPLOAD_DIR)) {
|
|
251
|
+
fs_1.default.mkdirSync(UPLOAD_DIR, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
const storage = multer_1.default.diskStorage({
|
|
254
|
+
destination: (_req, _file, cb) => {
|
|
255
|
+
cb(null, UPLOAD_DIR);
|
|
256
|
+
},
|
|
257
|
+
filename: (_req, file, cb) => {
|
|
258
|
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
|
259
|
+
const ext = path_1.default.extname(file.originalname);
|
|
260
|
+
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
const upload = (0, multer_1.default)({
|
|
264
|
+
storage,
|
|
265
|
+
limits: { fileSize: 10 * 1024 * 1024 },
|
|
266
|
+
fileFilter: (_req, file, cb) => {
|
|
267
|
+
const allowedTypes = /jpeg|jpg|png|gif|webp|svg/;
|
|
268
|
+
const extname = allowedTypes.test(path_1.default.extname(file.originalname).toLowerCase());
|
|
269
|
+
const mimetype = allowedTypes.test(file.mimetype);
|
|
270
|
+
if (extname && mimetype) {
|
|
271
|
+
return cb(null, true);
|
|
272
|
+
}
|
|
273
|
+
cb(new Error('Only image files (JPG, PNG, GIF, WEBP, SVG) are allowed!'));
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
function getMediaFiles() {
|
|
277
|
+
if (!fs_1.default.existsSync(UPLOAD_DIR))
|
|
278
|
+
return [];
|
|
279
|
+
const files = fs_1.default.readdirSync(UPLOAD_DIR);
|
|
280
|
+
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'];
|
|
281
|
+
return files
|
|
282
|
+
.filter(file => imageExtensions.includes(path_1.default.extname(file).toLowerCase()))
|
|
283
|
+
.map(file => {
|
|
284
|
+
const stats = fs_1.default.statSync(path_1.default.join(UPLOAD_DIR, file));
|
|
285
|
+
return {
|
|
286
|
+
name: file,
|
|
287
|
+
url: `/uploads/${file}`,
|
|
288
|
+
size: (stats.size / 1024).toFixed(1) + ' KB',
|
|
289
|
+
mtime: stats.mtime
|
|
290
|
+
};
|
|
291
|
+
})
|
|
292
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
293
|
+
}
|
|
294
|
+
// ==========================================
|
|
295
|
+
// 2. DASHBOARD & PAGE MANAGEMENT ROUTES
|
|
296
|
+
// ==========================================
|
|
297
|
+
// GET /admin - Dashboard Overview
|
|
298
|
+
router.get('/', requireRole('Editor'), async (_req, res) => {
|
|
299
|
+
try {
|
|
300
|
+
const [totalRows] = await db_1.default.execute('SELECT COUNT(*) AS totalPages FROM pages WHERE status != "Deleted"');
|
|
301
|
+
const [pubRows] = await db_1.default.execute('SELECT COUNT(*) AS publishedPages FROM pages WHERE status = "Published"');
|
|
302
|
+
const [draftRows] = await db_1.default.execute('SELECT COUNT(*) AS draftPages FROM pages WHERE status = "Draft"');
|
|
303
|
+
const [trashRows] = await db_1.default.execute('SELECT COUNT(*) AS trashPages FROM pages WHERE status = "Deleted"');
|
|
304
|
+
const [userRows] = await db_1.default.execute('SELECT COUNT(*) AS totalUsers FROM users');
|
|
305
|
+
const pagesDir = path_1.default.join(__dirname, '../../views/pages');
|
|
306
|
+
let staticFiles = [];
|
|
307
|
+
if (fs_1.default.existsSync(pagesDir)) {
|
|
308
|
+
staticFiles = fs_1.default.readdirSync(pagesDir).filter(file => file.endsWith('.ejs'));
|
|
309
|
+
}
|
|
310
|
+
res.render('admin/dashboard', {
|
|
311
|
+
stats: {
|
|
312
|
+
totalPages: totalRows[0]?.totalPages || 0,
|
|
313
|
+
publishedPages: pubRows[0]?.publishedPages || 0,
|
|
314
|
+
draftPages: draftRows[0]?.draftPages || 0,
|
|
315
|
+
trashPages: trashRows[0]?.trashPages || 0,
|
|
316
|
+
staticPagesCount: staticFiles.length,
|
|
317
|
+
totalUsers: userRows[0]?.totalUsers || 0,
|
|
318
|
+
mediaCount: getMediaFiles().length
|
|
319
|
+
},
|
|
320
|
+
user: _req.session?.user
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
console.error('[Radix Dashboard Stats Error]:', err);
|
|
325
|
+
res.status(500).render('defaults/500');
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
// GET /admin/pages - Pages Management List
|
|
329
|
+
router.get('/pages', requireRole('Editor'), async (req, res) => {
|
|
330
|
+
try {
|
|
331
|
+
const showTrash = req.query.view === 'trash';
|
|
332
|
+
const statusFilter = showTrash ? 'Deleted' : req.query.status || null;
|
|
333
|
+
let sql = 'SELECT * FROM pages ';
|
|
334
|
+
const params = [];
|
|
335
|
+
if (showTrash) {
|
|
336
|
+
sql += 'WHERE status = "Deleted" ';
|
|
337
|
+
}
|
|
338
|
+
else if (statusFilter) {
|
|
339
|
+
sql += 'WHERE status = ? ';
|
|
340
|
+
params.push(statusFilter);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
sql += 'WHERE status != "Deleted" ';
|
|
344
|
+
}
|
|
345
|
+
sql += 'ORDER BY created_at DESC';
|
|
346
|
+
const [pages] = await db_1.default.execute(sql, params);
|
|
347
|
+
const pagesDir = path_1.default.join(__dirname, '../../views/pages');
|
|
348
|
+
let staticSlugs = [];
|
|
349
|
+
if (fs_1.default.existsSync(pagesDir)) {
|
|
350
|
+
staticSlugs = fs_1.default.readdirSync(pagesDir)
|
|
351
|
+
.filter(file => file.endsWith('.ejs'))
|
|
352
|
+
.map(file => path_1.default.basename(file, '.ejs').toLowerCase());
|
|
353
|
+
}
|
|
354
|
+
const pagesWithFlags = pages.map(page => ({
|
|
355
|
+
...page,
|
|
356
|
+
isStaticOverride: staticSlugs.includes(String(page.slug).toLowerCase())
|
|
357
|
+
}));
|
|
358
|
+
res.render('admin/pages', {
|
|
359
|
+
pages: pagesWithFlags,
|
|
360
|
+
showTrash,
|
|
361
|
+
currentStatusFilter: statusFilter || 'All',
|
|
362
|
+
user: req.session?.user
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
console.error('[Radix Pages Route Error]:', err);
|
|
367
|
+
res.status(500).render('defaults/500');
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
// GET /admin/create - Render Create Form
|
|
371
|
+
router.get('/create', requireRole('Editor'), async (req, res) => {
|
|
372
|
+
try {
|
|
373
|
+
const templates = (0, themeScanner_1.getAvailableTemplates)();
|
|
374
|
+
res.render('admin/create-page', {
|
|
375
|
+
templates,
|
|
376
|
+
user: req.session?.user
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
console.error('[Radix Create Page Route Error]:', err);
|
|
381
|
+
res.status(500).render('defaults/500');
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
// POST /admin/create - Process Page Creation
|
|
385
|
+
router.post('/create', requireRole('Editor'), async (req, res) => {
|
|
386
|
+
const { title, slug, content, template_name, status, min_role, publish_at } = req.body;
|
|
387
|
+
try {
|
|
388
|
+
const publishDate = publish_at ? new Date(publish_at) : new Date();
|
|
389
|
+
const selectedTemplate = template_name && template_name.trim() !== '' ? template_name : 'default';
|
|
390
|
+
await db_1.default.execute(`INSERT INTO pages (title, slug, content, template_name, status, min_role, publish_at, created_at)
|
|
391
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())`, [
|
|
392
|
+
title,
|
|
393
|
+
slug,
|
|
394
|
+
content || '',
|
|
395
|
+
selectedTemplate,
|
|
396
|
+
status || 'Draft',
|
|
397
|
+
min_role || 'Unregistered',
|
|
398
|
+
publishDate
|
|
399
|
+
]);
|
|
400
|
+
res.redirect('/admin/pages');
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
console.error('[Radix Page Creation Error]:', err);
|
|
404
|
+
res.status(500).render('defaults/500');
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
// GET /admin/edit/:id - Render Edit Page Form
|
|
408
|
+
router.get('/edit/:id', requireRole('Editor'), async (req, res) => {
|
|
409
|
+
try {
|
|
410
|
+
const [pages] = await db_1.default.execute('SELECT * FROM pages WHERE id = ?', [req.params.id]);
|
|
411
|
+
if (pages.length === 0) {
|
|
412
|
+
res.status(404).render('defaults/404');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const templates = (0, themeScanner_1.getAvailableTemplates)();
|
|
416
|
+
res.render('admin/edit-page', {
|
|
417
|
+
page: pages[0],
|
|
418
|
+
templates,
|
|
419
|
+
user: req.session?.user
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
console.error('[Radix Edit Fetch Error]:', err);
|
|
424
|
+
res.status(500).render('defaults/500');
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
// POST /admin/edit/:id - Process Edit Page Update
|
|
428
|
+
router.post('/edit/:id', requireRole('Editor'), async (req, res) => {
|
|
429
|
+
const pageId = req.params.id;
|
|
430
|
+
const { title, slug, content, template_name, status, min_role, publish_at } = req.body;
|
|
431
|
+
try {
|
|
432
|
+
const publishDate = publish_at ? new Date(publish_at) : new Date();
|
|
433
|
+
await db_1.default.execute(`UPDATE pages
|
|
434
|
+
SET title = ?, slug = ?, content = ?, template_name = ?, status = ?, min_role = ?, publish_at = ?
|
|
435
|
+
WHERE id = ?`, [
|
|
436
|
+
title,
|
|
437
|
+
slug,
|
|
438
|
+
content || '',
|
|
439
|
+
template_name || 'default',
|
|
440
|
+
status || 'Draft',
|
|
441
|
+
min_role || 'Unregistered',
|
|
442
|
+
publishDate,
|
|
443
|
+
pageId
|
|
444
|
+
]);
|
|
445
|
+
res.redirect('/admin/pages');
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
console.error('[Radix Page Update Error]:', err);
|
|
449
|
+
res.status(500).render('defaults/500');
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
// POST /admin/delete/:id - Soft Delete Page
|
|
453
|
+
router.post('/delete/:id', requireRole('Editor'), async (req, res) => {
|
|
454
|
+
try {
|
|
455
|
+
await db_1.default.execute('UPDATE pages SET status = "Deleted" WHERE id = ?', [req.params.id]);
|
|
456
|
+
res.redirect('/admin/pages');
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
console.error('[Radix Soft Delete Error]:', err);
|
|
460
|
+
res.status(500).render('defaults/500');
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
// POST /admin/restore/:id - Restore Page from Trash
|
|
464
|
+
router.post('/restore/:id', requireRole('Editor'), async (req, res) => {
|
|
465
|
+
try {
|
|
466
|
+
await db_1.default.execute('UPDATE pages SET status = "Draft" WHERE id = ?', [req.params.id]);
|
|
467
|
+
res.redirect('/admin/pages?view=trash');
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
console.error('[Radix Restore Error]:', err);
|
|
471
|
+
res.status(500).render('defaults/500');
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
// POST /admin/permanent-delete/:id - Hard Delete Page
|
|
475
|
+
router.post('/permanent-delete/:id', requireRole('Administrator'), async (req, res) => {
|
|
476
|
+
try {
|
|
477
|
+
await db_1.default.execute('DELETE FROM pages WHERE id = ? AND status = "Deleted"', [req.params.id]);
|
|
478
|
+
res.redirect('/admin/pages?view=trash');
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
console.error('[Radix Permanent Delete Error]:', err);
|
|
482
|
+
res.status(500).render('defaults/500');
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
// ==========================================
|
|
486
|
+
// 3. USER MANAGEMENT & RBAC ROUTES
|
|
487
|
+
// ==========================================
|
|
488
|
+
router.get('/users', requireRole('Administrator'), async (req, res) => {
|
|
489
|
+
try {
|
|
490
|
+
const [users] = await db_1.default.execute('SELECT id, username, email, full_name, role, created_at FROM users ORDER BY created_at DESC');
|
|
491
|
+
res.render('admin/users', {
|
|
492
|
+
users,
|
|
493
|
+
user: req.session?.user
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
console.error('[Radix User Fetch Error]:', err);
|
|
498
|
+
res.status(500).render('defaults/500');
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
router.post('/users/create', requireRole('Administrator'), async (req, res) => {
|
|
502
|
+
const { username, email, full_name, password, role } = req.body;
|
|
503
|
+
if (!username || !password || !role) {
|
|
504
|
+
res.status(400).send('Username, password, and role are required.');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const validRoles = ['Administrator', 'Manager', 'Editor', 'Registered', 'Unregistered'];
|
|
508
|
+
if (!validRoles.includes(role)) {
|
|
509
|
+
res.status(400).send('Invalid role specified.');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const [existing] = await db_1.default.execute('SELECT id FROM users WHERE username = ? OR (email IS NOT NULL AND email = ? AND email != "")', [username, email || '']);
|
|
514
|
+
if (existing.length > 0) {
|
|
515
|
+
res.status(400).send('User with that username or email already exists.');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const hashedPassword = await bcrypt_1.default.hash(password, 10);
|
|
519
|
+
await db_1.default.execute('INSERT INTO users (username, email, full_name, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, NOW())', [username, email || null, full_name || null, hashedPassword, role]);
|
|
520
|
+
res.redirect('/admin/users');
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
console.error('[Radix User Creation Error]:', err);
|
|
524
|
+
res.status(500).render('defaults/500');
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
router.get('/users/edit/:id', requireRole('Administrator'), async (req, res) => {
|
|
528
|
+
try {
|
|
529
|
+
const [users] = await db_1.default.execute('SELECT id, username, email, full_name, role FROM users WHERE id = ?', [req.params.id]);
|
|
530
|
+
if (users.length === 0) {
|
|
531
|
+
res.status(404).render('defaults/404');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
res.render('admin/edit-user', {
|
|
535
|
+
editUser: users[0],
|
|
536
|
+
user: req.session?.user
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
console.error('[Radix User Edit Fetch Error]:', err);
|
|
541
|
+
res.status(500).render('defaults/500');
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
router.post('/users/edit/:id', requireRole('Administrator'), async (req, res) => {
|
|
545
|
+
const userId = req.params.id;
|
|
546
|
+
const { username, email, full_name, password, role } = req.body;
|
|
547
|
+
if (!username || !role) {
|
|
548
|
+
res.status(400).send('Username and role are required.');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const validRoles = ['Administrator', 'Manager', 'Editor', 'Registered', 'Unregistered'];
|
|
552
|
+
if (!validRoles.includes(role)) {
|
|
553
|
+
res.status(400).send('Invalid role specified.');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const [existing] = await db_1.default.execute('SELECT id FROM users WHERE (username = ? OR (email IS NOT NULL AND email = ? AND email != "")) AND id != ?', [username, email || '', userId]);
|
|
558
|
+
if (existing.length > 0) {
|
|
559
|
+
res.status(400).send('Another user with that username or email already exists.');
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (password && password.trim()) {
|
|
563
|
+
const hashedPassword = await bcrypt_1.default.hash(password, 10);
|
|
564
|
+
await db_1.default.execute('UPDATE users SET username = ?, email = ?, full_name = ?, role = ?, password_hash = ? WHERE id = ?', [username, email || null, full_name || null, role, hashedPassword, userId]);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
await db_1.default.execute('UPDATE users SET username = ?, email = ?, full_name = ?, role = ? WHERE id = ?', [username, email || null, full_name || null, role, userId]);
|
|
568
|
+
}
|
|
569
|
+
const sessionUser = req.session?.user;
|
|
570
|
+
if (sessionUser && String(sessionUser.id) === String(userId)) {
|
|
571
|
+
sessionUser.username = username;
|
|
572
|
+
sessionUser.role = role;
|
|
573
|
+
}
|
|
574
|
+
res.redirect('/admin/users');
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
console.error('[Radix User Update Error]:', err);
|
|
578
|
+
res.status(500).render('defaults/500');
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
router.post('/users/update-role', requireRole('Administrator'), async (req, res) => {
|
|
582
|
+
const { userId, newRole } = req.body;
|
|
583
|
+
const validRoles = ['Administrator', 'Manager', 'Editor', 'Registered', 'Unregistered'];
|
|
584
|
+
if (!validRoles.includes(newRole)) {
|
|
585
|
+
res.status(400).send('Invalid role specified.');
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
await db_1.default.execute('UPDATE users SET role = ? WHERE id = ?', [newRole, userId]);
|
|
590
|
+
res.redirect('/admin/users');
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
console.error('[Radix Role Update Error]:', err);
|
|
594
|
+
res.status(500).render('defaults/500');
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
router.post('/users/delete/:id', requireRole('Administrator'), async (req, res) => {
|
|
598
|
+
const targetUserId = req.params.id;
|
|
599
|
+
const currentUserId = req.session?.user?.id;
|
|
600
|
+
if (String(targetUserId) === String(currentUserId)) {
|
|
601
|
+
res.status(400).send('Security error: You cannot delete your own active administrator account.');
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
await db_1.default.execute('DELETE FROM users WHERE id = ?', [targetUserId]);
|
|
606
|
+
res.redirect('/admin/users');
|
|
607
|
+
}
|
|
608
|
+
catch (err) {
|
|
609
|
+
console.error('[Radix User Delete Error]:', err);
|
|
610
|
+
res.status(500).render('defaults/500');
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
// ==========================================
|
|
614
|
+
// 4. MEDIA LIBRARY ROUTES
|
|
615
|
+
// ==========================================
|
|
616
|
+
router.get('/media', requireRole('Editor'), (req, res) => {
|
|
617
|
+
const media = getMediaFiles();
|
|
618
|
+
res.render('admin/media', { media, user: req.session?.user });
|
|
619
|
+
});
|
|
620
|
+
router.post('/media/upload', requireRole('Editor'), upload.single('file'), (req, res) => {
|
|
621
|
+
if (!req.file) {
|
|
622
|
+
res.status(400).json({ error: 'No file uploaded.' });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
res.json({ location: `/uploads/${req.file.filename}` });
|
|
626
|
+
});
|
|
627
|
+
router.post('/media/delete', requireRole('Administrator'), (req, res) => {
|
|
628
|
+
const { filename } = req.body;
|
|
629
|
+
if (!filename) {
|
|
630
|
+
res.status(400).send('Filename required.');
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const filePath = path_1.default.join(UPLOAD_DIR, path_1.default.basename(filename));
|
|
634
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
635
|
+
fs_1.default.unlinkSync(filePath);
|
|
636
|
+
}
|
|
637
|
+
res.redirect('/admin/media');
|
|
638
|
+
});
|
|
639
|
+
// ==========================================
|
|
640
|
+
// 5. THEME MANAGEMENT
|
|
641
|
+
// ==========================================
|
|
642
|
+
router.get('/themes', requireRole('Editor'), async (req, res) => {
|
|
643
|
+
try {
|
|
644
|
+
const templates = (0, themeScanner_1.getAvailableTemplates)();
|
|
645
|
+
res.render('admin/themes', {
|
|
646
|
+
templates,
|
|
647
|
+
user: req.session?.user
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
console.error('[Radix Themes Route Error]:', err);
|
|
652
|
+
res.status(500).render('defaults/500');
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
// ==========================================
|
|
656
|
+
// 6. SETTINGS
|
|
657
|
+
// ==========================================
|
|
658
|
+
// GET /admin/settings - Render Settings Page
|
|
659
|
+
router.get('/settings', requireRole('Administrator'), async (req, res) => {
|
|
660
|
+
try {
|
|
661
|
+
const [rows] = await db_1.default.execute('SELECT * FROM settings');
|
|
662
|
+
const settings = {};
|
|
663
|
+
rows.forEach(row => {
|
|
664
|
+
settings[row.setting_key] = row.setting_value || '';
|
|
665
|
+
});
|
|
666
|
+
res.render('admin/settings', {
|
|
667
|
+
settings,
|
|
668
|
+
message: req.query.msg ? String(req.query.msg) : null,
|
|
669
|
+
error: req.query.err ? String(req.query.err) : null,
|
|
670
|
+
user: req.session?.user
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
console.error('[Radix Settings GET Error]:', err);
|
|
675
|
+
res.status(500).render('defaults/500');
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
// POST /admin/settings - Save Settings
|
|
679
|
+
router.post('/settings', requireRole('Administrator'), async (req, res) => {
|
|
680
|
+
const { site_logo, mail_host, mail_port, mail_secure, mail_user, mail_pass, mail_from } = req.body;
|
|
681
|
+
const updates = [
|
|
682
|
+
['site_logo', site_logo || ''],
|
|
683
|
+
['mail_host', mail_host || ''],
|
|
684
|
+
['mail_port', mail_port || '25'],
|
|
685
|
+
['mail_secure', mail_secure === '1' ? '1' : '0'],
|
|
686
|
+
['mail_user', mail_user || ''],
|
|
687
|
+
['mail_pass', mail_pass || ''],
|
|
688
|
+
['mail_from', mail_from || '']
|
|
689
|
+
];
|
|
690
|
+
try {
|
|
691
|
+
for (const [key, value] of updates) {
|
|
692
|
+
await db_1.default.execute('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?', [key, value, value]);
|
|
693
|
+
}
|
|
694
|
+
res.redirect('/admin/settings?msg=Settings+saved+successfully.');
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
console.error('[Radix Settings Save Error]:', err);
|
|
698
|
+
res.redirect('/admin/settings?err=Failed+to+save+settings.');
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
// POST /admin/settings/test-email - Dispatch SMTP Test Email
|
|
702
|
+
router.post('/settings/test-email', requireRole('Administrator'), async (req, res) => {
|
|
703
|
+
const { recipient_email } = req.body;
|
|
704
|
+
if (!recipient_email) {
|
|
705
|
+
res.redirect('/admin/settings?err=Recipient+email+address+is+required.');
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
const [rows] = await db_1.default.execute('SELECT * FROM settings');
|
|
710
|
+
const cfg = {};
|
|
711
|
+
rows.forEach(r => { cfg[r.setting_key] = r.setting_value || ''; });
|
|
712
|
+
const transporter = nodemailer_1.default.createTransport({
|
|
713
|
+
host: cfg.mail_host || process.env.MAIL_HOST || 'localhost',
|
|
714
|
+
port: Number(cfg.mail_port || process.env.MAIL_PORT || 25),
|
|
715
|
+
secure: cfg.mail_secure === '1',
|
|
716
|
+
auth: cfg.mail_user ? {
|
|
717
|
+
user: cfg.mail_user,
|
|
718
|
+
pass: cfg.mail_pass
|
|
719
|
+
} : undefined,
|
|
720
|
+
tls: { rejectUnauthorized: false }
|
|
721
|
+
});
|
|
722
|
+
await transporter.sendMail({
|
|
723
|
+
from: cfg.mail_from || `Radix CMS <noreply@${process.env.SITE_URL || 'localhost'}>`,
|
|
724
|
+
to: recipient_email,
|
|
725
|
+
subject: 'Radix CMS - SMTP Test Connection',
|
|
726
|
+
text: 'Congratulations! Your Radix CMS mail server configuration is working correctly.',
|
|
727
|
+
html: '<h3>Radix CMS Mail Test</h3><p>Your mail server settings are configured correctly.</p>'
|
|
728
|
+
});
|
|
729
|
+
res.redirect('/admin/settings?msg=Test+email+sent+successfully+to+' + encodeURIComponent(recipient_email));
|
|
730
|
+
}
|
|
731
|
+
catch (err) {
|
|
732
|
+
console.error('[SMTP Test Error]:', err);
|
|
733
|
+
res.redirect('/admin/settings?err=' + encodeURIComponent(`Email dispatch failed: ${err.message}`));
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
// ==========================================
|
|
737
|
+
// 7. UPDATES
|
|
738
|
+
// ==========================================
|
|
739
|
+
// GET /admin/updates - Render System Updates Dashboard
|
|
740
|
+
router.get('/updates', requireRole('Editor'), async (req, res) => {
|
|
741
|
+
try {
|
|
742
|
+
const updateInfo = await (0, versionCheck_1.checkForUpdates)();
|
|
743
|
+
res.render('admin/updates', {
|
|
744
|
+
updateInfo,
|
|
745
|
+
message: req.query.msg ? String(req.query.msg) : null,
|
|
746
|
+
error: req.query.err ? String(req.query.err) : null,
|
|
747
|
+
user: req.session?.user
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
console.error('[Radix Updates Route Error]:', err);
|
|
752
|
+
res.status(500).render('defaults/500');
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
// POST /admin/updates/apply - Download & Overwrite Core Codebase
|
|
756
|
+
router.post('/updates/apply', requireRole('Administrator'), async (req, res) => {
|
|
757
|
+
const updateInfo = await (0, versionCheck_1.checkForUpdates)();
|
|
758
|
+
if (!updateInfo.hasUpdate || !updateInfo.details) {
|
|
759
|
+
res.redirect('/admin/updates?err=' + encodeURIComponent('No update available or release package details missing.'));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const fileUrl = `https://vault.ultraspark.net/radix/${updateInfo.details.InstallFile}`;
|
|
763
|
+
const zipPath = path_1.default.join(process.cwd(), 'temp_update.zip');
|
|
764
|
+
const extractDir = path_1.default.join(process.cwd(), 'temp_update_extract');
|
|
765
|
+
try {
|
|
766
|
+
// 1. Download zip archive
|
|
767
|
+
const response = await fetch(fileUrl);
|
|
768
|
+
if (!response.ok)
|
|
769
|
+
throw new Error(`Download failed with status ${response.status}`);
|
|
770
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
771
|
+
await fs_1.default.promises.writeFile(zipPath, Buffer.from(arrayBuffer));
|
|
772
|
+
// 2. Extract contents
|
|
773
|
+
const zip = new adm_zip_1.default(zipPath);
|
|
774
|
+
zip.extractAllTo(extractDir, true);
|
|
775
|
+
// 3. Overwrite codebase excluding environment and runtime assets
|
|
776
|
+
const protectedPaths = ['.env', 'node_modules', 'uploads', 'temp_update.zip', 'temp_update_extract'];
|
|
777
|
+
const copyFolderRecursive = async (src, dest) => {
|
|
778
|
+
const entries = await fs_1.default.promises.readdir(src, { withFileTypes: true });
|
|
779
|
+
for (const entry of entries) {
|
|
780
|
+
if (protectedPaths.includes(entry.name))
|
|
781
|
+
continue;
|
|
782
|
+
const srcPath = path_1.default.join(src, entry.name);
|
|
783
|
+
const destPath = path_1.default.join(dest, entry.name);
|
|
784
|
+
if (entry.isDirectory()) {
|
|
785
|
+
await fs_1.default.promises.mkdir(destPath, { recursive: true });
|
|
786
|
+
await copyFolderRecursive(srcPath, destPath);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
await fs_1.default.promises.copyFile(srcPath, destPath);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
await copyFolderRecursive(extractDir, process.cwd());
|
|
794
|
+
updateEnvVersion(updateInfo.remoteVersion);
|
|
795
|
+
// 4. Clean up temporary files
|
|
796
|
+
await fs_1.default.promises.unlink(zipPath).catch(() => { });
|
|
797
|
+
await fs_1.default.promises.rm(extractDir, { recursive: true, force: true }).catch(() => { });
|
|
798
|
+
res.redirect('/admin/updates?msg=' + encodeURIComponent('System updated successfully! Restart application process to reflect server changes.'));
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
console.error('[Update Apply Error]:', err);
|
|
802
|
+
// Cleanup on failure
|
|
803
|
+
await fs_1.default.promises.unlink(zipPath).catch(() => { });
|
|
804
|
+
await fs_1.default.promises.rm(extractDir, { recursive: true, force: true }).catch(() => { });
|
|
805
|
+
res.redirect('/admin/updates?err=' + encodeURIComponent(`Update execution failed: ${err.message}`));
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
exports.default = router;
|