@manojkmfsi/monodog 1.0.24 → 1.1.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/config/swagger-config.js +345 -0
- package/dist/constants/index.js +26 -0
- package/dist/constants/middleware.js +71 -0
- package/dist/constants/port.js +20 -0
- package/dist/constants/security.js +67 -0
- package/dist/middleware/dashboard-startup.js +20 -21
- package/dist/middleware/error-handler.js +3 -10
- package/dist/middleware/index.js +4 -2
- package/dist/middleware/logger.js +63 -0
- package/dist/middleware/security.js +11 -10
- package/dist/middleware/server-startup.js +32 -25
- package/dist/middleware/swagger-middleware.js +54 -0
- package/dist/routes/health-routes.js +1 -1
- package/dist/routes/package-routes.js +1 -1
- package/dist/serve.js +15 -2
- package/dist/services/health-service.js +84 -64
- package/dist/services/package-service.js +23 -1
- package/monodog-dashboard/dist/assets/{index-746f6c13.js → index-45e19f29.js} +1 -1
- package/monodog-dashboard/dist/index.html +1 -1
- package/package.json +14 -4
- 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/config/swagger-config.ts +344 -0
- package/src/constants/index.ts +13 -0
- package/src/constants/middleware.ts +83 -0
- package/src/constants/port.ts +20 -0
- package/src/constants/security.ts +78 -0
- package/src/middleware/dashboard-startup.ts +35 -24
- package/src/middleware/error-handler.ts +2 -15
- package/src/middleware/index.ts +3 -1
- package/src/middleware/logger.ts +58 -0
- package/src/middleware/security.ts +19 -10
- package/src/middleware/server-startup.ts +43 -30
- package/src/middleware/swagger-middleware.ts +57 -0
- package/src/routes/health-routes.ts +1 -1
- package/src/routes/package-routes.ts +1 -1
- package/src/serve.ts +19 -3
- package/src/services/health-service.ts +103 -79
- package/src/services/package-service.ts +27 -1
- package/src/types/swagger-jsdoc.d.ts +15 -0
- package/prisma/schema.prisma +0 -116
- /package/prisma/migrations/{20251219074511_create_unique_composite_key_for_commits → 20251219090102_composite_key_for_table_commits}/migration.sql +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Constants
|
|
3
|
+
* Defines security-related configuration and constants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Request timeout duration in milliseconds (30 seconds)
|
|
8
|
+
*/
|
|
9
|
+
export const REQUEST_TIMEOUT = 30000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Response timeout duration in milliseconds (30 seconds)
|
|
13
|
+
*/
|
|
14
|
+
export const RESPONSE_TIMEOUT = 30000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CORS methods allowed for API
|
|
18
|
+
*/
|
|
19
|
+
export const CORS_API_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* CORS headers allowed
|
|
23
|
+
*/
|
|
24
|
+
export const CORS_ALLOWED_HEADERS = ['Content-Type', 'Authorization'] as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Body parser JSON size limit
|
|
28
|
+
*/
|
|
29
|
+
export const BODY_PARSER_LIMIT = '1mb';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Cache control header for no-cache responses
|
|
33
|
+
*/
|
|
34
|
+
export const CACHE_CONTROL_NO_CACHE = 'private, no-cache, no-store, must-revalidate';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cache control header for static assets
|
|
38
|
+
*/
|
|
39
|
+
export const CACHE_CONTROL_STATIC = '1d';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Default localhost hostname
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_LOCALHOST = 'localhost';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wildcard address for listening on all interfaces
|
|
48
|
+
*/
|
|
49
|
+
export const WILDCARD_ADDRESS = '0.0.0.0';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* HTTP protocol prefix
|
|
53
|
+
*/
|
|
54
|
+
export const HTTP_PROTOCOL = 'http://';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* CSP directives for Helmet
|
|
58
|
+
*/
|
|
59
|
+
export const CSP_DIRECTIVES = {
|
|
60
|
+
defaultSrc: ["'self'"],
|
|
61
|
+
scriptSrc: ["'self'"],
|
|
62
|
+
imgSrc: ["'self'", 'data:', 'https:'],
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Static file extensions pattern
|
|
67
|
+
*/
|
|
68
|
+
export const STATIC_FILE_PATTERN = /(.ico|.js|.css|.jpg|.png|.map|.woff|.woff2|.ttf)$/i;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Expires header for no-cache responses
|
|
72
|
+
*/
|
|
73
|
+
export const EXPIRES_HEADER = '-1';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pragma header for no-cache responses
|
|
77
|
+
*/
|
|
78
|
+
export const PRAGMA_HEADER = 'no-cache';
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
import express from 'express';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import type { Express } from 'express';
|
|
8
|
+
import { httpLogger, AppLogger } from './logger';
|
|
8
9
|
|
|
9
10
|
import { appConfig } from '../config-loader';
|
|
10
11
|
import {
|
|
11
12
|
errorHandler,
|
|
12
|
-
requestLogger,
|
|
13
13
|
} from './error-handler';
|
|
14
14
|
import {
|
|
15
15
|
createHelmetMiddleware,
|
|
@@ -17,10 +17,23 @@ import {
|
|
|
17
17
|
createTimeoutMiddleware,
|
|
18
18
|
buildApiUrl,
|
|
19
19
|
} from './security';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
import {
|
|
21
|
+
PORT_MIN,
|
|
22
|
+
PORT_MAX,
|
|
23
|
+
PORT_VALIDATION_ERROR_MESSAGE,
|
|
24
|
+
CACHE_CONTROL_NO_CACHE,
|
|
25
|
+
EXPIRES_HEADER,
|
|
26
|
+
PRAGMA_HEADER,
|
|
27
|
+
STATIC_FILE_PATTERN,
|
|
28
|
+
CONTENT_TYPE_JAVASCRIPT,
|
|
29
|
+
ERROR_SERVING_INDEX_HTML,
|
|
30
|
+
MESSAGE_GRACEFUL_SHUTDOWN,
|
|
31
|
+
MESSAGE_DASHBOARD_GRACEFUL_SHUTDOWN,
|
|
32
|
+
MESSAGE_DASHBOARD_CLOSED,
|
|
33
|
+
SUCCESS_DASHBOARD_START,
|
|
34
|
+
ERROR_PORT_IN_USE,
|
|
35
|
+
ERROR_PERMISSION_DENIED,
|
|
36
|
+
} from '../constants';
|
|
24
37
|
|
|
25
38
|
/**
|
|
26
39
|
* Validate port number
|
|
@@ -29,7 +42,7 @@ function validatePort(port: string | number): number {
|
|
|
29
42
|
const portNum = typeof port === 'string' ? parseInt(port, 10) : port;
|
|
30
43
|
|
|
31
44
|
if (isNaN(portNum) || portNum < PORT_MIN || portNum > PORT_MAX) {
|
|
32
|
-
throw new Error(
|
|
45
|
+
throw new Error(PORT_VALIDATION_ERROR_MESSAGE(PORT_MIN, PORT_MAX));
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
return portNum;
|
|
@@ -55,8 +68,8 @@ function createDashboardApp(): Express {
|
|
|
55
68
|
|
|
56
69
|
// Environment config endpoint
|
|
57
70
|
app.get('/env-config.js', (_req, res) => {
|
|
58
|
-
res.setHeader('Content-Type',
|
|
59
|
-
res.setHeader('Cache-Control',
|
|
71
|
+
res.setHeader('Content-Type', CONTENT_TYPE_JAVASCRIPT);
|
|
72
|
+
res.setHeader('Cache-Control', CACHE_CONTROL_NO_CACHE);
|
|
60
73
|
|
|
61
74
|
res.send(
|
|
62
75
|
`window.ENV = { API_URL: "${apiUrl}" };`
|
|
@@ -64,24 +77,24 @@ function createDashboardApp(): Express {
|
|
|
64
77
|
});
|
|
65
78
|
|
|
66
79
|
// Request logging
|
|
67
|
-
app.use(
|
|
80
|
+
app.use(httpLogger);
|
|
68
81
|
|
|
69
82
|
// SPA routing: serve index.html for non-static routes
|
|
70
83
|
app.use((_req, _res, next) => {
|
|
71
|
-
if (
|
|
84
|
+
if (STATIC_FILE_PATTERN.test(_req.path)) {
|
|
72
85
|
next();
|
|
73
86
|
} else {
|
|
74
87
|
_res.header(
|
|
75
88
|
'Cache-Control',
|
|
76
|
-
|
|
89
|
+
CACHE_CONTROL_NO_CACHE
|
|
77
90
|
);
|
|
78
|
-
_res.header('Expires',
|
|
79
|
-
_res.header('Pragma',
|
|
91
|
+
_res.header('Expires', EXPIRES_HEADER);
|
|
92
|
+
_res.header('Pragma', PRAGMA_HEADER);
|
|
80
93
|
_res.sendFile('index.html', {
|
|
81
94
|
root: path.resolve(__dirname, '..', '..', 'monodog-dashboard', 'dist'),
|
|
82
95
|
}, (err: Error | null) => {
|
|
83
96
|
if (err) {
|
|
84
|
-
|
|
97
|
+
AppLogger.error(ERROR_SERVING_INDEX_HTML, err);
|
|
85
98
|
_res.status(500).json({ error: 'Internal server error' });
|
|
86
99
|
}
|
|
87
100
|
});
|
|
@@ -90,7 +103,7 @@ function createDashboardApp(): Express {
|
|
|
90
103
|
|
|
91
104
|
// Static files
|
|
92
105
|
const staticPath = path.join(__dirname, '..', '..', 'monodog-dashboard', 'dist');
|
|
93
|
-
|
|
106
|
+
AppLogger.debug('Serving static files from:', { path: staticPath });
|
|
94
107
|
app.use(express.static(staticPath, {
|
|
95
108
|
maxAge: '1d',
|
|
96
109
|
etag: false,
|
|
@@ -115,36 +128,34 @@ export function serveDashboard(rootPath: string): void {
|
|
|
115
128
|
const app = createDashboardApp();
|
|
116
129
|
|
|
117
130
|
const server = app.listen(validatedPort, host, () => {
|
|
118
|
-
console.log(
|
|
131
|
+
console.log(SUCCESS_DASHBOARD_START(host, validatedPort));
|
|
119
132
|
console.log('Press Ctrl+C to quit.');
|
|
120
133
|
});
|
|
121
134
|
|
|
122
135
|
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
123
136
|
if (err.code === 'EADDRINUSE') {
|
|
124
|
-
|
|
137
|
+
AppLogger.error(ERROR_PORT_IN_USE(validatedPort), err);
|
|
125
138
|
process.exit(1);
|
|
126
139
|
} else if (err.code === 'EACCES') {
|
|
127
|
-
|
|
128
|
-
`Error: Permission denied to listen on port ${validatedPort}.`
|
|
129
|
-
);
|
|
140
|
+
AppLogger.error(ERROR_PERMISSION_DENIED(validatedPort), err);
|
|
130
141
|
process.exit(1);
|
|
131
142
|
} else {
|
|
132
|
-
|
|
143
|
+
AppLogger.error('Server failed to start:', err);
|
|
133
144
|
process.exit(1);
|
|
134
145
|
}
|
|
135
146
|
});
|
|
136
147
|
|
|
137
148
|
// Graceful shutdown
|
|
138
149
|
process.on('SIGTERM', () => {
|
|
139
|
-
|
|
150
|
+
AppLogger.info(MESSAGE_DASHBOARD_GRACEFUL_SHUTDOWN);
|
|
140
151
|
server.close(() => {
|
|
141
|
-
|
|
152
|
+
AppLogger.info(MESSAGE_DASHBOARD_CLOSED);
|
|
142
153
|
process.exit(0);
|
|
143
154
|
});
|
|
144
155
|
});
|
|
145
156
|
} catch (error: unknown) {
|
|
146
157
|
const err = error as Error & { message?: string };
|
|
147
|
-
|
|
158
|
+
AppLogger.error('Failed to start dashboard:', err);
|
|
148
159
|
process.exit(1);
|
|
149
160
|
}
|
|
150
161
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
|
|
6
|
+
import { AppLogger } from './logger';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Custom error interface extending Error
|
|
@@ -24,7 +25,7 @@ export const errorHandler: ErrorRequestHandler = (
|
|
|
24
25
|
): void => {
|
|
25
26
|
const status = err.status || err.statusCode || 500;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
AppLogger.error('Request error occurred', {
|
|
28
29
|
status,
|
|
29
30
|
method: req.method,
|
|
30
31
|
path: req.path,
|
|
@@ -47,17 +48,3 @@ export const notFoundHandler = (_req: Request, res: Response): void => {
|
|
|
47
48
|
timestamp: Date.now(),
|
|
48
49
|
});
|
|
49
50
|
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Request logging middleware
|
|
53
|
-
*/
|
|
54
|
-
export const requestLogger = (
|
|
55
|
-
req: Request,
|
|
56
|
-
_res: Response,
|
|
57
|
-
next: NextFunction
|
|
58
|
-
): void => {
|
|
59
|
-
console.log(
|
|
60
|
-
`[${new Date().toISOString()}] ${req.method} ${req.path}`
|
|
61
|
-
);
|
|
62
|
-
next();
|
|
63
|
-
};
|
package/src/middleware/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Middleware exports
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export { errorHandler, notFoundHandler
|
|
5
|
+
export { errorHandler, notFoundHandler } from './error-handler';
|
|
6
6
|
export type { CustomError } from './error-handler';
|
|
7
7
|
|
|
8
8
|
export {
|
|
@@ -16,3 +16,5 @@ export {
|
|
|
16
16
|
|
|
17
17
|
export { startServer } from './server-startup';
|
|
18
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
|
+
}
|
|
@@ -6,6 +6,15 @@ import { Request, Response, NextFunction } from 'express';
|
|
|
6
6
|
import helmet from 'helmet';
|
|
7
7
|
import cors, { CorsOptions } from 'cors';
|
|
8
8
|
import type { MonodogConfig } from '../types/config';
|
|
9
|
+
import {
|
|
10
|
+
REQUEST_TIMEOUT,
|
|
11
|
+
RESPONSE_TIMEOUT,
|
|
12
|
+
CORS_API_METHODS,
|
|
13
|
+
CORS_ALLOWED_HEADERS,
|
|
14
|
+
DEFAULT_LOCALHOST,
|
|
15
|
+
WILDCARD_ADDRESS,
|
|
16
|
+
HTTP_PROTOCOL,
|
|
17
|
+
} from '../constants';
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* Create Helmet security middleware with Content Security Policy
|
|
@@ -28,10 +37,10 @@ export function createHelmetMiddleware(apiUrl: string) {
|
|
|
28
37
|
*/
|
|
29
38
|
export function createApiCorsMiddleware(dashboardUrl: string) {
|
|
30
39
|
const corsOptions: CorsOptions = {
|
|
31
|
-
origin:
|
|
40
|
+
origin: dashboardUrl,
|
|
32
41
|
credentials: true,
|
|
33
|
-
methods: [
|
|
34
|
-
allowedHeaders: [
|
|
42
|
+
methods: [...CORS_API_METHODS],
|
|
43
|
+
allowedHeaders: [...CORS_ALLOWED_HEADERS],
|
|
35
44
|
};
|
|
36
45
|
|
|
37
46
|
return cors(corsOptions);
|
|
@@ -53,8 +62,8 @@ export function createDashboardCorsMiddleware() {
|
|
|
53
62
|
*/
|
|
54
63
|
export function createTimeoutMiddleware() {
|
|
55
64
|
return (req: Request, res: Response, next: NextFunction): void => {
|
|
56
|
-
req.setTimeout(
|
|
57
|
-
res.setTimeout(
|
|
65
|
+
req.setTimeout(REQUEST_TIMEOUT);
|
|
66
|
+
res.setTimeout(RESPONSE_TIMEOUT);
|
|
58
67
|
next();
|
|
59
68
|
};
|
|
60
69
|
}
|
|
@@ -66,16 +75,16 @@ export function buildApiUrl(
|
|
|
66
75
|
host: string,
|
|
67
76
|
port: number
|
|
68
77
|
): string {
|
|
69
|
-
const apiHost = host ===
|
|
70
|
-
return
|
|
78
|
+
const apiHost = host === WILDCARD_ADDRESS ? DEFAULT_LOCALHOST : host;
|
|
79
|
+
return `${HTTP_PROTOCOL}${apiHost}:${port}`;
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
/**
|
|
74
83
|
* Build dashboard URL based on config
|
|
75
84
|
*/
|
|
76
85
|
export function buildDashboardUrl(config: MonodogConfig): string {
|
|
77
|
-
const dashboardHost = config.dashboard.host ===
|
|
78
|
-
?
|
|
86
|
+
const dashboardHost = config.dashboard.host === WILDCARD_ADDRESS
|
|
87
|
+
? DEFAULT_LOCALHOST
|
|
79
88
|
: config.dashboard.host;
|
|
80
|
-
return
|
|
89
|
+
return `${HTTP_PROTOCOL}${dashboardHost}:${config.dashboard.port}`;
|
|
81
90
|
}
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
import express from 'express';
|
|
6
6
|
import { json } from 'body-parser';
|
|
7
7
|
import type { Express } from 'express';
|
|
8
|
+
import { httpLogger, AppLogger } from './logger';
|
|
8
9
|
|
|
9
10
|
import { appConfig } from '../config-loader';
|
|
10
11
|
import {
|
|
11
12
|
errorHandler,
|
|
12
13
|
notFoundHandler,
|
|
13
|
-
requestLogger,
|
|
14
14
|
} from './error-handler';
|
|
15
15
|
import {
|
|
16
16
|
createHelmetMiddleware,
|
|
@@ -19,15 +19,23 @@ import {
|
|
|
19
19
|
buildApiUrl,
|
|
20
20
|
buildDashboardUrl,
|
|
21
21
|
} from './security';
|
|
22
|
+
import { setupSwaggerDocs } from './swagger-middleware';
|
|
22
23
|
|
|
23
24
|
import packageRouter from '../routes/package-routes';
|
|
24
25
|
import commitRouter from '../routes/commit-routes';
|
|
25
26
|
import healthRouter from '../routes/health-routes';
|
|
26
27
|
import configRouter from '../routes/config-routes';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
import {
|
|
29
|
+
PORT_MIN,
|
|
30
|
+
PORT_MAX,
|
|
31
|
+
PORT_VALIDATION_ERROR_MESSAGE,
|
|
32
|
+
BODY_PARSER_LIMIT,
|
|
33
|
+
SUCCESS_SERVER_START,
|
|
34
|
+
ERROR_PORT_IN_USE,
|
|
35
|
+
ERROR_PERMISSION_DENIED,
|
|
36
|
+
MESSAGE_GRACEFUL_SHUTDOWN,
|
|
37
|
+
MESSAGE_SERVER_CLOSED,
|
|
38
|
+
} from '../constants';
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
41
|
* Validate port number
|
|
@@ -36,7 +44,7 @@ function validatePort(port: string | number): number {
|
|
|
36
44
|
const portNum = typeof port === 'string' ? parseInt(port, 10) : port;
|
|
37
45
|
|
|
38
46
|
if (isNaN(portNum) || portNum < PORT_MIN || portNum > PORT_MAX) {
|
|
39
|
-
throw new Error(
|
|
47
|
+
throw new Error(PORT_VALIDATION_ERROR_MESSAGE(PORT_MIN, PORT_MAX));
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
return portNum;
|
|
@@ -62,10 +70,13 @@ function createApp(rootPath: string): Express {
|
|
|
62
70
|
app.use(createApiCorsMiddleware(dashboardUrl));
|
|
63
71
|
|
|
64
72
|
// Body parser
|
|
65
|
-
app.use(json({ limit:
|
|
73
|
+
app.use(json({ limit: BODY_PARSER_LIMIT }));
|
|
74
|
+
|
|
75
|
+
// HTTP request logging with Morgan
|
|
76
|
+
app.use(httpLogger);
|
|
66
77
|
|
|
67
|
-
//
|
|
68
|
-
app
|
|
78
|
+
// Setup Swagger documentation
|
|
79
|
+
setupSwaggerDocs(app);
|
|
69
80
|
|
|
70
81
|
// Routes
|
|
71
82
|
app.use('/api/packages', packageRouter);
|
|
@@ -91,50 +102,52 @@ export function startServer(rootPath: string): void {
|
|
|
91
102
|
const host = appConfig.server.host;
|
|
92
103
|
const validatedPort = validatePort(port);
|
|
93
104
|
|
|
105
|
+
AppLogger.info(`Starting Monodog API server...`);
|
|
106
|
+
AppLogger.info(`Analyzing monorepo at root: ${rootPath}`);
|
|
107
|
+
|
|
94
108
|
const app = createApp(rootPath);
|
|
95
109
|
|
|
96
110
|
const server = app.listen(validatedPort, host, () => {
|
|
97
|
-
console.log(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
console.log(SUCCESS_SERVER_START(host, validatedPort));
|
|
112
|
+
AppLogger.info('API endpoints available:', {
|
|
113
|
+
endpoints: [
|
|
114
|
+
'GET /api/health',
|
|
115
|
+
'GET /api/packages/refresh',
|
|
116
|
+
'GET /api/packages',
|
|
117
|
+
'GET /api/packages/:name',
|
|
118
|
+
'PUT /api/packages/update-config',
|
|
119
|
+
'GET /api/commits/:packagePath',
|
|
120
|
+
'GET /api/health/packages',
|
|
121
|
+
'PUT /api/config/files/:id',
|
|
122
|
+
'GET /api/config/files',
|
|
123
|
+
],
|
|
124
|
+
});
|
|
108
125
|
});
|
|
109
126
|
|
|
110
127
|
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
111
128
|
if (err.code === 'EADDRINUSE') {
|
|
112
|
-
|
|
113
|
-
`Error: Port ${validatedPort} is already in use. Please specify a different port.`
|
|
114
|
-
);
|
|
129
|
+
AppLogger.error(ERROR_PORT_IN_USE(validatedPort), err);
|
|
115
130
|
process.exit(1);
|
|
116
131
|
} else if (err.code === 'EACCES') {
|
|
117
|
-
|
|
118
|
-
`Error: Permission denied to listen on port ${validatedPort}. Use a port above 1024.`
|
|
119
|
-
);
|
|
132
|
+
AppLogger.error(ERROR_PERMISSION_DENIED(validatedPort), err);
|
|
120
133
|
process.exit(1);
|
|
121
134
|
} else {
|
|
122
|
-
|
|
135
|
+
AppLogger.error('Server failed to start:', err);
|
|
123
136
|
process.exit(1);
|
|
124
137
|
}
|
|
125
138
|
});
|
|
126
139
|
|
|
127
140
|
// Graceful shutdown
|
|
128
141
|
process.on('SIGTERM', () => {
|
|
129
|
-
|
|
142
|
+
AppLogger.info(MESSAGE_GRACEFUL_SHUTDOWN);
|
|
130
143
|
server.close(() => {
|
|
131
|
-
|
|
144
|
+
AppLogger.info(MESSAGE_SERVER_CLOSED);
|
|
132
145
|
process.exit(0);
|
|
133
146
|
});
|
|
134
147
|
});
|
|
135
148
|
} catch (error: unknown) {
|
|
136
149
|
const err = error as Error & { message?: string };
|
|
137
|
-
|
|
150
|
+
AppLogger.error('Failed to start server:', err);
|
|
138
151
|
process.exit(1);
|
|
139
152
|
}
|
|
140
153
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swagger Documentation Middleware
|
|
3
|
+
* Sets up Swagger UI for API documentation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import swaggerUi from 'swagger-ui-express';
|
|
7
|
+
import swaggerJsDoc from 'swagger-jsdoc';
|
|
8
|
+
import type { Express, Request, Response } from 'express';
|
|
9
|
+
import { swaggerOptions } from '../config/swagger-config';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Setup Swagger documentation endpoint
|
|
13
|
+
* @param app Express application instance
|
|
14
|
+
*/
|
|
15
|
+
export function setupSwaggerDocs(app: Express): void {
|
|
16
|
+
try {
|
|
17
|
+
const specs = swaggerJsDoc(swaggerOptions) as Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
// Serve raw Swagger JSON FIRST (before the middleware catches all /api-docs paths)
|
|
20
|
+
app.get('/api-docs/swagger.json', (_req: Request, res: Response) => {
|
|
21
|
+
res.setHeader('Content-Type', 'application/json');
|
|
22
|
+
res.send(specs);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Serve Swagger UI at /api-docs
|
|
26
|
+
app.use(
|
|
27
|
+
'/api-docs',
|
|
28
|
+
swaggerUi.serve,
|
|
29
|
+
swaggerUi.setup(specs, {
|
|
30
|
+
swaggerOptions: {
|
|
31
|
+
url: '/api-docs/swagger.json',
|
|
32
|
+
persistAuthorization: true,
|
|
33
|
+
displayOperationId: true,
|
|
34
|
+
filter: true,
|
|
35
|
+
showExtensions: true,
|
|
36
|
+
},
|
|
37
|
+
customCss: `
|
|
38
|
+
.swagger-ui .topbar {
|
|
39
|
+
background-color: #2c3e50;
|
|
40
|
+
}
|
|
41
|
+
.swagger-ui .info .title {
|
|
42
|
+
color: #2c3e50;
|
|
43
|
+
font-weight: bold;
|
|
44
|
+
}
|
|
45
|
+
.swagger-ui .btn-box .btn {
|
|
46
|
+
background-color: #2c3e50;
|
|
47
|
+
}
|
|
48
|
+
`,
|
|
49
|
+
customCssUrl: 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.min.css',
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
console.log('Swagger documentation available at /api-docs');
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Failed to setup Swagger documentation:', error);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/serve.ts
CHANGED
|
@@ -12,10 +12,26 @@
|
|
|
12
12
|
import { startServer, serveDashboard } from './index';
|
|
13
13
|
import { findMonorepoRoot } from './utils/utilities';
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
let logLevel = '';
|
|
16
|
+
let nodeEnv = 'production';
|
|
17
|
+
|
|
18
|
+
const args = process.argv;
|
|
19
|
+
|
|
20
|
+
if (args.includes('--dev')) {
|
|
21
|
+
nodeEnv = 'development';
|
|
22
|
+
}
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
// Priority: Check for debug first, then fall back to info
|
|
25
|
+
if (args.includes('--debug')) {
|
|
26
|
+
logLevel = 'debug';
|
|
27
|
+
} else if (args.includes('--info')) {
|
|
28
|
+
logLevel = 'info';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.env.LOG_LEVEL = logLevel;
|
|
32
|
+
process.env.NODE_ENV = nodeEnv
|
|
33
|
+
|
|
34
|
+
const rootPath = findMonorepoRoot();
|
|
19
35
|
|
|
20
36
|
// Start the Express server and dashboard
|
|
21
37
|
|