Skip to content

Commit

Permalink
feat: Add APIContext object to Auth Config
Browse files Browse the repository at this point in the history
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 #60, #52, #50.
  • Loading branch information
JordanSekky committed Aug 11, 2024
1 parent e6da1df commit 3577c14
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 22 deletions.
60 changes: 52 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion module.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
declare module 'auth:config' {
const config: import('./src/config').FullAuthConfig
const config: import('./src/config').UserAuthConfig
export default config
}

Expand Down
66 changes: 55 additions & 11 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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()
Expand Down Expand Up @@ -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)
},
}
Expand All @@ -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<Session | null> {
export async function getSession(
req: Request,
options = authConfig
): Promise<Session | null> {
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
Expand All @@ -122,3 +136,33 @@ export async function getSession(req: Request, options = authConfig): Promise<Se
if (status === 200) return data
throw new Error(data.message)
}

/**
* Fetches the current session when using a lazy auth config. Use for fetching the session from Astro Pages.
* @param astro The Astro global object.
* @returns The current session, or `null` if there is no session.
*/
export async function getSessionByGlobal(
astro: AstroGlobal,
options = authConfig
): Promise<Session | null> {
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<Session | null> {
const config = isUserConfigLazy(options) ? await options.endpointConfig(ctx) : options
return await getSession(ctx.request, config)
}

export function isUserConfigLazy(config: UserAuthConfig) {
return 'endpointConfig' in config
}
10 changes: 9 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -43,3 +44,10 @@ export const defineConfig = (config: FullAuthConfig) => {
config.basePath = config.prefix
return config
}

export type UserAuthConfig =
| {
endpointConfig: (ctx: APIContext) => FullAuthConfig | Promise<FullAuthConfig>
pageConfig: (astro: AstroGlobal) => FullAuthConfig | Promise<FullAuthConfig>
}
| FullAuthConfig
2 changes: 1 addition & 1 deletion src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default (config: AstroAuthConfig = {}): AstroIntegration => ({
injectRoute({
pattern: config.prefix + '/[...auth]',
entrypoint: entrypoint,
entryPoint: entrypoint
entryPoint: entrypoint,
})
}

Expand Down

0 comments on commit 3577c14

Please sign in to comment.