Upload 13 files
Browse files- exocore-web/src/console.ts +469 -0
- exocore-web/src/file-manager.ts +730 -0
- exocore-web/src/forgot.ts +88 -0
- exocore-web/src/login.ts +180 -0
- exocore-web/src/logout.ts +73 -0
- exocore-web/src/otp.ts +167 -0
- exocore-web/src/panel.ts +143 -0
- exocore-web/src/project.ts +193 -0
- exocore-web/src/register.ts +186 -0
- exocore-web/src/shell.ts +222 -0
- exocore-web/src/skills.ts +140 -0
- exocore-web/src/userinfo.ts +136 -0
- exocore-web/src/userinfoEdit.ts +147 -0
exocore-web/src/console.ts
ADDED
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import fs from "fs";
|
4 |
+
import path from "path";
|
5 |
+
import { spawn, ChildProcess } from "child_process";
|
6 |
+
import { WebSocketServer, WebSocket, RawData } from 'ws';
|
7 |
+
import http from 'http';
|
8 |
+
import express from 'express';
|
9 |
+
|
10 |
+
const LOG_DIR: string = path.resolve(__dirname, "../models/data");
|
11 |
+
const LOG_FILE: string = path.join(LOG_DIR, "logs.txt");
|
12 |
+
|
13 |
+
let processInstance: ChildProcess | null = null;
|
14 |
+
let isRunning: boolean = false;
|
15 |
+
let isAwaitingInput: boolean = false;
|
16 |
+
|
17 |
+
const INPUT_PROMPT_STRING = "__NEEDS_INPUT__";
|
18 |
+
|
19 |
+
interface ApiType {
|
20 |
+
clearLogFile: () => void;
|
21 |
+
broadcast: (msg: string | Buffer, wss?: WebSocketServer, isError?: boolean, silent?: boolean) => void;
|
22 |
+
parseExocoreRun: (filePath: string) => { exportCommands: string[]; runCommand: string | null };
|
23 |
+
executeCommand: (
|
24 |
+
command: string,
|
25 |
+
cwd: string,
|
26 |
+
onCloseCallback: (outcome: number | string | Error) => void,
|
27 |
+
wss?: WebSocketServer
|
28 |
+
) => ChildProcess | null;
|
29 |
+
runCommandsSequentially: (
|
30 |
+
commands: string[],
|
31 |
+
cwd: string,
|
32 |
+
onSequenceDone: (success: boolean) => void,
|
33 |
+
wss?: WebSocketServer,
|
34 |
+
index?: number
|
35 |
+
) => void;
|
36 |
+
start: (wss?: WebSocketServer, args?: string) => void;
|
37 |
+
stop: (wss?: WebSocketServer) => void;
|
38 |
+
restart: (wss?: WebSocketServer, args?: string) => void;
|
39 |
+
status: () => "running" | "stopped";
|
40 |
+
}
|
41 |
+
|
42 |
+
const api: ApiType = {
|
43 |
+
clearLogFile(): void {
|
44 |
+
if (!fs.existsSync(LOG_DIR)) {
|
45 |
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
46 |
+
}
|
47 |
+
fs.writeFileSync(LOG_FILE, "");
|
48 |
+
},
|
49 |
+
|
50 |
+
broadcast(msg: string | Buffer, wss?: WebSocketServer, isError: boolean = false, silent: boolean = false): void {
|
51 |
+
const data = msg.toString();
|
52 |
+
if (!fs.existsSync(LOG_DIR)) {
|
53 |
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
54 |
+
}
|
55 |
+
if (!data.includes(INPUT_PROMPT_STRING)) {
|
56 |
+
fs.appendFileSync(LOG_FILE, `${data}`);
|
57 |
+
}
|
58 |
+
|
59 |
+
if (!silent && wss && wss.clients) {
|
60 |
+
wss.clients.forEach((client: WebSocket) => {
|
61 |
+
if (client.readyState === WebSocket.OPEN) {
|
62 |
+
if (data.includes(INPUT_PROMPT_STRING)) {
|
63 |
+
const cleanData = data.replace(INPUT_PROMPT_STRING, "").trim();
|
64 |
+
isAwaitingInput = true;
|
65 |
+
client.send(JSON.stringify({ type: 'INPUT_REQUIRED', payload: cleanData }));
|
66 |
+
} else {
|
67 |
+
client.send(data);
|
68 |
+
}
|
69 |
+
}
|
70 |
+
});
|
71 |
+
}
|
72 |
+
if (isError) {
|
73 |
+
console.error(`BROADCAST_ERROR_LOG: ${data.trim()}`);
|
74 |
+
}
|
75 |
+
},
|
76 |
+
|
77 |
+
parseExocoreRun(filePath: string): { exportCommands: string[]; runCommand: string | null } {
|
78 |
+
const raw: string = fs.readFileSync(filePath, "utf8");
|
79 |
+
const exportMatch: RegExpMatchArray | null = raw.match(/export\s*=\s*{([\s\S]*?)}/);
|
80 |
+
const functionMatch: RegExpMatchArray | null = raw.match(/function\s*=\s*{([\s\S]*?)}/);
|
81 |
+
const exportCommands: string[] = [];
|
82 |
+
|
83 |
+
if (exportMatch && exportMatch[1]) {
|
84 |
+
const lines: string[] = exportMatch[1].split(";");
|
85 |
+
for (let line of lines) {
|
86 |
+
const matchResult: RegExpMatchArray | null = line.match(/["'](.+?)["']/);
|
87 |
+
if (matchResult && typeof matchResult[1] === 'string' && matchResult[1].trim() !== '') {
|
88 |
+
exportCommands.push(matchResult[1]);
|
89 |
+
}
|
90 |
+
}
|
91 |
+
}
|
92 |
+
|
93 |
+
let runCommand: string | null = null;
|
94 |
+
if (functionMatch && functionMatch[1]) {
|
95 |
+
const runMatch: RegExpMatchArray | null = functionMatch[1].match(/run\s*=\s*["'](.+?)["']/);
|
96 |
+
if (runMatch && typeof runMatch[1] === 'string' && runMatch[1].trim() !== '') {
|
97 |
+
runCommand = runMatch[1];
|
98 |
+
}
|
99 |
+
}
|
100 |
+
return { exportCommands, runCommand };
|
101 |
+
},
|
102 |
+
|
103 |
+
executeCommand(
|
104 |
+
command: string,
|
105 |
+
cwd: string,
|
106 |
+
onCloseCallback: (outcome: number | string | Error) => void,
|
107 |
+
wss?: WebSocketServer
|
108 |
+
): ChildProcess | null {
|
109 |
+
let proc: ChildProcess | null = null;
|
110 |
+
try {
|
111 |
+
proc = spawn(command, {
|
112 |
+
cwd,
|
113 |
+
shell: true,
|
114 |
+
env: { ...process.env, FORCE_COLOR: "1", LANG: "en_US.UTF-8" },
|
115 |
+
});
|
116 |
+
} catch (rawErr: unknown) {
|
117 |
+
let errMsg = "Unknown spawn error";
|
118 |
+
if (rawErr instanceof Error) errMsg = rawErr.message;
|
119 |
+
else if (typeof rawErr === 'string') errMsg = rawErr;
|
120 |
+
api.broadcast(`\x1b[31m❌ Spawn error for command "${command}": ${errMsg}\x1b[0m`, wss, true, false);
|
121 |
+
if (typeof onCloseCallback === 'function') {
|
122 |
+
if (rawErr instanceof Error) onCloseCallback(rawErr);
|
123 |
+
else onCloseCallback(new Error(String(rawErr)));
|
124 |
+
}
|
125 |
+
return null;
|
126 |
+
}
|
127 |
+
|
128 |
+
if (proc.stdout) {
|
129 |
+
proc.stdout.on("data", (data: Buffer | string) => api.broadcast(data, wss, false, false));
|
130 |
+
}
|
131 |
+
if (proc.stderr) {
|
132 |
+
proc.stderr.on("data", (data: Buffer | string) => api.broadcast(`\x1b[31m${data.toString()}\x1b[0m`, wss, true, false));
|
133 |
+
}
|
134 |
+
|
135 |
+
proc.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
|
136 |
+
if (typeof onCloseCallback === 'function') {
|
137 |
+
if (code === null) onCloseCallback(signal || 'signaled');
|
138 |
+
else onCloseCallback(code);
|
139 |
+
}
|
140 |
+
});
|
141 |
+
|
142 |
+
proc.on("error", (err: Error) => {
|
143 |
+
api.broadcast(`\x1b[31m❌ Command execution error for "${command}": ${err.message}\x1b[0m`, wss, true, false);
|
144 |
+
if (typeof onCloseCallback === 'function') onCloseCallback(err);
|
145 |
+
});
|
146 |
+
return proc;
|
147 |
+
},
|
148 |
+
|
149 |
+
runCommandsSequentially(
|
150 |
+
commands: string[],
|
151 |
+
cwd: string,
|
152 |
+
onSequenceDone: (success: boolean) => void,
|
153 |
+
wss?: WebSocketServer,
|
154 |
+
index: number = 0
|
155 |
+
): void {
|
156 |
+
if (index >= commands.length) {
|
157 |
+
if (typeof onSequenceDone === 'function') onSequenceDone(true);
|
158 |
+
return;
|
159 |
+
}
|
160 |
+
const cmd = commands[index];
|
161 |
+
|
162 |
+
if (typeof cmd !== 'string' || cmd.trim() === "") {
|
163 |
+
api.broadcast(`\x1b[31m❌ Invalid or empty setup command at index ${index}.\x1b[0m`, wss, true, false);
|
164 |
+
if (typeof onSequenceDone === 'function') onSequenceDone(false);
|
165 |
+
return;
|
166 |
+
}
|
167 |
+
|
168 |
+
const proc = api.executeCommand(cmd, cwd, (outcome: number | string | Error) => {
|
169 |
+
const benignSignal = (typeof outcome === 'string' && (outcome.toUpperCase() === 'SIGTERM' || outcome.toUpperCase() === 'SIGKILL'));
|
170 |
+
if (outcome !== 0 && (typeof outcome === 'number' || outcome instanceof Error || (typeof outcome === 'string' && !benignSignal))) {
|
171 |
+
const errorMessage = outcome instanceof Error ? outcome.message : String(outcome);
|
172 |
+
api.broadcast(`\x1b[31m❌ Setup command "${cmd}" failed: ${errorMessage}\x1b[0m`, wss, true, false);
|
173 |
+
if (typeof onSequenceDone === 'function') onSequenceDone(false);
|
174 |
+
return;
|
175 |
+
}
|
176 |
+
api.runCommandsSequentially(commands, cwd, onSequenceDone, wss, index + 1);
|
177 |
+
}, wss
|
178 |
+
);
|
179 |
+
|
180 |
+
if (!proc) {
|
181 |
+
api.broadcast(`\x1b[31m❌ Failed to initiate setup command "${cmd}".\x1b[0m`, wss, true, false);
|
182 |
+
if (typeof onSequenceDone === 'function') onSequenceDone(false);
|
183 |
+
}
|
184 |
+
},
|
185 |
+
|
186 |
+
start(wss?: WebSocketServer, args?: string): void {
|
187 |
+
if (isRunning) {
|
188 |
+
api.broadcast("\x1b[33m⚠️ Process is already running.\x1b[0m", wss, true, false);
|
189 |
+
return;
|
190 |
+
}
|
191 |
+
api.clearLogFile();
|
192 |
+
api.broadcast(`\x1b[36m[SYSTEM] Starting process...\x1b[0m`, wss, false, true);
|
193 |
+
isAwaitingInput = false;
|
194 |
+
|
195 |
+
let projectPath: string | undefined;
|
196 |
+
|
197 |
+
try {
|
198 |
+
const configJsonPath: string = path.resolve(process.cwd(), 'config.json');
|
199 |
+
if (fs.existsSync(configJsonPath)) {
|
200 |
+
const configRaw: string = fs.readFileSync(configJsonPath, 'utf-8');
|
201 |
+
if (configRaw.trim() !== "") {
|
202 |
+
const configData: any = JSON.parse(configRaw);
|
203 |
+
if (configData && typeof configData.project === 'string' && configData.project.trim() !== "") {
|
204 |
+
const configJsonDir: string = path.dirname(configJsonPath);
|
205 |
+
const resolvedPath: string = path.resolve(configJsonDir, configData.project);
|
206 |
+
if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
|
207 |
+
projectPath = resolvedPath;
|
208 |
+
} else {
|
209 |
+
api.broadcast(`\x1b[31m❌ Error: Project path from config.json is invalid: ${resolvedPath}\x1b[0m`, wss, true, false);
|
210 |
+
return;
|
211 |
+
}
|
212 |
+
} else {
|
213 |
+
api.broadcast(`\x1b[31m❌ Error: 'project' key in config.json is missing or invalid.\x1b[0m`, wss, true, false);
|
214 |
+
return;
|
215 |
+
}
|
216 |
+
} else {
|
217 |
+
api.broadcast(`\x1b[31m❌ Error: config.json is empty.\x1b[0m`, wss, true, false);
|
218 |
+
return;
|
219 |
+
}
|
220 |
+
} else {
|
221 |
+
api.broadcast(`\x1b[31m❌ Error: Configuration file not found at ${configJsonPath}.\x1b[0m`, wss, true, false);
|
222 |
+
return;
|
223 |
+
}
|
224 |
+
} catch (rawErr: unknown) {
|
225 |
+
api.broadcast(`\x1b[31m❌ Error reading or parsing project configuration.\x1b[0m`, wss, true, false);
|
226 |
+
return;
|
227 |
+
}
|
228 |
+
|
229 |
+
if (typeof projectPath !== 'string') {
|
230 |
+
api.broadcast(`\x1b[31m❌ Project path could not be determined.\x1b[0m`, wss, true, false);
|
231 |
+
return;
|
232 |
+
}
|
233 |
+
|
234 |
+
const currentProjectPath: string = projectPath;
|
235 |
+
const exocoreRunPath: string = path.join(currentProjectPath, "exocore.run");
|
236 |
+
|
237 |
+
if (!fs.existsSync(exocoreRunPath)) {
|
238 |
+
api.broadcast(`\x1b[31m❌ Missing exocore.run file in ${currentProjectPath}.\x1b[0m`, wss, true, false);
|
239 |
+
return;
|
240 |
+
}
|
241 |
+
|
242 |
+
const { exportCommands, runCommand } = api.parseExocoreRun(exocoreRunPath);
|
243 |
+
|
244 |
+
if (!runCommand) {
|
245 |
+
api.broadcast("\x1b[31m❌ Missing run command in exocore.run.\x1b[0m", wss, true, false);
|
246 |
+
return;
|
247 |
+
}
|
248 |
+
|
249 |
+
const currentRunCommand: string = args ? `${runCommand} ${args}` : runCommand;
|
250 |
+
|
251 |
+
api.runCommandsSequentially([...exportCommands], currentProjectPath, (setupSuccess: boolean) => {
|
252 |
+
if (!setupSuccess) {
|
253 |
+
api.broadcast(`\x1b[31m❌ Setup commands failed. Main process will not start.\x1b[0m`, wss, true, false);
|
254 |
+
return;
|
255 |
+
}
|
256 |
+
|
257 |
+
api.broadcast(`\x1b[36m[SYSTEM] Executing: ${currentRunCommand}\x1b[0m`, wss, false, true);
|
258 |
+
|
259 |
+
let spawnedProc: ChildProcess | null = null;
|
260 |
+
try {
|
261 |
+
spawnedProc = spawn(currentRunCommand, {
|
262 |
+
cwd: currentProjectPath,
|
263 |
+
shell: true,
|
264 |
+
detached: true,
|
265 |
+
env: { ...process.env, FORCE_COLOR: "1", LANG: "en_US.UTF-8" },
|
266 |
+
});
|
267 |
+
} catch (rawErr: unknown) {
|
268 |
+
api.broadcast(`\x1b[31m❌ Failed to spawn main process.\x1b[0m`, wss, true, false);
|
269 |
+
return;
|
270 |
+
}
|
271 |
+
|
272 |
+
if (!spawnedProc || typeof spawnedProc.pid !== 'number') {
|
273 |
+
api.broadcast(`\x1b[31m❌ Failed to get process handle.\x1b[0m`, wss, true, false);
|
274 |
+
return;
|
275 |
+
}
|
276 |
+
|
277 |
+
processInstance = spawnedProc;
|
278 |
+
isRunning = true;
|
279 |
+
api.broadcast(`\x1b[32m[SYSTEM] Process started with PID: ${processInstance.pid}\x1b[0m`, wss, false, true);
|
280 |
+
|
281 |
+
if (processInstance.stdout) {
|
282 |
+
processInstance.stdout.on("data", (data: Buffer | string) => api.broadcast(data, wss, false, false));
|
283 |
+
}
|
284 |
+
if (processInstance.stderr) {
|
285 |
+
processInstance.stderr.on("data", (data: Buffer | string) =>
|
286 |
+
api.broadcast(`\x1b[31m${data.toString()}\x1b[0m`, wss, true, false)
|
287 |
+
);
|
288 |
+
}
|
289 |
+
|
290 |
+
processInstance.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
|
291 |
+
isRunning = false;
|
292 |
+
processInstance = null;
|
293 |
+
isAwaitingInput = false;
|
294 |
+
const exitReason = signal ? `signal ${signal}` : `code ${code}`;
|
295 |
+
api.broadcast(`\x1b[33m[SYSTEM] Process exited with ${exitReason}. It will not be restarted automatically.\x1b[0m`, wss, false, false);
|
296 |
+
});
|
297 |
+
|
298 |
+
processInstance.on("error", (err: Error) => {
|
299 |
+
api.broadcast(`\x1b[31m❌ Error with main process: ${err.message}\x1b[0m`, wss, true, false);
|
300 |
+
isRunning = false;
|
301 |
+
processInstance = null;
|
302 |
+
isAwaitingInput = false;
|
303 |
+
});
|
304 |
+
}, wss);
|
305 |
+
},
|
306 |
+
|
307 |
+
stop(wss?: WebSocketServer): void {
|
308 |
+
if (!processInstance || typeof processInstance.pid !== 'number') {
|
309 |
+
api.broadcast("\x1b[33m⚠️ No active process to stop.\x1b[0m", wss, true, false);
|
310 |
+
if (isRunning) {
|
311 |
+
isRunning = false;
|
312 |
+
processInstance = null;
|
313 |
+
}
|
314 |
+
return;
|
315 |
+
}
|
316 |
+
|
317 |
+
if (!isRunning) {
|
318 |
+
api.broadcast("\x1b[33m⚠️ Process is already stopped.\x1b[0m", wss, true, false);
|
319 |
+
return;
|
320 |
+
}
|
321 |
+
|
322 |
+
const pidToStop: number = processInstance.pid;
|
323 |
+
api.broadcast(`\x1b[36m[SYSTEM] Stopping process group PID: ${pidToStop}...\x1b[0m`, wss, false, true);
|
324 |
+
|
325 |
+
try {
|
326 |
+
process.kill(-pidToStop, "SIGTERM");
|
327 |
+
|
328 |
+
setTimeout(() => {
|
329 |
+
if (processInstance && processInstance.pid === pidToStop && !processInstance.killed) {
|
330 |
+
api.broadcast(`\x1b[33m[SYSTEM] ⚠️ Process ${pidToStop} unresponsive, sending SIGKILL.\x1b[0m`, wss, true, false);
|
331 |
+
try {
|
332 |
+
process.kill(-pidToStop, "SIGKILL");
|
333 |
+
} catch (rawKillErr: unknown) {
|
334 |
+
const err = rawKillErr as NodeJS.ErrnoException;
|
335 |
+
if (err.code !== 'ESRCH') {
|
336 |
+
api.broadcast(`\x1b[31m[SYSTEM] ❌ Error sending SIGKILL to PID ${pidToStop}: ${err.message}\x1b[0m`, wss, true, false);
|
337 |
+
}
|
338 |
+
} finally {
|
339 |
+
if (processInstance && processInstance.pid === pidToStop) {
|
340 |
+
isRunning = false;
|
341 |
+
processInstance = null;
|
342 |
+
}
|
343 |
+
}
|
344 |
+
}
|
345 |
+
}, 3000);
|
346 |
+
} catch (rawTermErr: unknown) {
|
347 |
+
const err = rawTermErr as NodeJS.ErrnoException;
|
348 |
+
if (err.code !== 'ESRCH') {
|
349 |
+
api.broadcast(`\x1b[31m❌ Error sending SIGTERM: ${err.message}\x1b[0m`, wss, true, false);
|
350 |
+
}
|
351 |
+
isRunning = false;
|
352 |
+
processInstance = null;
|
353 |
+
}
|
354 |
+
},
|
355 |
+
|
356 |
+
restart(wss?: WebSocketServer, args?: string): void {
|
357 |
+
api.broadcast(`\x1b[36m[SYSTEM] Restarting process...\x1b[0m`, wss, false, true);
|
358 |
+
api.stop(wss);
|
359 |
+
setTimeout(() => {
|
360 |
+
api.start(wss, args);
|
361 |
+
}, 1000);
|
362 |
+
},
|
363 |
+
|
364 |
+
status(): "running" | "stopped" {
|
365 |
+
if (isRunning && processInstance && !processInstance.killed) {
|
366 |
+
try {
|
367 |
+
process.kill(processInstance.pid!, 0);
|
368 |
+
return "running";
|
369 |
+
} catch (e) {
|
370 |
+
isRunning = false;
|
371 |
+
processInstance = null;
|
372 |
+
return "stopped";
|
373 |
+
}
|
374 |
+
}
|
375 |
+
return "stopped";
|
376 |
+
},
|
377 |
+
};
|
378 |
+
|
379 |
+
export interface RouteHandlerParamsSuperset {
|
380 |
+
app?: express.Application;
|
381 |
+
req: express.Request;
|
382 |
+
res: express.Response;
|
383 |
+
wss?: WebSocketServer;
|
384 |
+
wssConsole?: WebSocketServer;
|
385 |
+
Shellwss?: WebSocketServer;
|
386 |
+
server?: http.Server;
|
387 |
+
}
|
388 |
+
|
389 |
+
export interface ExpressRouteModule {
|
390 |
+
method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
|
391 |
+
path: string;
|
392 |
+
install: (params: Partial<RouteHandlerParamsSuperset>) => void;
|
393 |
+
}
|
394 |
+
|
395 |
+
type StartStopRestartParams = Pick<RouteHandlerParamsSuperset, 'req' | 'res' | 'wssConsole'>;
|
396 |
+
type ConsoleStatusParams = Pick<RouteHandlerParamsSuperset, 'res'>;
|
397 |
+
|
398 |
+
export const modules: Array<{
|
399 |
+
method: "get" | "post";
|
400 |
+
path: string;
|
401 |
+
install: (params: any) => void;
|
402 |
+
}> = [
|
403 |
+
{
|
404 |
+
method: "post",
|
405 |
+
path: "/start",
|
406 |
+
install: ({ req, res, wssConsole }: StartStopRestartParams) => {
|
407 |
+
if (!wssConsole) return res.status(500).send("Console WebSocket server not available.");
|
408 |
+
api.start(wssConsole, req.body?.args);
|
409 |
+
res.send(`Process start initiated.`);
|
410 |
+
},
|
411 |
+
},
|
412 |
+
{
|
413 |
+
method: "post",
|
414 |
+
path: "/stop",
|
415 |
+
install: ({ res, wssConsole }: StartStopRestartParams) => {
|
416 |
+
if (!wssConsole) return res.status(500).send("Console WebSocket server not available.");
|
417 |
+
api.stop(wssConsole);
|
418 |
+
res.send(`Process stop initiated.`);
|
419 |
+
},
|
420 |
+
},
|
421 |
+
{
|
422 |
+
method: "post",
|
423 |
+
path: "/restart",
|
424 |
+
install: ({ req, res, wssConsole }: StartStopRestartParams) => {
|
425 |
+
if (!wssConsole) return res.status(500).send("Console WebSocket server not available.");
|
426 |
+
api.restart(wssConsole, req.body?.args);
|
427 |
+
res.send(`Process restart initiated.`);
|
428 |
+
},
|
429 |
+
},
|
430 |
+
{
|
431 |
+
method: "get",
|
432 |
+
path: "/console/status",
|
433 |
+
install: ({ res }: ConsoleStatusParams) => {
|
434 |
+
res.send(api.status());
|
435 |
+
},
|
436 |
+
},
|
437 |
+
];
|
438 |
+
|
439 |
+
export function setupConsoleWS(wssConsole: WebSocketServer): void {
|
440 |
+
wssConsole.on("connection", (ws: WebSocket) => {
|
441 |
+
console.log("Console WebSocket client connected");
|
442 |
+
try {
|
443 |
+
const logContent: string = fs.existsSync(LOG_FILE) ? fs.readFileSync(LOG_FILE, "utf8") : "\x1b[33mℹ️ No previous logs.\x1b[0m";
|
444 |
+
ws.send(logContent);
|
445 |
+
} catch (err) {
|
446 |
+
ws.send("\x1b[31mError reading past logs.\x1b[0m");
|
447 |
+
}
|
448 |
+
|
449 |
+
ws.on("message", (rawMessage: RawData) => {
|
450 |
+
try {
|
451 |
+
const message = JSON.parse(rawMessage.toString());
|
452 |
+
|
453 |
+
if (message.type === 'STDIN_INPUT' && typeof message.payload === 'string') {
|
454 |
+
if (processInstance && isRunning && isAwaitingInput && processInstance.stdin && processInstance.stdin.writable) {
|
455 |
+
processInstance.stdin.write(message.payload + '\n');
|
456 |
+
isAwaitingInput = false;
|
457 |
+
} else {
|
458 |
+
api.broadcast(`\x1b[33m[SYSTEM-WARN] Process not running or not awaiting input.\x1b[0m`, wssConsole, true, false);
|
459 |
+
}
|
460 |
+
}
|
461 |
+
} catch (e) {
|
462 |
+
console.log(`Received non-command message from client: ${rawMessage.toString()}`);
|
463 |
+
}
|
464 |
+
});
|
465 |
+
|
466 |
+
ws.on("close", () => console.log("Console WebSocket client disconnected"));
|
467 |
+
ws.on("error", (error: Error) => console.error("Console WebSocket client error:", error));
|
468 |
+
});
|
469 |
+
}
|
exocore-web/src/file-manager.ts
ADDED
@@ -0,0 +1,730 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import fs from 'fs';
|
2 |
+
import path from 'path';
|
3 |
+
import archiver, { ArchiverError } from 'archiver';
|
4 |
+
import multer, { Multer } from 'multer';
|
5 |
+
import extract from 'extract-zip';
|
6 |
+
import yauzl, { Options as YauzlOptions, ZipFile as YauzlZipFile, Entry as YauzlEntry } from 'yauzl';
|
7 |
+
import { Request, Response, Application, RequestHandler } from 'express';
|
8 |
+
|
9 |
+
const configJsonPath: string = path.resolve(__dirname, '../config.json');
|
10 |
+
const UPLOAD_TEMP_DIR: string = path.resolve(__dirname, '../upload_temp');
|
11 |
+
|
12 |
+
let currentProjectPathFromConfig: string = '';
|
13 |
+
let currentBaseDir: string = '';
|
14 |
+
|
15 |
+
async function pathExists(p: string): Promise<boolean> {
|
16 |
+
try {
|
17 |
+
await fs.promises.access(p, fs.constants.F_OK);
|
18 |
+
return true;
|
19 |
+
} catch {
|
20 |
+
return false;
|
21 |
+
}
|
22 |
+
}
|
23 |
+
|
24 |
+
async function ensureDirExists(dirPath: string): Promise<void> {
|
25 |
+
if (dirPath && !(await pathExists(dirPath))) {
|
26 |
+
await fs.promises.mkdir(dirPath, { recursive: true });
|
27 |
+
}
|
28 |
+
}
|
29 |
+
|
30 |
+
ensureDirExists(UPLOAD_TEMP_DIR).catch(err => {
|
31 |
+
console.error("FATAL: Failed to create UPLOAD_TEMP_DIR on startup:", err);
|
32 |
+
});
|
33 |
+
|
34 |
+
interface AppConfig {
|
35 |
+
project?: string;
|
36 |
+
[key: string]: any;
|
37 |
+
}
|
38 |
+
|
39 |
+
function updatePathsFromConfig(): boolean {
|
40 |
+
const oldRawProjectPath = currentProjectPathFromConfig;
|
41 |
+
const oldBaseDir = currentBaseDir;
|
42 |
+
const wasPreviouslyValid = oldBaseDir !== '';
|
43 |
+
|
44 |
+
let newRawProjectPathCandidate: string = '';
|
45 |
+
let newResolvedBaseDirCandidate: string = '';
|
46 |
+
let isNowValidConfig: boolean = false;
|
47 |
+
|
48 |
+
try {
|
49 |
+
if (!fs.existsSync(configJsonPath)) {
|
50 |
+
newRawProjectPathCandidate = '';
|
51 |
+
newResolvedBaseDirCandidate = '';
|
52 |
+
isNowValidConfig = false;
|
53 |
+
} else {
|
54 |
+
const rawConfigData: string = fs.readFileSync(configJsonPath, 'utf-8');
|
55 |
+
if (rawConfigData.trim() === '') {
|
56 |
+
newRawProjectPathCandidate = '';
|
57 |
+
newResolvedBaseDirCandidate = '';
|
58 |
+
isNowValidConfig = false;
|
59 |
+
} else {
|
60 |
+
const parsedConfig: AppConfig = JSON.parse(rawConfigData);
|
61 |
+
if (parsedConfig && typeof parsedConfig.project === 'string') {
|
62 |
+
newRawProjectPathCandidate = parsedConfig.project;
|
63 |
+
const trimmedProjectPath: string = parsedConfig.project.trim();
|
64 |
+
if (trimmedProjectPath !== '') {
|
65 |
+
const configJsonDir: string = path.dirname(configJsonPath);
|
66 |
+
newResolvedBaseDirCandidate = path.resolve(configJsonDir, trimmedProjectPath);
|
67 |
+
isNowValidConfig = true;
|
68 |
+
} else {
|
69 |
+
newResolvedBaseDirCandidate = '';
|
70 |
+
isNowValidConfig = false;
|
71 |
+
}
|
72 |
+
} else {
|
73 |
+
newRawProjectPathCandidate = '';
|
74 |
+
newResolvedBaseDirCandidate = '';
|
75 |
+
isNowValidConfig = false;
|
76 |
+
}
|
77 |
+
}
|
78 |
+
}
|
79 |
+
} catch (error: any) {
|
80 |
+
const errMsg = error instanceof Error ? error.message : String(error);
|
81 |
+
console.error(`[ConfigMonitor] ERROR reading/parsing ${configJsonPath}: ${errMsg}`);
|
82 |
+
newRawProjectPathCandidate = '';
|
83 |
+
newResolvedBaseDirCandidate = '';
|
84 |
+
isNowValidConfig = false;
|
85 |
+
}
|
86 |
+
|
87 |
+
currentProjectPathFromConfig = newRawProjectPathCandidate;
|
88 |
+
currentBaseDir = isNowValidConfig ? newResolvedBaseDirCandidate : '';
|
89 |
+
|
90 |
+
if (isNowValidConfig) {
|
91 |
+
if (!wasPreviouslyValid) {
|
92 |
+
console.log(`[ConfigMonitor] PROJECT LOADED: Project path "${currentProjectPathFromConfig.trim()}" configured. Base directory: ${currentBaseDir}`);
|
93 |
+
} else if (currentBaseDir !== oldBaseDir) {
|
94 |
+
console.log(`[ConfigMonitor] PROJECT CHANGED: Project path updated to "${currentProjectPathFromConfig.trim()}". New base directory: ${currentBaseDir}`);
|
95 |
+
}
|
96 |
+
} else {
|
97 |
+
if (wasPreviouslyValid) {
|
98 |
+
console.warn(`[ConfigMonitor] PROJECT UNCONFIGURED: Project path "${oldRawProjectPath}" (was base: "${oldBaseDir}") is now invalid, removed, or config error. Waiting for configuration.`);
|
99 |
+
}
|
100 |
+
}
|
101 |
+
return isNowValidConfig;
|
102 |
+
}
|
103 |
+
|
104 |
+
function startConfigMonitor(): void {
|
105 |
+
console.log(`[ConfigMonitor] Initializing: Monitoring ${configJsonPath} for project configuration.`);
|
106 |
+
const initiallyValid = updatePathsFromConfig();
|
107 |
+
|
108 |
+
if (!initiallyValid) {
|
109 |
+
console.warn(`[ConfigMonitor] Initial Status: No valid project path found in ${configJsonPath}. Waiting for configuration.`);
|
110 |
+
}
|
111 |
+
|
112 |
+
setInterval(() => {
|
113 |
+
updatePathsFromConfig();
|
114 |
+
}, 1000);
|
115 |
+
}
|
116 |
+
|
117 |
+
startConfigMonitor();
|
118 |
+
|
119 |
+
const upload: Multer = multer({ dest: UPLOAD_TEMP_DIR });
|
120 |
+
|
121 |
+
function runMulterMiddleware(req: Request, res: Response, multerMiddleware: RequestHandler): Promise<void> {
|
122 |
+
return new Promise<void>((resolve, reject) => {
|
123 |
+
multerMiddleware(req, res, (err: any) => {
|
124 |
+
if (err) {
|
125 |
+
return reject(err);
|
126 |
+
}
|
127 |
+
resolve();
|
128 |
+
});
|
129 |
+
});
|
130 |
+
}
|
131 |
+
|
132 |
+
async function safePathResolve(relativePathInput: string | undefined | null, checkExists: boolean = false): Promise<string> {
|
133 |
+
if (!currentBaseDir) {
|
134 |
+
console.error("[safePathResolve] Error: Attempted to operate while project base directory is not configured.");
|
135 |
+
throw new Error('Project base directory is not configured. Please check config.json and ensure the "project" key is set to a valid path.');
|
136 |
+
}
|
137 |
+
|
138 |
+
const activeBaseDir: string = path.resolve(currentBaseDir);
|
139 |
+
|
140 |
+
if (relativePathInput === '') {
|
141 |
+
await ensureDirExists(activeBaseDir);
|
142 |
+
if (checkExists && !(await pathExists(activeBaseDir))) {
|
143 |
+
throw new Error(`Base project directory not found: ${activeBaseDir}`);
|
144 |
+
}
|
145 |
+
return activeBaseDir;
|
146 |
+
}
|
147 |
+
|
148 |
+
if (typeof relativePathInput !== 'string' || relativePathInput.trim() === '') {
|
149 |
+
throw new Error('Invalid path provided: path is empty or not a string (and not an intentional empty string for base directory).');
|
150 |
+
}
|
151 |
+
|
152 |
+
const trimmedRelativePath: string = relativePathInput.trim();
|
153 |
+
const resolvedPath: string = path.resolve(activeBaseDir, trimmedRelativePath);
|
154 |
+
|
155 |
+
if (!resolvedPath.startsWith(activeBaseDir + path.sep) && resolvedPath !== activeBaseDir) {
|
156 |
+
console.error(`[safePathResolve] Unsafe path attempt: Resolved "${resolvedPath}" is outside base "${activeBaseDir}" from relative "${trimmedRelativePath}"`);
|
157 |
+
throw new Error('Unsafe path: Access denied due to path traversal attempt.');
|
158 |
+
}
|
159 |
+
|
160 |
+
if (checkExists && !(await pathExists(resolvedPath))) {
|
161 |
+
const displayPath: string = path.relative(activeBaseDir, resolvedPath) || trimmedRelativePath;
|
162 |
+
console.warn(`[safePathResolve] Path not found: "${displayPath}" (resolved from "${trimmedRelativePath}" within base "${activeBaseDir}")`);
|
163 |
+
throw new Error(`Path not found: ${displayPath}`);
|
164 |
+
}
|
165 |
+
return resolvedPath;
|
166 |
+
}
|
167 |
+
|
168 |
+
interface FileManagerRouteParams {
|
169 |
+
req: Request;
|
170 |
+
res: Response;
|
171 |
+
app?: Application;
|
172 |
+
}
|
173 |
+
|
174 |
+
type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
|
175 |
+
|
176 |
+
interface FileManagerExpressRouteModule {
|
177 |
+
method: HttpMethod;
|
178 |
+
path: string;
|
179 |
+
install: (params: FileManagerRouteParams) => Promise<void> | void;
|
180 |
+
}
|
181 |
+
|
182 |
+
export const modules: FileManagerExpressRouteModule[] = [
|
183 |
+
{
|
184 |
+
method: 'post',
|
185 |
+
path: '/file/list', // <--- Here it is
|
186 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
187 |
+
try {
|
188 |
+
const dirToList: string = await safePathResolve('');
|
189 |
+
|
190 |
+
const itemNames: string[] = await fs.promises.readdir(dirToList);
|
191 |
+
const items = await Promise.all(itemNames.map(async (name) => {
|
192 |
+
const fullPath: string = path.join(dirToList, name);
|
193 |
+
const stat: fs.Stats = await fs.promises.stat(fullPath);
|
194 |
+
return { name, isDir: stat.isDirectory() };
|
195 |
+
}));
|
196 |
+
res.json(items);
|
197 |
+
} catch (e: any) {
|
198 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
199 |
+
console.error('Error in /file/list:', errMsg, e);
|
200 |
+
if (errMsg.includes('Project base directory is not configured')) {
|
201 |
+
res.status(503).send(errMsg);
|
202 |
+
} else if (e instanceof Error && e.message.startsWith('Path not found')) {
|
203 |
+
res.status(404).send("Root project directory not found or accessible.");
|
204 |
+
} else {
|
205 |
+
res.status(500).send(errMsg);
|
206 |
+
}
|
207 |
+
}
|
208 |
+
},
|
209 |
+
},
|
210 |
+
|
211 |
+
{
|
212 |
+
method: 'post',
|
213 |
+
path: '/file/list',
|
214 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
215 |
+
try {
|
216 |
+
const dirToList: string = await safePathResolve('');
|
217 |
+
|
218 |
+
const itemNames: string[] = await fs.promises.readdir(dirToList);
|
219 |
+
const items = await Promise.all(itemNames.map(async (name) => {
|
220 |
+
const fullPath: string = path.join(dirToList, name);
|
221 |
+
const stat: fs.Stats = await fs.promises.stat(fullPath);
|
222 |
+
return { name, isDir: stat.isDirectory() };
|
223 |
+
}));
|
224 |
+
res.json(items);
|
225 |
+
} catch (e: any) {
|
226 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
227 |
+
console.error('Error in /file/list:', errMsg, e);
|
228 |
+
if (errMsg.includes('Project base directory is not configured')) {
|
229 |
+
res.status(503).send(errMsg);
|
230 |
+
} else if (e instanceof Error && e.message.startsWith('Path not found')) {
|
231 |
+
res.status(404).send("Root project directory not found or accessible.");
|
232 |
+
} else {
|
233 |
+
res.status(500).send(errMsg);
|
234 |
+
}
|
235 |
+
}
|
236 |
+
},
|
237 |
+
},
|
238 |
+
{
|
239 |
+
method: 'post',
|
240 |
+
path: '/file/open',
|
241 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
242 |
+
try {
|
243 |
+
const filePath: string = await safePathResolve(req.body.file, true);
|
244 |
+
const content: string = await fs.promises.readFile(filePath, 'utf8');
|
245 |
+
res.send(content);
|
246 |
+
} catch (e: any) {
|
247 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
248 |
+
console.error('Error in /file/open:', errMsg, e);
|
249 |
+
let statusCode: number = 500;
|
250 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
251 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
252 |
+
else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
|
253 |
+
res.status(statusCode).send(errMsg);
|
254 |
+
}
|
255 |
+
},
|
256 |
+
},
|
257 |
+
{
|
258 |
+
method: 'post',
|
259 |
+
path: '/file/save',
|
260 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
261 |
+
try {
|
262 |
+
const fileRelativePath: string = req.body.file;
|
263 |
+
const content: string = req.body.content;
|
264 |
+
|
265 |
+
if (typeof content !== 'string') {
|
266 |
+
res.status(400).send('Invalid content for file save.');
|
267 |
+
return;
|
268 |
+
}
|
269 |
+
const filePath: string = await safePathResolve(fileRelativePath, false);
|
270 |
+
const dirForFile: string = path.dirname(filePath);
|
271 |
+
await ensureDirExists(dirForFile);
|
272 |
+
|
273 |
+
await fs.promises.writeFile(filePath, content, 'utf8');
|
274 |
+
res.send('File saved');
|
275 |
+
} catch (e: any) {
|
276 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
277 |
+
console.error('Error in /file/save:', errMsg, e);
|
278 |
+
let statusCode: number = 500;
|
279 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
280 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
281 |
+
res.status(statusCode).send(errMsg);
|
282 |
+
}
|
283 |
+
},
|
284 |
+
},
|
285 |
+
{
|
286 |
+
method: 'post',
|
287 |
+
path: '/file/upload',
|
288 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
289 |
+
try {
|
290 |
+
if (!currentBaseDir) {
|
291 |
+
throw new Error('Project base directory is not configured. Cannot accept uploads.');
|
292 |
+
}
|
293 |
+
await runMulterMiddleware(req, res, upload.single('file'));
|
294 |
+
|
295 |
+
if (!req.file) {
|
296 |
+
res.status(400).send('No file uploaded.');
|
297 |
+
return;
|
298 |
+
}
|
299 |
+
const uploadedFile: Express.Multer.File = req.file as Express.Multer.File;
|
300 |
+
|
301 |
+
const relativeTargetSubfolder: string = typeof req.body.path === 'string' && req.body.path.trim() !== ''
|
302 |
+
? req.body.path.trim()
|
303 |
+
: '';
|
304 |
+
|
305 |
+
const targetDirectory: string = await safePathResolve(relativeTargetSubfolder, false);
|
306 |
+
|
307 |
+
try {
|
308 |
+
const targetStat: fs.Stats = await fs.promises.stat(targetDirectory);
|
309 |
+
if (!targetStat.isDirectory()) {
|
310 |
+
await fs.promises.unlink(uploadedFile.path);
|
311 |
+
res.status(400).send('Target path exists but is not a directory.');
|
312 |
+
return;
|
313 |
+
}
|
314 |
+
} catch (statError: any) {
|
315 |
+
if (statError && typeof statError === 'object' && 'code' in statError && statError.code === 'ENOENT') {
|
316 |
+
await ensureDirExists(targetDirectory);
|
317 |
+
} else {
|
318 |
+
await fs.promises.unlink(uploadedFile.path);
|
319 |
+
throw statError;
|
320 |
+
}
|
321 |
+
}
|
322 |
+
|
323 |
+
const finalDestination: string = path.join(targetDirectory, uploadedFile.originalname);
|
324 |
+
if (!finalDestination.startsWith(targetDirectory + path.sep) && finalDestination !== targetDirectory) {
|
325 |
+
await fs.promises.unlink(uploadedFile.path);
|
326 |
+
throw new Error("Upload failed: constructed final path is outside target directory.");
|
327 |
+
}
|
328 |
+
|
329 |
+
await fs.promises.rename(uploadedFile.path, finalDestination);
|
330 |
+
const displayPath: string = path.relative(path.resolve(currentBaseDir), finalDestination).split(path.sep).join('/') || uploadedFile.originalname;
|
331 |
+
res.send('File uploaded to ' + displayPath);
|
332 |
+
|
333 |
+
} catch (e: any) {
|
334 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
335 |
+
console.error('Error in /file/upload handler:', errMsg, e);
|
336 |
+
const currentReqFile = req.file as Express.Multer.File | undefined;
|
337 |
+
if (currentReqFile && currentReqFile.path && await pathExists(currentReqFile.path)) {
|
338 |
+
await fs.promises.unlink(currentReqFile.path).catch(unlinkErr => console.error("Failed to cleanup temp upload file:", (unlinkErr as Error).message));
|
339 |
+
}
|
340 |
+
if (!res.headersSent) {
|
341 |
+
let statusCode: number = 500;
|
342 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
343 |
+
else if (e instanceof multer.MulterError) statusCode = 400;
|
344 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
345 |
+
res.status(statusCode).send(errMsg);
|
346 |
+
}
|
347 |
+
}
|
348 |
+
},
|
349 |
+
},
|
350 |
+
{
|
351 |
+
method: 'post',
|
352 |
+
path: '/file/download',
|
353 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
354 |
+
try {
|
355 |
+
const filePath: string = await safePathResolve(req.body.file, true);
|
356 |
+
const stat: fs.Stats = await fs.promises.stat(filePath);
|
357 |
+
if (!stat.isFile()) {
|
358 |
+
res.status(400).send('Specified path is not a file.');
|
359 |
+
return;
|
360 |
+
}
|
361 |
+
res.download(filePath);
|
362 |
+
} catch (e: any) {
|
363 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
364 |
+
console.error('Error in /file/download:', errMsg, e);
|
365 |
+
let statusCode: number = 500;
|
366 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
367 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
368 |
+
else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
|
369 |
+
res.status(statusCode).send(errMsg);
|
370 |
+
}
|
371 |
+
},
|
372 |
+
},
|
373 |
+
{
|
374 |
+
method: 'post',
|
375 |
+
path: '/file/download-zip',
|
376 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
377 |
+
try {
|
378 |
+
const folderRelativePath: string = typeof req.body.folder === 'string' ? req.body.folder.trim() : '';
|
379 |
+
const folderToZip: string = await safePathResolve(folderRelativePath, true);
|
380 |
+
|
381 |
+
const stat: fs.Stats = await fs.promises.stat(folderToZip);
|
382 |
+
if (!stat.isDirectory()) {
|
383 |
+
res.status(400).send('Specified path is not a directory.');
|
384 |
+
return;
|
385 |
+
}
|
386 |
+
|
387 |
+
const zipName: string = (path.basename(folderToZip) || 'archive') + '.zip';
|
388 |
+
res.setHeader('Content-Disposition', `attachment; filename="${zipName}"`);
|
389 |
+
res.setHeader('Content-Type', 'application/zip');
|
390 |
+
|
391 |
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
392 |
+
|
393 |
+
archive.on('warning', (err: ArchiverError) => {
|
394 |
+
console.warn('[Archiver Warning]', err.code, err.message);
|
395 |
+
});
|
396 |
+
archive.on('error', (err: Error) => {
|
397 |
+
console.error('[Archiver Error]', err.message, err);
|
398 |
+
if (!res.headersSent) {
|
399 |
+
res.status(500).send({ error: 'Failed to create zip file: ' + err.message });
|
400 |
+
} else if (res.writable && !res.writableEnded) {
|
401 |
+
res.end();
|
402 |
+
}
|
403 |
+
});
|
404 |
+
archive.pipe(res);
|
405 |
+
archive.directory(folderToZip, false);
|
406 |
+
await archive.finalize();
|
407 |
+
|
408 |
+
} catch (e: any) {
|
409 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
410 |
+
console.error('Error in /file/download-zip setup:', errMsg, e);
|
411 |
+
if (!res.headersSent) {
|
412 |
+
let statusCode: number = 500;
|
413 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
414 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
415 |
+
else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
|
416 |
+
res.status(statusCode).send(errMsg);
|
417 |
+
}
|
418 |
+
}
|
419 |
+
},
|
420 |
+
},
|
421 |
+
{
|
422 |
+
method: 'post',
|
423 |
+
path: '/file/unzip',
|
424 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
425 |
+
const {
|
426 |
+
zipFilePath: zipFileRelativePath,
|
427 |
+
destinationPath: destinationRelativePathInput,
|
428 |
+
overwrite
|
429 |
+
}: { zipFilePath: string; destinationPath?: string; overwrite?: boolean } = req.body;
|
430 |
+
|
431 |
+
try {
|
432 |
+
if (typeof zipFileRelativePath !== 'string' || zipFileRelativePath.trim() === '') {
|
433 |
+
res.status(400).send('zipFilePath is required.'); return;
|
434 |
+
}
|
435 |
+
const absoluteZipFilePath: string = await safePathResolve(zipFileRelativePath.trim(), true);
|
436 |
+
if (!(await fs.promises.stat(absoluteZipFilePath)).isFile()) {
|
437 |
+
res.status(400).send(`Specified zipFilePath '${zipFileRelativePath}' is not a file.`); return;
|
438 |
+
}
|
439 |
+
if (!absoluteZipFilePath.toLowerCase().endsWith('.zip')) {
|
440 |
+
console.warn(`[Unzip] Warning: File '${zipFileRelativePath}' may not be a .zip file based on extension. Attempting unzip.`);
|
441 |
+
}
|
442 |
+
|
443 |
+
let absoluteDestinationDir: string;
|
444 |
+
const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir);
|
445 |
+
|
446 |
+
if (typeof destinationRelativePathInput === 'string' && destinationRelativePathInput.trim() !== '') {
|
447 |
+
absoluteDestinationDir = await safePathResolve(destinationRelativePathInput.trim(), false);
|
448 |
+
} else {
|
449 |
+
absoluteDestinationDir = path.dirname(absoluteZipFilePath);
|
450 |
+
if (!absoluteDestinationDir.startsWith(resolvedCurrentBaseDir + path.sep) && absoluteDestinationDir !== resolvedCurrentBaseDir) {
|
451 |
+
console.error(`[Unzip] Default destination dir "${absoluteDestinationDir}" is outside base "${resolvedCurrentBaseDir}"`);
|
452 |
+
throw new Error('Default extraction path is outside the allowed project directory.');
|
453 |
+
}
|
454 |
+
}
|
455 |
+
|
456 |
+
try {
|
457 |
+
const destStat: fs.Stats = await fs.promises.stat(absoluteDestinationDir);
|
458 |
+
if (!destStat.isDirectory()) {
|
459 |
+
res.status(400).send(`Destination path '${path.relative(resolvedCurrentBaseDir, absoluteDestinationDir).split(path.sep).join('/') || '.'}' exists and is not a directory.`); return;
|
460 |
+
}
|
461 |
+
} catch (statError: any) {
|
462 |
+
if (statError && typeof statError === 'object' && 'code' in statError && statError.code === 'ENOENT') {
|
463 |
+
} else {
|
464 |
+
throw statError;
|
465 |
+
}
|
466 |
+
}
|
467 |
+
await ensureDirExists(absoluteDestinationDir);
|
468 |
+
|
469 |
+
if (!overwrite) {
|
470 |
+
const conflictingEntries: string[] = [];
|
471 |
+
const openZipForRead = (filePath: string, options: YauzlOptions): Promise<YauzlZipFile> =>
|
472 |
+
new Promise<YauzlZipFile>((resolveMain, rejectMain) => {
|
473 |
+
yauzl.open(filePath, options, (err: Error | null, zipfile?: YauzlZipFile) => {
|
474 |
+
if (err || !zipfile) rejectMain(err || new Error("Failed to open zipfile. Path: " + filePath));
|
475 |
+
else resolveMain(zipfile);
|
476 |
+
});
|
477 |
+
});
|
478 |
+
|
479 |
+
let zipfile: YauzlZipFile | null = null;
|
480 |
+
try {
|
481 |
+
zipfile = await openZipForRead(absoluteZipFilePath, { lazyEntries: true });
|
482 |
+
const currentZipfile: YauzlZipFile = zipfile;
|
483 |
+
await new Promise<void>((resolveLoop, rejectLoop) => {
|
484 |
+
currentZipfile.on('error', rejectLoop);
|
485 |
+
currentZipfile.on('end', resolveLoop);
|
486 |
+
currentZipfile.on('entry', async (entry: YauzlEntry) => {
|
487 |
+
const targetPath: string = path.resolve(absoluteDestinationDir, entry.fileName);
|
488 |
+
if (!targetPath.startsWith(absoluteDestinationDir)) {
|
489 |
+
console.warn(`[Unzip] Skipped potentially unsafe entry during conflict check: ${entry.fileName} (resolves outside destination)`);
|
490 |
+
currentZipfile.readEntry();
|
491 |
+
return;
|
492 |
+
}
|
493 |
+
if (await pathExists(targetPath)) {
|
494 |
+
const existingStat: fs.Stats = await fs.promises.stat(targetPath);
|
495 |
+
const entryIsDirectory: boolean = entry.fileName.endsWith('/');
|
496 |
+
if (existingStat.isDirectory() !== entryIsDirectory) {
|
497 |
+
conflictingEntries.push(`${entry.fileName} (type mismatch: existing is ${existingStat.isDirectory() ? 'dir' : 'file'}, zip entry is ${entryIsDirectory ? 'dir' : 'file'})`);
|
498 |
+
} else if (!entryIsDirectory) {
|
499 |
+
conflictingEntries.push(entry.fileName);
|
500 |
+
}
|
501 |
+
}
|
502 |
+
currentZipfile.readEntry();
|
503 |
+
});
|
504 |
+
currentZipfile.readEntry();
|
505 |
+
});
|
506 |
+
} finally {
|
507 |
+
zipfile?.close();
|
508 |
+
}
|
509 |
+
|
510 |
+
if (conflictingEntries.length > 0) {
|
511 |
+
res.status(409).send({
|
512 |
+
message: "Extraction would overwrite or conflict with existing files/directories. Please use 'overwrite: true' in your request to proceed.",
|
513 |
+
conflicts: conflictingEntries
|
514 |
+
});
|
515 |
+
return;
|
516 |
+
}
|
517 |
+
}
|
518 |
+
|
519 |
+
await extract(absoluteZipFilePath, { dir: absoluteDestinationDir });
|
520 |
+
const displayDestPath: string = path.relative(resolvedCurrentBaseDir, absoluteDestinationDir).split(path.sep).join('/') || '.';
|
521 |
+
res.send(`Successfully unzipped '${path.basename(zipFileRelativePath)}' to '${displayDestPath}'`);
|
522 |
+
|
523 |
+
} catch (e: any) {
|
524 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
525 |
+
let statusCode: number = 500;
|
526 |
+
let clientMsg: string = `Failed to unzip: ${errMsg}`;
|
527 |
+
|
528 |
+
if (errMsg.includes('Project base directory is not configured')) { statusCode = 503; clientMsg = errMsg; }
|
529 |
+
else if (e instanceof Error) {
|
530 |
+
if (e.message.startsWith('Unsafe path')) { statusCode = 403; clientMsg = errMsg; }
|
531 |
+
else if (e.message.startsWith('Path not found')) { statusCode = 404; clientMsg = errMsg; }
|
532 |
+
else if (e.message.includes('is not a directory')) { statusCode = 400; clientMsg = errMsg; }
|
533 |
+
else if (errMsg.toLowerCase().includes("invalid") && (errMsg.toLowerCase().includes("zip") || errMsg.toLowerCase().includes("archive"))) {
|
534 |
+
statusCode = 400; clientMsg = `Failed to unzip '${zipFileRelativePath || 'file'}': May be corrupted or not a valid ZIP.`;
|
535 |
+
} else if (errMsg.includes("yauzl") && (errMsg.includes("Не найден указанный файл") || errMsg.includes("end of central directory record signature not found") || errMsg.includes("Failed to open zipfile"))) {
|
536 |
+
statusCode = 400; clientMsg = `Failed to read '${zipFileRelativePath || 'file'}': Invalid or corrupted ZIP file.`;
|
537 |
+
} else if (e.message.includes('EEXIST') || e.message.includes('ENOTDIR') || (e.message.includes('entry') && e.message.includes('isDirectory') && e.message.includes('false but is a directory'))) {
|
538 |
+
statusCode = 409;
|
539 |
+
clientMsg = `Failed to unzip: A conflict occurred during extraction (e.g., a file exists where a directory was expected, or vice-versa). Original error: ${errMsg}`;
|
540 |
+
}
|
541 |
+
}
|
542 |
+
console.error(`Error in /file/unzip (HTTP ${statusCode}):`, errMsg, e);
|
543 |
+
if (!res.headersSent) res.status(statusCode).send(clientMsg);
|
544 |
+
}
|
545 |
+
},
|
546 |
+
},
|
547 |
+
{
|
548 |
+
method: 'post',
|
549 |
+
path: '/file/create',
|
550 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
551 |
+
try {
|
552 |
+
const fileRelativePath: string = req.body.file;
|
553 |
+
if (typeof fileRelativePath !== 'string' || fileRelativePath.trim() === '') {
|
554 |
+
res.status(400).send('File path is required.'); return;
|
555 |
+
}
|
556 |
+
const filePath: string = await safePathResolve(fileRelativePath.trim(), false);
|
557 |
+
if (await pathExists(filePath)) {
|
558 |
+
res.status(400).send('File or folder already exists at the target path.'); return;
|
559 |
+
}
|
560 |
+
await ensureDirExists(path.dirname(filePath));
|
561 |
+
await fs.promises.writeFile(filePath, '');
|
562 |
+
res.send('File created: ' + (path.relative(path.resolve(currentBaseDir), filePath).split(path.sep).join('/') || path.basename(filePath)));
|
563 |
+
} catch (e: any) {
|
564 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
565 |
+
console.error('Error in /file/create:', errMsg, e);
|
566 |
+
let statusCode: number = 500;
|
567 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
568 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
569 |
+
res.status(statusCode).send(errMsg);
|
570 |
+
}
|
571 |
+
},
|
572 |
+
},
|
573 |
+
{
|
574 |
+
method: 'post',
|
575 |
+
path: '/file/create-folder',
|
576 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
577 |
+
try {
|
578 |
+
const folderRelativePath: string = req.body.folder;
|
579 |
+
if (typeof folderRelativePath !== 'string' || folderRelativePath.trim() === '') {
|
580 |
+
res.status(400).send('Folder path is required.'); return;
|
581 |
+
}
|
582 |
+
const folderPath: string = await safePathResolve(folderRelativePath.trim(), false);
|
583 |
+
if (await pathExists(folderPath)) {
|
584 |
+
res.status(400).send('File or folder already exists at the target path.'); return;
|
585 |
+
}
|
586 |
+
await fs.promises.mkdir(folderPath, { recursive: true });
|
587 |
+
res.send('Folder created: ' + (path.relative(path.resolve(currentBaseDir), folderPath).split(path.sep).join('/') || path.basename(folderPath)));
|
588 |
+
} catch (e: any) {
|
589 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
590 |
+
console.error('Error in /file/create-folder:', errMsg, e);
|
591 |
+
let statusCode: number = 500;
|
592 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
593 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
594 |
+
res.status(statusCode).send(errMsg);
|
595 |
+
}
|
596 |
+
},
|
597 |
+
},
|
598 |
+
{
|
599 |
+
method: 'post',
|
600 |
+
path: '/file/open-folder',
|
601 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
602 |
+
try {
|
603 |
+
const relativeFolderPath: string = typeof req.body.folder === 'string' ? req.body.folder.trim() : '';
|
604 |
+
const folderPath: string = await safePathResolve(relativeFolderPath, true);
|
605 |
+
|
606 |
+
if (!(await fs.promises.stat(folderPath)).isDirectory()) {
|
607 |
+
res.status(400).send(`Specified path '${relativeFolderPath}' is not a directory.`); return;
|
608 |
+
}
|
609 |
+
const itemNames: string[] = await fs.promises.readdir(folderPath);
|
610 |
+
const items = await Promise.all(itemNames.map(async (name) => {
|
611 |
+
const fullPath: string = path.join(folderPath, name);
|
612 |
+
const itemStat: fs.Stats = await fs.promises.stat(fullPath);
|
613 |
+
return { name, isDir: itemStat.isDirectory(), size: itemStat.size, lastModified: itemStat.mtimeMs };
|
614 |
+
}));
|
615 |
+
|
616 |
+
const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir);
|
617 |
+
const currentDisplayPath: string = path.relative(resolvedCurrentBaseDir, folderPath).split(path.sep).join('/');
|
618 |
+
|
619 |
+
res.json({
|
620 |
+
currentPath: folderPath === resolvedCurrentBaseDir ? '' : currentDisplayPath,
|
621 |
+
items,
|
622 |
+
});
|
623 |
+
} catch (e: any) {
|
624 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
625 |
+
console.error('Error in /file/open-folder:', errMsg, e);
|
626 |
+
let statusCode: number = 500;
|
627 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
628 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
629 |
+
else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
|
630 |
+
res.status(statusCode).send(errMsg);
|
631 |
+
}
|
632 |
+
},
|
633 |
+
},
|
634 |
+
{
|
635 |
+
method: 'post',
|
636 |
+
path: '/file/rename',
|
637 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
638 |
+
try {
|
639 |
+
const { oldPath: oldRelativePath, newPath: newRelativePath }: { oldPath: string; newPath: string } = req.body;
|
640 |
+
if (typeof oldRelativePath !== 'string' || !oldRelativePath.trim() ||
|
641 |
+
typeof newRelativePath !== 'string' || !newRelativePath.trim()) {
|
642 |
+
res.status(400).send('Old and new paths are required and cannot be empty.'); return;
|
643 |
+
}
|
644 |
+
|
645 |
+
const trimmedOldRelativePath: string = oldRelativePath.trim();
|
646 |
+
const trimmedNewRelativePath: string = newRelativePath.trim();
|
647 |
+
|
648 |
+
if (trimmedOldRelativePath === trimmedNewRelativePath) {
|
649 |
+
res.status(400).send('Old and new paths cannot be the same.'); return;
|
650 |
+
}
|
651 |
+
|
652 |
+
const oldFullPath: string = await safePathResolve(trimmedOldRelativePath, true);
|
653 |
+
|
654 |
+
const newName: string = path.basename(trimmedNewRelativePath);
|
655 |
+
if (!newName || newName === '.' || newName === '..') {
|
656 |
+
res.status(400).send('New name is invalid.'); return;
|
657 |
+
}
|
658 |
+
const newParentDirRelative: string = path.dirname(trimmedNewRelativePath);
|
659 |
+
const newParentDirAbsolute: string = await safePathResolve(newParentDirRelative, false);
|
660 |
+
await ensureDirExists(newParentDirAbsolute);
|
661 |
+
|
662 |
+
const newFullPath: string = path.join(newParentDirAbsolute, newName);
|
663 |
+
|
664 |
+
const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir);
|
665 |
+
if (!newFullPath.startsWith(resolvedCurrentBaseDir + path.sep) && newFullPath !== resolvedCurrentBaseDir) {
|
666 |
+
throw new Error('Unsafe new path: Target is outside the allowed directory.');
|
667 |
+
}
|
668 |
+
|
669 |
+
if (oldFullPath === newFullPath) {
|
670 |
+
res.status(400).send('Resolved old and new paths are identical.'); return;
|
671 |
+
}
|
672 |
+
if (await pathExists(newFullPath)) {
|
673 |
+
res.status(400).send(`Item at new path '${trimmedNewRelativePath}' already exists.`); return;
|
674 |
+
}
|
675 |
+
|
676 |
+
await fs.promises.rename(oldFullPath, newFullPath);
|
677 |
+
res.send('Renamed successfully to ' + (path.relative(resolvedCurrentBaseDir, newFullPath).split(path.sep).join('/') || newName));
|
678 |
+
} catch (e: any) {
|
679 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
680 |
+
console.error('Error in /file/rename:', errMsg, e);
|
681 |
+
let statusCode: number = 500;
|
682 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
683 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
684 |
+
else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
|
685 |
+
res.status(statusCode).send(errMsg);
|
686 |
+
}
|
687 |
+
},
|
688 |
+
},
|
689 |
+
{
|
690 |
+
method: 'post',
|
691 |
+
path: '/file/delete',
|
692 |
+
install: async ({ req, res }: FileManagerRouteParams) => {
|
693 |
+
try {
|
694 |
+
const itemRelativePath: string = req.body.path;
|
695 |
+
if (typeof itemRelativePath !== 'string' || !itemRelativePath.trim()) {
|
696 |
+
res.status(400).send('Path for deletion is required.'); return;
|
697 |
+
}
|
698 |
+
const trimmedItemRelativePath: string = itemRelativePath.trim();
|
699 |
+
if (trimmedItemRelativePath === '.' || trimmedItemRelativePath === '/') {
|
700 |
+
res.status(400).send('Invalid path for deletion. Cannot delete root.'); return;
|
701 |
+
}
|
702 |
+
|
703 |
+
const itemFullPath: string = await safePathResolve(trimmedItemRelativePath, true);
|
704 |
+
const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir);
|
705 |
+
|
706 |
+
if (itemFullPath === resolvedCurrentBaseDir) {
|
707 |
+
res.status(400).send('Cannot delete the root project directory itself.'); return;
|
708 |
+
}
|
709 |
+
|
710 |
+
const stat: fs.Stats = await fs.promises.stat(itemFullPath);
|
711 |
+
const displayPath: string = path.relative(resolvedCurrentBaseDir, itemFullPath).split(path.sep).join('/') || trimmedItemRelativePath;
|
712 |
+
|
713 |
+
if (stat.isDirectory()) {
|
714 |
+
await fs.promises.rm(itemFullPath, { recursive: true, force: true });
|
715 |
+
} else {
|
716 |
+
await fs.promises.unlink(itemFullPath);
|
717 |
+
}
|
718 |
+
res.send(`${stat.isDirectory() ? 'Folder' : 'File'} deleted: ${displayPath}`);
|
719 |
+
} catch (e: any) {
|
720 |
+
const errMsg: string = e instanceof Error ? e.message : String(e);
|
721 |
+
console.error('Error in /file/delete:', errMsg, e);
|
722 |
+
let statusCode: number = 500;
|
723 |
+
if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
|
724 |
+
else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
|
725 |
+
else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
|
726 |
+
res.status(statusCode).send(errMsg);
|
727 |
+
}
|
728 |
+
},
|
729 |
+
},
|
730 |
+
];
|
exocore-web/src/forgot.ts
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import axios, { AxiosResponse } from 'axios';
|
4 |
+
import fs from 'fs';
|
5 |
+
import fsPromises from 'fs/promises';
|
6 |
+
import path from 'path';
|
7 |
+
import { Request, Response } from 'express';
|
8 |
+
|
9 |
+
const ACC_FILE_PATH = path.resolve(__dirname, '../models/data/acc.json');
|
10 |
+
const ACC_DIR_PATH = path.dirname(ACC_FILE_PATH);
|
11 |
+
|
12 |
+
const api = {
|
13 |
+
getLink: async (): Promise<string> => {
|
14 |
+
const res = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
|
15 |
+
if (typeof res.data === 'string' && res.data.startsWith('http')) {
|
16 |
+
return res.data;
|
17 |
+
}
|
18 |
+
if (res.data && typeof res.data === 'object' && typeof res.data.link === 'string') {
|
19 |
+
return res.data.link;
|
20 |
+
}
|
21 |
+
throw new Error('Failed to fetch external link from pastebin');
|
22 |
+
},
|
23 |
+
|
24 |
+
forgotPass: async (req: Request, res: Response) => {
|
25 |
+
try {
|
26 |
+
const { identifier, action, otpCode, newPass } = req.body;
|
27 |
+
|
28 |
+
if (!identifier || !action) {
|
29 |
+
return res.status(400).json({ error: 'Missing identifier or action' });
|
30 |
+
}
|
31 |
+
|
32 |
+
const link = await api.getLink();
|
33 |
+
const apiURL = `${link}/forgot-password`;
|
34 |
+
|
35 |
+
const body: Record<string, any> = { identifier };
|
36 |
+
|
37 |
+
if (action === 'SendOtp') {
|
38 |
+
body.action = 'sent';
|
39 |
+
} else if (action === 'ResetPassword') {
|
40 |
+
if (!otpCode || !newPass) {
|
41 |
+
return res.status(400).json({ error: 'Missing otpCode or newPass for reset' });
|
42 |
+
}
|
43 |
+
body.action = 'submit';
|
44 |
+
body.otpCode = otpCode;
|
45 |
+
body.newPass = newPass;
|
46 |
+
} else if (action === 'VerifyOtp') {
|
47 |
+
if (!otpCode) {
|
48 |
+
return res.status(400).json({ error: 'Missing otpCode for verification' });
|
49 |
+
}
|
50 |
+
body.action = 'verify';
|
51 |
+
body.otpCode = otpCode;
|
52 |
+
} else {
|
53 |
+
return res.status(400).json({ error: 'Invalid action' });
|
54 |
+
}
|
55 |
+
|
56 |
+
const response: AxiosResponse = await axios.post(apiURL, body);
|
57 |
+
const data = response.data;
|
58 |
+
|
59 |
+
if (data.status === 'success' || data.message) {
|
60 |
+
try {
|
61 |
+
if (!fs.existsSync(ACC_DIR_PATH)) {
|
62 |
+
await fsPromises.mkdir(ACC_DIR_PATH, { recursive: true });
|
63 |
+
}
|
64 |
+
await fsPromises.writeFile(ACC_FILE_PATH, JSON.stringify(data, null, 2));
|
65 |
+
} catch (err) {
|
66 |
+
console.error('[ForgotPass Route] Failed to write acc.json:', err);
|
67 |
+
}
|
68 |
+
return res.json({ success: true, data });
|
69 |
+
} else {
|
70 |
+
return res.status(400).json({ success: false, error: data.error || 'Unexpected error' });
|
71 |
+
}
|
72 |
+
} catch (err) {
|
73 |
+
const msg = err instanceof Error ? err.message : String(err);
|
74 |
+
console.error('[ForgotPass Route] Error:', msg);
|
75 |
+
return res.status(500).json({ error: 'Server error', details: msg });
|
76 |
+
}
|
77 |
+
}
|
78 |
+
};
|
79 |
+
|
80 |
+
export const modules = [
|
81 |
+
{
|
82 |
+
method: 'post',
|
83 |
+
path: '/forgotpass',
|
84 |
+
install: async ({ req, res }: { req: Request; res: Response }) => {
|
85 |
+
return api.forgotPass(req, res);
|
86 |
+
},
|
87 |
+
},
|
88 |
+
];
|
exocore-web/src/login.ts
ADDED
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import axios, { AxiosError, AxiosResponse } from 'axios';
|
4 |
+
import fs from 'fs';
|
5 |
+
import fsPromises from 'fs/promises';
|
6 |
+
import path from 'path';
|
7 |
+
import { Request, Response } from 'express';
|
8 |
+
|
9 |
+
console.log(`[Login Module] src/login.ts file is being evaluated. __dirname is: ${__dirname}`);
|
10 |
+
|
11 |
+
interface PastebinData {
|
12 |
+
link?: string;
|
13 |
+
[key: string]: any; // To accommodate if res.data is an object with more fields
|
14 |
+
}
|
15 |
+
|
16 |
+
interface LoginClientRequestBody {
|
17 |
+
user: string;
|
18 |
+
pass: string;
|
19 |
+
}
|
20 |
+
|
21 |
+
interface LoginApiPayload {
|
22 |
+
user: string;
|
23 |
+
pass: string;
|
24 |
+
}
|
25 |
+
|
26 |
+
interface LoginApiResponseData {
|
27 |
+
token: string;
|
28 |
+
userDetails: object; // Consider defining a more specific interface if the structure is known
|
29 |
+
message: string;
|
30 |
+
status: string; // e.g., "success"
|
31 |
+
}
|
32 |
+
|
33 |
+
function isLoginClientRequestBody(body: any): body is LoginClientRequestBody {
|
34 |
+
if (body && typeof body === 'object' &&
|
35 |
+
typeof body.user === 'string' && body.user.length > 0 &&
|
36 |
+
typeof body.pass === 'string' && body.pass.length > 0) {
|
37 |
+
return true;
|
38 |
+
}
|
39 |
+
return false;
|
40 |
+
}
|
41 |
+
|
42 |
+
const ACC_FILE_PATH: string = path.resolve(__dirname, '../models/data/acc.json');
|
43 |
+
const ACC_DIR_PATH: string = path.dirname(ACC_FILE_PATH);
|
44 |
+
|
45 |
+
const api = {
|
46 |
+
async getLink(): Promise<string> {
|
47 |
+
console.log("[Login API - getLink] Attempting to fetch link...");
|
48 |
+
try {
|
49 |
+
const res: AxiosResponse<PastebinData | string> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
|
50 |
+
let extractedLink: string | null = null;
|
51 |
+
|
52 |
+
if (res.data && typeof res.data === 'object' && typeof res.data.link === 'string') {
|
53 |
+
extractedLink = res.data.link;
|
54 |
+
} else if (typeof res.data === 'string' && res.data.startsWith('http')) {
|
55 |
+
extractedLink = res.data;
|
56 |
+
}
|
57 |
+
|
58 |
+
if (!extractedLink) {
|
59 |
+
console.error("[Login API - getLink] Invalid/missing link in Pastebin response:", res.data);
|
60 |
+
throw new Error('Invalid or missing link in Pastebin response data.');
|
61 |
+
}
|
62 |
+
console.log("[Login API - getLink] Fetched link:", extractedLink);
|
63 |
+
return extractedLink;
|
64 |
+
} catch (err: unknown) {
|
65 |
+
const error = err instanceof Error ? err : new Error(String(err));
|
66 |
+
console.error("[Login API - getLink] Failed to get base link.", error.message, error.cause || error);
|
67 |
+
throw new Error('Failed to get base link.', { cause: error });
|
68 |
+
}
|
69 |
+
},
|
70 |
+
|
71 |
+
async login(body: LoginApiPayload): Promise<LoginApiResponseData> {
|
72 |
+
console.log("[Login API - login] Attempting external API login.");
|
73 |
+
try {
|
74 |
+
const link = await api.getLink();
|
75 |
+
console.log("[Login API - login] External API Link:", link);
|
76 |
+
console.log("[Login API - login] Payload to external API:", body);
|
77 |
+
|
78 |
+
const res: AxiosResponse<LoginApiResponseData> = await axios.post(`${link}/signin`, body);
|
79 |
+
console.log("[Login API - login] Response from external API:", res.data);
|
80 |
+
return res.data;
|
81 |
+
} catch (err: unknown) {
|
82 |
+
const error = err instanceof Error ? err : new Error(String(err));
|
83 |
+
console.error("[Login API - login] Error with /signin endpoint.", error.message, error.cause || error);
|
84 |
+
if (axios.isAxiosError(error) && error.response) {
|
85 |
+
console.error("[Login API - login] Axios error data:", error.response.data, "Status:", error.response.status);
|
86 |
+
}
|
87 |
+
throw new Error('Failed to contact/process signin endpoint.', { cause: error });
|
88 |
+
}
|
89 |
+
},
|
90 |
+
};
|
91 |
+
|
92 |
+
interface LoginRouteParams {
|
93 |
+
req: Request<any, any, unknown>; // Request body is unknown, will use typeguard
|
94 |
+
res: Response;
|
95 |
+
}
|
96 |
+
|
97 |
+
interface LoginExpressRouteModule {
|
98 |
+
method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
|
99 |
+
path: string;
|
100 |
+
install: (params: LoginRouteParams) => Promise<void> | void;
|
101 |
+
}
|
102 |
+
|
103 |
+
export const modules: LoginExpressRouteModule[] = [
|
104 |
+
{
|
105 |
+
method: 'post',
|
106 |
+
path: '/login',
|
107 |
+
install: async ({ req, res }: LoginRouteParams): Promise<void> => {
|
108 |
+
try {
|
109 |
+
const requestBody: unknown = req.body;
|
110 |
+
|
111 |
+
if (isLoginClientRequestBody(requestBody)) {
|
112 |
+
const clientData: LoginClientRequestBody = requestBody;
|
113 |
+
|
114 |
+
const apiLoginPayload: LoginApiPayload = {
|
115 |
+
user: clientData.user,
|
116 |
+
pass: clientData.pass,
|
117 |
+
};
|
118 |
+
|
119 |
+
const responseData: LoginApiResponseData = await api.login(apiLoginPayload);
|
120 |
+
|
121 |
+
if (responseData && responseData.status === 'success' && responseData.token) {
|
122 |
+
console.log("[Login Route] External login successful.");
|
123 |
+
try {
|
124 |
+
if (!fs.existsSync(ACC_DIR_PATH)) {
|
125 |
+
await fsPromises.mkdir(ACC_DIR_PATH, { recursive: true });
|
126 |
+
}
|
127 |
+
await fsPromises.writeFile(ACC_FILE_PATH, JSON.stringify(responseData, null, 2));
|
128 |
+
} catch (fileErr: unknown) {
|
129 |
+
const fError = fileErr instanceof Error ? fileErr : new Error(String(fileErr));
|
130 |
+
console.error(`[Login Route] CRITICAL: Failed to write acc.json:`, fError);
|
131 |
+
}
|
132 |
+
res.json({ success: true, data: responseData });
|
133 |
+
} else {
|
134 |
+
console.warn("[Login Route] External login failed or unexpected response:", responseData);
|
135 |
+
res.status(401).json({ success: false, error: responseData?.message || 'Login failed.' });
|
136 |
+
}
|
137 |
+
} else {
|
138 |
+
console.warn("[Login Route] Validation failed: req.body does not conform to LoginClientRequestBody. Received:", requestBody);
|
139 |
+
res.status(400).json({ success: false, error: 'Invalid request format: user and pass are required strings.' });
|
140 |
+
return;
|
141 |
+
}
|
142 |
+
} catch (err: unknown) {
|
143 |
+
let error: Error & { cause?: unknown };
|
144 |
+
if (err instanceof Error) {
|
145 |
+
error = err as Error & { cause?: unknown };
|
146 |
+
} else {
|
147 |
+
error = new Error(String(err)) as Error & { cause?: unknown };
|
148 |
+
}
|
149 |
+
|
150 |
+
let errorMessage = "An unknown error occurred.";
|
151 |
+
let statusCode = 500;
|
152 |
+
console.error(`[Login Route] Overall error:`, error.message, error.cause || error);
|
153 |
+
|
154 |
+
const originalError: unknown = error.cause || error;
|
155 |
+
|
156 |
+
if (axios.isAxiosError(originalError)) {
|
157 |
+
const axiosError = originalError as AxiosError<any>; // Type assertion
|
158 |
+
errorMessage = axiosError.message;
|
159 |
+
statusCode = axiosError.response?.status || 500;
|
160 |
+
const responseDataError = axiosError.response?.data?.error || axiosError.response?.data?.message;
|
161 |
+
if (responseDataError) {
|
162 |
+
errorMessage = `Login API Error (${statusCode}): ${responseDataError}`;
|
163 |
+
}
|
164 |
+
} else if (originalError instanceof Error) {
|
165 |
+
errorMessage = originalError.message || String(originalError);
|
166 |
+
} else {
|
167 |
+
errorMessage = String(originalError);
|
168 |
+
}
|
169 |
+
|
170 |
+
|
171 |
+
console.error(`[Login Route] Error to client: ${errorMessage}, Status: ${statusCode}`);
|
172 |
+
if (!res.headersSent) {
|
173 |
+
res.status(statusCode).json({ success: false, error: errorMessage });
|
174 |
+
} else {
|
175 |
+
console.warn("[Login Route] Headers sent, cannot send error to client.");
|
176 |
+
}
|
177 |
+
}
|
178 |
+
},
|
179 |
+
},
|
180 |
+
];
|
exocore-web/src/logout.ts
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import fs from 'fs';
|
4 |
+
import path from 'path';
|
5 |
+
import { Request, Response, Application } from 'express';
|
6 |
+
import { WebSocketServer } from 'ws';
|
7 |
+
import { Server as HttpServer } from 'http';
|
8 |
+
|
9 |
+
interface LogoutRouteParams {
|
10 |
+
req: Request;
|
11 |
+
res: Response;
|
12 |
+
app?: Application;
|
13 |
+
wss?: WebSocketServer;
|
14 |
+
wssConsole?: WebSocketServer;
|
15 |
+
Shellwss?: WebSocketServer;
|
16 |
+
server?: HttpServer;
|
17 |
+
}
|
18 |
+
|
19 |
+
interface LogoutExpressRouteModule {
|
20 |
+
method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
|
21 |
+
path: string;
|
22 |
+
install: (params: Pick<LogoutRouteParams, 'req' | 'res'>) => Promise<void> | void;
|
23 |
+
}
|
24 |
+
|
25 |
+
const ACC_FILE_PATH: string = path.resolve(__dirname, '../models/data/acc.json');
|
26 |
+
const ACC_DIR_PATH: string = path.dirname(ACC_FILE_PATH);
|
27 |
+
|
28 |
+
export const modules: LogoutExpressRouteModule[] = [
|
29 |
+
{
|
30 |
+
method: 'post',
|
31 |
+
path: '/logout',
|
32 |
+
install: async ({ req, res }: Pick<LogoutRouteParams, 'req' | 'res'>): Promise<void> => {
|
33 |
+
const html: string = `
|
34 |
+
<!DOCTYPE html>
|
35 |
+
<html lang="en">
|
36 |
+
<head>
|
37 |
+
<meta charset="UTF-8" />
|
38 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
39 |
+
<title>Logout</title>
|
40 |
+
<script>
|
41 |
+
localStorage.removeItem("exocore-token");
|
42 |
+
localStorage.removeItem("exocore-cookies");
|
43 |
+
// Optional: Redirect after clearing localStorage
|
44 |
+
// window.location.href = '/login'; // Or your desired redirect path
|
45 |
+
</script>
|
46 |
+
<style>
|
47 |
+
body { font-family: monospace, sans-serif; padding: 20px; }
|
48 |
+
pre { white-space: pre-wrap; word-wrap: break-word; }
|
49 |
+
</style>
|
50 |
+
</head>
|
51 |
+
<body>
|
52 |
+
<p>You have been logged out. localStorage items 'exocore-token' and 'exocore-cookies' have been cleared.</p>
|
53 |
+
<pre>{
|
54 |
+
"status": "success"
|
55 |
+
}</pre>
|
56 |
+
</body>
|
57 |
+
</html>`;
|
58 |
+
|
59 |
+
res.setHeader('Content-Type', 'text/html');
|
60 |
+
res.send(html);
|
61 |
+
|
62 |
+
try {
|
63 |
+
if (!fs.existsSync(ACC_DIR_PATH)) {
|
64 |
+
await fs.promises.mkdir(ACC_DIR_PATH, { recursive: true });
|
65 |
+
}
|
66 |
+
await fs.promises.writeFile(ACC_FILE_PATH, JSON.stringify({}, null, 2));
|
67 |
+
} catch (fileError: unknown) {
|
68 |
+
const errMsg = fileError instanceof Error ? fileError.message : String(fileError);
|
69 |
+
console.error(`Failed to clear or write acc.json during logout: ${errMsg}`, fileError);
|
70 |
+
}
|
71 |
+
},
|
72 |
+
},
|
73 |
+
];
|
exocore-web/src/otp.ts
ADDED
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import axios, { AxiosResponse, AxiosError } from 'axios';
|
4 |
+
import { Request, Response, Application } from 'express';
|
5 |
+
import { WebSocketServer } from 'ws';
|
6 |
+
import { Server as HttpServer } from 'http';
|
7 |
+
|
8 |
+
interface PastebinResponse {
|
9 |
+
link: string;
|
10 |
+
}
|
11 |
+
|
12 |
+
interface OtpRequestBody {
|
13 |
+
token: string;
|
14 |
+
cookies: Record<string, any> | string;
|
15 |
+
action: string;
|
16 |
+
otp?: string;
|
17 |
+
}
|
18 |
+
|
19 |
+
interface OtpApiResponseData {
|
20 |
+
success: boolean;
|
21 |
+
message: string;
|
22 |
+
data?: Record<string, any> | any[];
|
23 |
+
}
|
24 |
+
|
25 |
+
const api = {
|
26 |
+
async getLink(): Promise<string> {
|
27 |
+
try {
|
28 |
+
const res: AxiosResponse<PastebinResponse | string> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
|
29 |
+
if (res.data && typeof res.data === 'object' && typeof (res.data as PastebinResponse).link === 'string') {
|
30 |
+
return (res.data as PastebinResponse).link;
|
31 |
+
} else if (typeof res.data === 'string' && res.data.startsWith('http')) {
|
32 |
+
return res.data;
|
33 |
+
}
|
34 |
+
throw new Error('Invalid or missing link in Pastebin response data.');
|
35 |
+
} catch (err: unknown) {
|
36 |
+
throw new Error('Failed to fetch API link.', { cause: err instanceof Error ? err : new Error(String(err)) });
|
37 |
+
}
|
38 |
+
},
|
39 |
+
|
40 |
+
async performOtpAction(body: OtpRequestBody): Promise<OtpApiResponseData> {
|
41 |
+
try {
|
42 |
+
const link: string = await api.getLink();
|
43 |
+
const { token, cookies, action, otp } = body;
|
44 |
+
|
45 |
+
const payload: {
|
46 |
+
token: string;
|
47 |
+
cookies: Record<string, any> | string;
|
48 |
+
action: string;
|
49 |
+
otp?: string;
|
50 |
+
} = {
|
51 |
+
token,
|
52 |
+
cookies,
|
53 |
+
action,
|
54 |
+
};
|
55 |
+
if (otp) {
|
56 |
+
payload.otp = otp;
|
57 |
+
}
|
58 |
+
|
59 |
+
const res: AxiosResponse<OtpApiResponseData> = await axios.post(`${link}/otp`, payload);
|
60 |
+
return res.data;
|
61 |
+
} catch (err: unknown) {
|
62 |
+
throw new Error('Failed to perform OTP action.', { cause: err instanceof Error ? err : new Error(String(err)) });
|
63 |
+
}
|
64 |
+
},
|
65 |
+
};
|
66 |
+
|
67 |
+
interface OtpRouteParams {
|
68 |
+
req: Request<any, any, OtpRequestBody>;
|
69 |
+
res: Response;
|
70 |
+
app?: Application;
|
71 |
+
wss?: WebSocketServer;
|
72 |
+
wssConsole?: WebSocketServer;
|
73 |
+
Shellwss?: WebSocketServer;
|
74 |
+
server?: HttpServer;
|
75 |
+
}
|
76 |
+
|
77 |
+
interface OtpExpressRouteModule {
|
78 |
+
method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
|
79 |
+
path: string;
|
80 |
+
install: (params: OtpRouteParams) => Promise<void> | void;
|
81 |
+
}
|
82 |
+
|
83 |
+
export const modules: OtpExpressRouteModule[] = [
|
84 |
+
{
|
85 |
+
method: 'post',
|
86 |
+
path: '/otp',
|
87 |
+
install: async ({ req, res }: OtpRouteParams): Promise<void> => {
|
88 |
+
try {
|
89 |
+
const { token, cookies, action, otp } = req.body;
|
90 |
+
|
91 |
+
if (typeof token !== 'string' || !token ||
|
92 |
+
(cookies === undefined || cookies === null) ||
|
93 |
+
typeof action !== 'string' || !action) {
|
94 |
+
res.status(400).json({
|
95 |
+
status: 'error',
|
96 |
+
message: 'Missing required fields: token, cookies, and action must be provided.',
|
97 |
+
});
|
98 |
+
return;
|
99 |
+
}
|
100 |
+
|
101 |
+
if (action.toLowerCase() === 'verify' && (typeof otp !== 'string' || !otp)) {
|
102 |
+
res.status(400).json({
|
103 |
+
status: 'error',
|
104 |
+
message: 'OTP is required for verify action.',
|
105 |
+
});
|
106 |
+
return;
|
107 |
+
}
|
108 |
+
|
109 |
+
let safeCookies: Record<string, any>;
|
110 |
+
if (typeof cookies === 'string') {
|
111 |
+
try {
|
112 |
+
safeCookies = JSON.parse(cookies);
|
113 |
+
} catch (parseError: unknown) {
|
114 |
+
const errMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
115 |
+
res.status(400).json({ status: 'error', message: `Invalid cookies JSON format: ${errMsg}` });
|
116 |
+
return;
|
117 |
+
}
|
118 |
+
} else if (typeof cookies === 'object' && cookies !== null) {
|
119 |
+
safeCookies = cookies;
|
120 |
+
} else {
|
121 |
+
res.status(400).json({ status: 'error', message: 'Cookies must be a JSON string or an object.' });
|
122 |
+
return;
|
123 |
+
}
|
124 |
+
|
125 |
+
const responseData: OtpApiResponseData = await api.performOtpAction({ token, cookies: safeCookies, action, otp });
|
126 |
+
|
127 |
+
res.json({
|
128 |
+
status: 'success',
|
129 |
+
data: responseData,
|
130 |
+
});
|
131 |
+
} catch (err: unknown) {
|
132 |
+
let errorMessage = "An unknown error occurred in OTP handler.";
|
133 |
+
let statusCode = 500;
|
134 |
+
|
135 |
+
const topLevelError = err instanceof Error ? err : new Error(String(err));
|
136 |
+
const originalError: unknown = topLevelError.cause || topLevelError;
|
137 |
+
|
138 |
+
if (axios.isAxiosError(originalError)) {
|
139 |
+
const axiosError = originalError as AxiosError<any>;
|
140 |
+
errorMessage = axiosError.message;
|
141 |
+
if (axiosError.response) {
|
142 |
+
statusCode = axiosError.response.status;
|
143 |
+
const responseDataError = axiosError.response.data?.error || axiosError.response.data?.message;
|
144 |
+
errorMessage = `OTP API Error (${statusCode}): ${responseDataError || axiosError.message}`;
|
145 |
+
console.error(`Axios error response from /otp endpoint: Status ${statusCode}`, axiosError.response.data);
|
146 |
+
} else if (axiosError.request) {
|
147 |
+
errorMessage = "No response received from OTP service.";
|
148 |
+
statusCode = 503;
|
149 |
+
console.error("Axios no response error for /otp:", axiosError.request);
|
150 |
+
}
|
151 |
+
} else if (originalError instanceof Error) {
|
152 |
+
errorMessage = originalError.message;
|
153 |
+
} else {
|
154 |
+
errorMessage = String(originalError);
|
155 |
+
}
|
156 |
+
|
157 |
+
console.error(`Error in /otp route: ${errorMessage}`, topLevelError);
|
158 |
+
if (!res.headersSent) {
|
159 |
+
res.status(statusCode).json({
|
160 |
+
status: 'error',
|
161 |
+
message: errorMessage,
|
162 |
+
});
|
163 |
+
}
|
164 |
+
}
|
165 |
+
},
|
166 |
+
},
|
167 |
+
];
|
exocore-web/src/panel.ts
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import fsPromises from 'fs/promises';
|
4 |
+
import path from 'path';
|
5 |
+
import { Request, Response } from 'express';
|
6 |
+
|
7 |
+
interface LoginClientRequestBody {
|
8 |
+
user: string;
|
9 |
+
pass: string;
|
10 |
+
}
|
11 |
+
|
12 |
+
function isLoginClientRequestBody(body: any): body is LoginClientRequestBody {
|
13 |
+
return (body && typeof body === 'object' &&
|
14 |
+
typeof body.user === 'string' && body.user.length > 0 &&
|
15 |
+
typeof body.pass === 'string' && body.pass.length > 0);
|
16 |
+
}
|
17 |
+
|
18 |
+
interface LoginRouteParams {
|
19 |
+
req: Request<any, any, unknown>;
|
20 |
+
res: Response;
|
21 |
+
}
|
22 |
+
|
23 |
+
interface ConfigData {
|
24 |
+
user: string;
|
25 |
+
pass: string;
|
26 |
+
[key: string]: any;
|
27 |
+
}
|
28 |
+
|
29 |
+
interface PanelExpressRouteModule {
|
30 |
+
method: 'post';
|
31 |
+
path: string;
|
32 |
+
install: (params: LoginRouteParams) => Promise<void>;
|
33 |
+
}
|
34 |
+
|
35 |
+
export const modules: PanelExpressRouteModule[] = [
|
36 |
+
{
|
37 |
+
method: 'post',
|
38 |
+
path: '/panel',
|
39 |
+
install: async ({ req, res }: LoginRouteParams): Promise<void> => {
|
40 |
+
const requestBody: unknown = req.body;
|
41 |
+
|
42 |
+
// DEBUG: Log the raw request body
|
43 |
+
console.log('[Panel Login] Raw request body:', JSON.stringify(requestBody));
|
44 |
+
|
45 |
+
if (!isLoginClientRequestBody(requestBody)) {
|
46 |
+
res.status(400).json({
|
47 |
+
message: 'Invalid request payload. "user" and "pass" are required and must be non-empty strings.',
|
48 |
+
status: 'error',
|
49 |
+
});
|
50 |
+
return;
|
51 |
+
}
|
52 |
+
|
53 |
+
const { user: clientUser, pass: clientPass } = requestBody;
|
54 |
+
|
55 |
+
// DEBUG: Log received client credentials (use quotes to see whitespace)
|
56 |
+
console.log(`[Panel Login] Received from client - User: "${clientUser}", Pass: "${clientPass}"`);
|
57 |
+
|
58 |
+
const relativeConfigPath = '../../config.json';
|
59 |
+
let configPath: string;
|
60 |
+
|
61 |
+
if (typeof __dirname !== 'undefined') {
|
62 |
+
configPath = path.resolve(__dirname, relativeConfigPath);
|
63 |
+
} else {
|
64 |
+
console.warn(
|
65 |
+
`[Panel Login] Warning: __dirname is not defined (standard in ES Modules). ` +
|
66 |
+
`Resolving config path "${relativeConfigPath}" relative to current working directory: ${process.cwd()}. ` +
|
67 |
+
`For robust file-relative paths in ES modules, the standard approach uses import.meta.url.`
|
68 |
+
);
|
69 |
+
configPath = path.resolve(process.cwd(), relativeConfigPath);
|
70 |
+
}
|
71 |
+
|
72 |
+
// DEBUG: Log the resolved config path
|
73 |
+
console.log(`[Panel Login] Attempting to load config from: "${configPath}"`);
|
74 |
+
|
75 |
+
try {
|
76 |
+
const configFileContent: string = await fsPromises.readFile(configPath, 'utf-8');
|
77 |
+
const config: ConfigData = JSON.parse(configFileContent);
|
78 |
+
|
79 |
+
// DEBUG: Log credentials loaded from config.json (use quotes to see whitespace)
|
80 |
+
if (config.user && config.pass) {
|
81 |
+
console.log(`[Panel Login] Loaded from config.json - User: "${config.user}", Pass: "${config.pass}"`);
|
82 |
+
} else {
|
83 |
+
console.log('[Panel Login] Loaded from config.json - "user" or "pass" field is missing or not a string.');
|
84 |
+
}
|
85 |
+
|
86 |
+
|
87 |
+
if (typeof config.user !== 'string' || typeof config.pass !== 'string') {
|
88 |
+
console.error(`[Panel Login] Invalid config.json structure at "${configPath}". "user" and "pass" must be strings.`);
|
89 |
+
res.status(500).json({
|
90 |
+
message: 'Server configuration error: Invalid config file structure.',
|
91 |
+
status: 'error',
|
92 |
+
});
|
93 |
+
return;
|
94 |
+
}
|
95 |
+
|
96 |
+
if (clientUser === config.user && clientPass === config.pass) {
|
97 |
+
console.log('[Panel Login] Credentials MATCHED!'); // DEBUG
|
98 |
+
res.status(200).json({
|
99 |
+
message: 'Login successful!',
|
100 |
+
status: 'success',
|
101 |
+
});
|
102 |
+
return;
|
103 |
+
} else {
|
104 |
+
console.log('[Panel Login] Credentials DID NOT MATCH.'); // DEBUG
|
105 |
+
console.log(`Comparison details: Client User ("${clientUser}") === Config User ("${config.user}") -> ${clientUser === config.user}`);
|
106 |
+
console.log(`Comparison details: Client Pass ("${clientPass}") === Config Pass ("${config.pass}") -> ${clientPass === config.pass}`);
|
107 |
+
res.status(401).json({
|
108 |
+
message: 'Invalid username or password.',
|
109 |
+
status: 'failed',
|
110 |
+
});
|
111 |
+
return;
|
112 |
+
}
|
113 |
+
} catch (error: unknown) {
|
114 |
+
console.error(`[Panel Login] Error during login process for config path "${configPath}":`, error instanceof Error ? error.message : String(error));
|
115 |
+
// ... (rest of your error handling) ...
|
116 |
+
if (error instanceof SyntaxError) {
|
117 |
+
res.status(500).json({
|
118 |
+
message: `Server configuration error: Malformed config file at "${configPath}". Please ensure it is valid JSON.`,
|
119 |
+
status: 'error',
|
120 |
+
});
|
121 |
+
return;
|
122 |
+
}
|
123 |
+
|
124 |
+
if (error instanceof Error && 'code' in error) {
|
125 |
+
const errnoError = error as NodeJS.ErrnoException;
|
126 |
+
if (errnoError.code === 'ENOENT') {
|
127 |
+
res.status(500).json({
|
128 |
+
message: `Server configuration error: Config file not found at "${configPath}". Please ensure the path is correct.`,
|
129 |
+
status: 'error',
|
130 |
+
});
|
131 |
+
return;
|
132 |
+
}
|
133 |
+
}
|
134 |
+
|
135 |
+
res.status(500).json({
|
136 |
+
message: 'An internal server error occurred during login.',
|
137 |
+
status: 'error',
|
138 |
+
});
|
139 |
+
return;
|
140 |
+
}
|
141 |
+
},
|
142 |
+
},
|
143 |
+
];
|
exocore-web/src/project.ts
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import axios, { AxiosResponse } from 'axios';
|
4 |
+
import fs from 'fs';
|
5 |
+
import path from 'path';
|
6 |
+
import simpleGit, { SimpleGit } from 'simple-git';
|
7 |
+
import { Request, Response, Application } from 'express';
|
8 |
+
import { WebSocketServer } from 'ws';
|
9 |
+
import { Server as HttpServer } from 'http';
|
10 |
+
|
11 |
+
const TEMPLATE_URL = 'https://raw.githubusercontent.com/ChoruOfficial/global-exocore/main/template.json';
|
12 |
+
const configPath = path.resolve(__dirname, '../config.json');
|
13 |
+
const git: SimpleGit = simpleGit();
|
14 |
+
|
15 |
+
interface Template {
|
16 |
+
id: string;
|
17 |
+
name: string;
|
18 |
+
description: string;
|
19 |
+
image: string;
|
20 |
+
git: string;
|
21 |
+
}
|
22 |
+
|
23 |
+
interface ConfigData {
|
24 |
+
project?: string;
|
25 |
+
[key: string]: any;
|
26 |
+
}
|
27 |
+
|
28 |
+
interface CreateProjectOptions {
|
29 |
+
templateId?: string;
|
30 |
+
gitUrl?: string;
|
31 |
+
}
|
32 |
+
|
33 |
+
async function pathExists(p: string): Promise<boolean> {
|
34 |
+
try {
|
35 |
+
await fs.promises.access(p);
|
36 |
+
return true;
|
37 |
+
} catch {
|
38 |
+
return false;
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
const api = {
|
43 |
+
async getTemplates(): Promise<Template[]> {
|
44 |
+
try {
|
45 |
+
const res: AxiosResponse<Template[]> = await axios.get(TEMPLATE_URL);
|
46 |
+
if (Array.isArray(res.data)) return res.data;
|
47 |
+
console.warn('Fetched template.json is not an array.');
|
48 |
+
return [];
|
49 |
+
} catch (err: unknown) {
|
50 |
+
console.error('Failed to fetch templates:', err);
|
51 |
+
return [];
|
52 |
+
}
|
53 |
+
},
|
54 |
+
|
55 |
+
async updateConfig(projectName: string): Promise<void> {
|
56 |
+
let config: ConfigData = {};
|
57 |
+
try {
|
58 |
+
if (await pathExists(configPath)) {
|
59 |
+
const file = await fs.promises.readFile(configPath, 'utf-8');
|
60 |
+
config = file.trim() ? JSON.parse(file) : {};
|
61 |
+
}
|
62 |
+
} catch (err) {
|
63 |
+
console.error('Error reading config:', err);
|
64 |
+
}
|
65 |
+
|
66 |
+
config.project = `../${projectName}`;
|
67 |
+
try {
|
68 |
+
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
|
69 |
+
console.log(`Config updated: ${configPath}`);
|
70 |
+
} catch (err) {
|
71 |
+
console.error('Error writing config:', err);
|
72 |
+
}
|
73 |
+
},
|
74 |
+
|
75 |
+
async cloneTemplate(gitUrl: string, targetPath: string): Promise<void> {
|
76 |
+
try {
|
77 |
+
await git.clone(gitUrl, targetPath);
|
78 |
+
console.log('Cloned template successfully.');
|
79 |
+
} catch (err) {
|
80 |
+
console.error('Git clone failed:', err);
|
81 |
+
throw new Error('Git clone failed');
|
82 |
+
}
|
83 |
+
},
|
84 |
+
|
85 |
+
async checkProjectStatus(): Promise<{ exists: boolean }> {
|
86 |
+
if (!await pathExists(configPath)) return { exists: false };
|
87 |
+
try {
|
88 |
+
const raw = await fs.promises.readFile(configPath, 'utf-8');
|
89 |
+
const config: ConfigData = JSON.parse(raw);
|
90 |
+
if (!config.project) return { exists: false };
|
91 |
+
|
92 |
+
const folder = path.resolve(path.dirname(configPath), config.project);
|
93 |
+
return { exists: await pathExists(folder) };
|
94 |
+
} catch (err) {
|
95 |
+
console.error('Error checking project status:', err);
|
96 |
+
return { exists: false };
|
97 |
+
}
|
98 |
+
},
|
99 |
+
|
100 |
+
async createProject(projectName: string, { templateId, gitUrl }: CreateProjectOptions): Promise<string> {
|
101 |
+
if (!projectName.trim()) throw new Error('Project name is required.');
|
102 |
+
|
103 |
+
let cloneUrl: string;
|
104 |
+
|
105 |
+
if (gitUrl && templateId) throw new Error('Provide either a template ID or a Git URL, not both.');
|
106 |
+
if (gitUrl) {
|
107 |
+
cloneUrl = gitUrl;
|
108 |
+
} else if (templateId) {
|
109 |
+
const templates = await api.getTemplates();
|
110 |
+
const template = templates.find(t => t.id === templateId);
|
111 |
+
if (!template) throw new Error(`Template "${templateId}" not found.`);
|
112 |
+
cloneUrl = template.git;
|
113 |
+
} else {
|
114 |
+
throw new Error('You must provide a template ID or Git URL.');
|
115 |
+
}
|
116 |
+
|
117 |
+
const base = path.resolve(__dirname, '../..');
|
118 |
+
const target = path.join(base, projectName);
|
119 |
+
|
120 |
+
if (await pathExists(target)) {
|
121 |
+
throw new Error(`Project "${projectName}" already exists.`);
|
122 |
+
}
|
123 |
+
|
124 |
+
await api.cloneTemplate(cloneUrl, target);
|
125 |
+
await api.updateConfig(projectName);
|
126 |
+
return `Project "${projectName}" created at ${target}`;
|
127 |
+
},
|
128 |
+
};
|
129 |
+
|
130 |
+
interface ProjectRouteParams {
|
131 |
+
req: Request;
|
132 |
+
res: Response;
|
133 |
+
app?: Application;
|
134 |
+
wss?: WebSocketServer;
|
135 |
+
wssConsole?: WebSocketServer;
|
136 |
+
Shellwss?: WebSocketServer;
|
137 |
+
server?: HttpServer;
|
138 |
+
}
|
139 |
+
|
140 |
+
interface ProjectExpressRouteModule {
|
141 |
+
method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head' | 'all';
|
142 |
+
path: string;
|
143 |
+
install: (params: any) => Promise<void> | void;
|
144 |
+
}
|
145 |
+
|
146 |
+
interface CreateProjectRequestBody {
|
147 |
+
name: string;
|
148 |
+
template?: string;
|
149 |
+
gitUrl?: string;
|
150 |
+
}
|
151 |
+
|
152 |
+
export const modules: ProjectExpressRouteModule[] = [
|
153 |
+
{
|
154 |
+
method: 'post',
|
155 |
+
path: '/templates',
|
156 |
+
install: async ({ res }: Pick<ProjectRouteParams, 'res'>) => {
|
157 |
+
try {
|
158 |
+
const templates = await api.getTemplates();
|
159 |
+
res.json(templates);
|
160 |
+
} catch (err) {
|
161 |
+
console.error('Error in /templates:', err);
|
162 |
+
res.status(500).json({ error: 'Failed to load templates.' });
|
163 |
+
}
|
164 |
+
}
|
165 |
+
},
|
166 |
+
{
|
167 |
+
method: 'post',
|
168 |
+
path: '/project',
|
169 |
+
install: async ({ req, res }: Pick<ProjectRouteParams, 'req' | 'res'>) => {
|
170 |
+
const { name, template, gitUrl } = req.body as CreateProjectRequestBody;
|
171 |
+
try {
|
172 |
+
const msg = await api.createProject(name, { templateId: template, gitUrl });
|
173 |
+
res.status(201).json({ success: true, message: msg, projectPath: name });
|
174 |
+
} catch (err) {
|
175 |
+
console.error('Project creation failed:', err);
|
176 |
+
res.status(400).json({ success: false, error: (err as Error).message });
|
177 |
+
}
|
178 |
+
}
|
179 |
+
},
|
180 |
+
{
|
181 |
+
method: 'post',
|
182 |
+
path: '/project/status',
|
183 |
+
install: async ({ res }: Pick<ProjectRouteParams, 'res'>) => {
|
184 |
+
try {
|
185 |
+
const status = await api.checkProjectStatus();
|
186 |
+
res.json(status);
|
187 |
+
} catch (err) {
|
188 |
+
console.error('Error checking status:', err);
|
189 |
+
res.status(500).json({ error: 'Unable to check project status.' });
|
190 |
+
}
|
191 |
+
}
|
192 |
+
}
|
193 |
+
];
|
exocore-web/src/register.ts
ADDED
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import axios, { AxiosResponse, AxiosError } from 'axios';
|
4 |
+
import { Request, Response } from 'express';
|
5 |
+
|
6 |
+
console.log(`[Register Module] src/register.ts file is being evaluated. __dirname is: ${__dirname}`);
|
7 |
+
|
8 |
+
interface PastebinResponse {
|
9 |
+
link: string;
|
10 |
+
}
|
11 |
+
|
12 |
+
interface RegisterClientPayload {
|
13 |
+
user: string;
|
14 |
+
pass: string;
|
15 |
+
email: string;
|
16 |
+
nickname?: string;
|
17 |
+
avatar?: string;
|
18 |
+
bio?: string;
|
19 |
+
dob?: string;
|
20 |
+
cover_photo?: string;
|
21 |
+
country?: string;
|
22 |
+
timezone?: string;
|
23 |
+
}
|
24 |
+
|
25 |
+
type ExternalApiSignUpPayload = RegisterClientPayload;
|
26 |
+
|
27 |
+
interface RegisterApiResponseData {
|
28 |
+
status: string;
|
29 |
+
token?: string;
|
30 |
+
cookies?: string;
|
31 |
+
avatar?: string;
|
32 |
+
cover_photo?: string;
|
33 |
+
message?: string;
|
34 |
+
}
|
35 |
+
|
36 |
+
function isRegisterClientPayload(body: any): body is RegisterClientPayload {
|
37 |
+
return !!(body && typeof body === 'object' &&
|
38 |
+
typeof body.user === 'string' && body.user.length > 0 &&
|
39 |
+
typeof body.pass === 'string' && body.pass.length > 0 &&
|
40 |
+
typeof body.email === 'string' && body.email.length > 0);
|
41 |
+
}
|
42 |
+
|
43 |
+
function isRegisterApiResponseData(data: any): data is RegisterApiResponseData {
|
44 |
+
return !!(data && typeof data === 'object' && typeof data.status === 'string');
|
45 |
+
}
|
46 |
+
|
47 |
+
const api = {
|
48 |
+
async getLink(): Promise<string> {
|
49 |
+
console.log("[Register API - getLink] Attempting to fetch link...");
|
50 |
+
try {
|
51 |
+
const res: AxiosResponse<string | PastebinResponse> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
|
52 |
+
const responseData = res.data;
|
53 |
+
if (responseData && typeof responseData === 'object' && typeof (responseData as PastebinResponse).link === 'string') {
|
54 |
+
return (responseData as PastebinResponse).link;
|
55 |
+
} else if (typeof responseData === 'string' && responseData.startsWith('http')) {
|
56 |
+
return responseData;
|
57 |
+
}
|
58 |
+
throw new Error('Invalid or missing link in Pastebin response data.');
|
59 |
+
} catch (err: unknown) {
|
60 |
+
const error = err instanceof Error ? err : new Error(String(err));
|
61 |
+
throw new Error('Failed to get base link.', { cause: error });
|
62 |
+
}
|
63 |
+
},
|
64 |
+
|
65 |
+
async register(body: ExternalApiSignUpPayload): Promise<RegisterApiResponseData> {
|
66 |
+
console.log("[Register API - register] Attempting external API signup, payload:", body);
|
67 |
+
try {
|
68 |
+
const link: string = await api.getLink();
|
69 |
+
const res: AxiosResponse<RegisterApiResponseData> = await axios.post(`${link}/signup`, body);
|
70 |
+
console.log("[Register API - register] Response from external API:", res.data);
|
71 |
+
return res.data; // Axios automatically parses JSON response
|
72 |
+
} catch (err: unknown) {
|
73 |
+
const error = err instanceof Error ? err : new Error(String(err));
|
74 |
+
if (axios.isAxiosError(error) && error.response) {
|
75 |
+
console.error("Axios error data:", error.response.data, "Status:", error.response.status);
|
76 |
+
}
|
77 |
+
throw new Error('Failed to contact/process signup endpoint.', { cause: error });
|
78 |
+
}
|
79 |
+
},
|
80 |
+
};
|
81 |
+
|
82 |
+
interface RegisterRouteParams {
|
83 |
+
req: Request<any, any, any>; // Body is initially any, refined by type guard
|
84 |
+
res: Response;
|
85 |
+
}
|
86 |
+
|
87 |
+
interface RegisterExpressRouteModule {
|
88 |
+
method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
|
89 |
+
path: string;
|
90 |
+
install: (params: RegisterRouteParams) => Promise<void> | void;
|
91 |
+
}
|
92 |
+
|
93 |
+
export const modules: RegisterExpressRouteModule[] = [
|
94 |
+
{
|
95 |
+
method: 'post',
|
96 |
+
path: '/register',
|
97 |
+
install: async ({ req, res }: RegisterRouteParams): Promise<void> => {
|
98 |
+
console.log(`[Register Route] Handler for ${req.method.toUpperCase()} ${req.path}`);
|
99 |
+
const rawClientBody: unknown = req.body;
|
100 |
+
console.log("[Register Route] Raw request body from client:", rawClientBody);
|
101 |
+
|
102 |
+
try {
|
103 |
+
if (isRegisterClientPayload(rawClientBody)) {
|
104 |
+
const clientData: RegisterClientPayload = rawClientBody; // Type guard refines type
|
105 |
+
|
106 |
+
const payloadForExternalApi: ExternalApiSignUpPayload = {
|
107 |
+
user: clientData.user,
|
108 |
+
pass: clientData.pass,
|
109 |
+
email: clientData.email,
|
110 |
+
};
|
111 |
+
|
112 |
+
if (clientData.nickname && clientData.nickname.trim() !== "") payloadForExternalApi.nickname = clientData.nickname;
|
113 |
+
if (clientData.avatar && clientData.avatar.trim() !== "") payloadForExternalApi.avatar = clientData.avatar;
|
114 |
+
if (clientData.bio && clientData.bio.trim() !== "") payloadForExternalApi.bio = clientData.bio;
|
115 |
+
if (clientData.dob && clientData.dob.trim() !== "") payloadForExternalApi.dob = clientData.dob;
|
116 |
+
if (clientData.cover_photo && clientData.cover_photo.trim() !== "") payloadForExternalApi.cover_photo = clientData.cover_photo;
|
117 |
+
if (clientData.country && clientData.country.trim() !== "") payloadForExternalApi.country = clientData.country;
|
118 |
+
if (clientData.timezone && clientData.timezone.trim() !== "") payloadForExternalApi.timezone = clientData.timezone;
|
119 |
+
|
120 |
+
const rawApiResult: RegisterApiResponseData = await api.register(payloadForExternalApi);
|
121 |
+
|
122 |
+
if (isRegisterApiResponseData(rawApiResult)) { // Guard for external API response structure
|
123 |
+
const resultFromExternalApi: RegisterApiResponseData = rawApiResult;
|
124 |
+
|
125 |
+
if (resultFromExternalApi.status === 'success') {
|
126 |
+
console.log("[Register Route] External API reported SUCCESS.");
|
127 |
+
res.json({
|
128 |
+
success: true,
|
129 |
+
message: resultFromExternalApi.message || "Registration successful!",
|
130 |
+
data: resultFromExternalApi
|
131 |
+
});
|
132 |
+
} else {
|
133 |
+
console.warn("[Register Route] External API reported FAILURE. Status:", resultFromExternalApi.status, "Message:", resultFromExternalApi.message);
|
134 |
+
res.status(400).json({
|
135 |
+
success: false,
|
136 |
+
error: resultFromExternalApi.message || "Registration failed at external API."
|
137 |
+
});
|
138 |
+
}
|
139 |
+
} else {
|
140 |
+
console.error("[Register Route] Unexpected response structure from external API:", rawApiResult);
|
141 |
+
res.status(500).json({ success: false, error: "Received an unexpected response from the registration service." });
|
142 |
+
}
|
143 |
+
|
144 |
+
} else {
|
145 |
+
console.warn("[Register Route] Validation failed: req.body does not conform to RegisterClientPayload. Received:", rawClientBody);
|
146 |
+
res.status(400).json({ success: false, error: 'Invalid registration data: user, pass, and email are required and must be valid non-empty strings.' });
|
147 |
+
return;
|
148 |
+
}
|
149 |
+
} catch (err: unknown) {
|
150 |
+
const error = err instanceof Error ? err : new Error(String(err));
|
151 |
+
let errorMessage = "An unknown error occurred";
|
152 |
+
let statusCode = 500;
|
153 |
+
console.error(`[Register Route] Overall error:`, error.message, (error as any).cause || error);
|
154 |
+
|
155 |
+
const originalError = (error as any).cause || error;
|
156 |
+
|
157 |
+
if (axios.isAxiosError(originalError)) {
|
158 |
+
const axiosError = originalError as AxiosError<any>; // Type assertion
|
159 |
+
errorMessage = axiosError.message;
|
160 |
+
statusCode = axiosError.response?.status || 503;
|
161 |
+
const responseData = axiosError.response?.data;
|
162 |
+
const specificApiError = responseData?.error || responseData?.message;
|
163 |
+
|
164 |
+
if (specificApiError) {
|
165 |
+
errorMessage = `Reg API Error (${statusCode}): ${specificApiError}`;
|
166 |
+
} else if (axiosError.response) {
|
167 |
+
errorMessage = `Reg API Error (${statusCode}): Req failed (status ${statusCode}).`;
|
168 |
+
} else if (axiosError.request) {
|
169 |
+
errorMessage = `No response from reg service: ${axiosError.message}`;
|
170 |
+
}
|
171 |
+
} else if (originalError instanceof Error) {
|
172 |
+
errorMessage = originalError.message || String(originalError);
|
173 |
+
} else {
|
174 |
+
errorMessage = String(originalError);
|
175 |
+
}
|
176 |
+
|
177 |
+
console.error(`[Register Route] Error to client: ${errorMessage}, Status: ${statusCode}`);
|
178 |
+
if (!res.headersSent) {
|
179 |
+
res.status(statusCode).json({ success: false, error: errorMessage });
|
180 |
+
} else {
|
181 |
+
console.warn("[Register Route] Headers sent, cannot send error.");
|
182 |
+
}
|
183 |
+
}
|
184 |
+
},
|
185 |
+
},
|
186 |
+
];
|
exocore-web/src/shell.ts
ADDED
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import { spawn, ChildProcess } from 'child_process';
|
4 |
+
import path from 'path';
|
5 |
+
import fs from 'fs';
|
6 |
+
import simpleGit from 'simple-git';
|
7 |
+
import { Request, Response } from 'express';
|
8 |
+
import { WebSocketServer, WebSocket } from 'ws';
|
9 |
+
|
10 |
+
let shellProcess: ChildProcess | null = null;
|
11 |
+
let currentCwd: string | null = null;
|
12 |
+
let projectRootPath: string | null = null;
|
13 |
+
|
14 |
+
const PROMPT_DELIMITER = '__EXOCORE_SHELL_PROMPT_BOUNDARY__\n';
|
15 |
+
|
16 |
+
const FORBIDDEN_COMMAND_PATTERNS: RegExp[] = [
|
17 |
+
/^cd\s+\.\.(?:\/\s*)?$/,
|
18 |
+
/^cd\s+\.\.\/exocore-web\s*$/,
|
19 |
+
/^cd\s+\.\.\/src\s*$/
|
20 |
+
];
|
21 |
+
|
22 |
+
interface WsMessage {
|
23 |
+
type: 'log' | 'prompt' | 'system';
|
24 |
+
data: string;
|
25 |
+
}
|
26 |
+
|
27 |
+
function isCommandForbidden(command: string): boolean {
|
28 |
+
const trimmedCommand = command.trim();
|
29 |
+
return FORBIDDEN_COMMAND_PATTERNS.some((pattern) => pattern.test(trimmedCommand));
|
30 |
+
}
|
31 |
+
|
32 |
+
function getFormattedCwdPrompt(cwd: string, root: string): string {
|
33 |
+
const rootName = path.basename(root);
|
34 |
+
if (cwd.startsWith(root)) {
|
35 |
+
const relativePath = path.relative(root, cwd);
|
36 |
+
return `/@${rootName}${relativePath ? `/${relativePath}` : ''}$ `;
|
37 |
+
}
|
38 |
+
return `${cwd}$ `;
|
39 |
+
}
|
40 |
+
|
41 |
+
function broadcastToShellWss(wssInstance: WebSocketServer, message: WsMessage): void {
|
42 |
+
if (wssInstance && wssInstance.clients) {
|
43 |
+
const messageString = JSON.stringify(message);
|
44 |
+
wssInstance.clients.forEach((client: WebSocket) => {
|
45 |
+
if (client.readyState === 1) {
|
46 |
+
client.send(messageString);
|
47 |
+
}
|
48 |
+
});
|
49 |
+
}
|
50 |
+
}
|
51 |
+
|
52 |
+
export const modules = [
|
53 |
+
{
|
54 |
+
method: 'post',
|
55 |
+
path: '/shell/sent',
|
56 |
+
install: async ({ req, res, Shellwss }: { req: Request, res: Response, Shellwss: WebSocketServer }): Promise<void> => {
|
57 |
+
const commandFromBody: string | null = req.body && typeof req.body.command === 'string' ? req.body.command : null;
|
58 |
+
if (!commandFromBody) {
|
59 |
+
res.status(400).send('No command provided.');
|
60 |
+
return;
|
61 |
+
}
|
62 |
+
const trimmedCommand = commandFromBody.trim();
|
63 |
+
|
64 |
+
if (isCommandForbidden(trimmedCommand)) {
|
65 |
+
broadcastToShellWss(Shellwss, { type: 'log', data: '\x1b[31m Access Denied \x1b[0m\n' });
|
66 |
+
res.status(403).send('Command execution is restricted.');
|
67 |
+
return;
|
68 |
+
}
|
69 |
+
|
70 |
+
if (trimmedCommand.startsWith('exocore git clone ')) {
|
71 |
+
const repoUrl: string = trimmedCommand.substring('exocore git clone '.length).trim();
|
72 |
+
if (!repoUrl) {
|
73 |
+
broadcastToShellWss(Shellwss, { type: 'log', data: '\x1b[31mError: No repository URL provided...\x1b[0m\n' });
|
74 |
+
res.status(400).send('No repository URL provided.');
|
75 |
+
return;
|
76 |
+
}
|
77 |
+
const baseDirForPkg = path.resolve(__dirname, '../../src/pkg');
|
78 |
+
try {
|
79 |
+
await fs.promises.mkdir(baseDirForPkg, { recursive: true });
|
80 |
+
const repoName = path.basename(repoUrl, '.git');
|
81 |
+
const clonePath = path.join(baseDirForPkg, repoName);
|
82 |
+
broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[33mCloning ${repoUrl}...\x1b[0m\n` });
|
83 |
+
if (fs.existsSync(clonePath) && fs.readdirSync(clonePath).length > 0) {
|
84 |
+
broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[33mDirectory ${clonePath} already exists. Skipping.\x1b[0m\n` });
|
85 |
+
res.status(200).send('Clone destination already exists.');
|
86 |
+
return;
|
87 |
+
}
|
88 |
+
const git = simpleGit();
|
89 |
+
await git.clone(repoUrl, clonePath);
|
90 |
+
broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[32mSuccessfully cloned ${repoUrl}\x1b[0m\n` });
|
91 |
+
res.send('Clone command executed successfully.');
|
92 |
+
} catch (error: any) {
|
93 |
+
const errMsg = error instanceof Error ? error.message : String(error);
|
94 |
+
broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[31mFailed to clone: ${errMsg}\x1b[0m\n` });
|
95 |
+
res.status(500).send('Clone command failed.');
|
96 |
+
}
|
97 |
+
return;
|
98 |
+
}
|
99 |
+
|
100 |
+
if (!shellProcess) {
|
101 |
+
let projectPath: string;
|
102 |
+
let customPkgPathForShellEnv: string | undefined;
|
103 |
+
|
104 |
+
try {
|
105 |
+
const configJsonPath = path.resolve(__dirname, '../config.json');
|
106 |
+
const configData = JSON.parse(fs.readFileSync(configJsonPath, 'utf-8'));
|
107 |
+
projectPath = path.resolve(path.dirname(configJsonPath), configData.project);
|
108 |
+
} catch (e) {
|
109 |
+
projectPath = path.resolve(__dirname, '..');
|
110 |
+
}
|
111 |
+
|
112 |
+
customPkgPathForShellEnv = path.resolve(__dirname, '../../src/pkg');
|
113 |
+
if (!fs.existsSync(customPkgPathForShellEnv) || !fs.statSync(customPkgPathForShellEnv).isDirectory()) {
|
114 |
+
customPkgPathForShellEnv = undefined;
|
115 |
+
}
|
116 |
+
|
117 |
+
projectRootPath = projectPath;
|
118 |
+
currentCwd = projectPath;
|
119 |
+
|
120 |
+
const currentEnv: NodeJS.ProcessEnv = { ...process.env };
|
121 |
+
let effectivePath: string | undefined = currentEnv.PATH;
|
122 |
+
|
123 |
+
if (customPkgPathForShellEnv) {
|
124 |
+
effectivePath = `${customPkgPathForShellEnv}:${effectivePath || ''}`;
|
125 |
+
}
|
126 |
+
|
127 |
+
const shellEnv: NodeJS.ProcessEnv = {
|
128 |
+
...currentEnv,
|
129 |
+
FORCE_COLOR: '1',
|
130 |
+
NPM_CONFIG_COLOR: 'always',
|
131 |
+
TERM: 'xterm-256color',
|
132 |
+
LANG: 'en_US.UTF-8',
|
133 |
+
PATH: effectivePath,
|
134 |
+
};
|
135 |
+
|
136 |
+
shellProcess = spawn('bash', { cwd: projectPath, shell: true, env: shellEnv });
|
137 |
+
|
138 |
+
let stdoutBuffer = '';
|
139 |
+
const handleShellOutput = (data: Buffer | string) => {
|
140 |
+
stdoutBuffer += data.toString();
|
141 |
+
while (stdoutBuffer.includes(PROMPT_DELIMITER)) {
|
142 |
+
const boundaryIndex = stdoutBuffer.indexOf(PROMPT_DELIMITER);
|
143 |
+
const chunk = stdoutBuffer.substring(0, boundaryIndex);
|
144 |
+
stdoutBuffer = stdoutBuffer.substring(boundaryIndex + PROMPT_DELIMITER.length);
|
145 |
+
|
146 |
+
const lines = chunk.trim().split('\n');
|
147 |
+
const newCwd = lines.pop()?.trim();
|
148 |
+
const commandOutput = lines.join('\n');
|
149 |
+
|
150 |
+
if (commandOutput) {
|
151 |
+
broadcastToShellWss(Shellwss, { type: 'log', data: commandOutput + '\n' });
|
152 |
+
}
|
153 |
+
if (newCwd && projectRootPath && fs.existsSync(newCwd)) {
|
154 |
+
currentCwd = newCwd;
|
155 |
+
const newPrompt = getFormattedCwdPrompt(currentCwd, projectRootPath);
|
156 |
+
broadcastToShellWss(Shellwss, { type: 'prompt', data: newPrompt });
|
157 |
+
}
|
158 |
+
}
|
159 |
+
};
|
160 |
+
|
161 |
+
shellProcess.stdout?.on('data', handleShellOutput);
|
162 |
+
shellProcess.stderr?.on('data', (data) => broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[31m${data.toString()}\x1b[0m` }));
|
163 |
+
shellProcess.on('close', (code) => {
|
164 |
+
broadcastToShellWss(Shellwss, { type: 'system', data: `\x1b[33mShell exited (code: ${code}).\x1b[0m\n` });
|
165 |
+
shellProcess = null;
|
166 |
+
currentCwd = null;
|
167 |
+
projectRootPath = null;
|
168 |
+
});
|
169 |
+
shellProcess.on('error', (err) => {
|
170 |
+
broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[31mFailed to start shell: ${err.message}\x1b[0m\n` });
|
171 |
+
shellProcess = null;
|
172 |
+
});
|
173 |
+
|
174 |
+
await new Promise(resolve => setTimeout(resolve, 100));
|
175 |
+
}
|
176 |
+
|
177 |
+
if (shellProcess && shellProcess.stdin?.writable) {
|
178 |
+
shellProcess.stdin.write(trimmedCommand + '\n');
|
179 |
+
shellProcess.stdin.write(`pwd && echo "${PROMPT_DELIMITER.trim()}"\n`);
|
180 |
+
res.send('Command sent to shell.');
|
181 |
+
} else {
|
182 |
+
broadcastToShellWss(Shellwss, { type: 'log', data: '\x1b[31mCannot send command: Shell is not ready.\x1b[0m\n' });
|
183 |
+
res.status(503).send('Shell process not available.');
|
184 |
+
}
|
185 |
+
},
|
186 |
+
},
|
187 |
+
{
|
188 |
+
method: 'post',
|
189 |
+
path: '/shell/kill',
|
190 |
+
install: ({ res, Shellwss }: { res: Response, Shellwss: WebSocketServer }) => {
|
191 |
+
if (shellProcess) {
|
192 |
+
shellProcess.kill();
|
193 |
+
broadcastToShellWss(Shellwss, { type: 'system', data: '\x1b[33mKill signal sent.\x1b[0m\n' });
|
194 |
+
res.send('Kill signal sent.');
|
195 |
+
} else {
|
196 |
+
broadcastToShellWss(Shellwss, { type: 'system', data: '\x1b[33mNo active shell to kill.\x1b[0m\n' });
|
197 |
+
res.status(404).send('No active shell process.');
|
198 |
+
}
|
199 |
+
},
|
200 |
+
},
|
201 |
+
];
|
202 |
+
|
203 |
+
export function setupShellWS(Shellwss: WebSocketServer): void {
|
204 |
+
Shellwss.on('connection', (ws: WebSocket) => {
|
205 |
+
ws.send(JSON.stringify({ type: 'system', data: '\x1b[32mWelcome to the interactive shell!\x1b[0m\n' }));
|
206 |
+
if (shellProcess && currentCwd && projectRootPath) {
|
207 |
+
const prompt = getFormattedCwdPrompt(currentCwd, projectRootPath);
|
208 |
+
ws.send(JSON.stringify({ type: 'prompt', data: prompt }));
|
209 |
+
} else {
|
210 |
+
ws.send(JSON.stringify({ type: 'system', data: '\x1b[33mShell not running. Send command to start.\x1b[0m\n' }));
|
211 |
+
}
|
212 |
+
ws.on('message', (message: Buffer | string) => {
|
213 |
+
ws.send(JSON.stringify({ type: 'system', data: '\x1b[33mPlease send commands through the terminal input, not directly via WebSocket message.\x1b[0m\n' }));
|
214 |
+
});
|
215 |
+
});
|
216 |
+
}
|
217 |
+
|
218 |
+
export function stopShellProcessOnExit(): void {
|
219 |
+
if (shellProcess) {
|
220 |
+
shellProcess.kill('SIGTERM');
|
221 |
+
}
|
222 |
+
}
|
exocore-web/src/skills.ts
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import fs from "fs";
|
4 |
+
import path from "path";
|
5 |
+
import { Request, Response } from 'express';
|
6 |
+
|
7 |
+
interface SkillResult {
|
8 |
+
name: string;
|
9 |
+
skill: string;
|
10 |
+
extension: string;
|
11 |
+
}
|
12 |
+
|
13 |
+
interface Config {
|
14 |
+
project: string;
|
15 |
+
}
|
16 |
+
|
17 |
+
const languageMap: { [key: string]: string } = {
|
18 |
+
"js": "JavaScript",
|
19 |
+
"ts": "TypeScript",
|
20 |
+
"html": "HTML",
|
21 |
+
"css": "CSS",
|
22 |
+
"json": "JSON",
|
23 |
+
"md": "Markdown",
|
24 |
+
"py": "Python",
|
25 |
+
"java": "Java",
|
26 |
+
"c": "C",
|
27 |
+
"cpp": "C++",
|
28 |
+
"cs": "C#",
|
29 |
+
"go": "Go",
|
30 |
+
"rb": "Ruby",
|
31 |
+
"php": "PHP",
|
32 |
+
"swift": "Swift",
|
33 |
+
"kt": "Kotlin",
|
34 |
+
"sh": "Shell Script",
|
35 |
+
"xml": "XML",
|
36 |
+
"yaml": "YAML",
|
37 |
+
"yml": "YAML",
|
38 |
+
"sql": "SQL",
|
39 |
+
"jsx": "React (JavaScript)",
|
40 |
+
"tsx": "React (TypeScript)",
|
41 |
+
};
|
42 |
+
|
43 |
+
function walkDir(dirPath: string, counts: Map<string, number>): number {
|
44 |
+
let totalFiles = 0;
|
45 |
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
46 |
+
console.warn(`[Skills Analyzer] Directory not found or not a directory: ${dirPath}`);
|
47 |
+
return 0;
|
48 |
+
}
|
49 |
+
|
50 |
+
const files = fs.readdirSync(dirPath);
|
51 |
+
for (const file of files) {
|
52 |
+
const fullPath = path.join(dirPath, file);
|
53 |
+
try {
|
54 |
+
const stats = fs.statSync(fullPath);
|
55 |
+
if (stats.isDirectory()) {
|
56 |
+
totalFiles += walkDir(fullPath, counts);
|
57 |
+
} else if (stats.isFile()) {
|
58 |
+
let ext = path.extname(file);
|
59 |
+
if (ext) {
|
60 |
+
ext = ext.substring(1).toLowerCase();
|
61 |
+
counts.set(ext, (counts.get(ext) || 0) + 1);
|
62 |
+
totalFiles++;
|
63 |
+
}
|
64 |
+
}
|
65 |
+
} catch (error: any) {
|
66 |
+
console.error(`[Skills Analyzer] Error processing file ${fullPath}:`, error.message);
|
67 |
+
}
|
68 |
+
}
|
69 |
+
return totalFiles;
|
70 |
+
}
|
71 |
+
|
72 |
+
async function analyzeProjectSkills(): Promise<{ project: string; skills: SkillResult[] }> {
|
73 |
+
const configPath = path.resolve(__dirname, '..', 'config.json');
|
74 |
+
console.log(`[Skills Analyzer] Attempting to read config from: ${configPath}`);
|
75 |
+
|
76 |
+
let projectRoot: string;
|
77 |
+
try {
|
78 |
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
79 |
+
const config: Config = JSON.parse(configContent);
|
80 |
+
if (!config.project) {
|
81 |
+
throw new Error("Missing 'project' key in config.json. Please ensure it's defined.");
|
82 |
+
}
|
83 |
+
projectRoot = path.resolve(path.dirname(configPath), config.project);
|
84 |
+
console.log(`[Skills Analyzer] Project path from config: ${projectRoot}`);
|
85 |
+
} catch (error: any) {
|
86 |
+
console.error(`[Skills Analyzer] Failed to read or parse config.json:`, error.message);
|
87 |
+
return { project: "Unknown Project (config error)", skills: [] };
|
88 |
+
}
|
89 |
+
|
90 |
+
const languageCounts = new Map<string, number>();
|
91 |
+
const totalFiles = walkDir(projectRoot, languageCounts);
|
92 |
+
|
93 |
+
if (totalFiles === 0) {
|
94 |
+
console.warn(`[Skills Analyzer] No files with recognized extensions found in project directory: ${projectRoot}`);
|
95 |
+
return { project: projectRoot, skills: [] };
|
96 |
+
}
|
97 |
+
|
98 |
+
const skills: SkillResult[] = [];
|
99 |
+
for (const [ext, count] of languageCounts.entries()) {
|
100 |
+
const percentage = ((count / totalFiles) * 100).toFixed(2);
|
101 |
+
const name = languageMap[ext] || ext.toUpperCase();
|
102 |
+
skills.push({
|
103 |
+
name: name.toLowerCase(),
|
104 |
+
skill: `${percentage}%`,
|
105 |
+
extension: ext,
|
106 |
+
});
|
107 |
+
}
|
108 |
+
|
109 |
+
skills.sort((a, b) => parseFloat(b.skill) - parseFloat(a.skill));
|
110 |
+
|
111 |
+
console.log(`[Skills Analyzer] Analysis complete for project: ${projectRoot}. Found ${totalFiles} files.`);
|
112 |
+
return { project: projectRoot, skills: skills };
|
113 |
+
}
|
114 |
+
|
115 |
+
interface LoginRouteParams {
|
116 |
+
req: Request<any, any, unknown>;
|
117 |
+
res: Response;
|
118 |
+
}
|
119 |
+
|
120 |
+
interface LoginExpressRouteModule {
|
121 |
+
method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
|
122 |
+
path: string;
|
123 |
+
install: (params: LoginRouteParams) => Promise<void> | void;
|
124 |
+
}
|
125 |
+
|
126 |
+
export const modules: LoginExpressRouteModule[] = [
|
127 |
+
{
|
128 |
+
method: "post",
|
129 |
+
path: "/skills",
|
130 |
+
install: async ({ req, res }: { req: Request; res: Response }) => {
|
131 |
+
try {
|
132 |
+
const { project, skills } = await analyzeProjectSkills();
|
133 |
+
res.json([{ project: project, skills: skills }]);
|
134 |
+
} catch (error: any) {
|
135 |
+
console.error("[Skills Route] Error serving skills:", error.message);
|
136 |
+
res.status(500).json({ error: "Failed to analyze project skills. Check server logs." });
|
137 |
+
}
|
138 |
+
},
|
139 |
+
},
|
140 |
+
];
|
exocore-web/src/userinfo.ts
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import axios, { AxiosResponse, AxiosError } from 'axios';
|
4 |
+
import { Request, Response, Application } from 'express';
|
5 |
+
import { WebSocketServer } from 'ws';
|
6 |
+
import { Server as HttpServer } from 'http';
|
7 |
+
|
8 |
+
interface PastebinResponse {
|
9 |
+
link: string;
|
10 |
+
}
|
11 |
+
|
12 |
+
interface UserInfoRequestBody {
|
13 |
+
token: string;
|
14 |
+
cookies: Record<string, any> | string | undefined;
|
15 |
+
}
|
16 |
+
|
17 |
+
interface UserInfoResponseData {
|
18 |
+
userId: string;
|
19 |
+
username: string;
|
20 |
+
roles: string[];
|
21 |
+
[key: string]: any;
|
22 |
+
}
|
23 |
+
|
24 |
+
const api = {
|
25 |
+
async getLink(): Promise<string> {
|
26 |
+
try {
|
27 |
+
const res: AxiosResponse<PastebinResponse | string> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
|
28 |
+
if (res.data && typeof res.data === 'object' && typeof (res.data as PastebinResponse).link === 'string') {
|
29 |
+
return (res.data as PastebinResponse).link;
|
30 |
+
} else if (typeof res.data === 'string' && res.data.startsWith('http')) {
|
31 |
+
return res.data;
|
32 |
+
}
|
33 |
+
throw new Error('Invalid or missing link in Pastebin response data.');
|
34 |
+
} catch (err: unknown) {
|
35 |
+
const error = err instanceof Error ? err : new Error(String(err));
|
36 |
+
throw new Error('Failed to get base link from Pastebin.', { cause: error });
|
37 |
+
}
|
38 |
+
},
|
39 |
+
|
40 |
+
async userinfo(body: UserInfoRequestBody): Promise<UserInfoResponseData> {
|
41 |
+
try {
|
42 |
+
const link: string = await api.getLink();
|
43 |
+
const res: AxiosResponse<UserInfoResponseData> = await axios.post(`${link}/userinfo`, body);
|
44 |
+
return res.data;
|
45 |
+
} catch (err: unknown) {
|
46 |
+
const error = err instanceof Error ? err : new Error(String(err));
|
47 |
+
throw new Error('Failed to contact userinfo endpoint.', { cause: error });
|
48 |
+
}
|
49 |
+
},
|
50 |
+
};
|
51 |
+
|
52 |
+
interface UserInfoRouteParams {
|
53 |
+
req: Request<any, any, UserInfoRequestBody>;
|
54 |
+
res: Response;
|
55 |
+
app?: Application;
|
56 |
+
wss?: WebSocketServer;
|
57 |
+
wssConsole?: WebSocketServer;
|
58 |
+
Shellwss?: WebSocketServer;
|
59 |
+
server?: HttpServer;
|
60 |
+
}
|
61 |
+
|
62 |
+
interface UserInfoExpressRouteModule {
|
63 |
+
method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
|
64 |
+
path: string;
|
65 |
+
install: (params: UserInfoRouteParams) => Promise<void> | void;
|
66 |
+
}
|
67 |
+
|
68 |
+
export const modules: UserInfoExpressRouteModule[] = [
|
69 |
+
{
|
70 |
+
method: 'post',
|
71 |
+
path: '/userinfo',
|
72 |
+
install: async ({ req, res }: UserInfoRouteParams): Promise<void> => {
|
73 |
+
try {
|
74 |
+
const { token, cookies: cookiesRaw } = req.body;
|
75 |
+
|
76 |
+
if (typeof token !== 'string' || !token) {
|
77 |
+
res.status(400).json({ error: "Token is required and must be a string." });
|
78 |
+
return;
|
79 |
+
}
|
80 |
+
if (cookiesRaw === undefined || cookiesRaw === null) {
|
81 |
+
res.status(400).json({ error: 'Cookies are required.' });
|
82 |
+
return;
|
83 |
+
}
|
84 |
+
|
85 |
+
let safeCookies: Record<string, any>;
|
86 |
+
if (typeof cookiesRaw === 'string') {
|
87 |
+
try {
|
88 |
+
safeCookies = JSON.parse(cookiesRaw);
|
89 |
+
} catch (parseError: unknown) {
|
90 |
+
const errMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
91 |
+
res.status(400).json({ error: `Invalid cookies JSON format: ${errMsg}` });
|
92 |
+
return;
|
93 |
+
}
|
94 |
+
} else if (typeof cookiesRaw === 'object' && cookiesRaw !== null) {
|
95 |
+
safeCookies = cookiesRaw;
|
96 |
+
} else {
|
97 |
+
res.status(400).json({ error: 'Cookies must be a JSON string or an object.' });
|
98 |
+
return;
|
99 |
+
}
|
100 |
+
|
101 |
+
const data: UserInfoResponseData = await api.userinfo({ token, cookies: safeCookies });
|
102 |
+
res.json({ data });
|
103 |
+
} catch (err: unknown) {
|
104 |
+
let errorMessage = "An unknown error occurred while fetching user info.";
|
105 |
+
let statusCode = 500;
|
106 |
+
|
107 |
+
const topLevelError = err instanceof Error ? err : new Error(String(err));
|
108 |
+
const originalError: unknown = topLevelError.cause || topLevelError;
|
109 |
+
|
110 |
+
if (axios.isAxiosError(originalError)) {
|
111 |
+
const axiosError = originalError as AxiosError<any>; // Type assertion
|
112 |
+
errorMessage = axiosError.message;
|
113 |
+
if (axiosError.response) {
|
114 |
+
statusCode = axiosError.response.status;
|
115 |
+
const responseDataError = (axiosError.response.data as any)?.error;
|
116 |
+
errorMessage = `API Error (${statusCode}): ${responseDataError || axiosError.message}`;
|
117 |
+
console.error(`Axios error response from /userinfo endpoint: Status ${statusCode}`, axiosError.response.data);
|
118 |
+
} else if (axiosError.request) {
|
119 |
+
errorMessage = "No response received from userinfo service.";
|
120 |
+
statusCode = 503;
|
121 |
+
console.error("Axios no response error for /userinfo:", axiosError.request);
|
122 |
+
}
|
123 |
+
} else if (originalError instanceof Error) {
|
124 |
+
errorMessage = originalError.message;
|
125 |
+
} else {
|
126 |
+
errorMessage = String(originalError);
|
127 |
+
}
|
128 |
+
|
129 |
+
console.error(`Error in /userinfo route: ${errorMessage}`, topLevelError);
|
130 |
+
if (!res.headersSent) {
|
131 |
+
res.status(statusCode).json({ error: errorMessage });
|
132 |
+
}
|
133 |
+
}
|
134 |
+
},
|
135 |
+
},
|
136 |
+
];
|
exocore-web/src/userinfoEdit.ts
ADDED
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// @ts-check
|
2 |
+
|
3 |
+
import axios, { AxiosResponse, AxiosError } from 'axios';
|
4 |
+
import { Request, Response, Application } from 'express';
|
5 |
+
import { WebSocketServer } from 'ws';
|
6 |
+
import { Server as HttpServer } from 'http';
|
7 |
+
|
8 |
+
interface PastebinResponse {
|
9 |
+
link: string;
|
10 |
+
}
|
11 |
+
|
12 |
+
interface UserInfoEditFunctionParams {
|
13 |
+
token: string;
|
14 |
+
cookies: Record<string, any> | string | undefined;
|
15 |
+
field: string;
|
16 |
+
edit: any;
|
17 |
+
}
|
18 |
+
|
19 |
+
interface UserInfoEditApiResponseData {
|
20 |
+
status: string;
|
21 |
+
updatedFields: Record<string, any>;
|
22 |
+
[key: string]: any; // Allow for other potential fields
|
23 |
+
}
|
24 |
+
|
25 |
+
const api = {
|
26 |
+
async getLink(): Promise<string> {
|
27 |
+
try {
|
28 |
+
const res: AxiosResponse<PastebinResponse | string> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
|
29 |
+
if (res.data && typeof res.data === 'object' && typeof (res.data as PastebinResponse).link === 'string') {
|
30 |
+
return (res.data as PastebinResponse).link;
|
31 |
+
} else if (typeof res.data === 'string' && res.data.startsWith('http')) {
|
32 |
+
return res.data;
|
33 |
+
}
|
34 |
+
throw new Error('Invalid or missing link in Pastebin response data.');
|
35 |
+
} catch (err: unknown) {
|
36 |
+
const error = err instanceof Error ? err : new Error(String(err));
|
37 |
+
throw new Error('Failed to get base link from Pastebin.', { cause: error });
|
38 |
+
}
|
39 |
+
},
|
40 |
+
|
41 |
+
async userinfoEdit({ token, cookies, field, edit }: UserInfoEditFunctionParams): Promise<UserInfoEditApiResponseData> {
|
42 |
+
try {
|
43 |
+
const link: string = await api.getLink();
|
44 |
+
const res: AxiosResponse<UserInfoEditApiResponseData> = await axios.post(`${link}/userinfoEdit`, { token, cookies, field, edit });
|
45 |
+
return res.data;
|
46 |
+
} catch (err: unknown) {
|
47 |
+
const error = err instanceof Error ? err : new Error(String(err));
|
48 |
+
throw new Error('Failed to contact userinfoEdit endpoint.', { cause: error });
|
49 |
+
}
|
50 |
+
},
|
51 |
+
};
|
52 |
+
|
53 |
+
interface UserInfoEditRouteHandlerParams {
|
54 |
+
req: Request<any, any, UserInfoEditFunctionParams>;
|
55 |
+
res: Response;
|
56 |
+
app?: Application;
|
57 |
+
wss?: WebSocketServer;
|
58 |
+
wssConsole?: WebSocketServer;
|
59 |
+
Shellwss?: WebSocketServer;
|
60 |
+
server?: HttpServer;
|
61 |
+
}
|
62 |
+
|
63 |
+
interface UserInfoEditExpressRouteModule {
|
64 |
+
method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
|
65 |
+
path: string;
|
66 |
+
install: (params: UserInfoEditRouteHandlerParams) => Promise<void> | void;
|
67 |
+
}
|
68 |
+
|
69 |
+
export const modules: UserInfoEditExpressRouteModule[] = [
|
70 |
+
{
|
71 |
+
method: 'post',
|
72 |
+
path: '/userinfoEdit',
|
73 |
+
install: async ({ req, res }: UserInfoEditRouteHandlerParams): Promise<void> => {
|
74 |
+
try {
|
75 |
+
const { token, cookies, field, edit } = req.body;
|
76 |
+
|
77 |
+
if (typeof token !== 'string' || !token) {
|
78 |
+
res.status(400).json({ error: 'Token is required and must be a string.' });
|
79 |
+
return;
|
80 |
+
}
|
81 |
+
if (typeof field !== 'string' || !field) {
|
82 |
+
res.status(400).json({ error: 'Field to edit is required and must be a string.' });
|
83 |
+
return;
|
84 |
+
}
|
85 |
+
if (edit === undefined) {
|
86 |
+
res.status(400).json({ error: 'New value for edit is required.' });
|
87 |
+
return;
|
88 |
+
}
|
89 |
+
if (cookies === undefined || cookies === null) {
|
90 |
+
res.status(400).json({ error: 'Cookies are required.' });
|
91 |
+
return;
|
92 |
+
}
|
93 |
+
|
94 |
+
let safeCookies: Record<string, any>;
|
95 |
+
if (typeof cookies === 'string') {
|
96 |
+
try {
|
97 |
+
safeCookies = JSON.parse(cookies);
|
98 |
+
} catch (parseError: unknown) {
|
99 |
+
const errMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
100 |
+
res.status(400).json({ error: `Invalid cookies JSON format: ${errMsg}` });
|
101 |
+
return;
|
102 |
+
}
|
103 |
+
} else if (typeof cookies === 'object' && cookies !== null) {
|
104 |
+
safeCookies = cookies;
|
105 |
+
} else {
|
106 |
+
res.status(400).json({ error: 'Cookies must be a JSON string or an object.' });
|
107 |
+
return;
|
108 |
+
}
|
109 |
+
|
110 |
+
const data: UserInfoEditApiResponseData = await api.userinfoEdit({ token, cookies: safeCookies, field, edit });
|
111 |
+
res.json({ data });
|
112 |
+
} catch (err: unknown) {
|
113 |
+
let errorMessage = "An unknown error occurred while editing user info.";
|
114 |
+
let statusCode = 500;
|
115 |
+
|
116 |
+
const topLevelError = err instanceof Error ? err : new Error(String(err));
|
117 |
+
const originalError: unknown = topLevelError.cause || topLevelError;
|
118 |
+
|
119 |
+
if (axios.isAxiosError(originalError)) {
|
120 |
+
const axiosError = originalError as AxiosError<any>; // Type assertion
|
121 |
+
errorMessage = axiosError.message;
|
122 |
+
if (axiosError.response) {
|
123 |
+
statusCode = axiosError.response.status;
|
124 |
+
const responseDataError = (axiosError.response.data as any)?.error;
|
125 |
+
errorMessage = `API Error (${statusCode}): ${responseDataError || axiosError.message}`;
|
126 |
+
console.error(`Axios error response from /userinfoEdit endpoint: Status ${statusCode}`, axiosError.response.data);
|
127 |
+
} else if (axiosError.request) {
|
128 |
+
errorMessage = "No response received from userinfoEdit service.";
|
129 |
+
statusCode = 503;
|
130 |
+
console.error("Axios no response error for /userinfoEdit:", axiosError.request);
|
131 |
+
}
|
132 |
+
} else if (originalError instanceof Error) {
|
133 |
+
errorMessage = originalError.message;
|
134 |
+
} else if (typeof originalError === 'string') {
|
135 |
+
errorMessage = originalError;
|
136 |
+
} else {
|
137 |
+
errorMessage = String(originalError);
|
138 |
+
}
|
139 |
+
|
140 |
+
console.error(`Error in /userinfoEdit route: ${errorMessage}`, topLevelError);
|
141 |
+
if (!res.headersSent) {
|
142 |
+
res.status(statusCode).json({ error: errorMessage });
|
143 |
+
}
|
144 |
+
}
|
145 |
+
},
|
146 |
+
},
|
147 |
+
];
|