@manojkmfsi/monodog 1.0.23 → 1.0.25
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/controllers/{commitController.js → commit-controller.js} +5 -4
- package/dist/controllers/{configController.js → config-controller.js} +3 -3
- package/dist/controllers/{healthController.js → health-controller.js} +3 -3
- package/dist/controllers/{packageController.js → package-controller.js} +6 -6
- package/dist/index.js +11 -236
- package/dist/middleware/dashboard-startup.js +124 -0
- package/dist/middleware/error-handler.js +36 -0
- package/dist/middleware/index.js +23 -0
- package/dist/middleware/logger.js +63 -0
- package/dist/middleware/security.js +78 -0
- package/dist/middleware/server-startup.js +117 -0
- package/dist/repositories/commit-repository.js +97 -0
- package/dist/repositories/dependency-repository.js +97 -0
- package/dist/repositories/index.js +18 -0
- package/dist/repositories/package-health-repository.js +65 -0
- package/dist/repositories/package-repository.js +126 -0
- package/dist/repositories/prisma-client.js +57 -0
- package/dist/routes/{commitRoutes.js → commit-routes.js} +2 -2
- package/dist/routes/{configRoutes.js → config-routes.js} +3 -3
- package/dist/routes/{healthRoutes.js → health-routes.js} +3 -3
- package/dist/routes/{packageRoutes.js → package-routes.js} +5 -5
- package/dist/serve.js +15 -2
- package/dist/services/{commitService.js → commit-service.js} +2 -2
- package/dist/services/{configService.js → config-service.js} +2 -40
- package/dist/services/{healthService.js → health-service.js} +11 -63
- package/dist/services/{packageService.js → package-service.js} +80 -54
- package/dist/types/git.js +11 -0
- package/dist/types/index.js +1 -0
- package/package.json +10 -3
- package/prisma/schema/commit.prisma +11 -0
- package/prisma/schema/dependency-info.prisma +12 -0
- package/prisma/schema/health-status.prisma +14 -0
- package/prisma/schema/package-health.prisma +15 -0
- package/prisma/schema/package.prisma +21 -0
- package/prisma/schema/schema.prisma +15 -0
- package/src/controllers/{commitController.ts → commit-controller.ts} +7 -5
- package/src/controllers/{configController.ts → config-controller.ts} +4 -3
- package/src/controllers/{healthController.ts → health-controller.ts} +4 -3
- package/src/controllers/{packageController.ts → package-controller.ts} +7 -6
- package/src/index.ts +9 -281
- package/src/middleware/dashboard-startup.ts +149 -0
- package/src/middleware/error-handler.ts +50 -0
- package/src/middleware/index.ts +20 -0
- package/src/middleware/logger.ts +58 -0
- package/src/middleware/security.ts +81 -0
- package/src/middleware/server-startup.ts +142 -0
- package/src/repositories/commit-repository.ts +107 -0
- package/src/repositories/dependency-repository.ts +109 -0
- package/src/repositories/index.ts +10 -0
- package/src/repositories/package-health-repository.ts +75 -0
- package/src/repositories/package-repository.ts +142 -0
- package/src/repositories/prisma-client.ts +25 -0
- package/src/routes/{commitRoutes.ts → commit-routes.ts} +1 -1
- package/src/routes/{configRoutes.ts → config-routes.ts} +1 -1
- package/src/routes/{healthRoutes.ts → health-routes.ts} +1 -1
- package/src/routes/{packageRoutes.ts → package-routes.ts} +1 -1
- package/src/serve.ts +19 -3
- package/src/services/{commitService.ts → commit-service.ts} +1 -1
- package/src/services/{configService.ts → config-service.ts} +22 -9
- package/src/services/{gitService.ts → git-service.ts} +4 -4
- package/src/services/{healthService.ts → health-service.ts} +17 -35
- package/src/services/package-service.ts +201 -0
- package/src/types/database.ts +57 -1
- package/src/types/git.ts +8 -8
- package/src/types/index.ts +1 -1
- package/dist/utils/db-utils.js +0 -227
- package/prisma/schema.prisma +0 -116
- package/src/services/packageService.ts +0 -115
- package/src/types/monorepo-scanner.d.ts +0 -32
- package/src/utils/db-utils.ts +0 -220
- /package/dist/services/{gitService.js → git-service.js} +0 -0
- /package/prisma/migrations/{20251219074511_create_unique_composite_key_for_commits → 20251219090102_composite_key_for_table_commits}/migration.sql +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Request, Response } from 'express';
|
|
2
|
+
import { updatePackageConfigurationService } from '../services/config-service';
|
|
3
|
+
import { getPackageDetailService, getPackagesService, refreshPackagesService } from '../services/package-service';
|
|
3
4
|
|
|
4
|
-
export const getPackages = async (_req:
|
|
5
|
+
export const getPackages = async (_req: Request, res: Response) => {
|
|
5
6
|
try {
|
|
6
7
|
const transformedPackages = await getPackagesService(_req.app.locals.rootPath);
|
|
7
8
|
res.json(transformedPackages);
|
|
@@ -10,7 +11,7 @@ export const getPackages = async (_req: any, res: any) => {
|
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export const refreshPackages = async (_req:
|
|
14
|
+
export const refreshPackages = async (_req: Request, res: Response) => {
|
|
14
15
|
console.log('Refreshing packages from source...'+ _req.app.locals.rootPath);
|
|
15
16
|
|
|
16
17
|
try {
|
|
@@ -21,7 +22,7 @@ export const refreshPackages = async (_req: any, res: any) => {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export const getPackageDetail = async (_req:
|
|
25
|
+
export const getPackageDetail = async (_req: Request, res: Response) => {
|
|
25
26
|
const { name } = _req.params;
|
|
26
27
|
try {
|
|
27
28
|
const packageDetail = await getPackageDetailService(name)
|
|
@@ -31,7 +32,7 @@ export const getPackageDetail = async (_req: any, res: any) => {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
export const updatePackageConfig = async (req:
|
|
35
|
+
export const updatePackageConfig = async (req: Request, res: Response) => {
|
|
35
36
|
try {
|
|
36
37
|
const { packageName, config, packagePath } = req.body;
|
|
37
38
|
|
package/src/index.ts
CHANGED
|
@@ -1,281 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { json } from 'body-parser';
|
|
11
|
-
import helmet from 'helmet';
|
|
12
|
-
|
|
13
|
-
import { appConfig } from './config-loader';
|
|
14
|
-
|
|
15
|
-
import packageRouter from './routes/packageRoutes';
|
|
16
|
-
import commitRouter from './routes/commitRoutes';
|
|
17
|
-
import healthRouter from './routes/healthRoutes';
|
|
18
|
-
import configRouter from './routes/configRoutes';
|
|
19
|
-
|
|
20
|
-
// Security constants
|
|
21
|
-
const PORT_MIN = 1024;
|
|
22
|
-
const PORT_MAX = 65535;
|
|
23
|
-
|
|
24
|
-
// Validate port number
|
|
25
|
-
function validatePort(port: string | number): number {
|
|
26
|
-
const portNum = typeof port === 'string' ? parseInt(port, 10) : port;
|
|
27
|
-
|
|
28
|
-
if (isNaN(portNum) || portNum < PORT_MIN || portNum > PORT_MAX) {
|
|
29
|
-
throw new Error(`Port must be between ${PORT_MIN} and ${PORT_MAX}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return portNum;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Global error handler
|
|
36
|
-
const errorHandler: ErrorRequestHandler = (err: any, req: Request, res: Response, _next: NextFunction) => {
|
|
37
|
-
const status = err.status || err.statusCode || 500;
|
|
38
|
-
|
|
39
|
-
console.error('[ERROR]', {
|
|
40
|
-
status,
|
|
41
|
-
method: req.method,
|
|
42
|
-
path: req.path,
|
|
43
|
-
message: err.message,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
res.status(status).json({
|
|
47
|
-
error: 'Internal server error'
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// The main function exported and called by the CLI
|
|
52
|
-
export function startServer(
|
|
53
|
-
rootPath: string
|
|
54
|
-
): void {
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const port = appConfig.server.port;
|
|
58
|
-
const host = appConfig.server.host;
|
|
59
|
-
const validatedPort = validatePort(port);
|
|
60
|
-
const app = express();
|
|
61
|
-
|
|
62
|
-
// Set request timeout (30 seconds)
|
|
63
|
-
app.use((req, res, next) => {
|
|
64
|
-
req.setTimeout(30000);
|
|
65
|
-
res.setTimeout(30000);
|
|
66
|
-
next();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
app.locals.rootPath = rootPath;
|
|
70
|
-
|
|
71
|
-
// Security middleware with CSP allowing API calls
|
|
72
|
-
const apiHost = host === '0.0.0.0' ? 'localhost' : host;
|
|
73
|
-
const apiUrl = process.env.API_URL || `http://${apiHost}:${validatedPort}`;
|
|
74
|
-
const dashboardHost = appConfig.dashboard.host === '0.0.0.0' ? 'localhost' : appConfig.dashboard.host;
|
|
75
|
-
const dashboardUrl = `http://${dashboardHost}:${appConfig.dashboard.port}`;
|
|
76
|
-
|
|
77
|
-
app.use(helmet({
|
|
78
|
-
contentSecurityPolicy: {
|
|
79
|
-
directives: {
|
|
80
|
-
defaultSrc: ["'self'"],
|
|
81
|
-
connectSrc: ["'self'", apiUrl, 'http://localhost:*', 'http://127.0.0.1:*'],
|
|
82
|
-
scriptSrc: ["'self'"],
|
|
83
|
-
imgSrc: ["'self'", 'data:', 'https:'],
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
}));
|
|
87
|
-
app.use(cors({
|
|
88
|
-
origin: process.env.CORS_ORIGIN || dashboardUrl,
|
|
89
|
-
credentials: true,
|
|
90
|
-
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
91
|
-
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
92
|
-
}));
|
|
93
|
-
|
|
94
|
-
app.use(json({ limit: '1mb' }));
|
|
95
|
-
|
|
96
|
-
// Request logging middleware (safe version)
|
|
97
|
-
app.use((_req: Request, _res: Response, next: NextFunction) => {
|
|
98
|
-
console.log(`[${new Date().toISOString()}] ${_req.method} ${_req.path}`);
|
|
99
|
-
next();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
app.use('/api/packages', packageRouter);
|
|
103
|
-
|
|
104
|
-
// Get commit details
|
|
105
|
-
app.use('/api/commits/', commitRouter);
|
|
106
|
-
|
|
107
|
-
// Health check endpoint
|
|
108
|
-
app.use('/api/health/', healthRouter);
|
|
109
|
-
|
|
110
|
-
// Configuration endpoint
|
|
111
|
-
app.use('/api/config/', configRouter);
|
|
112
|
-
|
|
113
|
-
// 404 handler
|
|
114
|
-
app.use('*', (_, res) => {
|
|
115
|
-
res.status(404).json({
|
|
116
|
-
error: 'Endpoint not found',
|
|
117
|
-
timestamp: Date.now(),
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Global error handler (must be last)
|
|
122
|
-
app.use(errorHandler);
|
|
123
|
-
|
|
124
|
-
const server = app.listen(validatedPort, host, () => {
|
|
125
|
-
console.log(`Backend server running on http://${host}:${validatedPort}`);
|
|
126
|
-
console.log(`API endpoints available:`);
|
|
127
|
-
console.log(` - GET /api/health`);
|
|
128
|
-
console.log(` - GET /api/packages/refresh`);
|
|
129
|
-
console.log(` - GET /api/packages`);
|
|
130
|
-
console.log(` - GET /api/packages/:name`);
|
|
131
|
-
console.log(` - PUT /api/packages/update-config`);
|
|
132
|
-
console.log(` - GET /api/commits/:packagePath`);
|
|
133
|
-
console.log(` - GET /api/health/packages`);
|
|
134
|
-
console.log(` - PUT /api/config/files/:id`);
|
|
135
|
-
console.log(` - GET /api/config/files`);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
server.on('error', (err: any) => {
|
|
139
|
-
// Handle common errors like EADDRINUSE (port already in use)
|
|
140
|
-
if (err.code === 'EADDRINUSE') {
|
|
141
|
-
console.error(
|
|
142
|
-
`Error: Port ${validatedPort} is already in use. Please specify a different port.`
|
|
143
|
-
);
|
|
144
|
-
process.exit(1);
|
|
145
|
-
} else if (err.code === 'EACCES') {
|
|
146
|
-
console.error(
|
|
147
|
-
`Error: Permission denied to listen on port ${validatedPort}. Use a port above 1024.`
|
|
148
|
-
);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
} else {
|
|
151
|
-
console.error('Server failed to start:', err.message);
|
|
152
|
-
process.exit(1);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Graceful shutdown
|
|
157
|
-
process.on('SIGTERM', () => {
|
|
158
|
-
console.log('SIGTERM signal received: closing HTTP server');
|
|
159
|
-
server.close(() => {
|
|
160
|
-
console.log('HTTP server closed');
|
|
161
|
-
process.exit(0);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
} catch (error: any) {
|
|
165
|
-
console.error('Failed to start server:', error.message);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function serveDashboard(
|
|
171
|
-
rootPath: string
|
|
172
|
-
): void {
|
|
173
|
-
try {
|
|
174
|
-
const port = appConfig.dashboard.port;
|
|
175
|
-
const host = appConfig.dashboard.host;
|
|
176
|
-
const validatedPort = validatePort(port);
|
|
177
|
-
const app = express();
|
|
178
|
-
|
|
179
|
-
// Security middleware
|
|
180
|
-
const serverHost = appConfig.server.host === '0.0.0.0' ? 'localhost' : appConfig.server.host;
|
|
181
|
-
const apiUrl = process.env.API_URL || `http://${serverHost}:${appConfig.server.port}`;
|
|
182
|
-
app.use(helmet({
|
|
183
|
-
contentSecurityPolicy: {
|
|
184
|
-
directives: {
|
|
185
|
-
defaultSrc: ["'self'"],
|
|
186
|
-
connectSrc: ["'self'", apiUrl, 'http://localhost:*', 'http://127.0.0.1:*'],
|
|
187
|
-
scriptSrc: ["'self'"],
|
|
188
|
-
imgSrc: ["'self'", 'data:', 'https:'],
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
}));
|
|
192
|
-
|
|
193
|
-
// Strict CORS for dashboard
|
|
194
|
-
app.use(cors({
|
|
195
|
-
origin: false, // Don't allow any origin for static assets
|
|
196
|
-
}));
|
|
197
|
-
|
|
198
|
-
// Set request timeout
|
|
199
|
-
app.use((req, res, next) => {
|
|
200
|
-
req.setTimeout(30000);
|
|
201
|
-
res.setTimeout(30000);
|
|
202
|
-
next();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
app.get('/env-config.js', (req, res) => {
|
|
206
|
-
res.setHeader('Content-Type', 'application/javascript');
|
|
207
|
-
res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate');
|
|
208
|
-
|
|
209
|
-
const serverHost = appConfig.server.host === '0.0.0.0' ? 'localhost' : appConfig.server.host;
|
|
210
|
-
const apiUrl = process.env.API_URL || `http://${serverHost}:${appConfig.server.port}`;
|
|
211
|
-
res.send(
|
|
212
|
-
`window.ENV = { API_URL: "${apiUrl}" };`
|
|
213
|
-
);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// This code makes sure that any request that does not matches a static file
|
|
217
|
-
// in the build folder, will just serve index.html. Client side routing is
|
|
218
|
-
// going to make sure that the correct content will be loaded.
|
|
219
|
-
app.use((req, res, next) => {
|
|
220
|
-
if (/(.ico|.js|.css|.jpg|.png|.map|.woff|.woff2|.ttf)$/i.test(req.path)) {
|
|
221
|
-
next();
|
|
222
|
-
} else {
|
|
223
|
-
res.header(
|
|
224
|
-
'Cache-Control',
|
|
225
|
-
'private, no-cache, no-store, must-revalidate'
|
|
226
|
-
);
|
|
227
|
-
res.header('Expires', '-1');
|
|
228
|
-
res.header('Pragma', 'no-cache');
|
|
229
|
-
res.sendFile('index.html', {
|
|
230
|
-
root: path.resolve(__dirname, '..', 'monodog-dashboard', 'dist'),
|
|
231
|
-
}, (err) => {
|
|
232
|
-
if (err) {
|
|
233
|
-
console.error('Error serving index.html:', err.message);
|
|
234
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
const staticPath = path.join(__dirname, '..', 'monodog-dashboard', 'dist');
|
|
241
|
-
console.log('Serving static files from:', staticPath);
|
|
242
|
-
app.use(express.static(staticPath, {
|
|
243
|
-
maxAge: '1d',
|
|
244
|
-
etag: false,
|
|
245
|
-
dotfiles: 'deny', // Don't serve dot files
|
|
246
|
-
}));
|
|
247
|
-
|
|
248
|
-
// Global error handler
|
|
249
|
-
app.use(errorHandler);
|
|
250
|
-
|
|
251
|
-
const server = app.listen(validatedPort, host, () => {
|
|
252
|
-
console.log(`Dashboard listening on http://${host}:${validatedPort}`);
|
|
253
|
-
console.log('Press Ctrl+C to quit.');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
server.on('error', (err: any) => {
|
|
257
|
-
if (err.code === 'EADDRINUSE') {
|
|
258
|
-
console.error(`Error: Port ${validatedPort} is already in use.`);
|
|
259
|
-
process.exit(1);
|
|
260
|
-
} else if (err.code === 'EACCES') {
|
|
261
|
-
console.error(`Error: Permission denied to listen on port ${validatedPort}.`);
|
|
262
|
-
process.exit(1);
|
|
263
|
-
} else {
|
|
264
|
-
console.error('Server failed to start:', err.message);
|
|
265
|
-
process.exit(1);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// Graceful shutdown
|
|
270
|
-
process.on('SIGTERM', () => {
|
|
271
|
-
console.log('SIGTERM signal received: closing dashboard server');
|
|
272
|
-
server.close(() => {
|
|
273
|
-
console.log('Dashboard server closed');
|
|
274
|
-
process.exit(0);
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
} catch (error: any) {
|
|
278
|
-
console.error('Failed to start dashboard:', error.message);
|
|
279
|
-
process.exit(1);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Monodog Application Entry Point
|
|
3
|
+
*
|
|
4
|
+
* This file exports the main server and dashboard startup functions
|
|
5
|
+
* All middleware, security, and error handling logic has been moved to separate files
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { startServer } from './middleware/server-startup';
|
|
9
|
+
export { serveDashboard } from './middleware/dashboard-startup';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard server startup logic
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import type { Express } from 'express';
|
|
8
|
+
import { httpLogger, AppLogger } from './logger';
|
|
9
|
+
|
|
10
|
+
import { appConfig } from '../config-loader';
|
|
11
|
+
import {
|
|
12
|
+
errorHandler,
|
|
13
|
+
} from './error-handler';
|
|
14
|
+
import {
|
|
15
|
+
createHelmetMiddleware,
|
|
16
|
+
createDashboardCorsMiddleware,
|
|
17
|
+
createTimeoutMiddleware,
|
|
18
|
+
buildApiUrl,
|
|
19
|
+
} from './security';
|
|
20
|
+
|
|
21
|
+
// Security constants
|
|
22
|
+
const PORT_MIN = 1024;
|
|
23
|
+
const PORT_MAX = 65535;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate port number
|
|
27
|
+
*/
|
|
28
|
+
function validatePort(port: string | number): number {
|
|
29
|
+
const portNum = typeof port === 'string' ? parseInt(port, 10) : port;
|
|
30
|
+
|
|
31
|
+
if (isNaN(portNum) || portNum < PORT_MIN || portNum > PORT_MAX) {
|
|
32
|
+
throw new Error(`Port must be between ${PORT_MIN} and ${PORT_MAX}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return portNum;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create Express app for dashboard with middleware
|
|
40
|
+
*/
|
|
41
|
+
function createDashboardApp(): Express {
|
|
42
|
+
const app = express();
|
|
43
|
+
|
|
44
|
+
// Timeout middleware
|
|
45
|
+
app.use(createTimeoutMiddleware());
|
|
46
|
+
|
|
47
|
+
// Security setup
|
|
48
|
+
const serverHost = appConfig.server.host === '0.0.0.0'
|
|
49
|
+
? 'localhost'
|
|
50
|
+
: appConfig.server.host;
|
|
51
|
+
const apiUrl = buildApiUrl(serverHost, appConfig.server.port);
|
|
52
|
+
|
|
53
|
+
app.use(createHelmetMiddleware(apiUrl));
|
|
54
|
+
app.use(createDashboardCorsMiddleware());
|
|
55
|
+
|
|
56
|
+
// Environment config endpoint
|
|
57
|
+
app.get('/env-config.js', (_req, res) => {
|
|
58
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
59
|
+
res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate');
|
|
60
|
+
|
|
61
|
+
res.send(
|
|
62
|
+
`window.ENV = { API_URL: "${apiUrl}" };`
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Request logging
|
|
67
|
+
app.use(httpLogger);
|
|
68
|
+
// app.use(requestLogger);
|
|
69
|
+
|
|
70
|
+
// SPA routing: serve index.html for non-static routes
|
|
71
|
+
app.use((_req, _res, next) => {
|
|
72
|
+
if (/(.ico|.js|.css|.jpg|.png|.map|.woff|.woff2|.ttf)$/i.test(_req.path)) {
|
|
73
|
+
next();
|
|
74
|
+
} else {
|
|
75
|
+
_res.header(
|
|
76
|
+
'Cache-Control',
|
|
77
|
+
'private, no-cache, no-store, must-revalidate'
|
|
78
|
+
);
|
|
79
|
+
_res.header('Expires', '-1');
|
|
80
|
+
_res.header('Pragma', 'no-cache');
|
|
81
|
+
_res.sendFile('index.html', {
|
|
82
|
+
root: path.resolve(__dirname, '..', '..', 'monodog-dashboard', 'dist'),
|
|
83
|
+
}, (err: Error | null) => {
|
|
84
|
+
if (err) {
|
|
85
|
+
AppLogger.error('Error serving index.html:', err);
|
|
86
|
+
_res.status(500).json({ error: 'Internal server error' });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Static files
|
|
93
|
+
const staticPath = path.join(__dirname, '..', '..', 'monodog-dashboard', 'dist');
|
|
94
|
+
AppLogger.debug('Serving static files from:', { path: staticPath });
|
|
95
|
+
app.use(express.static(staticPath, {
|
|
96
|
+
maxAge: '1d',
|
|
97
|
+
etag: false,
|
|
98
|
+
dotfiles: 'deny',
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
// Global error handler (must be last)
|
|
102
|
+
app.use(errorHandler);
|
|
103
|
+
|
|
104
|
+
return app;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start the dashboard server
|
|
109
|
+
*/
|
|
110
|
+
export function serveDashboard(rootPath: string): void {
|
|
111
|
+
try {
|
|
112
|
+
const port = appConfig.dashboard.port;
|
|
113
|
+
const host = appConfig.dashboard.host;
|
|
114
|
+
const validatedPort = validatePort(port);
|
|
115
|
+
|
|
116
|
+
const app = createDashboardApp();
|
|
117
|
+
|
|
118
|
+
const server = app.listen(validatedPort, host, () => {
|
|
119
|
+
console.log(`Dashboard listening on http://${host}:${validatedPort}`);
|
|
120
|
+
console.log('Press Ctrl+C to quit.');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
124
|
+
if (err.code === 'EADDRINUSE') {
|
|
125
|
+
AppLogger.error(`Port ${validatedPort} is already in use.`, err);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
} else if (err.code === 'EACCES') {
|
|
128
|
+
AppLogger.error(`Permission denied to listen on port ${validatedPort}.`, err);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
} else {
|
|
131
|
+
AppLogger.error('Server failed to start:', err);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Graceful shutdown
|
|
137
|
+
process.on('SIGTERM', () => {
|
|
138
|
+
AppLogger.info('SIGTERM signal received: closing dashboard server');
|
|
139
|
+
server.close(() => {
|
|
140
|
+
AppLogger.info('Dashboard server closed');
|
|
141
|
+
process.exit(0);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
} catch (error: unknown) {
|
|
145
|
+
const err = error as Error & { message?: string };
|
|
146
|
+
AppLogger.error('Failed to start dashboard:', err);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error handling middleware for Express
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
|
|
6
|
+
import { AppLogger } from './logger';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom error interface extending Error
|
|
10
|
+
*/
|
|
11
|
+
export interface CustomError extends Error {
|
|
12
|
+
status?: number;
|
|
13
|
+
statusCode?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Global error handler middleware
|
|
18
|
+
* Must be registered last in the middleware chain
|
|
19
|
+
*/
|
|
20
|
+
export const errorHandler: ErrorRequestHandler = (
|
|
21
|
+
err: CustomError,
|
|
22
|
+
req: Request,
|
|
23
|
+
res: Response,
|
|
24
|
+
_next: NextFunction
|
|
25
|
+
): void => {
|
|
26
|
+
const status = err.status || err.statusCode || 500;
|
|
27
|
+
|
|
28
|
+
AppLogger.error('Request error occurred', {
|
|
29
|
+
status,
|
|
30
|
+
method: req.method,
|
|
31
|
+
path: req.path,
|
|
32
|
+
message: err.message,
|
|
33
|
+
timestamp: new Date().toISOString(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
res.status(status).json({
|
|
37
|
+
error: 'Internal server error',
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 404 Not Found handler
|
|
44
|
+
*/
|
|
45
|
+
export const notFoundHandler = (_req: Request, res: Response): void => {
|
|
46
|
+
res.status(404).json({
|
|
47
|
+
error: 'Endpoint not found',
|
|
48
|
+
timestamp: Date.now(),
|
|
49
|
+
});
|
|
50
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { errorHandler, notFoundHandler } from './error-handler';
|
|
6
|
+
export type { CustomError } from './error-handler';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
createHelmetMiddleware,
|
|
10
|
+
createApiCorsMiddleware,
|
|
11
|
+
createDashboardCorsMiddleware,
|
|
12
|
+
createTimeoutMiddleware,
|
|
13
|
+
buildApiUrl,
|
|
14
|
+
buildDashboardUrl,
|
|
15
|
+
} from './security';
|
|
16
|
+
|
|
17
|
+
export { startServer } from './server-startup';
|
|
18
|
+
export { serveDashboard } from './dashboard-startup';
|
|
19
|
+
|
|
20
|
+
export { httpLogger, AppLogger } from './logger';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger configuration using Morgan
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import morgan from 'morgan';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* HTTP request logger middleware using Morgan
|
|
9
|
+
*/
|
|
10
|
+
export const httpLogger = morgan('dev');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Application logger for non-HTTP events
|
|
14
|
+
*/
|
|
15
|
+
export class AppLogger {
|
|
16
|
+
private static readonly prefix = '[APP]';
|
|
17
|
+
|
|
18
|
+
static info(message: string, data?: Record<string, unknown>): void {
|
|
19
|
+
if (process.env.LOG_LEVEL == 'info' || process.env.LOG_LEVEL == 'debug') {
|
|
20
|
+
if (data) {
|
|
21
|
+
console.log(`${this.prefix} [INFO]`, message, JSON.stringify(data, null, 2));
|
|
22
|
+
} else {
|
|
23
|
+
console.log(`${this.prefix} [INFO]`, message);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static error(message: string, error?: Error | Record<string, unknown>): void {
|
|
29
|
+
if (error instanceof Error) {
|
|
30
|
+
console.error(`${this.prefix} [ERROR]`, message, {
|
|
31
|
+
message: error.message,
|
|
32
|
+
stack: error.stack,
|
|
33
|
+
});
|
|
34
|
+
} else if (error) {
|
|
35
|
+
console.error(`${this.prefix} [ERROR]`, message, error);
|
|
36
|
+
} else {
|
|
37
|
+
console.error(`${this.prefix} [ERROR]`, message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static warn(message: string, data?: Record<string, unknown>): void {
|
|
42
|
+
if (data) {
|
|
43
|
+
console.warn(`${this.prefix} [WARN]`, message, JSON.stringify(data, null, 2));
|
|
44
|
+
} else {
|
|
45
|
+
console.warn(`${this.prefix} [WARN]`, message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static debug(message: string, data?: Record<string, unknown>): void {
|
|
50
|
+
if (process.env.LOG_LEVEL == 'debug') {
|
|
51
|
+
if (data) {
|
|
52
|
+
console.log(`${this.prefix} [DEBUG]`, message, JSON.stringify(data, null, 2));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(`${this.prefix} [DEBUG]`, message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security middleware and configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Request, Response, NextFunction } from 'express';
|
|
6
|
+
import helmet from 'helmet';
|
|
7
|
+
import cors, { CorsOptions } from 'cors';
|
|
8
|
+
import type { MonodogConfig } from '../types/config';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create Helmet security middleware with Content Security Policy
|
|
12
|
+
*/
|
|
13
|
+
export function createHelmetMiddleware(apiUrl: string) {
|
|
14
|
+
return helmet({
|
|
15
|
+
contentSecurityPolicy: {
|
|
16
|
+
directives: {
|
|
17
|
+
defaultSrc: ["'self'"],
|
|
18
|
+
connectSrc: ["'self'", apiUrl, 'http://localhost:*', 'http://127.0.0.1:*'],
|
|
19
|
+
scriptSrc: ["'self'"],
|
|
20
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create CORS middleware for API server
|
|
28
|
+
*/
|
|
29
|
+
export function createApiCorsMiddleware(dashboardUrl: string) {
|
|
30
|
+
const corsOptions: CorsOptions = {
|
|
31
|
+
origin: dashboardUrl,
|
|
32
|
+
credentials: true,
|
|
33
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
34
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return cors(corsOptions);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create CORS middleware for dashboard (no cross-origin)
|
|
42
|
+
*/
|
|
43
|
+
export function createDashboardCorsMiddleware() {
|
|
44
|
+
const corsOptions: CorsOptions = {
|
|
45
|
+
origin: false, // Don't allow any origin for static assets
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return cors(corsOptions);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Request timeout middleware (30 seconds)
|
|
53
|
+
*/
|
|
54
|
+
export function createTimeoutMiddleware() {
|
|
55
|
+
return (req: Request, res: Response, next: NextFunction): void => {
|
|
56
|
+
req.setTimeout(30000);
|
|
57
|
+
res.setTimeout(30000);
|
|
58
|
+
next();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build API URL based on config
|
|
64
|
+
*/
|
|
65
|
+
export function buildApiUrl(
|
|
66
|
+
host: string,
|
|
67
|
+
port: number
|
|
68
|
+
): string {
|
|
69
|
+
const apiHost = host === '0.0.0.0' ? 'localhost' : host;
|
|
70
|
+
return `http://${apiHost}:${port}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build dashboard URL based on config
|
|
75
|
+
*/
|
|
76
|
+
export function buildDashboardUrl(config: MonodogConfig): string {
|
|
77
|
+
const dashboardHost = config.dashboard.host === '0.0.0.0'
|
|
78
|
+
? 'localhost'
|
|
79
|
+
: config.dashboard.host;
|
|
80
|
+
return `http://${dashboardHost}:${config.dashboard.port}`;
|
|
81
|
+
}
|