adorn-api 1.1.13 → 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.
Files changed (36) hide show
  1. package/README.md +613 -930
  2. package/dist/adapter/express/controllers.d.ts +3 -1
  3. package/dist/adapter/express/controllers.js +4 -1
  4. package/dist/adapter/express/index.js +5 -1
  5. package/dist/adapter/express/types.d.ts +3 -0
  6. package/dist/adapter/fastify/controllers.d.ts +3 -1
  7. package/dist/adapter/fastify/controllers.js +2 -25
  8. package/dist/adapter/fastify/index.js +7 -1
  9. package/dist/adapter/fastify/types.d.ts +3 -0
  10. package/dist/adapter/native/controllers.d.ts +3 -0
  11. package/dist/adapter/native/controllers.js +2 -25
  12. package/dist/adapter/native/index.js +14 -1
  13. package/dist/adapter/native/types.d.ts +3 -0
  14. package/dist/core/auth.d.ts +33 -3
  15. package/dist/core/auth.js +74 -22
  16. package/dist/core/openapi.d.ts +2 -0
  17. package/dist/core/openapi.js +19 -1
  18. package/examples/bearer-auth-swagger/app.ts +28 -0
  19. package/examples/bearer-auth-swagger/auth.controller.ts +45 -0
  20. package/examples/bearer-auth-swagger/index.ts +20 -0
  21. package/examples/bearer-auth-swagger/session.dtos.ts +19 -0
  22. package/package.json +1 -1
  23. package/src/adapter/express/controllers.ts +23 -18
  24. package/src/adapter/express/index.ts +12 -1
  25. package/src/adapter/express/types.ts +13 -10
  26. package/src/adapter/fastify/controllers.ts +16 -41
  27. package/src/adapter/fastify/index.ts +27 -13
  28. package/src/adapter/fastify/types.ts +13 -10
  29. package/src/adapter/native/controllers.ts +16 -41
  30. package/src/adapter/native/index.ts +28 -15
  31. package/src/adapter/native/types.ts +13 -10
  32. package/src/core/auth.ts +134 -56
  33. package/src/core/openapi.ts +22 -1
  34. package/tests/e2e/bearer-auth.e2e.test.ts +158 -0
  35. package/tests/unit/auth.test.ts +96 -12
  36. package/tests/unit/openapi-parameters.test.ts +54 -6
@@ -8,4 +8,6 @@ import type { InputCoercionSetting, MultipartOptions, ValidationOptions } from "
8
8
  * @param inputCoercion - Input coercion setting
9
9
  * @param multipart - Multipart file upload configuration
10
10
  */
11
- export declare function attachControllers(app: Express, controllers: Constructor[], inputCoercion?: InputCoercionSetting, multipart?: boolean | MultipartOptions, validation?: boolean | ValidationOptions): Promise<void>;
11
+ export declare function attachControllers(app: Express, controllers: Constructor[], inputCoercion?: InputCoercionSetting, multipart?: boolean | MultipartOptions, validation?: boolean | ValidationOptions, auth?: {
12
+ userProperty?: string;
13
+ }): Promise<void>;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.attachControllers = attachControllers;
4
4
  const metadata_1 = require("../../core/metadata");
5
+ const auth_1 = require("../../core/auth");
5
6
  const errors_1 = require("../../core/errors");
6
7
  const response_1 = require("../../core/response");
7
8
  const coercion_1 = require("./coercion");
@@ -18,7 +19,7 @@ const validation_errors_1 = require("../../core/validation-errors");
18
19
  * @param inputCoercion - Input coercion setting
19
20
  * @param multipart - Multipart file upload configuration
20
21
  */
