diff --git a/bun.lockb b/bun.lockb index cee7838..b36b12b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/nuxt-configuration.md b/docs/nuxt-configuration.md index d9dd42d..768ec22 100644 --- a/docs/nuxt-configuration.md +++ b/docs/nuxt-configuration.md @@ -14,6 +14,7 @@ export default defineNuxtConfig({ // guestRedirectTo: "/", // where to redirect if the user is not authenticated // authenticatedRedirectTo: "/", // where to redirect if the user is authenticated // baseUrl: "" // should be something like https://www.my-app.com + // basePath: "/api/auth" // must match catch-all NuxtAuthHandler route // }, runtimeConfig: { authJs: { diff --git a/packages/authjs-nuxt/package.json b/packages/authjs-nuxt/package.json index b908883..be85397 100644 --- a/packages/authjs-nuxt/package.json +++ b/packages/authjs-nuxt/package.json @@ -50,6 +50,7 @@ "defu": "^6.1.2", "immer": "^10.0.3", "jose": "^4.15.4", + "ufo": "^1.4.0", "unctx": "^2.3.1" }, "devDependencies": { diff --git a/packages/authjs-nuxt/src/auth-config.d.ts b/packages/authjs-nuxt/src/auth-config.d.ts new file mode 100644 index 0000000..c56e3bf --- /dev/null +++ b/packages/authjs-nuxt/src/auth-config.d.ts @@ -0,0 +1,7 @@ +declare module "#auth-config" { + export const verifyClientOnEveryRequest: boolean + export const guestRedirectTo: string + export const authenticatedRedirectTo: string + export const baseUrl: string + export const basePath: string +} \ No newline at end of file diff --git a/packages/authjs-nuxt/src/module.ts b/packages/authjs-nuxt/src/module.ts index f272c5e..26730ce 100644 --- a/packages/authjs-nuxt/src/module.ts +++ b/packages/authjs-nuxt/src/module.ts @@ -1,4 +1,4 @@ -import { addImports, addPlugin, addRouteMiddleware, addTypeTemplate, createResolver, defineNuxtModule, useLogger } from "@nuxt/kit" +import { addImports, addPlugin, addRouteMiddleware, addTemplate, addTypeTemplate, createResolver, defineNuxtModule, useLogger } from "@nuxt/kit" import { defu } from "defu" import { configKey } from "./runtime/utils" @@ -9,6 +9,7 @@ export interface ModuleOptions { guestRedirectTo?: string authenticatedRedirectTo?: string baseUrl: string + basePath?: string } export default defineNuxtModule({ @@ -20,7 +21,8 @@ export default defineNuxtModule({ verifyClientOnEveryRequest: true, guestRedirectTo: "/", authenticatedRedirectTo: "/", - baseUrl: "" + baseUrl: "", + basePath: "/api/auth" }, setup(userOptions, nuxt) { const logger = useLogger(NAME) @@ -50,7 +52,19 @@ export default defineNuxtModule({ nuxt.options.alias["#auth"] = resolve("./runtime/lib/client") // nuxt.options.build.transpile.push(resolve("./runtime/lib/client")) This doesn't look it's needed ? - // 4. Add types + // 5. Add auth config + const { dst: authConfigDst } = addTemplate({ + filename: "auth.config.mjs", + write: true, + getContents: () => [ + "// Generated by @auth/nuxt module", + "", + ...Object.entries(options).map(([key, value]) => `export const ${key} = ${JSON.stringify(value)}`) + ].join("\n") + }) + nuxt.options.alias["#auth-config"] = authConfigDst + + // 6. Add types addTypeTemplate({ filename: "types/auth.d.ts", write: true, @@ -62,6 +76,10 @@ export default defineNuxtModule({ ` const getServerSession: typeof import('${resolve("./runtime/lib/server")}').getServerSession`, ` const NuxtAuthHandler: typeof import('${resolve("./runtime/lib/server")}').NuxtAuthHandler`, ` const getServerToken: typeof import('${resolve("./runtime/lib/server")}').getServerToken`, + "}", + "", + "declare module '#auth-config' {", + ...Object.keys(options).map(key => ` const ${key}: typeof import('${authConfigDst}').${key}`), "}" ].join("\n") }) diff --git a/packages/authjs-nuxt/src/runtime/lib/client.ts b/packages/authjs-nuxt/src/runtime/lib/client.ts index 1c94140..0ddff3f 100644 --- a/packages/authjs-nuxt/src/runtime/lib/client.ts +++ b/packages/authjs-nuxt/src/runtime/lib/client.ts @@ -2,11 +2,14 @@ // @todo find a way to make this reference work import type { BuiltInProviderType, Provider, RedirectableProviderType } from "@auth/core/providers" import type { Session } from "@auth/core/types" +import { joinURL } from "ufo" import { makeNativeHeadersFromCookieObject } from "../utils" import { useAuth } from "../composables/useAuth" import type { LiteralUnion, SignInAuthorizationParams, SignInOptions, SignOutParams } from "./types" import { navigateTo, reloadNuxtApp, useRouter } from "#imports" +import { basePath } from "#auth-config" + async function postToInternal({ url, options, @@ -73,7 +76,7 @@ export async function signIn

("/api/auth/signout", { + const data = await $fetch<{ url: string }>(joinURL(basePath, "signout"), { method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded", @@ -148,7 +151,7 @@ export async function signOut(options?: SignOutParams) { * See [OAuth Sign In](https://authjs.dev/guides/basics/pages#oauth-sign-in) for more details. */ export async function getProviders() { - return $fetch[]>("/api/auth/providers") + return $fetch[]>(joinURL(basePath, "providers")) } /** * Verify if the user session is still valid @@ -161,7 +164,7 @@ export async function verifyClientSession() { if (cookies.value === null) throw new Error("No session found") - const data = await $fetch("/api/auth/session", { + const data = await $fetch(joinURL(basePath, "session"), { headers: makeNativeHeadersFromCookieObject(cookies.value) }) const hasSession = data && Object.keys(data).length diff --git a/packages/authjs-nuxt/src/runtime/lib/server.ts b/packages/authjs-nuxt/src/runtime/lib/server.ts index effe854..d4084a3 100644 --- a/packages/authjs-nuxt/src/runtime/lib/server.ts +++ b/packages/authjs-nuxt/src/runtime/lib/server.ts @@ -4,8 +4,11 @@ import type { H3Event } from "h3" import { eventHandler, getRequestHeaders, getRequestURL } from "h3" import type { AuthConfig, Session } from "@auth/core/types" import { getToken } from "@auth/core/jwt" +import { parseURL, resolveURL, stringifyParsedURL } from "ufo" import { checkOrigin, getAuthJsSecret, getRequestFromEvent, getServerOrigin, makeCookiesFromCookieString } from "../utils" +import { basePath } from "#auth-config" + if (!globalThis.crypto) { // eslint-disable-next-line no-console console.log("Polyfilling crypto...") @@ -18,6 +21,16 @@ if (!globalThis.crypto) { }) } +/** + * Returns the domain part of a given URL. + * @param url string|URL - The URL to extract the domain from + * @returns The domain part of the URL + */ +function getDomain(url: string | URL): string { + const { protocol, auth, host } = parseURL(String(url)) + return stringifyParsedURL({ protocol, auth, host }) +} + /** * This is the event handler for the catch-all route. * Everything can be customized by adding a custom route that takes priority over the handler. @@ -84,7 +97,7 @@ async function getServerSessionResponse( options: AuthConfig ) { options.trustHost ??= true - const url = new URL("/api/auth/session", getRequestURL(event)) + const url = resolveURL(getDomain(getRequestURL(event)), basePath, "session") return Auth( new Request(url, { headers: getRequestHeaders(event) as any }), options diff --git a/packages/authjs-nuxt/src/runtime/plugin.ts b/packages/authjs-nuxt/src/runtime/plugin.ts index 522d9df..ed3e6ce 100644 --- a/packages/authjs-nuxt/src/runtime/plugin.ts +++ b/packages/authjs-nuxt/src/runtime/plugin.ts @@ -1,14 +1,17 @@ import type { Session } from "@auth/core/types" +import { joinURL } from "ufo" import { useAuth } from "./composables/useAuth" import { makeCookiesFromCookieString } from "./utils" import { defineNuxtPlugin, useRequestHeaders } from "#app" +import { basePath } from "#auth-config" + export default defineNuxtPlugin(async () => { // We try to get the session when the app SSRs. No need to repeat this on the client. if (import.meta.server) { const { updateSession, removeSession, cookies } = useAuth() const headers = useRequestHeaders() as any - const data = await $fetch("/api/auth/session", { + const data = await $fetch(joinURL(basePath, "session"), { headers }) const hasSession = data && Object.keys(data).length diff --git a/test/custom-base-path.test.ts b/test/custom-base-path.test.ts new file mode 100644 index 0000000..f61fe23 --- /dev/null +++ b/test/custom-base-path.test.ts @@ -0,0 +1,46 @@ +import { fileURLToPath } from "node:url" +import { describe, expect, it } from "vitest" +import { $fetch, setup } from "@nuxt/test-utils" +import { createPage } from "@nuxt/test-utils/e2e" + +describe("custom base path test", async () => { + await setup({ + rootDir: fileURLToPath(new URL("./fixtures/custom-base-path", import.meta.url)) + }) + + it("displays homepage", async () => { + const html = await $fetch("/") + expect(html).toContain("Sign In") + }) + + it("displays signin page", async () => { + const page = await $fetch("/custom/auth/signin") + expect(page).toContain("Username") + expect(page).toContain("Password") + }) + + it("fetches auth providers", async () => { + const json = await $fetch("/custom/auth/providers") + expect(json).toMatchObject(expect.objectContaining({ + credentials: expect.objectContaining({ + id: "credentials", + name: "Credentials", + type: "credentials", + signinUrl: expect.stringMatching(/\/custom\/auth\/signin\/credentials$/), + callbackUrl: expect.stringMatching(/\/custom\/auth\/callback\/credentials$/) + }) + })) + }) + + // @todo: requires install playwright-core + it.skip("can signin", async () => { + const page = await createPage("/custom/auth/signin") + expect(await page.title()).toBe("Sign In") + + await page.getByLabel("Username").fill("admin") + await page.getByLabel("Password").fill("admin") + await page.locator("#submitButton").click() + + expect(await page.getByText("authenticated")).toBeTruthy() + }) +}) diff --git a/test/fixtures/custom-base-path/nuxt.config.ts b/test/fixtures/custom-base-path/nuxt.config.ts new file mode 100644 index 0000000..1f32ae5 --- /dev/null +++ b/test/fixtures/custom-base-path/nuxt.config.ts @@ -0,0 +1,18 @@ +export default defineNuxtConfig({ + modules: ["../../packages/authjs-nuxt/src/module.ts"], + authJs: { + baseUrl: "http://localhost:3000", + basePath: "/custom/auth", + verifyClientOnEveryRequest: true + }, + experimental: { + renderJsonPayloads: true + }, + runtimeConfig: { + authJs: { secret: "/OEjlRC2DK74ZEj5nl8qHNy+E6/JptnouIyHnANbBz0=" }, + github: { + clientId: "", + clientSecret: "" + } + } +}) diff --git a/test/fixtures/custom-base-path/package.json b/test/fixtures/custom-base-path/package.json new file mode 100644 index 0000000..e95422c --- /dev/null +++ b/test/fixtures/custom-base-path/package.json @@ -0,0 +1,8 @@ +{ + "name": "basic-fixture", + "private": true, + "dependencies": { + "@hebilicious/authjs-nuxt": "latest", + "nuxt": "3.7.4" + } +} diff --git a/test/fixtures/custom-base-path/pages/index.vue b/test/fixtures/custom-base-path/pages/index.vue new file mode 100644 index 0000000..956b043 --- /dev/null +++ b/test/fixtures/custom-base-path/pages/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/test/fixtures/custom-base-path/pages/private.vue b/test/fixtures/custom-base-path/pages/private.vue new file mode 100644 index 0000000..c9f18d5 --- /dev/null +++ b/test/fixtures/custom-base-path/pages/private.vue @@ -0,0 +1,7 @@ + + + diff --git a/test/fixtures/custom-base-path/server/routes/custom/auth/[...].ts b/test/fixtures/custom-base-path/server/routes/custom/auth/[...].ts new file mode 100644 index 0000000..d109efc --- /dev/null +++ b/test/fixtures/custom-base-path/server/routes/custom/auth/[...].ts @@ -0,0 +1,36 @@ +import CredentialsProvider from "@auth/core/providers/credentials" +import type { AuthConfig } from "@auth/core/types" + +import { NuxtAuthHandler } from "#auth" + +// The #auth virtual import comes from this module. You can use it on the client +// and server side, however not every export is universal. For example do not +// use sign-in and sign-out on the server side. + +const runtimeConfig = useRuntimeConfig() + +// Refer to Auth.js docs for more details + +export const authOptions: AuthConfig = { + secret: runtimeConfig.authJs.secret, + providers: [ + CredentialsProvider({ + credentials: { + username: { label: "Username", type: "text", placeholder: "admin" }, + password: { label: "Password", type: "password" } + }, + async authorize(credentials) { + if ( + credentials.username === "admin" + && credentials.password === "admin" + ) + return { id: "1", name: "admin", email: "admin", role: "test" } + return null + } + }) + ] +} + +export default NuxtAuthHandler(authOptions, runtimeConfig) +// If you don't want to pass the full runtime config, +// you can pass something like this: { public: { authJs: { baseUrl: "" } } }