diff --git a/app/src/adapter/astro/astro.adapter.ts b/app/src/adapter/astro/astro.adapter.ts index 6076750..71ad9f6 100644 --- a/app/src/adapter/astro/astro.adapter.ts +++ b/app/src/adapter/astro/astro.adapter.ts @@ -1,10 +1,8 @@ import { Api, type ApiOptions } from "bknd"; +import { App, type CreateAppConfig } from "bknd"; type TAstro = { - request: { - url: string; - headers: Headers; - }; + request: Request; }; export type Options = { @@ -19,3 +17,15 @@ export function getApi(Astro: TAstro, options: Options = { mode: "static" }) { headers: options.mode === "dynamic" ? Astro.request.headers : undefined }); } + +let app: App; +export function serve(config: CreateAppConfig) { + return async (args: TAstro) => { + if (!app) { + app = App.create(config); + + await app.build(); + } + return app.fetch(args.request); + }; +} diff --git a/app/src/auth/api/AuthController.ts b/app/src/auth/api/AuthController.ts index 5b0de2d..9afd302 100644 --- a/app/src/auth/api/AuthController.ts +++ b/app/src/auth/api/AuthController.ts @@ -11,10 +11,21 @@ export class AuthController implements ClassController { getMiddleware: MiddlewareHandler = async (c, next) => { // @todo: ONLY HOTFIX + // middlewares are added for all routes are registered. But we need to make sure that + // only HTML/JSON routes are adding a cookie to the response. Config updates might + // also use an extension "syntax", e.g. /api/system/patch/data/entities.posts + // This middleware should be extracted and added by each Controller individually, + // but it requires access to the auth secret. + // Note: This doesn't mean endpoints aren't protected, just the cookie is not set. const url = new URL(c.req.url); const last = url.pathname.split("/")?.pop(); const ext = last?.includes(".") ? last.split(".")?.pop() : undefined; - if (ext) { + if ( + !this.auth.authenticator.isJsonRequest(c) && + ["GET", "HEAD", "OPTIONS"].includes(c.req.method) && + ext && + ["js", "css", "png", "jpg", "jpeg", "svg", "ico"].includes(ext) + ) { isDebug() && console.log("Skipping auth", { ext }, url.pathname); } else { const user = await this.auth.authenticator.resolveAuthFromRequest(c); diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 1955643..9040440 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -249,6 +249,7 @@ export class Authenticator = Record< } } + // @todo: move this to a server helper isJsonRequest(c: Context): boolean { //return c.req.header("Content-Type") === "application/x-www-form-urlencoded"; return c.req.header("Content-Type") === "application/json"; diff --git a/bun.lockb b/bun.lockb index 5ea65b5..9717d29 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/integration/astro.mdx b/docs/integration/astro.mdx new file mode 100644 index 0000000..7625494 --- /dev/null +++ b/docs/integration/astro.mdx @@ -0,0 +1,120 @@ +--- +title: 'Astro' +description: 'Run bknd inside Astro' +--- +import InstallBknd from '/snippets/install-bknd.mdx'; + +## Installation +Install bknd as a dependency: + + +For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/): +```bash +npx astro add react +``` + +You also need to make sure to set the output to `hybrid` in your Astro config: +```js {6} +// astro.config.mjs +import { defineConfig } from "astro/config"; +import react from "@astrojs/react"; + +export default defineConfig({ + output: "hybrid", + integrations: [react()] +}); +``` + + + If you don't want to use React with Astro, there is also an option to serve the bknd Admin UI + statically using Astro's middleware. In case you're interested in this, feel free to reach + out in [Discord](https://discord.gg/952SFk8Tb8) or open an [issue on GitHub](https://github.com/bknd-io/bknd/issues/new). + + +## Serve the API +Create a new catch-all route at `src/pages/api/[...api].ts`: +```ts src/pages/api/[...api].ts +import { serve } from "bknd/adapter/astro"; + +export const prerender = false; + +export const ALL = serve({ + connection: { + type: "libsql", + config: { + url: "http://127.0.0.1:8080" + } + } +}); +``` +For more information about the connection object, refer to the [Setup](/setup) guide. In the +special case of astro, you may also use your Astro DB credentials since it's also using LibSQL +under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information. + +## Enabling the Admin UI +Create a new catch-all route at `src/pages/admin/[...admin].astro`: +```jsx src/pages/admin/[...admin].astro +--- +import { Admin } from "bknd/ui"; +import "bknd/dist/styles.css"; + +import { getApi } from "bknd/adapter/astro"; + +const api = getApi(Astro, { mode: "dynamic" }); +const user = api.getUser(); + +export const prerender = false; +--- + + + + + + +``` + +## Example usage of the API +You use the API in both static and SSR pages. Just note that on static pages, authentication +might not work as expected, because Cookies are not available in the static context. + +Here is an example of using the API in static context: +```jsx +--- +import { getApi } from "bknd/adapter/astro"; +const api = getApi(Astro); +const { data } = await api.data.readMany("todos"); +--- + +
    + {data.map((todo) => ( +
  • {todo.title}
  • + ))} +
+``` + +On SSR pages, you can also access the authenticated user: +```jsx +--- +import { getApi } from "bknd/adapter/astro"; +const api = getApi(Astro, { mode: "dynamic" }); +const user = api.getUser(); +const { data } = await api.data.readMany("todos"); + +export const prerender = false; +--- + +{user + ?

Logged in as {user.email}.

+ :

Not authenticated.

} +
    + {data.map((todo) => ( +
  • {todo.title}
  • + ))} +
+``` + +Check the [astro example](https://github.com/bknd-io/bknd/tree/main/examples/astro) for more implementation details. \ No newline at end of file diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 38c4e63..d456f2b 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -46,6 +46,15 @@ in the future, so stay tuned! } href="/integration/remix" /> + + + + + } + href="/integration/astro" + /> diff --git a/docs/mint.json b/docs/mint.json index ecb2342..01f1cb4 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -108,6 +108,7 @@ "integration/bun", "integration/vite", "integration/express", + "integration/astro", "integration/nodejs", "integration/deno", "integration/browser" diff --git a/docs/package.json b/docs/package.json index 3459b6e..f9d266c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -5,6 +5,6 @@ "dev": "mintlify dev" }, "devDependencies": { - "mintlify": "^4.0.269" + "mintlify": "^4.0.285" } } \ No newline at end of file diff --git a/examples/astro/src/pages/api/[...api].ts b/examples/astro/src/pages/api/[...api].ts index 5baa0fa..ac2a896 100644 --- a/examples/astro/src/pages/api/[...api].ts +++ b/examples/astro/src/pages/api/[...api].ts @@ -1,21 +1,12 @@ -import type { APIRoute } from "astro"; -import { App } from "bknd"; +import { serve } from "bknd/adapter/astro"; export const prerender = false; -let app: App; -export const ALL: APIRoute = async ({ request }) => { - if (!app) { - app = App.create({ - connection: { - type: "libsql", - config: { - url: "http://127.0.0.1:8080" - } - } - }); - - await app.build(); +export const ALL = serve({ + connection: { + type: "libsql", + config: { + url: "http://127.0.0.1:8080" + } } - return app.fetch(request); -}; +});