skyguard-js 1.1.8 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +395 -42
- package/dist/app.d.ts +21 -2
- package/dist/app.js +33 -4
- package/dist/crypto/hasher.js +2 -1
- package/dist/crypto/jwt.js +2 -1
- package/dist/helpers/http.d.ts +37 -0
- package/dist/helpers/http.js +37 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/logger.d.ts +10 -1
- package/dist/http/logger.js +44 -8
- package/dist/http/nodeNativeHttp.d.ts +3 -1
- package/dist/http/nodeNativeHttp.js +4 -3
- package/dist/http/request.d.ts +10 -11
- package/dist/http/request.js +8 -11
- package/dist/http/response.d.ts +47 -3
- package/dist/http/response.js +77 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +5 -2
- package/dist/middlewares/cors.js +2 -2
- package/dist/middlewares/csrf.d.ts +69 -0
- package/dist/middlewares/csrf.js +315 -0
- package/dist/middlewares/index.d.ts +2 -0
- package/dist/middlewares/index.js +5 -1
- package/dist/middlewares/rateLimiter.d.ts +82 -0
- package/dist/middlewares/rateLimiter.js +159 -0
- package/dist/routing/routerGroup.d.ts +2 -2
- package/dist/routing/routerGroup.js +2 -2
- package/dist/sessions/databaseSessionStorage.d.ts +52 -0
- package/dist/sessions/databaseSessionStorage.js +166 -0
- package/dist/sessions/fileSessionStorage.js +1 -1
- package/dist/sessions/index.d.ts +1 -0
- package/dist/sessions/index.js +3 -1
- package/dist/sessions/memorySessionStorage.js +1 -1
- package/dist/storage/uploader.d.ts +1 -1
- package/dist/storage/uploader.js +7 -6
- package/dist/types/index.d.ts +1 -10
- package/dist/validators/rules/bigIntRule.d.ts +0 -6
- package/dist/validators/rules/bigIntRule.js +0 -24
- package/dist/validators/rules/convertPrimitiveRule.d.ts +12 -0
- package/dist/validators/rules/convertPrimitiveRule.js +24 -0
- package/dist/validators/rules/dateRule.js +3 -13
- package/dist/validators/types.d.ts +47 -1
- package/dist/validators/validationRule.d.ts +2 -0
- package/dist/validators/validationRule.js +6 -1
- package/dist/validators/validationSchema.d.ts +83 -9
- package/dist/validators/validationSchema.js +151 -12
- package/dist/validators/validator.js +20 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/skyguard-js)
|
|
8
8
|
[](https://github.com/Pipe930/Skyguard-js/actions/workflows/pipeline.yml)
|
|
9
|
+
[](https://badge.socket.dev/npm/package/skyguard-js/1.1.8)
|
|
9
10
|
|
|
10
11
|
**Skyguard.js** is a **lightweight, dependency-free web framework** built entirely with **TypeScript**.
|
|
11
12
|
|
|
@@ -32,10 +33,11 @@ Skyguard.js currently delivers a solid core that includes **routing**, **type-sa
|
|
|
32
33
|
- Global, group, and route-level middlewares
|
|
33
34
|
- Request / Response abstractions
|
|
34
35
|
- Declarative data validation
|
|
35
|
-
-
|
|
36
|
+
- Support for template motors (handlebars, pugs, ejs, etc.)
|
|
36
37
|
- Built-in HTTP exceptions
|
|
37
38
|
- Password hashing and JWT token generation
|
|
38
39
|
- CORS middleware
|
|
40
|
+
- CSRF middleware protection
|
|
39
41
|
- File uploads (via middleware)
|
|
40
42
|
- Static file serving
|
|
41
43
|
- Session handling (via middleware)
|
|
@@ -44,6 +46,24 @@ Skyguard.js currently delivers a solid core that includes **routing**, **type-sa
|
|
|
44
46
|
|
|
45
47
|
## 📦 Installation
|
|
46
48
|
|
|
49
|
+
You need to have [NodeJS](https://nodejs.org/) version 22 or later installed.
|
|
50
|
+
|
|
51
|
+
Create the `package.json` file to start a new [NodeJS](https://nodejs.org/) project using the `npm init` command.
|
|
52
|
+
|
|
53
|
+
After configuring the package.json, install [Typescript](https://www.typescriptlang.org/).
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install typescript -D
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
After installing [Typescript](https://www.typescriptlang.org/) in your project, you need to create the [Typescript](https://www.typescriptlang.org/) configuration file `tsconfig.json`.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx tsc --init
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Now that [Typescript](https://www.typescriptlang.org/) is configured, we can install the library. Since it's a module that's in the [NPM Registry](https://www.npmjs.com/), we use the npm package manager.
|
|
66
|
+
|
|
47
67
|
```bash
|
|
48
68
|
npm install skyguard-js
|
|
49
69
|
```
|
|
@@ -68,9 +88,6 @@ app.run(PORT, () => {
|
|
|
68
88
|
});
|
|
69
89
|
```
|
|
70
90
|
|
|
71
|
-
> [!NOTE]
|
|
72
|
-
> It is recommended to develop with `TypeScript` for a more secure and efficient development process; the framework already has native support for `TypeScript` and includes the necessary types.
|
|
73
|
-
|
|
74
91
|
---
|
|
75
92
|
|
|
76
93
|
## 🛣️ Routing
|
|
@@ -97,8 +114,8 @@ Route groups allow you to organize endpoints under a shared prefix.
|
|
|
97
114
|
|
|
98
115
|
```ts
|
|
99
116
|
app.group("/api", api => {
|
|
100
|
-
api.get("/users", () =>
|
|
101
|
-
api.get("/products", () =>
|
|
117
|
+
api.get("/users", () => res.json({ message: "Users" }));
|
|
118
|
+
api.get("/products", () => res.json({ message: "Products" }));
|
|
102
119
|
});
|
|
103
120
|
```
|
|
104
121
|
|
|
@@ -109,30 +126,30 @@ app.group("/api", api => {
|
|
|
109
126
|
Middlewares can be registered **globally**, **per group**, or **per route**.
|
|
110
127
|
|
|
111
128
|
```ts
|
|
112
|
-
import { Request, Response, RouteHandler } from "skyguard-js";
|
|
129
|
+
import { Request, Response, json, RouteHandler } from "skyguard-js";
|
|
113
130
|
|
|
114
131
|
const authMiddleware = async (
|
|
115
132
|
request: Request,
|
|
116
133
|
next: RouteHandler,
|
|
117
134
|
): Promise<Response> => {
|
|
118
135
|
if (request.headers["authorization"] !== "secret") {
|
|
119
|
-
return
|
|
136
|
+
return json({ message: "Unauthorized" }).setStatus(401);
|
|
120
137
|
}
|
|
121
138
|
|
|
122
139
|
return next(request);
|
|
123
140
|
};
|
|
124
141
|
|
|
125
142
|
// Global middleware
|
|
126
|
-
app.middlewares(
|
|
143
|
+
app.middlewares(authMiddleware);
|
|
127
144
|
|
|
128
145
|
// Group middleware
|
|
129
146
|
app.group("/admin", admin => {
|
|
130
|
-
admin.middlewares(
|
|
131
|
-
admin.get("/dashboard", () =>
|
|
147
|
+
admin.middlewares(authMiddleware);
|
|
148
|
+
admin.get("/dashboard", () => json({ ok: true }));
|
|
132
149
|
});
|
|
133
150
|
|
|
134
151
|
// Route-level middleware
|
|
135
|
-
app.get("/secure", () =>
|
|
152
|
+
app.get("/secure", () => json({ secure: true }), [authMiddleware]);
|
|
136
153
|
```
|
|
137
154
|
|
|
138
155
|
---
|
|
@@ -144,14 +161,135 @@ To enable CORS, use the built-in `cors` middleware.
|
|
|
144
161
|
```ts
|
|
145
162
|
import { cors, HttpMethods } from "skyguard-js";
|
|
146
163
|
|
|
147
|
-
app.middlewares(
|
|
164
|
+
app.middlewares(
|
|
148
165
|
cors({
|
|
149
166
|
origin: ["http://localhost:3000", "https://myapp.com"],
|
|
150
167
|
methods: [HttpMethods.get, HttpMethods.post],
|
|
151
168
|
allowedHeaders: ["Content-Type", "Authorization"],
|
|
152
169
|
credentials: true,
|
|
153
170
|
}),
|
|
154
|
-
|
|
171
|
+
);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 🛡️ CSRF Middleware
|
|
177
|
+
|
|
178
|
+
Use the built-in `csrf` middleware to protect endpoints against CSRF attacks.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { csrf, json } from "skyguard-js";
|
|
182
|
+
|
|
183
|
+
app.middlewares(
|
|
184
|
+
csrf({
|
|
185
|
+
cookieName: "XSRF-TOKEN",
|
|
186
|
+
headerNames: ["x-csrf-token"],
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
app.post("/transfer", () => {
|
|
191
|
+
return json({ ok: true });
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The middleware follows a hardened **double-submit cookie** strategy:
|
|
196
|
+
|
|
197
|
+
- It issues a CSRF cookie when missing (including first GET/HEAD/OPTIONS and failed protected requests).
|
|
198
|
+
- For state-changing requests (POST/PUT/PATCH/DELETE), it validates the token from header/body against the cookie value.
|
|
199
|
+
- It validates `Origin`/`Referer` for protected requests (and requires `Referer` on HTTPS when `Origin` is missing).
|
|
200
|
+
- It rejects duplicated CSRF header values to avoid ambiguous token parsing.
|
|
201
|
+
|
|
202
|
+
### Example: CSRF token in HTML templates (Express Handlebars)
|
|
203
|
+
|
|
204
|
+
When you render server-side HTML, you can pass the CSRF token to your template and include it as a hidden field in forms.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { createApp, csrf, render, json } from "skyguard-js";
|
|
208
|
+
import { engine } from "express-handlebars";
|
|
209
|
+
import { join } from "node:path";
|
|
210
|
+
|
|
211
|
+
const app = createApp();
|
|
212
|
+
|
|
213
|
+
app.views(__dirname, "views");
|
|
214
|
+
app.engineTemplates(
|
|
215
|
+
"hbs",
|
|
216
|
+
engine({
|
|
217
|
+
extname: "hbs",
|
|
218
|
+
layoutsDir: join(__dirname, "views"),
|
|
219
|
+
defaultLayout: "main",
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
app.middlewares(
|
|
224
|
+
csrf({
|
|
225
|
+
cookieName: "XSRF-TOKEN",
|
|
226
|
+
headerNames: ["x-csrf-token"],
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
app.get("/transfer", request => {
|
|
231
|
+
return render("transfer", {
|
|
232
|
+
csrfToken: request.cookies["XSRF-TOKEN"],
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
app.post("/transfer", request => {
|
|
237
|
+
// If middleware passes, token is valid
|
|
238
|
+
return json({ ok: true, amount: request.body.amount });
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`views/transfer.hbs`:
|
|
243
|
+
|
|
244
|
+
```hbs
|
|
245
|
+
<form action="/transfer" method="POST">
|
|
246
|
+
<input type="hidden" name="csrf" value="{{csrfToken}}" />
|
|
247
|
+
<input type="number" name="amount" />
|
|
248
|
+
<button type="submit">Send</button>
|
|
249
|
+
</form>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
For `fetch`/AJAX requests, send the same token in headers:
|
|
253
|
+
|
|
254
|
+
```html
|
|
255
|
+
<script>
|
|
256
|
+
const csrfToken = "{{csrfToken}}";
|
|
257
|
+
|
|
258
|
+
async function sendTransfer() {
|
|
259
|
+
await fetch("/transfer", {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: {
|
|
262
|
+
"Content-Type": "application/json",
|
|
263
|
+
"x-csrf-token": csrfToken,
|
|
264
|
+
},
|
|
265
|
+
body: JSON.stringify({ amount: 150 }),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
</script>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## 🚦 Rate Limit Middleware
|
|
274
|
+
|
|
275
|
+
You can limit requests with the built-in `rateLimit` middleware.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { rateLimit, Response } from "skyguard-js";
|
|
279
|
+
|
|
280
|
+
const apiRateLimit = rateLimit({
|
|
281
|
+
windowMs: 60_000, // 1 minute
|
|
282
|
+
max: 100,
|
|
283
|
+
message: "Too many requests from this IP",
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
app.get(
|
|
287
|
+
"/api/users",
|
|
288
|
+
() => {
|
|
289
|
+
return Response.json([{ id: 1 }]);
|
|
290
|
+
},
|
|
291
|
+
[apiRateLimit],
|
|
292
|
+
);
|
|
155
293
|
```
|
|
156
294
|
|
|
157
295
|
---
|
|
@@ -175,33 +313,26 @@ app.staticFiles(join(__dirname, "..", "static"));
|
|
|
175
313
|
To validate the data in the body of client requests, the framework provides the creation of validation schemes and a middleware function to validate the body of HTTP requests, used as follows:
|
|
176
314
|
|
|
177
315
|
```ts
|
|
178
|
-
import { v, schema,
|
|
316
|
+
import { v, schema, validateRequest, json } from "skyguard-js";
|
|
179
317
|
|
|
180
318
|
// Created Schema
|
|
181
319
|
const userSchema = schema({
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
320
|
+
body: {
|
|
321
|
+
name: v.string({ maxLength: 60 }),
|
|
322
|
+
email: v.email(),
|
|
323
|
+
age: v.number({ min: 18 }),
|
|
324
|
+
active: v.boolean().default(false),
|
|
325
|
+
birthdate: v.date({ max: new Date() }),
|
|
326
|
+
},
|
|
187
327
|
});
|
|
188
328
|
|
|
189
|
-
// Typing Interface
|
|
190
|
-
interface User {
|
|
191
|
-
name: string;
|
|
192
|
-
email: string;
|
|
193
|
-
age: number;
|
|
194
|
-
active: boolean;
|
|
195
|
-
birthdate: Date;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
329
|
app.post(
|
|
199
330
|
"/test",
|
|
200
331
|
(request: Request) => {
|
|
201
|
-
const data = request.
|
|
332
|
+
const data = request.body;
|
|
202
333
|
return json(data).setStatusCode(201);
|
|
203
334
|
},
|
|
204
|
-
[
|
|
335
|
+
[validateRequest(userSchema)],
|
|
205
336
|
);
|
|
206
337
|
```
|
|
207
338
|
|
|
@@ -221,7 +352,7 @@ Validation is:
|
|
|
221
352
|
The framework provides a set of built-in HTTP exceptions that can be thrown from route handlers or middleware. When an exception is thrown, the framework detects it and sends an appropriate HTTP response with the status code and message you specified in the class.
|
|
222
353
|
|
|
223
354
|
```ts
|
|
224
|
-
import { NotFoundError, InternalServerError } from "skyguard-js";
|
|
355
|
+
import { NotFoundError, InternalServerError, json } from "skyguard-js";
|
|
225
356
|
|
|
226
357
|
const listResources = ["1", "2", "3"];
|
|
227
358
|
|
|
@@ -232,7 +363,7 @@ app.get("/resource/{id}", (request: Request) => {
|
|
|
232
363
|
throw new NotFoundError("Resource not found");
|
|
233
364
|
}
|
|
234
365
|
|
|
235
|
-
return
|
|
366
|
+
return json(resource);
|
|
236
367
|
});
|
|
237
368
|
|
|
238
369
|
app.get("/divide", (request: Request) => {
|
|
@@ -240,7 +371,7 @@ app.get("/divide", (request: Request) => {
|
|
|
240
371
|
const { a, b } = request.query;
|
|
241
372
|
const result = Number(a) / Number(b);
|
|
242
373
|
|
|
243
|
-
return
|
|
374
|
+
return json({ result });
|
|
244
375
|
} catch (error) {
|
|
245
376
|
throw new InternalServerError(
|
|
246
377
|
"An error occurred while processing your request",
|
|
@@ -256,9 +387,9 @@ app.get("/divide", (request: Request) => {
|
|
|
256
387
|
To handle sessions, you must use the framework’s built-in middleware. Depending on where you want to store them (in memory, in files, or in a database), you need to use the corresponding storage class.
|
|
257
388
|
|
|
258
389
|
```ts
|
|
259
|
-
import { sessions, FileSessionStorage } from "skyguard-js";
|
|
390
|
+
import { sessions, FileSessionStorage, json } from "skyguard-js";
|
|
260
391
|
|
|
261
|
-
app.middlewares(
|
|
392
|
+
app.middlewares(
|
|
262
393
|
sessions(FileSessionStorage, {
|
|
263
394
|
name: "connect.sid",
|
|
264
395
|
rolling: true,
|
|
@@ -271,7 +402,7 @@ app.middlewares([
|
|
|
271
402
|
path: "/",
|
|
272
403
|
},
|
|
273
404
|
}),
|
|
274
|
-
|
|
405
|
+
);
|
|
275
406
|
|
|
276
407
|
app.post("/login", (request: Request) => {
|
|
277
408
|
const { username, password } = request.data;
|
|
@@ -297,6 +428,227 @@ app.get("/me", (request: Request) => {
|
|
|
297
428
|
});
|
|
298
429
|
```
|
|
299
430
|
|
|
431
|
+
For **database-backed sessions**, configure `DatabaseSessionStorage` once with an adapter that maps to your DB client/ORM. This keeps the framework **DB-engine agnostic** (MySQL, MariaDB, SQLite, PostgreSQL, SQL Server, Oracle, etc.).
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import {
|
|
435
|
+
sessions,
|
|
436
|
+
DatabaseSessionStorage,
|
|
437
|
+
type SessionDatabaseAdapter,
|
|
438
|
+
} from "skyguard-js";
|
|
439
|
+
|
|
440
|
+
const sessionAdapter: SessionDatabaseAdapter = {
|
|
441
|
+
async findById(id) {
|
|
442
|
+
// query row by id and return: { data: parsedJson, expiresAt: unixMs }
|
|
443
|
+
return null;
|
|
444
|
+
},
|
|
445
|
+
async upsert(id, payload) {
|
|
446
|
+
// insert/update row depending on your DB driver
|
|
447
|
+
},
|
|
448
|
+
async deleteById(id) {
|
|
449
|
+
// delete row by id
|
|
450
|
+
},
|
|
451
|
+
async deleteExpired(now) {
|
|
452
|
+
// delete rows where expiresAt <= now
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
DatabaseSessionStorage.configure(sessionAdapter);
|
|
457
|
+
|
|
458
|
+
app.middlewares(sessions(DatabaseSessionStorage));
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Concrete DB adapter examples
|
|
462
|
+
|
|
463
|
+
> Suggested table shape (portable across engines):
|
|
464
|
+
>
|
|
465
|
+
> - `id` (string/varchar, primary key)
|
|
466
|
+
> - `data` (JSON/TEXT containing serialized object)
|
|
467
|
+
> - `expires_at` (bigint/timestamp in unix milliseconds)
|
|
468
|
+
|
|
469
|
+
To keep the code cleaner, you should create a separate file where you can configure the database sessions, such as `src/sessions/config.ts`
|
|
470
|
+
|
|
471
|
+
#### Prisma (MySQL / PostgreSQL / SQLite / SQL Server / CockroachDB)
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
import { PrismaClient } from "@prisma/client";
|
|
475
|
+
import {
|
|
476
|
+
DatabaseSessionStorage,
|
|
477
|
+
type SessionDatabaseAdapter,
|
|
478
|
+
} from "skyguard-js";
|
|
479
|
+
|
|
480
|
+
const prisma = new PrismaClient();
|
|
481
|
+
|
|
482
|
+
// model Session {
|
|
483
|
+
// id String @id
|
|
484
|
+
// data String
|
|
485
|
+
// expiresAt BigInt @map("expires_at")
|
|
486
|
+
// @@map("sessions")
|
|
487
|
+
// }
|
|
488
|
+
|
|
489
|
+
const adapter: SessionDatabaseAdapter = {
|
|
490
|
+
async findById(id) {
|
|
491
|
+
const row = await prisma.session.findUnique({ where: { id } });
|
|
492
|
+
if (!row) return null;
|
|
493
|
+
return { data: JSON.parse(row.data), expiresAt: Number(row.expiresAt) };
|
|
494
|
+
},
|
|
495
|
+
async upsert(id, payload) {
|
|
496
|
+
await prisma.session.upsert({
|
|
497
|
+
where: { id },
|
|
498
|
+
update: {
|
|
499
|
+
data: JSON.stringify(payload.data),
|
|
500
|
+
expiresAt: BigInt(payload.expiresAt),
|
|
501
|
+
},
|
|
502
|
+
create: {
|
|
503
|
+
id,
|
|
504
|
+
data: JSON.stringify(payload.data),
|
|
505
|
+
expiresAt: BigInt(payload.expiresAt),
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
},
|
|
509
|
+
async deleteById(id) {
|
|
510
|
+
await prisma.session.deleteMany({ where: { id } });
|
|
511
|
+
},
|
|
512
|
+
async deleteExpired(now) {
|
|
513
|
+
await prisma.session.deleteMany({
|
|
514
|
+
where: { expiresAt: { lte: BigInt(now) } },
|
|
515
|
+
});
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
DatabaseSessionStorage.configure(adapter);
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
#### TypeORM (MySQL / MariaDB / PostgreSQL / SQLite / MSSQL / Oracle)
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
import {
|
|
526
|
+
DataSource,
|
|
527
|
+
Entity,
|
|
528
|
+
Column,
|
|
529
|
+
PrimaryColumn,
|
|
530
|
+
LessThanOrEqual,
|
|
531
|
+
} from "typeorm";
|
|
532
|
+
import {
|
|
533
|
+
DatabaseSessionStorage,
|
|
534
|
+
type SessionDatabaseAdapter,
|
|
535
|
+
} from "skyguard-js";
|
|
536
|
+
|
|
537
|
+
@Entity({ name: "sessions" })
|
|
538
|
+
class SessionEntity {
|
|
539
|
+
@PrimaryColumn({ type: "varchar", length: 64 })
|
|
540
|
+
id!: string;
|
|
541
|
+
|
|
542
|
+
@Column({ type: "text" })
|
|
543
|
+
data!: string;
|
|
544
|
+
|
|
545
|
+
@Column({ name: "expires_at", type: "bigint" })
|
|
546
|
+
expiresAt!: string;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const ds = new DataSource({ /* your db config */ entities: [SessionEntity] });
|
|
550
|
+
await ds.initialize();
|
|
551
|
+
const repo = ds.getRepository(SessionEntity);
|
|
552
|
+
|
|
553
|
+
const adapter: SessionDatabaseAdapter = {
|
|
554
|
+
async findById(id) {
|
|
555
|
+
const row = await repo.findOneBy({ id });
|
|
556
|
+
if (!row) return null;
|
|
557
|
+
return { data: JSON.parse(row.data), expiresAt: Number(row.expiresAt) };
|
|
558
|
+
},
|
|
559
|
+
async upsert(id, payload) {
|
|
560
|
+
await repo.save({
|
|
561
|
+
id,
|
|
562
|
+
data: JSON.stringify(payload.data),
|
|
563
|
+
expiresAt: String(payload.expiresAt),
|
|
564
|
+
});
|
|
565
|
+
},
|
|
566
|
+
async deleteById(id) {
|
|
567
|
+
await repo.delete({ id });
|
|
568
|
+
},
|
|
569
|
+
async deleteExpired(now) {
|
|
570
|
+
await repo.delete({ expiresAt: LessThanOrEqual(String(now)) });
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
DatabaseSessionStorage.configure(adapter);
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### mysql2 (MySQL)
|
|
578
|
+
|
|
579
|
+
```ts
|
|
580
|
+
import mysql from "mysql2/promise";
|
|
581
|
+
import {
|
|
582
|
+
DatabaseSessionStorage,
|
|
583
|
+
type SessionDatabaseAdapter,
|
|
584
|
+
} from "skyguard-js";
|
|
585
|
+
|
|
586
|
+
const pool = mysql.createPool({ uri: process.env.DATABASE_URL });
|
|
587
|
+
|
|
588
|
+
const adapter: SessionDatabaseAdapter = {
|
|
589
|
+
async findById(id) {
|
|
590
|
+
const [rows] = await pool.query<any[]>(
|
|
591
|
+
"SELECT data, expires_at FROM sessions WHERE id = ? LIMIT 1",
|
|
592
|
+
[id],
|
|
593
|
+
);
|
|
594
|
+
const row = rows[0];
|
|
595
|
+
if (!row) return null;
|
|
596
|
+
return { data: JSON.parse(row.data), expiresAt: Number(row.expires_at) };
|
|
597
|
+
},
|
|
598
|
+
async upsert(id, payload) {
|
|
599
|
+
await pool.query(
|
|
600
|
+
`INSERT INTO sessions (id, data, expires_at) VALUES (?, ?, ?)
|
|
601
|
+
ON DUPLICATE KEY UPDATE data = VALUES(data), expires_at = VALUES(expires_at)`,
|
|
602
|
+
[id, JSON.stringify(payload.data), payload.expiresAt],
|
|
603
|
+
);
|
|
604
|
+
},
|
|
605
|
+
async deleteById(id) {
|
|
606
|
+
await pool.query("DELETE FROM sessions WHERE id = ?", [id]);
|
|
607
|
+
},
|
|
608
|
+
async deleteExpired(now) {
|
|
609
|
+
await pool.query("DELETE FROM sessions WHERE expires_at <= ?", [now]);
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
DatabaseSessionStorage.configure(adapter);
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
#### sqlite3 (SQLite)
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
import sqlite3 from "sqlite3";
|
|
620
|
+
import { open } from "sqlite";
|
|
621
|
+
import { type SessionDatabaseAdapter } from "skyguard-js";
|
|
622
|
+
|
|
623
|
+
const db = await open({ filename: "./sessions.db", driver: sqlite3.Database });
|
|
624
|
+
|
|
625
|
+
const adapter: SessionDatabaseAdapter = {
|
|
626
|
+
async findById(id) {
|
|
627
|
+
const row = await db.get<{ data: string; expires_at: number }>(
|
|
628
|
+
"SELECT data, expires_at FROM sessions WHERE id = ? LIMIT 1",
|
|
629
|
+
[id],
|
|
630
|
+
);
|
|
631
|
+
if (!row) return null;
|
|
632
|
+
return { data: JSON.parse(row.data), expiresAt: Number(row.expires_at) };
|
|
633
|
+
},
|
|
634
|
+
async upsert(id, payload) {
|
|
635
|
+
await db.run(
|
|
636
|
+
`INSERT INTO sessions (id, data, expires_at) VALUES (?, ?, ?)
|
|
637
|
+
ON CONFLICT(id) DO UPDATE SET data = excluded.data, expires_at = excluded.expires_at`,
|
|
638
|
+
[id, JSON.stringify(payload.data), payload.expiresAt],
|
|
639
|
+
);
|
|
640
|
+
},
|
|
641
|
+
async deleteById(id) {
|
|
642
|
+
await db.run("DELETE FROM sessions WHERE id = ?", [id]);
|
|
643
|
+
},
|
|
644
|
+
async deleteExpired(now) {
|
|
645
|
+
await db.run("DELETE FROM sessions WHERE expires_at <= ?", [now]);
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
DatabaseSessionStorage.configure(adapter);
|
|
650
|
+
```
|
|
651
|
+
|
|
300
652
|
---
|
|
301
653
|
|
|
302
654
|
## 🛡️ Security
|
|
@@ -304,7 +656,7 @@ app.get("/me", (request: Request) => {
|
|
|
304
656
|
The framework includes some password hashing and JWT token generation functions, and also includes JWT authentication middleware.
|
|
305
657
|
|
|
306
658
|
```ts
|
|
307
|
-
import { Hasher, JWT } from "skyguard-js";
|
|
659
|
+
import { Hasher, JWT, json } from "skyguard-js";
|
|
308
660
|
|
|
309
661
|
app.post("/register", async (request: Request) => {
|
|
310
662
|
const { username, password } = request.data;
|
|
@@ -313,7 +665,7 @@ app.post("/register", async (request: Request) => {
|
|
|
313
665
|
// Save username and hashedPassword to database
|
|
314
666
|
// ...
|
|
315
667
|
|
|
316
|
-
return
|
|
668
|
+
return json({ message: "User registered" });
|
|
317
669
|
});
|
|
318
670
|
|
|
319
671
|
app.post("/login", async (request: Request) => {
|
|
@@ -333,7 +685,7 @@ app.post("/login", async (request: Request) => {
|
|
|
333
685
|
expiresIn: "1h",
|
|
334
686
|
});
|
|
335
687
|
|
|
336
|
-
return
|
|
688
|
+
return json({ token });
|
|
337
689
|
});
|
|
338
690
|
```
|
|
339
691
|
|
|
@@ -344,7 +696,7 @@ app.post("/login", async (request: Request) => {
|
|
|
344
696
|
To handle file uploads, use the built-in `createUploader` function to create an uploader middleware with the desired storage configuration.
|
|
345
697
|
|
|
346
698
|
```ts
|
|
347
|
-
import { createUploader, StorageType } from "skyguard-js";
|
|
699
|
+
import { createUploader, StorageType, json } from "skyguard-js";
|
|
348
700
|
|
|
349
701
|
const uploader = createUploader({
|
|
350
702
|
storageType: StorageType.DISK,
|
|
@@ -358,7 +710,7 @@ const uploader = createUploader({
|
|
|
358
710
|
app.post(
|
|
359
711
|
"/upload",
|
|
360
712
|
(request: Request) => {
|
|
361
|
-
return
|
|
713
|
+
return json({
|
|
362
714
|
message: "File uploaded successfully",
|
|
363
715
|
file: request.file,
|
|
364
716
|
});
|
|
@@ -379,6 +731,7 @@ To render views, you must first set up the template engine using the `engineTemp
|
|
|
379
731
|
import { engine } from "express-handlebars";
|
|
380
732
|
import ejs from "ejs";
|
|
381
733
|
import { join } from "node:path";
|
|
734
|
+
import { render } from "skyguard-js";
|
|
382
735
|
|
|
383
736
|
app.views(__dirname, "views");
|
|
384
737
|
|
package/dist/app.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RouterGroup } from "./routing";
|
|
2
|
+
import { type LogFormat } from "./http";
|
|
2
3
|
import type { Middleware, RouteHandler } from "./types";
|
|
3
4
|
import { type TemplateEngineFunction } from "./views/engineTemplate";
|
|
4
5
|
/**
|
|
@@ -19,13 +20,15 @@ import { type TemplateEngineFunction } from "./views/engineTemplate";
|
|
|
19
20
|
* from the runtime platform (Node, Bun, Deno, etc.)
|
|
20
21
|
* through {@link HttpAdapter} and {@link Server}.
|
|
21
22
|
*/
|
|
22
|
-
|
|
23
|
+
declare class App {
|
|
23
24
|
/** Main routing system */
|
|
24
25
|
private router;
|
|
25
26
|
/** Static file handler (optional) */
|
|
26
27
|
private staticFileHandler;
|
|
27
28
|
/** View engine for rendering templates (optional) */
|
|
28
29
|
private viewEngine;
|
|
30
|
+
/** Logger configuration */
|
|
31
|
+
private loggerOptions;
|
|
29
32
|
/**
|
|
30
33
|
* Bootstraps and configures the application.
|
|
31
34
|
*
|
|
@@ -107,6 +110,21 @@ export declare class App {
|
|
|
107
110
|
* @param port - TCP port to listen on
|
|
108
111
|
*/
|
|
109
112
|
run(port: number, callback: VoidFunction, hostname?: string): void;
|
|
113
|
+
/**
|
|
114
|
+
* Configures HTTP request logger output format and optional file output.
|
|
115
|
+
*
|
|
116
|
+
* Supported formats are inspired by morgan:
|
|
117
|
+
* - "combined"
|
|
118
|
+
* - "common"
|
|
119
|
+
* - "dev"
|
|
120
|
+
* - "short"
|
|
121
|
+
* - "tiny"
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* app.logger("common");
|
|
125
|
+
* app.logger("combined", "./logs/http.log");
|
|
126
|
+
*/
|
|
127
|
+
logger(format?: LogFormat, filePath?: string): void;
|
|
110
128
|
/**
|
|
111
129
|
* Sets a global prefix for all routes.
|
|
112
130
|
*
|
|
@@ -140,7 +158,7 @@ export declare class App {
|
|
|
140
158
|
*
|
|
141
159
|
* app.middlewares(auth);
|
|
142
160
|
*/
|
|
143
|
-
middlewares(middlewares: Middleware[]): void;
|
|
161
|
+
middlewares(...middlewares: Middleware[]): void;
|
|
144
162
|
/**
|
|
145
163
|
* Creates a route group with a shared prefix.
|
|
146
164
|
*
|
|
@@ -159,3 +177,4 @@ export declare class App {
|
|
|
159
177
|
private handleError;
|
|
160
178
|
}
|
|
161
179
|
export declare const createApp: () => App;
|
|
180
|
+
export {};
|