21
- async function attachControllers(app, controllers, inputCoercion = "safe", multipart, validation) {
22
+ async function attachControllers(app, controllers, inputCoercion = "safe", multipart, validation, auth) {
22
23
  const multipartOptions = (0, multipart_1.normalizeMultipartOptions)(multipart);
23
24
  for (const controller of controllers) {
24
25
  const meta = (0, metadata_1.getControllerMeta)(controller);
@@ -51,9 +52,11 @@ async function attachControllers(app, controllers, inputCoercion = "safe", multi
51
52
  }
52
53
  // Determine if validation is enabled for this route
53
54
  const isValidationEnabled = validation !== false && validation?.enabled !== false;
55
+ const authMeta = (0, auth_1.getRouteAuthMeta)(controller, route.handlerName);
54
56
  // Main route handler
55
57
  const routeHandler = async (req, res, next) => {
56
58
  try {
59
+ await (0, auth_1.assertRouteAuthorized)(authMeta, req, auth);
57
60
  const files = (0, multipart_1.extractFiles)(req);
58
61
  // Create context
59
62
  const ctx = {
@@ -25,6 +25,7 @@ const cors_1 = require("./cors");
25
25
  const controllers_1 = require("./controllers");
26
26
  const openapi_1 = require("./openapi");
27
27
  const lifecycle_1 = require("../../core/lifecycle");
28
+ const auth_1 = require("../../core/auth");
28
29
  __exportStar(require("./types"), exports);
29
30
  var cors_2 = require("./cors");
30
31
  Object.defineProperty(exports, "attachCors", { enumerable: true, get: function () { return cors_2.attachCors; } });
@@ -45,8 +46,11 @@ async function createExpressApp(options) {
45
46
  if (options.jsonBody ?? true) {
46
47
  app.use(express_1.default.json({ limit: options.jsonLimit }));
47
48
  }
49
+ if (options.bearerAuth) {
50
+ app.use((0, auth_1.createBearerAuthMiddleware)(options.bearerAuth));
51
+ }
48
52
  const inputCoercion = options.inputCoercion ?? "safe";
49
- await (0, controllers_1.attachControllers)(app, options.controllers, inputCoercion, options.multipart, options.validation);
53
+ await (0, controllers_1.attachControllers)(app, options.controllers, inputCoercion, options.multipart, options.validation, { userProperty: options.bearerAuth?.userProperty });
50
54
  if (options.openApi) {
51
55
  (0, openapi_1.attachOpenApi)(app, options.controllers, options.openApi);
52
56
  }
@@ -1,5 +1,6 @@
1
1
  import type { Constructor, RequestContext as CoreRequestContext, UploadedFileInfo } from "../../core/types";
2
2
  import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
3
+ import type { BearerAuthOptions } from "../../core/auth";
3
4
  export { UploadedFileInfo };
4
5
  /**
5
6
  * Request context provided to route handlers.
@@ -94,6 +95,8 @@ export interface ExpressAdapterOptions {
94
95
  inputCoercion?: InputCoercionSetting;
95
96
  /** CORS configuration. Set to true for permissive defaults, or provide options. */
96
97
  cors?: boolean | CorsOptions;
98
+ /** Built-in bearer token authentication for protected routes. */
99
+ bearerAuth?: BearerAuthOptions;
97
100
  /** Multipart file upload configuration. Set to true for defaults, or provide options. */
98
101
  multipart?: boolean | MultipartOptions;
99
102
  /** Validation configuration. Set to false to disable validation, or provide options. */
@@ -4,4 +4,6 @@ import type { InputCoercionSetting, MultipartOptions, ValidationOptions } from "
4
4
  /**
5
5
  * Attaches controllers to a Fastify application.
6
6
  */
7
- export declare function attachControllers(app: FastifyInstance, controllers: Constructor[], inputCoercion?: InputCoercionSetting, multipart?: boolean | MultipartOptions, validation?: boolean | ValidationOptions): Promise<void>;
7
+ export declare function attachControllers(app: FastifyInstance, controllers: Constructor[], inputCoercion?: InputCoercionSetting, multipart?: boolean | MultipartOptions, validation?: boolean | ValidationOptions, auth?: {
8
+ userProperty?: string;
9
+ }): Promise<void>;
@@ -15,7 +15,7 @@ const validation_errors_1 = require("../../core/validation-errors");
15
15
  /**
16
16
  * Attaches controllers to a Fastify application.
17
17
  */
18
- async function attachControllers(app, controllers, inputCoercion = "safe", multipart, validation) {
18
+ async function attachControllers(app, controllers, inputCoercion = "safe", multipart, validation, auth) {
19
19
  const multipartOptions = (0, multipart_1.normalizeMultipartOptions)(multipart);
20
20
  for (const controller of controllers) {
21
21
  const meta = (0, metadata_1.getControllerMeta)(controller);
@@ -48,30 +48,7 @@ async function attachControllers(app, controllers, inputCoercion = "safe", multi
48
48
  handler: async (req, reply) => {
49
49
  try {
50
50
  // Apply auth guard if metadata exists
51
- if (authMeta && authMeta.requiresAuth && !authMeta.isPublic) {
52
- const user = req.user || req.raw.user;
53
- if (!user) {
54
- throw new errors_1.HttpError(401, "Unauthorized");
55
- }
56
- if (authMeta.roles?.length) {
57
- const hasRole = authMeta.roles.some((role) => user.roles?.includes(role));
58
- if (!hasRole) {
59
- throw new errors_1.HttpError(403, "Insufficient permissions");
60
- }
61
- }
62
- if (authMeta.allRoles?.length) {
63
- const hasAllRoles = authMeta.allRoles.every((role) => user.roles?.includes(role));
64
- if (!hasAllRoles) {
65
- throw new errors_1.HttpError(403, "Insufficient permissions");
66
- }
67
- }
68
- if (authMeta.guard) {
69
- const allowed = await authMeta.guard(user, req);
70
- if (!allowed) {
71
- throw new errors_1.HttpError(403, "Access denied by guard");
72
- }
73
- }
74
- }
51
+ await (0, auth_1.assertRouteAuthorized)(authMeta, req, auth);
75
52
  let files = undefined;
76
53
  if (multipartOptions && (0, multipart_1.hasFileUploads)(route.files)) {
77
54
  files = await (0, multipart_1.extractFiles)(req);
@@ -24,6 +24,7 @@ const fastify_1 = __importDefault(require("fastify"));
24
24
  const controllers_1 = require("./controllers");
25
25
  const openapi_1 = require("./openapi");
26
26
  const lifecycle_1 = require("../../core/lifecycle");
27
+ const auth_1 = require("../../core/auth");
27
28
  const cors_1 = __importDefault(require("@fastify/cors"));
28
29
  const multipart_1 = __importDefault(require("@fastify/multipart"));
29
30
  __exportStar(require("./types"), exports);
@@ -50,8 +51,13 @@ async function createFastifyApp(options) {
50
51
  }
51
52
  });
52
53
  }
54
+ if (options.bearerAuth) {
55
+ app.addHook("preHandler", async (req) => {
56
+ await (0, auth_1.authenticateBearerRequest)(req, options.bearerAuth);
57
+ });
58
+ }
53
59
  const inputCoercion = options.inputCoercion ?? "safe";
54
- await (0, controllers_1.attachControllers)(app, options.controllers, inputCoercion, options.multipart, options.validation);
60
+ await (0, controllers_1.attachControllers)(app, options.controllers, inputCoercion, options.multipart, options.validation, { userProperty: options.bearerAuth?.userProperty });
55
61
  if (options.openApi) {
56
62
  (0, openapi_1.attachOpenApi)(app, options.controllers, options.openApi);
57
63
  }
@@ -1,5 +1,6 @@
1
1
  import type { Constructor, RequestContext as CoreRequestContext, UploadedFileInfo } from "../../core/types";
2
2
  import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
3
+ import type { BearerAuthOptions } from "../../core/auth";
3
4
  /**
4
5
  * Request context provided to Fastify route handlers.
5
6
  */
@@ -93,6 +94,8 @@ export interface FastifyAdapterOptions {
93
94
  inputCoercion?: InputCoercionSetting;
94
95
  /** CORS configuration. Set to true for permissive defaults, or provide options. */
95
96
  cors?: boolean | CorsOptions;
97
+ /** Built-in bearer token authentication for protected routes. */
98
+ bearerAuth?: BearerAuthOptions;
96
99
  /** Multipart file upload configuration. Set to true for defaults, or provide options. */
97
100
  multipart?: boolean | MultipartOptions;
98
101
  /** Validation configuration. Set to false to disable validation, or provide options. */
@@ -14,4 +14,7 @@ export declare function dispatchRequest(req: IncomingMessage, res: ServerRespons
14
14
  validation?: boolean | ValidationOptions;
15
15
  body?: any;
16
16
  query?: Record<string, any>;
17
+ auth?: {
18
+ userProperty?: string;
19
+ };
17
20
  }): Promise<void>;
@@ -34,7 +34,7 @@ async function registerControllers(router, controllers) {
34
34
  */
35
35
  async function dispatchRequest(req, res, match, options) {
36
36
  const { controller: instance, route, params: rawParams } = match;
37
- const { inputCoercion, validation, body: rawBody, query: rawQuery } = options;
37
+ const { inputCoercion, validation, body: rawBody, query: rawQuery, auth } = options;
38
38
  const handler = instance[route.handlerName];
39
39
  if (typeof handler !== "function") {
40
40
  throw new Error(`Handler "${String(route.handlerName)}" is not a function.`);
@@ -52,30 +52,7 @@ async function dispatchRequest(req, res, match, options) {
52
52
  const authMeta = (0, auth_1.getRouteAuthMeta)(instance.constructor, route.handlerName);
53
53
  try {
54
54
  // Apply auth guard if metadata exists
55
- if (authMeta && authMeta.requiresAuth && !authMeta.isPublic) {
56
- const user = req.user;
57
- if (!user) {
58
- throw new errors_1.HttpError(401, "Unauthorized");
59
- }
60
- if (authMeta.roles?.length) {
61
- const hasRole = authMeta.roles.some((role) => user.roles?.includes(role));
62
- if (!hasRole) {
63
- throw new errors_1.HttpError(403, "Insufficient permissions");
64
- }
65
- }
66
- if (authMeta.allRoles?.length) {
67
- const hasAllRoles = authMeta.allRoles.every((role) => user.roles?.includes(role));
68
- if (!hasAllRoles) {
69
- throw new errors_1.HttpError(403, "Insufficient permissions");
70
- }
71
- }
72
- if (authMeta.guard) {
73
- const allowed = await authMeta.guard(user, req);
74
- if (!allowed) {
75
- throw new errors_1.HttpError(403, "Access denied by guard");
76
- }
77
- }
78
- }
55
+ await (0, auth_1.assertRouteAuthorized)(authMeta, req, auth);
79
56
  const body = (coerceBody && rawBody) ? coerceBody(rawBody) : rawBody;
80
57
  const query = (coerceQuery && rawQuery) ? coerceQuery(rawQuery) : rawQuery;
81
58
  const params = (coerceParams && rawParams) ? coerceParams(rawParams) : rawParams;
@@ -21,6 +21,7 @@ const node_http_1 = require("node:http");
21
21
  const controllers_1 = require("./controllers");
22
22
  const openapi_1 = require("./openapi");
23
23
  const lifecycle_1 = require("../../core/lifecycle");
24
+ const auth_1 = require("../../core/auth");
24
25
  const router_1 = require("./router");
25
26
  __exportStar(require("./types"), exports);
26
27
  var controllers_2 = require("./controllers");
@@ -48,6 +49,17 @@ async function createNativeApp(options) {
48
49
  res.end(JSON.stringify({ message: `Not Found: ${req.method} ${url.pathname}` }));
49
50
  return;
50
51
  }
52
+ if (options.bearerAuth) {
53
+ try {
54
+ await (0, auth_1.authenticateBearerRequest)(req, options.bearerAuth);
55
+ }
56
+ catch {
57
+ res.statusCode = 500;
58
+ res.setHeader("Content-Type", "application/json");
59
+ res.end(JSON.stringify({ message: "Internal server error" }));
60
+ return;
61
+ }
62
+ }
51
63
  const query = {};
52
64
  url.searchParams.forEach((value, key) => {
53
65
  if (query[key]) {
@@ -78,7 +90,8 @@ async function createNativeApp(options) {
78
90
  inputCoercion,
79
91
  validation: options.validation,
80
92
  body,
81
- query
93
+ query,
94
+ auth: { userProperty: options.bearerAuth?.userProperty }
82
95
  });
83
96
  };
84
97
  await lifecycle_1.lifecycleRegistry.callOnApplicationBootstrap();
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { Constructor, RequestContext as CoreRequestContext, UploadedFileInfo } from "../../core/types";
3
3
  import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
4
+ import type { BearerAuthOptions } from "../../core/auth";
4
5
  export { UploadedFileInfo };
5
6
  /**
6
7
  * Request context provided to native route handlers.
@@ -63,6 +64,8 @@ export interface NativeAdapterOptions {
63
64
  openApi?: OpenApiNativeOptions;
64
65
  /** Input coercion setting */
65
66
  inputCoercion?: InputCoercionSetting;
67
+ /** Built-in bearer token authentication for protected routes. */
68
+ bearerAuth?: BearerAuthOptions;
66
69
  /** Validation configuration. Set to false to disable validation, or provide options. */
67
70
  validation?: boolean | ValidationOptions;
68
71
  }
@@ -22,6 +22,10 @@ export interface AuthOptions {
22
22
  * Function to extract user from request.
23
23
  */
24
24
  export type AuthExtractor = (req: any) => AuthUser | null | Promise<AuthUser | null>;
25
+ /**
26
+ * Function to validate a bearer token and resolve the authenticated user.
27
+ */
28
+ export type BearerTokenVerifier = (token: string, req: any) => AuthUser | null | Promise<AuthUser | null>;
25
29
  /**
26
30
  * Options for creating auth middleware.
27
31
  */
@@ -35,10 +39,19 @@ export interface AuthMiddlewareOptions {
35
39
  /** Custom forbidden response */
36
40
  onForbidden?: (req: any, res: any, reason?: string) => void;
37
41
  }
42
+ /**
43
+ * Options for built-in Bearer authentication.
44
+ */
45
+ export interface BearerAuthOptions {
46
+ /** Function to validate token and return user context */
47
+ verifyToken: BearerTokenVerifier;
48
+ /** Property name to attach user to request (default: "user") */
49
+ userProperty?: string;
50
+ }
38
51
  /**
39
52
  * Metadata for authentication on routes/controllers.
40
53
  */
41
- interface AuthMeta {
54
+ export interface AuthMeta {
42
55
  /** Whether authentication is required */
43
56
  requiresAuth: boolean;
44
57
  /** Whether route is public (overrides controller-level auth) */
@@ -85,6 +98,10 @@ export declare function getRouteAuthMeta(controller: Constructor, handlerName: s
85
98
  * Gets auth metadata for a controller.
86
99
  */
87
100
  export declare function getControllerAuthMeta(controller: Constructor): AuthMeta | undefined;
101
+ /**
102
+ * Extracts a token from the Authorization: Bearer <token> header.
103
+ */
104
+ export declare function extractBearerToken(req: any): string | undefined;
88
105
  /**
89
106
  * Creates Express middleware for authentication.
90
107
  * Use this as a global middleware, then use route-level checks.
@@ -92,6 +109,20 @@ export declare function getControllerAuthMeta(controller: Constructor): AuthMeta
92
109
  * @returns Express middleware function
93
110
  */
94
111
  export declare function createAuthMiddleware(options: AuthMiddlewareOptions): (req: any, res: any, next: (err?: any) => void) => Promise<void>;
112
+ /**
113
+ * Verifies a bearer token from the request and attaches the user when valid.
114
+ */
115
+ export declare function authenticateBearerRequest(req: any, options: BearerAuthOptions): Promise<AuthUser | null>;
116
+ /**
117
+ * Creates middleware that extracts and verifies Authorization bearer tokens.
118
+ */
119
+ export declare function createBearerAuthMiddleware(options: BearerAuthOptions): (req: any, _res: any, next: (err?: any) => void) => Promise<void>;
120
+ /**
121
+ * Applies route auth metadata to a request that may already have a user attached.
122
+ */
123
+ export declare function assertRouteAuthorized(authMeta: AuthMeta | undefined, req: any, options?: {
124
+ userProperty?: string;
125
+ }): Promise<void>;
95
126
  /**
96
127
  * Creates a route guard middleware that checks auth metadata.
97
128
  * @param controller - Controller class
@@ -101,7 +132,7 @@ export declare function createAuthMiddleware(options: AuthMiddlewareOptions): (r
101
132
  */
102
133
  export declare function createRouteGuard(controller: Constructor, handlerName: string | symbol, options?: {
103
134
  userProperty?: string;
104
- }): (req: any, res: any, next: (err?: any) => void) => Promise<void>;
135
+ }): (req: any, _res: any, next: (err?: any) => void) => Promise<void>;
105
136
  /**
106
137
  * Helper to get user from request in controllers.
107
138
  * @param req - Raw request
@@ -117,4 +148,3 @@ export declare function getUser<T extends AuthUser = AuthUser>(req: any, userPro
117
148
  * @throws HttpError if user not found
118
149
  */
119
150
  export declare function requireUser<T extends AuthUser = AuthUser>(req: any, userProperty?: string): T;
120
- export {};
package/dist/core/auth.js CHANGED
@@ -6,7 +6,11 @@ exports.AllRoles = AllRoles;
6
6
  exports.Public = Public;
7
7
  exports.getRouteAuthMeta = getRouteAuthMeta;
8
8
  exports.getControllerAuthMeta = getControllerAuthMeta;
9
+ exports.extractBearerToken = extractBearerToken;
9
10
  exports.createAuthMiddleware = createAuthMiddleware;
11
+ exports.authenticateBearerRequest = authenticateBearerRequest;
12
+ exports.createBearerAuthMiddleware = createBearerAuthMiddleware;
13
+ exports.assertRouteAuthorized = assertRouteAuthorized;
10
14
  exports.createRouteGuard = createRouteGuard;
11
15
  exports.getUser = getUser;
12
16
  exports.requireUser = requireUser;
@@ -153,6 +157,19 @@ function hasAllRoles(user, roles) {
153
157
  return false;
154
158
  return roles.every((role) => user.roles.includes(role));
155
159
  }
160
+ /**
161
+ * Extracts a token from the Authorization: Bearer <token> header.
162
+ */
163
+ function extractBearerToken(req) {
164
+ const headers = req?.headers;
165
+ const rawHeader = headers?.authorization ?? headers?.Authorization;
166
+ const header = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
167
+ if (typeof header !== "string") {
168
+ return undefined;
169
+ }
170
+ const match = header.match(/^Bearer\s+(\S+)$/i);
171
+ return match?.[1];
172
+ }
156
173
  /**
157
174
  * Creates Express middleware for authentication.
158
175
  * Use this as a global middleware, then use route-level checks.
@@ -174,6 +191,61 @@ function createAuthMiddleware(options) {
174
191
  }
175
192
  };
176
193
  }
194
+ /**
195
+ * Verifies a bearer token from the request and attaches the user when valid.
196
+ */
197
+ async function authenticateBearerRequest(req, options) {
198
+ const token = extractBearerToken(req);
199
+ if (!token) {
200
+ return null;
201
+ }
202
+ const user = await options.verifyToken(token, req);
203
+ if (user) {
204
+ req[options.userProperty ?? "user"] = user;
205
+ }
206
+ return user;
207
+ }
208
+ /**
209
+ * Creates middleware that extracts and verifies Authorization bearer tokens.
210
+ */
211
+ function createBearerAuthMiddleware(options) {
212
+ return async (req, _res, next) => {
213
+ try {
214
+ await authenticateBearerRequest(req, options);
215
+ next();
216
+ }
217
+ catch (error) {
218
+ next(error);
219
+ }
220
+ };
221
+ }
222
+ /**
223
+ * Applies route auth metadata to a request that may already have a user attached.
224
+ */
225
+ async function assertRouteAuthorized(authMeta, req, options = {}) {
226
+ if (!authMeta || authMeta.isPublic || !authMeta.requiresAuth) {
227
+ return;
228
+ }
229
+ const userProperty = options.userProperty ?? "user";
230
+ const requestRecord = req;
231
+ const rawRecord = (req?.raw ?? {});
232
+ const user = (requestRecord[userProperty] ?? rawRecord[userProperty]);
233
+ if (!user) {
234
+ throw new errors_1.HttpError(401, "Unauthorized");
235
+ }
236
+ if (authMeta.roles?.length && !hasAnyRole(user, authMeta.roles)) {
237
+ throw new errors_1.HttpError(403, "Insufficient permissions");
238
+ }
239
+ if (authMeta.allRoles?.length && !hasAllRoles(user, authMeta.allRoles)) {
240
+ throw new errors_1.HttpError(403, "Insufficient permissions");
241
+ }
242
+ if (authMeta.guard) {
243
+ const allowed = await authMeta.guard(user, req);
244
+ if (!allowed) {
245
+ throw new errors_1.HttpError(403, "Access denied by guard");
246
+ }
247
+ }
248
+ }
177
249
  /**
178
250
  * Creates a route guard middleware that checks auth metadata.
179
251
  * @param controller - Controller class
@@ -182,29 +254,9 @@ function createAuthMiddleware(options) {
182
254
  * @returns Express middleware function
183
255
  */
184
256
  function createRouteGuard(controller, handlerName, options = {}) {
185
- const userProperty = options.userProperty ?? "user";
186
257
  const authMeta = getRouteAuthMeta(controller, handlerName);
187
- return async (req, res, next) => {
188
- if (!authMeta || authMeta.isPublic || !authMeta.requiresAuth) {
189
- next();
190
- return;
191
- }
192
- const user = req[userProperty];
193
- if (!user) {
194
- throw new errors_1.HttpError(401, "Unauthorized");
195
- }
196
- if (authMeta.roles?.length && !hasAnyRole(user, authMeta.roles)) {
197
- throw new errors_1.HttpError(403, "Insufficient permissions");
198
- }
199
- if (authMeta.allRoles?.length && !hasAllRoles(user, authMeta.allRoles)) {
200
- throw new errors_1.HttpError(403, "Insufficient permissions");
201
- }
202
- if (authMeta.guard) {
203
- const allowed = await authMeta.guard(user, req);
204
- if (!allowed) {
205
- throw new errors_1.HttpError(403, "Access denied by guard");
206
- }
207
- }
258
+ return async (req, _res, next) => {
259
+ await assertRouteAuthorized(authMeta, req, options);
208
260
  next();
209
261
  };
210
262
  }
@@ -49,6 +49,8 @@ export interface OpenApiDocument {
49
49
  components: {
50
50
  /** Schema definitions */
51
51
  schemas: Record<string, JsonSchema>;
52
+ /** Reusable security scheme definitions */
53
+ securitySchemes?: Record<string, unknown>;
52
54
  };
53
55
  }
54
56
  /**
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildOpenApi = buildOpenApi;
4
4
  const schema_builder_1 = require("./schema-builder");
5
5
  const metadata_1 = require("./metadata");
6
+ const auth_1 = require("./auth");
6
7
  /**
7
8
  * Validates that all registered DTOs have non-empty field metadata.
8
9
  * Throws an error with helpful diagnostic information if empty schemas are found.
@@ -39,6 +40,7 @@ function buildOpenApi(options) {
39
40
  const context = (0, schema_builder_1.createSchemaContext)();
40
41
  const controllers = filterControllers(options.controllers);
41
42
  const paths = {};
43
+ let hasBearerAuth = false;
42
44
  for (const controller of controllers) {
43
45
  const tagFallback = controller.meta.tags ?? [controller.meta.controller.name];
44
46
  for (const route of controller.meta.routes) {
@@ -56,6 +58,13 @@ function buildOpenApi(options) {
56
58
  const requestBody = hasFiles
57
59
  ? buildMultipartRequestBody(route.files, route.body, context)
58
60
  : buildRequestBody(route.body, context);
61
+ const authMeta = (0, auth_1.getRouteAuthMeta)(controller.meta.controller, route.handlerName);
62
+ const security = authMeta?.requiresAuth && !authMeta.isPublic
63
+ ? [{ bearerAuth: [] }]
64
+ : undefined;
65
+ if (security) {
66
+ hasBearerAuth = true;
67
+ }
59
68
  pathItem[route.httpMethod] = {
60
69
  operationId: `${controller.meta.controller.name}.${String(route.handlerName)}`,
61
70
  summary: route.summary,
@@ -63,6 +72,7 @@ function buildOpenApi(options) {
63
72
  tags: route.tags ?? tagFallback,
64
73
  parameters: parameters.length ? parameters : undefined,
65
74
  requestBody,
75
+ security,
66
76
  responses
67
77
  };
68
78
  }
@@ -74,7 +84,15 @@ function buildOpenApi(options) {
74
84
  servers: options.servers,
75
85
  paths,
76
86
  components: {
77
- schemas: context.components
87
+ schemas: context.components,
88
+ securitySchemes: hasBearerAuth
89
+ ? {
90
+ bearerAuth: {
91
+ type: "http",
92
+ scheme: "bearer"
93
+ }
94
+ }
95
+ : undefined
78
96
  }
79
97
  };
80
98
  validateSchemas(context.components);
@@ -0,0 +1,28 @@
1
+ import { createExpressApp, type AuthUser } from "../../src";
2
+ import { AuthDemoController } from "./auth.controller";
3
+
4
+ function verifyToken(token: string): AuthUser | null {
5
+ if (token === "user-token") {
6
+ return { id: "user-1", roles: ["user"] };
7
+ }
8
+ if (token === "admin-token") {
9
+ return { id: "admin-1", roles: ["user", "admin"] };
10
+ }
11
+ return null;
12
+ }
13
+
14
+ export async function createApp() {
15
+ return createExpressApp({
16
+ controllers: [AuthDemoController],
17
+ bearerAuth: { verifyToken },
18
+ openApi: {
19
+ info: {
20
+ title: "Bearer Auth Swagger Demo",
21
+ version: "1.0.0",
22
+ description: "Demo for Authorization: Bearer <token> with protected OpenAPI routes."
23
+ },
24
+ path: "/openapi.json",
25
+ docs: true
26
+ }
27
+ });
28
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ Auth,
3
+ Controller,
4
+ Get,
5
+ Public,
6
+ Returns,
7
+ Roles,
8
+ getUser,
9
+ type AuthUser
10
+ } from "../../src";
11
+ import { PublicStatusDto, SessionDto } from "./session.dtos";
12
+
13
+ @Controller("/auth-demo")
14
+ export class AuthDemoController {
15
+ @Get("/public")
16
+ @Public()
17
+ @Returns(PublicStatusDto)
18
+ publicStatus() {
19
+ return { status: "public route, no bearer token required" };
20
+ }
21
+
22
+ @Get("/me")
23
+ @Auth()
24
+ @Returns(SessionDto)
25
+ me(ctx: any) {
26
+ const user = getUser<AuthUser>(ctx.req)!;
27
+ return {
28
+ id: user.id,
29
+ roles: user.roles ?? [],
30
+ message: "Bearer token accepted"
31
+ };
32
+ }
33
+
34
+ @Get("/admin")
35
+ @Roles("admin")
36
+ @Returns(SessionDto)
37
+ admin(ctx: any) {
38
+ const user = getUser<AuthUser>(ctx.req)!;
39
+ return {
40
+ id: user.id,
41
+ roles: user.roles ?? [],
42
+ message: "Admin role accepted"
43
+ };
44
+ }
45
+ }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "adorn-api",
3
- "version": "1.1.13",
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",