Skip to content

Commit daa38e7

Browse files
committed
feat(adapter): take away error handling from adapters (nextauthjs#1871)
1 parent 51597c5 commit daa38e7

12 files changed

+838
-182
lines changed

config/babel.config.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// We aim to have the same support as Next.js
2+
// https://nextjs.org/docs/getting-started#system-requirements
3+
// https://nextjs.org/docs/basic-features/supported-browsers-features
4+
5+
module.exports = {
6+
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
7+
plugins: [
8+
"@babel/plugin-proposal-class-properties",
9+
"@babel/plugin-transform-runtime",
10+
],
11+
comments: false,
12+
overrides: [
13+
{
14+
test: ["../src/client/**"],
15+
presets: [["@babel/preset-env", { targets: { ie: "11" } }]],
16+
},
17+
{
18+
test: ["../src/server/pages/**"],
19+
presets: ["preact"],
20+
},
21+
],
22+
}

config/babel.config.json

-15
This file was deleted.

package-lock.json

+386-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@
3030
},
3131
"scripts": {
3232
"build": "npm run build:js && npm run build:css",
33-
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
33+
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.js src --out-dir dist",
3434
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
3535
"dev:setup": "npm run build:css && cd app && npm i",
3636
"dev": "cd app && npm run dev",
3737
"watch": "npm run watch:js | npm run watch:css",
38-
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
38+
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
3939
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
4040
"test": "echo \"Write some tests...\"; npm run test:types",
4141
"test:types": "dtslint types",
@@ -61,6 +61,7 @@
6161
],
6262
"license": "ISC",
6363
"dependencies": {
64+
"@babel/runtime": "^7.14.0",
6465
"@next-auth/prisma-legacy-adapter": "canary",
6566
"@next-auth/typeorm-legacy-adapter": "canary",
6667
"crypto-js": "^4.0.0",
@@ -91,6 +92,7 @@
9192
"@babel/cli": "^7.8.4",
9293
"@babel/core": "^7.9.6",
9394
"@babel/plugin-proposal-class-properties": "^7.13.0",
95+
"@babel/plugin-transform-runtime": "^7.13.15",
9496
"@babel/preset-env": "^7.9.6",
9597
"@prisma/client": "^2.16.1",
9698
"@semantic-release/commit-analyzer": "^8.0.1",

src/adapters/error-handler.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { UnknownError } from "../lib/errors"
2+
3+
/**
4+
* Handles adapter induced errors.
5+
* @param {import("types/adapters").AdapterInstance} adapter
6+
* @param {import("types").LoggerInstance} logger
7+
* @return {import("types/adapters").AdapterInstance}
8+
*/
9+
export default function adapterErrorHandler(adapter, logger) {
10+
return Object.keys(adapter).reduce((acc, method) => {
11+
const name = capitalize(method)
12+
const code = upperSnake(name, adapter.displayName)
13+
14+
const adapterMethod = adapter[method]
15+
acc[method] = async (...args) => {
16+
try {
17+
logger.debug(code, ...args)
18+
return await adapterMethod(...args)
19+
} catch (error) {
20+
logger.error(`${code}_ERROR`, error)
21+
const e = new UnknownError(error)
22+
e.name = `${name}Error`
23+
throw e
24+
}
25+
}
26+
return acc
27+
}, {})
28+
}
29+
30+
function capitalize(s) {
31+
return `${s[0].toUpperCase()}${s.slice(1)}`
32+
}
33+
34+
function upperSnake(s, prefix = "ADAPTER") {
35+
return `${prefix}_${s.replace(/([A-Z])/g, "_$1")}`.toUpperCase()
36+
}

src/server/lib/callback-handler.js

+50-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { AccountNotLinkedError } from '../../lib/errors'
2-
import dispatchEvent from '../lib/dispatch-event'
1+
import { AccountNotLinkedError } from "../../lib/errors"
2+
import dispatchEvent from "../lib/dispatch-event"
3+
import adapterErrorHandler from "../../adapters/error-handler"
34

45
/**
56
* This function handles the complex flow of signing users in, and either creating,
@@ -12,20 +13,29 @@ import dispatchEvent from '../lib/dispatch-event'
1213
* All verification (e.g. OAuth flows or email address verificaiton flows) are
1314
* done prior to this handler being called to avoid additonal complexity in this
1415
* handler.
16+
* @param {import("types").Session} sessionToken
17+
* @param {import("types").Profile} profile
18+
* @param {import("types").Account} account
19+
* @param {import("types/internals").AppOptions} options
1520
*/
16-
export default async function callbackHandler (sessionToken, profile, providerAccount, options) {
21+
export default async function callbackHandler(
22+
sessionToken,
23+
profile,
24+
providerAccount,
25+
options
26+
) {
1727
// Input validation
18-
if (!profile) throw new Error('Missing profile')
19-
if (!providerAccount?.id || !providerAccount.type) throw new Error('Missing or invalid provider account')
20-
if (!['email', 'oauth'].includes(providerAccount.type)) throw new Error('Provider not supported')
28+
if (!profile) throw new Error("Missing profile")
29+
if (!providerAccount?.id || !providerAccount.type)
30+
throw new Error("Missing or invalid provider account")
31+
if (!["email", "oauth"].includes(providerAccount.type))
32+
throw new Error("Provider not supported")
2133

2234
const {
2335
adapter,
2436
jwt,
2537
events,
26-
session: {
27-
jwt: useJwtSession
28-
}
38+
session: { jwt: useJwtSession },
2939
} = options
3040

3141
// If no adapter is configured then we don't have a database and cannot
@@ -34,7 +44,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
3444
return {
3545
user: profile,
3646
account: providerAccount,
37-
session: {}
47+
session: {},
3848
}
3949
}
4050

@@ -47,8 +57,8 @@ export default async function callbackHandler (sessionToken, profile, providerAc
4757
linkAccount,
4858
createSession,
4959
getSession,
50-
deleteSession
51-
} = await adapter.getAdapter(options)
60+
deleteSession,
61+
} = adapterErrorHandler(await adapter.getAdapter(options), options.logger)
5262

5363
let session = null
5464
let user = null
@@ -74,9 +84,11 @@ export default async function callbackHandler (sessionToken, profile, providerAc
7484
}
7585
}
7686

