@superdangerous/app-framework 4.9.2 → 4.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/api/logsRouter.d.ts +4 -1
- package/dist/api/logsRouter.d.ts.map +1 -1
- package/dist/api/logsRouter.js +100 -118
- package/dist/api/logsRouter.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/validation.d.ts +48 -43
- package/dist/middleware/validation.d.ts.map +1 -1
- package/dist/middleware/validation.js +48 -43
- package/dist/middleware/validation.js.map +1 -1
- package/dist/services/emailService.d.ts +146 -0
- package/dist/services/emailService.d.ts.map +1 -0
- package/dist/services/emailService.js +649 -0
- package/dist/services/emailService.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/websocketServer.d.ts +7 -4
- package/dist/services/websocketServer.d.ts.map +1 -1
- package/dist/services/websocketServer.js +22 -16
- package/dist/services/websocketServer.js.map +1 -1
- package/dist/types/index.d.ts +7 -8
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +11 -2
- package/src/api/logsRouter.ts +119 -138
- package/src/index.ts +14 -0
- package/src/middleware/validation.ts +82 -90
- package/src/services/emailService.ts +812 -0
- package/src/services/index.ts +14 -0
- package/src/services/websocketServer.ts +37 -23
- package/src/types/index.ts +7 -8
- package/ui/data-table/components/BatchActionsBar.tsx +53 -0
- package/ui/data-table/components/ColumnVisibility.tsx +111 -0
- package/ui/data-table/components/DataTablePage.tsx +238 -0
- package/ui/data-table/components/Pagination.tsx +203 -0
- package/ui/data-table/components/PaginationControls.tsx +122 -0
- package/ui/data-table/components/TableFilters.tsx +139 -0
- package/ui/data-table/components/index.ts +27 -0
- package/ui/data-table/hooks/index.ts +17 -0
- package/ui/data-table/hooks/useColumnOrder.ts +233 -0
- package/ui/data-table/hooks/useColumnVisibility.ts +128 -0
- package/ui/data-table/hooks/usePagination.ts +160 -0
- package/ui/data-table/hooks/useResizableColumns.ts +280 -0
- package/ui/data-table/index.ts +74 -0
- package/ui/dist/index.d.mts +207 -5
- package/ui/dist/index.d.ts +207 -5
- package/ui/dist/index.js +36 -43
- package/ui/dist/index.js.map +1 -1
- package/ui/dist/index.mjs +36 -43
- package/ui/dist/index.mjs.map +1 -1
package/src/api/logsRouter.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Standardized Logging API Router
|
|
3
|
-
* Provides consistent logging endpoints for all
|
|
3
|
+
* Provides consistent logging endpoints for all EpiSensor applications
|
|
4
|
+
*
|
|
5
|
+
* SECURITY WARNING: This router exposes sensitive log files without authentication.
|
|
6
|
+
* Before production use, implement proper authentication middleware.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import express, { Request, Response } from "express";
|
|
@@ -26,13 +29,99 @@ export interface LogFile {
|
|
|
26
29
|
path?: string;
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Validate and sanitize file path to prevent directory traversal
|
|
34
|
+
*/
|
|
35
|
+
function validateFilePath(filename: string, baseDir: string): string | null {
|
|
36
|
+
if (!filename || typeof filename !== "string") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Normalize the filename and resolve path
|
|
41
|
+
const safeName = path.normalize(filename).replace(/^(\.\.[/\\])+/, "");
|
|
42
|
+
const resolvedPath = path.resolve(baseDir, safeName);
|
|
43
|
+
const resolvedBase = path.resolve(baseDir);
|
|
44
|
+
|
|
45
|
+
// Ensure the resolved path is within the base directory
|
|
46
|
+
if (
|
|
47
|
+
!resolvedPath.startsWith(resolvedBase + path.sep) &&
|
|
48
|
+
resolvedPath !== resolvedBase
|
|
49
|
+
) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return resolvedPath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Safely parse integer with fallback
|
|
58
|
+
*/
|
|
59
|
+
function parseIntSafe(value: any, defaultValue: number): number {
|
|
60
|
+
if (typeof value === "number") return Math.floor(value);
|
|
61
|
+
if (typeof value === "string") {
|
|
62
|
+
const parsed = parseInt(value, 10);
|
|
63
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
64
|
+
}
|
|
65
|
+
return defaultValue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Helper function to format bytes
|
|
69
|
+
function formatBytes(bytes: number, decimals = 2): string {
|
|
70
|
+
if (bytes === 0) return "0 Bytes";
|
|
71
|
+
const k = 1024;
|
|
72
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
73
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
74
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
75
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Helper to get log files from a directory
|
|
80
|
+
*/
|
|
81
|
+
async function getLogFilesFromDir(
|
|
82
|
+
logsDir: string,
|
|
83
|
+
includeFullPath = false,
|
|
84
|
+
): Promise<LogFile[]> {
|
|
85
|
+
if (!existsSync(logsDir)) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const files = await fs.readdir(logsDir);
|
|
90
|
+
const logFiles: LogFile[] = [];
|
|
91
|
+
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
if (
|
|
94
|
+
file.endsWith(".log") ||
|
|
95
|
+
file.endsWith(".txt") ||
|
|
96
|
+
file.endsWith(".gz")
|
|
97
|
+
) {
|
|
98
|
+
const filePath = path.join(logsDir, file);
|
|
99
|
+
const stats = await fs.stat(filePath);
|
|
100
|
+
|
|
101
|
+
logFiles.push({
|
|
102
|
+
name: file,
|
|
103
|
+
size: stats.size,
|
|
104
|
+
modified: stats.mtime.toISOString(),
|
|
105
|
+
...(includeFullPath && { path: filePath }),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Sort by modified date, newest first
|
|
111
|
+
logFiles.sort(
|
|
112
|
+
(a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime(),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return logFiles;
|
|
116
|
+
}
|
|
117
|
+
|
|
29
118
|
/**
|
|
30
119
|
* Get recent log entries
|
|
31
120
|
* GET /api/logs/entries?limit=100&level=info
|
|
32
121
|
*/
|
|
33
122
|
router.get("/entries", async (req: Request, res: Response) => {
|
|
34
123
|
try {
|
|
35
|
-
const limit =
|
|
124
|
+
const limit = parseIntSafe(req.query.limit, 100);
|
|
36
125
|
const level = (req.query.level as string) || "all";
|
|
37
126
|
|
|
38
127
|
const logger = getLogger;
|
|
@@ -59,36 +148,7 @@ router.get("/entries", async (req: Request, res: Response) => {
|
|
|
59
148
|
router.get("/files", async (_req: Request, res: Response) => {
|
|
60
149
|
try {
|
|
61
150
|
const logsDir = path.join(process.cwd(), "data", "logs");
|
|
62
|
-
|
|
63
|
-
if (!existsSync(logsDir)) {
|
|
64
|
-
res.json({
|
|
65
|
-
success: true,
|
|
66
|
-
files: [],
|
|
67
|
-
});
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const files = await fs.readdir(logsDir);
|
|
72
|
-
const logFiles: LogFile[] = [];
|
|
73
|
-
|
|
74
|
-
for (const file of files) {
|
|
75
|
-
if (file.endsWith(".log") || file.endsWith(".txt")) {
|
|
76
|
-
const filePath = path.join(logsDir, file);
|
|
77
|
-
const stats = await fs.stat(filePath);
|
|
78
|
-
|
|
79
|
-
logFiles.push({
|
|
80
|
-
name: file,
|
|
81
|
-
size: stats.size,
|
|
82
|
-
modified: stats.mtime.toISOString(),
|
|
83
|
-
path: filePath,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Sort by modified date, newest first
|
|
89
|
-
logFiles.sort(
|
|
90
|
-
(a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime(),
|
|
91
|
-
);
|
|
151
|
+
const logFiles = await getLogFilesFromDir(logsDir, true);
|
|
92
152
|
|
|
93
153
|
res.json({
|
|
94
154
|
success: true,
|
|
@@ -112,10 +172,10 @@ router.get("/download/:filename", async (req: Request, res: Response) => {
|
|
|
112
172
|
try {
|
|
113
173
|
const { filename } = req.params;
|
|
114
174
|
const logsDir = path.join(process.cwd(), "data", "logs");
|
|
115
|
-
const filePath = path.join(logsDir, filename);
|
|
116
175
|
|
|
117
176
|
// Security check - prevent directory traversal
|
|
118
|
-
|
|
177
|
+
const filePath = validateFilePath(filename, logsDir);
|
|
178
|
+
if (!filePath) {
|
|
119
179
|
res.status(403).json({
|
|
120
180
|
success: false,
|
|
121
181
|
error: "Access denied",
|
|
@@ -149,10 +209,10 @@ router.get("/stream/:filename", async (req: Request, res: Response) => {
|
|
|
149
209
|
try {
|
|
150
210
|
const { filename } = req.params;
|
|
151
211
|
const logsDir = path.join(process.cwd(), "data", "logs");
|
|
152
|
-
const filePath = path.join(logsDir, filename);
|
|
153
212
|
|
|
154
|
-
// Security check
|
|
155
|
-
|
|
213
|
+
// Security check - prevent directory traversal
|
|
214
|
+
const filePath = validateFilePath(filename, logsDir);
|
|
215
|
+
if (!filePath) {
|
|
156
216
|
res.status(403).json({
|
|
157
217
|
success: false,
|
|
158
218
|
error: "Access denied",
|
|
@@ -203,47 +263,21 @@ router.post("/clear", async (_req: Request, res: Response) => {
|
|
|
203
263
|
});
|
|
204
264
|
|
|
205
265
|
/**
|
|
206
|
-
* Get archived log files
|
|
266
|
+
* Get archived log files (alias for /files for backward compatibility)
|
|
207
267
|
* GET /api/logs/archives
|
|
208
268
|
*/
|
|
209
269
|
router.get("/archives", async (_req: Request, res: Response) => {
|
|
210
270
|
try {
|
|
211
271
|
const logsDir = path.join(process.cwd(), "data", "logs");
|
|
272
|
+
const archives = await getLogFilesFromDir(logsDir);
|
|
212
273
|
|
|
213
|
-
|
|
214
|
-
return res.json({
|
|
215
|
-
success: true,
|
|
216
|
-
archives: [],
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const files = await fs.readdir(logsDir);
|
|
221
|
-
const archives: LogFile[] = [];
|
|
222
|
-
|
|
223
|
-
for (const file of files) {
|
|
224
|
-
if (file.endsWith(".log") || file.endsWith(".txt")) {
|
|
225
|
-
const filePath = path.join(logsDir, file);
|
|
226
|
-
const stats = await fs.stat(filePath);
|
|
227
|
-
archives.push({
|
|
228
|
-
name: file,
|
|
229
|
-
size: stats.size,
|
|
230
|
-
modified: stats.mtime.toISOString(),
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Sort by modified date, newest first
|
|
236
|
-
archives.sort(
|
|
237
|
-
(a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime(),
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
return res.json({
|
|
274
|
+
res.json({
|
|
241
275
|
success: true,
|
|
242
276
|
archives,
|
|
243
277
|
});
|
|
244
278
|
} catch (_error) {
|
|
245
279
|
console.error("Failed to fetch archives:", _error);
|
|
246
|
-
|
|
280
|
+
res.status(500).json({
|
|
247
281
|
success: false,
|
|
248
282
|
error: "Failed to fetch archives",
|
|
249
283
|
archives: [],
|
|
@@ -259,10 +293,10 @@ router.delete("/archive/:filename", async (req: Request, res: Response) => {
|
|
|
259
293
|
try {
|
|
260
294
|
const { filename } = req.params;
|
|
261
295
|
const logsDir = path.join(process.cwd(), "data", "logs");
|
|
262
|
-
const filePath = path.join(logsDir, filename);
|
|
263
296
|
|
|
264
|
-
// Security check
|
|
265
|
-
|
|
297
|
+
// Security check - prevent directory traversal
|
|
298
|
+
const filePath = validateFilePath(filename, logsDir);
|
|
299
|
+
if (!filePath) {
|
|
266
300
|
return res.status(403).json({
|
|
267
301
|
success: false,
|
|
268
302
|
error: "Access denied",
|
|
@@ -338,52 +372,14 @@ router.get("/export", async (req: Request, res: Response) => {
|
|
|
338
372
|
*/
|
|
339
373
|
router.get("/stats", async (_req: Request, res: Response) => {
|
|
340
374
|
try {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
if (!existsSync(logsDir)) {
|
|
344
|
-
res.json({
|
|
345
|
-
success: true,
|
|
346
|
-
stats: {
|
|
347
|
-
totalSize: 0,
|
|
348
|
-
fileCount: 0,
|
|
349
|
-
oldestLog: null,
|
|
350
|
-
newestLog: null,
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const files = await fs.readdir(logsDir);
|
|
357
|
-
let totalSize = 0;
|
|
358
|
-
let oldestTime: Date | null = null;
|
|
359
|
-
let newestTime: Date | null = null;
|
|
360
|
-
let fileCount = 0;
|
|
361
|
-
|
|
362
|
-
for (const file of files) {
|
|
363
|
-
if (file.endsWith(".log") || file.endsWith(".txt")) {
|
|
364
|
-
const filePath = path.join(logsDir, file);
|
|
365
|
-
const stats = await fs.stat(filePath);
|
|
366
|
-
|
|
367
|
-
totalSize += stats.size;
|
|
368
|
-
fileCount++;
|
|
369
|
-
|
|
370
|
-
if (!oldestTime || stats.birthtime < oldestTime) {
|
|
371
|
-
oldestTime = stats.birthtime;
|
|
372
|
-
}
|
|
373
|
-
if (!newestTime || stats.mtime > newestTime) {
|
|
374
|
-
newestTime = stats.mtime;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
375
|
+
const logger = getLogger;
|
|
376
|
+
const stats = await logger.getLogStats();
|
|
378
377
|
|
|
379
378
|
res.json({
|
|
380
379
|
success: true,
|
|
381
380
|
stats: {
|
|
382
|
-
|
|
383
|
-
totalSizeFormatted: formatBytes(totalSize),
|
|
384
|
-
fileCount,
|
|
385
|
-
oldestLog: oldestTime?.toISOString() || null,
|
|
386
|
-
newestLog: newestTime?.toISOString() || null,
|
|
381
|
+
...stats,
|
|
382
|
+
totalSizeFormatted: formatBytes(stats.totalSize || 0),
|
|
387
383
|
},
|
|
388
384
|
});
|
|
389
385
|
} catch (_error) {
|
|
@@ -506,26 +502,21 @@ router.get(
|
|
|
506
502
|
async (req: Request, res: Response) => {
|
|
507
503
|
try {
|
|
508
504
|
const { filename } = req.params;
|
|
509
|
-
|
|
510
|
-
// Security: prevent directory traversal
|
|
511
|
-
const safeName = path.basename(filename);
|
|
512
|
-
|
|
513
|
-
// Check in logs directory first
|
|
514
505
|
const logsDir = path.join(process.cwd(), "data", "logs");
|
|
515
|
-
let filePath = path.join(logsDir, safeName);
|
|
516
506
|
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
filePath = path.join(logsDir, "archive", safeName);
|
|
520
|
-
}
|
|
507
|
+
// Security: prevent directory traversal using proper validation
|
|
508
|
+
let filePath = validateFilePath(filename, logsDir);
|
|
521
509
|
|
|
522
|
-
// If
|
|
523
|
-
if (!
|
|
524
|
-
const
|
|
525
|
-
|
|
510
|
+
// If not found in main logs, try archive directory
|
|
511
|
+
if (!filePath || !existsSync(filePath)) {
|
|
512
|
+
const archiveDir = path.join(logsDir, "archive");
|
|
513
|
+
const archiveFilename = filename.startsWith("archive/")
|
|
514
|
+
? filename.substring("archive/".length)
|
|
515
|
+
: filename;
|
|
516
|
+
filePath = validateFilePath(archiveFilename, archiveDir);
|
|
526
517
|
}
|
|
527
518
|
|
|
528
|
-
if (!existsSync(filePath)) {
|
|
519
|
+
if (!filePath || !existsSync(filePath)) {
|
|
529
520
|
return res.status(404).json({
|
|
530
521
|
success: false,
|
|
531
522
|
error: "File not found",
|
|
@@ -540,7 +531,7 @@ router.get(
|
|
|
540
531
|
res.setHeader("Content-Type", "text/plain");
|
|
541
532
|
}
|
|
542
533
|
|
|
543
|
-
return res.download(filePath,
|
|
534
|
+
return res.download(filePath, path.basename(filePath));
|
|
544
535
|
} catch (_error) {
|
|
545
536
|
return res.status(500).json({
|
|
546
537
|
success: false,
|
|
@@ -587,14 +578,4 @@ router.post("/rotate", async (_req: Request, res: Response) => {
|
|
|
587
578
|
}
|
|
588
579
|
});
|
|
589
580
|
|
|
590
|
-
// Helper function to format bytes
|
|
591
|
-
function formatBytes(bytes: number, decimals = 2): string {
|
|
592
|
-
if (bytes === 0) return "0 Bytes";
|
|
593
|
-
const k = 1024;
|
|
594
|
-
const dm = decimals < 0 ? 0 : decimals;
|
|
595
|
-
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
596
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
597
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
|
598
|
-
}
|
|
599
|
-
|
|
600
581
|
export default router;
|
package/src/index.ts
CHANGED
|
@@ -83,6 +83,20 @@ export type {
|
|
|
83
83
|
ProcessInfo,
|
|
84
84
|
} from "./services/systemMonitor.js";
|
|
85
85
|
|
|
86
|
+
// Email service exports
|
|
87
|
+
export {
|
|
88
|
+
EmailService,
|
|
89
|
+
getEmailService,
|
|
90
|
+
createEmailService,
|
|
91
|
+
} from "./services/emailService.js";
|
|
92
|
+
export type {
|
|
93
|
+
EmailConfig,
|
|
94
|
+
EmailOptions,
|
|
95
|
+
EmailServiceStatus,
|
|
96
|
+
NotificationEvent,
|
|
97
|
+
NotificationEventType,
|
|
98
|
+
} from "./services/emailService.js";
|
|
99
|
+
|
|
86
100
|
// Health check exports
|
|
87
101
|
export {
|
|
88
102
|
createHealthCheckRouter,
|