adorn-api 1.1.12 → 1.1.14
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 +614 -913
- package/dist/adapter/express/controllers.d.ts +3 -1
- package/dist/adapter/express/controllers.js +4 -1
- package/dist/adapter/express/index.js +5 -1
- package/dist/adapter/express/types.d.ts +3 -0
- package/dist/adapter/fastify/controllers.d.ts +3 -1
- package/dist/adapter/fastify/controllers.js +2 -25
- package/dist/adapter/fastify/index.js +7 -1
- package/dist/adapter/fastify/types.d.ts +3 -0
- package/dist/adapter/metal-orm/index.d.ts +1 -1
- package/dist/adapter/metal-orm/types.d.ts +23 -0
- package/dist/adapter/native/controllers.d.ts +3 -0
- package/dist/adapter/native/controllers.js +2 -25
- package/dist/adapter/native/index.js +14 -1
- package/dist/adapter/native/types.d.ts +3 -0
- package/dist/core/auth.d.ts +33 -3
- package/dist/core/auth.js +74 -22
- package/dist/core/openapi.d.ts +2 -0
- package/dist/core/openapi.js +19 -1
- package/examples/bearer-auth-swagger/app.ts +28 -0
- package/examples/bearer-auth-swagger/auth.controller.ts +45 -0
- package/examples/bearer-auth-swagger/index.ts +20 -0
- package/examples/bearer-auth-swagger/session.dtos.ts +19 -0
- package/package.json +3 -1
- package/src/adapter/express/controllers.ts +23 -18
- package/src/adapter/express/index.ts +12 -1
- package/src/adapter/express/types.ts +13 -10
- package/src/adapter/fastify/controllers.ts +16 -41
- package/src/adapter/fastify/index.ts +27 -13
- package/src/adapter/fastify/types.ts +13 -10
- package/src/adapter/metal-orm/index.ts +3 -0
- package/src/adapter/metal-orm/types.ts +25 -0
- package/src/adapter/native/controllers.ts +16 -41
- package/src/adapter/native/index.ts +28 -15
- package/src/adapter/native/types.ts +13 -10
- package/src/core/auth.ts +134 -56
- package/src/core/openapi.ts +22 -1
- package/tests/e2e/bearer-auth.e2e.test.ts +158 -0
- package/tests/typecheck/query-params.typecheck.ts +42 -0
- package/tests/unit/auth.test.ts +96 -12
- package/tests/unit/openapi-parameters.test.ts +54 -6
- package/tsconfig.typecheck.json +8 -0
- package/vitest.config.ts +47 -7
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createApp } from "./app";
|
|
2
|
+
import { startExampleServer } from "../utils/start-server";
|
|
3
|
+
|
|
4
|
+
async function start() {
|
|
5
|
+
const app = await createApp();
|
|
6
|
+
startExampleServer(app, {
|
|
7
|
+
name: "Bearer Auth Swagger Demo",
|
|
8
|
+
port: 3001,
|
|
9
|
+
extraLogs: [
|
|
10
|
+
(port) => `Swagger UI: http://localhost:${port}/docs`,
|
|
11
|
+
(port) => `OpenAPI JSON: http://localhost:${port}/openapi.json`,
|
|
12
|
+
() => "Try tokens in Swagger Authorize: user-token or admin-token"
|
|
13
|
+
]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
start().catch((err) => {
|
|
18
|
+
console.error(err);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Dto, Field, t } from "../../src";
|
|
2
|
+
|
|
3
|
+
@Dto({ description: "Authenticated user session returned by protected routes." })
|
|
4
|
+
export class SessionDto {
|
|
5
|
+
@Field(t.string({ description: "User identifier." }))
|
|
6
|
+
id!: string;
|
|
7
|
+
|
|
8
|
+
@Field(t.array(t.string(), { description: "Roles granted to the bearer token." }))
|
|
9
|
+
roles!: string[];
|
|
10
|
+
|
|
11
|
+
@Field(t.string({ description: "Human-readable route result." }))
|
|
12
|
+
message!: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Dto({ description: "Public health response." })
|
|
16
|
+
export class PublicStatusDto {
|
|
17
|
+
@Field(t.string())
|
|
18
|
+
status!: string;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adorn-api",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "Decorator-first web framework with OpenAPI 3.1 schema generation.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"pretest": "npm run build",
|
|
9
10
|
"dev": "tsx examples/basic/index.ts",
|
|
10
11
|
"test": "vitest run",
|
|
11
12
|
"test:watch": "vitest",
|
|
13
|
+
"typecheck:tests": "tsc -p tsconfig.typecheck.json --noEmit",
|
|
12
14
|
"example": "node scripts/run-example.js",
|
|
13
15
|
"check": "tsc -p tsconfig.json --noEmit && npm test",
|
|
14
16
|
"lint": "eslint . --ext .ts,.tsx,.js"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Express, Request, Response, NextFunction } from "express";
|
|
2
|
-
import type { Constructor } from "../../core/types";
|
|
3
|
-
import type { SchemaSource } from "../../core/schema";
|
|
4
|
-
import { getControllerMeta } from "../../core/metadata";
|
|
5
|
-
import {
|
|
2
|
+
import type { Constructor } from "../../core/types";
|
|
3
|
+
import type { SchemaSource } from "../../core/schema";
|
|
4
|
+
import { getControllerMeta } from "../../core/metadata";
|
|
5
|
+
import { assertRouteAuthorized, getRouteAuthMeta } from "../../core/auth";
|
|
6
|
+
import { isHttpError, type HttpError } from "../../core/errors";
|
|
6
7
|
import { isHttpResponse } from "../../core/response";
|
|
7
8
|
import type { InputCoercionSetting, MultipartOptions, RequestContext, ValidationOptions } from "./types";
|
|
8
9
|
import { createInputCoercer } from "./coercion";
|
|
@@ -25,13 +26,14 @@ import { ValidationErrors, isValidationErrors } from "../../core/validation-erro
|
|
|
25
26
|
* @param inputCoercion - Input coercion setting
|
|
26
27
|
* @param multipart - Multipart file upload configuration
|
|
27
28
|
*/
|
|
28
|
-
export async function attachControllers(
|
|
29
|
-
app: Express,
|
|
30
|
-
controllers: Constructor[],
|
|
31
|
-
inputCoercion: InputCoercionSetting = "safe",
|
|
32
|
-
multipart?: boolean | MultipartOptions,
|
|
33
|
-
validation?: boolean | ValidationOptions
|
|
34
|
-
|
|
29
|
+
export async function attachControllers(
|
|
30
|
+
app: Express,
|
|
31
|
+
controllers: Constructor[],
|
|
32
|
+
inputCoercion: InputCoercionSetting = "safe",
|
|
33
|
+
multipart?: boolean | MultipartOptions,
|
|
34
|
+
validation?: boolean | ValidationOptions,
|
|
35
|
+
auth?: { userProperty?: string }
|
|
36
|
+
): Promise<void> {
|
|
35
37
|
const multipartOptions = normalizeMultipartOptions(multipart);
|
|
36
38
|
for (const controller of controllers) {
|
|
37
39
|
const meta = getControllerMeta(controller);
|
|
@@ -68,13 +70,16 @@ export async function attachControllers(
|
|
|
68
70
|
middlewares.push(createMultipartMiddleware(route.files!, multipartOptions));
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
// Determine if validation is enabled for this route
|
|
72
|
-
const isValidationEnabled = validation !== false && (validation as ValidationOptions)?.enabled !== false;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
// Determine if validation is enabled for this route
|
|
74
|
+
const isValidationEnabled = validation !== false && (validation as ValidationOptions)?.enabled !== false;
|
|
75
|
+
const authMeta = getRouteAuthMeta(controller, route.handlerName);
|
|
76
|
+
|
|
77
|
+
// Main route handler
|
|
78
|
+
const routeHandler = async (req: Request, res: Response, next: NextFunction) => {
|
|
79
|
+
try {
|
|
80
|
+
await assertRouteAuthorized(authMeta, req, auth);
|
|
81
|
+
|
|
82
|
+
const files = extractFiles(req);
|
|
78
83
|
|
|
79
84
|
// Create context
|
|
80
85
|
const ctx = {
|
|
@@ -4,6 +4,7 @@ import { attachCors } from "./cors";
|
|
|
4
4
|
import { attachControllers } from "./controllers";
|
|
5
5
|
import { attachOpenApi } from "./openapi";
|
|
6
6
|
import { lifecycleRegistry } from "../../core/lifecycle";
|
|
7
|
+
import { createBearerAuthMiddleware } from "../../core/auth";
|
|
7
8
|
|
|
8
9
|
export * from "./types";
|
|
9
10
|
export { attachCors } from "./cors";
|
|
@@ -23,8 +24,18 @@ export async function createExpressApp(options: ExpressAdapterOptions): Promise<
|
|
|
23
24
|
if (options.jsonBody ?? true) {
|
|
24
25
|
app.use(express.json({ limit: options.jsonLimit }));
|
|
25
26
|
}
|
|
27
|
+
if (options.bearerAuth) {
|
|
28
|
+
app.use(createBearerAuthMiddleware(options.bearerAuth));
|
|
29
|
+
}
|
|
26
30
|
const inputCoercion = options.inputCoercion ?? "safe";
|
|
27
|
-
await attachControllers(
|
|
31
|
+
await attachControllers(
|
|
32
|
+
app,
|
|
33
|
+
options.controllers,
|
|
34
|
+
inputCoercion,
|
|
35
|
+
options.multipart,
|
|
36
|
+
options.validation,
|
|
37
|
+
{ userProperty: options.bearerAuth?.userProperty }
|
|
38
|
+
);
|
|
28
39
|
if (options.openApi) {
|
|
29
40
|
attachOpenApi(app, options.controllers, options.openApi);
|
|
30
41
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Constructor,
|
|
3
|
-
RequestContext as CoreRequestContext,
|
|
4
|
-
UploadedFileInfo
|
|
5
|
-
} from "../../core/types";
|
|
6
|
-
import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
|
|
1
|
+
import type {
|
|
2
|
+
Constructor,
|
|
3
|
+
RequestContext as CoreRequestContext,
|
|
4
|
+
UploadedFileInfo
|
|
5
|
+
} from "../../core/types";
|
|
6
|
+
import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
|
|
7
|
+
import type { BearerAuthOptions } from "../../core/auth";
|
|
7
8
|
|
|
8
9
|
export { UploadedFileInfo };
|
|
9
10
|
|
|
@@ -112,10 +113,12 @@ export interface ExpressAdapterOptions {
|
|
|
112
113
|
openApi?: OpenApiExpressOptions;
|
|
113
114
|
/** Input coercion setting */
|
|
114
115
|
inputCoercion?: InputCoercionSetting;
|
|
115
|
-
/** CORS configuration. Set to true for permissive defaults, or provide options. */
|
|
116
|
-
cors?: boolean | CorsOptions;
|
|
117
|
-
/**
|
|
118
|
-
|
|
116
|
+
/** CORS configuration. Set to true for permissive defaults, or provide options. */
|
|
117
|
+
cors?: boolean | CorsOptions;
|
|
118
|
+
/** Built-in bearer token authentication for protected routes. */
|
|
119
|
+
bearerAuth?: BearerAuthOptions;
|
|
120
|
+
/** Multipart file upload configuration. Set to true for defaults, or provide options. */
|
|
121
|
+
multipart?: boolean | MultipartOptions;
|
|
119
122
|
/** Validation configuration. Set to false to disable validation, or provide options. */
|
|
120
123
|
validation?: boolean | ValidationOptions;
|
|
121
124
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
|
2
|
-
import type { Constructor, RequestContext } from "../../core/types";
|
|
3
|
-
import type { SchemaSource } from "../../core/schema";
|
|
4
|
-
import { getControllerMeta } from "../../core/metadata";
|
|
5
|
-
import { getRouteAuthMeta } from "../../core/auth";
|
|
6
|
-
import { isHttpError
|
|
2
|
+
import type { Constructor, RequestContext } from "../../core/types";
|
|
3
|
+
import type { SchemaSource } from "../../core/schema";
|
|
4
|
+
import { getControllerMeta } from "../../core/metadata";
|
|
5
|
+
import { assertRouteAuthorized, getRouteAuthMeta } from "../../core/auth";
|
|
6
|
+
import { isHttpError } from "../../core/errors";
|
|
7
7
|
import { isHttpResponse } from "../../core/response";
|
|
8
8
|
import type { InputCoercionSetting, MultipartOptions, ValidationOptions } from "./types";
|
|
9
9
|
import { createInputCoercer } from "./coercion";
|
|
@@ -21,13 +21,14 @@ import { ValidationErrors, isValidationErrors } from "../../core/validation-erro
|
|
|
21
21
|
/**
|
|
22
22
|
* Attaches controllers to a Fastify application.
|
|
23
23
|
*/
|
|
24
|
-
export async function attachControllers(
|
|
25
|
-
app: FastifyInstance,
|
|
26
|
-
controllers: Constructor[],
|
|
27
|
-
inputCoercion: InputCoercionSetting = "safe",
|
|
28
|
-
multipart?: boolean | MultipartOptions,
|
|
29
|
-
validation?: boolean | ValidationOptions
|
|
30
|
-
|
|
24
|
+
export async function attachControllers(
|
|
25
|
+
app: FastifyInstance,
|
|
26
|
+
controllers: Constructor[],
|
|
27
|
+
inputCoercion: InputCoercionSetting = "safe",
|
|
28
|
+
multipart?: boolean | MultipartOptions,
|
|
29
|
+
validation?: boolean | ValidationOptions,
|
|
30
|
+
auth?: { userProperty?: string }
|
|
31
|
+
): Promise<void> {
|
|
31
32
|
const multipartOptions = normalizeMultipartOptions(multipart);
|
|
32
33
|
|
|
33
34
|
for (const controller of controllers) {
|
|
@@ -68,35 +69,9 @@ export async function attachControllers(
|
|
|
68
69
|
method: route.httpMethod.toUpperCase() as any,
|
|
69
70
|
url: path,
|
|
70
71
|
handler: async (req: FastifyRequest, reply: FastifyReply) => {
|
|
71
|
-
try {
|
|
72
|
-
// Apply auth guard if metadata exists
|
|
73
|
-
|
|
74
|
-
const user = (req as any).user || (req.raw as any).user;
|
|
75
|
-
if (!user) {
|
|
76
|
-
throw new HttpError(401, "Unauthorized");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (authMeta.roles?.length) {
|
|
80
|
-
const hasRole = authMeta.roles.some((role: string) => user.roles?.includes(role));
|
|
81
|
-
if (!hasRole) {
|
|
82
|
-
throw new HttpError(403, "Insufficient permissions");
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (authMeta.allRoles?.length) {
|
|
87
|
-
const hasAllRoles = authMeta.allRoles.every((role: string) => user.roles?.includes(role));
|
|
88
|
-
if (!hasAllRoles) {
|
|
89
|
-
throw new HttpError(403, "Insufficient permissions");
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (authMeta.guard) {
|
|
94
|
-
const allowed = await authMeta.guard(user, req);
|
|
95
|
-
if (!allowed) {
|
|
96
|
-
throw new HttpError(403, "Access denied by guard");
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
72
|
+
try {
|
|
73
|
+
// Apply auth guard if metadata exists
|
|
74
|
+
await assertRouteAuthorized(authMeta, req, auth);
|
|
100
75
|
|
|
101
76
|
let files: any = undefined;
|
|
102
77
|
if (multipartOptions && hasFileUploads(route.files)) {
|
|
@@ -2,9 +2,10 @@ import fastify from "fastify";
|
|
|
2
2
|
import type { FastifyAdapterOptions } from "./types";
|
|
3
3
|
import { attachControllers } from "./controllers";
|
|
4
4
|
import { attachOpenApi } from "./openapi";
|
|
5
|
-
import { lifecycleRegistry } from "../../core/lifecycle";
|
|
6
|
-
import
|
|
7
|
-
import
|
|
5
|
+
import { lifecycleRegistry } from "../../core/lifecycle";
|
|
6
|
+
import { authenticateBearerRequest } from "../../core/auth";
|
|
7
|
+
import cors from "@fastify/cors";
|
|
8
|
+
import multipart from "@fastify/multipart";
|
|
8
9
|
|
|
9
10
|
export * from "./types";
|
|
10
11
|
export { attachControllers } from "./controllers";
|
|
@@ -24,16 +25,29 @@ export async function createFastifyApp(options: FastifyAdapterOptions): Promise<
|
|
|
24
25
|
app.register(cors, options.cors === true ? {} : options.cors);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
if (options.multipart) {
|
|
28
|
-
app.register(multipart, {
|
|
29
|
-
limits: {
|
|
30
|
-
fileSize: typeof options.multipart === "object" ? options.multipart.maxFileSize : undefined
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
if (options.multipart) {
|
|
29
|
+
app.register(multipart, {
|
|
30
|
+
limits: {
|
|
31
|
+
fileSize: typeof options.multipart === "object" ? options.multipart.maxFileSize : undefined
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.bearerAuth) {
|
|
37
|
+
app.addHook("preHandler", async (req) => {
|
|
38
|
+
await authenticateBearerRequest(req, options.bearerAuth!);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const inputCoercion = options.inputCoercion ?? "safe";
|
|
43
|
+
await attachControllers(
|
|
44
|
+
app,
|
|
45
|
+
options.controllers,
|
|
46
|
+
inputCoercion,
|
|
47
|
+
options.multipart,
|
|
48
|
+
options.validation,
|
|
49
|
+
{ userProperty: options.bearerAuth?.userProperty }
|
|
50
|
+
);
|
|
37
51
|
|
|
38
52
|
if (options.openApi) {
|
|
39
53
|
attachOpenApi(app, options.controllers, options.openApi);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Constructor,
|
|
3
|
-
RequestContext as CoreRequestContext,
|
|
4
|
-
UploadedFileInfo
|
|
5
|
-
} from "../../core/types";
|
|
6
|
-
import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
|
|
1
|
+
import type {
|
|
2
|
+
Constructor,
|
|
3
|
+
RequestContext as CoreRequestContext,
|
|
4
|
+
UploadedFileInfo
|
|
5
|
+
} from "../../core/types";
|
|
6
|
+
import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
|
|
7
|
+
import type { BearerAuthOptions } from "../../core/auth";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Request context provided to Fastify route handlers.
|
|
@@ -110,10 +111,12 @@ export interface FastifyAdapterOptions {
|
|
|
110
111
|
openApi?: OpenApiFastifyOptions;
|
|
111
112
|
/** Input coercion setting */
|
|
112
113
|
inputCoercion?: InputCoercionSetting;
|
|
113
|
-
/** CORS configuration. Set to true for permissive defaults, or provide options. */
|
|
114
|
-
cors?: boolean | CorsOptions;
|
|
115
|
-
/**
|
|
116
|
-
|
|
114
|
+
/** CORS configuration. Set to true for permissive defaults, or provide options. */
|
|
115
|
+
cors?: boolean | CorsOptions;
|
|
116
|
+
/** Built-in bearer token authentication for protected routes. */
|
|
117
|
+
bearerAuth?: BearerAuthOptions;
|
|
118
|
+
/** Multipart file upload configuration. Set to true for defaults, or provide options. */
|
|
119
|
+
multipart?: boolean | MultipartOptions;
|
|
117
120
|
/** Validation configuration. Set to false to disable validation, or provide options. */
|
|
118
121
|
validation?: boolean | ValidationOptions;
|
|
119
122
|
}
|
|
@@ -78,6 +78,7 @@ export type {
|
|
|
78
78
|
PaginationConfig,
|
|
79
79
|
PaginationOptions,
|
|
80
80
|
ParsedPagination,
|
|
81
|
+
PaginationQueryParams,
|
|
81
82
|
Filter,
|
|
82
83
|
FilterMapping,
|
|
83
84
|
FilterFieldMapping,
|
|
@@ -89,6 +90,8 @@ export type {
|
|
|
89
90
|
ParseSortOptions,
|
|
90
91
|
ParsedSort,
|
|
91
92
|
SortDirection,
|
|
93
|
+
SortingQueryParams,
|
|
94
|
+
PagedQueryParams,
|
|
92
95
|
CrudListSortTerm,
|
|
93
96
|
RunPagedListOptions,
|
|
94
97
|
ExecuteCrudListOptions,
|
|
@@ -74,6 +74,16 @@ export interface ParsedPagination {
|
|
|
74
74
|
pageSize: number;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Pagination query params for consumer-side TypeScript interfaces.
|
|
79
|
+
*/
|
|
80
|
+
export interface PaginationQueryParams {
|
|
81
|
+
/** Page number */
|
|
82
|
+
page?: number;
|
|
83
|
+
/** Page size */
|
|
84
|
+
pageSize?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
/**
|
|
78
88
|
* Filter field mapping.
|
|
79
89
|
*/
|
|
@@ -155,6 +165,21 @@ export interface ParseFilterOptions<T = Record<string, unknown>> {
|
|
|
155
165
|
*/
|
|
156
166
|
export type SortDirection = "asc" | "desc";
|
|
157
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Sorting query params for consumer-side TypeScript interfaces.
|
|
170
|
+
*/
|
|
171
|
+
export interface SortingQueryParams {
|
|
172
|
+
/** Requested sort key */
|
|
173
|
+
sortBy?: string;
|
|
174
|
+
/** Sort direction */
|
|
175
|
+
sortDirection?: SortDirection;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Combined pagination + sorting query params.
|
|
180
|
+
*/
|
|
181
|
+
export interface PagedQueryParams extends PaginationQueryParams, SortingQueryParams {}
|
|
182
|
+
|
|
158
183
|
/**
|
|
159
184
|
* Sort parsing options.
|
|
160
185
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import type { Constructor, RequestContext } from "../../core/types";
|
|
3
|
-
import type { SchemaSource } from "../../core/schema";
|
|
4
|
-
import { getControllerMeta } from "../../core/metadata";
|
|
5
|
-
import { getRouteAuthMeta } from "../../core/auth";
|
|
6
|
-
import { isHttpError
|
|
2
|
+
import type { Constructor, RequestContext } from "../../core/types";
|
|
3
|
+
import type { SchemaSource } from "../../core/schema";
|
|
4
|
+
import { getControllerMeta } from "../../core/metadata";
|
|
5
|
+
import { assertRouteAuthorized, getRouteAuthMeta } from "../../core/auth";
|
|
6
|
+
import { isHttpError } from "../../core/errors";
|
|
7
7
|
import { isHttpResponse } from "../../core/response";
|
|
8
8
|
import type { InputCoercionSetting, ValidationOptions, RequestContext as NativeRequestContext } from "./types";
|
|
9
9
|
import { createInputCoercer } from "./coercion";
|
|
@@ -45,13 +45,14 @@ export async function dispatchRequest(
|
|
|
45
45
|
match: any,
|
|
46
46
|
options: {
|
|
47
47
|
inputCoercion: InputCoercionSetting;
|
|
48
|
-
validation?: boolean | ValidationOptions;
|
|
49
|
-
body?: any;
|
|
50
|
-
query?: Record<string, any>;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const {
|
|
48
|
+
validation?: boolean | ValidationOptions;
|
|
49
|
+
body?: any;
|
|
50
|
+
query?: Record<string, any>;
|
|
51
|
+
auth?: { userProperty?: string };
|
|
52
|
+
}
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const { controller: instance, route, params: rawParams } = match;
|
|
55
|
+
const { inputCoercion, validation, body: rawBody, query: rawQuery, auth } = options;
|
|
55
56
|
|
|
56
57
|
const handler = instance[route.handlerName];
|
|
57
58
|
if (typeof handler !== "function") {
|
|
@@ -75,35 +76,9 @@ export async function dispatchRequest(
|
|
|
75
76
|
|
|
76
77
|
const authMeta = getRouteAuthMeta(instance.constructor as Constructor, route.handlerName);
|
|
77
78
|
|
|
78
|
-
try {
|
|
79
|
-
// Apply auth guard if metadata exists
|
|
80
|
-
|
|
81
|
-
const user = (req as any).user;
|
|
82
|
-
if (!user) {
|
|
83
|
-
throw new HttpError(401, "Unauthorized");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (authMeta.roles?.length) {
|
|
87
|
-
const hasRole = authMeta.roles.some((role: string) => user.roles?.includes(role));
|
|
88
|
-
if (!hasRole) {
|
|
89
|
-
throw new HttpError(403, "Insufficient permissions");
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (authMeta.allRoles?.length) {
|
|
94
|
-
const hasAllRoles = authMeta.allRoles.every((role: string) => user.roles?.includes(role));
|
|
95
|
-
if (!hasAllRoles) {
|
|
96
|
-
throw new HttpError(403, "Insufficient permissions");
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (authMeta.guard) {
|
|
101
|
-
const allowed = await authMeta.guard(user, req);
|
|
102
|
-
if (!allowed) {
|
|
103
|
-
throw new HttpError(403, "Access denied by guard");
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
79
|
+
try {
|
|
80
|
+
// Apply auth guard if metadata exists
|
|
81
|
+
await assertRouteAuthorized(authMeta, req, auth);
|
|
107
82
|
|
|
108
83
|
const body = (coerceBody && rawBody) ? coerceBody(rawBody) : rawBody;
|
|
109
84
|
const query = (coerceQuery && rawQuery) ? coerceQuery(rawQuery) : rawQuery;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { NativeAdapterOptions, NativeApp } from "./types";
|
|
3
3
|
import { registerControllers, dispatchRequest } from "./controllers";
|
|
4
|
-
import { registerOpenApi } from "./openapi";
|
|
5
|
-
import { lifecycleRegistry } from "../../core/lifecycle";
|
|
6
|
-
import {
|
|
4
|
+
import { registerOpenApi } from "./openapi";
|
|
5
|
+
import { lifecycleRegistry } from "../../core/lifecycle";
|
|
6
|
+
import { authenticateBearerRequest } from "../../core/auth";
|
|
7
|
+
import { Router } from "./router";
|
|
7
8
|
|
|
8
9
|
export * from "./types";
|
|
9
10
|
export { registerControllers as attachControllers } from "./controllers";
|
|
@@ -28,12 +29,23 @@ export async function createNativeApp(options: NativeAdapterOptions): Promise<Na
|
|
|
28
29
|
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
29
30
|
const match = router.match(req.method || "GET", url.pathname);
|
|
30
31
|
|
|
31
|
-
if (!match) {
|
|
32
|
-
res.statusCode = 404;
|
|
33
|
-
res.setHeader("Content-Type", "application/json");
|
|
34
|
-
res.end(JSON.stringify({ message: `Not Found: ${req.method} ${url.pathname}` }));
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
32
|
+
if (!match) {
|
|
33
|
+
res.statusCode = 404;
|
|
34
|
+
res.setHeader("Content-Type", "application/json");
|
|
35
|
+
res.end(JSON.stringify({ message: `Not Found: ${req.method} ${url.pathname}` }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.bearerAuth) {
|
|
40
|
+
try {
|
|
41
|
+
await authenticateBearerRequest(req, options.bearerAuth);
|
|
42
|
+
} catch {
|
|
43
|
+
res.statusCode = 500;
|
|
44
|
+
res.setHeader("Content-Type", "application/json");
|
|
45
|
+
res.end(JSON.stringify({ message: "Internal server error" }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
|
|
38
50
|
const query: Record<string, any> = {};
|
|
39
51
|
url.searchParams.forEach((value, key) => {
|
|
@@ -60,12 +72,13 @@ export async function createNativeApp(options: NativeAdapterOptions): Promise<Na
|
|
|
60
72
|
}
|
|
61
73
|
}
|
|
62
74
|
|
|
63
|
-
await dispatchRequest(req, res, match, {
|
|
64
|
-
inputCoercion,
|
|
65
|
-
validation: options.validation,
|
|
66
|
-
body,
|
|
67
|
-
query
|
|
68
|
-
|
|
75
|
+
await dispatchRequest(req, res, match, {
|
|
76
|
+
inputCoercion,
|
|
77
|
+
validation: options.validation,
|
|
78
|
+
body,
|
|
79
|
+
query,
|
|
80
|
+
auth: { userProperty: options.bearerAuth?.userProperty }
|
|
81
|
+
});
|
|
69
82
|
};
|
|
70
83
|
|
|
71
84
|
await lifecycleRegistry.callOnApplicationBootstrap();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import type {
|
|
3
|
-
Constructor,
|
|
4
|
-
RequestContext as CoreRequestContext,
|
|
5
|
-
UploadedFileInfo
|
|
6
|
-
} from "../../core/types";
|
|
7
|
-
import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
|
|
2
|
+
import type {
|
|
3
|
+
Constructor,
|
|
4
|
+
RequestContext as CoreRequestContext,
|
|
5
|
+
UploadedFileInfo
|
|
6
|
+
} from "../../core/types";
|
|
7
|
+
import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
|
|
8
|
+
import type { BearerAuthOptions } from "../../core/auth";
|
|
8
9
|
|
|
9
10
|
export { UploadedFileInfo };
|
|
10
11
|
|
|
@@ -79,10 +80,12 @@ export interface NativeAdapterOptions {
|
|
|
79
80
|
bodyLimit?: number;
|
|
80
81
|
/** OpenAPI configuration */
|
|
81
82
|
openApi?: OpenApiNativeOptions;
|
|
82
|
-
/** Input coercion setting */
|
|
83
|
-
inputCoercion?: InputCoercionSetting;
|
|
84
|
-
/**
|
|
85
|
-
|
|
83
|
+
/** Input coercion setting */
|
|
84
|
+
inputCoercion?: InputCoercionSetting;
|
|
85
|
+
/** Built-in bearer token authentication for protected routes. */
|
|
86
|
+
bearerAuth?: BearerAuthOptions;
|
|
87
|
+
/** Validation configuration. Set to false to disable validation, or provide options. */
|
|
88
|
+
validation?: boolean | ValidationOptions;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
/**
|