Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pnpm workspaces and improve Nuxt example #1

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion gatsby/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"dotenv": "16.0.0",
"gatsby": "5.9.1",
"next-auth": "workspace:*",
"next-auth": "^4.24.7",
"react": "18.2.0",
"react-dom": "18.2.0"
},
Expand Down
4 changes: 2 additions & 2 deletions nuxt/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
NEXTAUTH_URL=
NEXTAUTH_SECRET=
AUTH_TRUST_HOST=1
AUTH_SECRET=
7 changes: 0 additions & 7 deletions nuxt/.eslintrc.cjs

This file was deleted.

31 changes: 18 additions & 13 deletions nuxt/components/Header.vue
Original file line number Diff line number Diff line change
@@ -1,45 +1,50 @@
<script setup lang="ts">
import { signIn, signOut } from "@/lib/auth/client"
import { signIn, signOut } from '@/lib/auth/client'

const session = useSession()
const { session, loggedIn } = useUserSession()
</script>

<template>
<header>
<div class="signedInStatus">
<p :class="['nojs-show', 'loaded']">
<template v-if="session">
<template v-if="loggedIn">
<span
v-if="session.user?.image"
v-if="session?.user"
:style="{ backgroundImage: `url(${session.user.image})` }"
class="avatar"
/>
<span class="signedInText">
<small>Signed in as</small><br />
<strong>{{ session.user?.email || session.user?.name }}</strong>
<small>Signed in as</small><br>
<strong>{{ session?.user?.email || session?.user?.name }}</strong>
</span>
<a href="/api/auth/signout" class="button" @click.prevent="signOut"
>Sign out</a
>
<b
href="/api/auth/signout"
class="button"
@click.prevent="signOut"
>Sign out</b>
</template>
<template v-else>
<span class="notSignedInText">You are not signed in</span>
<a
href="/api/auth/signin"
class="buttonPrimary"
@click.prevent="signIn"
>Sign in</a
>
>Sign in</a>
</template>
</p>
</div>
<nav>
<ul class="navItems">
<li class="navItem">
<NuxtLink to="/"> Home </NuxtLink>
<NuxtLink to="/">
Home
</NuxtLink>
</li>
<li class="navItem">
<NuxtLink to="/protected"> Protected </NuxtLink>
<NuxtLink to="/protected">
Protected
</NuxtLink>
</li>
</ul>
</nav>
Expand Down
21 changes: 21 additions & 0 deletions nuxt/composables/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Session } from '@auth/core/types'

const useSessionState = () => useState<Session | null>('nuxt-session', () => null)

export function useUserSession() {
const sessionState = useSessionState()

return {
loggedIn: computed(() => Boolean(sessionState.value)),
user: computed(() => sessionState.value || null),
session: sessionState,
fetch,
}
}

async function fetch() {
useSessionState().value = await $fetch<Session | null>('/api/auth/session', {
headers: useRequestHeaders(),
retry: false,
})
}
5 changes: 0 additions & 5 deletions nuxt/composables/useSession.ts

This file was deleted.

8 changes: 8 additions & 0 deletions nuxt/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'

export default createConfigForNuxt({
features: {
tooling: true,
stylistic: true,
},
})
89 changes: 56 additions & 33 deletions nuxt/lib/auth/client.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,45 @@
import type {
LiteralUnion,
SignInOptions,
SignInAuthorizationParams,
SignOutParams,
} from "./types"
import type {
BuiltInProviderType,
RedirectableProviderType,
} from "@auth/core/providers"
} from '@auth/core/providers'

type LiteralUnion<T extends U, U = string> = T | (U & Record<never, never>)

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
}

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
}

/** Match `inputType` of `new URLSearchParams(inputType)` */
export type SignInAuthorizationParams =
| string
| string[][]
| Record<string, string>
| URLSearchParams

