Skip to content

Commit

Permalink
refactor(auth-js): session provider (#775)
Browse files Browse the repository at this point in the history
* refactor: session provider

* format
  • Loading branch information
divyam234 authored Oct 20, 2024
1 parent 9506e3c commit c19b51b
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 408 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-tips-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/auth-js': patch
---

refactor session provider
106 changes: 41 additions & 65 deletions packages/auth-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,78 +16,72 @@ Before starting using the middleware you must set the following environment vari

```plain
AUTH_SECRET=#required
AUTH_URL=#optional
AUTH_URL=https://example.com/api/auth
```

## How to Use

```ts
import { Hono, Context } from 'hono'
import { authHandler, initAuthConfig, verifyAuth, type AuthConfig } from "@hono/auth-js"
import GitHub from "@auth/core/providers/github"
import { Hono } from 'hono'
import { authHandler, initAuthConfig, verifyAuth } from '@hono/auth-js'
import GitHub from '@auth/core/providers/github'

const app = new Hono()

app.use("*", initAuthConfig(getAuthConfig))
app.use(
'*',
initAuthConfig((c) => ({
secret: c.env.AUTH_SECRET,
providers: [
GitHub({
clientId: c.env.GITHUB_ID,
clientSecret: c.env.GITHUB_SECRET,
}),
],
}))
)

app.use("/api/auth/*", authHandler())
app.use('/api/auth/*', authHandler())

app.use('/api/*', verifyAuth())

app.get('/api/protected', (c) => {
const auth = c.get("authUser")
const auth = c.get('authUser')
return c.json(auth)
})

function getAuthConfig(c: Context): AuthConfig {
return {
secret: c.env.AUTH_SECRET,
providers: [
GitHub({
clientId: c.env.GITHUB_ID,
clientSecret: c.env.GITHUB_SECRET
}),
]
}
}

export default app
```

React component
```tsx
import { SessionProvider } from "@hono/auth-js/react"

export default function App() {
```tsx
import { SessionProvider, useSession } from '@hono/auth-js/react'

export default function App() {
return (
<SessionProvider>
<Children />
<Children />
</SessionProvider>
)
}

function Children() {
const { data: session, status } = useSession()
return (
<div >
I am {session?.user}
</div>
)
return <div>I am {session?.user}</div>
}
```

Default `/api/auth` path can be changed to something else but that will also require you to change path in react app.

```tsx
import {SessionProvider,authConfigManager,useSession } from "@hono/auth-js/react"
import { SessionProvider, authConfigManager, useSession } from '@hono/auth-js/react'

authConfigManager.setConfig({
baseUrl: '', //needed for cross domain setup.
basePath: '/custom', // if auth route is diff from /api/auth
credentials:'same-origin' //needed for cross domain setup
});
})

