@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 CHANGED
@@ -9,4 +9,5 @@ VERSION="1.0.0"
9
9
  LANGMETA="en"
10
10
  HOST="http://localhost"
11
11
  PORT=3001
12
- STATIC_PATH="frontend/public"
12
+ STATIC_PATH="frontend/public"
13
+ JWT_SECRET="JWT_SECRET"
package/backend/index.js CHANGED
@@ -4,8 +4,7 @@ import routerUsers from "./routes/app.js";
4
4
  const app = new App({
5
5
  routes: [routerUsers],
6
6
  cors: [],
7
- middlewares: [],
8
- port: 3000,
7
+ middlewares: [],
9
8
  host: "http://localhost"
10
9
  })
11
10
 
@@ -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
- * cookieParser: true,
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 || true
72
- this.notFound = options.notFound || true
73
- this.json = options.json || true
74
- this.urlencoded = options.urlencoded || true
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 || true
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("./route.js")
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;
@@ -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
@@ -255,8 +255,7 @@ class Validator {
255
255
  success: false,
256
256
  error: true,
257
257
  errors: errors,
258
- message: messages,
259
- html: messages.map(e => `<p class="text-red-500 text-danger">${e}</p>`)
258
+ message: messages
260
259
  };
261
260
  }
262
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seip/blue-bird",
3
- "version": "0.2.4",
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
+ }