77-
if (providerAccount.type === 'email') {
87+
if (providerAccount.type === "email") {
7888
// If signing in with an email, check if an account with the same email address exists already
79-
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
89+
const userByEmail = profile.email
90+
? await getUserByEmail(profile.email)
91+
: null
8092
if (userByEmail) {
8193
// If they are not already signed in as the same user, this flow will
8294
// sign them out of the current session and sign them in as the new user
@@ -107,11 +119,14 @@ export default async function callbackHandler (sessionToken, profile, providerAc
107119
return {
108120
session,
109121
user,
110-
isNewUser
122+
isNewUser,
111123
}
112-
} else if (providerAccount.type === 'oauth') {
124+
} else if (providerAccount.type === "oauth") {
113125
// If signing in with oauth account, check to see if the account exists already
114-
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
126+
const userByProviderAccountId = await getUserByProviderAccountId(
127+
providerAccount.provider,
128+
providerAccount.id
129+
)
115130
if (userByProviderAccountId) {
116131
if (isSignedIn) {
117132
// If the user is already signed in with this account, we don't need to do anything
@@ -122,7 +137,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
122137
return {
123138
session,
124139
user,
125-
isNewUser
140+
isNewUser,
126141
}
127142
}
128143
// If the user is currently signed in, but the new account they are signing in
@@ -132,11 +147,13 @@ export default async function callbackHandler (sessionToken, profile, providerAc
132147
}
133148
// If there is no active session, but the account being signed in with is already
134149
// associated with a valid user then create session to sign the user in.
135-
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
150+
session = useJwtSession
151+
? {}
152+
: await createSession(userByProviderAccountId)
136153
return {
137154
session,
138155
user: userByProviderAccountId,
139-
isNewUser
156+
isNewUser,
140157
}
141158
} else {
142159
if (isSignedIn) {
@@ -151,13 +168,16 @@ export default async function callbackHandler (sessionToken, profile, providerAc
151168
providerAccount.accessToken,
152169
providerAccount.accessTokenExpires
153170
)
154-
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
171+
await dispatchEvent(events.linkAccount, {
172+
user,
173+
providerAccount: providerAccount,
174+
})
155175

156176
// As they are already signed in, we don't need to do anything after linking them
157177
return {
158178
session,
159179
user,
160-
isNewUser
180+
isNewUser,
161181
}
162182
}
163183

@@ -178,7 +198,9 @@ export default async function callbackHandler (sessionToken, profile, providerAc
178198
//
179199
// OAuth providers should require email address verification to prevent this, but in
180200
// practice that is not always the case; this helps protect against that.
181-
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
201+
const userByEmail = profile.email
202+
? await getUserByEmail(profile.email)
203+
: null
182204
if (userByEmail) {
183205
// We end up here when we don't have an account with the same [provider].id *BUT*
184206
// we do already have an account with the same email address as the one in the
@@ -207,14 +229,17 @@ export default async function callbackHandler (sessionToken, profile, providerAc
207229
providerAccount.accessToken,
208230
providerAccount.accessTokenExpires
209231
)
210-
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
232+
await dispatchEvent(events.linkAccount, {
233+
user,
234+
providerAccount: providerAccount,
235+
})
211236

212237
session = useJwtSession ? {} : await createSession(user)
213238
isNewUser = true
214239
return {
215240
session,
216241
user,
217-
isNewUser
242+
isNewUser,
218243
}
219244
}
220245
}

src/server/lib/signin/email.js

+29-7
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
1-
import { randomBytes } from 'crypto'
1+
import { randomBytes } from "crypto"
2+
import adapterErrorHandler from "../../../adapters/error-handler"
23

3-
export default async function email (email, provider, options) {
4+
/**
5+
*
6+
* @param {string} email
7+
* @param {import("types/providers").EmailConfig} provider
8+
* @param {import("types/internals").AppOptions} options
9+
* @returns
10+
*/
11+
export default async function email(email, provider, options) {
412
try {
5-
const { baseUrl, basePath, adapter } = options
13+
const { baseUrl, basePath, adapter, logger } = options
614

7-
const { createVerificationRequest } = await adapter.getAdapter(options)
15+
const { createVerificationRequest } = adapterErrorHandler(
16+
await adapter.getAdapter(options),
17+
logger
18+
)
819

920
// Prefer provider specific secret, but use default secret if none specified
1021
const secret = provider.secret || options.secret
1122

1223
// Generate token
13-
const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
24+
const token =
25+
(await provider.generateVerificationToken?.()) ??
26+
randomBytes(32).toString("hex")
1427

1528
// Send email with link containing token (the unhashed version)
16-
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
29+
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(
30+
provider.id
31+
)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
1732

1833
// @TODO Create invite (send secret so can be hashed)
19-
await createVerificationRequest(email, url, token, secret, provider, options)
34+
await createVerificationRequest(
35+
email,
36+
url,
37+
token,
38+
secret,
39+
provider,
40+
options
41+
)
2042

2143
// Return promise
2244
return Promise.resolve()

0 commit comments

Comments
 (0)