export default function App() {
export default function App() {
return (
<SessionProvider>
<Children />
Expand All @@ -97,45 +91,27 @@ export default function App() {

function Children() {
const { data: session, status } = useSession()
return (
<div >
I am {session?.user}
</div>
)
return <div>I am {session?.user}</div>
}
```
For cross domain setup as mentioned above you need to set these cors headers in hono along with change in same site cookie attribute.[Read More Here](https://next-auth.js.org/configuration/options#cookies)
``` ts
app.use(
"*",
cors({
origin: (origin) => origin,
allowHeaders: ["Content-Type"],
credentials: true,
})
)
```


SessionProvider is not needed with react query.This wrapper is enough
SessionProvider is not needed with react query.Use useQuery hook to fetch session data.

```ts
const useSession = ()=>{
const { data ,status } = useQuery({
queryKey: ["session"],
queryFn: async () => {
const res = await fetch("/api/auth/session")
return res.json();
},
staleTime: 5 * (60 * 1000),
gcTime: 10 * (60 * 1000),
refetchOnWindowFocus: true,
})
return { session:data, status }
const useSession = () => {
const { data, status } = useQuery({
queryKey: ['session'],
queryFn: async () => {
const res = await fetch('/api/auth/session')
return res.json()
},
staleTime: 5 * (60 * 1000),
gcTime: 10 * (60 * 1000),
refetchOnWindowFocus: true,
})
return { session: data, status }
}
```
> [!WARNING]
> You can't use event updates which SessionProvider provides and session will not be in sync across tabs if you use react query wrapper but in RQ5 you can enable this using Broadcast channel (see RQ docs).

Working example repo https://github.com/divyam234/next-auth-hono-react

Expand Down
70 changes: 42 additions & 28 deletions packages/auth-js/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { AuthError } from '@auth/core/errors'
import type { BuiltInProviderType, ProviderType } from '@auth/core/providers'
import type { LoggerInstance, Session } from '@auth/core/types'
import * as React from 'react'
import { useEffect, useState } from 'react'

class ClientFetchError extends AuthError {}

export class ClientSessionError extends AuthError {}

export interface GetSessionParams {
event?: 'storage' | 'timer' | 'hidden' | string
triggerEvent?: boolean
}

export interface AuthClientConfig {
baseUrl: string
basePath: string
credentials?: RequestCredentials
_session?: Session | null | undefined
_lastSync: number
_getSession: (...args: any[]) => any
credentials: RequestCredentials
lastSync: number
session: Session | null
fetchSession: (params?: GetSessionParams) => Promise<void>
}

export interface UseSessionOptions<R extends boolean> {
Expand All @@ -32,13 +37,7 @@ export interface ClientSafeProvider {
}

export interface SignInOptions extends Record<string, unknown> {
/**
* Specify to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from.
*
* [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl)
*/
callbackUrl?: string
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option) */
redirect?: boolean
}

Expand All @@ -60,26 +59,39 @@ export interface SignOutResponse {
}

export interface SignOutParams<R extends boolean = true> {
/** [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl-1) */
callbackUrl?: string
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */
redirect?: R
}


export interface SessionProviderProps {
children: React.ReactNode
session?: Session | null
baseUrl?: string
basePath?: string
refetchInterval?: number
refetchOnWindowFocus?: boolean
refetchWhenOffline?: false
}

export type UpdateSession = (data?: any) => Promise<Session | null>

export type SessionContextValue<R extends boolean = false> = R extends true
?
| { update: UpdateSession; data: Session; status: 'authenticated' }
| { update: UpdateSession; data: null; status: 'loading' }
:
| { update: UpdateSession; data: Session; status: 'authenticated' }
| {
update: UpdateSession
data: null
status: 'unauthenticated' | 'loading'
}

export async function fetchData<T = any>(
path: string,
config: AuthClientConfig,
config: {
baseUrl: string
basePath: string
credentials: RequestCredentials
},
logger: LoggerInstance,
req: any = {}
): Promise<T | null> {
Expand Down Expand Up @@ -111,20 +123,22 @@ export async function fetchData<T = any>(
}

export function useOnline() {
const [isOnline, setIsOnline] = React.useState(
const [isOnline, setIsOnline] = useState(
typeof navigator !== 'undefined' ? navigator.onLine : false
)

React.useEffect(() => {
useEffect(() => {
const abortController = new AbortController()
const { signal } = abortController

const setOnline = () => setIsOnline(true)
const setOffline = () => setIsOnline(false)

window.addEventListener('online', setOnline)
window.addEventListener('offline', setOffline)
window.addEventListener('online', setOnline, { signal })
window.addEventListener('offline', setOffline, { signal })

return () => {
window.removeEventListener('online', setOnline)
window.removeEventListener('offline', setOffline)
abortController.abort()
}
}, [])

Expand All @@ -136,17 +150,17 @@ export function now() {
}

export function parseUrl(url?: string) {
const defaultUrl = 'http://localhost:3000/api/auth';
const parsedUrl = new URL(url?.startsWith('http') ? url : `https://${url}` || defaultUrl);
const defaultUrl = 'http://localhost:3000/api/auth'
const parsedUrl = new URL(url?.startsWith('http') ? url : `https://${url}` || defaultUrl)

const path = parsedUrl.pathname === '/' ? '/api/auth' : parsedUrl.pathname.replace(/\/$/, '');
const base = `${parsedUrl.origin}${path}`;
const path = parsedUrl.pathname === '/' ? '/api/auth' : parsedUrl.pathname.replace(/\/$/, '')
const base = `${parsedUrl.origin}${path}`

return {
origin: parsedUrl.origin,
host: parsedUrl.host,
path,
base,
toString: () => base,
};
}
}
45 changes: 21 additions & 24 deletions packages/auth-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,10 @@ async function cloneRequest(input: URL | string, request: Request, headers?: Hea
headers: headers ?? new Headers(request.headers),
body:
request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.blob(),
// @ts-ignore: TS2353
referrer: 'referrer' in request ? (request.referrer as string) : undefined,
// deno-lint-ignore no-explicit-any
referrerPolicy: request.referrerPolicy as any,
referrerPolicy: request.referrerPolicy,
mode: request.mode,
credentials: request.credentials,
// @ts-ignore: TS2353
cache: request.cache,
redirect: request.redirect,
integrity: request.integrity,
Expand All @@ -66,25 +63,26 @@ export async function reqWithEnvUrl(req: Request, authUrl?: string) {
const reqUrlObj = new URL(req.url)
const authUrlObj = new URL(authUrl)
const props = ['hostname', 'protocol', 'port', 'password', 'username'] as const
props.forEach((prop) => (reqUrlObj[prop] = authUrlObj[prop]))
return cloneRequest(reqUrlObj.href, req)
} else {
const url = new URL(req.url)
const headers = new Headers(req.headers)
const proto = headers.get('x-forwarded-proto')
const host = headers.get('x-forwarded-host') ?? headers.get('host')
if (proto != null) url.protocol = proto.endsWith(':') ? proto : proto + ':'
if (host != null) {
url.host = host
const portMatch = host.match(/:(\d+)$/)
if (portMatch) url.port = portMatch[1]
else url.port = ''
headers.delete('x-forwarded-host')
headers.delete('Host')
headers.set('Host', host)
for (const prop of props) {
if (authUrlObj[prop]) reqUrlObj[prop] = authUrlObj[prop]
}
return cloneRequest(url.href, req, headers)
return cloneRequest(reqUrlObj.href, req)
}
const url = new URL(req.url)
const headers = new Headers(req.headers)
const proto = headers.get('x-forwarded-proto')
const host = headers.get('x-forwarded-host') ?? headers.get('host')
if (proto != null) url.protocol = proto.endsWith(':') ? proto : `${proto}:`
if (host != null) {
url.host = host
const portMatch = host.match(/:(\d+)$/)
if (portMatch) url.port = portMatch[1]
else url.port = ''
headers.delete('x-forwarded-host')
headers.delete('Host')
headers.set('Host', host)
}
return cloneRequest(url.href, req, headers)
}

export async function getAuthUser(c: Context): Promise<AuthUser | null> {
Expand Down Expand Up @@ -114,7 +112,7 @@ export async function getAuthUser(c: Context): Promise<AuthUser | null> {

const session = (await response.json()) as Session | null

return session && session.user ? authUser : null
return session?.user ? authUser : null
}

export function verifyAuth(): MiddlewareHandler {
Expand All @@ -126,9 +124,8 @@ export function verifyAuth(): MiddlewareHandler {
status: 401,
})
throw new HTTPException(401, { res })
} else {
c.set('authUser', authUser)
}
c.set('authUser', authUser)

await next()
}
Expand Down
Loading

0 comments on commit c19b51b

Please sign in to comment.