Skip to content

Commit

Permalink
feat(nuxt): export event handler (#892)
Browse files Browse the repository at this point in the history
* chore(nuxt): bump nuxt dependencies

* feat(nuxt): export event handler
  • Loading branch information
wangsijie authored Dec 27, 2024
1 parent 01524a7 commit f583d81
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 93 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-donkeys-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@logto/nuxt": minor
---

export logtoEventHandler
1 change: 1 addition & 0 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogtoRuntimeConfigInput> = defineNuxtModule<LogtoRuntimeConfigInput>({
meta: {
Expand Down
96 changes: 3 additions & 93 deletions packages/nuxt/src/runtime/server/event-handler.ts
Original file line number Diff line number Diff line change
@@ -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);
});
97 changes: 97 additions & 0 deletions packages/nuxt/src/runtime/utils/handler.ts
Original file line number Diff line number Diff line change
@@ -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;
};

0 comments on commit f583d81

Please sign in to comment.