@synchjs/ewb 1.0.1 → 1.0.2
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 +76 -196
- package/dist/Components/ServeMemoryStore.d.ts +6 -2
- package/dist/Components/ServeMemoryStore.d.ts.map +1 -1
- package/dist/Components/ServeMemoryStore.js +90 -26
- package/dist/Components/Server.d.ts +10 -3
- package/dist/Components/Server.d.ts.map +1 -1
- package/dist/Components/Server.js +170 -60
- package/dist/Components/UserHandler.d.ts +21 -0
- package/dist/Components/UserHandler.d.ts.map +1 -0
- package/dist/Components/UserHandler.js +9 -0
- package/dist/Decorations/Authorized.d.ts +2 -5
- package/dist/Decorations/Authorized.d.ts.map +1 -1
- package/dist/Decorations/Authorized.js +6 -6
- package/dist/Decorations/Security.d.ts +0 -1
- package/dist/Decorations/Security.d.ts.map +1 -1
- package/dist/Decorations/Security.js +0 -3
- package/dist/Decorations/Serve.d.ts.map +1 -1
- package/dist/Decorations/Serve.js +17 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +12 -9
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# @synchjs/ewb
|
|
2
2
|
|
|
3
|
-
A robust, decorator-based web server framework built on top of **Express** and optimized for **Bun**. It provides a structured way to build scalable APIs with built-in support for **OpenAPI (Swagger)**, **Validation**, **
|
|
3
|
+
A robust, decorator-based web server framework built on top of **Express** and optimized for **Bun**. It provides a structured way to build scalable APIs with built-in support for **OpenAPI (Swagger)**, **Validation**, **Custom User Management**, and **Frontend Asset Serving** with **HMR (Hot Module Replacement)**.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- 🏗 **Decorator-based Routing**: Define controllers and routes using concise decorators (`@Controller`, `@Get`, `@Post`).
|
|
8
|
-
-
|
|
8
|
+
- 👥 **Flexible User Management**: Pluggable `UserHandler` for custom Auth/Identity logic (JWT, Session, Database, etc.).
|
|
9
9
|
- 📜 **Auto-generated Swagger Docs**: Your API documentation is generated automatically from your code.
|
|
10
|
-
- ✅ **Request Validation**:
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- 🖥 **TUI
|
|
10
|
+
- ✅ **Request Validation**: Strict schema-based validation using AJV.
|
|
11
|
+
- 🔥 **Hot Module Replacement (HMR)**: Real-time frontend updates without full page reloads using Socket.io and hot script swapping.
|
|
12
|
+
- 🎨 **Frontend Serving**: Serve HTML/CSS/JS assets from memory, with built-in **Tailwind CSS** processing.
|
|
13
|
+
- 🐚 **Security Hardened**: Built-in protection with **Helmet**, **CORS**, **Rate Limiting**, and path traversal protection.
|
|
14
|
+
- 🖥 **Premium TUI Logs**: Clean, informative, and beautiful startup messages.
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -26,16 +26,14 @@ bun add @synchjs/ewb
|
|
|
26
26
|
Create a file `controllers/HomeController.ts`:
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
|
-
import { Controller, Get } from "ewb";
|
|
30
|
-
import type { Request, Response } from "express";
|
|
29
|
+
import { Controller, Get } from "@synchjs/ewb";
|
|
31
30
|
|
|
32
31
|
@Controller("/")
|
|
33
32
|
export class HomeController {
|
|
34
33
|
@Get("/", {
|
|
35
34
|
summary: "Welcome endpoint",
|
|
36
|
-
description: "Returns a welcome message.",
|
|
37
35
|
})
|
|
38
|
-
public index(
|
|
36
|
+
public index() {
|
|
39
37
|
return { message: "Hello from @synchjs/ewb!" };
|
|
40
38
|
}
|
|
41
39
|
}
|
|
@@ -46,246 +44,128 @@ export class HomeController {
|
|
|
46
44
|
Create `index.ts`:
|
|
47
45
|
|
|
48
46
|
```typescript
|
|
49
|
-
import { Server } from "ewb";
|
|
47
|
+
import { Server } from "@synchjs/ewb";
|
|
50
48
|
|
|
51
49
|
const server = new Server({
|
|
52
50
|
id: "main",
|
|
53
51
|
port: 3000,
|
|
54
|
-
controllersDir: "controllers",
|
|
55
|
-
enableSwagger: true, // Enable Swagger UI at /api-docs
|
|
52
|
+
controllersDir: "controllers",
|
|
56
53
|
});
|
|
57
54
|
|
|
58
|
-
server.init();
|
|
55
|
+
await server.init();
|
|
59
56
|
```
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
bun run index.ts
|
|
65
|
-
```
|
|
58
|
+
## Authentication & User Management
|
|
66
59
|
|
|
67
|
-
|
|
60
|
+
The framework uses a `UserHandler` system to give you full control over identity.
|
|
68
61
|
|
|
69
|
-
|
|
62
|
+
### 1. Define your User Handler
|
|
70
63
|
|
|
71
64
|
```typescript
|
|
72
|
-
|
|
73
|
-
id: "main",
|
|
74
|
-
port: 3000,
|
|
75
|
-
enableSwagger: true,
|
|
76
|
-
swaggerPath: "/docs", // Now available at http://localhost:3000/docs
|
|
77
|
-
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## Detailed Usage
|
|
81
|
-
|
|
82
|
-
### Controllers & Routing
|
|
83
|
-
|
|
84
|
-
Use `@Controller` to define a base path and HTTP method decorators for routes.
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
import { Controller, Get, Post, Put, Delete } from "ewb";
|
|
65
|
+
import { UserHandler } from "@synchjs/ewb";
|
|
88
66
|
import type { Request, Response } from "express";
|
|
89
67
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
68
|
+
class MyUserHandler extends UserHandler {
|
|
69
|
+
// Determine who the user is for every request
|
|
70
|
+
public async authenticate(req: Request, res: Response) {
|
|
71
|
+
const token = req.headers.authorization?.replace("Bearer ", "");
|
|
72
|
+
if (token === "admin-secret") {
|
|
73
|
+
return { id: 1, name: "Admin", roles: ["admin"] };
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
96
76
|
}
|
|
97
77
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
201: { description: "User created" },
|
|
102
|
-
},
|
|
103
|
-
})
|
|
104
|
-
public createUser(req: Request, res: Response) {
|
|
105
|
-
// Logic to create user
|
|
106
|
-
return { id: 1, ...req.body };
|
|
78
|
+
// Optional: Automatic routes for /auth/signin, signup, logout
|
|
79
|
+
public signin(req: Request, res: Response) {
|
|
80
|
+
/* ... */
|
|
107
81
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
### Authentication
|
|
112
|
-
|
|
113
|
-
Secure your endpoints using `@BearerAuth`. It integrates with JWT verification automatically.
|
|
114
|
-
|
|
115
|
-
- **Class Level:** Protects all routes in the controller.
|
|
116
|
-
- **Method Level:** Protects specific routes.
|
|
117
|
-
- **@Public():** Excludes a route from class-level auth.
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
import { Controller, Get, BearerAuth, Public } from "ewb";
|
|
121
|
-
|
|
122
|
-
@Controller("/secure")
|
|
123
|
-
@BearerAuth("my_secret_key") // Optional: Custom secret, defaults to process.env.JWT_SECRET
|
|
124
|
-
export class SecureController {
|
|
125
|
-
@Get("/dashboard")
|
|
126
|
-
public dashboard(req: Request, res: Response) {
|
|
127
|
-
// Access user data attached by middleware (req.user)
|
|
128
|
-
const user = (req as any).user;
|
|
129
|
-
return { message: "Secret data", user };
|
|
82
|
+
public signup(req: Request, res: Response) {
|
|
83
|
+
/* ... */
|
|
130
84
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
@Public() // Accessible without token
|
|
134
|
-
public status(req: Request, res: Response) {
|
|
135
|
-
return { status: "OK" };
|
|
85
|
+
public logout(req: Request, res: Response) {
|
|
86
|
+
/* ... */
|
|
136
87
|
}
|
|
137
88
|
}
|
|
138
89
|
```
|
|
139
90
|
|
|
140
|
-
|
|
91
|
+
### 2. Protect Routes
|
|
141
92
|
|
|
142
|
-
|
|
93
|
+
Use `@Authorized` to require a user, or specify roles.
|
|
143
94
|
|
|
144
95
|
```typescript
|
|
145
|
-
import { Controller, Get,
|
|
96
|
+
import { Controller, Get, Authorized } from "@synchjs/ewb";
|
|
146
97
|
|
|
147
|
-
@Controller("/
|
|
148
|
-
export class
|
|
98
|
+
@Controller("/dashboard")
|
|
99
|
+
export class DashboardController {
|
|
149
100
|
@Get("/profile")
|
|
150
|
-
@
|
|
151
|
-
public getProfile() {
|
|
152
|
-
return {
|
|
101
|
+
@Authorized() // Requires a non-null return from your UserHandler
|
|
102
|
+
public getProfile({ user }: { user: any }) {
|
|
103
|
+
return { message: `Hello ${user.name}` };
|
|
153
104
|
}
|
|
154
105
|
|
|
155
|
-
@Get("/
|
|
156
|
-
@
|
|
157
|
-
public
|
|
158
|
-
return {
|
|
106
|
+
@Get("/admin")
|
|
107
|
+
@Authorized(["admin"]) // Checks roles array from UserHandler
|
|
108
|
+
public adminOnly() {
|
|
109
|
+
return { message: "Admin area" };
|
|
159
110
|
}
|
|
160
111
|
}
|
|
161
112
|
```
|
|
162
113
|
|
|
163
|
-
|
|
114
|
+
### 3. Register the Handler
|
|
164
115
|
|
|
165
116
|
```typescript
|
|
166
|
-
const server = new Server({
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
oauth2: {
|
|
170
|
-
type: "oauth2",
|
|
171
|
-
flows: {
|
|
172
|
-
/* ... standard OpenAPI flow definition ... */
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
securityHandlers: {
|
|
177
|
-
oauth2: (req, res, next) => {
|
|
178
|
-
// Your OAuth validation logic here
|
|
179
|
-
next();
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
});
|
|
117
|
+
const server = new Server({ ... });
|
|
118
|
+
server.setUserHandler(new MyUserHandler()); // Routes /auth/* are created automatically
|
|
119
|
+
await server.init();
|
|
183
120
|
```
|
|
184
121
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
Define a JSON schema in the `@Post` (or other method) decorator to automatically validate the request body using AJV.
|
|
122
|
+
## Hot Module Replacement (HMR)
|
|
188
123
|
|
|
189
|
-
|
|
190
|
-
@Post("/register", {
|
|
191
|
-
summary: "Register new user",
|
|
192
|
-
requestBody: {
|
|
193
|
-
content: {
|
|
194
|
-
"application/json": {
|
|
195
|
-
schema: {
|
|
196
|
-
type: "object",
|
|
197
|
-
properties: {
|
|
198
|
-
username: { type: "string" },
|
|
199
|
-
email: { type: "string", format: "email" },
|
|
200
|
-
age: { type: "integer", minimum: 18 }
|
|
201
|
-
},
|
|
202
|
-
required: ["username", "email"]
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
public register(req: Request, res: Response) {
|
|
209
|
-
// If execution reaches here, req.body is valid
|
|
210
|
-
return { success: true };
|
|
211
|
-
}
|
|
212
|
-
```
|
|
124
|
+
HMR is active by default when `NODE_ENV` is not set to `production`. It uses Socket.io to sync changes.
|
|
213
125
|
|
|
214
|
-
|
|
126
|
+
- **Fast Refresh**: Specifically optimized for React applications.
|
|
127
|
+
- **Hot Script Swapping**: Updates code in the browser without losing state or full page reloads.
|
|
128
|
+
- **Cache Control**: Automatically disables browser caching during development.
|
|
215
129
|
|
|
216
|
-
|
|
130
|
+
## Serving Frontend Assets
|
|
217
131
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```
|
|
221
|
-
/views
|
|
222
|
-
src/
|
|
223
|
-
index.html
|
|
224
|
-
index.css (imports tailwind, or handled via plugin)
|
|
225
|
-
frontend.tsx
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
2. **Controller Setup**:
|
|
132
|
+
Use `@Serve` to bundle and serve frontend entry points.
|
|
229
133
|
|
|
230
134
|
```typescript
|
|
231
|
-
import { Controller, Get, Serve, Tailwindcss } from "ewb";
|
|
135
|
+
import { Controller, Get, Serve, Tailwindcss } from "@synchjs/ewb";
|
|
232
136
|
|
|
233
137
|
@Controller("/")
|
|
234
|
-
@Tailwindcss({
|
|
235
|
-
enable: true,
|
|
236
|
-
plugins: [
|
|
237
|
-
/* Bun plugins or custom PostCSS wrappers */
|
|
238
|
-
],
|
|
239
|
-
})
|
|
138
|
+
@Tailwindcss({ enable: true })
|
|
240
139
|
export class FrontendController {
|
|
241
140
|
@Get("/")
|
|
242
|
-
@Serve("views/src/index.html")
|
|
243
|
-
public app(
|
|
244
|
-
//
|
|
141
|
+
@Serve("views/src/index.html")
|
|
142
|
+
public app() {
|
|
143
|
+
// Handled by memory store
|
|
245
144
|
}
|
|
246
145
|
}
|
|
247
146
|
```
|
|
248
147
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Apply custom Express middleware to controllers or routes using `@Middleware`.
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
import { Controller, Get, Middleware } from "ewb";
|
|
255
|
-
|
|
256
|
-
const logAccess = (req: Request, res: Response, next: NextFunction) => {
|
|
257
|
-
console.log("Accessed!");
|
|
258
|
-
next();
|
|
259
|
-
};
|
|
148
|
+
## Security & Best Practices
|
|
260
149
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
public index(req: Request, res: Response) {
|
|
266
|
-
return { message: "Audited" };
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
```
|
|
150
|
+
- **Strict Validation**: AJV validates all request bodies defined in Swagger options. It automatically removes additional properties and enforces strict types.
|
|
151
|
+
- **Rate Limiting**: Protects your API from brute-force/DoS. (Static assets are automatically exempt).
|
|
152
|
+
- **Security Headers**: Powered by Helmet, with dynamic CSP adjustments for HMR.
|
|
153
|
+
- **Error Handling**: Production mode masks internal errors and stack traces.
|
|
270
154
|
|
|
271
155
|
## Server Configuration
|
|
272
156
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
|
276
|
-
|
|
|
277
|
-
| `
|
|
278
|
-
| `
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
283
|
-
| `
|
|
284
|
-
| `
|
|
285
|
-
| `rateLimitOptions` | `RateLimitOptions` | Configuration for rate limiting. |
|
|
286
|
-
| `securitySchemes` | `object` | custom OpenAPI security schemes definitions. |
|
|
287
|
-
| `securityHandlers` | `object` | Middleware handlers for security schemes. |
|
|
288
|
-
| `container` | `object` | IoC container for dependency injection (must have `get` method). |
|
|
157
|
+
| Option | Type | Description |
|
|
158
|
+
| ------------------ | ---------- | ------------------------------------------------------ |
|
|
159
|
+
| `id` | `string` | ID for logging and console output. |
|
|
160
|
+
| `port` | `number` | Port to listen on. |
|
|
161
|
+
| `controllersDir` | `string` | Location of your controllers (default: `controllers`). |
|
|
162
|
+
| `viewsDir` | `string` | Base directory for frontend views. |
|
|
163
|
+
| `enableSwagger` | `boolean` | Enable Swagger UI (default: `false`). |
|
|
164
|
+
| `swaggerPath` | `string` | Path for docs (default: `/api-docs`). |
|
|
165
|
+
| `helmetOptions` | `object` | Custom Helmet configuration. |
|
|
166
|
+
| `corsOptions` | `object` | Custom CORS configuration. |
|
|
167
|
+
| `rateLimitOptions` | `object` | Custom Rate Limit configuration. |
|
|
168
|
+
| `roleHandler` | `function` | Custom function for advanced role checking logic. |
|
|
289
169
|
|
|
290
170
|
## License
|
|
291
171
|
|
|
@@ -8,11 +8,15 @@ export declare class ServeMemoryStore {
|
|
|
8
8
|
private _watchers;
|
|
9
9
|
private _listeners;
|
|
10
10
|
private constructor();
|
|
11
|
+
private ensureCacheDir;
|
|
12
|
+
clearCache(): void;
|
|
11
13
|
static get instance(): ServeMemoryStore;
|
|
12
14
|
setDevMode(enabled: boolean): void;
|
|
13
|
-
onRebuild(listener: (
|
|
15
|
+
onRebuild(listener: (data?: {
|
|
16
|
+
html: string;
|
|
17
|
+
}) => void): void;
|
|
14
18
|
private notify;
|
|
15
|
-
getAsset(
|
|
19
|
+
getAsset(rawPath: string): {
|
|
16
20
|
type: string;
|
|
17
21
|
content: Uint8Array;
|
|
18
22
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServeMemoryStore.d.ts","sourceRoot":"","sources":["../../src/Components/ServeMemoryStore.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"ServeMemoryStore.d.ts","sourceRoot":"","sources":["../../src/Components/ServeMemoryStore.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAK/D,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAC,SAAS,CAAmB;IAC3C,OAAO,CAAC,OAAO,CACH;IACZ,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,SAAS,CAA0C;IAC3D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAwC;IACzD,OAAO,CAAC,UAAU,CAA6C;IAE/D,OAAO;IAIP,OAAO,CAAC,cAAc;IAUf,UAAU;IAgBjB,WAAkB,QAAQ,IAAI,gBAAgB,CAK7C;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO;IAI3B,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI;IAI5D,OAAO,CAAC,MAAM;IAIP,QAAQ,CAAC,OAAO,EAAE,MAAM;cAzDM,MAAM;iBAAW,UAAU;;IAgEhE,OAAO,CAAC,WAAW;YAQL,YAAY;IAyB1B,OAAO,CAAC,YAAY;IA4CP,aAAa,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,eAAgD,GACxD,OAAO,CAAC,MAAM,CAAC;CA4JnB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import tailwindPlugin from "bun-plugin-tailwind";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { load } from "cheerio";
|
|
4
5
|
export class ServeMemoryStore {
|
|
5
6
|
static _instance;
|
|
6
7
|
_assets = new Map();
|
|
@@ -10,6 +11,9 @@ export class ServeMemoryStore {
|
|
|
10
11
|
_watchers = new Map();
|
|
11
12
|
_listeners = [];
|
|
12
13
|
constructor() {
|
|
14
|
+
this.ensureCacheDir();
|
|
15
|
+
}
|
|
16
|
+
ensureCacheDir() {
|
|
13
17
|
if (!fs.existsSync(this._cacheDir)) {
|
|
14
18
|
try {
|
|
15
19
|
fs.mkdirSync(this._cacheDir, { recursive: true });
|
|
@@ -19,6 +23,22 @@ export class ServeMemoryStore {
|
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
25
|
}
|
|
26
|
+
clearCache() {
|
|
27
|
+
if (fs.existsSync(this._cacheDir)) {
|
|
28
|
+
try {
|
|
29
|
+
const files = fs.readdirSync(this._cacheDir);
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
fs.unlinkSync(path.join(this._cacheDir, file));
|
|
32
|
+
}
|
|
33
|
+
// Silent clear
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
console.error("[Cache] Error clearing cache:", e);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this._htmlCache.clear();
|
|
40
|
+
this._assets.clear();
|
|
41
|
+
}
|
|
22
42
|
static get instance() {
|
|
23
43
|
if (!ServeMemoryStore._instance) {
|
|
24
44
|
ServeMemoryStore._instance = new ServeMemoryStore();
|
|
@@ -31,11 +51,15 @@ export class ServeMemoryStore {
|
|
|
31
51
|
onRebuild(listener) {
|
|
32
52
|
this._listeners.push(listener);
|
|
33
53
|
}
|
|
34
|
-
notify() {
|
|
35
|
-
this._listeners.forEach((l) => l());
|
|
54
|
+
notify(data) {
|
|
55
|
+
this._listeners.forEach((l) => l(data));
|
|
36
56
|
}
|
|
37
|
-
getAsset(
|
|
38
|
-
|
|
57
|
+
getAsset(rawPath) {
|
|
58
|
+
// Basic path traversal protection
|
|
59
|
+
const normalizedPath = path.posix.normalize(rawPath);
|
|
60
|
+
if (normalizedPath.includes(".."))
|
|
61
|
+
return undefined;
|
|
62
|
+
return this._assets.get(normalizedPath);
|
|
39
63
|
}
|
|
40
64
|
getCacheKey(htmlPath, options) {
|
|
41
65
|
return (htmlPath +
|
|
@@ -43,6 +67,8 @@ export class ServeMemoryStore {
|
|
|
43
67
|
(options.plugins ? ":" + options.plugins.length : ""));
|
|
44
68
|
}
|
|
45
69
|
async loadFromDisk(cacheKey) {
|
|
70
|
+
if (this._devMode)
|
|
71
|
+
return null; // Skip disk cache in dev mode
|
|
46
72
|
const hash = Bun.hash(cacheKey).toString(16);
|
|
47
73
|
const cacheFile = path.join(this._cacheDir, `${hash}.json`);
|
|
48
74
|
if (fs.existsSync(cacheFile)) {
|
|
@@ -77,7 +103,7 @@ export class ServeMemoryStore {
|
|
|
77
103
|
filename.includes(".git") ||
|
|
78
104
|
filename.includes(".ebw-cache"))
|
|
79
105
|
return;
|
|
80
|
-
|
|
106
|
+
// Silent detect
|
|
81
107
|
// Clear cache for this entry
|
|
82
108
|
this._htmlCache.delete(cacheKey);
|
|
83
109
|
// Clear disk cache by deleting file
|
|
@@ -87,8 +113,8 @@ export class ServeMemoryStore {
|
|
|
87
113
|
fs.unlinkSync(cacheFile);
|
|
88
114
|
}
|
|
89
115
|
try {
|
|
90
|
-
await this.buildAndCache(htmlPath, options);
|
|
91
|
-
this.notify();
|
|
116
|
+
const newHtml = await this.buildAndCache(htmlPath, options);
|
|
117
|
+
this.notify({ html: newHtml });
|
|
92
118
|
}
|
|
93
119
|
catch (e) {
|
|
94
120
|
console.error("[HMR] Rebuild failed:", e);
|
|
@@ -99,11 +125,10 @@ export class ServeMemoryStore {
|
|
|
99
125
|
async buildAndCache(htmlPath, options = { enable: false, plugins: [] }) {
|
|
100
126
|
const cacheKey = this.getCacheKey(htmlPath, options);
|
|
101
127
|
// 1. Check Memory Cache
|
|
102
|
-
if (this._htmlCache.has(cacheKey)) {
|
|
128
|
+
if (!this._devMode && this._htmlCache.has(cacheKey)) {
|
|
103
129
|
return this._htmlCache.get(cacheKey);
|
|
104
130
|
}
|
|
105
|
-
// 2. Check Disk Cache
|
|
106
|
-
// but usually disk cache is fine as watcher will invalidate it)
|
|
131
|
+
// 2. Check Disk Cache
|
|
107
132
|
const diskHtml = await this.loadFromDisk(cacheKey);
|
|
108
133
|
if (diskHtml) {
|
|
109
134
|
if (this._devMode) {
|
|
@@ -154,37 +179,76 @@ export class ServeMemoryStore {
|
|
|
154
179
|
// Inject HMR/Reload script in dev mode
|
|
155
180
|
if (this._devMode) {
|
|
156
181
|
const reloadScript = `
|
|
182
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
157
183
|
<script id="ebw-hmr-script">
|
|
158
184
|
(function() {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
185
|
+
const socket = io(window.location.origin);
|
|
186
|
+
|
|
187
|
+
socket.on('rebuild', (data) => {
|
|
188
|
+
if (!data || !data.html) return;
|
|
189
|
+
console.log('[HMR] Rebuild detected. Hot swapping scripts...');
|
|
190
|
+
|
|
191
|
+
const parser = new DOMParser();
|
|
192
|
+
const newDoc = parser.parseFromString(data.html, 'text/html');
|
|
193
|
+
const newScripts = Array.from(newDoc.querySelectorAll('script[src]'))
|
|
194
|
+
.filter(s => !s.id && s.getAttribute('src').includes('-')); // Find bundled scripts
|
|
195
|
+
|
|
196
|
+
if (newScripts.length === 0) {
|
|
197
|
+
console.warn('[HMR] No bundled scripts found in rebuild. Falling back to reload.');
|
|
163
198
|
location.reload();
|
|
199
|
+
return;
|
|
164
200
|
}
|
|
165
|
-
|
|
166
|
-
|
|
201
|
+
|
|
202
|
+
// Remove old bundled scripts
|
|
203
|
+
const oldScripts = Array.from(document.querySelectorAll('script[src]'))
|
|
204
|
+
.filter(s => !s.id && s.getAttribute('src').includes('-'));
|
|
205
|
+
|
|
206
|
+
oldScripts.forEach(s => s.remove());
|
|
207
|
+
|
|
208
|
+
// Add new scripts
|
|
209
|
+
newScripts.forEach(s => {
|
|
210
|
+
const script = document.createElement('script');
|
|
211
|
+
Array.from(s.attributes).forEach(attr => script.setAttribute(attr.name, attr.value));
|
|
212
|
+
document.body.appendChild(script);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
console.log('[HMR] Hot swap complete!');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
socket.on('reload', () => {
|
|
219
|
+
console.log('[HMR] Hard reload signal received.');
|
|
220
|
+
location.reload();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
socket.on('disconnect', () => {
|
|
167
224
|
console.warn('[HMR] Connection lost. Attempting to reconnect...');
|
|
168
|
-
};
|
|
225
|
+
});
|
|
169
226
|
})();
|
|
170
227
|
</script>
|
|
171
228
|
`;
|
|
172
|
-
|
|
173
|
-
|
|
229
|
+
// In dev mode, we might want to prevent caching by appending a timestamp to asset links in the HTML
|
|
230
|
+
const timestamp = Date.now();
|
|
231
|
+
htmlContent = htmlContent.replace(/(\.js|\.css)(\?.*)?/g, `$1?t=${timestamp}`);
|
|
232
|
+
const $ = load(htmlContent);
|
|
233
|
+
if ($("body").length > 0) {
|
|
234
|
+
$("body").append(reloadScript);
|
|
174
235
|
}
|
|
175
236
|
else {
|
|
176
|
-
|
|
237
|
+
$.root().append(reloadScript);
|
|
177
238
|
}
|
|
239
|
+
htmlContent = $.html();
|
|
178
240
|
this.setupWatcher(htmlPath, options);
|
|
179
241
|
}
|
|
180
242
|
// 4. Save to Memory and Disk
|
|
181
243
|
this._htmlCache.set(cacheKey, htmlContent);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
244
|
+
if (!this._devMode) {
|
|
245
|
+
const hash = Bun.hash(cacheKey).toString(16);
|
|
246
|
+
const cacheFile = path.join(this._cacheDir, `${hash}.json`);
|
|
247
|
+
fs.writeFileSync(cacheFile, JSON.stringify({
|
|
248
|
+
html: htmlContent,
|
|
249
|
+
assets: currentBuildAssets,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
188
252
|
return htmlContent;
|
|
189
253
|
}
|
|
190
254
|
catch (error) {
|
|
@@ -2,10 +2,12 @@ import { type Request, type Response, type NextFunction } from "express";
|
|
|
2
2
|
import { type HelmetOptions } from "helmet";
|
|
3
3
|
import cors from "cors";
|
|
4
4
|
import { type Options as RateLimitOptions } from "express-rate-limit";
|
|
5
|
+
import { UserHandler } from "./UserHandler";
|
|
5
6
|
export declare class Server {
|
|
6
7
|
private readonly _app;
|
|
7
8
|
private readonly _port;
|
|
8
9
|
private readonly _controllersDir;
|
|
10
|
+
private readonly _viewsDir?;
|
|
9
11
|
readonly _id: string;
|
|
10
12
|
private readonly _enableSwagger;
|
|
11
13
|
private readonly _swaggerPath;
|
|
@@ -14,20 +16,24 @@ export declare class Server {
|
|
|
14
16
|
private readonly _ajv;
|
|
15
17
|
private readonly _securityHandlers;
|
|
16
18
|
private readonly _container?;
|
|
19
|
+
private readonly _roleHandler?;
|
|
20
|
+
private _userHandler?;
|
|
17
21
|
private _serverInstance?;
|
|
22
|
+
private _io?;
|
|
18
23
|
private _controllerCount;
|
|
19
24
|
private _routeCount;
|
|
20
25
|
private _tailwindEnabled;
|
|
21
26
|
private _devMode;
|
|
22
|
-
private _sseClients;
|
|
23
27
|
constructor(options: ServerOptions);
|
|
24
28
|
private setupHmr;
|
|
25
29
|
private log;
|
|
26
30
|
init(): Promise<void>;
|
|
27
31
|
private printStartupMessage;
|
|
28
32
|
close(): void;
|
|
33
|
+
setUserHandler(handler: UserHandler): void;
|
|
34
|
+
private setupAuthRoutes;
|
|
29
35
|
private loadControllers;
|
|
30
|
-
private
|
|
36
|
+
private createRoleMiddleware;
|
|
31
37
|
private createAuthMiddleware;
|
|
32
38
|
private createValidationMiddleware;
|
|
33
39
|
private setupSwagger;
|
|
@@ -39,7 +45,7 @@ export interface ServerOptions {
|
|
|
39
45
|
logging?: boolean;
|
|
40
46
|
id: string;
|
|
41
47
|
controllersDir?: string;
|
|
42
|
-
|
|
48
|
+
viewsDir?: string;
|
|
43
49
|
corsOptions?: cors.CorsOptions;
|
|
44
50
|
helmetOptions?: HelmetOptions;
|
|
45
51
|
rateLimitOptions?: Partial<RateLimitOptions>;
|
|
@@ -48,5 +54,6 @@ export interface ServerOptions {
|
|
|
48
54
|
container?: {
|
|
49
55
|
get: (target: any) => any;
|
|
50
56
|
};
|
|
57
|
+
roleHandler?: (req: Request, roles: string[]) => boolean | Promise<boolean>;
|
|
51
58
|
}
|
|
52
59
|
//# sourceMappingURL=Server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Server.d.ts","sourceRoot":"","sources":["../../src/Components/Server.ts"],"names":[],"mappings":"AAAA,OAAgB,EAEd,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,YAAY,EAClB,MAAM,SAAS,CAAC;AAKjB,OAAe,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAkB,EAChB,KAAK,OAAO,IAAI,gBAAgB,EACjC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"Server.d.ts","sourceRoot":"","sources":["../../src/Components/Server.ts"],"names":[],"mappings":"AAAA,OAAgB,EAEd,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,YAAY,EAClB,MAAM,SAAS,CAAC;AAKjB,OAAe,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAkB,EAChB,KAAK,OAAO,IAAI,gBAAgB,EACjC,MAAM,oBAAoB,CAAC;AAM5B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAkB5C,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAU;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAS;IACpC,SAAgB,GAAG,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAM;IACxC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAM;IAC3B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAGhC;IACF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAgC;IAC5D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAGE;IAChC,OAAO,CAAC,YAAY,CAAC,CAAc;IACnC,OAAO,CAAC,eAAe,CAAC,CAAa;IACrC,OAAO,CAAC,GAAG,CAAC,CAAe;IAE3B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAS;gBAEb,OAAO,EAAE,aAAa;IAgElC,OAAO,CAAC,QAAQ;IAchB,OAAO,CAAC,GAAG;IAME,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAqElC,OAAO,CAAC,mBAAmB;IA0CpB,KAAK,IAAI,IAAI;IASb,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAIjD,OAAO,CAAC,eAAe;YA+BT,eAAe;IAuQ7B,OAAO,CAAC,oBAAoB;IAkD5B,OAAO,CAAC,oBAAoB;IAyD5B,OAAO,CAAC,0BAA0B;IAclC,OAAO,CAAC,YAAY;CAmErB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC;IAC/B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC7C,gBAAgB,CAAC,EAAE,MAAM,CACvB,MAAM,EACN,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAC1D,CAAC;IACF,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACtC,SAAS,CAAC,EAAE;QAAE,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,GAAG,CAAA;KAAE,CAAC;IAC1C,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7E"}
|
|
@@ -9,8 +9,9 @@ import rateLimit from "express-rate-limit";
|
|
|
9
9
|
import Ajv from "ajv";
|
|
10
10
|
import addFormats from "ajv-formats";
|
|
11
11
|
import { MetadataStorage } from "../Decorations";
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
12
|
+
import { createServer } from "http";
|
|
13
|
+
import { Server as SocketServer } from "socket.io";
|
|
14
|
+
import { PUBLIC_METADATA_KEY, ROLES_METADATA_KEY, } from "../Decorations/Authorized";
|
|
14
15
|
import { SECURITY_METADATA_KEY, } from "../Decorations/Security";
|
|
15
16
|
import { TAILWIND_METADATA_KEY, } from "../Decorations/Tailwind";
|
|
16
17
|
import { SERVE_HTML_METADATA_KEY } from "../Decorations/Serve";
|
|
@@ -21,6 +22,7 @@ export class Server {
|
|
|
21
22
|
_app;
|
|
22
23
|
_port;
|
|
23
24
|
_controllersDir;
|
|
25
|
+
_viewsDir;
|
|
24
26
|
_id;
|
|
25
27
|
_enableSwagger;
|
|
26
28
|
_swaggerPath;
|
|
@@ -29,13 +31,15 @@ export class Server {
|
|
|
29
31
|
_ajv;
|
|
30
32
|
_securityHandlers;
|
|
31
33
|
_container;
|
|
34
|
+
_roleHandler;
|
|
35
|
+
_userHandler;
|
|
32
36
|
_serverInstance;
|
|
37
|
+
_io;
|
|
33
38
|
// Stats
|
|
34
39
|
_controllerCount = 0;
|
|
35
40
|
_routeCount = 0;
|
|
36
41
|
_tailwindEnabled = false;
|
|
37
42
|
_devMode = false;
|
|
38
|
-
_sseClients = [];
|
|
39
43
|
constructor(options) {
|
|
40
44
|
this._port = options.port;
|
|
41
45
|
this._app = express();
|
|
@@ -45,21 +49,44 @@ export class Server {
|
|
|
45
49
|
this._enableLogging =
|
|
46
50
|
options.logging === undefined ? true : options.logging;
|
|
47
51
|
this._controllersDir = options.controllersDir || "controllers";
|
|
52
|
+
this._viewsDir = options.viewsDir;
|
|
48
53
|
this._securityHandlers = options.securityHandlers || {};
|
|
49
54
|
this._securitySchemes = options.securitySchemes;
|
|
50
55
|
this._container = options.container;
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
this._roleHandler = options.roleHandler;
|
|
57
|
+
// Initialize AJV with strict mode for better security
|
|
58
|
+
this._ajv = new Ajv({
|
|
59
|
+
allErrors: true,
|
|
60
|
+
strict: true,
|
|
61
|
+
removeAdditional: true, // Automatically remove properties not in schema
|
|
62
|
+
});
|
|
53
63
|
addFormats(this._ajv);
|
|
54
64
|
// Security Middleware
|
|
55
|
-
|
|
65
|
+
const helmetOptions = options.helmetOptions || {};
|
|
66
|
+
if (process.env.NODE_ENV !== "production") {
|
|
67
|
+
// Relax CSP for HMR in development
|
|
68
|
+
helmetOptions.contentSecurityPolicy = {
|
|
69
|
+
directives: {
|
|
70
|
+
defaultSrc: ["'self'"],
|
|
71
|
+
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "blob:"],
|
|
72
|
+
connectSrc: ["'self'", "ws:", "wss:", "http:", "https:"],
|
|
73
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
74
|
+
imgSrc: ["'self'", "data:", "blob:"],
|
|
75
|
+
frameSrc: ["'self'"],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
this._app.use(helmet(helmetOptions));
|
|
56
80
|
this._app.use(cors(options.corsOptions));
|
|
57
81
|
this._app.use(rateLimit(options.rateLimitOptions || {
|
|
58
82
|
windowMs: 15 * 60 * 1000,
|
|
59
83
|
max: 100,
|
|
84
|
+
skip: (req) => ServeMemoryStore.instance.getAsset(req.path) !== undefined, // Don't rate limit static assets
|
|
60
85
|
}));
|
|
61
|
-
this._app.use(express.json());
|
|
62
|
-
|
|
86
|
+
this._app.use(express.json({ limit: "1mb" })); // Protection against large payloads
|
|
87
|
+
// Clear cache on startup
|
|
88
|
+
ServeMemoryStore.instance.clearCache();
|
|
89
|
+
this._devMode = process.env.NODE_ENV !== "production";
|
|
63
90
|
if (this._devMode) {
|
|
64
91
|
this.setupHmr();
|
|
65
92
|
}
|
|
@@ -67,22 +94,16 @@ export class Server {
|
|
|
67
94
|
}
|
|
68
95
|
setupHmr() {
|
|
69
96
|
ServeMemoryStore.instance.setDevMode(true);
|
|
70
|
-
ServeMemoryStore.instance.onRebuild(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
78
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
79
|
-
res.setHeader("Connection", "keep-alive");
|
|
80
|
-
res.flushHeaders();
|
|
81
|
-
this._sseClients.push(res);
|
|
82
|
-
req.on("close", () => {
|
|
83
|
-
this._sseClients = this._sseClients.filter((c) => c !== res);
|
|
84
|
-
});
|
|
97
|
+
ServeMemoryStore.instance.onRebuild((data) => {
|
|
98
|
+
if (data && data.html) {
|
|
99
|
+
this._io?.emit("rebuild", { html: data.html });
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this._io?.emit("reload");
|
|
103
|
+
}
|
|
85
104
|
});
|
|
105
|
+
// No redundant global watcher here, we rely on ServeMemoryStore's specific watchers
|
|
106
|
+
// which rebuild before notifying.
|
|
86
107
|
}
|
|
87
108
|
log(message) {
|
|
88
109
|
if (this._enableLogging) {
|
|
@@ -90,6 +111,9 @@ export class Server {
|
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
113
|
async init() {
|
|
114
|
+
if (this._userHandler) {
|
|
115
|
+
this.setupAuthRoutes();
|
|
116
|
+
}
|
|
93
117
|
await this.loadControllers();
|
|
94
118
|
if (this._enableSwagger) {
|
|
95
119
|
this.setupSwagger();
|
|
@@ -100,6 +124,11 @@ export class Server {
|
|
|
100
124
|
return next();
|
|
101
125
|
const asset = ServeMemoryStore.instance.getAsset(req.path);
|
|
102
126
|
if (asset) {
|
|
127
|
+
if (this._devMode) {
|
|
128
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
129
|
+
res.setHeader("Pragma", "no-cache");
|
|
130
|
+
res.setHeader("Expires", "0");
|
|
131
|
+
}
|
|
103
132
|
res.type(asset.type).send(Buffer.from(asset.content));
|
|
104
133
|
return;
|
|
105
134
|
}
|
|
@@ -107,13 +136,31 @@ export class Server {
|
|
|
107
136
|
});
|
|
108
137
|
// Global Error Handler
|
|
109
138
|
this._app.use((err, req, res, next) => {
|
|
110
|
-
|
|
139
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
140
|
+
if (!isProd) {
|
|
141
|
+
console.error(`[${this._id}] Error:`, err);
|
|
142
|
+
}
|
|
111
143
|
res.status(err.status || 500).json({
|
|
112
144
|
error: "Internal Server Error",
|
|
113
|
-
message:
|
|
145
|
+
message: isProd
|
|
146
|
+
? "An unexpected error occurred"
|
|
147
|
+
: err.message || "An unexpected error occurred",
|
|
148
|
+
// Mask stack trace in production
|
|
149
|
+
stack: isProd ? undefined : err.stack,
|
|
114
150
|
});
|
|
115
151
|
});
|
|
116
|
-
this._serverInstance = this._app
|
|
152
|
+
this._serverInstance = createServer(this._app);
|
|
153
|
+
if (this._devMode) {
|
|
154
|
+
this._io = new SocketServer(this._serverInstance, {
|
|
155
|
+
cors: {
|
|
156
|
+
origin: [/localhost/, /127\.0\.0\.1/], // Restrict HMR to local development origin
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
this._io.on("connection", (socket) => {
|
|
160
|
+
// Disconnected client log
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
this._serverInstance.listen(this._port, () => {
|
|
117
164
|
if (this._enableLogging) {
|
|
118
165
|
this.printStartupMessage();
|
|
119
166
|
}
|
|
@@ -131,6 +178,9 @@ export class Server {
|
|
|
131
178
|
`${pc.bold(pad("Tailwind:"))}${this._tailwindEnabled ? pc.blue("Enabled") : pc.dim("Disabled")}`,
|
|
132
179
|
`${pc.bold(pad("HMR:"))}${this._devMode ? pc.cyan("Active") : pc.dim("Inactive")}`,
|
|
133
180
|
];
|
|
181
|
+
if (this._viewsDir) {
|
|
182
|
+
lines.push(`${pc.bold(pad("Views:"))}${this._viewsDir}`);
|
|
183
|
+
}
|
|
134
184
|
if (this._enableSwagger) {
|
|
135
185
|
lines.push(`${pc.bold(pad("Swagger:"))}http://localhost:${this._port}${this._swaggerPath}`);
|
|
136
186
|
}
|
|
@@ -151,6 +201,40 @@ export class Server {
|
|
|
151
201
|
}
|
|
152
202
|
global.servers.delete(this._id);
|
|
153
203
|
}
|
|
204
|
+
setUserHandler(handler) {
|
|
205
|
+
this._userHandler = handler;
|
|
206
|
+
}
|
|
207
|
+
setupAuthRoutes() {
|
|
208
|
+
if (!this._userHandler)
|
|
209
|
+
return;
|
|
210
|
+
this._app.post("/auth/signin", async (req, res, next) => {
|
|
211
|
+
try {
|
|
212
|
+
const result = await this._userHandler.signin(req, res);
|
|
213
|
+
res.json(result);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
next(err);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
this._app.post("/auth/signup", async (req, res, next) => {
|
|
220
|
+
try {
|
|
221
|
+
const result = await this._userHandler.signup(req, res);
|
|
222
|
+
res.json(result);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
next(err);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
this._app.post("/auth/logout", async (req, res, next) => {
|
|
229
|
+
try {
|
|
230
|
+
const result = await this._userHandler.logout(req, res);
|
|
231
|
+
res.json(result);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
next(err);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
154
238
|
async loadControllers() {
|
|
155
239
|
const absoluteControllersPath = path.resolve(process.cwd(), this._controllersDir);
|
|
156
240
|
if (!fs.existsSync(absoluteControllersPath)) {
|
|
@@ -178,28 +262,18 @@ export class Server {
|
|
|
178
262
|
const instance = this._container?.get
|
|
179
263
|
? this._container.get(controller.target)
|
|
180
264
|
: new controller.target();
|
|
181
|
-
// Check class level auth
|
|
182
|
-
const classAuthInfo = Reflect.getMetadata(AUTH_METADATA_KEY, controller.target);
|
|
183
265
|
for (const route of controller.routes) {
|
|
184
266
|
const fullPath = (controller.path + "/" + route.path).replace(/\/+/g, "/");
|
|
185
267
|
const middlewares = [];
|
|
186
|
-
// Check
|
|
187
|
-
const
|
|
268
|
+
// Check ROLES metadata
|
|
269
|
+
const classRoles = Reflect.getMetadata(ROLES_METADATA_KEY, controller.target);
|
|
270
|
+
const methodRoles = Reflect.getMetadata(ROLES_METADATA_KEY, controller.target.prototype, route.handlerName);
|
|
271
|
+
const requiredRoles = methodRoles || classRoles;
|
|
188
272
|
const isPublic = Reflect.getMetadata(PUBLIC_METADATA_KEY, controller.target.prototype, route.handlerName);
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
else if (methodAuthInfo) {
|
|
195
|
-
authSecret = methodAuthInfo.secret || process.env.JWT_SECRET;
|
|
196
|
-
}
|
|
197
|
-
else if (classAuthInfo) {
|
|
198
|
-
authSecret = classAuthInfo.secret || process.env.JWT_SECRET;
|
|
199
|
-
}
|
|
200
|
-
// 1. Auth Middleware (New Decorator Logic)
|
|
201
|
-
if (authSecret) {
|
|
202
|
-
middlewares.push(this.createJwtMiddleware(authSecret));
|
|
273
|
+
// 1. Auth & Role Middlewares
|
|
274
|
+
if (!isPublic) {
|
|
275
|
+
// If not public, we always run the role check (which might include authentication via UserHandler)
|
|
276
|
+
middlewares.push(this.createRoleMiddleware(requiredRoles || []));
|
|
203
277
|
// Inject Swagger security definition automatically
|
|
204
278
|
if (!route.swagger) {
|
|
205
279
|
route.swagger = {
|
|
@@ -219,7 +293,8 @@ export class Server {
|
|
|
219
293
|
route.swagger.security.push({ bearerAuth: [] });
|
|
220
294
|
}
|
|
221
295
|
}
|
|
222
|
-
//
|
|
296
|
+
// 1.1 Role Middlewares - now handled above for non-public routes
|
|
297
|
+
// 2. Generic Security Middleware (@Security, @ApiKey, etc)
|
|
223
298
|
const classGenericSecurity = Reflect.getMetadata(SECURITY_METADATA_KEY, controller.target) || [];
|
|
224
299
|
const methodGenericSecurity = Reflect.getMetadata(SECURITY_METADATA_KEY, controller.target.prototype, route.handlerName) || [];
|
|
225
300
|
let genericRequirements = [];
|
|
@@ -301,9 +376,17 @@ export class Server {
|
|
|
301
376
|
middlewares.push(...route.middlewares);
|
|
302
377
|
}
|
|
303
378
|
// 3. Route Handler
|
|
304
|
-
const handler = (req, res, next) => {
|
|
379
|
+
const handler = async (req, res, next) => {
|
|
305
380
|
try {
|
|
306
|
-
const
|
|
381
|
+
const user = this._userHandler
|
|
382
|
+
? await this._userHandler.authenticate(req, res)
|
|
383
|
+
: req.user;
|
|
384
|
+
const result = instance[route.handlerName]({
|
|
385
|
+
req,
|
|
386
|
+
res,
|
|
387
|
+
user,
|
|
388
|
+
next,
|
|
389
|
+
});
|
|
307
390
|
const handleResult = (val) => {
|
|
308
391
|
if (val !== undefined && !res.headersSent) {
|
|
309
392
|
if (typeof val === "string") {
|
|
@@ -330,28 +413,55 @@ export class Server {
|
|
|
330
413
|
// Pre-build if @Serve is used
|
|
331
414
|
const htmlPath = Reflect.getMetadata(SERVE_HTML_METADATA_KEY, controller.target.prototype, route.handlerName);
|
|
332
415
|
if (htmlPath) {
|
|
333
|
-
|
|
416
|
+
// Silent pre-build
|
|
334
417
|
await ServeMemoryStore.instance.buildAndCache(htmlPath, tailwindOptions);
|
|
335
418
|
}
|
|
336
419
|
}
|
|
337
420
|
}
|
|
338
421
|
}
|
|
339
|
-
|
|
340
|
-
return (req, res, next) => {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
422
|
+
createRoleMiddleware(roles) {
|
|
423
|
+
return async (req, res, next) => {
|
|
424
|
+
const user = this._userHandler
|
|
425
|
+
? await this._userHandler.authenticate(req, res)
|
|
426
|
+
: req.user;
|
|
427
|
+
if (!user) {
|
|
428
|
+
return res.status(401).json({ error: "Unauthorized: No user found" });
|
|
429
|
+
}
|
|
430
|
+
// If no specific roles required, but it's not public, just authentication is enough
|
|
431
|
+
if (roles.length === 0) {
|
|
432
|
+
return next();
|
|
433
|
+
}
|
|
434
|
+
if (!this._roleHandler) {
|
|
435
|
+
// Default role check behavior
|
|
436
|
+
const userRoles = Array.isArray(user.roles)
|
|
437
|
+
? user.roles
|
|
438
|
+
: user.role
|
|
439
|
+
? [user.role]
|
|
440
|
+
: [];
|
|
441
|
+
const hasRole = roles.some((role) => userRoles.includes(role));
|
|
442
|
+
if (!hasRole) {
|
|
443
|
+
return res.status(403).json({
|
|
444
|
+
error: "Forbidden: You do not have the required permissions",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return next();
|
|
346
448
|
}
|
|
347
|
-
const token = authHeader.split(" ")[1];
|
|
348
449
|
try {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
450
|
+
const result = await this._roleHandler(req, roles);
|
|
451
|
+
if (result) {
|
|
452
|
+
// Assuming 'result' is a boolean indicating success
|
|
453
|
+
next();
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
// The provided snippet seems to be for serving HTML, which is out of context for a role middleware.
|
|
457
|
+
// Applying the original logic for role failure.
|
|
458
|
+
res.status(403).json({
|
|
459
|
+
error: "Forbidden: You do not have the required permissions",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
352
462
|
}
|
|
353
463
|
catch (err) {
|
|
354
|
-
|
|
464
|
+
next(err);
|
|
355
465
|
}
|
|
356
466
|
};
|
|
357
467
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
export declare abstract class UserHandler {
|
|
3
|
+
/**
|
|
4
|
+
* Optional: Logic to authenticate the request and return the user object.
|
|
5
|
+
* If this is not implemented, 'user' will be undefined in route parameters.
|
|
6
|
+
*/
|
|
7
|
+
authenticate(req: Request, res: Response): Promise<any | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Sign in logic. Usually returns a token or user info.
|
|
10
|
+
*/
|
|
11
|
+
abstract signin(req: Request, res: Response): Promise<any> | any;
|
|
12
|
+
/**
|
|
13
|
+
* Sign up logic.
|
|
14
|
+
*/
|
|
15
|
+
abstract signup(req: Request, res: Response): Promise<any> | any;
|
|
16
|
+
/**
|
|
17
|
+
* Logout logic.
|
|
18
|
+
*/
|
|
19
|
+
abstract logout(req: Request, res: Response): Promise<any> | any;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=UserHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserHandler.d.ts","sourceRoot":"","sources":["../../src/Components/UserHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAgB,MAAM,SAAS,CAAC;AAE1D,8BAAsB,WAAW;IAC/B;;;OAGG;IACU,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAI3E;;OAEG;aACa,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG;IAEvE;;OAEG;aACa,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG;IAEvE;;OAEG;aACa,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG;CACxE"}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
export declare const AUTH_METADATA_KEY = "auth:info";
|
|
2
1
|
export declare const PUBLIC_METADATA_KEY = "auth:public";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
export declare function BearerAuth(secret?: string): MethodDecorator & ClassDecorator;
|
|
2
|
+
export declare const ROLES_METADATA_KEY = "auth:roles";
|
|
3
|
+
export declare function Authorized(roles?: string[]): MethodDecorator & ClassDecorator;
|
|
7
4
|
export declare function Public(): MethodDecorator;
|
|
8
5
|
//# sourceMappingURL=Authorized.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Authorized.d.ts","sourceRoot":"","sources":["../../src/Decorations/Authorized.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"Authorized.d.ts","sourceRoot":"","sources":["../../src/Decorations/Authorized.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AACjD,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAE/C,wBAAgB,UAAU,CACxB,KAAK,GAAE,MAAM,EAAO,GACnB,eAAe,GAAG,cAAc,CAclC;AAED,wBAAgB,MAAM,IAAI,eAAe,CAQxC"}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// Key to store auth metadata
|
|
2
|
-
export const AUTH_METADATA_KEY = "auth:info";
|
|
3
2
|
export const PUBLIC_METADATA_KEY = "auth:public";
|
|
4
|
-
export
|
|
3
|
+
export const ROLES_METADATA_KEY = "auth:roles";
|
|
4
|
+
export function Authorized(roles = []) {
|
|
5
5
|
return function (target, propertyKey, descriptor) {
|
|
6
|
-
// If used on a method
|
|
7
6
|
if (propertyKey) {
|
|
8
|
-
Reflect.defineMetadata(
|
|
7
|
+
Reflect.defineMetadata(ROLES_METADATA_KEY, roles, target, propertyKey);
|
|
8
|
+
// Explicitly mark as not public to prevent bypass if @Public() is also used
|
|
9
|
+
Reflect.defineMetadata(PUBLIC_METADATA_KEY, false, target, propertyKey);
|
|
9
10
|
}
|
|
10
11
|
else {
|
|
11
|
-
|
|
12
|
-
Reflect.defineMetadata(AUTH_METADATA_KEY, { secret }, target);
|
|
12
|
+
Reflect.defineMetadata(ROLES_METADATA_KEY, roles, target);
|
|
13
13
|
}
|
|
14
14
|
};
|
|
15
15
|
}
|
|
@@ -4,6 +4,5 @@ export interface SecurityRequirement {
|
|
|
4
4
|
[name: string]: string[];
|
|
5
5
|
}
|
|
6
6
|
export declare function Security(name: string, scopes?: string[]): MethodDecorator & ClassDecorator;
|
|
7
|
-
export declare function OAuth(scopes?: string[]): MethodDecorator & ClassDecorator;
|
|
8
7
|
export declare function ApiKey(name: string): MethodDecorator & ClassDecorator;
|
|
9
8
|
//# sourceMappingURL=Security.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Security.d.ts","sourceRoot":"","sources":["../../src/Decorations/Security.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,eAAO,MAAM,qBAAqB,eAAqB,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,MAAM,EAAO,GACpB,eAAe,GAAG,cAAc,CAyBlC;AAED,wBAAgB,
|
|
1
|
+
{"version":3,"file":"Security.d.ts","sourceRoot":"","sources":["../../src/Decorations/Security.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B,eAAO,MAAM,qBAAqB,eAAqB,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,MAAM,EAAO,GACpB,eAAe,GAAG,cAAc,CAyBlC;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,cAAc,CAErE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Serve.d.ts","sourceRoot":"","sources":["../../src/Decorations/Serve.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,uBAAuB,eAAe,CAAC;AAEpD,wBAAgB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"Serve.d.ts","sourceRoot":"","sources":["../../src/Decorations/Serve.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,uBAAuB,eAAe,CAAC;AAEpD,wBAAgB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAwEvD"}
|
|
@@ -6,10 +6,12 @@ export function Serve(htmlPath) {
|
|
|
6
6
|
// Store the HTML path in metadata for pre-building
|
|
7
7
|
Reflect.defineMetadata(SERVE_HTML_METADATA_KEY, htmlPath, target, propertyKey);
|
|
8
8
|
const originalMethod = descriptor.value;
|
|
9
|
-
descriptor.value = async function (req, res, next) {
|
|
9
|
+
descriptor.value = async function ({ req, res, user, next, }) {
|
|
10
10
|
try {
|
|
11
11
|
// Execute the original method
|
|
12
|
-
const result = await originalMethod.apply(this, [
|
|
12
|
+
const result = await originalMethod.apply(this, [
|
|
13
|
+
{ req, res, user, next },
|
|
14
|
+
]);
|
|
13
15
|
// Check if response has been sent or if result is not undefined/void
|
|
14
16
|
if (res.headersSent || result !== undefined) {
|
|
15
17
|
return result;
|
|
@@ -19,14 +21,25 @@ export function Serve(htmlPath) {
|
|
|
19
21
|
// If no response, build and serve the HTML
|
|
20
22
|
const html = await ServeMemoryStore.instance.buildAndCache(htmlPath, tailwindOptions);
|
|
21
23
|
if (html) {
|
|
24
|
+
const devMode = process.env.NODE_ENV === "development";
|
|
25
|
+
if (devMode) {
|
|
26
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
|
|
27
|
+
res.setHeader("Pragma", "no-cache");
|
|
28
|
+
res.setHeader("Expires", "0");
|
|
29
|
+
}
|
|
22
30
|
res.type("html").send(html);
|
|
23
31
|
}
|
|
24
|
-
else {
|
|
32
|
+
else if (next) {
|
|
25
33
|
next(); // No HTML found?
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
catch (error) {
|
|
29
|
-
next
|
|
37
|
+
if (next) {
|
|
38
|
+
next(error);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
30
43
|
}
|
|
31
44
|
};
|
|
32
45
|
return descriptor;
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,WAAW,CAAC;AACnB,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,WAAW,CAAC;AACnB,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synchjs/ewb",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -26,26 +26,29 @@
|
|
|
26
26
|
"example": "bun run --watch example/index.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@types/cors": "^2.8.19",
|
|
30
|
+
"@types/express": "^5.0.6",
|
|
31
|
+
"@types/express-serve-static-core": "^5.1.1",
|
|
32
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
33
|
+
"@types/socket.io": "^3.0.2",
|
|
34
|
+
"@types/swagger-jsdoc": "^6.0.4",
|
|
35
|
+
"@types/swagger-ui-express": "^4.1.8",
|
|
29
36
|
"ajv": "^8.18.0",
|
|
30
37
|
"ajv-formats": "^3.0.1",
|
|
31
38
|
"boxen": "^8.0.1",
|
|
32
39
|
"bun-plugin-tailwind": "^0.1.2",
|
|
40
|
+
"cheerio": "^1.2.0",
|
|
33
41
|
"cors": "^2.8.6",
|
|
34
42
|
"express": "^5.2.1",
|
|
35
43
|
"express-rate-limit": "^8.2.1",
|
|
36
44
|
"helmet": "^8.1.0",
|
|
37
45
|
"jsonwebtoken": "^9.0.3",
|
|
46
|
+
"openapi-types": "^12.1.3",
|
|
38
47
|
"picocolors": "^1.1.1",
|
|
39
48
|
"reflect-metadata": "^0.2.2",
|
|
49
|
+
"socket.io": "^4.8.3",
|
|
40
50
|
"swagger-jsdoc": "^6.2.8",
|
|
41
51
|
"swagger-ui-express": "^5.0.1",
|
|
42
|
-
"tailwindcss": "^4.1.18"
|
|
43
|
-
"openapi-types": "^12.1.3",
|
|
44
|
-
"@types/express": "^5.0.6",
|
|
45
|
-
"@types/cors": "^2.8.19",
|
|
46
|
-
"@types/jsonwebtoken": "^9.0.10",
|
|
47
|
-
"@types/swagger-jsdoc": "^6.0.4",
|
|
48
|
-
"@types/swagger-ui-express": "^4.1.8",
|
|
49
|
-
"@types/express-serve-static-core": "^5.1.1"
|
|
52
|
+
"tailwindcss": "^4.1.18"
|
|
50
53
|
}
|
|
51
54
|
}
|