bro-framework 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/bro.js +16 -2
- package/package.json +1 -1
- package/src/auth.js +4 -12
- package/src/index.d.ts +12 -1
- package/src/router.js +79 -52
- package/src/sdk.js +11 -4
- package/src/server.js +74 -19
- package/src/tasks.js +14 -4
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<a href="https://brojs.yessindevs.me">Documentation Website</a>
|
|
18
18
|
</p>
|
|
19
19
|
</p>
|
|
20
|
+
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
> "NestJS wants four decorators, three modules, and an existential crisis just to handle a GET request. Express makes you write the same 40 lines of CORS, JSON parsing, and auth middleware for every project. bro.js gives you file routing, auto-validation, JWT auth, WebSockets, and live docs out of the box. Be honest: you just want to return an object."
|
package/bin/bro.js
CHANGED
|
@@ -9,7 +9,7 @@ register();
|
|
|
9
9
|
|
|
10
10
|
import { createServer } from '../src/server.js';
|
|
11
11
|
import { colors, printBanner, printRoute, printHotReload } from '../src/logger.js';
|
|
12
|
-
import { scanTasks } from '../src/tasks.js';
|
|
12
|
+
import { scanTasks, stopTasks } from '../src/tasks.js';
|
|
13
13
|
import { generateSDK } from '../src/sdk.js';
|
|
14
14
|
import dotenv from 'dotenv';
|
|
15
15
|
import chokidar from 'chokidar';
|
|
@@ -122,7 +122,7 @@ if (command === 'init') {
|
|
|
122
122
|
|
|
123
123
|
if (['sdk', 'generate-client', 'client'].includes(command)) {
|
|
124
124
|
generateSDK().then(() => {
|
|
125
|
-
console.log(`\n ${colors.green} bro-
|
|
125
|
+
console.log(`\n ${colors.green} bro-sdk.js generated successfully!${colors.reset}\n`);
|
|
126
126
|
process.exit(0);
|
|
127
127
|
}).catch(err => {
|
|
128
128
|
console.error(`\n ${colors.red} Error generating SDK:${colors.reset}`, err.message);
|
|
@@ -182,6 +182,7 @@ async function bootstrap() {
|
|
|
182
182
|
console.error("");
|
|
183
183
|
process.exit(1);
|
|
184
184
|
}
|
|
185
|
+
globalConfig.envData = envResult.data;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
if (!fs.existsSync(routesDir)) {
|
|
@@ -236,6 +237,19 @@ async function bootstrap() {
|
|
|
236
237
|
}
|
|
237
238
|
});
|
|
238
239
|
}
|
|
240
|
+
|
|
241
|
+
const handleShutdown = async (signal) => {
|
|
242
|
+
console.log(`\n[bro.js] Received ${signal}. Shutting down gracefully...`);
|
|
243
|
+
stopTasks();
|
|
244
|
+
if (io) io.close();
|
|
245
|
+
server.close(() => {
|
|
246
|
+
console.log('[bro.js] HTTP server closed.');
|
|
247
|
+
process.exit(0);
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
252
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
239
253
|
});
|
|
240
254
|
}
|
|
241
255
|
|
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken';
|
|
2
2
|
|
|
3
|
-
let secret = 'bro_default_secret_key';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Update the secret used for signing and verifying JWTs.
|
|
7
|
-
* @param {string} newSecret
|
|
8
|
-
*/
|
|
9
|
-
export function setJwtSecret(newSecret) {
|
|
10
|
-
secret = newSecret;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
3
|
/**
|
|
14
4
|
* Signs a JWT payload.
|
|
15
5
|
* @param {Object} payload - The data to embed in the token.
|
|
6
|
+
* @param {string} secret - The JWT secret key.
|
|
16
7
|
* @param {jwt.SignOptions} [options] - jsonwebtoken sign options.
|
|
17
8
|
* @returns {string} The signed JWT token.
|
|
18
9
|
*/
|
|
19
|
-
export function signJwt(payload, options = { expiresIn: '1d' }) {
|
|
10
|
+
export function signJwt(payload, secret, options = { expiresIn: '1d' }) {
|
|
20
11
|
return jwt.sign(payload, secret, options);
|
|
21
12
|
}
|
|
22
13
|
|
|
23
14
|
/**
|
|
24
15
|
* Verifies and decodes a JWT token.
|
|
25
16
|
* @param {string} token - The JWT token to verify.
|
|
17
|
+
* @param {string} secret - The JWT secret key.
|
|
26
18
|
* @returns {{ valid: boolean, payload?: any, error?: string }}
|
|
27
19
|
*/
|
|
28
|
-
export function verifyJwt(token) {
|
|
20
|
+
export function verifyJwt(token, secret) {
|
|
29
21
|
try {
|
|
30
22
|
const payload = jwt.verify(token, secret);
|
|
31
23
|
return { valid: true, payload };
|
package/src/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { z, ZodTypeAny } from 'zod';
|
|
|
3
3
|
type InferZod<T> = T extends ZodTypeAny ? z.infer<T> : any;
|
|
4
4
|
|
|
5
5
|
export interface BroContext<Body = any, Params = any, Query = any> {
|
|
6
|
+
env?: any;
|
|
7
|
+
jwt?: { sign: (payload: any, options?: any) => string };
|
|
6
8
|
body: InferZod<Body>;
|
|
7
9
|
params: InferZod<Params>;
|
|
8
10
|
query: InferZod<Query>;
|
|
@@ -15,7 +17,8 @@ export interface BroContext<Body = any, Params = any, Query = any> {
|
|
|
15
17
|
|
|
16
18
|
export interface RouteConfig<Body = any, Params = any, Query = any> {
|
|
17
19
|
auth?: boolean;
|
|
18
|
-
upload?: boolean;
|
|
20
|
+
upload?: boolean | { limits?: any, fields?: { name: string, maxCount?: number }[], single?: string, array?: string, fileFilter?: any, storage?: any };
|
|
21
|
+
schema?: { body?: Body; params?: Params; query?: Query; };
|
|
19
22
|
body?: Body;
|
|
20
23
|
params?: Params;
|
|
21
24
|
query?: Query;
|
|
@@ -46,6 +49,14 @@ export interface BroConfig {
|
|
|
46
49
|
windowMs: number;
|
|
47
50
|
max: number;
|
|
48
51
|
};
|
|
52
|
+
upload?: {
|
|
53
|
+
limits?: {
|
|
54
|
+
fileSize?: number;
|
|
55
|
+
files?: number;
|
|
56
|
+
fields?: number;
|
|
57
|
+
[key: string]: any;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
49
60
|
db?: () => Promise<any> | any;
|
|
50
61
|
sockets?: (io: any, db: any) => Promise<void> | void;
|
|
51
62
|
}
|
package/src/router.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
|
+
import crypto from 'crypto';
|
|
4
5
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -48,10 +49,11 @@ export function parseRouteFile(filePath, routesDir) {
|
|
|
48
49
|
if (routePath === '/.') routePath = '';
|
|
49
50
|
|
|
50
51
|
if (namePart !== 'index') {
|
|
51
|
-
|
|
52
|
-
routePath += `/${formattedName}`;
|
|
52
|
+
routePath += `/${namePart}`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
routePath = routePath.replace(/\[(.*?)\]/g, ':$1');
|
|
56
|
+
|
|
55
57
|
if (routePath === '') routePath = '/';
|
|
56
58
|
|
|
57
59
|
return { routePath, method };
|
|
@@ -68,6 +70,7 @@ export function parseRouteFile(filePath, routesDir) {
|
|
|
68
70
|
export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
|
|
69
71
|
const files = scanDir(routesDir);
|
|
70
72
|
const loadedRoutes = [];
|
|
73
|
+
const routeModules = [];
|
|
71
74
|
|
|
72
75
|
for (const file of files) {
|
|
73
76
|
const routeInfo = parseRouteFile(file, routesDir);
|
|
@@ -77,68 +80,92 @@ export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
|
|
|
77
80
|
|
|
78
81
|
if (typeof app[method] !== 'function') continue;
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
const moduleUrl = pathToFileURL(file).href + '?update=' + crypto.randomUUID();
|
|
84
|
+
const module = await import(moduleUrl);
|
|
85
|
+
const config = module.default;
|
|
86
|
+
|
|
87
|
+
if (!config) continue;
|
|
88
|
+
|
|
89
|
+
routeModules.push({ file, routePath, method, config });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const { file, routePath, method, config } of routeModules) {
|
|
93
|
+
const handler = createHandler(config);
|
|
94
|
+
app[method](routePath, handler);
|
|
95
|
+
|
|
96
|
+
if (openApiSpec) {
|
|
97
|
+
const openApiPath = routePath.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
|
|
98
|
+
if (!openApiSpec.paths[openApiPath]) openApiSpec.paths[openApiPath] = {};
|
|
86
99
|
|
|
87
|
-
const
|
|
88
|
-
|
|
100
|
+
const operation = {
|
|
101
|
+
summary: config.summary || `${method.toUpperCase()} ${routePath}`,
|
|
102
|
+
responses: { '200': { description: 'Successful response' } }
|
|
103
|
+
};
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
const bodySchema = config.schema?.body || config.body;
|
|
106
|
+
const paramsSchema = config.schema?.params || config.params;
|
|
107
|
+
const querySchema = config.schema?.query || config.query;
|
|
108
|
+
|
|
109
|
+
if (bodySchema) {
|
|
110
|
+
operation.requestBody = {
|
|
111
|
+
content: { 'application/json': { schema: zodToJsonSchema(bodySchema) } }
|
|
97
112
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
operation.parameters = operation.parameters || [];
|
|
107
|
-
const pSchema = zodToJsonSchema(config.params);
|
|
108
|
-
if (pSchema.properties) {
|
|
109
|
-
for (const [key, schema] of Object.entries(pSchema.properties)) {
|
|
110
|
-
operation.parameters.push({ name: key, in: 'path', required: true, schema });
|
|
111
|
-
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (paramsSchema) {
|
|
116
|
+
operation.parameters = operation.parameters || [];
|
|
117
|
+
const pSchema = zodToJsonSchema(paramsSchema);
|
|
118
|
+
if (pSchema.properties) {
|
|
119
|
+
for (const [key, schema] of Object.entries(pSchema.properties)) {
|
|
120
|
+
operation.parameters.push({ name: key, in: 'path', required: true, schema });
|
|
112
121
|
}
|
|
113
122
|
}
|
|
123
|
+
}
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
125
|
+
if (querySchema) {
|
|
126
|
+
operation.parameters = operation.parameters || [];
|
|
127
|
+
const qSchema = zodToJsonSchema(querySchema);
|
|
128
|
+
if (qSchema.properties) {
|
|
129
|
+
for (const [key, schema] of Object.entries(qSchema.properties)) {
|
|
130
|
+
operation.parameters.push({
|
|
131
|
+
name: key,
|
|
132
|
+
in: 'query',
|
|
133
|
+
required: qSchema.required?.includes(key),
|
|
134
|
+
schema
|
|
135
|
+
});
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
|
-
|
|
130
|
-
openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
|
|
131
138
|
}
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
// Auto-inject security definition if auth is true
|
|
141
|
+
if (config.auth) {
|
|
142
|
+
operation.security = [{ bearerAuth: [] }];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (config.upload) {
|
|
146
|
+
operation.requestBody = operation.requestBody || { content: {} };
|
|
147
|
+
operation.requestBody.content['multipart/form-data'] = {
|
|
148
|
+
schema: { type: 'object' }
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (bodySchema || paramsSchema || querySchema) {
|
|
153
|
+
operation.responses['400'] = { $ref: '#/components/responses/BadRequest' };
|
|
154
|
+
}
|
|
155
|
+
if (config.auth) {
|
|
156
|
+
operation.responses['401'] = { $ref: '#/components/responses/Unauthorized' };
|
|
157
|
+
}
|
|
158
|
+
operation.responses['404'] = { $ref: '#/components/responses/NotFound' };
|
|
159
|
+
operation.responses['500'] = { $ref: '#/components/responses/ServerError' };
|
|
138
160
|
|
|
139
|
-
|
|
140
|
-
console.error(`[bro.js] Failed to load route ${file}:`, err);
|
|
161
|
+
openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
|
|
141
162
|
}
|
|
163
|
+
|
|
164
|
+
loadedRoutes.push({
|
|
165
|
+
method: method.toUpperCase(),
|
|
166
|
+
path: routePath,
|
|
167
|
+
auth: !!config.auth
|
|
168
|
+
});
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
return loadedRoutes;
|
package/src/sdk.js
CHANGED
|
@@ -44,7 +44,10 @@ async function request(method, path, data) {
|
|
|
44
44
|
options.body = JSON.stringify(data);
|
|
45
45
|
} else if (data && ['GET', 'DELETE'].includes(method.toUpperCase())) {
|
|
46
46
|
const params = new URLSearchParams(data);
|
|
47
|
-
|
|
47
|
+
const qs = params.toString();
|
|
48
|
+
if (qs) {
|
|
49
|
+
path += '?' + qs;
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
const url = CONFIG.baseURL + path;
|
|
@@ -113,12 +116,16 @@ function generateApiObject(endpoints) {
|
|
|
113
116
|
function renderTree(node, indent = ' ') {
|
|
114
117
|
let result = '';
|
|
115
118
|
|
|
119
|
+
const isValidIdentifier = (key) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key);
|
|
120
|
+
|
|
116
121
|
for (const [key, val] of Object.entries(node)) {
|
|
122
|
+
const formattedKey = isValidIdentifier(key) ? key : `"${key}"`;
|
|
123
|
+
|
|
117
124
|
if (val._isParam) {
|
|
118
|
-
result += `${indent}${
|
|
125
|
+
result += `${indent}${formattedKey}: (${key}) => ({\n`;
|
|
119
126
|
|
|
120
127
|
for (const [m, p] of Object.entries(val._methods)) {
|
|
121
|
-
const templatedPath = p.replace(/:([a-zA-Z0-9_]+)/g, '${$1}');
|
|
128
|
+
const templatedPath = p.replace(/:([a-zA-Z0-9_]+)/g, '${encodeURIComponent($1)}');
|
|
122
129
|
result += `${indent} ${m}: (data) => request('${m}', \`${templatedPath}\`, data),\n`;
|
|
123
130
|
}
|
|
124
131
|
|
|
@@ -129,7 +136,7 @@ function generateApiObject(endpoints) {
|
|
|
129
136
|
|
|
130
137
|
result += `${indent}}),\n`;
|
|
131
138
|
} else {
|
|
132
|
-
result += `${indent}${
|
|
139
|
+
result += `${indent}${formattedKey}: {\n`;
|
|
133
140
|
for (const [m, p] of Object.entries(val._methods)) {
|
|
134
141
|
result += `${indent} ${m}: (data) => request('${m}', '${p}', data),\n`;
|
|
135
142
|
}
|
package/src/server.js
CHANGED
|
@@ -5,11 +5,9 @@ import { Server } from 'socket.io';
|
|
|
5
5
|
import rateLimit from 'express-rate-limit';
|
|
6
6
|
import multer from 'multer';
|
|
7
7
|
import { apiReference } from '@scalar/express-api-reference';
|
|
8
|
-
import { verifyJwt, signJwt
|
|
8
|
+
import { verifyJwt, signJwt } from './auth.js';
|
|
9
9
|
import { loadRoutes } from './router.js';
|
|
10
10
|
|
|
11
|
-
const upload = multer();
|
|
12
|
-
|
|
13
11
|
/**
|
|
14
12
|
* Creates and configures the core Express server.
|
|
15
13
|
* @param {Object} globalConfig - User's bro.config.js configurations.
|
|
@@ -18,12 +16,19 @@ const upload = multer();
|
|
|
18
16
|
* @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, io: import('socket.io').Server }>}
|
|
19
17
|
*/
|
|
20
18
|
export async function createServer(globalConfig, routesDir, db) {
|
|
19
|
+
if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
|
|
20
|
+
throw new Error('CRITICAL SECURITY ERROR: You are running in production with a default JWT secret! Please set auth.jwtSecret in bro.config.js or via JWT_SECRET environment variable.');
|
|
21
|
+
}
|
|
22
|
+
|
|
21
23
|
const app = express();
|
|
22
24
|
const server = http.createServer(app);
|
|
23
25
|
|
|
24
26
|
const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : true;
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
if (corsConfig !== false) {
|
|
29
|
+
app.use(cors(typeof corsConfig === 'object' ? corsConfig : {}));
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
app.use(express.json());
|
|
28
33
|
|
|
29
34
|
if (globalConfig.rateLimit) {
|
|
@@ -36,24 +41,49 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
36
41
|
await globalConfig.sockets(io, db);
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
if (globalConfig.jwtSecret) {
|
|
40
|
-
setJwtSecret(globalConfig.jwtSecret);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
44
|
const createHandler = (routeConfig) => {
|
|
44
45
|
const middlewares = [];
|
|
46
|
+
const bodySchema = routeConfig.schema?.body || routeConfig.body;
|
|
47
|
+
const paramsSchema = routeConfig.schema?.params || routeConfig.params;
|
|
48
|
+
const querySchema = routeConfig.schema?.query || routeConfig.query;
|
|
45
49
|
|
|
46
50
|
if (routeConfig.rateLimit) {
|
|
47
51
|
middlewares.push(rateLimit(routeConfig.rateLimit));
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
if (routeConfig.upload) {
|
|
51
|
-
|
|
55
|
+
const routeMulterConfig = {
|
|
56
|
+
limits: globalConfig.upload?.limits || {
|
|
57
|
+
fileSize: 10 * 1024 * 1024,
|
|
58
|
+
files: 5,
|
|
59
|
+
fields: 20
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
if (typeof routeConfig.upload === 'object') {
|
|
63
|
+
if (routeConfig.upload.limits) {
|
|
64
|
+
routeMulterConfig.limits = { ...routeMulterConfig.limits, ...routeConfig.upload.limits };
|
|
65
|
+
}
|
|
66
|
+
if (routeConfig.upload.fileFilter) routeMulterConfig.fileFilter = routeConfig.upload.fileFilter;
|
|
67
|
+
if (routeConfig.upload.storage) routeMulterConfig.storage = routeConfig.upload.storage;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const uploadParser = multer(routeMulterConfig);
|
|
71
|
+
|
|
72
|
+
if (typeof routeConfig.upload === 'object' && routeConfig.upload.fields) {
|
|
73
|
+
middlewares.push(uploadParser.fields(routeConfig.upload.fields));
|
|
74
|
+
} else if (typeof routeConfig.upload === 'object' && routeConfig.upload.single) {
|
|
75
|
+
middlewares.push(uploadParser.single(routeConfig.upload.single));
|
|
76
|
+
} else if (typeof routeConfig.upload === 'object' && routeConfig.upload.array) {
|
|
77
|
+
middlewares.push(uploadParser.array(routeConfig.upload.array));
|
|
78
|
+
} else {
|
|
79
|
+
middlewares.push(uploadParser.any());
|
|
80
|
+
}
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
middlewares.push(async (req, res) => {
|
|
55
84
|
try {
|
|
56
85
|
const ctx = {
|
|
86
|
+
env: globalConfig.envData || process.env,
|
|
57
87
|
db,
|
|
58
88
|
io,
|
|
59
89
|
body: req.body,
|
|
@@ -61,7 +91,7 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
61
91
|
query: req.query,
|
|
62
92
|
files: req.files || req.file,
|
|
63
93
|
user: null,
|
|
64
|
-
jwt: { sign: signJwt },
|
|
94
|
+
jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
|
|
65
95
|
error: (status, message) => {
|
|
66
96
|
const err = new Error(message);
|
|
67
97
|
err.status = status;
|
|
@@ -76,7 +106,7 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
76
106
|
}
|
|
77
107
|
|
|
78
108
|
const token = authHeader.split(' ')[1];
|
|
79
|
-
const authResult = verifyJwt(token);
|
|
109
|
+
const authResult = verifyJwt(token, globalConfig.jwtSecret);
|
|
80
110
|
|
|
81
111
|
if (!authResult.valid) {
|
|
82
112
|
return res.status(401).json({ error: 'Unauthorized', details: authResult.error });
|
|
@@ -85,24 +115,24 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
85
115
|
ctx.user = authResult.payload;
|
|
86
116
|
}
|
|
87
117
|
|
|
88
|
-
if (
|
|
89
|
-
const result =
|
|
118
|
+
if (paramsSchema) {
|
|
119
|
+
const result = paramsSchema.safeParse(req.params);
|
|
90
120
|
if (!result.success) {
|
|
91
121
|
return res.status(400).json({ error: 'Invalid URL Parameters', details: result.error.flatten() });
|
|
92
122
|
}
|
|
93
123
|
ctx.params = result.data;
|
|
94
124
|
}
|
|
95
125
|
|
|
96
|
-
if (
|
|
97
|
-
const result =
|
|
126
|
+
if (bodySchema) {
|
|
127
|
+
const result = bodySchema.safeParse(req.body);
|
|
98
128
|
if (!result.success) {
|
|
99
129
|
return res.status(400).json({ error: 'Invalid Request Body', details: result.error.flatten() });
|
|
100
130
|
}
|
|
101
131
|
ctx.body = result.data;
|
|
102
132
|
}
|
|
103
133
|
|
|
104
|
-
if (
|
|
105
|
-
const result =
|
|
134
|
+
if (querySchema) {
|
|
135
|
+
const result = querySchema.safeParse(req.query);
|
|
106
136
|
if (!result.success) {
|
|
107
137
|
return res.status(400).json({ error: 'Invalid Query Parameters', details: result.error.flatten() });
|
|
108
138
|
}
|
|
@@ -143,6 +173,21 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
143
173
|
let openApiSpec = {
|
|
144
174
|
openapi: '3.0.0',
|
|
145
175
|
info: { title: 'bro.js API', version: '1.0.0' },
|
|
176
|
+
components: {
|
|
177
|
+
securitySchemes: {
|
|
178
|
+
bearerAuth: {
|
|
179
|
+
type: 'http',
|
|
180
|
+
scheme: 'bearer',
|
|
181
|
+
bearerFormat: 'JWT'
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
responses: {
|
|
185
|
+
BadRequest: { description: 'Bad Request', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
|
|
186
|
+
Unauthorized: { description: 'Unauthorized', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
|
|
187
|
+
NotFound: { description: 'Not Found', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
|
|
188
|
+
ServerError: { description: 'Internal Server Error', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } }
|
|
189
|
+
}
|
|
190
|
+
},
|
|
146
191
|
paths: {}
|
|
147
192
|
};
|
|
148
193
|
|
|
@@ -174,10 +219,20 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
174
219
|
routeStack(req, res, next);
|
|
175
220
|
});
|
|
176
221
|
|
|
222
|
+
app.use((req, res) => {
|
|
223
|
+
res.status(404).json({ error: 'Not Found' });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
app.use((err, req, res, next) => {
|
|
227
|
+
console.error(`[bro.js] Uncaught Error:`, err);
|
|
228
|
+
res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
|
|
229
|
+
});
|
|
230
|
+
|
|
177
231
|
const reload = async () => {
|
|
178
232
|
const newRouter = express.Router();
|
|
179
|
-
|
|
180
|
-
const routes = await loadRoutes(newRouter, routesDir, createHandler,
|
|
233
|
+
const tempSpec = { paths: {} };
|
|
234
|
+
const routes = await loadRoutes(newRouter, routesDir, createHandler, tempSpec);
|
|
235
|
+
openApiSpec.paths = tempSpec.paths;
|
|
181
236
|
routeStack = newRouter;
|
|
182
237
|
return routes;
|
|
183
238
|
};
|
package/src/tasks.js
CHANGED
|
@@ -3,29 +3,39 @@ import path from 'path';
|
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import cron from 'node-cron';
|
|
5
5
|
import { colors } from './logger.js';
|
|
6
|
+
import { scanDir } from './router.js';
|
|
7
|
+
|
|
8
|
+
let taskHandles = [];
|
|
9
|
+
|
|
10
|
+
export function stopTasks() {
|
|
11
|
+
taskHandles.forEach(t => t.stop());
|
|
12
|
+
taskHandles = [];
|
|
13
|
+
}
|
|
6
14
|
|
|
7
15
|
export async function scanTasks(ctx) {
|
|
8
16
|
const tasksDir = path.join(process.cwd(), 'tasks');
|
|
9
17
|
if (!fs.existsSync(tasksDir)) return;
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
stopTasks();
|
|
20
|
+
|
|
21
|
+
const files = scanDir(tasksDir);
|
|
12
22
|
if (files.length === 0) return;
|
|
13
23
|
|
|
14
24
|
let count = 0;
|
|
15
25
|
for (const file of files) {
|
|
16
|
-
const filePath = path.join(tasksDir, file);
|
|
17
26
|
try {
|
|
18
|
-
const moduleUrl = pathToFileURL(
|
|
27
|
+
const moduleUrl = pathToFileURL(file).href;
|
|
19
28
|
const taskModule = await import(moduleUrl);
|
|
20
29
|
|
|
21
30
|
if (taskModule.cron && typeof taskModule.handler === 'function') {
|
|
22
|
-
cron.schedule(taskModule.cron, async () => {
|
|
31
|
+
const task = cron.schedule(taskModule.cron, async () => {
|
|
23
32
|
try {
|
|
24
33
|
await taskModule.handler(ctx);
|
|
25
34
|
} catch (err) {
|
|
26
35
|
console.error(`\n ${colors.red}❌ Task Error (${file}):${colors.reset}`, err);
|
|
27
36
|
}
|
|
28
37
|
});
|
|
38
|
+
taskHandles.push(task);
|
|
29
39
|
count++;
|
|
30
40
|
}
|
|
31
41
|
} catch (err) {
|