ezpm2gui 1.5.0 → 1.8.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/LICENSE +661 -0
- package/README.md +330 -295
- package/bin/ezpm2gui.js +10 -10
- package/bin/ezpm2gui.ts +51 -51
- package/bin/generate-ecosystem.js +36 -36
- package/bin/generate-ecosystem.ts +56 -56
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/server/config/cron-jobs.json +1 -0
- package/dist/server/config/project-configs.json +235 -236
- package/dist/server/config/remote-connections.json +3 -0
- package/dist/server/index.js +44 -3
- package/dist/server/routes/deployApplication.js +47 -45
- package/dist/server/routes/logStreaming.js +31 -24
- package/dist/server/routes/modules.js +55 -0
- package/dist/server/routes/pageAuth.d.ts +3 -0
- package/dist/server/routes/pageAuth.js +177 -0
- package/dist/server/routes/remoteConnections.js +13 -9
- package/dist/server/routes/remoteMetrics.d.ts +3 -0
- package/dist/server/routes/remoteMetrics.js +84 -0
- package/dist/server/services/ProjectSetupService.d.ts +1 -1
- package/dist/server/services/ProjectSetupService.js +25 -9
- package/dist/server/utils/metrics-history.d.ts +21 -0
- package/dist/server/utils/metrics-history.js +68 -0
- package/dist/server/utils/remote-metrics-db.d.ts +29 -0
- package/dist/server/utils/remote-metrics-db.js +134 -0
- package/dist/server/utils/remote-metrics-poller.d.ts +8 -0
- package/dist/server/utils/remote-metrics-poller.js +67 -0
- package/package.json +86 -73
- package/scripts/postinstall.js +36 -36
- package/src/client/build/asset-manifest.json +6 -6
- package/src/client/build/favicon.ico +2 -2
- package/src/client/build/index.html +1 -1
- package/src/client/build/logo192.svg +7 -7
- package/src/client/build/logo512.svg +7 -7
- package/src/client/build/manifest.json +24 -24
- package/src/client/build/static/css/main.9decb204.css +5 -0
- package/src/client/build/static/css/main.9decb204.css.map +1 -0
- package/src/client/build/static/js/main.28a4a583.js +3 -0
- package/src/client/build/static/js/main.28a4a583.js.map +1 -0
- package/src/client/build/static/css/main.2d095544.css +0 -5
- package/src/client/build/static/css/main.2d095544.css.map +0 -1
- package/src/client/build/static/js/main.17e17668.js +0 -3
- package/src/client/build/static/js/main.17e17668.js.map +0 -1
- /package/src/client/build/static/js/{main.17e17668.js.LICENSE.txt → main.28a4a583.js.LICENSE.txt} +0 -0
|
@@ -9,60 +9,69 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const ProjectSetupService_1 = require("../services/ProjectSetupService");
|
|
11
11
|
const router = (0, express_1.Router)();
|
|
12
|
-
// Deploy a new application
|
|
12
|
+
// Deploy a new application (SSE streaming)
|
|
13
13
|
router.post('/', async (req, res) => {
|
|
14
14
|
const { name, script, cwd, namespace, instances, exec_mode, autorestart, watch, max_memory_restart, env, appType, autoSetup = true } = req.body;
|
|
15
|
-
// Validate required fields
|
|
15
|
+
// Validate required fields before starting SSE
|
|
16
16
|
if (!name || !script) {
|
|
17
17
|
return res.status(400).json({ error: 'Name and script path are required' });
|
|
18
18
|
}
|
|
19
|
-
// Validate script path exists
|
|
20
19
|
if (!fs_1.default.existsSync(script)) {
|
|
21
20
|
return res.status(400).json({ error: `Script file not found: ${script}` });
|
|
22
21
|
}
|
|
22
|
+
// Switch to SSE
|
|
23
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
24
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
25
|
+
res.setHeader('Connection', 'keep-alive');
|
|
26
|
+
res.flushHeaders();
|
|
27
|
+
const send = (type, data) => {
|
|
28
|
+
if (!res.writableEnded) {
|
|
29
|
+
res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const log = (message) => send('log', { message });
|
|
33
|
+
const done = (success, extra = {}) => {
|
|
34
|
+
send('done', { success, ...extra });
|
|
35
|
+
res.end();
|
|
36
|
+
};
|
|
23
37
|
try {
|
|
24
38
|
const projectPath = cwd || path_1.default.dirname(script);
|
|
25
|
-
let setupResult = null;
|
|
26
39
|
let finalEnv = env || {};
|
|
27
40
|
let interpreterPath = '';
|
|
28
|
-
|
|
41
|
+
let setupResult = null;
|
|
42
|
+
// Auto-detect project type
|
|
29
43
|
let detectedType = appType;
|
|
30
44
|
if (!detectedType) {
|
|
31
45
|
detectedType = ProjectSetupService_1.projectSetupService.detectProjectType(projectPath);
|
|
32
46
|
if (detectedType) {
|
|
33
|
-
|
|
47
|
+
log(`Auto-detected project type: ${detectedType}`);
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
|
-
// Run project setup
|
|
50
|
+
// Run project setup
|
|
37
51
|
if (autoSetup && detectedType && ['node', 'python', 'dotnet'].includes(detectedType)) {
|
|
38
|
-
|
|
52
|
+
log(`Running ${detectedType} project setup...`);
|
|
39
53
|
try {
|
|
40
|
-
setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, detectedType);
|
|
54
|
+
setupResult = await ProjectSetupService_1.projectSetupService.setupProject(projectPath, detectedType, log);
|
|
41
55
|
if (!setupResult.success) {
|
|
42
|
-
return
|
|
56
|
+
return done(false, {
|
|
43
57
|
error: 'Project setup failed',
|
|
44
58
|
details: setupResult.errors,
|
|
45
|
-
warnings: setupResult.warnings
|
|
46
|
-
steps: setupResult.steps
|
|
59
|
+
warnings: setupResult.warnings
|
|
47
60
|
});
|
|
48
61
|
}
|
|
49
|
-
// Merge environment variables from setup
|
|
50
62
|
finalEnv = { ...setupResult.environment, ...finalEnv };
|
|
51
|
-
// Set interpreter path for Python projects
|
|
52
63
|
if (setupResult.interpreterPath) {
|
|
53
64
|
interpreterPath = setupResult.interpreterPath;
|
|
54
65
|
}
|
|
55
|
-
console.log('Project setup completed successfully');
|
|
56
66
|
}
|
|
57
67
|
catch (setupError) {
|
|
58
|
-
|
|
59
|
-
return res.status(500).json({
|
|
68
|
+
return done(false, {
|
|
60
69
|
error: 'Project setup failed',
|
|
61
70
|
details: setupError instanceof Error ? setupError.message : 'Unknown setup error'
|
|
62
71
|
});
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
|
-
//
|
|
74
|
+
// Build PM2 config
|
|
66
75
|
const appConfig = {
|
|
67
76
|
name,
|
|
68
77
|
script,
|
|
@@ -75,45 +84,38 @@ router.post('/', async (req, res) => {
|
|
|
75
84
|
max_memory_restart: max_memory_restart || '150M',
|
|
76
85
|
env: finalEnv
|
|
77
86
|
};
|
|
78
|
-
// Set interpreter for Python projects
|
|
79
87
|
if (detectedType === 'python' && interpreterPath) {
|
|
80
88
|
appConfig.interpreter = interpreterPath;
|
|
81
89
|
}
|
|
82
90
|
else if (detectedType === 'dotnet') {
|
|
83
91
|
appConfig.interpreter = 'dotnet';
|
|
84
|
-
// For .NET projects, update script to point to the published DLL if available
|
|
85
92
|
const publishedDll = path_1.default.join(projectPath, 'publish', `${path_1.default.basename(projectPath)}.dll`);
|
|
86
93
|
if (fs_1.default.existsSync(publishedDll)) {
|
|
87
94
|
appConfig.script = publishedDll;
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
error: `Failed to deploy application: ${err.message || 'Unknown error'}`
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
res.json({
|
|
104
|
-
success: true,
|
|
105
|
-
message: `Application ${name} deployed successfully`,
|
|
106
|
-
setupResult: setupResult ? {
|
|
107
|
-
steps: setupResult.steps,
|
|
108
|
-
warnings: setupResult.warnings
|
|
109
|
-
} : null
|
|
97
|
+
log(`Starting PM2 process: ${name}`);
|
|
98
|
+
await new Promise((resolve, reject) => {
|
|
99
|
+
pm2_1.default.connect((err) => {
|
|
100
|
+
if (err)
|
|
101
|
+
return reject(err);
|
|
102
|
+
pm2_1.default.start(appConfig, (startErr) => {
|
|
103
|
+
pm2_1.default.disconnect();
|
|
104
|
+
if (startErr)
|
|
105
|
+
return reject(startErr);
|
|
106
|
+
resolve();
|
|
110
107
|
});
|
|
111
108
|
});
|
|
112
109
|
});
|
|
110
|
+
log(`Process "${name}" started successfully.`);
|
|
111
|
+
done(true, {
|
|
112
|
+
message: `Application ${name} deployed successfully`,
|
|
113
|
+
setupResult: setupResult ? { steps: setupResult.steps, warnings: setupResult.warnings } : null
|
|
114
|
+
});
|
|
113
115
|
}
|
|
114
116
|
catch (error) {
|
|
115
117
|
console.error('Deployment error:', error);
|
|
116
|
-
|
|
118
|
+
done(false, {
|
|
117
119
|
error: 'Deployment failed',
|
|
118
120
|
details: error instanceof Error ? error.message : 'Unknown error'
|
|
119
121
|
});
|
|
@@ -159,8 +161,8 @@ router.post('/generate-ecosystem', (req, res) => {
|
|
|
159
161
|
env: pm2Env.env || {}
|
|
160
162
|
};
|
|
161
163
|
});
|
|
162
|
-
const ecosystemConfig = `module.exports = {
|
|
163
|
-
apps: ${JSON.stringify(apps, null, 2)}
|
|
164
|
+
const ecosystemConfig = `module.exports = {
|
|
165
|
+
apps: ${JSON.stringify(apps, null, 2)}
|
|
164
166
|
};`;
|
|
165
167
|
// Create the file (either at specified path or default location)
|
|
166
168
|
const filePath = req.body.path || path_1.default.join(process.cwd(), 'ecosystem.config.js');
|
|
@@ -213,8 +215,8 @@ router.get('/generate-ecosystem-preview', (req, res) => {
|
|
|
213
215
|
env: pm2Env.env || {}
|
|
214
216
|
};
|
|
215
217
|
});
|
|
216
|
-
const ecosystemConfig = `module.exports = {
|
|
217
|
-
apps: ${JSON.stringify(apps, null, 2)}
|
|
218
|
+
const ecosystemConfig = `module.exports = {
|
|
219
|
+
apps: ${JSON.stringify(apps, null, 2)}
|
|
218
220
|
};`;
|
|
219
221
|
pm2_1.default.disconnect();
|
|
220
222
|
res.json({
|
|
@@ -7,7 +7,6 @@ exports.setupLogStreaming = void 0;
|
|
|
7
7
|
const express_1 = require("express");
|
|
8
8
|
const pm2_1 = __importDefault(require("pm2"));
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const child_process_1 = require("child_process");
|
|
11
10
|
const remote_connection_1 = require("../utils/remote-connection");
|
|
12
11
|
const router = (0, express_1.Router)();
|
|
13
12
|
// This variable will hold references to active log streams
|
|
@@ -18,7 +17,7 @@ const getLogStream = (io, processId, logType) => {
|
|
|
18
17
|
const streamKey = `${processId}-${logType}`;
|
|
19
18
|
// If stream already exists, return it
|
|
20
19
|
if (activeStreams[streamKey]) {
|
|
21
|
-
return activeStreams[streamKey];
|
|
20
|
+
return Promise.resolve(activeStreams[streamKey]);
|
|
22
21
|
}
|
|
23
22
|
return new Promise((resolve, reject) => {
|
|
24
23
|
pm2_1.default.describe(processId, (err, processDesc) => {
|
|
@@ -32,30 +31,38 @@ const getLogStream = (io, processId, logType) => {
|
|
|
32
31
|
reject(new Error(`Log file not found: ${logPath}`));
|
|
33
32
|
return;
|
|
34
33
|
}
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
// Cross-platform tail using fs.watch — no Unix `tail` binary needed
|
|
35
|
+
let position = fs_1.default.statSync(logPath).size; // start at EOF, don't replay history
|
|
36
|
+
const watcher = fs_1.default.watch(logPath, (eventType) => {
|
|
37
|
+
if (eventType !== 'change')
|
|
38
|
+
return;
|
|
39
|
+
try {
|
|
40
|
+
const stat = fs_1.default.statSync(logPath);
|
|
41
|
+
// Handle log rotation / truncation
|
|
42
|
+
if (stat.size < position)
|
|
43
|
+
position = 0;
|
|
44
|
+
if (stat.size === position)
|
|
45
|
+
return;
|
|
46
|
+
const length = stat.size - position;
|
|
47
|
+
const buffer = Buffer.alloc(length);
|
|
48
|
+
const fd = fs_1.default.openSync(logPath, 'r');
|
|
49
|
+
fs_1.default.readSync(fd, buffer, 0, length, position);
|
|
50
|
+
fs_1.default.closeSync(fd);
|
|
51
|
+
position = stat.size;
|
|
52
|
+
const lines = buffer.toString('utf8').split('\n').filter((l) => l.trim() !== '');
|
|
53
|
+
lines.forEach((line) => {
|
|
54
|
+
io.to(streamKey).emit('log-line', { processId, logType, line });
|
|
46
55
|
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
tail.on('close', (code) => {
|
|
53
|
-
console.log(`Tail process exited with code ${code}`);
|
|
54
|
-
delete activeStreams[streamKey];
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error('Error reading log file:', e);
|
|
59
|
+
}
|
|
55
60
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
watcher.on('error', (e) => console.error(`Watcher error for ${streamKey}:`, e));
|
|
62
|
+
// Expose a kill() so existing cleanup code works unchanged
|
|
63
|
+
const streamObj = { kill: () => { watcher.close(); delete activeStreams[streamKey]; } };
|
|
64
|
+
activeStreams[streamKey] = streamObj;
|
|
65
|
+
resolve(streamObj);
|
|
59
66
|
});
|
|
60
67
|
});
|
|
61
68
|
};
|
|
@@ -5,6 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const express_1 = require("express");
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
8
11
|
const pm2_1 = __importDefault(require("pm2"));
|
|
9
12
|
const pm2_connection_1 = require("../utils/pm2-connection");
|
|
10
13
|
const router = (0, express_1.Router)();
|
|
@@ -123,4 +126,56 @@ router.delete('/:moduleName', async (req, res) => {
|
|
|
123
126
|
});
|
|
124
127
|
}
|
|
125
128
|
});
|
|
129
|
+
// @group Utilities : Resolve ~/.pm2/module_conf.json path
|
|
130
|
+
const moduleConfPath = () => path_1.default.join(os_1.default.homedir(), '.pm2', 'module_conf.json');
|
|
131
|
+
// @group APIEndpoints : Get config for a specific installed module
|
|
132
|
+
router.get('/:moduleName/config', (req, res) => {
|
|
133
|
+
var _a;
|
|
134
|
+
const { moduleName } = req.params;
|
|
135
|
+
if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
|
|
136
|
+
return res.status(400).json({ error: 'Invalid module name' });
|
|
137
|
+
}
|
|
138
|
+
const confFile = moduleConfPath();
|
|
139
|
+
if (!fs_1.default.existsSync(confFile)) {
|
|
140
|
+
return res.json({ config: {} });
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const raw = JSON.parse(fs_1.default.readFileSync(confFile, 'utf8'));
|
|
144
|
+
// PM2 stores module config under the module name key
|
|
145
|
+
const config = (_a = raw[moduleName]) !== null && _a !== void 0 ? _a : {};
|
|
146
|
+
res.json({ config });
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
res.status(500).json({ error: 'Failed to read module configuration' });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// @group APIEndpoints : Set one or more config keys for an installed module
|
|
153
|
+
router.put('/:moduleName/config', async (req, res) => {
|
|
154
|
+
const { moduleName } = req.params;
|
|
155
|
+
const { config } = req.body;
|
|
156
|
+
if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
|
|
157
|
+
return res.status(400).json({ error: 'Invalid module name' });
|
|
158
|
+
}
|
|
159
|
+
if (!config || typeof config !== 'object') {
|
|
160
|
+
return res.status(400).json({ error: 'config object is required' });
|
|
161
|
+
}
|
|
162
|
+
// Validate all keys — no shell metacharacters
|
|
163
|
+
for (const key of Object.keys(config)) {
|
|
164
|
+
if (!/^[a-zA-Z0-9_\-.]+$/.test(key)) {
|
|
165
|
+
return res.status(400).json({ error: `Invalid config key: ${key}` });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
// Run pm2 set for each key sequentially
|
|
170
|
+
for (const [key, value] of Object.entries(config)) {
|
|
171
|
+
// Wrap value in quotes to handle spaces; strip any embedded quotes first
|
|
172
|
+
const safeValue = String(value).replace(/"/g, '');
|
|
173
|
+
await runPM2CLI(`set ${moduleName}:${key} "${safeValue}"`);
|
|
174
|
+
}
|
|
175
|
+
res.json({ success: true, message: `Configuration updated for ${moduleName}` });
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
res.status(500).json({ error: 'Failed to update configuration', details: error.message });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
126
181
|
exports.default = router;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
// @group Configuration : Path to the stored auth config file
|
|
11
|
+
const AUTH_FILE = path_1.default.join(__dirname, '../config/auth.json');
|
|
12
|
+
// @group Utilities : Load auth config — returns null when no password is set
|
|
13
|
+
function loadAuthConfig() {
|
|
14
|
+
try {
|
|
15
|
+
if (!fs_1.default.existsSync(AUTH_FILE))
|
|
16
|
+
return null;
|
|
17
|
+
const raw = fs_1.default.readFileSync(AUTH_FILE, 'utf8').trim();
|
|
18
|
+
if (!raw)
|
|
19
|
+
return null;
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// @group Utilities : Persist auth config to disk
|
|
27
|
+
function saveAuthConfig(config) {
|
|
28
|
+
const dir = path_1.default.dirname(AUTH_FILE);
|
|
29
|
+
if (!fs_1.default.existsSync(dir))
|
|
30
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
31
|
+
fs_1.default.writeFileSync(AUTH_FILE, JSON.stringify(config), 'utf8');
|
|
32
|
+
}
|
|
33
|
+
// @group Utilities : Hash a plaintext password with PBKDF2 + salt
|
|
34
|
+
function hashPassword(password, salt) {
|
|
35
|
+
return crypto_1.default.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
|
|
36
|
+
}
|
|
37
|
+
// @group Router : Express router for password-protection endpoints
|
|
38
|
+
const router = express_1.default.Router();
|
|
39
|
+
// @group Endpoints : GET /api/auth/status — is a password/PIN configured?
|
|
40
|
+
router.get('/status', (_req, res) => {
|
|
41
|
+
var _a;
|
|
42
|
+
const config = loadAuthConfig();
|
|
43
|
+
res.json({
|
|
44
|
+
passwordSet: config !== null,
|
|
45
|
+
pinSet: !!(config === null || config === void 0 ? void 0 : config.pinHash),
|
|
46
|
+
autoLockMinutes: (_a = config === null || config === void 0 ? void 0 : config.autoLockMinutes) !== null && _a !== void 0 ? _a : 0,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
// @group Endpoints : PATCH /api/auth/settings — update non-password settings (e.g. autoLockMinutes)
|
|
50
|
+
router.patch('/settings', (req, res) => {
|
|
51
|
+
var _a;
|
|
52
|
+
const { autoLockMinutes } = req.body;
|
|
53
|
+
const config = loadAuthConfig();
|
|
54
|
+
if (!config) {
|
|
55
|
+
return res.status(400).json({ success: false, error: 'No password set — configure a password first' });
|
|
56
|
+
}
|
|
57
|
+
const minutes = typeof autoLockMinutes === 'number' && autoLockMinutes >= 0 ? Math.floor(autoLockMinutes) : (_a = config.autoLockMinutes) !== null && _a !== void 0 ? _a : 0;
|
|
58
|
+
saveAuthConfig({ ...config, autoLockMinutes: minutes });
|
|
59
|
+
res.json({ success: true, autoLockMinutes: minutes });
|
|
60
|
+
});
|
|
61
|
+
// @group Endpoints : POST /api/auth/set — set or change the password
|
|
62
|
+
router.post('/set', (req, res) => {
|
|
63
|
+
var _a;
|
|
64
|
+
const { password, currentPassword } = req.body;
|
|
65
|
+
if (!password || typeof password !== 'string' || password.length < 4) {
|
|
66
|
+
return res.status(400).json({ success: false, error: 'Password must be at least 4 characters' });
|
|
67
|
+
}
|
|
68
|
+
const existing = loadAuthConfig();
|
|
69
|
+
// If a password is already set, require the current one before changing
|
|
70
|
+
if (existing) {
|
|
71
|
+
if (!currentPassword) {
|
|
72
|
+
return res.status(401).json({ success: false, error: 'Current password required to change password' });
|
|
73
|
+
}
|
|
74
|
+
const currentHash = hashPassword(currentPassword, existing.salt);
|
|
75
|
+
if (!crypto_1.default.timingSafeEqual(Buffer.from(currentHash, 'hex'), Buffer.from(existing.hash, 'hex'))) {
|
|
76
|
+
return res.status(401).json({ success: false, error: 'Current password is incorrect' });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const salt = crypto_1.default.randomBytes(32).toString('hex');
|
|
80
|
+
const hash = hashPassword(password, salt);
|
|
81
|
+
// Preserve PIN and autoLock settings when changing password
|
|
82
|
+
saveAuthConfig({
|
|
83
|
+
hash,
|
|
84
|
+
salt,
|
|
85
|
+
autoLockMinutes: (_a = existing === null || existing === void 0 ? void 0 : existing.autoLockMinutes) !== null && _a !== void 0 ? _a : 0,
|
|
86
|
+
...((existing === null || existing === void 0 ? void 0 : existing.pinHash) ? { pinHash: existing.pinHash, pinSalt: existing.pinSalt } : {}),
|
|
87
|
+
});
|
|
88
|
+
res.json({ success: true });
|
|
89
|
+
});
|
|
90
|
+
// @group Endpoints : POST /api/auth/verify — verify a password attempt
|
|
91
|
+
router.post('/verify', (req, res) => {
|
|
92
|
+
const { password } = req.body;
|
|
93
|
+
if (!password || typeof password !== 'string') {
|
|
94
|
+
return res.status(400).json({ success: false, error: 'Password is required' });
|
|
95
|
+
}
|
|
96
|
+
const config = loadAuthConfig();
|
|
97
|
+
if (!config) {
|
|
98
|
+
// No password set — treat as unlocked
|
|
99
|
+
return res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
|
|
100
|
+
}
|
|
101
|
+
const hash = hashPassword(password, config.salt);
|
|
102
|
+
const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.hash, 'hex'));
|
|
103
|
+
if (!match) {
|
|
104
|
+
return res.status(401).json({ success: false, error: 'Incorrect password' });
|
|
105
|
+
}
|
|
106
|
+
res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
|
|
107
|
+
});
|
|
108
|
+
// @group Endpoints : DELETE /api/auth/remove — remove the password (requires current password)
|
|
109
|
+
router.delete('/remove', (req, res) => {
|
|
110
|
+
const { password } = req.body;
|
|
111
|
+
const config = loadAuthConfig();
|
|
112
|
+
if (!config) {
|
|
113
|
+
return res.json({ success: true }); // nothing to remove
|
|
114
|
+
}
|
|
115
|
+
if (!password || typeof password !== 'string') {
|
|
116
|
+
return res.status(400).json({ success: false, error: 'Current password required' });
|
|
117
|
+
}
|
|
118
|
+
const hash = hashPassword(password, config.salt);
|
|
119
|
+
const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.hash, 'hex'));
|
|
120
|
+
if (!match) {
|
|
121
|
+
return res.status(401).json({ success: false, error: 'Incorrect password' });
|
|
122
|
+
}
|
|
123
|
+
fs_1.default.unlinkSync(AUTH_FILE);
|
|
124
|
+
res.json({ success: true });
|
|
125
|
+
});
|
|
126
|
+
// @group Endpoints : POST /api/auth/pin/set — set or change the PIN (4-digit)
|
|
127
|
+
router.post('/pin/set', (req, res) => {
|
|
128
|
+
const { pin } = req.body;
|
|
129
|
+
if (!pin || !/^\d{4}$/.test(pin)) {
|
|
130
|
+
return res.status(400).json({ success: false, error: 'PIN must be exactly 4 digits' });
|
|
131
|
+
}
|
|
132
|
+
const config = loadAuthConfig();
|
|
133
|
+
if (!config) {
|
|
134
|
+
return res.status(400).json({ success: false, error: 'Set a password first before adding a PIN' });
|
|
135
|
+
}
|
|
136
|
+
const pinSalt = crypto_1.default.randomBytes(32).toString('hex');
|
|
137
|
+
const pinHash = hashPassword(pin, pinSalt);
|
|
138
|
+
saveAuthConfig({ ...config, pinHash, pinSalt });
|
|
139
|
+
res.json({ success: true });
|
|
140
|
+
});
|
|
141
|
+
// @group Endpoints : POST /api/auth/pin/verify — verify a PIN attempt
|
|
142
|
+
router.post('/pin/verify', (req, res) => {
|
|
143
|
+
const { pin } = req.body;
|
|
144
|
+
if (!pin || typeof pin !== 'string') {
|
|
145
|
+
return res.status(400).json({ success: false, error: 'PIN is required' });
|
|
146
|
+
}
|
|
147
|
+
const config = loadAuthConfig();
|
|
148
|
+
if (!(config === null || config === void 0 ? void 0 : config.pinHash) || !(config === null || config === void 0 ? void 0 : config.pinSalt)) {
|
|
149
|
+
return res.status(400).json({ success: false, error: 'No PIN configured' });
|
|
150
|
+
}
|
|
151
|
+
const hash = hashPassword(pin, config.pinSalt);
|
|
152
|
+
const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.pinHash, 'hex'));
|
|
153
|
+
if (!match) {
|
|
154
|
+
return res.status(401).json({ success: false, error: 'Incorrect PIN' });
|
|
155
|
+
}
|
|
156
|
+
res.json({ success: true, token: crypto_1.default.randomBytes(32).toString('hex') });
|
|
157
|
+
});
|
|
158
|
+
// @group Endpoints : DELETE /api/auth/pin/remove — remove PIN (requires current password)
|
|
159
|
+
router.delete('/pin/remove', (req, res) => {
|
|
160
|
+
const { password } = req.body;
|
|
161
|
+
const config = loadAuthConfig();
|
|
162
|
+
if (!config) {
|
|
163
|
+
return res.json({ success: true }); // nothing to remove
|
|
164
|
+
}
|
|
165
|
+
if (!password || typeof password !== 'string') {
|
|
166
|
+
return res.status(400).json({ success: false, error: 'Current password required to remove PIN' });
|
|
167
|
+
}
|
|
168
|
+
const hash = hashPassword(password, config.salt);
|
|
169
|
+
const match = crypto_1.default.timingSafeEqual(Buffer.from(hash, 'hex'), Buffer.from(config.hash, 'hex'));
|
|
170
|
+
if (!match) {
|
|
171
|
+
return res.status(401).json({ success: false, error: 'Incorrect password' });
|
|
172
|
+
}
|
|
173
|
+
const { pinHash: _ph, pinSalt: _ps, ...rest } = config;
|
|
174
|
+
saveAuthConfig(rest);
|
|
175
|
+
res.json({ success: true });
|
|
176
|
+
});
|
|
177
|
+
exports.default = router;
|
|
@@ -819,15 +819,19 @@ router.get('/:connectionId/log-file/download', async (req, res) => {
|
|
|
819
819
|
router.get('/connections', async (req, res) => {
|
|
820
820
|
try {
|
|
821
821
|
const connections = remote_connection_1.remoteConnectionManager.getAllConnections();
|
|
822
|
-
const connectionsList = Array.from(connections.entries()).map(([id, conn]) =>
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
822
|
+
const connectionsList = Array.from(connections.entries()).map(([id, conn]) => {
|
|
823
|
+
const connected = conn.isConnected();
|
|
824
|
+
return {
|
|
825
|
+
id,
|
|
826
|
+
name: conn.name || `${conn.username}@${conn.host}`,
|
|
827
|
+
host: conn.host,
|
|
828
|
+
port: conn.port,
|
|
829
|
+
username: conn.username,
|
|
830
|
+
connected,
|
|
831
|
+
isPM2Installed: conn.isPM2Installed,
|
|
832
|
+
status: connected ? 'connected' : 'disconnected',
|
|
833
|
+
};
|
|
834
|
+
});
|
|
831
835
|
res.json(connectionsList);
|
|
832
836
|
}
|
|
833
837
|
catch (error) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const remote_metrics_db_1 = require("../utils/remote-metrics-db");
|
|
8
|
+
const router = express_1.default.Router();
|
|
9
|
+
// @group Constants : Default and maximum query bounds
|
|
10
|
+
const DEFAULT_RANGE_MS = 3600000; // 1 hour
|
|
11
|
+
const MAX_RANGE_MS = 7 * 24 * 3600000; // 7 days
|
|
12
|
+
const MAX_POINTS = 500;
|
|
13
|
+
// @group Utilities : Parse and clamp a query-string integer
|
|
14
|
+
const parseIntParam = (raw, fallback, min, max) => {
|
|
15
|
+
const n = parseInt(raw, 10);
|
|
16
|
+
if (!Number.isFinite(n))
|
|
17
|
+
return fallback;
|
|
18
|
+
return Math.max(min, Math.min(max, n));
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* List all remote connections that have recorded metrics
|
|
22
|
+
* GET /api/remote-metrics/connections
|
|
23
|
+
*/
|
|
24
|
+
router.get('/connections', (_req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const connections = remote_metrics_db_1.remoteMetricsDB.getConnectionsWithData();
|
|
27
|
+
res.json({ success: true, connections });
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
res.status(500).json({ success: false, error: err.message });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* List all process names recorded for a connection
|
|
35
|
+
* GET /api/remote-metrics/:connectionId/processes
|
|
36
|
+
*/
|
|
37
|
+
router.get('/:connectionId/processes', (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
const { connectionId } = req.params;
|
|
40
|
+
const names = remote_metrics_db_1.remoteMetricsDB.getProcessNames(connectionId);
|
|
41
|
+
res.json({ success: true, processes: names });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
res.status(500).json({ success: false, error: err.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Latest snapshot (most recent metric point per process) for a connection
|
|
49
|
+
* GET /api/remote-metrics/:connectionId/snapshot
|
|
50
|
+
*/
|
|
51
|
+
router.get('/:connectionId/snapshot', (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const { connectionId } = req.params;
|
|
54
|
+
const snapshot = remote_metrics_db_1.remoteMetricsDB.getLatestSnapshot(connectionId);
|
|
55
|
+
res.json({ success: true, snapshot });
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
res.status(500).json({ success: false, error: err.message });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Time-series metrics for one process on a connection
|
|
63
|
+
* GET /api/remote-metrics/:connectionId/:processName
|
|
64
|
+
* ?from=<unix-ms> (default: now - 1h)
|
|
65
|
+
* ?to=<unix-ms> (default: now)
|
|
66
|
+
* ?maxPoints=<n> (default: 500, max: 1000)
|
|
67
|
+
*/
|
|
68
|
+
router.get('/:connectionId/:processName', (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const { connectionId, processName } = req.params;
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const to = parseIntParam(req.query.to, now, 0, now + 60000);
|
|
73
|
+
const from = parseIntParam(req.query.from, to - DEFAULT_RANGE_MS, 0, now);
|
|
74
|
+
// Clamp range to max window
|
|
75
|
+
const clampedFrom = Math.max(from, to - MAX_RANGE_MS);
|
|
76
|
+
const maxPoints = parseIntParam(req.query.maxPoints, MAX_POINTS, 50, 1000);
|
|
77
|
+
const metrics = remote_metrics_db_1.remoteMetricsDB.getMetrics(connectionId, processName, clampedFrom, to, maxPoints);
|
|
78
|
+
res.json({ success: true, metrics });
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
res.status(500).json({ success: false, error: err.message });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
exports.default = router;
|
|
@@ -62,7 +62,7 @@ export declare class ProjectSetupService {
|
|
|
62
62
|
private ensureLogDirectory;
|
|
63
63
|
private log;
|
|
64
64
|
detectProjectType(projectPath: string): string | null;
|
|
65
|
-
setupProject(projectPath: string, projectType: string): Promise<SetupResult>;
|
|
65
|
+
setupProject(projectPath: string, projectType: string, onLog?: (msg: string) => void): Promise<SetupResult>;
|
|
66
66
|
private shouldSkipStep;
|
|
67
67
|
private executeStep;
|
|
68
68
|
private validateSetup;
|