diff --git a/packages/core/src/middleware/koa-auth/index.ts b/packages/core/src/middleware/koa-auth/index.ts index 4ec6b2b9d..4befefc53 100644 --- a/packages/core/src/middleware/koa-auth/index.ts +++ b/packages/core/src/middleware/koa-auth/index.ts @@ -5,7 +5,7 @@ import type { Optional } from '@silverhand/essentials'; import type { JWK } from 'jose'; import { createLocalJWKSet, jwtVerify } from 'jose'; import type { MiddlewareType, Request } from 'koa'; -import type { IRouterParamContext } from 'koa-router'; +import type { IMiddleware, IRouterParamContext } from 'koa-router'; import { z } from 'zod'; import { EnvSet } from '#src/env-set/index.js'; @@ -103,11 +103,17 @@ export const verifyBearerTokenFromRequest = async ( } }; +export const isKoaAuthMiddleware = (function_: Type) => + function_.name === 'authMiddleware'; + export default function koaAuth( envSet: EnvSet, audience: string ): MiddlewareType, ResponseBodyT> { - return async (ctx, next) => { + const authMiddleware: MiddlewareType, ResponseBodyT> = async ( + ctx, + next + ) => { const { sub, clientId, scopes } = await verifyBearerTokenFromRequest( envSet, ctx.request, @@ -126,4 +132,6 @@ export default function koaAuth( koaGuard({ params: object({ id: string().min(1) }), response: scopeResponseGuard.array(), - status: [200, 404], + status: [200, 400, 404], }), async (ctx, next) => { const { diff --git a/packages/core/src/routes/swagger.ts b/packages/core/src/routes/swagger.ts index cd560c39b..0433ae280 100644 --- a/packages/core/src/routes/swagger.ts +++ b/packages/core/src/routes/swagger.ts @@ -4,6 +4,7 @@ import type Router from 'koa-router'; import type { OpenAPIV3 } from 'openapi-types'; import { ZodObject, ZodOptional } from 'zod'; +import { isKoaAuthMiddleware } from '#src/middleware/koa-auth/index.js'; import type { WithGuardConfig } from '#src/middleware/koa-guard.js'; import { isGuardMiddleware } from '#src/middleware/koa-guard.js'; import { fallbackDefaultPageSize, isPaginationMiddleware } from '#src/middleware/koa-pagination.js'; @@ -77,7 +78,11 @@ const buildTag = (path: string) => { return toTitle(root ?? 'General'); }; -const buildOperation = (stack: IMiddleware[], path: string): OpenAPIV3.OperationObject => { +const buildOperation = ( + stack: IMiddleware[], + path: string, + isAuthGuarded: boolean +): OpenAPIV3.OperationObject => { const guard = stack.find((function_): function_ is WithGuardConfig => isGuardMiddleware(function_) ); @@ -101,9 +106,9 @@ const buildOperation = (stack: IMiddleware[], path: string): OpenAPIV3.Operation const hasInputGuard = Boolean(params ?? query ?? body); const responses: OpenAPIV3.ResponsesObject = Object.fromEntries( - deduplicate(conditionalArray(status ?? 200, hasInputGuard && 400)).map< - [number, OpenAPIV3.ResponseObject] - >((status) => { + deduplicate( + conditionalArray(status ?? 200, hasInputGuard && 400, isAuthGuarded && [401, 403]) + ).map<[number, OpenAPIV3.ResponseObject]>((status) => { const description = codeToMessage[status]; if (!description) { @@ -136,6 +141,11 @@ const buildOperation = (stack: IMiddleware[], path: string): OpenAPIV3.Operation }; }; +const isManagementApiRouter = ({ stack }: Router) => + stack + .filter(({ path }) => !path.includes('.*')) + .some(({ stack }) => stack.some((function_) => isKoaAuthMiddleware(function_))); + // Keep using `any` to accept various custom context types. // eslint-disable-next-line @typescript-eslint/no-explicit-any export default function swaggerRoutes>( @@ -143,26 +153,30 @@ export default function swaggerRoutes { - const routes = allRouters.flatMap((router) => - router.stack - // Filter out universal routes (mostly like a proxy route to withtyped) - .filter(({ path }) => !path.includes('.*')) - .flatMap(({ path: routerPath, stack, methods }) => - methods - .map((method) => method.toLowerCase()) - // There is no need to show the HEAD method. - .filter((method): method is OpenAPIV3.HttpMethods => method !== 'head') - .map((httpMethod) => { - const path = `/api${routerPath}`; + const routes = allRouters.flatMap((router) => { + const isAuthGuarded = isManagementApiRouter(router); - return { - path, - method: httpMethod, - operation: buildOperation(stack, routerPath), - }; - }) - ) - ); + return ( + router.stack + // Filter out universal routes (mostly like a proxy route to withtyped) + .filter(({ path }) => !path.includes('.*')) + .flatMap(({ path: routerPath, stack, methods, name }) => + methods + .map((method) => method.toLowerCase()) + // There is no need to show the HEAD method. + .filter((method): method is OpenAPIV3.HttpMethods => method !== 'head') + .map((httpMethod) => { + const path = `/api${routerPath}`; + + return { + path, + method: httpMethod, + operation: buildOperation(stack, routerPath, isAuthGuarded), + }; + }) + ) + ); + }); const pathMap = new Map();