From 3577c14015aeda38b601305e94ad0362938d43a0 Mon Sep 17 00:00:00 2001 From: Jordan Sechler Date: Sat, 10 Aug 2024 23:52:13 -0400 Subject: [PATCH] feat: Add APIContext object to Auth Config Adds an option to the Auth Config file where users can define functions which accept the Astro global object (for Astro pages/components) or the APIContext object (for API Routes). This allows Cloudflare D1 users to use the environment provided by the cloudflare bindings, rather the the vite environment variables. Adds getSessionByGlobal() and getSessionByApiContext(), which allow fetching the session when using a lazy auth config. Fixes nowaythatworked#60, nowaythatworked#52, nowaythatworked#50. --- README.md | 60 +++++++++++++++++++++++++++++++++++------ module.d.ts | 2 +- server.ts | 66 ++++++++++++++++++++++++++++++++++++++-------- src/config.ts | 10 ++++++- src/integration.ts | 2 +- 5 files changed, 118 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index cb569c2..9bb3f9f 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,58 @@ 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) and a function +which accepts 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 type { APIContext } from "astro"; +import { defineConfig } from "auth-astro"; +import type { FullAuthConfig } from "auth-astro/src/config"; +import type { UserAuthConfig } from "auth-astro/src/config"; +import { drizzle } from "drizzle-orm/d1"; + +type RuntimeEnvironment = APIContext["locals"]["runtime"]["env"]; + +const configFromEnv = (env: RuntimeEnvironment): FullAuthConfig => { + 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, + }), + ], + }); +}; + +export default { + endpointConfig: (ctx) => { + return configFromEnv(ctx.locals.runtime.env); + }, + pageConfig: (astro) => { + return configFromEnv(astro.locals.runtime.env); + }, +} 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..15ad79f 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.endpointConfig(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.endpointConfig(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.endpointConfig(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) }, } @@ -107,11 +116,16 @@ export function AstroAuth(options = authConfig) { * @param req The request object. * @returns The current session, or `null` if there is no session. */ -export async function getSession(req: Request, options = authConfig): Promise { +export async function getSession( + req: Request, + options = authConfig +): Promise { + if (isUserConfigLazy(options)) { + throw new Error("User Auth Configuration is Lazy. Fetch the session using getSessionByGlobal() (for Astro Pages / Components) or getSessionByApiContext() (for API Routes).") + } // @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 +136,33 @@ export async function getSession(req: Request, options = authConfig): Promise { + const config = isUserConfigLazy(options) ? await options.pageConfig(astro) : options + return await getSession(astro.request, config) +} + +/** + * Fetches the current session when using a lazy auth config. Use for fetching the session from API Routes. + * @param ctx The APIContext object. + * @returns The current session, or `null` if there is no session. + */ +export async function getSessionByApiContext( + ctx: APIContext, + options = authConfig +): Promise { + const config = isUserConfigLazy(options) ? await options.endpointConfig(ctx) : options + return await getSession(ctx.request, config) +} + +export function isUserConfigLazy(config: UserAuthConfig) { + return 'endpointConfig' in config +} diff --git a/src/config.ts b/src/config.ts index 509ed5a..c6a7198 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,6 @@ import type { PluginOption } from 'vite' import type { AuthConfig } from '@auth/core/types' +import type { APIContext, AstroGlobal } from 'astro' export const virtualConfigModule = (configFile: string = './auth.config'): PluginOption => { const virtualModuleId = 'auth:config' @@ -27,7 +28,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 +44,10 @@ export const defineConfig = (config: FullAuthConfig) => { config.basePath = config.prefix return config } + +export type UserAuthConfig = + | { + endpointConfig: (ctx: APIContext) => FullAuthConfig | Promise + pageConfig: (astro: AstroGlobal) => 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, }) }