4bnode 4.0.9 → 4.1.1
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 +25 -206
- package/add-crud.js +6 -106
- package/add-docs.js +91 -0
- package/add-email.js +63 -0
- package/add-security.js +143 -0
- package/index.js +76 -8
- package/lib/codegen.js +1223 -0
- package/lib/codegen.test.js +480 -0
- package/lib/indexFile.js +10 -12
- package/lib/names.js +4 -21
- package/lib/routeFile.js +9 -27
- package/lib/sync-skeleton.js +47 -0
- package/lib/ui.js +15 -2
- package/package.json +4 -2
- package/scripts/audit-dev-surface.mjs +153 -0
- package/scripts/reveal-passkey.mjs +35 -0
- package/skeleton/.4bnode/assets/js/App.js +31 -9
- package/skeleton/.4bnode/assets/js/Root.js +1 -5
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +9 -5
- package/skeleton/.4bnode/assets/js/lib.js +18 -5
- package/skeleton/.4bnode/assets/js/pages/AiBuilderPage.js +268 -0
- package/skeleton/.4bnode/assets/js/pages/ApiKeysPage.js +164 -0
- package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +275 -9
- package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +44 -205
- package/skeleton/.4bnode/assets/js/pages/DocsPage.js +142 -0
- package/skeleton/.4bnode/assets/js/pages/MailPage.js +134 -0
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +5 -266
- package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +23 -213
- package/skeleton/.4bnode/assets/js/pages/SecurityPage.js +74 -0
- package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +61 -61
- package/skeleton/.4bnode/codegen.js +1223 -0
- package/skeleton/.4bnode/dev-api.js +634 -728
- package/skeleton/.4bnode/ui.html +62 -40
- package/skeleton/_gitignore +7 -0
- package/skeleton/index.js +41 -3
- package/skeleton/package.json +4 -1
- package/skeleton/src/middleware/apiKey.js +36 -0
- package/skeleton/src/middleware/errorHandler.js +15 -0
- package/skeleton/src/middleware/roles.js +14 -0
- package/skeleton/src/middleware/validate.js +20 -0
- package/skeleton/.4bnode/assets/images/mysql.png +0 -0
- package/skeleton/.4bnode/assets/images/postgresql.png +0 -0
- package/skeleton/.4bnode/assets/images/sqlite.png +0 -0
- package/skeleton/.4bnode/assets/js/pages/ReportsPage.js +0 -667
- package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +0 -66
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
A
|
|
12
|
+
A CLI to generate, manage, and test Node.js/Express applications.
|
|
13
13
|
|
|
14
14
|
Built by [4Brains Technologies](https://4brains.in).
|
|
15
15
|
|
|
@@ -21,226 +21,45 @@ cd my-app
|
|
|
21
21
|
npm run dev
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Your app runs at `http://localhost:3000
|
|
24
|
+
Your app runs at `http://localhost:3000`.
|
|
25
25
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
### Create a Project
|
|
26
|
+
## Commands
|
|
29
27
|
|
|
30
28
|
```sh
|
|
31
|
-
npx 4bnode
|
|
29
|
+
npx 4bnode add # interactive menu
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
Scaffolds a new Express project with:
|
|
35
|
-
- ES Module syntax
|
|
36
|
-
- Environment config (`.env.development`, `.env.production`)
|
|
37
|
-
- CORS, JSON body parser (50MB limit)
|
|
38
|
-
- Static file serving from `public/`
|
|
39
|
-
- Built-in dev dashboard at `/_dev`
|
|
40
|
-
|
|
41
|
-
### Add Features
|
|
42
|
-
|
|
43
|
-
Use the interactive menu:
|
|
44
|
-
|
|
45
|
-
```sh
|
|
46
|
-
npx 4bnode add
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Or add directly:
|
|
50
|
-
|
|
51
32
|
| Command | Description |
|
|
52
33
|
|---------|-------------|
|
|
53
|
-
| `4bnode
|
|
54
|
-
| `4bnode add
|
|
34
|
+
| `4bnode init <name>` | Scaffold a new Express project |
|
|
35
|
+
| `4bnode add api <name>` | Create an API endpoint |
|
|
36
|
+
| `4bnode add mongo` | Configure MongoDB |
|
|
55
37
|
| `4bnode add crud` | Add CRUD operations to a route |
|
|
56
|
-
| `4bnode add login` |
|
|
57
|
-
| `4bnode add
|
|
58
|
-
| `4bnode add
|
|
38
|
+
| `4bnode add login` | JWT authentication & login |
|
|
39
|
+
| `4bnode add docs` | OpenAPI docs at `/docs` |
|
|
40
|
+
| `4bnode add security` | Helmet, rate-limit, RBAC, API keys, validation |
|
|
41
|
+
| `4bnode add email` | Transactional email |
|
|
42
|
+
| `4bnode add socket` | Socket.IO |
|
|
43
|
+
| `4bnode add websocket` | Native WebSocket |
|
|
59
44
|
| `4bnode add serialport` | Serial port communication |
|
|
45
|
+
| `4bnode schema [name]` | Create / edit Mongoose schemas |
|
|
46
|
+
| `4bnode list` | Show models, routes & config |
|
|
60
47
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```sh
|
|
64
|
-
4bnode add api users
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
When creating an API endpoint:
|
|
68
|
-
- Select HTTP method (GET, POST, PUT, DELETE, PATCH)
|
|
69
|
-
- Choose fields from existing model (if available)
|
|
70
|
-
- Add custom fields received from client
|
|
71
|
-
- File fields auto-configure `multer` for multipart uploads
|
|
72
|
-
- All routes use `/api/` prefix
|
|
73
|
-
|
|
74
|
-
### Schema Management
|
|
75
|
-
|
|
76
|
-
```sh
|
|
77
|
-
4bnode schema # Interactive create/view/edit
|
|
78
|
-
4bnode schema user # Create or edit 'user' schema
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Supported field types: `String`, `Number`, `Boolean`, `Date`, `Array`, `ObjectId`, `Mixed`, `Buffer`
|
|
82
|
-
|
|
83
|
-
Field constraints: `required`, `unique`, `default`
|
|
84
|
-
|
|
85
|
-
Presets available: `user`, `product`, `post`
|
|
86
|
-
|
|
87
|
-
### List Project Info
|
|
88
|
-
|
|
89
|
-
```sh
|
|
90
|
-
4bnode list # Show all models, routes & config
|
|
91
|
-
4bnode list models # List models only
|
|
92
|
-
4bnode list routes # List routes only
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Dashboard Passkey
|
|
96
|
-
|
|
97
|
-
During `npx 4bnode init`, you'll be prompted to set a 6-digit passkey to protect the dev dashboard.
|
|
98
|
-
|
|
99
|
-
To change the passkey later, run inside your project:
|
|
100
|
-
|
|
101
|
-
```sh
|
|
102
|
-
4bnode passkey
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### Start Dev Dashboard
|
|
106
|
-
|
|
107
|
-
```sh
|
|
108
|
-
4bnode ui
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Runs `npm run dev` and opens the visual dashboard. Must be run inside a 4bnode project.
|
|
112
|
-
|
|
113
|
-
## Visual Dashboard
|
|
114
|
-
|
|
115
|
-
Access at `http://localhost:3000/_dev` when running `npm run dev`.
|
|
116
|
-
|
|
117
|
-
### Dashboard Features
|
|
118
|
-
|
|
119
|
-
**Overview** - Project status, configured features, quick stats
|
|
120
|
-
|
|
121
|
-
**Database** - MongoDB connection setup, URI management
|
|
122
|
-
|
|
123
|
-
**Schemas** - Create, view, edit MongoDB schemas with field management. Add/remove fields, toggle required/unique, set defaults.
|
|
124
|
-
|
|
125
|
-
**API Routes** - Full route management:
|
|
126
|
-
- Create new routes (Methods, CRUD, Login, Register)
|
|
127
|
-
- Add methods to existing routes with field selection
|
|
128
|
-
- Select fields from models or add custom fields
|
|
129
|
-
- File upload fields auto-configure multer
|
|
130
|
-
- View route source code, edit inline
|
|
131
|
-
- Share endpoint docs with frontend team (Markdown format)
|
|
132
|
-
- Delete routes (removes file + index.js registration)
|
|
133
|
-
|
|
134
|
-
**API Tester** - Built-in Postman-like API testing tool:
|
|
135
|
-
- REST API, Socket.IO, and WebSocket testing
|
|
136
|
-
- **Body types**: JSON, Form Data, URL Encoded, Raw, Binary
|
|
137
|
-
- **File uploads**: Choose file, send as File or Base64
|
|
138
|
-
- **Auto-detect fields**: Matches URL to route and pre-fills body/form fields from route code
|
|
139
|
-
- **Path variables**: Auto-detects `:id` params, shows editable fields
|
|
140
|
-
- **Query params**: Key-value editor, auto-fills from `req.query`
|
|
141
|
-
- **Headers**: Key-value editor with quick presets (Bearer Auth, JSON, Form Data, URL Encoded, Accept)
|
|
142
|
-
- **Response viewer**: Body + Headers tabs, copy response, save responses
|
|
143
|
-
- **Collection management**: Save requests, organize with responses, delete with confirmation
|
|
144
|
-
- **Share collection**: Generate shareable API documentation page with:
|
|
145
|
-
- Endpoint details, headers, body fields
|
|
146
|
-
- Code examples (cURL, Fetch, Axios) with copy button
|
|
147
|
-
- Form data / file upload code generation
|
|
148
|
-
- Saved response examples
|
|
149
|
-
- Search/filter endpoints
|
|
150
|
-
- Persistent URL (updates on re-share)
|
|
151
|
-
- **Import from routes**: One-click import endpoints from your project routes
|
|
152
|
-
- All data persisted in `.4bnode/` JSON files (not localStorage)
|
|
153
|
-
|
|
154
|
-
**Real-time** - Socket.IO and WebSocket configuration
|
|
155
|
-
|
|
156
|
-
**SerialPort** - Serial port setup with path and baud rate config
|
|
157
|
-
|
|
158
|
-
**Reports** - Export MongoDB data as Excel (.xlsx) reports:
|
|
159
|
-
- Select models and fields to include
|
|
160
|
-
- Include system fields (_id, createdAt, updatedAt)
|
|
161
|
-
- Populate referenced ObjectId fields with sub-field selection
|
|
162
|
-
- Custom column headers and field prefixes
|
|
163
|
-
- Drag-and-drop column reordering
|
|
164
|
-
- Configurable date format and timezone
|
|
165
|
-
- Set row limit for large collections
|
|
166
|
-
- Preview data before downloading
|
|
167
|
-
- Download as `.xlsx` file
|
|
168
|
-
|
|
169
|
-
**Logs** - Real-time server log monitoring:
|
|
170
|
-
- Live log streaming via SSE (Server-Sent Events)
|
|
171
|
-
- Filter by log type (LOG, INFO, WARN, ERROR)
|
|
172
|
-
- Search through log entries
|
|
173
|
-
- Auto-scroll with toggle
|
|
174
|
-
- Keeps last 500 log entries
|
|
175
|
-
|
|
176
|
-
**Environment** - View and edit `.env.development` and `.env.production`
|
|
177
|
-
|
|
178
|
-
## Generated Project Structure
|
|
48
|
+
## Project Structure
|
|
179
49
|
|
|
180
50
|
```
|
|
181
51
|
my-app/
|
|
182
|
-
├── index.js
|
|
183
|
-
├──
|
|
184
|
-
├── .env.
|
|
185
|
-
├──
|
|
186
|
-
├── .4bnode/ # Dashboard engine & data (managed by 4bnode)
|
|
187
|
-
├── public/ # Static files
|
|
188
|
-
│ └── index.html # App status landing page
|
|
52
|
+
├── index.js # Express entry point
|
|
53
|
+
├── .env.development
|
|
54
|
+
├── .env.production
|
|
55
|
+
├── public/ # static files
|
|
189
56
|
└── src/
|
|
190
|
-
├── db.js
|
|
191
|
-
├── models/
|
|
192
|
-
└── routes/
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Real-time Communication
|
|
196
|
-
|
|
197
|
-
### Socket.IO
|
|
198
|
-
|
|
199
|
-
```sh
|
|
200
|
-
4bnode add socket
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Access in routes via `req.io`:
|
|
204
|
-
|
|
205
|
-
```js
|
|
206
|
-
router.get('/notify', (req, res) => {
|
|
207
|
-
req.io.emit('message', 'Hello from server');
|
|
208
|
-
res.json({ sent: true });
|
|
209
|
-
});
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### WebSocket
|
|
213
|
-
|
|
214
|
-
```sh
|
|
215
|
-
4bnode add websocket
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Access in routes via `req.wss`:
|
|
219
|
-
|
|
220
|
-
```js
|
|
221
|
-
router.get('/broadcast', (req, res) => {
|
|
222
|
-
req.wss.clients.forEach(client => {
|
|
223
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
224
|
-
client.send('Hello from server');
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
res.json({ sent: true });
|
|
228
|
-
});
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Help
|
|
232
|
-
|
|
233
|
-
```sh
|
|
234
|
-
4bnode -h
|
|
235
|
-
4bnode --help
|
|
57
|
+
├── db.js # MongoDB connection
|
|
58
|
+
├── models/ # Mongoose schemas
|
|
59
|
+
└── routes/ # API routes
|
|
236
60
|
```
|
|
237
61
|
|
|
238
62
|
## License
|
|
239
63
|
|
|
240
|
-
Proprietary
|
|
241
|
-
|
|
242
|
-
(c) 2025 4Brains Technologies. All rights reserved.
|
|
243
|
-
|
|
244
|
-
Unauthorized copying, distribution, modification, or use of this code is strictly prohibited.
|
|
245
|
-
|
|
246
|
-
For licensing or commercial use: contact@4brains.in
|
|
64
|
+
Proprietary. © 2025 4Brains Technologies. All rights reserved.
|
|
65
|
+
Unauthorized copying, distribution, or use is prohibited. contact@4brains.in
|
package/add-crud.js
CHANGED
|
@@ -18,112 +18,12 @@ import {
|
|
|
18
18
|
showFileAction,
|
|
19
19
|
installDeps,
|
|
20
20
|
} from "./lib/ui.js";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let passwordLogic = "";
|
|
28
|
-
if (fields.includes("password")) {
|
|
29
|
-
passwordLogic = `
|
|
30
|
-
if (newData.password) {
|
|
31
|
-
const salt = await bcrypt.genSalt(10);
|
|
32
|
-
newData.password = await bcrypt.hash(newData.password, salt);
|
|
33
|
-
}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return `
|
|
37
|
-
router.post('/create', async (req, res) => {
|
|
38
|
-
const newData = {
|
|
39
|
-
${assignments}
|
|
40
|
-
};
|
|
41
|
-
${passwordLogic}
|
|
42
|
-
try {
|
|
43
|
-
const newDocument = new ${pascalName}(newData);
|
|
44
|
-
await newDocument.save();
|
|
45
|
-
res.json({ message: 'Data inserted successfully', data: newDocument });
|
|
46
|
-
} catch (err) {
|
|
47
|
-
console.error(err);
|
|
48
|
-
res.status(500).json({ message: 'Server error' });
|
|
49
|
-
}
|
|
50
|
-
});`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function buildReadCode(pascalName) {
|
|
54
|
-
return `
|
|
55
|
-
router.get('/', async (req, res) => {
|
|
56
|
-
try {
|
|
57
|
-
const documents = await ${pascalName}.find();
|
|
58
|
-
res.json({ data: documents });
|
|
59
|
-
} catch (err) {
|
|
60
|
-
console.error(err);
|
|
61
|
-
res.status(500).json({ message: 'Server error' });
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
router.get('/:id', async (req, res) => {
|
|
66
|
-
try {
|
|
67
|
-
const document = await ${pascalName}.findById(req.params.id);
|
|
68
|
-
if (!document) {
|
|
69
|
-
return res.status(404).json({ message: 'Document not found' });
|
|
70
|
-
}
|
|
71
|
-
res.json({ data: document });
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.error(err);
|
|
74
|
-
res.status(500).json({ message: 'Server error' });
|
|
75
|
-
}
|
|
76
|
-
});`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function buildUpdateCode(pascalName, fields, dataSource) {
|
|
80
|
-
const assignments = fields
|
|
81
|
-
.map((f) => ` ${f}: ${dataSource}.${f}`)
|
|
82
|
-
.join(",\n");
|
|
83
|
-
|
|
84
|
-
let passwordLogic = "";
|
|
85
|
-
if (fields.includes("password")) {
|
|
86
|
-
passwordLogic = `
|
|
87
|
-
if (updatedData.password) {
|
|
88
|
-
const salt = await bcrypt.genSalt(10);
|
|
89
|
-
updatedData.password = await bcrypt.hash(updatedData.password, salt);
|
|
90
|
-
}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return `
|
|
94
|
-
router.put('/:id', async (req, res) => {
|
|
95
|
-
const updatedData = {
|
|
96
|
-
${assignments}
|
|
97
|
-
};
|
|
98
|
-
${passwordLogic}
|
|
99
|
-
try {
|
|
100
|
-
const updatedDocument = await ${pascalName}.findByIdAndUpdate(req.params.id, updatedData, { returnDocument: 'after' });
|
|
101
|
-
if (!updatedDocument) {
|
|
102
|
-
return res.status(404).json({ message: 'Document not found' });
|
|
103
|
-
}
|
|
104
|
-
res.json({ message: 'Data updated successfully', data: updatedDocument });
|
|
105
|
-
} catch (err) {
|
|
106
|
-
console.error(err);
|
|
107
|
-
res.status(500).json({ message: 'Server error' });
|
|
108
|
-
}
|
|
109
|
-
});`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function buildDeleteCode(pascalName) {
|
|
113
|
-
return `
|
|
114
|
-
router.delete('/:id', async (req, res) => {
|
|
115
|
-
try {
|
|
116
|
-
const deletedDocument = await ${pascalName}.findByIdAndDelete(req.params.id);
|
|
117
|
-
if (!deletedDocument) {
|
|
118
|
-
return res.status(404).json({ message: 'Document not found' });
|
|
119
|
-
}
|
|
120
|
-
res.json({ message: 'Data deleted successfully', data: deletedDocument });
|
|
121
|
-
} catch (err) {
|
|
122
|
-
console.error(err);
|
|
123
|
-
res.status(500).json({ message: 'Server error' });
|
|
124
|
-
}
|
|
125
|
-
});`;
|
|
126
|
-
}
|
|
21
|
+
import {
|
|
22
|
+
buildInsertCode,
|
|
23
|
+
buildReadCode,
|
|
24
|
+
buildUpdateCode,
|
|
25
|
+
buildDeleteCode,
|
|
26
|
+
} from "./lib/codegen.js";
|
|
127
27
|
|
|
128
28
|
async function main() {
|
|
129
29
|
showHeader("add-crud", "Add CRUD operations to a route");
|
package/add-docs.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { getProjectRoot, listRoutes, listModels, getModelSchemaDetailed } from "./lib/project.js";
|
|
6
|
+
import { addRouteRegistration, readIndex } from "./lib/indexFile.js";
|
|
7
|
+
import { showHeader, showTaskDone, showError, showFileAction, showInfo } from "./lib/ui.js";
|
|
8
|
+
import { extractRequestFields, buildOpenApiSpec, buildDocsRouter } from "./lib/codegen.js";
|
|
9
|
+
|
|
10
|
+
function collectEndpoints() {
|
|
11
|
+
const root = getProjectRoot();
|
|
12
|
+
const routesDir = path.join(root, "src", "routes");
|
|
13
|
+
if (!fs.existsSync(routesDir)) return [];
|
|
14
|
+
const indexContent = readIndex();
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const file of listRoutes()) {
|
|
17
|
+
const name = path.basename(file, ".js");
|
|
18
|
+
const content = fs.readFileSync(path.join(routesDir, file), "utf8");
|
|
19
|
+
let prefix = "/api/" + name;
|
|
20
|
+
const reg = indexContent.match(new RegExp(`app\\.use\\(['"]([^'"]+)['"]\\s*,\\s*${name}Router`));
|
|
21
|
+
if (reg) prefix = reg[1];
|
|
22
|
+
const modelImport = content.match(/import\s+(\w+)\s+from\s+['"]\.\.\/models\/(\w+)(?:\.js)?['"]/);
|
|
23
|
+
const modelFields = modelImport ? getModelSchemaDetailed(modelImport[2]) : [];
|
|
24
|
+
const epRegex = /router\.(get|post|put|patch|delete)\(\s*['"`](\/[^'"`]*?)['"`]\s*,([\s\S]*?)(?=\nrouter\.|$)/gi;
|
|
25
|
+
let m;
|
|
26
|
+
while ((m = epRegex.exec(content)) !== null) {
|
|
27
|
+
const method = m[1].toUpperCase();
|
|
28
|
+
const epath = m[2];
|
|
29
|
+
const handler = m[3] || "";
|
|
30
|
+
const bodyFields = extractRequestFields(handler, "body").map((fn) => {
|
|
31
|
+
const meta = modelFields.find((x) => x.name === fn);
|
|
32
|
+
return { name: fn, type: meta?.type || "String", required: meta?.required || false };
|
|
33
|
+
});
|
|
34
|
+
const queryFields = extractRequestFields(handler, "query").map((fn) => ({ name: fn, type: "String" }));
|
|
35
|
+
const params = (epath.match(/:(\w+)/g) || []).map((p) => p.slice(1));
|
|
36
|
+
const fileFields = [];
|
|
37
|
+
const us = handler.match(/upload\.single\(\s*['"](\w+)['"]/);
|
|
38
|
+
if (us) fileFields.push({ name: us[1] });
|
|
39
|
+
const fullPath = (prefix + (epath === "/" ? "" : epath)).replace(/\/+/g, "/");
|
|
40
|
+
out.push({ method, path: epath, fullPath, bodyFields, queryFields, params, fileFields, hasAuth: /\bauth\b\s*,/.test(m[0]) });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function projectTitle() {
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(getProjectRoot(), "package.json"), "utf8"));
|
|
49
|
+
return (pkg.name || "API") + " API";
|
|
50
|
+
} catch { return "API"; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
showHeader("add-docs", "Generate OpenAPI docs + Swagger UI");
|
|
55
|
+
|
|
56
|
+
const root = getProjectRoot();
|
|
57
|
+
const srcDir = path.join(root, "src");
|
|
58
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
const docsPath = path.join(srcDir, "docs.js");
|
|
61
|
+
if (!fs.existsSync(docsPath)) {
|
|
62
|
+
fs.writeFileSync(docsPath, buildDocsRouter(), "utf8");
|
|
63
|
+
showFileAction("created", "src/docs.js");
|
|
64
|
+
} else {
|
|
65
|
+
showInfo("src/docs.js already exists — keeping it.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const endpoints = collectEndpoints();
|
|
69
|
+
const models = listModels().map((f) => {
|
|
70
|
+
const name = path.basename(f, ".js");
|
|
71
|
+
return { name, fields: getModelSchemaDetailed(name) };
|
|
72
|
+
});
|
|
73
|
+
const spec = buildOpenApiSpec({ title: projectTitle(), endpoints, models });
|
|
74
|
+
fs.writeFileSync(path.join(srcDir, "openapi.json"), JSON.stringify(spec, null, 2), "utf8");
|
|
75
|
+
showFileAction("created", "src/openapi.json");
|
|
76
|
+
|
|
77
|
+
addRouteRegistration("import docsRouter from './src/docs.js';", "app.use('/docs', docsRouter);");
|
|
78
|
+
showFileAction("updated", "index.js");
|
|
79
|
+
|
|
80
|
+
showTaskDone("API docs configured", [
|
|
81
|
+
`${endpoints.length} endpoint${endpoints.length === 1 ? "" : "s"} documented`,
|
|
82
|
+
"Swagger UI at /docs",
|
|
83
|
+
"Re-run after changing routes to refresh the spec",
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch((err) => {
|
|
88
|
+
if (err.name === "ExitPromptError") process.exit(0);
|
|
89
|
+
showError(err.message);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
package/add-email.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { input, select, confirm, password } from "@inquirer/prompts";
|
|
6
|
+
import { getProjectRoot } from "./lib/project.js";
|
|
7
|
+
import { updateEnvFile } from "./lib/env.js";
|
|
8
|
+
import { showHeader, showTaskDone, showError, showInfo, showFileAction, installDeps } from "./lib/ui.js";
|
|
9
|
+
import { MAIL_PROVIDERS, buildMailerService } from "./lib/codegen.js";
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
showHeader("add-email", "Set up transactional email (nodemailer)");
|
|
13
|
+
|
|
14
|
+
const providerId = await select({
|
|
15
|
+
message: "Email provider:",
|
|
16
|
+
choices: Object.entries(MAIL_PROVIDERS).map(([id, p]) => ({ name: p.label, value: id })),
|
|
17
|
+
});
|
|
18
|
+
const preset = MAIL_PROVIDERS[providerId];
|
|
19
|
+
if (preset.hint) showInfo(preset.hint);
|
|
20
|
+
|
|
21
|
+
const host = (await input({ message: "SMTP host:", default: preset.host || "" })).trim();
|
|
22
|
+
const port = (await input({ message: "SMTP port:", default: String(preset.port || 587) })).trim();
|
|
23
|
+
const secure = await confirm({ message: "Use TLS/secure (port 465)?", default: !!preset.secure });
|
|
24
|
+
const user = (await input({ message: "SMTP user:", default: preset.user || "" })).trim();
|
|
25
|
+
const pass = (await password({ message: "SMTP password / API key:", mask: "*" })).trim();
|
|
26
|
+
const from = (await input({ message: "From address:", default: user })).trim();
|
|
27
|
+
|
|
28
|
+
console.log();
|
|
29
|
+
|
|
30
|
+
const root = getProjectRoot();
|
|
31
|
+
const dir = path.join(root, "src", "services");
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
const mailerPath = path.join(dir, "mailer.js");
|
|
34
|
+
if (!fs.existsSync(mailerPath)) {
|
|
35
|
+
fs.writeFileSync(mailerPath, buildMailerService(), "utf8");
|
|
36
|
+
showFileAction("created", "src/services/mailer.js");
|
|
37
|
+
} else {
|
|
38
|
+
showInfo("src/services/mailer.js already exists — keeping it.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const envPath = path.join(root, ".env");
|
|
42
|
+
updateEnvFile(envPath, "SMTP_HOST", host);
|
|
43
|
+
updateEnvFile(envPath, "SMTP_PORT", port || "587");
|
|
44
|
+
updateEnvFile(envPath, "SMTP_SECURE", secure ? "true" : "false");
|
|
45
|
+
updateEnvFile(envPath, "SMTP_USER", user);
|
|
46
|
+
if (pass) updateEnvFile(envPath, "SMTP_PASS", pass);
|
|
47
|
+
updateEnvFile(envPath, "SMTP_FROM", from || user);
|
|
48
|
+
showFileAction("updated", ".env");
|
|
49
|
+
|
|
50
|
+
await installDeps("nodemailer");
|
|
51
|
+
|
|
52
|
+
showTaskDone("Email configured", [
|
|
53
|
+
`Provider: ${preset.label}`,
|
|
54
|
+
`Host: ${host}:${port || 587}`,
|
|
55
|
+
"Use sendMail({ to, subject, html }) from src/services/mailer.js",
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
main().catch((err) => {
|
|
60
|
+
if (err.name === "ExitPromptError") process.exit(0);
|
|
61
|
+
showError(err.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
package/add-security.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import { checkbox, confirm } from "@inquirer/prompts";
|
|
7
|
+
import { getProjectRoot, listModels, getModelSchemaDetailed } from "./lib/project.js";
|
|
8
|
+
import { updateEnvFile } from "./lib/env.js";
|
|
9
|
+
import { addImport, insertBeforeRoutes, insertBeforeListen } from "./lib/indexFile.js";
|
|
10
|
+
import {
|
|
11
|
+
showHeader,
|
|
12
|
+
showTaskDone,
|
|
13
|
+
showError,
|
|
14
|
+
showInfo,
|
|
15
|
+
showFileAction,
|
|
16
|
+
installDeps,
|
|
17
|
+
} from "./lib/ui.js";
|
|
18
|
+
import {
|
|
19
|
+
buildErrorHandlerFile,
|
|
20
|
+
buildRolesGuardFile,
|
|
21
|
+
buildApiKeyFile,
|
|
22
|
+
buildValidateFile,
|
|
23
|
+
buildZodSchemaFile,
|
|
24
|
+
securityWiring,
|
|
25
|
+
} from "./lib/codegen.js";
|
|
26
|
+
|
|
27
|
+
function writeFileSafe(relPath, content) {
|
|
28
|
+
const full = path.join(getProjectRoot(), relPath);
|
|
29
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
30
|
+
if (fs.existsSync(full)) {
|
|
31
|
+
showInfo(`${relPath} already exists — skipped.`);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
fs.writeFileSync(full, content, "utf8");
|
|
35
|
+
showFileAction("created", relPath);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
showHeader("add-security", "Harden your app with security middleware");
|
|
41
|
+
|
|
42
|
+
const features = await checkbox({
|
|
43
|
+
message: "Select protections to add:",
|
|
44
|
+
choices: [
|
|
45
|
+
{ name: "Helmet Secure HTTP response headers", value: "helmet", checked: true },
|
|
46
|
+
{ name: "Rate limiting Throttle requests per IP (express-rate-limit)", value: "rateLimit", checked: true },
|
|
47
|
+
{ name: "Error handler Central 404 + error JSON responses", value: "errorHandler", checked: true },
|
|
48
|
+
{ name: "RBAC roles roles('admin') route guard", value: "roles" },
|
|
49
|
+
{ name: "API keys x-api-key header auth", value: "apiKey" },
|
|
50
|
+
{ name: "Validation Zod request validation middleware", value: "validation" },
|
|
51
|
+
],
|
|
52
|
+
validate: (v) => (v.length > 0 ? true : "Select at least one protection."),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log();
|
|
56
|
+
|
|
57
|
+
const projectRoot = getProjectRoot();
|
|
58
|
+
const deps = new Set();
|
|
59
|
+
const summary = [];
|
|
60
|
+
|
|
61
|
+
// ── App-level wiring (helmet / rate limit / error handler) ──
|
|
62
|
+
const appLevel = features.filter((f) => ["helmet", "rateLimit", "errorHandler"].includes(f));
|
|
63
|
+
if (appLevel.length) {
|
|
64
|
+
const wiring = securityWiring(appLevel);
|
|
65
|
+
wiring.imports.forEach((imp) => addImport(imp));
|
|
66
|
+
if (wiring.beforeRoutes) insertBeforeRoutes(wiring.beforeRoutes);
|
|
67
|
+
if (wiring.beforeListen) insertBeforeListen(wiring.beforeListen);
|
|
68
|
+
showFileAction("updated", "index.js");
|
|
69
|
+
}
|
|
70
|
+
if (features.includes("helmet")) { deps.add("helmet"); summary.push("Helmet security headers"); }
|
|
71
|
+
if (features.includes("rateLimit")) { deps.add("express-rate-limit"); summary.push("Rate limiting (100 req / 15 min per IP)"); }
|
|
72
|
+
|
|
73
|
+
// ── Middleware files ──
|
|
74
|
+
if (features.includes("errorHandler")) {
|
|
75
|
+
writeFileSafe("src/middleware/errorHandler.js", buildErrorHandlerFile());
|
|
76
|
+
summary.push("Central error handler");
|
|
77
|
+
}
|
|
78
|
+
if (features.includes("roles")) {
|
|
79
|
+
writeFileSafe("src/middleware/roles.js", buildRolesGuardFile());
|
|
80
|
+
summary.push("RBAC roles() guard");
|
|
81
|
+
}
|
|
82
|
+
if (features.includes("apiKey")) {
|
|
83
|
+
writeFileSafe("src/middleware/apiKey.js", buildApiKeyFile());
|
|
84
|
+
const envPath = path.join(projectRoot, ".env");
|
|
85
|
+
const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
86
|
+
if (!/^API_KEYS=/m.test(existing)) {
|
|
87
|
+
const key = crypto.randomBytes(24).toString("hex");
|
|
88
|
+
updateEnvFile(envPath, "API_KEYS", key);
|
|
89
|
+
showFileAction("updated", ".env");
|
|
90
|
+
showInfo(`Generated API key: ${key}`);
|
|
91
|
+
}
|
|
92
|
+
summary.push("API key auth (x-api-key)");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Validation (Zod) ──
|
|
96
|
+
if (features.includes("validation")) {
|
|
97
|
+
deps.add("zod");
|
|
98
|
+
writeFileSafe("src/middleware/validate.js", buildValidateFile());
|
|
99
|
+
|
|
100
|
+
const models = listModels().map((f) => path.basename(f, ".js"));
|
|
101
|
+
let generatedValidators = 0;
|
|
102
|
+
if (models.length) {
|
|
103
|
+
const wantValidators = await confirm({
|
|
104
|
+
message: "Generate Zod validators from existing models?",
|
|
105
|
+
default: true,
|
|
106
|
+
});
|
|
107
|
+
if (wantValidators) {
|
|
108
|
+
const chosen = await checkbox({
|
|
109
|
+
message: "Select models to generate validators for:",
|
|
110
|
+
choices: models.map((m) => ({ name: m, value: m, checked: true })),
|
|
111
|
+
});
|
|
112
|
+
for (const m of chosen) {
|
|
113
|
+
const fields = getModelSchemaDetailed(m);
|
|
114
|
+
if (!fields.length) {
|
|
115
|
+
showInfo(`${m}: no fields found — skipped.`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (writeFileSafe(`src/validators/${m}.js`, buildZodSchemaFile(m, fields))) {
|
|
119
|
+
generatedValidators++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
summary.push(
|
|
125
|
+
generatedValidators > 0
|
|
126
|
+
? `Zod validation (+ ${generatedValidators} model validator${generatedValidators > 1 ? "s" : ""})`
|
|
127
|
+
: "Zod validation middleware"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (deps.size) {
|
|
132
|
+
console.log();
|
|
133
|
+
await installDeps([...deps]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
showTaskDone("Security & middleware configured", summary);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main().catch((err) => {
|
|
140
|
+
if (err.name === "ExitPromptError") process.exit(0);
|
|
141
|
+
showError(err.message);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
});
|