diff --git a/README.md b/README.md index cb569c2..1460649 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,48 @@ AUTH_TRUST_HOST=true #### Deploying to Vercel? Setting `AUTH_TRUST_HOST` is not needed, as we also check for an active Vercel environment. -### Requirements -- Node version `>= 17.4` -- Astro config set to output mode `server` -- [SSR](https://docs.astro.build/en/guides/server-side-rendering/) enabled in your Astro project - -Resources: -- [Enabling SSR in Your Project](https://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project) -- [Adding an Adapter](https://docs.astro.build/en/guides/server-side-rendering/#adding-an-adapter) +### Using API Context and Runtime Environment in your Configuration + +Some database providers like Cloudflare D1 provide bindings to your databases in the runtime +environment, which isn't accessible statically, but is provided on each request. You can define +your configuration as a function which accepts the APIContext (for API Routes) or the Astro +global value (for Astro pages/components). + +```ts title="auth.config.ts" +// auth.config.ts +import Patreon from "@auth/core/providers/patreon"; +import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import { defineConfig } from "auth-astro"; +import type { UserAuthConfig } from "auth-astro/src/config"; +import { drizzle } from "drizzle-orm/d1"; + +export default { + config: (ctx) => { + const { env } = ctx.locals.runtime; + const db = env.DB; + return defineConfig({ + secret: env.AUTH_SECRET, + trustHost: env.AUTH_TRUST_HOST === "true", + adapter: DrizzleAdapter(drizzle(db)), + providers: [ + Patreon({ + clientId: env.AUTH_PATREON_ID, + clientSecret: env.AUTH_PATREON_SECRET, + }), + ], + }); + }, +} satisfies UserAuthConfig; +``` + +### requirements +- node version `>= 17.4` +- astro config set to output mode `server` +- [ssr](https://docs.astro.build/en/guides/server-side-rendering/) enabled in your astro project + +resources: +- [enabling ssr in your project](https://docs.astro.build/en/guides/server-side-rendering/#enabling-ssr-in-your-project) +- [adding an adapter](https://docs.astro.build/en/guides/server-side-rendering/#adding-an-adapter) # Usage diff --git a/module.d.ts b/module.d.ts index 30eb1e3..08c2360 100644 --- a/module.d.ts +++ b/module.d.ts @@ -1,5 +1,5 @@ declare module 'auth:config' { - const config: import('./src/config').FullAuthConfig + const config: import('./src/config').UserAuthConfig export default config } diff --git a/server.ts b/server.ts index 4ae1c8b..74252b0 100644 --- a/server.ts +++ b/server.ts @@ -25,9 +25,10 @@ */ import { Auth } from '@auth/core' import type { AuthAction, Session } from '@auth/core/types' -import type { APIContext } from 'astro' +import type { APIContext, AstroGlobal } from 'astro' import { parseString } from 'set-cookie-parser' import authConfig from 'auth:config' +import type { UserAuthConfig } from './src/config' const actions: AuthAction[] = [ 'providers', @@ -41,13 +42,15 @@ const actions: AuthAction[] = [ ] function AstroAuthHandler(prefix: string, options = authConfig) { - return async ({ cookies, request }: APIContext) => { + return async (ctx: APIContext) => { + const { cookies, request } = ctx const url = new URL(request.url) const action = url.pathname.slice(prefix.length + 1).split('/')[0] as AuthAction if (!actions.includes(action) || !url.pathname.startsWith(prefix + '/')) return - const res = await Auth(request, options) + const config = isUserConfigLazy(options) ? await options.config(ctx) : options + const res = await Auth(request, config) if (['callback', 'signin', 'signout'].includes(action)) { // Properly handle multiple Set-Cookie headers (they can't be concatenated in one) const getSetCookie = res.headers.getSetCookie() @@ -86,17 +89,23 @@ export function AstroAuth(options = authConfig) { // @ts-ignore const { AUTH_SECRET, AUTH_TRUST_HOST, VERCEL, NODE_ENV } = import.meta.env - options.secret ??= AUTH_SECRET - options.trustHost ??= !!(AUTH_TRUST_HOST ?? VERCEL ?? NODE_ENV !== 'production') - - const { prefix = '/api/auth', ...authOptions } = options - - const handler = AstroAuthHandler(prefix, authOptions) return { async GET(context: APIContext) { + const config = isUserConfigLazy(options) ? await options.config(context) : options + config.secret ??= AUTH_SECRET + config.trustHost ??= !!(AUTH_TRUST_HOST ?? VERCEL ?? NODE_ENV !== 'production') + + const { prefix = '/api/auth', ...authOptions } = config + const handler = AstroAuthHandler(prefix, authOptions) return await handler(context) }, async POST(context: APIContext) { + const config = isUserConfigLazy(options) ? await options.config(context) : options + config.secret ??= AUTH_SECRET + config.trustHost ??= !!(AUTH_TRUST_HOST ?? VERCEL ?? NODE_ENV !== 'production') + + const { prefix = '/api/auth', ...authOptions } = config + const handler = AstroAuthHandler(prefix, authOptions) return await handler(context) }, } @@ -108,10 +117,14 @@ export function AstroAuth(options = authConfig) { * @returns The current session, or `null` if there is no session. */ export async function getSession(req: Request, options = authConfig): Promise { + if (isUserConfigLazy(options)) { + throw new Error( + 'User Auth Configuration is Lazy. Fetch the session using getSessionByContext().' + ) + } // @ts-ignore options.secret ??= import.meta.env.AUTH_SECRET options.trustHost ??= true - const url = new URL(`${options.prefix}/session`, req.url) const response = await Auth(new Request(url, { headers: req.headers }), options) const { status = 200 } = response @@ -122,3 +135,20 @@ export async function getSession(req: Request, options = authConfig): Promise { + const config = isUserConfigLazy(options) ? await options.config(ctx) : options + return await getSession(ctx.request, config) +} + +export function isUserConfigLazy(config: UserAuthConfig) { + return 'config' in config +} diff --git a/src/config.ts b/src/config.ts index 509ed5a..8699b50 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,7 @@ import type { PluginOption } from 'vite' import type { AuthConfig } from '@auth/core/types' +import type { APIContext, AstroGlobal } from 'astro' +import type { ActionAPIContext } from 'astro/dist/actions/runtime/store' export const virtualConfigModule = (configFile: string = './auth.config'): PluginOption => { const virtualModuleId = 'auth:config' @@ -27,7 +29,7 @@ export interface AstroAuthConfig { */ prefix?: string /** - * Defineds wether or not you want the integration to handle the API routes + * Defines whether or not you want the integration to handle the API routes * @default true */ injectEndpoints?: boolean @@ -43,3 +45,9 @@ export const defineConfig = (config: FullAuthConfig) => { config.basePath = config.prefix return config } + +export type UserAuthConfig = + | { + config: (ctx: APIContext | AstroGlobal | ActionAPIContext) => FullAuthConfig | Promise + } + | FullAuthConfig diff --git a/src/integration.ts b/src/integration.ts index 5e5fcbe..4c68061 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -32,7 +32,7 @@ export default (config: AstroAuthConfig = {}): AstroIntegration => ({ injectRoute({ pattern: config.prefix + '/[...auth]', entrypoint: entrypoint, - entryPoint: entrypoint + entryPoint: entrypoint, }) }