@seip/blue-bird 0.2.4 → 0.2.6
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/.env_example +2 -1
- package/backend/index.js +1 -2
- package/backend/routes/app.js +47 -1
- package/core/app.js +185 -8
- package/core/cache.js +36 -0
- package/core/cli/init.js +1 -1
- package/core/debug.js +249 -0
- package/core/swagger.js +25 -0
- package/core/validate.js +1 -2
- package/package.json +10 -5
package/.env_example
CHANGED
package/backend/index.js
CHANGED
package/backend/routes/app.js
CHANGED
|
@@ -5,6 +5,18 @@ import Template from "@seip/blue-bird/core/template.js"
|
|
|
5
5
|
|
|
6
6
|
const routerUsers = new Router("/")
|
|
7
7
|
|
|
8
|
+
//Example swagger docs
|
|
9
|
+
/**
|
|
10
|
+
* @swagger
|
|
11
|
+
* /users:
|
|
12
|
+
* get:
|
|
13
|
+
* summary: Get all users
|
|
14
|
+
* tags: [Users]
|
|
15
|
+
* responses:
|
|
16
|
+
* 200:
|
|
17
|
+
* description: List of users
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
8
20
|
routerUsers.get("/users", (req, res) => {
|
|
9
21
|
const users = [
|
|
10
22
|
{
|
|
@@ -25,7 +37,41 @@ const loginSchema = {
|
|
|
25
37
|
};
|
|
26
38
|
|
|
27
39
|
const loginValidator = new Validator(loginSchema, 'es');
|
|
28
|
-
|
|
40
|
+
/**
|
|
41
|
+
* @swagger
|
|
42
|
+
* /login:
|
|
43
|
+
* post:
|
|
44
|
+
* summary: Login
|
|
45
|
+
* tags: [Users]
|
|
46
|
+
* description: Login with email and password
|
|
47
|
+
* requestBody:
|
|
48
|
+
* required: true
|
|
49
|
+
* content:
|
|
50
|
+
* application/json:
|
|
51
|
+
* schema:
|
|
52
|
+
* type: object
|
|
53
|
+
* required:
|
|
54
|
+
* - email
|
|
55
|
+
* - password
|
|
56
|
+
* properties:
|
|
57
|
+
* email:
|
|
58
|
+
* type: string
|
|
59
|
+
* format: email
|
|
60
|
+
* example: example@email.com
|
|
61
|
+
* password:
|
|
62
|
+
* type: string
|
|
63
|
+
* format: password
|
|
64
|
+
* example: 123456
|
|
65
|
+
* responses:
|
|
66
|
+
* 200:
|
|
67
|
+
* description: Login success
|
|
68
|
+
*
|
|
69
|
+
* 400:
|
|
70
|
+
* description: Error
|
|
71
|
+
* 401:
|
|
72
|
+
* description: Error in request
|
|
73
|
+
*/
|
|
74
|
+
|
|
29
75
|
routerUsers.post('/login', loginValidator.middleware(), (req, res) => {
|
|
30
76
|
res.json({ message: 'Login successful' });
|
|
31
77
|
});
|
package/core/app.js
CHANGED
|
@@ -3,8 +3,13 @@ import cors from "cors"
|
|
|
3
3
|
import path from "path"
|
|
4
4
|
import chalk from "chalk"
|
|
5
5
|
import cookieParser from "cookie-parser"
|
|
6
|
+
import rateLimit from "express-rate-limit"
|
|
7
|
+
import xss from "xss"
|
|
8
|
+
import helmet from "helmet"
|
|
6
9
|
import Config from "./config.js"
|
|
7
10
|
import Logger from "./logger.js"
|
|
11
|
+
import Debug from "./debug.js"
|
|
12
|
+
import Swagger from "./swagger.js"
|
|
8
13
|
|
|
9
14
|
const __dirname = Config.dirname()
|
|
10
15
|
const props = Config.props()
|
|
@@ -27,10 +32,14 @@ class App {
|
|
|
27
32
|
* @param {boolean} [options.urlencoded=true] - Whether to enable URL-encoded body parsing.
|
|
28
33
|
* @param {Object} [options.static={path: null, options: {}}] - Static file configuration.
|
|
29
34
|
* @param {boolean} [options.cookieParser=true] - Whether to enable cookie parsing.
|
|
35
|
+
* @param {boolean|Object} [options.rateLimit=false] - Enable global rate limiting.
|
|
36
|
+
* @param {boolean|Object} [options.helmet=true] - Enable Helmet security headers.
|
|
37
|
+
* @param {boolean} [options.xssClean=true] - Enable XSS body sanitization.
|
|
38
|
+
* @param {boolean|Object} [options.swagger=true] - Enable swagger
|
|
30
39
|
* @example
|
|
31
40
|
* const app = new App({
|
|
32
41
|
* routes: [],
|
|
33
|
-
* cors: {},
|
|
42
|
+
* cors: {}, // { origin: "https://domain:port" }
|
|
34
43
|
* middlewares: [],
|
|
35
44
|
* port: 3000,
|
|
36
45
|
* host: "http://localhost",
|
|
@@ -42,7 +51,21 @@ class App {
|
|
|
42
51
|
* path: "public",
|
|
43
52
|
* options: {}
|
|
44
53
|
* },
|
|
45
|
-
*
|
|
54
|
+
* cookieParser: true,
|
|
55
|
+
* rateLimit: {
|
|
56
|
+
* windowMs: 10 * 60 * 1000,
|
|
57
|
+
* max: 50
|
|
58
|
+
* },
|
|
59
|
+
* helmet:true,
|
|
60
|
+
* xssClean:true,
|
|
61
|
+
* swagger:{
|
|
62
|
+
* info: {
|
|
63
|
+
* title: "Blue Bird API",
|
|
64
|
+
* version: "1.0.0",
|
|
65
|
+
* description: "Blue Bird Framework API Documentation"
|
|
66
|
+
* },
|
|
67
|
+
* url : "http://localhost:8000"
|
|
68
|
+
* }
|
|
46
69
|
* });
|
|
47
70
|
*/
|
|
48
71
|
constructor(options = {
|
|
@@ -60,6 +83,10 @@ class App {
|
|
|
60
83
|
options: {}
|
|
61
84
|
},
|
|
62
85
|
cookieParser: true,
|
|
86
|
+
rateLimit: false,
|
|
87
|
+
helmet: false,
|
|
88
|
+
xssClean: true,
|
|
89
|
+
swagger: true
|
|
63
90
|
|
|
64
91
|
}) {
|
|
65
92
|
this.app = express()
|
|
@@ -68,12 +95,16 @@ class App {
|
|
|
68
95
|
this.middlewares = options.middlewares || []
|
|
69
96
|
this.port = options.port || props.port
|
|
70
97
|
this.host = options.host || props.host
|
|
71
|
-
this.logger = options.logger
|
|
72
|
-
this.notFound = options.notFound
|
|
73
|
-
this.json = options.json
|
|
74
|
-
this.urlencoded = options.urlencoded
|
|
98
|
+
this.logger = options.logger ?? true
|
|
99
|
+
this.notFound = options.notFound ?? true
|
|
100
|
+
this.json = options.json ?? true
|
|
101
|
+
this.urlencoded = options.urlencoded ?? true
|
|
75
102
|
this.static = options.static || props.static
|
|
76
|
-
this.cookieParser = options.cookieParser
|
|
103
|
+
this.cookieParser = options.cookieParser ?? true
|
|
104
|
+
this.rateLimit = options.rateLimit ?? false
|
|
105
|
+
this.helmet = options.helmet ?? true
|
|
106
|
+
this.xssClean = options.xssClean ?? true
|
|
107
|
+
this.swagger = options.swagger ?? true
|
|
77
108
|
this.dispatch()
|
|
78
109
|
|
|
79
110
|
}
|
|
@@ -110,15 +141,91 @@ class App {
|
|
|
110
141
|
if (this.urlencoded) this.app.use(express.urlencoded({ extended: true }))
|
|
111
142
|
if (this.cookieParser) this.app.use(cookieParser())
|
|
112
143
|
if (this.static.path) this.app.use(express.static(path.join(__dirname, this.static.path), this.static.options))
|
|
144
|
+
|
|
113
145
|
this.app.use(cors(this.cors))
|
|
146
|
+
if (this.rateLimit) {
|
|
147
|
+
if (!this.app.get('trust proxy')) {
|
|
148
|
+
this.app.set('trust proxy', 1);
|
|
149
|
+
}
|
|
150
|
+
const defaultRateLimit = {
|
|
151
|
+
windowMs: 15 * 60 * 1000,
|
|
152
|
+
max: 100,
|
|
153
|
+
standardHeaders: true,
|
|
154
|
+
legacyHeaders: false,
|
|
155
|
+
message: {
|
|
156
|
+
success: false,
|
|
157
|
+
message: "Too many requests, please try again later."
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const optionsRateLimiter = {
|
|
161
|
+
...defaultRateLimit,
|
|
162
|
+
...(typeof this.rateLimit === "object" ? this.rateLimit : {})
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (props.debug) {
|
|
166
|
+
optionsRateLimiter.skip = (req) =>
|
|
167
|
+
req.path.startsWith("/debug");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const limiter = rateLimit(optionsRateLimiter);
|
|
171
|
+
|
|
172
|
+
this.app.use(limiter);
|
|
173
|
+
}
|
|
174
|
+
if (this.helmet) {
|
|
175
|
+
|
|
176
|
+
const defaultHelmetOptions = {
|
|
177
|
+
contentSecurityPolicy: props.debug
|
|
178
|
+
? false
|
|
179
|
+
: undefined
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const helmetOptions = {
|
|
183
|
+
...defaultHelmetOptions,
|
|
184
|
+
...(typeof this.helmet === "object" ? this.helmet : {})
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
this.app.use(helmet(helmetOptions));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.xssClean) {
|
|
191
|
+
this.app.use(this.xssMiddleware());
|
|
192
|
+
}
|
|
114
193
|
this.middlewares.map(middleware => {
|
|
115
194
|
this.app.use(middleware)
|
|
116
195
|
})
|
|
196
|
+
|
|
117
197
|
if (this.logger) this.middlewareLogger()
|
|
198
|
+
|
|
118
199
|
this.app.use((req, res, next) => {
|
|
119
200
|
res.setHeader('X-Powered-By', 'Blue Bird');
|
|
120
201
|
next();
|
|
121
202
|
});
|
|
203
|
+
|
|
204
|
+
if (props.debug) {
|
|
205
|
+
Debug.middlewareMetrics(this.app);
|
|
206
|
+
}
|
|
207
|
+
this.errorHandler();
|
|
208
|
+
|
|
209
|
+
if (this.swagger) {
|
|
210
|
+
|
|
211
|
+
const defaultSwaggerOptions = {
|
|
212
|
+
info: {
|
|
213
|
+
title: "Blue Bird API",
|
|
214
|
+
version: "1.0.0",
|
|
215
|
+
description: "Blue Bird Framework API Documentation"
|
|
216
|
+
},
|
|
217
|
+
url: `${this.host}:${this.port}`,
|
|
218
|
+
route: "/docs"
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const swaggerOptions = {
|
|
222
|
+
...defaultSwaggerOptions,
|
|
223
|
+
...(typeof this.swagger === "object" ? this.swagger : {})
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
Swagger.init(this.app, swaggerOptions);
|
|
227
|
+
}
|
|
228
|
+
|
|
122
229
|
this.dispatchRoutes()
|
|
123
230
|
|
|
124
231
|
if (this.notFound) this.notFoundDefault()
|
|
@@ -131,7 +238,7 @@ class App {
|
|
|
131
238
|
middlewareLogger() {
|
|
132
239
|
this.app.use((req, res, next) => {
|
|
133
240
|
const method = req.method
|
|
134
|
-
const url = req.url
|
|
241
|
+
const url = req.url.replace(/(password|token|authorization)=([^&]+)/gi, "$1=***")
|
|
135
242
|
const params = Object.keys(req.params).length > 0 ? ` ${JSON.stringify(req.params)}` : ""
|
|
136
243
|
const ip = req.ip
|
|
137
244
|
const now = new Date().toISOString()
|
|
@@ -146,14 +253,84 @@ class App {
|
|
|
146
253
|
next()
|
|
147
254
|
})
|
|
148
255
|
}
|
|
256
|
+
errorHandler() {
|
|
257
|
+
this.app.use((err, req, res, next) => {
|
|
258
|
+
const logger = new Logger();
|
|
259
|
+
logger.error(err.stack || err.message);
|
|
260
|
+
|
|
261
|
+
if (props.debug) {
|
|
262
|
+
return res.status(err.status || 500).json({
|
|
263
|
+
success: false,
|
|
264
|
+
message: err.message,
|
|
265
|
+
stack: err.stack
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return res.status(err.status || 500).json({
|
|
270
|
+
success: false
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
sanitizeObject(obj) {
|
|
276
|
+
if (typeof obj === "string") return xss(obj);
|
|
277
|
+
|
|
278
|
+
if (Array.isArray(obj)) {
|
|
279
|
+
return obj.map(item => this.sanitizeObject(item));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (typeof obj === "object" && obj !== null) {
|
|
283
|
+
const sanitized = {};
|
|
284
|
+
for (const key in obj) {
|
|
285
|
+
sanitized[key] = this.sanitizeObject(obj[key]);
|
|
286
|
+
}
|
|
287
|
+
return sanitized;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return obj;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
xssMiddleware() {
|
|
294
|
+
return (req, res, next) => {
|
|
295
|
+
|
|
296
|
+
if (req.body && typeof req.body === "object") {
|
|
297
|
+
this.mutateSanitized(req.body);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (req.query && typeof req.query === "object") {
|
|
301
|
+
this.mutateSanitized(req.query);
|
|
302
|
+
}
|
|
149
303
|
|
|
304
|
+
if (req.params && typeof req.params === "object") {
|
|
305
|
+
this.mutateSanitized(req.params);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
next();
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
mutateSanitized(obj) {
|
|
312
|
+
for (const key in obj) {
|
|
313
|
+
if (typeof obj[key] === "string") {
|
|
314
|
+
obj[key] = xss(obj[key]);
|
|
315
|
+
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
316
|
+
this.mutateSanitized(obj[key]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
150
320
|
/**
|
|
151
321
|
* Iterates through the stored routes and attaches them to the Express application instance.
|
|
152
322
|
*/
|
|
153
323
|
dispatchRoutes() {
|
|
324
|
+
if (props.debug) {
|
|
325
|
+
const debug = new Debug();
|
|
326
|
+
const debugRouter = debug.getRouter();
|
|
327
|
+
this.app.use(debugRouter.path, debugRouter.router);
|
|
328
|
+
}
|
|
154
329
|
this.routes.map(route => {
|
|
155
330
|
this.app.use(route.path, route.router)
|
|
156
331
|
})
|
|
332
|
+
|
|
333
|
+
|
|
157
334
|
}
|
|
158
335
|
/**
|
|
159
336
|
* Default 404 handler for unmatched routes.
|
package/core/cache.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const CACHE = {};
|
|
2
|
+
/**
|
|
3
|
+
* Cache Middleware
|
|
4
|
+
* @example
|
|
5
|
+
* router.get("/stats",
|
|
6
|
+
Cache.middleware(120),
|
|
7
|
+
controller.stats
|
|
8
|
+
);
|
|
9
|
+
* */
|
|
10
|
+
class Cache {
|
|
11
|
+
|
|
12
|
+
static middleware(seconds = 60) {
|
|
13
|
+
return (req, res, next) => {
|
|
14
|
+
|
|
15
|
+
const key = req.originalUrl;
|
|
16
|
+
|
|
17
|
+
if (CACHE[key] && CACHE[key].expiry > Date.now()) {
|
|
18
|
+
return res.json(CACHE[key].data);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const originalJson = res.json.bind(res);
|
|
22
|
+
|
|
23
|
+
res.json = (body) => {
|
|
24
|
+
CACHE[key] = {
|
|
25
|
+
data: body,
|
|
26
|
+
expiry: Date.now() + seconds * 1000
|
|
27
|
+
};
|
|
28
|
+
return originalJson(body);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
next();
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default Cache;
|
package/core/cli/init.js
CHANGED
|
@@ -110,7 +110,7 @@ const command = args[0];
|
|
|
110
110
|
|
|
111
111
|
if (command === "react") import("./react.js");
|
|
112
112
|
else if (command === "route") import("./route.js")
|
|
113
|
-
else if (command === "component") import("./
|
|
113
|
+
else if (command === "component") import("./component.js")
|
|
114
114
|
else initializer.run();
|
|
115
115
|
|
|
116
116
|
|
package/core/debug.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import Router from "./router.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Advanced Debug module for Blue Bird.
|
|
5
|
+
* Provides metrics history, route statistics and live monitoring.
|
|
6
|
+
*/
|
|
7
|
+
class Debug {
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.router = new Router("/debug");
|
|
11
|
+
this.limit = 50;
|
|
12
|
+
this.initStore();
|
|
13
|
+
this.registerRoutes();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
initStore() {
|
|
17
|
+
if (!global.__bluebird_debug_store__) {
|
|
18
|
+
global.__bluebird_debug_store__ = {
|
|
19
|
+
requests: [],
|
|
20
|
+
routes: {},
|
|
21
|
+
errors4xx: 0,
|
|
22
|
+
errors5xx: 0
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static shouldTrack(req) {
|
|
28
|
+
const url = req.originalUrl || "";
|
|
29
|
+
|
|
30
|
+
if (url.startsWith("/debug")) return false;
|
|
31
|
+
|
|
32
|
+
if (/\.(json|css|js|map|png|jpg|jpeg|gif|svg|ico|webp)$/i.test(url)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (req.method === "OPTIONS") return false;
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static middlewareMetrics(app) {
|
|
42
|
+
app.use((req, res, next) => {
|
|
43
|
+
|
|
44
|
+
if (!Debug.shouldTrack(req)) return next();
|
|
45
|
+
|
|
46
|
+
const start = process.hrtime();
|
|
47
|
+
|
|
48
|
+
res.on("finish", () => {
|
|
49
|
+
|
|
50
|
+
const diff = process.hrtime(start);
|
|
51
|
+
const responseTime = diff[0] * 1e3 + diff[1] / 1e6;
|
|
52
|
+
|
|
53
|
+
const memory = process.memoryUsage();
|
|
54
|
+
const ramUsedMB = memory.rss / 1024 / 1024;
|
|
55
|
+
|
|
56
|
+
const cpuUsage = process.cpuUsage();
|
|
57
|
+
const cpuUsedMS = (cpuUsage.user + cpuUsage.system) / 1000;
|
|
58
|
+
|
|
59
|
+
const record = {
|
|
60
|
+
method: req.method,
|
|
61
|
+
url: req.originalUrl,
|
|
62
|
+
status: res.statusCode,
|
|
63
|
+
responseTime: Number(responseTime.toFixed(2)),
|
|
64
|
+
ramUsedMB: Number(ramUsedMB.toFixed(2)),
|
|
65
|
+
cpuUsedMS: Number(cpuUsedMS.toFixed(2)),
|
|
66
|
+
date: new Date().toISOString()
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const store = global.__bluebird_debug_store__;
|
|
70
|
+
|
|
71
|
+
store.requests.unshift(record);
|
|
72
|
+
if (store.requests.length > 50) store.requests.pop();
|
|
73
|
+
|
|
74
|
+
const routeKey = `${req.method} ${req.route?.path || req.path}`;
|
|
75
|
+
|
|
76
|
+
if (!store.routes[routeKey]) {
|
|
77
|
+
store.routes[routeKey] = {
|
|
78
|
+
count: 0,
|
|
79
|
+
totalTime: 0
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
store.routes[routeKey].count += 1;
|
|
84
|
+
store.routes[routeKey].totalTime += record.responseTime;
|
|
85
|
+
|
|
86
|
+
if (record.status >= 400 && record.status < 500) store.errors4xx++;
|
|
87
|
+
if (record.status >= 500) store.errors5xx++;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
next();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
registerRoutes() {
|
|
95
|
+
|
|
96
|
+
this.router.get("/", (req, res) => {
|
|
97
|
+
|
|
98
|
+
const store = global.__bluebird_debug_store__;
|
|
99
|
+
|
|
100
|
+
if (req.query.fetch === "true") {
|
|
101
|
+
return res.json(store);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (req.query.reset === "true") {
|
|
105
|
+
global.__bluebird_debug_store__ = {
|
|
106
|
+
requests: [],
|
|
107
|
+
routes: {},
|
|
108
|
+
errors4xx: 0,
|
|
109
|
+
errors5xx: 0
|
|
110
|
+
};
|
|
111
|
+
return res.json({ ok: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
res.send(`
|
|
115
|
+
<!DOCTYPE html>
|
|
116
|
+
<html>
|
|
117
|
+
<head>
|
|
118
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
119
|
+
<title>Blue Bird Debug</title>
|
|
120
|
+
</head>
|
|
121
|
+
<body class="bg-gray-100 text-gray-800 p-10">
|
|
122
|
+
|
|
123
|
+
<div class="max-w-7xl mx-auto">
|
|
124
|
+
|
|
125
|
+
<div class="flex justify-between items-center mb-8">
|
|
126
|
+
<h1 class="text-3xl font-bold text-blue-600">Blue Bird Debug Panel</h1>
|
|
127
|
+
<button onclick="resetData()" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg shadow">
|
|
128
|
+
Reset
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div class="grid grid-cols-3 gap-6 mb-8">
|
|
133
|
+
|
|
134
|
+
<div class="bg-white p-6 rounded-xl shadow">
|
|
135
|
+
<h3 class="text-gray-500 text-sm">Total Requests</h3>
|
|
136
|
+
<p id="totalReq" class="text-2xl font-bold">0</p>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="bg-yellow-100 p-6 rounded-xl shadow">
|
|
140
|
+
<h3 class="text-yellow-600 text-sm">4xx Errors</h3>
|
|
141
|
+
<p id="err4" class="text-2xl font-bold text-yellow-700">0</p>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="bg-red-100 p-6 rounded-xl shadow">
|
|
145
|
+
<h3 class="text-red-600 text-sm">5xx Errors</h3>
|
|
146
|
+
<p id="err5" class="text-2xl font-bold text-red-700">0</p>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="grid grid-cols-2 gap-10">
|
|
152
|
+
|
|
153
|
+
<div class="bg-white p-6 rounded-xl shadow">
|
|
154
|
+
<h2 class="text-lg font-semibold mb-4">Route Stats</h2>
|
|
155
|
+
<table class="table-fixed w-full text-sm">
|
|
156
|
+
<thead>
|
|
157
|
+
<tr class="border-b">
|
|
158
|
+
<th class="text-left w-1/2 py-2">Route</th>
|
|
159
|
+
<th class="text-left w-1/4">Hits</th>
|
|
160
|
+
<th class="text-left w-1/4">Avg Time</th>
|
|
161
|
+
</tr>
|
|
162
|
+
</thead>
|
|
163
|
+
<tbody id="routesBody"></tbody>
|
|
164
|
+
</table>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="bg-white p-6 rounded-xl shadow">
|
|
168
|
+
<h2 class="text-lg font-semibold mb-4">Last Requests</h2>
|
|
169
|
+
<table class="table-fixed w-full text-sm">
|
|
170
|
+
<thead>
|
|
171
|
+
<tr class="border-b">
|
|
172
|
+
<th class="text-left w-1/2 py-2">URL</th>
|
|
173
|
+
<th class="text-left w-1/6">Method</th>
|
|
174
|
+
<th class="text-left w-1/6">Status</th>
|
|
175
|
+
<th class="text-left w-1/6">Time</th>
|
|
176
|
+
</tr>
|
|
177
|
+
</thead>
|
|
178
|
+
<tbody id="historyBody"></tbody>
|
|
179
|
+
</table>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<script>
|
|
187
|
+
async function loadData() {
|
|
188
|
+
const res = await fetch('/debug?fetch=true');
|
|
189
|
+
const data = await res.json();
|
|
190
|
+
|
|
191
|
+
document.getElementById("totalReq").innerText = data.requests.length;
|
|
192
|
+
document.getElementById("err4").innerText = data.errors4xx;
|
|
193
|
+
document.getElementById("err5").innerText = data.errors5xx;
|
|
194
|
+
|
|
195
|
+
const routesBody = document.getElementById("routesBody");
|
|
196
|
+
const historyBody = document.getElementById("historyBody");
|
|
197
|
+
|
|
198
|
+
routesBody.innerHTML = "";
|
|
199
|
+
historyBody.innerHTML = "";
|
|
200
|
+
|
|
201
|
+
Object.entries(data.routes).forEach(([key, value]) => {
|
|
202
|
+
const avg = (value.totalTime / value.count).toFixed(2);
|
|
203
|
+
let color = "";
|
|
204
|
+
if (avg > 500) color = "text-red-600 font-semibold";
|
|
205
|
+
else if (avg > 200) color = "text-yellow-600";
|
|
206
|
+
|
|
207
|
+
routesBody.innerHTML += \`
|
|
208
|
+
<tr class="border-b">
|
|
209
|
+
<td class="py-1 truncate">\${key}</td>
|
|
210
|
+
<td>\${value.count}</td>
|
|
211
|
+
<td class="\${color}">\${avg} ms</td>
|
|
212
|
+
</tr>\`;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
data.requests.forEach(r => {
|
|
216
|
+
historyBody.innerHTML += \`
|
|
217
|
+
<tr class="border-b text-xs">
|
|
218
|
+
<td class="truncate">\${r.url}</td>
|
|
219
|
+
<td>\${r.method}</td>
|
|
220
|
+
<td>\${r.status}</td>
|
|
221
|
+
<td>\${r.responseTime} ms</td>
|
|
222
|
+
</tr>\`;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function resetData() {
|
|
227
|
+
await fetch('/debug?reset=true');
|
|
228
|
+
loadData();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
loadData();
|
|
232
|
+
setInterval(loadData, 3000);
|
|
233
|
+
</script>
|
|
234
|
+
|
|
235
|
+
</body>
|
|
236
|
+
</html>
|
|
237
|
+
`);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getRouter() {
|
|
242
|
+
return {
|
|
243
|
+
path: this.router.getPath(),
|
|
244
|
+
router: this.router.getRouter()
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export default Debug;
|
package/core/swagger.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import swaggerUi from "swagger-ui-express";
|
|
2
|
+
import swaggerJSDoc from "swagger-jsdoc";
|
|
3
|
+
|
|
4
|
+
class Swagger {
|
|
5
|
+
|
|
6
|
+
static init(app, options) {
|
|
7
|
+
|
|
8
|
+
const optionsJsDoc = {
|
|
9
|
+
definition: {
|
|
10
|
+
openapi: "3.0.0",
|
|
11
|
+
info: options.info,
|
|
12
|
+
servers: [
|
|
13
|
+
{ url: options.url }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
apis: ["./backend/routes/*.js"]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const specs = swaggerJSDoc(optionsJsDoc);
|
|
20
|
+
|
|
21
|
+
app.use("/docs", swaggerUi.serve, swaggerUi.setup(specs));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default Swagger;
|
package/core/validate.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seip/blue-bird",
|
|
3
|
-
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Express + React opinionated framework with SPA or API architecture and built-in JWT auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"keywords": [
|
|
14
14
|
"express",
|
|
15
15
|
"react",
|
|
16
|
-
"framework",
|
|
16
|
+
"framework",
|
|
17
17
|
"vite"
|
|
18
18
|
],
|
|
19
19
|
"author": "Seip25",
|
|
@@ -34,9 +34,14 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"chalk": "^5.6.2",
|
|
36
36
|
"cookie-parser": "^1.4.7",
|
|
37
|
-
"cors": "^2.8.6",
|
|
37
|
+
"cors": "^2.8.6",
|
|
38
38
|
"express": "^5.2.1",
|
|
39
|
+
"express-rate-limit": "^8.2.1",
|
|
40
|
+
"helmet": "^8.1.0",
|
|
39
41
|
"jsonwebtoken": "^9.0.2",
|
|
40
|
-
"multer": "^2.0.2"
|
|
42
|
+
"multer": "^2.0.2",
|
|
43
|
+
"swagger-jsdoc": "^6.2.8",
|
|
44
|
+
"swagger-ui-express": "^5.0.1",
|
|
45
|
+
"xss": "^1.0.15"
|
|
41
46
|
}
|
|
42
|
-
}
|
|
47
|
+
}
|