diff --git a/config/babel.config.js b/config/babel.config.js new file mode 100644 index 0000000000..2a290a3ea2 --- /dev/null +++ b/config/babel.config.js @@ -0,0 +1,22 @@ +// We aim to have the same support as Next.js +// https://nextjs.org/docs/getting-started#system-requirements +// https://nextjs.org/docs/basic-features/supported-browsers-features + +module.exports = { + presets: [["@babel/preset-env", { targets: { node: "10.13" } }]], + plugins: [ + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-runtime", + ], + comments: false, + overrides: [ + { + test: ["../src/client/**"], + presets: [["@babel/preset-env", { targets: { ie: "11" } }]], + }, + { + test: ["../src/server/pages/**"], + presets: ["preact"], + }, + ], +} diff --git a/config/babel.config.json b/config/babel.config.json deleted file mode 100644 index e530a844cf..0000000000 --- a/config/babel.config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { "targets": { "esmodules": true } }] - ], - "plugins": [ - "@babel/plugin-proposal-class-properties" - ], - "comments": false, - "overrides": [ - { - "test": ["../src/server/pages/**"], - "presets": ["preact"] - } - ] -} diff --git a/package-lock.json b/package-lock.json index d204911c8b..16e5e010da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -489,6 +489,227 @@ } } }, + "@babel/helper-define-polyfill-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.0.tgz", + "integrity": "sha512-JT8tHuFjKBo8NnaUbblz7mIu1nnvUDiHVjXXkulZULyidvo/7P6TY7+YqpV37IfF+KUFxmlK04elKtGKXaiVgw==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/compat-data": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz", + "integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==", + "dev": true + }, + "@babel/generator": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz", + "integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==", + "dev": true, + "requires": { + "@babel/types": "^7.14.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.13.16", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz", + "integrity": "sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.15", + "@babel/helper-validator-option": "^7.12.17", + "browserslist": "^4.14.5", + "semver": "^6.3.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-module-imports": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", + "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", + "dev": true + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz", + "integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", + "integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.0", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.14.0", + "@babel/types": "^7.14.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", + "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "to-fast-properties": "^2.0.0" + } + }, + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + } + }, + "caniuse-lite": { + "version": "1.0.30001219", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001219.tgz", + "integrity": "sha512-c0yixVG4v9KBc/tQ2rlbB3A/bgBFRvl8h8M4IeUbqCca4gsiCfvtaheUssbnux/Mb66Vjz7x8yYjDgYcNQOhyQ==", + "dev": true + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.723", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.723.tgz", + "integrity": "sha512-L+WXyXI7c7+G1V8ANzRsPI5giiimLAUDC6Zs1ojHHPhYXb3k/iTABFmWjivEtsWrRQymjnO66/rO2ZTABGdmWg==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node-releases": { + "version": "1.1.71", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", + "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "@babel/helper-explode-assignable-expression": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", @@ -2029,6 +2250,59 @@ } } }, + "@babel/plugin-transform-runtime": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.15.tgz", + "integrity": "sha512-d+ezl76gx6Jal08XngJUkXM4lFXK/5Ikl9Mh4HKDxSfGJXmZ9xG64XT2oivBzfxb/eQ62VfvoMkaCZUKJMVrBA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-plugin-utils": "^7.13.0", + "babel-plugin-polyfill-corejs2": "^0.2.0", + "babel-plugin-polyfill-corejs3": "^0.2.0", + "babel-plugin-polyfill-regenerator": "^0.2.0", + "semver": "^6.3.0" + }, + "dependencies": { + "@babel/helper-module-imports": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", + "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz", + "integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "to-fast-properties": "^2.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "@babel/plugin-transform-shorthand-properties": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", @@ -2272,10 +2546,9 @@ } }, "@babel/runtime": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", - "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", - "dev": true, + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", + "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -2514,9 +2787,9 @@ "integrity": "sha512-rIIisYBvVtxDbY9Lbm+HOLbZyOaaEmtGc9wDN3tJLDUu3sLJOXNN7Pz29ThS+gf2lpMxXnfvk587hxGrnhCghQ==" }, "@next-auth/typeorm-legacy-adapter": { - "version": "0.0.2-canary.116", - "resolved": "https://registry.npmjs.org/@next-auth/typeorm-legacy-adapter/-/typeorm-legacy-adapter-0.0.2-canary.116.tgz", - "integrity": "sha512-06knawdYdHkiMVw5GfR6Ku5ryITdaOLWIGCbRwAtgCZE5Pf6am+nhnqQVeGq18odu84SmzA+hTtkGSAYbR6MIA==", + "version": "0.0.2-canary.117", + "resolved": "https://registry.npmjs.org/@next-auth/typeorm-legacy-adapter/-/typeorm-legacy-adapter-0.0.2-canary.117.tgz", + "integrity": "sha512-sYJZPWMsM1ZTJcl749UojYDF4q8+ZiYcrR7rM4SACc0qiA9VBdOYUUMUMpQoazKOiwo1rWDKu4wHPt6CdV0ctQ==", "requires": { "crypto-js": "^4.0.0", "require_optional": "^1.0.1", @@ -4080,6 +4353,105 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-polyfill-corejs2": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.0.tgz", + "integrity": "sha512-9bNwiR0dS881c5SHnzCmmGlMkJLl0OUZvxrxHo9w/iNoRuqaPjqlvBf4HrovXtQs/au5yKkpcdgfT1cC5PAZwg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.0", + "semver": "^6.1.1" + }, + "dependencies": { + "@babel/compat-data": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz", + "integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.0.tgz", + "integrity": "sha512-zZyi7p3BCUyzNxLx8KV61zTINkkV65zVkDAFNZmrTCRVhjo1jAS+YLvDJ9Jgd/w2tsAviCwFHReYfxO3Iql8Yg==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.0", + "core-js-compat": "^3.9.1" + }, + "dependencies": { + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + } + }, + "caniuse-lite": { + "version": "1.0.30001219", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001219.tgz", + "integrity": "sha512-c0yixVG4v9KBc/tQ2rlbB3A/bgBFRvl8h8M4IeUbqCca4gsiCfvtaheUssbnux/Mb66Vjz7x8yYjDgYcNQOhyQ==", + "dev": true + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "core-js-compat": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.11.1.tgz", + "integrity": "sha512-aZ0e4tmlG/aOBHj92/TuOuZwp6jFvn1WNabU5VOVixzhu5t5Ao+JZkQOPlgNXu6ynwLrwJxklT4Gw1G1VGEh+g==", + "dev": true, + "requires": { + "browserslist": "^4.16.5", + "semver": "7.0.0" + } + }, + "electron-to-chromium": { + "version": "1.3.723", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.723.tgz", + "integrity": "sha512-L+WXyXI7c7+G1V8ANzRsPI5giiimLAUDC6Zs1ojHHPhYXb3k/iTABFmWjivEtsWrRQymjnO66/rO2ZTABGdmWg==", + "dev": true + }, + "node-releases": { + "version": "1.1.71", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", + "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", + "dev": true + }, + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.0.tgz", + "integrity": "sha512-J7vKbCuD2Xi/eEHxquHN14bXAW9CXtecwuLrOIDJtcZzTaPzV1VdEfoUf9AzcRBMolKUQKM9/GVojeh0hFiqMg==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.0" + } + }, "babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", @@ -8903,6 +9275,12 @@ "integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=", "dev": true }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, "lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", @@ -15935,8 +16313,7 @@ "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" }, "regenerator-transform": { "version": "0.14.5", diff --git a/package.json b/package.json index 63b10c17d2..82736d6b0e 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,12 @@ }, "scripts": { "build": "npm run build:js && npm run build:css", - "build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist", + "build:js": "node ./config/build.js && babel --config-file ./config/babel.config.js src --out-dir dist", "build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js", "dev:setup": "npm run build:css && cd app && npm i", "dev": "cd app && npm run dev", "watch": "npm run watch:js | npm run watch:css", - "watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist", + "watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist", "watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist", "test": "echo \"Write some tests...\"; npm run test:types", "test:types": "dtslint types", @@ -61,6 +61,7 @@ ], "license": "ISC", "dependencies": { + "@babel/runtime": "^7.14.0", "@next-auth/prisma-legacy-adapter": "canary", "@next-auth/typeorm-legacy-adapter": "canary", "crypto-js": "^4.0.0", @@ -91,6 +92,7 @@ "@babel/cli": "^7.8.4", "@babel/core": "^7.9.6", "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-transform-runtime": "^7.13.15", "@babel/preset-env": "^7.9.6", "@prisma/client": "^2.16.1", "@semantic-release/commit-analyzer": "^8.0.1", diff --git a/src/adapters/error-handler.js b/src/adapters/error-handler.js new file mode 100644 index 0000000000..f111deaaad --- /dev/null +++ b/src/adapters/error-handler.js @@ -0,0 +1,36 @@ +import { UnknownError } from "../lib/errors" + +/** + * Handles adapter induced errors. + * @param {import("types/adapters").AdapterInstance} adapter + * @param {import("types").LoggerInstance} logger + * @return {import("types/adapters").AdapterInstance} + */ +export default function adapterErrorHandler(adapter, logger) { + return Object.keys(adapter).reduce((acc, method) => { + const name = capitalize(method) + const code = upperSnake(name, adapter.displayName) + + const adapterMethod = adapter[method] + acc[method] = async (...args) => { + try { + logger.debug(code, ...args) + return await adapterMethod(...args) + } catch (error) { + logger.error(`${code}_ERROR`, error) + const e = new UnknownError(error) + e.name = `${name}Error` + throw e + } + } + 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() +} diff --git a/src/server/lib/callback-handler.js b/src/server/lib/callback-handler.js index ce51fe514e..a3c67dfcb9 100644 --- a/src/server/lib/callback-handler.js +++ b/src/server/lib/callback-handler.js @@ -1,5 +1,6 @@ -import { AccountNotLinkedError } from '../../lib/errors' -import dispatchEvent from '../lib/dispatch-event' +import { AccountNotLinkedError } from "../../lib/errors" +import dispatchEvent from "../lib/dispatch-event" +import adapterErrorHandler from "../../adapters/error-handler" /** * This function handles the complex flow of signing users in, and either creating, @@ -12,20 +13,29 @@ import dispatchEvent from '../lib/dispatch-event' * All verification (e.g. OAuth flows or email address verificaiton flows) are * done prior to this handler being called to avoid additonal complexity in this * handler. + * @param {import("types").Session} sessionToken + * @param {import("types").Profile} profile + * @param {import("types").Account} account + * @param {import("types/internals").AppOptions} options */ -export default async function callbackHandler (sessionToken, profile, providerAccount, options) { +export default async function callbackHandler( + sessionToken, + profile, + providerAccount, + options +) { // Input validation - if (!profile) throw new Error('Missing profile') - if (!providerAccount?.id || !providerAccount.type) throw new Error('Missing or invalid provider account') - if (!['email', 'oauth'].includes(providerAccount.type)) throw new Error('Provider not supported') + if (!profile) throw new Error("Missing profile") + if (!providerAccount?.id || !providerAccount.type) + throw new Error("Missing or invalid provider account") + if (!["email", "oauth"].includes(providerAccount.type)) + throw new Error("Provider not supported") const { adapter, jwt, events, - session: { - jwt: useJwtSession - } + session: { jwt: useJwtSession }, } = options // 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 return { user: profile, account: providerAccount, - session: {} + session: {}, } } @@ -47,8 +57,8 @@ export default async function callbackHandler (sessionToken, profile, providerAc linkAccount, createSession, getSession, - deleteSession - } = await adapter.getAdapter(options) + deleteSession, + } = adapterErrorHandler(await adapter.getAdapter(options), options.logger) let session = null let user = null @@ -74,9 +84,11 @@ export default async function callbackHandler (sessionToken, profile, providerAc } } - if (providerAccount.type === 'email') { + if (providerAccount.type === "email") { // If signing in with an email, check if an account with the same email address exists already - const userByEmail = profile.email ? await getUserByEmail(profile.email) : null + const userByEmail = profile.email + ? await getUserByEmail(profile.email) + : null if (userByEmail) { // If they are not already signed in as the same user, this flow will // 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 return { session, user, - isNewUser + isNewUser, } - } else if (providerAccount.type === 'oauth') { + } else if (providerAccount.type === "oauth") { // If signing in with oauth account, check to see if the account exists already - const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id) + const userByProviderAccountId = await getUserByProviderAccountId( + providerAccount.provider, + providerAccount.id + ) if (userByProviderAccountId) { if (isSignedIn) { // 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 return { session, user, - isNewUser + isNewUser, } } // 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 } // If there is no active session, but the account being signed in with is already // associated with a valid user then create session to sign the user in. - session = useJwtSession ? {} : await createSession(userByProviderAccountId) + session = useJwtSession + ? {} + : await createSession(userByProviderAccountId) return { session, user: userByProviderAccountId, - isNewUser + isNewUser, } } else { if (isSignedIn) { @@ -151,13 +168,16 @@ export default async function callbackHandler (sessionToken, profile, providerAc providerAccount.accessToken, providerAccount.accessTokenExpires ) - await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount }) + await dispatchEvent(events.linkAccount, { + user, + providerAccount: providerAccount, + }) // As they are already signed in, we don't need to do anything after linking them return { session, user, - isNewUser + isNewUser, } } @@ -178,7 +198,9 @@ export default async function callbackHandler (sessionToken, profile, providerAc // // OAuth providers should require email address verification to prevent this, but in // practice that is not always the case; this helps protect against that. - const userByEmail = profile.email ? await getUserByEmail(profile.email) : null + const userByEmail = profile.email + ? await getUserByEmail(profile.email) + : null if (userByEmail) { // We end up here when we don't have an account with the same [provider].id *BUT* // 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 providerAccount.accessToken, providerAccount.accessTokenExpires ) - await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount }) + await dispatchEvent(events.linkAccount, { + user, + providerAccount: providerAccount, + }) session = useJwtSession ? {} : await createSession(user) isNewUser = true return { session, user, - isNewUser + isNewUser, } } } diff --git a/src/server/lib/signin/email.js b/src/server/lib/signin/email.js index 5e7916acdd..0716dc9f39 100644 --- a/src/server/lib/signin/email.js +++ b/src/server/lib/signin/email.js @@ -1,22 +1,44 @@ -import { randomBytes } from 'crypto' +import { randomBytes } from "crypto" +import adapterErrorHandler from "../../../adapters/error-handler" -export default async function email (email, provider, options) { +/** + * + * @param {string} email + * @param {import("types/providers").EmailConfig} provider + * @param {import("types/internals").AppOptions} options + * @returns + */ +export default async function email(email, provider, options) { try { - const { baseUrl, basePath, adapter } = options + const { baseUrl, basePath, adapter, logger } = options - const { createVerificationRequest } = await adapter.getAdapter(options) + const { createVerificationRequest } = adapterErrorHandler( + await adapter.getAdapter(options), + logger + ) // Prefer provider specific secret, but use default secret if none specified const secret = provider.secret || options.secret // Generate token - const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex') + const token = + (await provider.generateVerificationToken?.()) ?? + randomBytes(32).toString("hex") // Send email with link containing token (the unhashed version) - const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}` + const url = `${baseUrl}${basePath}/callback/${encodeURIComponent( + provider.id + )}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}` // @TODO Create invite (send secret so can be hashed) - await createVerificationRequest(email, url, token, secret, provider, options) + await createVerificationRequest( + email, + url, + token, + secret, + provider, + options + ) // Return promise return Promise.resolve() diff --git a/src/server/routes/callback.js b/src/server/routes/callback.js index c97a189696..5a63681953 100644 --- a/src/server/routes/callback.js +++ b/src/server/routes/callback.js @@ -1,15 +1,15 @@ -import oAuthCallback from '../lib/oauth/callback' -import callbackHandler from '../lib/callback-handler' -import * as cookie from '../lib/cookie' -import logger from '../../lib/logger' -import dispatchEvent from '../lib/dispatch-event' +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" /** * Handle callbacks from login services * @param {import("types/internals").NextAuthRequest} req * @param {import("types/internals").NextAuthResponse} res */ -export default async function callback (req, res) { +export default async function callback(req, res) { const { provider, adapter, @@ -22,21 +22,23 @@ export default async function callback (req, res) { jwt, events, callbacks, - session: { - jwt: useJwtSession, - maxAge: sessionMaxAge - } + session: { jwt: useJwtSession, maxAge: sessionMaxAge }, + logger, } = req.options // Get session ID (if set) const sessionToken = req.cookies?.[cookies.sessionToken.name] ?? null - if (provider.type === 'oauth') { + if (provider.type === "oauth") { try { const { profile, account, OAuthProfile } = await oAuthCallback(req) try { // Make it easier to debug when adding a new provider - logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile }) + logger.debug("OAUTH_CALLBACK_RESPONSE", { + profile, + account, + OAuthProfile, + }) // If we don't have a profile object then either something went wrong // or the user cancelled signing in. We don't know which, so we just @@ -56,52 +58,85 @@ export default async function callback (req, res) { // (that just means it's a new user signing in for the first time). let userOrProfile = profile if (adapter) { - const { getUserByProviderAccountId } = await adapter.getAdapter(req.options) - const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id) + const { getUserByProviderAccountId } = adapterErrorHandler( + await adapter.getAdapter(req.options), + logger + ) + const userFromProviderAccountId = await getUserByProviderAccountId( + account.provider, + account.id + ) if (userFromProviderAccountId) { userOrProfile = userFromProviderAccountId } } try { - const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile) + const signInCallbackResponse = await callbacks.signIn( + userOrProfile, + account, + OAuthProfile + ) if (signInCallbackResponse === false) { - return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) - } else if (typeof signInCallbackResponse === 'string') { + return res.redirect( + `${baseUrl}${basePath}/error?error=AccessDenied` + ) + } else if (typeof signInCallbackResponse === "string") { return res.redirect(signInCallbackResponse) } } catch (error) { if (error instanceof Error) { - return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`) + return res.redirect( + `${baseUrl}${basePath}/error?error=${encodeURIComponent( + error.message + )}` + ) } // TODO: Remove in a future major release - logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT') + logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT") return res.redirect(error) } // Sign user in - const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options) + const { user, session, isNewUser } = await callbackHandler( + sessionToken, + profile, + account, + req.options + ) if (useJwtSession) { const defaultJwtPayload = { name: user.name, email: user.email, picture: user.image, - sub: user.id?.toString() + sub: user.id?.toString(), } - const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser) + const jwtPayload = await callbacks.jwt( + defaultJwtPayload, + user, + account, + OAuthProfile, + isNewUser + ) // Sign and encrypt token const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload }) // Set cookie expiry date const cookieExpires = new Date() - cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000)) + cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000) - cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options }) + cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { + expires: cookieExpires.toISOString(), + ...cookies.sessionToken.options, + }) } else { // Save Session Token in cookie - cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options }) + cookie.set(res, cookies.sessionToken.name, session.sessionToken, { + expires: session.expires || null, + ...cookies.sessionToken.options, + }) } await dispatchEvent(events.signIn, { user, account, isNewUser }) @@ -110,94 +145,145 @@ export default async function callback (req, res) { // e.g. option to send users to a new account landing page on initial login // Note that the callback URL is preserved, so the journey can still be resumed if (isNewUser && pages.newUser) { - return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`) + return res.redirect( + `${pages.newUser}${ + pages.newUser.includes("?") ? "&" : "?" + }callbackUrl=${encodeURIComponent(callbackUrl)}` + ) } // Callback URL is already verified at this point, so safe to use if specified return res.redirect(callbackUrl || baseUrl) } catch (error) { - if (error.name === 'AccountNotLinkedError') { + if (error.name === "AccountNotLinkedError") { // If the email on the account is already linked, but not with this OAuth account - return res.redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`) - } else if (error.name === 'CreateUserError') { - return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`) + return res.redirect( + `${baseUrl}${basePath}/error?error=OAuthAccountNotLinked` + ) + } else if (error.name === "CreateUserError") { + return res.redirect( + `${baseUrl}${basePath}/error?error=OAuthCreateAccount` + ) } - logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error) + logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=Callback`) } } catch (error) { - if (error.name === 'OAuthCallbackError') { - logger.error('CALLBACK_OAUTH_ERROR', error) + if (error.name === "OAuthCallbackError") { + logger.error("CALLBACK_OAUTH_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`) } - logger.error('OAUTH_CALLBACK_ERROR', error) + logger.error("OAUTH_CALLBACK_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=Callback`) } - } else if (provider.type === 'email') { + } else if (provider.type === "email") { try { if (!adapter) { - logger.error('EMAIL_REQUIRES_ADAPTER_ERROR') + logger.error("EMAIL_REQUIRES_ADAPTER_ERROR") return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`) } - const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(req.options) + const { + getVerificationRequest, + deleteVerificationRequest, + getUserByEmail, + } = adapterErrorHandler(await adapter.getAdapter(req.options), logger) const verificationToken = req.query.token const email = req.query.email // Verify email and verification token exist in database - const invite = await getVerificationRequest(email, verificationToken, secret, provider) + const invite = await getVerificationRequest( + email, + verificationToken, + secret, + provider + ) if (!invite) { return res.redirect(`${baseUrl}${basePath}/error?error=Verification`) } // If verification token is valid, delete verification request token from // the database so it cannot be used again - await deleteVerificationRequest(email, verificationToken, secret, provider) + await deleteVerificationRequest( + email, + verificationToken, + secret, + provider + ) // If is an existing user return a user object (otherwise use placeholder) - const profile = await getUserByEmail(email) || { email } - const account = { id: provider.id, type: 'email', providerAccountId: email } + const profile = (await getUserByEmail(email)) || { email } + const account = { + id: provider.id, + type: "email", + providerAccountId: email, + } // Check if user is allowed to sign in try { - const signInCallbackResponse = await callbacks.signIn(profile, account, { email }) + const signInCallbackResponse = await callbacks.signIn( + profile, + account, + { email } + ) if (signInCallbackResponse === false) { return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) - } else if (typeof signInCallbackResponse === 'string') { + } else if (typeof signInCallbackResponse === "string") { return res.redirect(signInCallbackResponse) } } catch (error) { if (error instanceof Error) { - return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`) + return res.redirect( + `${baseUrl}${basePath}/error?error=${encodeURIComponent( + error.message + )}` + ) } // TODO: Remove in a future major release - logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT') + logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT") return res.redirect(error) } // Sign user in - const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options) + const { user, session, isNewUser } = await callbackHandler( + sessionToken, + profile, + account, + req.options + ) if (useJwtSession) { const defaultJwtPayload = { name: user.name, email: user.email, picture: user.image, - sub: user.id?.toString() + sub: user.id?.toString(), } - const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser) + const jwtPayload = await callbacks.jwt( + defaultJwtPayload, + user, + account, + profile, + isNewUser + ) // Sign and encrypt token const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload }) // Set cookie expiry date const cookieExpires = new Date() - cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000)) + cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000) - cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options }) + cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { + expires: cookieExpires.toISOString(), + ...cookies.sessionToken.options, + }) } else { // Save Session Token in cookie - cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options }) + cookie.set(res, cookies.sessionToken.name, session.sessionToken, { + expires: session.expires || null, + ...cookies.sessionToken.options, + }) } await dispatchEvent(events.signIn, { user, account, isNewUser }) @@ -206,55 +292,93 @@ export default async function callback (req, res) { // e.g. option to send users to a new account landing page on initial login // Note that the callback URL is preserved, so the journey can still be resumed if (isNewUser && pages.newUser) { - return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`) + return res.redirect( + `${pages.newUser}${ + pages.newUser.includes("?") ? "&" : "?" + }callbackUrl=${encodeURIComponent(callbackUrl)}` + ) } // Callback URL is already verified at this point, so safe to use if specified return res.redirect(callbackUrl || baseUrl) } catch (error) { - if (error.name === 'CreateUserError') { - return res.redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`) + if (error.name === "CreateUserError") { + return res.redirect( + `${baseUrl}${basePath}/error?error=EmailCreateAccount` + ) } - logger.error('CALLBACK_EMAIL_ERROR', error) + logger.error("CALLBACK_EMAIL_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=Callback`) } - } else if (provider.type === 'credentials' && req.method === 'POST') { + } else if (provider.type === "credentials" && req.method === "POST") { if (!useJwtSession) { - logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled') - return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`) + logger.error( + "CALLBACK_CREDENTIALS_JWT_ERROR", + "Signin in with credentials is only supported if JSON Web Tokens are enabled" + ) + return res + .status(500) + .redirect(`${baseUrl}${basePath}/error?error=Configuration`) } if (!provider.authorize) { - logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider') - return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`) + logger.error( + "CALLBACK_CREDENTIALS_HANDLER_ERROR", + "Must define an authorize() handler to use credentials authentication provider" + ) + return res + .status(500) + .redirect(`${baseUrl}${basePath}/error?error=Configuration`) } const credentials = req.body let userObjectReturnedFromAuthorizeHandler try { - userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials) + userObjectReturnedFromAuthorizeHandler = await provider.authorize( + credentials + ) if (!userObjectReturnedFromAuthorizeHandler) { - return res.status(401).redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`) + return res + .status(401) + .redirect( + `${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent( + provider.id + )}` + ) } } catch (error) { if (error instanceof Error) { - return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`) + return res.redirect( + `${baseUrl}${basePath}/error?error=${encodeURIComponent( + error.message + )}` + ) } return res.redirect(error) } const user = userObjectReturnedFromAuthorizeHandler - const account = { id: provider.id, type: 'credentials' } + const account = { id: provider.id, type: "credentials" } try { - const signInCallbackResponse = await callbacks.signIn(user, account, credentials) + const signInCallbackResponse = await callbacks.signIn( + user, + account, + credentials + ) if (signInCallbackResponse === false) { - return res.status(403).redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) + return res + .status(403) + .redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) } } catch (error) { if (error instanceof Error) { - return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`) + return res.redirect( + `${baseUrl}${basePath}/error?error=${encodeURIComponent( + error.message + )}` + ) } return res.redirect(error) } @@ -263,22 +387,33 @@ export default async function callback (req, res) { name: user.name, email: user.email, picture: user.image, - sub: user.id?.toString() + sub: user.id?.toString(), } - const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false) + const jwtPayload = await callbacks.jwt( + defaultJwtPayload, + user, + account, + userObjectReturnedFromAuthorizeHandler, + false + ) // Sign and encrypt token const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload }) // Set cookie expiry date const cookieExpires = new Date() - cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000)) + cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000) - cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options }) + cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { + expires: cookieExpires.toISOString(), + ...cookies.sessionToken.options, + }) await dispatchEvent(events.signIn, { user, account }) return res.redirect(callbackUrl || baseUrl) } - return res.status(500).end(`Error: Callback for provider type ${provider.type} not supported`) + return res + .status(500) + .end(`Error: Callback for provider type ${provider.type} not supported`) } diff --git a/src/server/routes/session.js b/src/server/routes/session.js index d8d3431625..b27622a13e 100644 --- a/src/server/routes/session.js +++ b/src/server/routes/session.js @@ -1,13 +1,15 @@ -import * as cookie from '../lib/cookie' -import logger from '../../lib/logger' -import dispatchEvent from '../lib/dispatch-event' +import * as cookie from "../lib/cookie" +import dispatchEvent from "../lib/dispatch-event" +import adapterErrorHandler from "../../adapters/error-handler" /** * Return a session object (without any private fields) * for Single Page App clients + * @param {import("types/internals").NextAuthRequest} req + * @param {import("types/internals").NextAuthResponse} res */ -export default async function session (req, res) { - const { cookies, adapter, jwt, events, callbacks } = req.options +export default async function session(req, res) { + const { cookies, adapter, jwt, events, callbacks, logger } = req.options const useJwtSession = req.options.session.jwt const sessionMaxAge = req.options.session.maxAge const sessionToken = req.cookies[cookies.sessionToken.name] @@ -24,7 +26,9 @@ export default async function session (req, res) { // Generate new session expiry date const sessionExpiresDate = new Date() - sessionExpiresDate.setTime(sessionExpiresDate.getTime() + (sessionMaxAge * 1000)) + sessionExpiresDate.setTime( + sessionExpiresDate.getTime() + sessionMaxAge * 1000 + ) const sessionExpires = sessionExpiresDate.toISOString() // By default, only exposes a limited subset of information to the client @@ -33,14 +37,17 @@ export default async function session (req, res) { user: { name: decodedJwt.name || null, email: decodedJwt.email || null, - image: decodedJwt.picture || null + image: decodedJwt.picture || null, }, - expires: sessionExpires + expires: sessionExpires, } // Pass Session and JSON Web Token through to the session callback const jwtPayload = await callbacks.jwt(decodedJwt) - const sessionPayload = await callbacks.session(defaultSessionPayload, jwtPayload) + const sessionPayload = await callbacks.session( + defaultSessionPayload, + jwtPayload + ) // Return session payload as response response = sessionPayload @@ -49,17 +56,29 @@ export default async function session (req, res) { const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload }) // Set cookie, to also update expiry date on cookie - cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: sessionExpires, ...cookies.sessionToken.options }) - - await dispatchEvent(events.session, { session: sessionPayload, jwt: jwtPayload }) + cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { + expires: sessionExpires, + ...cookies.sessionToken.options, + }) + + await dispatchEvent(events.session, { + session: sessionPayload, + jwt: jwtPayload, + }) } catch (error) { // If JWT not verifiable, make sure the cookie for it is removed and return empty object - logger.error('JWT_SESSION_ERROR', error) - cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 }) + logger.error("JWT_SESSION_ERROR", error) + cookie.set(res, cookies.sessionToken.name, "", { + ...cookies.sessionToken.options, + maxAge: 0, + }) } } else { try { - const { getUser, getSession, updateSession } = await adapter.getAdapter(req.options) + const { getUser, getSession, updateSession } = adapterErrorHandler( + await adapter.getAdapter(req.options), + logger + ) const session = await getSession(sessionToken) if (session) { // Trigger update to session object to update session expiry @@ -73,29 +92,38 @@ export default async function session (req, res) { user: { name: user.name, email: user.email, - image: user.image + image: user.image, }, accessToken: session.accessToken, - expires: session.expires + expires: session.expires, } // Pass Session through to the session callback - const sessionPayload = await callbacks.session(defaultSessionPayload, user) + const sessionPayload = await callbacks.session( + defaultSessionPayload, + user + ) // Return session payload as response response = sessionPayload // Set cookie again to update expiry - cookie.set(res, cookies.sessionToken.name, sessionToken, { expires: session.expires, ...cookies.sessionToken.options }) + cookie.set(res, cookies.sessionToken.name, sessionToken, { + expires: session.expires, + ...cookies.sessionToken.options, + }) await dispatchEvent(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. - cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 }) + cookie.set(res, cookies.sessionToken.name, "", { + ...cookies.sessionToken.options, + maxAge: 0, + }) } } catch (error) { - logger.error('SESSION_ERROR', error) + logger.error("SESSION_ERROR", error) } } diff --git a/src/server/routes/signin.js b/src/server/routes/signin.js index 5d09e49d7e..93935c5600 100644 --- a/src/server/routes/signin.js +++ b/src/server/routes/signin.js @@ -1,35 +1,43 @@ -import getAuthorizationUrl from '../lib/signin/oauth' -import emailSignin from '../lib/signin/email' -import logger from '../../lib/logger' +import getAuthorizationUrl from "../lib/signin/oauth" +import emailSignin from "../lib/signin/email" +import adapterErrorHandler from "../../adapters/error-handler" -/** Handle requests to /api/auth/signin */ -export default async function signin (req, res) { +/** + * Handle requests to /api/auth/signin + * @param {import("types/internals").NextAuthRequest} req + * @param {import("types/internals").NextAuthResponse} res + */ +export default async function signin(req, res) { const { provider, baseUrl, basePath, adapter, - callbacks + callbacks, + logger, } = req.options if (!provider.type) { return res.status(500).end(`Error: Type not specified for ${provider.name}`) } - if (provider.type === 'oauth' && req.method === 'POST') { + if (provider.type === "oauth" && req.method === "POST") { try { const authorizationUrl = await getAuthorizationUrl(req) return res.redirect(authorizationUrl) } catch (error) { - logger.error('SIGNIN_OAUTH_ERROR', error) + logger.error("SIGNIN_OAUTH_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`) } - } else if (provider.type === 'email' && req.method === 'POST') { + } else if (provider.type === "email" && req.method === "POST") { if (!adapter) { - logger.error('EMAIL_REQUIRES_ADAPTER_ERROR') + logger.error("EMAIL_REQUIRES_ADAPTER_ERROR") return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`) } - const { getUserByEmail } = await adapter.getAdapter(req.options) + const { getUserByEmail } = adapterErrorHandler( + await adapter.getAdapter(req.options), + logger + ) // Note: Technically the part of the email address local mailbox element // (everything before the @ symbol) should be treated as 'case sensitive' @@ -39,36 +47,43 @@ export default async function signin (req, res) { const email = req.body.email?.toLowerCase() ?? null // If is an existing user return a user object (otherwise use placeholder) - const profile = await getUserByEmail(email) || { email } - const account = { id: provider.id, type: 'email', providerAccountId: email } + const profile = (await getUserByEmail(email)) || { email } + const account = { id: provider.id, type: "email", providerAccountId: email } // Check if user is allowed to sign in try { - const signInCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true }) + const signInCallbackResponse = await callbacks.signIn(profile, account, { + email, + verificationRequest: true, + }) if (signInCallbackResponse === false) { return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`) - } else if (typeof signInCallbackResponse === 'string') { + } else if (typeof signInCallbackResponse === "string") { return res.redirect(signInCallbackResponse) } } catch (error) { if (error instanceof Error) { - return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`) + return res.redirect( + `${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}` + ) } // TODO: Remove in a future major release - logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT') + logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT") return res.redirect(error) } try { await emailSignin(email, provider, req.options) } catch (error) { - logger.error('SIGNIN_EMAIL_ERROR', error) + logger.error("SIGNIN_EMAIL_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`) } - return res.redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent( - provider.id - )}&type=${encodeURIComponent(provider.type)}`) + return res.redirect( + `${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent( + provider.id + )}&type=${encodeURIComponent(provider.type)}` + ) } return res.redirect(`${baseUrl}${basePath}/signin`) } diff --git a/src/server/routes/signout.js b/src/server/routes/signout.js index 175cbfb238..8e5e9ee336 100644 --- a/src/server/routes/signout.js +++ b/src/server/routes/signout.js @@ -1,10 +1,14 @@ -import * as cookie from '../lib/cookie' -import logger from '../../lib/logger' -import dispatchEvent from '../lib/dispatch-event' +import * as cookie from "../lib/cookie" +import dispatchEvent from "../lib/dispatch-event" +import adapterErrorHandler from "../../adapters/error-handler" -/** Handle requests to /api/auth/signout */ -export default async function signout (req, res) { - const { adapter, cookies, events, jwt, callbackUrl } = req.options +/** + * Handle requests to /api/auth/signout + * @param {import("types/internals").NextAuthRequest} req + * @param {import("types/internals").NextAuthResponse} res + */ +export default async function signout(req, res) { + const { adapter, cookies, events, jwt, callbackUrl, logger } = req.options const useJwtSession = req.options.session.jwt const sessionToken = req.cookies[cookies.sessionToken.name] @@ -18,7 +22,10 @@ export default async function signout (req, res) { } } else { // Get session from database - const { getSession, deleteSession } = await adapter.getAdapter(req.options) + const { getSession, deleteSession } = adapterErrorHandler( + await adapter.getAdapter(req.options), + logger + ) try { // Dispatch signout event @@ -33,14 +40,14 @@ export default async function signout (req, res) { await deleteSession(sessionToken) } catch (error) { // If error, log it but continue - logger.error('SIGNOUT_ERROR', error) + logger.error("SIGNOUT_ERROR", error) } } // Remove Session Token - cookie.set(res, cookies.sessionToken.name, '', { + cookie.set(res, cookies.sessionToken.name, "", { ...cookies.sessionToken.options, - maxAge: 0 + maxAge: 0, }) return res.redirect(callbackUrl) diff --git a/types/adapters.d.ts b/types/adapters.d.ts index 6ee248050d..dac42dd1c1 100644 --- a/types/adapters.d.ts +++ b/types/adapters.d.ts @@ -22,6 +22,8 @@ export default Adapters * [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter) */ export interface AdapterInstance { + /** Used as a prefix for adapter related log messages. (Defaults to `ADAPTER_`) */ + displayName?: string createUser(profile: P): Promise getUser(id: string): Promise getUserByEmail(email: string): Promise