Skip to content

Commit

Permalink
feat(events): use named params for all event callbacks (#2342)
Browse files Browse the repository at this point in the history
Unified API for all of our user-facing methods.

NOTE: `events.error` has been removed. This method has never been called in the core, so it did actually nothing. If you want to log errors to a third-party, check out the [`logger`](https://next-auth.js.org/configuration/options#logger) option instead.

BREAKING CHANGE:

Two event signatures changed to use named params, `signOut` and `updateUser`:
```diff
// [...nextauth].js
...
events: {
- signOut(tokenOrSession),
+ signOut({ token, session }), // token if using JWT, session if DB persisted sessions.
- updateUser(user)
+ updateUser({ user })
}
```
  • Loading branch information
balazsorban44 committed Jul 11, 2021
1 parent acc9393 commit 111d5fc
Show file tree
Hide file tree
Showing 14 changed files with 115 additions and 150 deletions.
12 changes: 2 additions & 10 deletions src/adapters/error-handler.js
@@ -1,4 +1,4 @@
import { UnknownError } from "../lib/errors"
import { capitalize, UnknownError, upperSnake } from "../lib/errors"

/**
* Handles adapter induced errors.
Expand All @@ -9,7 +9,7 @@ import { UnknownError } from "../lib/errors"
export default function adapterErrorHandler(adapter, logger) {
return Object.keys(adapter).reduce((acc, method) => {
const name = capitalize(method)
const code = upperSnake(name, adapter.displayName)
const code = `${adapter.displayName ?? "ADAPTER"}_${upperSnake(name)}`

const adapterMethod = adapter[method]
acc[method] = async (...args) => {
Expand All @@ -26,11 +26,3 @@ export default function adapterErrorHandler(adapter, logger) {
return acc
}, {})
}

function capitalize(s) {
return `${s[0].toUpperCase()}${s.slice(1)}`
}

function upperSnake(s, prefix = "ADAPTER") {
return `${prefix}_${s.replace(/([A-Z])/g, "_$1")}`.toUpperCase()
}
8 changes: 8 additions & 0 deletions src/lib/errors.js
Expand Up @@ -32,3 +32,11 @@ export class OAuthCallbackError extends UnknownError {
export class AccountNotLinkedError extends UnknownError {
name = "AccountNotLinkedError"
}

export function upperSnake(s) {
return s.replace(/([A-Z])/g, "_$1").toUpperCase()
}

export function capitalize(s) {
return `${s[0].toUpperCase()}${s.slice(1)}`
}
10 changes: 5 additions & 5 deletions src/server/index.js
Expand Up @@ -2,7 +2,7 @@ import jwt from "../lib/jwt"
import parseUrl from "../lib/parse-url"
import logger, { setLogger } from "../lib/logger"
import * as cookie from "./lib/cookie"
import * as defaultEvents from "./lib/default-events"
import { withErrorHandling, defaultEvents } from "./lib/default-events"
import * as defaultCallbacks from "./lib/default-callbacks"
import parseProviders from "./lib/providers"
import * as routes from "./routes"
Expand Down Expand Up @@ -124,10 +124,10 @@ async function NextAuthHandler(req, res, userOptions) {
...userOptions.jwt,
},
// Event messages
events: {
...defaultEvents,
...userOptions.events,
},
events: withErrorHandling(
{ ...defaultEvents, ...userOptions.events },
logger
),
// Callback functions
callbacks: {
...defaultCallbacks,
Expand Down
17 changes: 5 additions & 12 deletions src/server/lib/callback-handler.js
@@ -1,5 +1,4 @@
import { AccountNotLinkedError } from "../../lib/errors"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"

/**
Expand Down Expand Up @@ -104,12 +103,12 @@ export default async function callbackHandler(
// Update emailVerified property on the user object
const currentDate = new Date()
user = await updateUser({ ...userByEmail, emailVerified: currentDate })
await dispatchEvent(events.updateUser, user)
await events.updateUser({ user })
} else {
// Create user account if there isn't one for the email address already
const currentDate = new Date()
user = await createUser({ ...profile, emailVerified: currentDate })
await dispatchEvent(events.createUser, user)
await events.createUser({ user })
isNewUser = true
}

Expand Down Expand Up @@ -168,10 +167,7 @@ export default async function callbackHandler(
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, {
user,
providerAccount: providerAccount,
})
await events.linkAccount({ user, providerAccount })

// As they are already signed in, we don't need to do anything after linking them
return {
Expand Down Expand Up @@ -218,7 +214,7 @@ export default async function callbackHandler(
// create a new account for the user, link it to the OAuth acccount and
// create a new session for them so they are signed in with it.
user = await createUser(profile)
await dispatchEvent(events.createUser, user)
await events.createUser({ user })

await linkAccount(
user.id,
Expand All @@ -229,10 +225,7 @@ export default async function callbackHandler(
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, {
user,
providerAccount: providerAccount,
})
await events.linkAccount({ user, providerAccount })

session = useJwtSession ? {} : await createSession(user)
isNewUser = true
Expand Down
45 changes: 26 additions & 19 deletions src/server/lib/default-events.js
@@ -1,23 +1,30 @@
/** Event triggered on successful sign in */
export async function signIn (message) {}
import { upperSnake } from "../../lib/errors"

/** Event triggered on sign out */
export async function signOut (message) {}

/** Event triggered on user creation */
export async function createUser (message) {}

/** Event triggered when a user object is updated */
export async function updateUser (message) {}

/** Event triggered when an account is linked to a user */
export async function linkAccount (message) {}

/** Event triggered when a session is active */
export async function session (message) {}
/** @type {import("types").EventCallbacks} */
export const defaultEvents = {
signIn() {},
signOut() {},
createUser() {},
updateUser() {},
linkAccount() {},
session() {},
}

/**
* @TODO Event triggered when something goes wrong in an authentication flow
* This event may be fired multiple times when an error occurs
* Wraps an object of methods and adds error handling.
* @param {import("types").EventCallbacks} methods
* @param {import("types").LoggerInstance} logger
* @return {import("types").EventCallbacks}
*/
export async function error (message) {}
export function withErrorHandling(methods, logger) {
return Object.entries(methods).reduce((acc, [name, method]) => {
acc[name] = async (...args) => {
try {
return await method(...args)
} catch (e) {
logger.error(`${upperSnake(name)}_EVENT_ERROR`, e)
}
}
return acc
}, {})
}
9 changes: 0 additions & 9 deletions src/server/lib/dispatch-event.js

This file was deleted.

10 changes: 5 additions & 5 deletions src/server/lib/oauth/callback.js
@@ -1,11 +1,10 @@
import { decode as jwtDecode } from "jsonwebtoken"
import oAuthClient from "./client"
import logger from "../../../lib/logger"
import { OAuthCallbackError } from "../../../lib/errors"

/** @param {import("types/internals").NextAuthRequest} req */
export default async function oAuthCallback(req) {
const { provider, pkce } = req.options
const { provider, pkce, logger } = req.options
const client = oAuthClient(provider)

if (provider.version?.startsWith("2.")) {
Expand Down Expand Up @@ -59,7 +58,7 @@ export default async function oAuthCallback(req) {
profileData = await client.get(provider, tokens.accessToken, tokens)
}

return getProfile({ profileData, provider, tokens, user })
return getProfile({ profileData, provider, tokens, user }, logger)
} catch (error) {
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", {
error,
Expand Down Expand Up @@ -88,7 +87,7 @@ export default async function oAuthCallback(req) {
tokens.oauth_token_secret
)

return getProfile({ profileData, tokens, provider })
return getProfile({ profileData, tokens, provider }, logger)
} catch (error) {
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error)
throw error
Expand Down Expand Up @@ -116,8 +115,9 @@ export default async function oAuthCallback(req) {
* provider: import("../..").Provider
* user?: object
* }} profileParams
* @param {import("types").LoggerInstance} logger
*/
async function getProfile({ profileData, tokens, provider, user }) {
async function getProfile({ profileData, tokens, provider, user }, logger) {
try {
// Convert profileData into an object if it's a string
if (typeof profileData === "string") {
Expand Down
3 changes: 1 addition & 2 deletions src/server/lib/signin/oauth.js
@@ -1,9 +1,8 @@
import oAuthClient from "../oauth/client"
import logger from "../../../lib/logger"

/** @param {import("types/internals").NextAuthRequest} req */
export default async function getAuthorizationUrl(req) {
const { provider } = req.options
const { provider, logger } = req.options

delete req.query?.nextauth
const params = {
Expand Down
7 changes: 3 additions & 4 deletions src/server/routes/callback.js
@@ -1,7 +1,6 @@
import oAuthCallback from "../lib/oauth/callback"
import callbackHandler from "../lib/callback-handler"
import * as cookie from "../lib/cookie"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"

/**
Expand Down Expand Up @@ -133,7 +132,7 @@ export default async function callback(req, res) {
})
}

await dispatchEvent(events.signIn, { user, account, isNewUser })
await events.signIn({ user, account, isNewUser })

// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login
Expand Down Expand Up @@ -275,7 +274,7 @@ export default async function callback(req, res) {
})
}

await dispatchEvent(events.signIn, { user, account, isNewUser })
await events.signIn({ user, account, isNewUser })

// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login
Expand Down Expand Up @@ -397,7 +396,7 @@ export default async function callback(req, res) {
...cookies.sessionToken.options,
})

await dispatchEvent(events.signIn, { user, account })
await events.signIn({ user, account })

return res.redirect(callbackUrl || baseUrl)
}
Expand Down
5 changes: 2 additions & 3 deletions src/server/routes/session.js
@@ -1,5 +1,4 @@
import * as cookie from "../lib/cookie"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"

/**
Expand Down Expand Up @@ -61,7 +60,7 @@ export default async function session(req, res) {
...cookies.sessionToken.options,
})

await dispatchEvent(events.session, { session, token })
await events.session({ session, token })
} catch (error) {
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
logger.error("JWT_SESSION_ERROR", error)
Expand Down Expand Up @@ -110,7 +109,7 @@ export default async function session(req, res) {
...cookies.sessionToken.options,
})

await dispatchEvent(events.session, { session: sessionPayload })
await events.session({ session: sessionPayload })
} else if (sessionToken) {
// If sessionToken was found set but it's not valid for a session then
// remove the sessionToken cookie from browser.
Expand Down
5 changes: 2 additions & 3 deletions src/server/routes/signout.js
@@ -1,5 +1,4 @@
import * as cookie from "../lib/cookie"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"

/**
Expand All @@ -16,7 +15,7 @@ export default async function signout(req, res) {
// Dispatch signout event
try {
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
await dispatchEvent(events.signOut, decodedJwt)
await events.signOut({ token: decodedJwt })
} catch (error) {
// Do nothing if decoding the JWT fails
}
Expand All @@ -30,7 +29,7 @@ export default async function signout(req, res) {
try {
// Dispatch signout event
const session = await getSession(sessionToken)
await dispatchEvent(events.signOut, session)
await events.signOut({ session })
} catch (error) {
// Do nothing if looking up the session fails
}
Expand Down

0 comments on commit 111d5fc

Please sign in to comment.