diff --git a/.changeset/green-donkeys-decide.md b/.changeset/green-donkeys-decide.md new file mode 100644 index 00000000..e33e7024 --- /dev/null +++ b/.changeset/green-donkeys-decide.md @@ -0,0 +1,5 @@ +--- +"@logto/nuxt": minor +--- + +export logtoEventHandler diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 835627a7..65d6cfb2 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -10,6 +10,7 @@ export * from '@logto/node'; export { default as LogtoNodeClient } from '@logto/node'; export * from './runtime/utils/types'; export * from './runtime/utils/constants'; +export * from './runtime/utils/handler'; const logtoModule: NuxtModule = defineNuxtModule({ meta: { diff --git a/packages/nuxt/src/runtime/server/event-handler.ts b/packages/nuxt/src/runtime/server/event-handler.ts index a172b50c..ff005da8 100644 --- a/packages/nuxt/src/runtime/server/event-handler.ts +++ b/packages/nuxt/src/runtime/server/event-handler.ts @@ -1,100 +1,10 @@ -import LogtoClient, { CookieStorage } from '@logto/node'; -import { trySafe } from '@silverhand/essentials'; -import { defineEventHandler, getRequestURL, getCookie, setCookie, sendRedirect } from 'h3'; +import { defineEventHandler } from 'h3'; import { useRuntimeConfig } from '#imports'; -import { defaults } from '../utils/constants'; -import { type LogtoRuntimeConfig } from '../utils/types'; +import { logtoEventHandler } from '../utils/handler'; export default defineEventHandler(async (event) => { const config = useRuntimeConfig(event); - - // eslint-disable-next-line no-restricted-syntax -- Optional fields are not inferred - const logtoConfig = config.logto as LogtoRuntimeConfig; - const { - cookieName, - cookieEncryptionKey, - cookieSecure, - fetchUserInfo, - pathnames, - postCallbackRedirectUri, - postLogoutRedirectUri, - customRedirectBaseUrl, - signInOptions, - ...clientConfig - } = logtoConfig; - - const defaultValueKeys = Object.entries(defaults) - // @ts-expect-error The type of `key` can only be string - .filter(([key, value]) => logtoConfig[key] === value) - .map(([key]) => key); - - if (defaultValueKeys.length > 0) { - console.warn( - `The following Logto configuration keys have default values: ${defaultValueKeys.join( - ', ' - )}. Please replace them with your own values.` - ); - } - - const requestUrl = getRequestURL(event); - - /** - * This approach allows us to: - * 1. Override the base URL when necessary (e.g., in proxy environments) - * 2. Preserve the original path and query parameters - * 3. Fall back to the original URL when no custom base is provided - * - * It's particularly useful in scenarios where the application is deployed - * behind a reverse proxy or in environments that rewrite URLs. - */ - const url = customRedirectBaseUrl - ? new URL(requestUrl.pathname + requestUrl.search + requestUrl.hash, customRedirectBaseUrl) - : requestUrl; - - const storage = new CookieStorage({ - cookieKey: cookieName, - encryptionKey: cookieEncryptionKey, - isSecure: cookieSecure, - getCookie: async (name) => getCookie(event, name), - setCookie: async (name, value, options) => { - setCookie(event, name, value, options); - }, - }); - - await storage.init(); - - const logto = new LogtoClient(clientConfig, { - navigate: async (url) => { - await sendRedirect(event, url, 302); - }, - storage, - }); - - if (url.pathname === pathnames.signIn) { - await logto.signIn({ - ...signInOptions, - redirectUri: new URL(pathnames.callback, url).href, - }); - return; - } - - if (url.pathname === pathnames.signOut) { - await logto.signOut(new URL(postLogoutRedirectUri, url).href); - return; - } - - if (url.pathname === pathnames.callback) { - await logto.handleSignInCallback(url.href); - await sendRedirect(event, postCallbackRedirectUri, 302); - return; - } - - // eslint-disable-next-line @silverhand/fp/no-mutation - event.context.logtoClient = logto; - // eslint-disable-next-line @silverhand/fp/no-mutation - event.context.logtoUser = (await logto.isAuthenticated()) - ? await trySafe(async () => (fetchUserInfo ? logto.fetchUserInfo() : logto.getIdTokenClaims())) - : undefined; + await logtoEventHandler(event, config); }); diff --git a/packages/nuxt/src/runtime/utils/handler.ts b/packages/nuxt/src/runtime/utils/handler.ts new file mode 100644 index 00000000..7a945c3d --- /dev/null +++ b/packages/nuxt/src/runtime/utils/handler.ts @@ -0,0 +1,97 @@ +import LogtoClient, { CookieStorage } from '@logto/node'; +import { trySafe } from '@silverhand/essentials'; +import { type H3Event, getRequestURL, getCookie, setCookie, sendRedirect } from 'h3'; +import type { RuntimeConfig } from 'nuxt/schema'; + +import { defaults } from './constants'; +import { type LogtoRuntimeConfig } from './types'; + +export const logtoEventHandler = async (event: H3Event, config: RuntimeConfig) => { + // eslint-disable-next-line no-restricted-syntax -- Optional fields are not inferred + const logtoConfig = config.logto as LogtoRuntimeConfig; + const { + cookieName, + cookieEncryptionKey, + cookieSecure, + fetchUserInfo, + pathnames, + postCallbackRedirectUri, + postLogoutRedirectUri, + customRedirectBaseUrl, + signInOptions, + ...clientConfig + } = logtoConfig; + + const defaultValueKeys = Object.entries(defaults) + // @ts-expect-error The type of `key` can only be string + .filter(([key, value]) => logtoConfig[key] === value) + .map(([key]) => key); + + if (defaultValueKeys.length > 0) { + console.warn( + `The following Logto configuration keys have default values: ${defaultValueKeys.join( + ', ' + )}. Please replace them with your own values.` + ); + } + + const requestUrl = getRequestURL(event); + + /** + * This approach allows us to: + * 1. Override the base URL when necessary (e.g., in proxy environments) + * 2. Preserve the original path and query parameters + * 3. Fall back to the original URL when no custom base is provided + * + * It's particularly useful in scenarios where the application is deployed + * behind a reverse proxy or in environments that rewrite URLs. + */ + const url = customRedirectBaseUrl + ? new URL(requestUrl.pathname + requestUrl.search + requestUrl.hash, customRedirectBaseUrl) + : requestUrl; + + const storage = new CookieStorage({ + cookieKey: cookieName, + encryptionKey: cookieEncryptionKey, + isSecure: cookieSecure, + getCookie: async (name) => getCookie(event, name), + setCookie: async (name, value, options) => { + setCookie(event, name, value, options); + }, + }); + + await storage.init(); + + const logto = new LogtoClient(clientConfig, { + navigate: async (url) => { + await sendRedirect(event, url, 302); + }, + storage, + }); + + if (url.pathname === pathnames.signIn) { + await logto.signIn({ + ...signInOptions, + redirectUri: new URL(pathnames.callback, url).href, + }); + return; + } + + if (url.pathname === pathnames.signOut) { + await logto.signOut(new URL(postLogoutRedirectUri, url).href); + return; + } + + if (url.pathname === pathnames.callback) { + await logto.handleSignInCallback(url.href); + await sendRedirect(event, postCallbackRedirectUri, 302); + return; + } + + // eslint-disable-next-line @silverhand/fp/no-mutation + event.context.logtoClient = logto; + // eslint-disable-next-line @silverhand/fp/no-mutation + event.context.logtoUser = (await logto.isAuthenticated()) + ? await trySafe(async () => (fetchUserInfo ? logto.fetchUserInfo() : logto.getIdTokenClaims())) + : undefined; +};