diff --git a/docs/1.docs/5.routing.md b/docs/1.docs/5.routing.md index 473112d1bc..e53ebb3558 100644 --- a/docs/1.docs/5.routing.md +++ b/docs/1.docs/5.routing.md @@ -312,7 +312,7 @@ export default defineHandler((event) => { You can use the [utilities available in H3](https://h3.dev/guide/basics/error) to handle errors in both routes and middlewares. -The way errors are sent back to the client depends on the environment. In development, requests with an `Accept` header of `text/html` (such as browsers) will receive a HTML error page. In production, errors are always sent in JSON. +The way errors are sent back to the client depends on the environment. In development, requests with an `Accept` header of `text/html` (such as browsers) will receive a HTML error page. In production, errors are always sent in JSON. This behaviour can be overridden by some request properties (e.g.: `Accept` or `User-Agent` headers). @@ -324,7 +324,7 @@ See [`inlineDynamicImports`](/config#inlinedynamicimports) to bundle everything ## Route rules -Nitro allows you to add logic at the top-level for each route of your configuration. It can be used for redirecting, proxying, caching and adding headers to routes. +Nitro allows you to add logic at the top-level for each route of your configuration. It can be used for redirecting, proxying, caching, authentication, and adding headers to routes. It is a map from route pattern (following [rou3](https://github.com/h3js/rou3)) to route options. @@ -351,6 +351,8 @@ export default defineConfig({ '/old-page/**': { redirect: '/new-page/**' }, '/proxy/example': { proxy: 'https://example.com' }, '/proxy/**': { proxy: '/api/**' }, + '/admin/**': { basicAuth: { username: 'admin', password: 'supersecret' } }, } }); ``` + diff --git a/src/build/virtual/routing.ts b/src/build/virtual/routing.ts index 4f51d4db83..6b0c6d1c6b 100644 --- a/src/build/virtual/routing.ts +++ b/src/build/virtual/routing.ts @@ -1,6 +1,6 @@ import type { Nitro, NitroEventHandler, NitroRouteRules } from "nitro/types"; -export const RuntimeRouteRules = ["headers", "redirect", "proxy", "cache"] as string[]; +export const RuntimeRouteRules = ["headers", "redirect", "proxy", "cache", "basicAuth"] as string[]; export default function routing(nitro: Nitro) { return { diff --git a/src/runtime/internal/route-rules.ts b/src/runtime/internal/route-rules.ts index 88214c5c17..1100bc50a6 100644 --- a/src/runtime/internal/route-rules.ts +++ b/src/runtime/internal/route-rules.ts @@ -1,10 +1,10 @@ -import { proxyRequest, redirect as sendRedirect } from "h3"; -import type { EventHandler, Middleware } from "h3"; +import { proxyRequest, redirect as sendRedirect, requireBasicAuth } from "h3"; +import type { BasicAuthOptions, EventHandler, Middleware } from "h3"; import type { MatchedRouteRule, NitroRouteRules } from "nitro/types"; import { joinURL, withQuery, withoutBase } from "ufo"; import { defineCachedHandler } from "./cache.ts"; -// Note: Remember to update RuntimeRouteRules in src/routing.ts when adding new route rules +// Note: Remember to update RuntimeRouteRules in src/build/virtual/routing.ts when adding new route rules type RouteRuleCtor = (m: MatchedRouteRule) => Middleware; @@ -79,3 +79,13 @@ export const cache: RouteRuleCtor<"cache"> = ((m) => } return cachedHandler(event); }) satisfies RouteRuleCtor<"cache">; + +// basicAuth auth route rule +export const basicAuth: RouteRuleCtor<"auth"> = ((m) => + async function authRouteRule(event, next) { + if (!m.options) { + return; + } + await requireBasicAuth(event, m.options as BasicAuthOptions); + return next(); + }) satisfies RouteRuleCtor<"auth">; diff --git a/src/types/route-rules.ts b/src/types/route-rules.ts index f6ec8e187d..a158b5a3ac 100644 --- a/src/types/route-rules.ts +++ b/src/types/route-rules.ts @@ -1,4 +1,4 @@ -import type { Middleware, ProxyOptions } from "h3"; +import type { Middleware, ProxyOptions, BasicAuthOptions } from "h3"; import type { ExcludeFunctions, IntRange } from "./_utils.ts"; import type { CachedEventHandlerOptions } from "./runtime/index.ts"; @@ -11,6 +11,7 @@ export interface NitroRouteConfig { prerender?: boolean; proxy?: string | ({ to: string } & ProxyOptions); isr?: number /* expiration */ | boolean | VercelISRConfig; + basicAuth?: Pick | false; // Shortcuts cors?: boolean; diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index ba9b84b47c..53fdb7b36b 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -106,6 +106,10 @@ export default defineConfig({ "/rules/_/cached/**": { swr: true }, "/api/proxy/**": { proxy: "/api/echo" }, "/cdn/**": { proxy: "https://cdn.jsdelivr.net/**" }, + "/rules/basic-auth/**": { + basicAuth: { username: "admin", password: "secret", realm: "Secure Area" }, + }, + "/rules/basic-auth/no-auth/**": { basicAuth: false }, "**": { headers: { "x-test": "test" } }, }, prerender: { diff --git a/test/tests.ts b/test/tests.ts index 684406edc3..a32df9bac8 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -386,6 +386,34 @@ export function testNitro( expect(headers).toMatchObject(expectedHeaders); }); + describe("handles route rules - basic auth", () => { + it("rejects request with bad creds", async () => { + const { status, headers } = await callHandler({ + url: "/rules/basic-auth", + headers: { + Authorization: "Basic " + btoa("user:wrongpass"), + }, + }); + expect(status).toBe(401); + expect(headers["www-authenticate"]).toBe('Basic realm="Secure Area"'); + }); + + it("allows request with correct password", async () => { + const { status } = await callHandler({ + url: "/rules/basic-auth/test", + headers: { + Authorization: "Basic " + btoa("admin:secret"), + }, + }); + expect(status).toBe(200); + }); + + it("disabled basic-auth for sub-rules", async () => { + const { status } = await callHandler({ url: "/rules/basic-auth/no-auth" }); + expect(status).toBe(200); + }); + }); + it("handles route rules - allowing overriding", async () => { const override = await callHandler({ url: "/rules/nested/override" }); expect(override.headers.location).toBe("/other");