skyguard-js 1.2.1 → 1.2.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 +9 -683
- package/dist/app.d.ts +14 -9
- package/dist/app.js +27 -24
- package/dist/http/context.d.ts +115 -0
- package/dist/http/context.js +147 -0
- package/dist/http/httpAdapter.d.ts +4 -4
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +3 -1
- package/dist/http/nodeNativeHttp.d.ts +4 -4
- package/dist/http/nodeNativeHttp.js +11 -4
- package/dist/http/request.d.ts +4 -0
- package/dist/http/request.js +8 -0
- package/dist/http/response.d.ts +21 -2
- package/dist/http/response.js +30 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middlewares/cors.d.ts +10 -4
- package/dist/middlewares/cors.js +35 -18
- package/dist/middlewares/csrf.js +33 -33
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/rateLimiter.d.ts +58 -6
- package/dist/middlewares/rateLimiter.js +149 -40
- package/dist/middlewares/session.js +4 -4
- package/dist/routing/routeResolveFunc.d.ts +10 -0
- package/dist/routing/routeResolveFunc.js +21 -0
- package/dist/routing/router.d.ts +16 -10
- package/dist/routing/router.js +32 -25
- package/dist/routing/routerGroup.d.ts +10 -5
- package/dist/routing/routerGroup.js +11 -10
- package/dist/storage/storage.d.ts +3 -3
- package/dist/storage/storage.js +7 -7
- package/dist/storage/types.d.ts +5 -5
- package/dist/storage/uploader.d.ts +9 -9
- package/dist/storage/uploader.js +62 -62
- package/dist/types/index.d.ts +11 -10
- package/dist/validators/validationSchema.js +8 -8
- package/package.json +2 -2
- package/dist/helpers/http.d.ts +0 -95
- package/dist/helpers/http.js +0 -112
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Skyguard.js currently delivers a solid core that includes **routing**, **type-sa
|
|
|
31
31
|
- HTTP routing by method (GET, POST, PUT, PATCH, DELETE)
|
|
32
32
|
- Route groups with prefixes
|
|
33
33
|
- Global, group, and route-level middlewares
|
|
34
|
-
-
|
|
34
|
+
- Unified `Context` abstraction (`ctx.req` + response helpers)
|
|
35
35
|
- Declarative data validation
|
|
36
36
|
- Support for template motors (handlebars, pugs, ejs, etc.)
|
|
37
37
|
- Built-in HTTP exceptions
|
|
@@ -73,700 +73,22 @@ npm install skyguard-js
|
|
|
73
73
|
## 🏁 Quick Start
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
|
-
import { createApp
|
|
76
|
+
import { createApp } from "skyguard-js";
|
|
77
77
|
|
|
78
78
|
const app = createApp();
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
app.get("/", ctx => ctx.json({ status: "ok" }));
|
|
81
81
|
|
|
82
|
-
app.
|
|
83
|
-
return Response.json({ status: "ok" });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
app.run(PORT, () => {
|
|
87
|
-
console.log(`Server running in port: http://localhost:${PORT}`);
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## 🛣️ Routing
|
|
94
|
-
|
|
95
|
-
Routes are registered using HTTP methods on the `app` instance.
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
app.get("/posts/{id}", (request: Request) => {
|
|
99
|
-
return Response.json(request.params);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
app.post("/posts", (request: Request) => {
|
|
103
|
-
return Response.json(request.data);
|
|
104
|
-
});
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Internally, the framework maps HTTP methods to route layers using an optimized routing table.
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## 🧩 Route Groups
|
|
112
|
-
|
|
113
|
-
Route groups allow you to organize endpoints under a shared prefix.
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
app.group("/api", api => {
|
|
117
|
-
api.get("/users", () => res.json({ message: "Users" }));
|
|
118
|
-
api.get("/products", () => res.json({ message: "Products" }));
|
|
119
|
-
});
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
## 🛠️ Middlewares
|
|
125
|
-
|
|
126
|
-
Middlewares can be registered **globally**, **per group**, or **per route**.
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
import { Request, Response, json, RouteHandler } from "skyguard-js";
|
|
130
|
-
|
|
131
|
-
const authMiddleware = async (
|
|
132
|
-
request: Request,
|
|
133
|
-
next: RouteHandler,
|
|
134
|
-
): Promise<Response> => {
|
|
135
|
-
if (request.headers["authorization"] !== "secret") {
|
|
136
|
-
return json({ message: "Unauthorized" }).setStatus(401);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return next(request);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// Global middleware
|
|
143
|
-
app.middlewares(authMiddleware);
|
|
144
|
-
|
|
145
|
-
// Group middleware
|
|
146
|
-
app.group("/admin", admin => {
|
|
147
|
-
admin.middlewares(authMiddleware);
|
|
148
|
-
admin.get("/dashboard", () => json({ ok: true }));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Route-level middleware
|
|
152
|
-
app.get("/secure", () => json({ secure: true }), [authMiddleware]);
|
|
82
|
+
app.run();
|
|
153
83
|
```
|
|
154
84
|
|
|
155
85
|
---
|
|
156
86
|
|
|
157
|
-
## 🌐 CORS Middleware
|
|
158
|
-
|
|
159
|
-
To enable CORS, use the built-in `cors` middleware.
|
|
160
|
-
|
|
161
|
-
```ts
|
|
162
|
-
import { cors, HttpMethods } from "skyguard-js";
|
|
163
|
-
|
|
164
|
-
app.middlewares(
|
|
165
|
-
cors({
|
|
166
|
-
origin: ["http://localhost:3000", "https://myapp.com"],
|
|
167
|
-
methods: [HttpMethods.get, HttpMethods.post],
|
|
168
|
-
allowedHeaders: ["Content-Type", "Authorization"],
|
|
169
|
-
credentials: true,
|
|
170
|
-
}),
|
|
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
|
-
);
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
---
|
|
296
|
-
|
|
297
|
-
## 📌 Static Files
|
|
298
|
-
|
|
299
|
-
To serve static files, use the application's `staticFiles` method with the directory path. The name of the folder will determine the initial route prefix.
|
|
300
|
-
|
|
301
|
-
```ts
|
|
302
|
-
import { join } from "node:path";
|
|
303
|
-
|
|
304
|
-
app.staticFiles(join(__dirname, "..", "static"));
|
|
305
|
-
|
|
306
|
-
// Route http://localhost:3000/static/style.css will serve the file located at ./static/style.css
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
---
|
|
310
|
-
|
|
311
|
-
## ⛔ Data Validation
|
|
312
|
-
|
|
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:
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
import { v, schema, validateRequest, json } from "skyguard-js";
|
|
317
|
-
|
|
318
|
-
// Created Schema
|
|
319
|
-
const userSchema = schema({
|
|
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
|
-
},
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
app.post(
|
|
330
|
-
"/test",
|
|
331
|
-
(request: Request) => {
|
|
332
|
-
const data = request.body;
|
|
333
|
-
return json(data).setStatusCode(201);
|
|
334
|
-
},
|
|
335
|
-
[validateRequest(userSchema)],
|
|
336
|
-
);
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
To type the request body, an interface is used and the .getData() method is used, which allows returning the typed bodym. By default each property you define in the schema is required, to define it optional you use the `.optional()` or `.default(value)` function
|
|
340
|
-
|
|
341
|
-
Validation is:
|
|
342
|
-
|
|
343
|
-
- Fail-fast per field
|
|
344
|
-
- Fully typed
|
|
345
|
-
- Reusable
|
|
346
|
-
- Decoupled from transport layer
|
|
347
|
-
|
|
348
|
-
---
|
|
349
|
-
|
|
350
|
-
## 🚨 Exceptions & Error Handling
|
|
351
|
-
|
|
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.
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
import { NotFoundError, InternalServerError, json } from "skyguard-js";
|
|
356
|
-
|
|
357
|
-
const listResources = ["1", "2", "3"];
|
|
358
|
-
|
|
359
|
-
app.get("/resource/{id}", (request: Request) => {
|
|
360
|
-
const resource = request.params["id"];
|
|
361
|
-
|
|
362
|
-
if (!listResources.includes(resource)) {
|
|
363
|
-
throw new NotFoundError("Resource not found");
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return json(resource);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
app.get("/divide", (request: Request) => {
|
|
370
|
-
try {
|
|
371
|
-
const { a, b } = request.query;
|
|
372
|
-
const result = Number(a) / Number(b);
|
|
373
|
-
|
|
374
|
-
return json({ result });
|
|
375
|
-
} catch (error) {
|
|
376
|
-
throw new InternalServerError(
|
|
377
|
-
"An error occurred while processing your request",
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
---
|
|
384
|
-
|
|
385
|
-
## 🧱 Sessions
|
|
386
|
-
|
|
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.
|
|
388
|
-
|
|
389
|
-
```ts
|
|
390
|
-
import { sessions, FileSessionStorage, json } from "skyguard-js";
|
|
391
|
-
|
|
392
|
-
app.middlewares(
|
|
393
|
-
sessions(FileSessionStorage, {
|
|
394
|
-
name: "connect.sid",
|
|
395
|
-
rolling: true,
|
|
396
|
-
saveUninitialized: false,
|
|
397
|
-
cookie: {
|
|
398
|
-
maxAge: 60 * 60 * 24,
|
|
399
|
-
httpOnly: true,
|
|
400
|
-
sameSite: "Lax",
|
|
401
|
-
secure: false,
|
|
402
|
-
path: "/",
|
|
403
|
-
},
|
|
404
|
-
}),
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
app.post("/login", (request: Request) => {
|
|
408
|
-
const { username, password } = request.data;
|
|
409
|
-
|
|
410
|
-
if (username === "admin" && password === "secret") {
|
|
411
|
-
request.session.set("user", {
|
|
412
|
-
id: 1,
|
|
413
|
-
username: "admin",
|
|
414
|
-
role: "admin",
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
return json({ message: "Logged in" });
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
throw new UnauthorizedError("Invalid credentials");
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
app.get("/me", (request: Request) => {
|
|
424
|
-
const user = request.session.get("user");
|
|
425
|
-
|
|
426
|
-
if (!user) throw new UnauthorizedError("Not authenticated");
|
|
427
|
-
return json({ user });
|
|
428
|
-
});
|
|
429
|
-
```
|
|
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
|
-
|
|
652
|
-
---
|
|
653
|
-
|
|
654
|
-
## 🛡️ Security
|
|
655
|
-
|
|
656
|
-
The framework includes some password hashing and JWT token generation functions, and also includes JWT authentication middleware.
|
|
657
|
-
|
|
658
|
-
```ts
|
|
659
|
-
import { Hasher, JWT, json } from "skyguard-js";
|
|
660
|
-
|
|
661
|
-
app.post("/register", async (request: Request) => {
|
|
662
|
-
const { username, password } = request.data;
|
|
663
|
-
const hashedPassword = await Hasher.hash(password);
|
|
664
|
-
|
|
665
|
-
// Save username and hashedPassword to database
|
|
666
|
-
// ...
|
|
667
|
-
|
|
668
|
-
return json({ message: "User registered" });
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
app.post("/login", async (request: Request) => {
|
|
672
|
-
const { username, password } = request.data;
|
|
673
|
-
|
|
674
|
-
// Retrieve user from database by username
|
|
675
|
-
// ...
|
|
676
|
-
|
|
677
|
-
const isValid = await Hasher.verify(password, user.hashedPassword);
|
|
678
|
-
|
|
679
|
-
if (!isValid) {
|
|
680
|
-
throw new UnauthorizedError("Invalid credentials");
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const token = JWT.create({ sub: "123" }, "secret-key", {
|
|
684
|
-
algorithm: "HS256",
|
|
685
|
-
expiresIn: "1h",
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
return json({ token });
|
|
689
|
-
});
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
---
|
|
693
|
-
|
|
694
|
-
## 📂 File Uploads
|
|
695
|
-
|
|
696
|
-
To handle file uploads, use the built-in `createUploader` function to create an uploader middleware with the desired storage configuration.
|
|
697
|
-
|
|
698
|
-
```ts
|
|
699
|
-
import { createUploader, StorageType, json } from "skyguard-js";
|
|
700
|
-
|
|
701
|
-
const uploader = createUploader({
|
|
702
|
-
storageType: StorageType.DISK,
|
|
703
|
-
storageOptions: {
|
|
704
|
-
disk: {
|
|
705
|
-
destination: "./uploads",
|
|
706
|
-
},
|
|
707
|
-
},
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
app.post(
|
|
711
|
-
"/upload",
|
|
712
|
-
(request: Request) => {
|
|
713
|
-
return json({
|
|
714
|
-
message: "File uploaded successfully",
|
|
715
|
-
file: request.file,
|
|
716
|
-
});
|
|
717
|
-
},
|
|
718
|
-
[uploader.single("file")],
|
|
719
|
-
);
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
Depending on the `Storage Type` you have selected, the storage options will contain two properties: `disk` and `memory`
|
|
723
|
-
|
|
724
|
-
---
|
|
725
|
-
|
|
726
|
-
## 📄 Views & Template Engine
|
|
727
|
-
|
|
728
|
-
To render views, you must first set up the template engine using the `engineTemplates` method of the `app`, set the view path with the `views` method of the `app`, and then you can use the `render` method within your handlers to render the views with the data you want to pass.
|
|
729
|
-
|
|
730
|
-
```ts
|
|
731
|
-
import { engine } from "express-handlebars";
|
|
732
|
-
import ejs from "ejs";
|
|
733
|
-
import { join } from "node:path";
|
|
734
|
-
import { render } from "skyguard-js";
|
|
735
|
-
|
|
736
|
-
app.views(__dirname, "views");
|
|
737
|
-
|
|
738
|
-
// Config for Express Handlebars
|
|
739
|
-
app.engineTemplates(
|
|
740
|
-
"hbs",
|
|
741
|
-
engine({
|
|
742
|
-
extname: "hbs",
|
|
743
|
-
layoutsDir: join(__dirname, "views"),
|
|
744
|
-
defaultLayout: "main",
|
|
745
|
-
}),
|
|
746
|
-
);
|
|
747
|
-
|
|
748
|
-
// Config for EJS
|
|
749
|
-
app.engineTemplates("ejs", (templatePath, data) => {
|
|
750
|
-
return ejs.renderFile(templatePath, data);
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
app.get("/home", () => {
|
|
754
|
-
return render("index", {
|
|
755
|
-
title: "Home Page",
|
|
756
|
-
message: "Welcome to the home page!",
|
|
757
|
-
});
|
|
758
|
-
});
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
Currently, it works with third-party template engines such as **Express Handlebars**, **Pug**, and **EJS**, but the idea is to implement its own template engine in the future.
|
|
762
|
-
|
|
763
|
-
---
|
|
764
|
-
|
|
765
87
|
## 🔮 Roadmap (Tentative)
|
|
766
88
|
|
|
767
89
|
- Middleware system (✅)
|
|
768
90
|
- Template engines supported (✅)
|
|
769
|
-
-
|
|
91
|
+
- Context abstraction (✅)
|
|
770
92
|
- Data validation (✅)
|
|
771
93
|
- Error handling improvements (✅)
|
|
772
94
|
- Sessions & cookies (✅)
|
|
@@ -775,3 +97,7 @@ Currently, it works with third-party template engines such as **Express Handleba
|
|
|
775
97
|
- Database & ORM integration
|
|
776
98
|
- Authentication & authorization
|
|
777
99
|
- WebSockets
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|