/**
* Client-side method to initiate a signin flow
* or send the user to the signin page listing all possible providers.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signin)
* ```ts
* import { signIn } from "@auth/solid-start/client"
* signIn()
* signIn("provider") // example: signIn("github")
* ```
*/
export async function signIn<
P extends RedirectableProviderType | undefined = undefined,
Expand All @@ -25,35 +50,33 @@ export async function signIn<
: BuiltInProviderType
>,
options?: SignInOptions,
authorizationParams?: SignInAuthorizationParams
authorizationParams?: SignInAuthorizationParams,
) {
const { callbackUrl = window.location.href, redirect = true } = options ?? {}

// TODO: Support custom providers
const isCredentials = providerId === "credentials"
const isEmail = providerId === "email"
const isCredentials = providerId === 'credentials'
const isEmail = providerId === 'email'
const isSupportingReturn = isCredentials || isEmail

// TODO: Handle custom base path
const signInUrl = `/api/auth/${
isCredentials ? "callback" : "signin"
isCredentials ? 'callback' : 'signin'
}/${providerId}`

const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`

// TODO: Handle custom base path
// TODO: Remove this since SvelteKit offers the CSRF protection via origin check
const { csrfToken } = await $fetch<{ csrfToken: string }>("/api/auth/csrf")

console.log(_signInUrl)
const csrfTokenResponse = await fetch('/api/auth/csrf')
const { csrfToken } = await csrfTokenResponse.json()

const res = await fetch(_signInUrl, {
method: "post",
method: 'post',
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
'Content-Type': 'application/x-www-form-urlencoded',
'X-Auth-Return-Redirect': '1',
},
// @ts-expect-error -- ignore
// @ts-ignore
body: new URLSearchParams({
...options,
csrfToken,
Expand All @@ -62,36 +85,36 @@ export async function signIn<
})

const data = await res.clone().json()
const error = new URL(data.url).searchParams.get("error")

const error = new URL(data.url).searchParams.get('error')
if (redirect || !isSupportingReturn || !error) {
// TODO: Do not redirect for Credentials and Email providers by default in next major
window.location.href = data.url ?? callbackUrl
window.location.href = data.url ?? data.redirect ?? callbackUrl
// If url contains a hash, the browser does not reload the page. We reload manually
if (data.url.includes("#")) window.location.reload()
if (data.url.includes('#')) window.location.reload()
return
}

return res
}

/**
* Signs the user out, by removing the session cookie.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
* ```ts
* import { signOut } from "@auth/solid-start/client"
* signOut()
* ```
*/
export async function signOut(options?: SignOutParams) {
const { callbackUrl = window.location.href } = options ?? {}
// TODO: Custom base path
// TODO: Remove this since SvelteKit offers the CSRF protection via origin check
const csrfTokenResponse = await fetch("/api/auth/csrf")
const csrfTokenResponse = await fetch('/api/auth/csrf')
const { csrfToken } = await csrfTokenResponse.json()
const res = await fetch(`/api/auth/signout`, {
method: "post",
method: 'post',
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
'Content-Type': 'application/x-www-form-urlencoded',
'X-Auth-Return-Redirect': '1',
},
body: new URLSearchParams({
csrfToken,
Expand All @@ -100,8 +123,8 @@ export async function signOut(options?: SignOutParams) {
})
const data = await res.json()

const url = data.url ?? callbackUrl
const url = data.url ?? data.redirect ?? callbackUrl
window.location.href = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
if (url.includes('#')) window.location.reload()
}
66 changes: 42 additions & 24 deletions nuxt/lib/auth/server.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,56 @@
import { AuthConfig, Session } from "@auth/core/types"
import { Auth } from "@auth/core"
import { fromNodeMiddleware, getRequestURL, H3Event } from "h3"
import { createMiddleware } from "@hattip/adapter-node"
import type { AuthAction, AuthConfig, Session } from '@auth/core/types'
import { Auth, isAuthAction } from '@auth/core'
import type { H3Event } from 'h3'
import { toWebRequest, eventHandler } from 'h3'

export interface NuxtAuthConfig extends AuthConfig {
/**
* Defines the base path for the auth routes.
* @default '/api/auth'
*/
prefix?: string
}

export function NuxtAuthHandler(authOptions: AuthConfig) {
authOptions.basePath ??= '/api/auth'
authOptions.secret ??= process.env.AUTH_SECRET
authOptions.trustHost ??= !!(
process.env.AUTH_TRUST_HOST
?? process.env.VERCEL
?? process.env.NODE_ENV !== 'production'
)

export function NuxtAuthHandler(options: AuthConfig) {
async function handler(ctx: { request: Request }) {
options.trustHost ??= true
return eventHandler(async (event) => {
const request = toWebRequest(event)

return Auth(ctx.request, options)
}
const url = new URL(request.url)
const action = url.pathname
.slice(authOptions.basePath!.length + 1)
.split('/')[0] as AuthAction

const middleware = createMiddleware(handler)
if (isAuthAction(action) && !url.pathname.startsWith(authOptions.basePath + '/')) {
return
}

return fromNodeMiddleware(middleware)
return await Auth(request, authOptions)
})
}

export type GetSessionResult = Promise<Session | null>

export async function getSession(
event: H3Event,
options: AuthConfig
): Promise<Session | null> {
options: Omit<AuthConfig, 'raw'>,
): GetSessionResult {
const req = toWebRequest(event)
options.basePath ??= '/api/auth'
options.secret ??= process.env.AUTH_SECRET
options.trustHost ??= true

const headers = getRequestHeaders(event)
const nodeHeaders = new Headers()

const url = new URL("/api/auth/session", getRequestURL(event))

Object.keys(headers).forEach((key) => {
nodeHeaders.append(key, headers[key] as any)
})

const url = new URL('/api/auth/session', req.url)
const response = await Auth(
new Request(url, { headers: nodeHeaders }),
options
new Request(url, { headers: req.headers }),
options,
)

const { status = 200 } = response
Expand Down
Loading