Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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
51 changes: 41 additions & 10 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
*/
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'
import type { ActionAPIContext } from 'astro/dist/actions/runtime/store'

const actions: AuthAction[] = [
'providers',
Expand All @@ -41,13 +43,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()
Expand Down Expand Up @@ -86,17 +90,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)
Comment on lines +95 to +109
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to have this a bit cleaner, not so repetitive.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @nowaythatworked @JordanSekky

I think this can be moved to the AstroAuthHandler implementation.
Resolving config with await options.config(context) is also could be a part of that handler.

function AstroAuthHandler(lazyConfig = authConfig) {
  return async (context: APIContext) => {
    const config = isUserConfigLazy(lazyConfig) ? await lazyConfig.config(context) : lazyConfig 
    config.secret ??= AUTH_SECRET
    config.trustHost ??= !!(AUTH_TRUST_HOST ?? VERCEL ?? NODE_ENV !== 'production')
    
    const { prefix = '/api/auth', ...options } = config
    
    // The rest remains the same.
  }
}


export function AstroAuth(options = authConfig) {
	const handler = AstroAuthHandler(options)
	return {
		async GET(context: APIContext) {
			return await handler(context)
		},
		async POST(context: APIContext) {
			return await handler(context)
		},
	}
}

return await handler(context)
},
}
Expand All @@ -108,10 +118,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<Session | null> {
if (isUserConfigLazy(options)) {
throw new Error(
'User Auth Configuration is Lazy. Fetch the session using getSessionByContext().'
)
}
Comment on lines +121 to +125
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you're already checking for lazy config, we could have the functionality of getSessionByContext directly in here, right?
The req argument would need to be replaced with context (AstroGlobal, etc) which would be a breaking change, but I think that would be fine? Let me know what you think.

// @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,20 @@ 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.
* @param ctx The Astro global object, or APIContext.
* @returns The current session, or `null` if there is no session.
*/
export async function getSessionByContext(
ctx: AstroGlobal | APIContext | ActionAPIContext,
options = authConfig
): Promise<Session | null> {
const config = isUserConfigLazy(options) ? await options.config(ctx) : options
return await getSession(ctx.request, config)
}

export function isUserConfigLazy(config: UserAuthConfig) {
return 'config' in config
}
10 changes: 9 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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>
}
| FullAuthConfig
Comment on lines +49 to +53

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you guys think about this?

Suggested change
export type UserAuthConfig =
| {
config: (ctx: APIContext | AstroGlobal | ActionAPIContext) => FullAuthConfig | Promise<FullAuthConfig>
}
| FullAuthConfig
export type LazyAuthConfig = (ctx: APIContext | AstroGlobal | ActionAPIContext) => FullAuthConfig | Promise<FullAuthConfig>
export type UserAuthConfig =
| LazyAuthConfig
| 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