@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 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**, **Authentication**, and **Frontend Asset Serving** (with Tailwind CSS support).
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
- - 🔒 **Built-in Authentication**: Easy-to-use Bearer Token authentication (`@BearerAuth`).
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**: Schema-based validation using AJV.
11
- - 🎨 **Frontend Serving**: Serve HTML/CSS/JS assets from memory, with built-in **Tailwind CSS** processing via Bun plugins.
12
- - 🧩 **Dependency Injection**: Support for IoC containers.
13
- - 🚀 **Bun Optimized**: Leverages Bun's build capabilities for static assets.
14
- - 🖥 **TUI-style Logs**: Clean and informative startup messages.
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(req: Request, res: Response) {
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", // Directory where your controllers are located
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
- Run with Bun:
62
-
63
- ```bash
64
- bun run index.ts
65
- ```
58
+ ## Authentication & User Management
66
59
 
67
- ### 3. Customize Swagger Path
60
+ The framework uses a `UserHandler` system to give you full control over identity.
68
61
 
69
- You can change where Swagger UI is served:
62
+ ### 1. Define your User Handler
70
63
 
71
64
  ```typescript
72
- const server = new Server({
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
- @Controller("/users", { tags: ["Users"] })
91
- export class UserController {
92
- @Get("/:id")
93
- public getUser(req: Request, res: Response) {
94
- const { id } = req.params;
95
- return { id, name: "User " + id };
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
- @Post("/", {
99
- summary: "Create User",
100
- responses: {
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
- @Get("/status")
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
- #### Advanced Security (@Security, @OAuth, @ApiKey)
91
+ ### 2. Protect Routes
141
92
 
142
- For other authentication methods like OAuth2 or API Keys, use generic decorators:
93
+ Use `@Authorized` to require a user, or specify roles.
143
94
 
144
95
  ```typescript
145
- import { Controller, Get, OAuth, ApiKey } from "ewb";
96
+ import { Controller, Get, Authorized } from "@synchjs/ewb";
146
97
 
147
- @Controller("/api")
148
- export class ApiController {
98
+ @Controller("/dashboard")
99
+ export class DashboardController {
149
100
  @Get("/profile")
150
- @OAuth(["read:profile"]) // Marks route as requiring OAuth2 with specific scope
151
- public getProfile() {
152
- return { name: "John Doe" };
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("/data")
156
- @ApiKey("X-API-KEY") // Custom security scheme name
157
- public getData() {
158
- return { sensitive: "data" };
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
- **Configure Handlers and Schemes:**
114
+ ### 3. Register the Handler
164
115
 
165
116
  ```typescript
166
- const server = new Server({
167
- // ...,
168
- securitySchemes: {
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
- ### Request Validation
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
- ```typescript
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
- ### Serving Frontend Assets (with Tailwind CSS)
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
- You can serve compiled HTML and automatically process Tailwind CSS using the `@Serve` and `@Tailwindcss` decorators.
130
+ ## Serving Frontend Assets
217
131
 
218
- 1. **Project Structure**:
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") // Path to your HTML entry point
243
- public app(req: Request, res: Response) {
244
- // The decorator handles the response.
141
+ @Serve("views/src/index.html")
142
+ public app() {
143
+ // Handled by memory store
245
144
  }
246
145
  }
247
146
  ```
248
147
 
249
- ### Middleware
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
- @Controller("/audit")
262
- @Middleware(logAccess)
263
- export class AuditController {
264
- @Get("/")
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
- The `Server` class accepts the following options:
274
-
275
- | Option | Type | Description |
276
- | ------------------ | ------------------ | ---------------------------------------------------------------- |
277
- | `id` | `string` | Unique identifier for the server instance. |
278
- | `port` | `number` | Port to listen on. |
279
- | `controllersDir` | `string` | Directory containing controller files. |
280
- | `enableSwagger` | `boolean` | Enable OpenAPI documentation. |
281
- | `swaggerPath` | `string` | Custom path for Swagger UI (default: `/api-docs`). |
282
- | `logging` | `boolean` | Enable/disable TUI startup messages (default: true). |
283
- | `corsOptions` | `CorsOptions` | Configuration for CORS. |
284
- | `helmetOptions` | `HelmetOptions` | Configuration for Helmet security headers. |
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: () => void): void;
15
+ onRebuild(listener: (data?: {
16
+ html: string;
17
+ }) => void): void;
14
18
  private notify;
15
- getAsset(path: string): {
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;AAI/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,CAAsB;IAExC,OAAO;IAUP,WAAkB,QAAQ,IAAI,gBAAgB,CAK7C;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO;IAI3B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI;IAIrC,OAAO,CAAC,MAAM;IAIP,QAAQ,CAAC,IAAI,EAAE,MAAM;cArCS,MAAM;iBAAW,UAAU;;IAyChE,OAAO,CAAC,WAAW;YAQL,YAAY;IAuB1B,OAAO,CAAC,YAAY;IA4CP,aAAa,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,eAAgD,GACxD,OAAO,CAAC,MAAM,CAAC;CAqHnB"}
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(path) {
38
- return this._assets.get(path);
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
- console.log(`[HMR] Change detected in ${filename}. Rebuilding...`);
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 (unless in dev mode and we want to ensure fresh start,
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 sse = new EventSource('/ebw-hmr');
160
- sse.onmessage = (e) => {
161
- if (e.data === 'reload') {
162
- console.log('[HMR] Reloading page...');
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
- sse.onerror = () => {
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
- if (htmlContent.includes("</body>")) {
173
- htmlContent = htmlContent.replace("</body>", `${reloadScript}</body>`);
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
- htmlContent += reloadScript;
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
- const hash = Bun.hash(cacheKey).toString(16);
183
- const cacheFile = path.join(this._cacheDir, `${hash}.json`);
184
- fs.writeFileSync(cacheFile, JSON.stringify({
185
- html: htmlContent,
186
- assets: currentBuildAssets,
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 createJwtMiddleware;
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
- devMode?: boolean;
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;AAwB5B,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,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,eAAe,CAAC,CAAa;IAErC,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAAkB;gBAEzB,OAAO,EAAE,aAAa;IAuClC,OAAO,CAAC,QAAQ;IAyBhB,OAAO,CAAC,GAAG;IAME,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAqClC,OAAO,CAAC,mBAAmB;IAsCpB,KAAK,IAAI,IAAI;YASN,eAAe;IAkQ7B,OAAO,CAAC,mBAAmB;IAoB3B,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,OAAO,CAAC,EAAE,OAAO,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;CAC3C"}
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 * as jwt from "jsonwebtoken";
13
- import { AUTH_METADATA_KEY, PUBLIC_METADATA_KEY, } from "../Decorations/Authorized";
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
- // Initialize AJV
52
- this._ajv = new Ajv({ allErrors: true, strict: false });
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
- this._app.use(helmet(options.helmetOptions));
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
- this._devMode = options.devMode ?? process.env.NODE_ENV === "development";
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
- this.log(`[${this._id}] Sending reload signal to ${this._sseClients.length} clients`);
72
- this._sseClients.forEach((res) => {
73
- res.write("data: reload\n\n");
74
- });
75
- });
76
- this._app.get("/ebw-hmr", (req, res) => {
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
- console.error(`[${this._id}] Error:`, err);
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: "An unexpected error occurred",
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.listen(this._port, () => {
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 method level metadata
187
- const methodAuthInfo = Reflect.getMetadata(AUTH_METADATA_KEY, controller.target.prototype, route.handlerName);
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
- // Determine if auth is required and which secret to use
190
- let authSecret;
191
- if (isPublic) {
192
- authSecret = undefined;
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
- // 2. Generic Security Middleware (@Security, @OAuth, etc)
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 result = instance[route.handlerName](req, res, next);
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
- this.log(`[${this._id}] Pre-building HTML: ${htmlPath}`);
416
+ // Silent pre-build
334
417
  await ServeMemoryStore.instance.buildAndCache(htmlPath, tailwindOptions);
335
418
  }
336
419
  }
337
420
  }
338
421
  }
339
- createJwtMiddleware(secret) {
340
- return (req, res, next) => {
341
- const authHeader = req.headers.authorization;
342
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
343
- return res
344
- .status(401)
345
- .json({ error: "Unauthorized: Missing Bearer token" });
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 decoded = jwt.verify(token, secret);
350
- req.user = decoded; // Attach user to request
351
- next();
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
- return res.status(403).json({ error: "Forbidden: Invalid token" });
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"}
@@ -0,0 +1,9 @@
1
+ export class UserHandler {
2
+ /**
3
+ * Optional: Logic to authenticate the request and return the user object.
4
+ * If this is not implemented, 'user' will be undefined in route parameters.
5
+ */
6
+ async authenticate(req, res) {
7
+ return req.user;
8
+ }
9
+ }
@@ -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 interface AuthInfo {
4
- secret?: string;
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,iBAAiB,cAAc,CAAC;AAC7C,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AAEjD,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,cAAc,CAmB5E;AAED,wBAAgB,MAAM,IAAI,eAAe,CAQxC"}
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 function BearerAuth(secret) {
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(AUTH_METADATA_KEY, { secret }, target, propertyKey);
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
- // If used on a class
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,KAAK,CAAC,MAAM,GAAE,MAAM,EAAO,GAAG,eAAe,GAAG,cAAc,CAE7E;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,cAAc,CAErE"}
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"}
@@ -16,9 +16,6 @@ export function Security(name, scopes = []) {
16
16
  }
17
17
  };
18
18
  }
19
- export function OAuth(scopes = []) {
20
- return Security("oauth2", scopes);
21
- }
22
19
  export function ApiKey(name) {
23
20
  return Security(name, []);
24
21
  }
@@ -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,CAmDvD"}
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, [req, res, next]);
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(error);
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
@@ -1,5 +1,6 @@
1
1
  import "./globals";
2
2
  export * from "./Components/Server";
3
3
  export * from "./Components/ServeMemoryStore";
4
+ export * from "./Components/UserHandler";
4
5
  export * from "./Decorations";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -2,4 +2,5 @@ global.servers = new Map();
2
2
  import "./globals";
3
3
  export * from "./Components/Server";
4
4
  export * from "./Components/ServeMemoryStore";
5
+ export * from "./Components/UserHandler";
5
6
  export * from "./Decorations";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synchjs/ewb",
3
- "version": "1.0.1",
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
  }